diff --git a/build.gradle.kts b/build.gradle.kts
index 426da767..47ca1fb2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -11,7 +11,7 @@ repositories {
allprojects {
group = "pl.spcode.navauth"
- version = "0.1.5-SNAPSHOT"
+ version = "0.1.6-SNAPSHOT"
}
tasks.register("formatAll") {
diff --git a/docs/docs/configuration/multification.md b/docs/docs/configuration/multification.md
index cd6b1b58..3651fc0d 100644
--- a/docs/docs/configuration/multification.md
+++ b/docs/docs/configuration/multification.md
@@ -5,6 +5,23 @@ Multification library was created by [**EternalCode**](https://github.com/Eterna
> Multification makes it simple to create customizable notifications and messages within large plugins that require handling a high volume of messages
+## With Multification, you have granular control over the notifications and messages sent to players.
+Here's a quick example of how easy it is:
+
+Here's the edited property from example above (messages.yml):
+```yml
+loginPasswordOnlyInstruction:
+ chat: <#2784F5>ᴇᴋɪᴘᴀ.ɢɢ#2784F5> × Zaloguj się za pomocą
+ /login
+ title: <#2784F5>ᴇᴋɪᴘᴀ.ɢɢ#2784F5>
+ subtitle: Zaloguj się
+ bossbar:
+ message: <#2784F5>ᴇᴋɪᴘᴀ.ɢɢ#2784F5> × Oczekiwanie na logowanie...
+ duration: 3s
+ color: BLUE
+ progress: '1.0'
+```
+
### 📝 Example usage (copy of original lib docs)
::: tip
diff --git a/docs/docs/public/multification/multification-login-example.png b/docs/docs/public/multification/multification-login-example.png
new file mode 100644
index 00000000..63a51180
Binary files /dev/null and b/docs/docs/public/multification/multification-login-example.png differ
diff --git a/navauth-common/src/main/java/pl/spcode/navauth/common/infra/MethodInvocation.java b/navauth-common/src/main/java/pl/spcode/navauth/common/infra/MethodInvocation.java
index 15477e62..7548e744 100644
--- a/navauth-common/src/main/java/pl/spcode/navauth/common/infra/MethodInvocation.java
+++ b/navauth-common/src/main/java/pl/spcode/navauth/common/infra/MethodInvocation.java
@@ -22,8 +22,13 @@
import java.lang.invoke.MethodHandles;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
class MethodInvocation implements Invocation, Comparable {
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodInvocation.class);
+
private final MethodHandle methodHandle;
private final WeakReference targetRef;
private final int priority;
@@ -41,7 +46,7 @@ public void invoke(Object event) {
try {
methodHandle.invoke(target, event);
} catch (Throwable t) {
- t.printStackTrace(); // Production: Use logger/SLF4J
+ logger.error("Error occurred while trying to invoke MethodInvocation", t);
}
}
@@ -52,6 +57,6 @@ public Object getTarget() {
@Override
public int compareTo(MethodInvocation o) {
- return Integer.compare(o.priority, priority); // Desc: higher first
+ return Integer.compare(o.priority, priority);
}
}
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserActivitySessionService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserActivitySessionService.kt
index f241520f..e00541f5 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserActivitySessionService.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserActivitySessionService.kt
@@ -25,6 +25,7 @@ import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import org.slf4j.Logger
import org.slf4j.LoggerFactory
+import pl.spcode.navauth.common.config.SessionsConfig
import pl.spcode.navauth.common.domain.common.IPAddress
import pl.spcode.navauth.common.domain.player.PlayerAdapter
import pl.spcode.navauth.common.domain.user.User
@@ -36,7 +37,10 @@ import pl.spcode.navauth.common.infra.persistence.Paginator
@Singleton
class UserActivitySessionService
@Inject
-constructor(private val userActivitySessionRepository: UserActivitySessionRepository) {
+constructor(
+ private val userActivitySessionRepository: UserActivitySessionRepository,
+ private val sessionsConfig: SessionsConfig,
+) {
private val logger: Logger = LoggerFactory.getLogger(UserActivitySessionService::class.java)
private val playerJoinedAtMap = ConcurrentHashMap()
@@ -45,18 +49,31 @@ constructor(private val userActivitySessionRepository: UserActivitySessionReposi
private data class PlayerSessionData(val joinedAt: Date, val ip: IPAddress)
fun registerPlayerJoin(playerAdapter: PlayerAdapter) {
+ if (!sessionsConfig.userActivitySessionsEnabled) {
+ return
+ }
playerJoinedAtMap[playerAdapter.getUuid().value] =
PlayerSessionData(Date(), playerAdapter.getIPAddress())
}
fun storePlayerSessionOnLeave(playerAdapter: PlayerAdapter) {
- val data = playerJoinedAtMap.get(playerAdapter.getUuid().value)
+ val data = playerJoinedAtMap[playerAdapter.getUuid().value]
if (data == null) {
- logger.debug("player with UUID {} session was not registered", playerAdapter.getUuid())
+ if (sessionsConfig.userActivitySessionsEnabled) {
+ logger.debug("player with UUID {} session was not registered", playerAdapter.getUuid())
+ }
return
}
val leftAt = Date()
+ if ((leftAt.time - data.joinedAt.time) < sessionsConfig.sessionMinimumTimeMs) {
+ logger.debug(
+ "player with UUID {} session was not registered, session minimum time not fulfilled",
+ playerAdapter.getUuid(),
+ )
+ return
+ }
+
val session =
UserActivitySession.create(
playerAdapter.getUuid(),
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserService.kt
index 136a5c44..2d2c9711 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserService.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserService.kt
@@ -82,13 +82,7 @@ constructor(
userRepository.save(user)
}
- fun deleteUserCredentials(user: User): User {
- return txService.inTransaction {
- return@inTransaction deleteUserCredentialsNoTx(user)
- }
- }
-
- private fun deleteUserCredentialsNoTx(user: User): User {
+ private fun deleteUserCredentialsUpdateUserNoTx(user: User): User {
val user = user.withCredentialsRequired(false)
userRepository.save(user)
userCredentialsService.deleteUserCredentials(user)
@@ -111,12 +105,15 @@ constructor(
val requireCredentials = credentials.isTwoFactorEnabled
val premiumUser = User.premium(user.uuid, user.username, mojangId, requireCredentials)
- userRepository.save(premiumUser)
+ val status = userRepository.save(premiumUser)
+ status.isCreated
+ status.isUpdated
+ status.numLinesChanged
if (requireCredentials) {
val newCredentials = credentials.withoutPassword()
userCredentialsService.storeUserCredentials(premiumUser, newCredentials)
} else {
- deleteUserCredentialsNoTx(user)
+ userCredentialsService.deleteUserCredentials(premiumUser)
}
return@inTransaction premiumUser
@@ -257,7 +254,7 @@ constructor(
val newCredentials = credentials.withoutTotpSecret()
userCredentialsService.storeUserCredentials(user, newCredentials)
} else {
- deleteUserCredentialsNoTx(user)
+ deleteUserCredentialsUpdateUserNoTx(user)
}
}
}
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/GeneralConfig.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/GeneralConfig.kt
index 567112c5..a9c70b87 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/GeneralConfig.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/GeneralConfig.kt
@@ -52,8 +52,17 @@ open class GeneralConfig : OkaeriConfig() {
@Comment("Maximum time of registration.")
var maxRegistrationDuration: Duration = Duration.ofSeconds(30)
+ @Comment("Interval between each register instruction multification.")
+ var intervalBetweenRegisterMultification: Duration = Duration.ofSeconds(3)
+
@Comment("Maximum time of login.") var maxLoginDuration: Duration = Duration.ofSeconds(20)
+ @Comment("Interval between each login instruction multification.")
+ var intervalBetweenLoginMultification: Duration = Duration.ofSeconds(3)
+
+ @Comment("Delay before sending multification on a successful premium authentication.")
+ var premiumAuthMultificationDelay: Duration = Duration.ofSeconds(1)
+
@Comment("Usernames config") var usernamesConfig: UsernamesConfig = UsernamesConfig()
@Comment("Two-factor (2FA) TOTP authentication config")
@@ -61,6 +70,24 @@ open class GeneralConfig : OkaeriConfig() {
@Comment("Passwords config") var passwordsConfig: PasswordsConfig = PasswordsConfig()
+ @Comment(
+ "When a non-premium user tries to join with a premium nickname,",
+ "an 'invalid session' error occurs. This is default Minecraft behavior.",
+ "Should we cache premium connections and send a more descriptive message",
+ "when they try to join again after getting disconnected right after the preLogin event?",
+ "",
+ "We cache player uuid, IP and protocol version.",
+ )
+ var descriptiveInvalidSessionEnabled = true
+
+ @Comment(
+ "Max time (milliseconds) to consider a reconnect as an invalid session connection.",
+ "(You can set this a little higher than the reconnect throttle from other plugins.)",
+ )
+ var descriptiveInvalidSessionCacheTimeMs = 8000
+
+ var sessionsConfig = SessionsConfig()
+
@Variable("CONFIG_VERSION")
@Comment("Config version. DO NOT CHANGE this property!")
var configVersion: Int = 0
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt
index 14f8175e..ea59cda2 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt
@@ -70,6 +70,14 @@ open class MessagesConfig : OkaeriConfig() {
var yourAccountDataHasBeenMigrated =
TextComponent("Your account data has been migrated to '%USERNAME%'.")
+ val possibleInvalidSessionReconnectError =
+ TextComponent(
+ "You've tried to log in to a premium account '%USERNAME%' one or more times. " +
+ " This might be an invalid session error, caused by one of the following:" +
+ " 1. An outdated session. If you own the account, restart your client." +
+ " 2. You don't own the premium account. In that case, use a non-premium username."
+ )
+
@Comment(
"Notifications which use multification library.",
"Here you can use chat messages, action bars, sounds etc. combined.",
@@ -191,6 +199,11 @@ open class MessagesConfig : OkaeriConfig() {
)
var adminCmdUserPremiumMigrationSuccess: Notice =
Notice.chat("User '%USERNAME%' successfully migrated to premium mode.")
+
+ var adminCmdReloadingConfig: Notice = Notice.chat("Reloading configuration...")
+ var adminCmdConfigReloadSuccess: Notice = Notice.chat("Config %NAME% has been reloaded.")
+ var adminCmdConfigReloadError: Notice =
+ Notice.chat("Failed to reload %NAME% configuration! Error: %CAUSE%")
}
@Variable("CONFIG_VERSION")
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/SessionsConfig.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/SessionsConfig.kt
new file mode 100644
index 00000000..370c6ef7
--- /dev/null
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/SessionsConfig.kt
@@ -0,0 +1,36 @@
+/*
+ * NavAuth
+ * Copyright © 2026 Oliwier Fijas (Navio1430)
+ *
+ * NavAuth is free software; You can redistribute it and/or modify it under the terms of:
+ * the GNU Affero General Public License version 3 as published by the Free Software Foundation.
+ *
+ * NavAuth is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with NavAuth. If not, see
+ * and navigate to version 3 of the GNU Affero General Public License.
+ *
+ */
+
+package pl.spcode.navauth.common.config
+
+import eu.okaeri.configs.OkaeriConfig
+import eu.okaeri.configs.annotation.Comment
+
+class SessionsConfig : OkaeriConfig() {
+
+ @Comment(
+ "Should we store player sessions?",
+ "https://navio1430.github.io/NavAuth/docs/general/user-lookup.html#lookup-user-sessions",
+ "Make sure you've included information about this function in your Privacy Policy.",
+ "We store join at, left at times and IP's.",
+ )
+ var userActivitySessionsEnabled = false
+
+ @Comment("Minimum time of milliseconds required to save a player activity session.")
+ var sessionMinimumTimeMs = 30000
+}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/InvalidSessionCacheService.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/InvalidSessionCacheService.kt
new file mode 100644
index 00000000..43b07348
--- /dev/null
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/InvalidSessionCacheService.kt
@@ -0,0 +1,79 @@
+/*
+ * NavAuth
+ * Copyright © 2026 Oliwier Fijas (Navio1430)
+ *
+ * NavAuth is free software; You can redistribute it and/or modify it under the terms of:
+ * the GNU Affero General Public License version 3 as published by the Free Software Foundation.
+ *
+ * NavAuth is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with NavAuth. If not, see
+ * and navigate to version 3 of the GNU Affero General Public License.
+ *
+ */
+
+package pl.spcode.navauth.velocity.application.auth
+
+import com.google.common.cache.CacheBuilder
+import com.google.inject.Inject
+import com.google.inject.Singleton
+import com.velocitypowered.api.proxy.InboundConnection
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.toJavaDuration
+import pl.spcode.navauth.common.config.GeneralConfig
+import pl.spcode.navauth.common.domain.common.IPAddress
+
+/**
+ * Service used to make the invalid session error more descriptive, which is a significant UX
+ * improvement.
+ */
+@Singleton
+class InvalidSessionCacheService @Inject constructor(val generalConfig: GeneralConfig) {
+
+ private val cache =
+ CacheBuilder.newBuilder()
+ .expireAfterWrite(
+ generalConfig.descriptiveInvalidSessionCacheTimeMs.milliseconds.toJavaDuration()
+ )
+ .build()
+
+ fun cachePremiumConnection(username: String, connection: InboundConnection) {
+ val ip = IPAddress.fromInetAddress(connection.remoteAddress.address)
+ val obj = ConnectionCache(username, ip, connection.protocolVersion.protocol)
+ cache.put(username, obj)
+ }
+
+ fun markAsEncryptedPremiumConnection(username: String) {
+ cache.getIfPresent(username)?.passedEncryption = true
+ }
+
+ fun isInvalidSessionReconnect(username: String, connection: InboundConnection): Boolean {
+ val cached = cache.getIfPresent(username) ?: return false
+ if (!cached.passedEncryption) {
+ val ip = IPAddress.fromInetAddress(connection.remoteAddress.address)
+ if (
+ cached.ip.compareTo(ip) == 0 &&
+ cached.protocolVersion == connection.protocolVersion.protocol
+ ) {
+ return true
+ }
+ }
+ return false
+ }
+
+ fun invalidate(username: String) {
+ cache.invalidate(username)
+ }
+
+ /** @param passedEncryption DO NOT USE THIS FOR SECURITY!!! */
+ data class ConnectionCache(
+ val username: String,
+ val ip: IPAddress,
+ val protocolVersion: Int,
+ var passedEncryption: Boolean = false,
+ )
+}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/session/VelocityAuthSessionFactory.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/session/VelocityAuthSessionFactory.kt
index f6a77417..18f0cd5e 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/session/VelocityAuthSessionFactory.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/session/VelocityAuthSessionFactory.kt
@@ -99,7 +99,14 @@ constructor(
uniqueSessionId: VelocityUniqueSessionId,
): VelocityAutoLoginAuthSession {
val session =
- VelocityAutoLoginAuthSession(player, scheduler, multification, messagesConfig, eventBus)
+ VelocityAutoLoginAuthSession(
+ player,
+ scheduler,
+ multification,
+ messagesConfig,
+ generalConfig,
+ eventBus,
+ )
return authSessionService.registerSession(uniqueSessionId, session)
}
}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/CommandsRegistry.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/CommandsRegistry.kt
index 7643784b..875b558f 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/CommandsRegistry.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/CommandsRegistry.kt
@@ -24,6 +24,7 @@ import pl.spcode.navauth.velocity.command.admin.ForceCrackedAdminCommand
import pl.spcode.navauth.velocity.command.admin.ForcePremiumAdminCommand
import pl.spcode.navauth.velocity.command.admin.MigrateUserDataAdminCommand
import pl.spcode.navauth.velocity.command.admin.PlayerLookupAdminCommand
+import pl.spcode.navauth.velocity.command.admin.ReloadConfigAdminCommand
import pl.spcode.navauth.velocity.command.root.MigrationRootCommand
import pl.spcode.navauth.velocity.command.user.ChangePasswordCommand
import pl.spcode.navauth.velocity.command.user.LoginCommand
@@ -50,6 +51,7 @@ class CommandsRegistry {
ForcePremiumAdminCommand::class,
MigrateUserDataAdminCommand::class,
PlayerLookupAdminCommand::class,
+ ReloadConfigAdminCommand::class,
// root (console only)
MigrationRootCommand::class,
)
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/Permissions.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/Permissions.kt
index c688a716..b81fcd62 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/Permissions.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/Permissions.kt
@@ -40,5 +40,6 @@ class Permissions {
const val ADMIN_FORCE_CRACKED = "$ADMIN_BASE.forcecracked"
const val ADMIN_FORCE_PREMIUM = "$ADMIN_BASE.forcepremium"
const val ADMIN_MIGRATE_USER_DATA = "$ADMIN_BASE.migrateuserdata"
+ const val ADMIN_RELOAD = "$ADMIN_BASE.reload"
}
}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceCrackedAdminCommand.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceCrackedAdminCommand.kt
index 369c0849..fc43b522 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceCrackedAdminCommand.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceCrackedAdminCommand.kt
@@ -20,7 +20,6 @@ package pl.spcode.navauth.velocity.command.admin
import com.google.inject.Inject
import com.velocitypowered.api.command.CommandSource
-import com.velocitypowered.api.proxy.ConsoleCommandSource
import dev.rollczi.litecommands.annotations.argument.Arg
import dev.rollczi.litecommands.annotations.async.Async
import dev.rollczi.litecommands.annotations.command.Command
@@ -28,7 +27,6 @@ import dev.rollczi.litecommands.annotations.context.Context
import dev.rollczi.litecommands.annotations.execute.Execute
import dev.rollczi.litecommands.annotations.permission.Permission
import java.util.Optional
-import me.uniodex.velocityrcon.commandsource.IRconCommandSource
import pl.spcode.navauth.common.annotation.Description
import pl.spcode.navauth.common.application.credentials.UserCredentialsService
import pl.spcode.navauth.common.application.user.UserService
@@ -38,6 +36,7 @@ import pl.spcode.navauth.common.extension.StringExtensions.Companion.applyPlaceh
import pl.spcode.navauth.common.shared.utils.StringUtils.Companion.generateRandomString
import pl.spcode.navauth.velocity.command.Permissions
import pl.spcode.navauth.velocity.multification.VelocityMultification
+import pl.spcode.navauth.velocity.util.CommandSourceUtils
@Command(name = "forcecracked")
@Permission(Permissions.ADMIN_FORCE_CRACKED)
@@ -77,7 +76,7 @@ constructor(
userService.migrateToNonPremium(user, hashedPassword)
val passwordText =
- if (sender is ConsoleCommandSource || sender is IRconCommandSource) {
+ if (CommandSourceUtils.isConsoleOrRcon(sender)) {
"$newPassword"
} else {
val placeholders = mapOf(Pair("PASSWORD", newPassword))
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ReloadConfigAdminCommand.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ReloadConfigAdminCommand.kt
new file mode 100644
index 00000000..5e5b1a12
--- /dev/null
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ReloadConfigAdminCommand.kt
@@ -0,0 +1,57 @@
+/*
+ * NavAuth
+ * Copyright © 2026 Oliwier Fijas (Navio1430)
+ *
+ * NavAuth is free software; You can redistribute it and/or modify it under the terms of:
+ * the GNU Affero General Public License version 3 as published by the Free Software Foundation.
+ *
+ * NavAuth is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with NavAuth. If not, see
+ * and navigate to version 3 of the GNU Affero General Public License.
+ *
+ */
+
+package pl.spcode.navauth.velocity.command.admin
+
+import com.google.inject.Inject
+import com.velocitypowered.api.command.CommandSource
+import dev.rollczi.litecommands.annotations.command.Command
+import dev.rollczi.litecommands.annotations.context.Context
+import dev.rollczi.litecommands.annotations.execute.Execute
+import dev.rollczi.litecommands.annotations.permission.Permission
+import pl.spcode.navauth.common.config.MessagesConfig
+import pl.spcode.navauth.velocity.command.Permissions
+import pl.spcode.navauth.velocity.multification.VelocityMultification
+
+@Command(name = "navauth")
+class ReloadConfigAdminCommand
+@Inject
+constructor(val config: MessagesConfig, val multification: VelocityMultification) {
+
+ @Execute(name = "reload")
+ @Permission(Permissions.ADMIN_RELOAD)
+ fun reload(@Context sender: CommandSource) {
+ multification
+ .create(sender) { it.multification.adminCmdReloadingConfig }
+ .placeholder("%NAME%", "messages")
+ .send()
+ try {
+ config.load()
+ } catch (e: Exception) {
+ multification
+ .create(sender) { it.multification.adminCmdConfigReloadError }
+ .placeholder("%NAME%", "messages")
+ .placeholder("%CAUSE%", e.message)
+ .send()
+ }
+ multification
+ .create(sender) { it.multification.adminCmdConfigReloadSuccess }
+ .placeholder("%NAME%", "messages")
+ .send()
+ }
+}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/root/MigrationRootCommand.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/root/MigrationRootCommand.kt
index 9c5b60e7..5ddea6ad 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/root/MigrationRootCommand.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/root/MigrationRootCommand.kt
@@ -20,14 +20,13 @@ package pl.spcode.navauth.velocity.command.root
import com.google.inject.Inject
import com.velocitypowered.api.command.CommandSource
-import com.velocitypowered.api.proxy.ConsoleCommandSource
import dev.rollczi.litecommands.annotations.command.Command
import dev.rollczi.litecommands.annotations.context.Context
import dev.rollczi.litecommands.annotations.execute.Execute
import dev.rollczi.litecommands.annotations.permission.Permission
-import me.uniodex.velocityrcon.commandsource.IRconCommandSource
import pl.spcode.navauth.common.annotation.Description
import pl.spcode.navauth.common.migrate.MigrationManager
+import pl.spcode.navauth.velocity.util.CommandSourceUtils
@Command(name = "migration")
@Permission("navauth.root")
@@ -41,7 +40,7 @@ class MigrationRootCommand @Inject constructor(private val migrationManager: Mig
)
fun startMigration(@Context sender: CommandSource) {
- if (sender !is ConsoleCommandSource && sender !is IRconCommandSource) {
+ if (!CommandSourceUtils.isConsoleOrRcon(sender)) {
sender.sendPlainMessage("This command can only be executed from console.")
return
}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityAutoLoginAuthSession.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityAutoLoginAuthSession.kt
index b7d55ae5..8f63faf8 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityAutoLoginAuthSession.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityAutoLoginAuthSession.kt
@@ -19,9 +19,9 @@
package pl.spcode.navauth.velocity.infra.auth
import com.velocitypowered.api.proxy.Player
-import java.time.Duration
import pl.spcode.navauth.api.domain.auth.AuthSessionType
import pl.spcode.navauth.api.event.NavAuthEventBus
+import pl.spcode.navauth.common.config.GeneralConfig
import pl.spcode.navauth.common.config.MessagesConfig
import pl.spcode.navauth.common.domain.auth.session.AuthSession
import pl.spcode.navauth.velocity.infra.player.VelocityPlayerAdapter
@@ -33,6 +33,7 @@ class VelocityAutoLoginAuthSession(
val scheduler: NavAuthScheduler,
val multification: VelocityMultification,
val messagesConfig: MessagesConfig,
+ val generalConfig: GeneralConfig,
eventBus: NavAuthEventBus,
) : AuthSession(VelocityPlayerAdapter(player), eventBus) {
@@ -55,7 +56,7 @@ class VelocityAutoLoginAuthSession(
}
}
)
- .delay(Duration.ofSeconds(1))
+ .delay(generalConfig.premiumAuthMultificationDelay)
.schedule()
}
}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityLoginAuthSession.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityLoginAuthSession.kt
index d444e3bb..5a0893f7 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityLoginAuthSession.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityLoginAuthSession.kt
@@ -81,7 +81,7 @@ class VelocityLoginAuthSession(
Runnable { multification.create().notice(notification).player(player.uniqueId).send() }
)
.delay(Duration.ofSeconds(1))
- .repeat(Duration.ofSeconds(3))
+ .repeat(generalConfig.intervalBetweenLoginMultification)
.schedule()
}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityRegisterAuthSession.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityRegisterAuthSession.kt
index 732795c7..73f77bb0 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityRegisterAuthSession.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityRegisterAuthSession.kt
@@ -67,7 +67,7 @@ class VelocityRegisterAuthSession(
}
)
.delay(Duration.ofSeconds(1))
- .repeat(Duration.ofSeconds(3))
+ .repeat(generalConfig.intervalBetweenRegisterMultification)
.schedule()
}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt
index 92093fc1..c881c490 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt
@@ -36,6 +36,8 @@ import pl.spcode.navauth.common.application.auth.username.UsernameResolutionServ
import pl.spcode.navauth.common.application.user.UserService
import pl.spcode.navauth.common.application.validator.UsernameValidator
import pl.spcode.navauth.common.component.TextColors
+import pl.spcode.navauth.common.component.TextComponent
+import pl.spcode.navauth.common.config.GeneralConfig
import pl.spcode.navauth.common.config.MessagesConfig
import pl.spcode.navauth.common.domain.auth.handshake.AuthHandshakeSession
import pl.spcode.navauth.common.domain.auth.handshake.EncryptionType
@@ -44,6 +46,7 @@ import pl.spcode.navauth.common.domain.user.MojangId
import pl.spcode.navauth.common.domain.user.User
import pl.spcode.navauth.common.domain.user.UserUuid
import pl.spcode.navauth.common.domain.user.Username
+import pl.spcode.navauth.velocity.application.auth.InvalidSessionCacheService
import pl.spcode.navauth.velocity.application.auth.session.VelocityAuthSessionFactory
import pl.spcode.navauth.velocity.extension.PlayerDisconnectExtension.Companion.disconnectIfActive
import pl.spcode.navauth.velocity.infra.auth.VelocityUniqueSessionId
@@ -55,8 +58,10 @@ constructor(
val usernameValidator: UsernameValidator,
val authHandshakeSessionService: AuthHandshakeSessionService,
val usernameResolutionService: UsernameResolutionService,
+ val invalidSessionCacheService: InvalidSessionCacheService,
val authSessionFactory: VelocityAuthSessionFactory,
val messagesConfig: MessagesConfig,
+ val generalConfig: GeneralConfig,
) {
val logger: Logger = LoggerFactory.getLogger(LoginListeners::class.java)
@@ -81,11 +86,21 @@ constructor(
is UsernameResResult.Success -> {
when (res.requestedEncryption) {
EncryptionType.ENFORCE_PREMIUM -> {
+ if (generalConfig.descriptiveInvalidSessionEnabled) {
+ if (
+ invalidSessionCacheService.isInvalidSessionReconnect(
+ event.username,
+ event.connection,
+ )
+ ) {
+ invalidSessionCacheService.invalidate(event.username)
+ event.result = invalidSessionReconnectDeniedResult(event.username)
+ return
+ }
+ invalidSessionCacheService.cachePremiumConnection(event.username, event.connection)
+ }
// We force velocity to handle the initiation of the "minecraft encryption protocol".
// User won't go any further than this event if not authenticated by velocity.
- // todo set session cookie token ->
- // if the same player disconnects twice at the same handshake stage
- // then display "You're trying to login into a premium account..."
event.result = PreLoginEvent.PreLoginComponentResult.forceOnlineMode()
}
EncryptionType.NONE ->
@@ -145,6 +160,10 @@ constructor(
return
}
+ if (generalConfig.descriptiveInvalidSessionEnabled && player.isOnlineMode) {
+ invalidSessionCacheService.markAsEncryptedPremiumConnection(player.username)
+ }
+
createAuthSession(player, handshakeSession, username)
authHandshakeSessionService.closeSession(sessionId)
}
@@ -216,6 +235,7 @@ constructor(
player.disconnectIfActive(Component.text("NavAuth: Bad auth state", TextColors.RED))
}
+ // todo move this out of here
private fun createAndStorePremiumUser(player: Player) {
val premiumUser =
User.premium(UserUuid(player.uniqueId), Username(player.username), MojangId(player.uniqueId))
@@ -276,6 +296,19 @@ constructor(
return PreLoginEvent.PreLoginComponentResult.denied(comp)
}
+ private fun invalidSessionReconnectDeniedResult(
+ username: String
+ ): PreLoginEvent.PreLoginComponentResult {
+ val comp =
+ withSupportFooter(
+ componentWithUsernamePlaceholder(
+ messagesConfig.possibleInvalidSessionReconnectError,
+ username,
+ )
+ )
+ return PreLoginEvent.PreLoginComponentResult.denied(comp)
+ }
+
private fun componentWithUsernamePlaceholder(
textComponent: pl.spcode.navauth.common.component.TextComponent,
username: String,
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/util/CommandSourceUtils.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/util/CommandSourceUtils.kt
new file mode 100644
index 00000000..3d516c84
--- /dev/null
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/util/CommandSourceUtils.kt
@@ -0,0 +1,35 @@
+/*
+ * NavAuth
+ * Copyright © 2026 Oliwier Fijas (Navio1430)
+ *
+ * NavAuth is free software; You can redistribute it and/or modify it under the terms of:
+ * the GNU Affero General Public License version 3 as published by the Free Software Foundation.
+ *
+ * NavAuth is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with NavAuth. If not, see
+ * and navigate to version 3 of the GNU Affero General Public License.
+ *
+ */
+
+package pl.spcode.navauth.velocity.util
+
+import com.velocitypowered.api.proxy.ConsoleCommandSource
+import me.uniodex.velocityrcon.commandsource.IRconCommandSource
+
+object CommandSourceUtils {
+ fun isConsoleOrRcon(sender: Any): Boolean {
+ if (sender is ConsoleCommandSource) return true
+ return try {
+ sender is IRconCommandSource
+ } catch (_: ClassNotFoundException) {
+ false
+ } catch (_: NoClassDefFoundError) {
+ false
+ }
+ }
+}