diff --git a/.gitignore b/.gitignore
index a51be904..97510a7f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ build/
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
+.idea/AndroidProjectSystem.xml
*.iws
*.iml
*.ipr
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 8ad8c861..cc87bc51 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,7 @@
-
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49eb1daa..8b0779f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,51 @@
+## 0.3.0
+
+### Changes
+```diff
++ Update to Java 25
++ Update to Velocity 4.1.2
+
++ Added commands configuration: [name, aliases, enabled] per command
+
++ Mojang profile lookup now also caches non-premium (not found) usernames, so repeated login
+ attempts against nicknames that have no Mojang account no longer hit the profile API every time
++ Only username resolution during login/register reads this not-found cache (avoids rate limits /
+ latency on login spam and brute-force attempts)
++ Authoritative lookups (e.g. /premium, admin commands, username migration) keep using the live
+ API and ignore the not-found cache, so they always reflect the current Mojang state
++ After successful login/register, the player is now sent directly to the initial
+ server via VelocityServerConnectService instead of firing a faked PlayerChooseInitialServerEvent
++ Priority of PlayerChooseInitialServerEvent and ServerPreConnectEvent listeners set to always
+ have the final say (MIN)
+- No more fake PlayerChooseInitialServerEvent dispatch
+```
+
+### Tradeoffs
+- A not-found cache entry is stored for the configured `profileCacheTTL`.
+ If a player becomes premium (buys the game / renames to that nickname) within that window,
+ username resolution may briefly still treat the name as non-premium until the cache expires.
+
+### Fixes
+- fix unexpectedErrorOccurred message: changed val to var (this was causing warning messages and no changes after /reload)
+- /changepassword now notifies premium accounts that they can't change their password
+
+### Config
+**Messages** config:
+```diff
++ profileApiFailureKickMessage
++ accountNotNonPremiumError
+```
+
+**General** config:
+```diff
++ commandsConfig
+```
+
+### New contributors
+WejsoneKK - implemented command aliases - Thanks!
+
+
## 0.2.0
### Changes
diff --git a/README.md b/README.md
index 849e5af6..abaa7aa6 100644
--- a/README.md
+++ b/README.md
@@ -9,12 +9,12 @@ We are working on additional configuration options and other improvements to pre
NavAuth is a next-generation Minecraft login plugin built for speed , security , and seamless player authentication. Designed with modern servers in mind, it combines performance , reliability , and integration flexibility .
[](https://modrinth.com/plugin/navauth)
- [](https://modrinth.com/plugin/navauth)
[](https://github.com/Navio1430/NavAuth)
+[//]: # ( [](https://modrinth.com/plugin/navauth))
+
[](https://gradle.org/)
[](https://kotlinlang.org/docs/home.html)
- 
[](https://navio1430.github.io/NavAuth/docs)
[](https://navio1430.github.io/NavAuth/javadoc)
@@ -60,6 +60,7 @@ You can also use other plugins (like Anti-VPN's) for that.
Thanks to **NavAuth contributors**:
- urhatedjack - logo design
- [KotreQ](https://github.com/KotreQ) - help with the QR code generation
+- [WejsoneKK](https://github.com/WejsoneKK) - commands configuration feature
Thanks to **Discord support team**:
- [Blavez](https://github.com/Blavezz)
@@ -79,10 +80,11 @@ Thanks to people that kept **LibreLoginProd alive** until NavAuth release and ke
### 🧩 Requirements
-* Java 21
+* Java 25
+* Velocity 4.1.2+
* Limbo server e.g., NanoLimbo, PicoLimbo
* Velocity forwarding set to MODERN
-* All backend servers must be 1.13+
+* All backend servers must be 1.13+ (because of modern forward)
### FAQ
Q: Why have I switched from maintaining LibreLoginProd?
@@ -99,12 +101,4 @@ NavAuth is licensed under the GNU AGPL v3. See the license file for more informa
[](https://www.gnu.org/licenses/agpl-3.0.en.html)
## 💡 TODO List
-More planned features are described in [Documentation](https://navio1430.github.io/NavAuth/docs/offer.html#%F0%9F%9A%80-planned-features)
-- readme:
- - add banner
-- github/gh actions:
- - modrinth CD
- - issue template:
- - bug report
- - feature request
-- commands and messages localization
+Planned features are described in [Documentation](https://navio1430.github.io/NavAuth/docs/offer.html#%F0%9F%9A%80-planned-features)
diff --git a/build.gradle.kts b/build.gradle.kts
index 8d479c78..6adc675c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,7 @@
plugins {
id("java")
- kotlin("jvm") version "2.2.21"
+ kotlin("jvm") version "2.4.10"
alias(libs.plugins.spotless)
}
@@ -11,7 +11,7 @@ repositories {
allprojects {
group = "pl.spcode.navauth"
- version = "0.2.0-SNAPSHOT"
+ version = "0.3.0-SNAPSHOT"
}
tasks.register("formatAll") {
@@ -43,12 +43,15 @@ subprojects {
}
kotlin {
- jvmToolchain(21)
+ jvmToolchain(25)
}
java {
- sourceCompatibility = JavaVersion.VERSION_21
- targetCompatibility = JavaVersion.VERSION_21
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(25)
+ targetCompatibility = JavaVersion.VERSION_25
+ sourceCompatibility = JavaVersion.VERSION_25
+ }
}
tasks.test {
@@ -62,6 +65,7 @@ subprojects {
dependsOn("spotlessJavaApply")
dependsOn("spotlessKotlinApply")
+ dependsOn("spotlessCheck")
}
spotless {
diff --git a/docs/docs/offer.md b/docs/docs/offer.md
index cb72053a..54fb1f4f 100644
--- a/docs/docs/offer.md
+++ b/docs/docs/offer.md
@@ -26,20 +26,18 @@ Cracking the whole database of a bad plugin takes less than a **FEW MINUTES**.
| Feature | NavAuth | LibreLoginProd |
|:----------------------------------------------------------------|----------------------------------------|---------------------------------------|
| Supported Platforms | Velocity | Paper, Velocity |
-| Premium auto-login | ✅ | ✅ |
+| Premium auto-login | ✅ | ✅ |
| Security | High | Moderate |
| Performance | Fast | Moderate |
-| Large scale support | ✅ | ❌ |
+| Commands aliases | ✅ | ❌ |
| Codebase quality | Good | Bad |
| Supported Databases | PostgreSQL, MySQL, MariaDB, SQLite, H2 | PostgreSQL, MySQL, MariaDB, SQLite |
-| [2FA](/general/2fa.html) | ✅ | 🟧 (needs additional plugin) |
-| [User sessions](/general/user-lookup.html#lookup-user-sessions) | ✅ | ❌ |
-| Active Maintenance | ✅ | ❌ |
-| Active Support | ✅ | ❌ |
-| Multification | ✅ | ❌ |
-| Plugins migration | ✅ (any db type) | 🟧 (only for specific configurations) |
-| Database type migration | ✅ | 🟧 (partial) |
-| Floodgate | ❌ (planned) | ✅ |
+| [2FA](/general/2fa.html) | ✅ | 🟧 (needs additional plugin) |
+| [User sessions](/general/user-lookup.html#lookup-user-sessions) | ✅ | ❌ |
+| Multification | ✅ | ❌ |
+| Plugins migration | ✅ (any db type) | 🟧 (only for specific configurations) |
+| Database type migration | ✅ | 🟧 (partial) |
+| Floodgate | ❌ (planned) | ✅ |
## 🚀 Planned Features
- ~~support Paper platform~~ based on experience with LibreLoginProd, I've decided not to continue with the idea of paper support
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5a63b546..38303740 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,16 +1,57 @@
[versions]
# libs
-velocity-api = "3.4.0-SNAPSHOT"
+velocity-api = "4.1.2-SNAPSHOT"
+bstats = "3.2.1"
+tribufu-rcon = "1.2.0"
+multification = "1.2.4"
+guice = "7.0.0"
+gson = "2.14.0"
+hikaricp = "7.1.0"
+ormlite = "6.1"
+okaeri-configs = "6.1.0-beta.4"
+bcrypt = "0.10.2"
+bouncycastle = "1.85"
+adventure-minimessage = "5.2.0"
+h2 = "2.4.240"
+mysql = "26.7.0"
+postgresql = "42.7.13"
+sqlite = "3.53.4.0"
+zxing = "3.5.4"
+kotest = "6.2.4"
+mockk = "1.14.11"
+ajqueue-api = "2.9.1"
# plugins
-shadow = "9.2.0"
+shadow = "9.6.1"
blossom = "2.2.0"
-spotless = "8.0.0"
-litecommands = "3.10.6"
+spotless = "8.10.1"
+litecommands = "3.11.0"
[libraries]
velocitypowered-velocity-api = { module = "com.velocitypowered:velocity-api", version.ref = "velocity-api" }
litecommands-core = { module = "dev.rollczi:litecommands-core", version.ref = "litecommands" }
litecommands-velocity = { module = "dev.rollczi:litecommands-velocity", version.ref = "litecommands" }
+bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" }
+tribufu-velocity-rcon = { module = "com.tribufu:Tribufu-VelocityRcon", version.ref = "tribufu-rcon" }
+multification-core = { module = "com.eternalcode:multification-core", version.ref = "multification" }
+multification-okaeri = { module = "com.eternalcode:multification-okaeri", version.ref = "multification" }
+guice = { module = "com.google.inject:guice", version.ref = "guice" }
+gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
+hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikaricp" }
+ormlite-jdbc = { module = "com.j256.ormlite:ormlite-jdbc", version.ref = "ormlite" }
+okaeri-configs-yaml-snakeyaml = { module = "eu.okaeri:okaeri-configs-yaml-snakeyaml", version.ref = "okaeri-configs" }
+bcrypt = { module = "at.favre.lib:bcrypt", version.ref = "bcrypt" }
+bouncycastle-prov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
+bouncycastle-util = { module = "org.bouncycastle:bcutil-jdk18on", version.ref = "bouncycastle" }
+adventure-text-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-minimessage" }
+h2 = { module = "com.h2database:h2", version.ref = "h2" }
+mysql = { module = "com.mysql:mysql-connector-j", version.ref = "mysql" }
+postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
+sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
+zxing-core = { module = "com.google.zxing:core", version.ref = "zxing" }
+kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
+kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
+mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
+ajqueue-api = { module = "us.ajg0702.queue.api:api", version.ref = "ajqueue-api" }
[plugins]
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 1b33c55b..eddabd2e 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ca025c83..ad7845be 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
+retries=0
+retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 23d15a93..249efbb0 100644
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
#!/bin/sh
#
-# Copyright © 2015-2021 the original authors.
+# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
##############################################################################
#
-# Gradle start up script for POSIX generated by Gradle.
+# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
-# ksh Gradle
+# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
diff --git a/gradlew.bat b/gradlew.bat
index db3a6ac2..a51ec4f5 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -19,12 +19,12 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
-@rem Gradle startup script for Windows
+@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
-goto fail
+"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,30 +65,18 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
-goto fail
+"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
-set CLASSPATH=
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+@rem Execute gradlew
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/integration/ajqueue/build.gradle.kts b/integration/ajqueue/build.gradle.kts
index 8b5da775..a122f777 100644
--- a/integration/ajqueue/build.gradle.kts
+++ b/integration/ajqueue/build.gradle.kts
@@ -43,5 +43,5 @@ dependencies {
compileOnly(project(":navauth-common"))
compileOnly(libs.velocitypowered.velocity.api)
kapt(libs.velocitypowered.velocity.api)
- compileOnly("us.ajg0702.queue.api:api:2.9.1")
+ compileOnly(libs.ajqueue.api)
}
diff --git a/integration/ajqueue/src/main/kotlin/pl/spcode/navauth/integration/ajqueue/AjQueueIntegrationPlugin.kt b/integration/ajqueue/src/main/kotlin/pl/spcode/navauth/integration/ajqueue/AjQueueIntegrationPlugin.kt
index 8e28365f..970ea0fa 100644
--- a/integration/ajqueue/src/main/kotlin/pl/spcode/navauth/integration/ajqueue/AjQueueIntegrationPlugin.kt
+++ b/integration/ajqueue/src/main/kotlin/pl/spcode/navauth/integration/ajqueue/AjQueueIntegrationPlugin.kt
@@ -73,7 +73,8 @@ constructor(
"""
|NavAuth version {} is too old. Required minimum version is {}.
|Please update NavAuth to at least version {}.
- |Download the latest version from: https://github.com/Navio1430/NavAuth"""
+ |Download the latest version from: https://github.com/Navio1430/NavAuth
+ """
.trimMargin(),
navAuthVersion,
requiredVersion,
diff --git a/navauth-api-examples/build.gradle.kts b/navauth-api-examples/build.gradle.kts
index afbdb8ea..d84346cd 100644
--- a/navauth-api-examples/build.gradle.kts
+++ b/navauth-api-examples/build.gradle.kts
@@ -14,7 +14,3 @@ dependencies {
compileOnly(libs.velocitypowered.velocity.api)
annotationProcessor(libs.velocitypowered.velocity.api)
}
-
-kotlin {
- jvmToolchain(21)
-}
\ No newline at end of file
diff --git a/navauth-common/build.gradle.kts b/navauth-common/build.gradle.kts
index 57d95ae5..027eb4fe 100644
--- a/navauth-common/build.gradle.kts
+++ b/navauth-common/build.gradle.kts
@@ -1,45 +1,43 @@
dependencies {
- // todo: move dependencies to libs.toml
-
api(project(":navauth-api"))
// EternalCode Multification
- api("com.eternalcode:multification-core:1.2.3")
- api("com.eternalcode:multification-okaeri:1.2.3")
+ api(libs.multification.core)
+ api(libs.multification.okaeri)
- implementation("com.google.inject:guice:7.0.0")
- api("com.google.code.gson:gson:2.13.2")
+ implementation(libs.guice)
+ api(libs.gson)
// database
- implementation("com.zaxxer:HikariCP:7.0.2")
- api("com.j256.ormlite:ormlite-jdbc:6.1")
+ implementation(libs.hikaricp)
+ api(libs.ormlite.jdbc)
// config
- api("eu.okaeri:okaeri-configs-yaml-snakeyaml:6.0.0-beta.27")
+ api(libs.okaeri.configs.yaml.snakeyaml)
// crypto
- implementation("at.favre.lib:bcrypt:0.10.2")
- implementation("org.bouncycastle:bcprov-jdk18on:1.83")
- implementation("org.bouncycastle:bcutil-jdk18on:1.83")
+ implementation(libs.bcrypt)
+ implementation(libs.bouncycastle.prov)
+ implementation(libs.bouncycastle.util)
- compileOnly("net.kyori:adventure-text-minimessage:4.25.0")
+ compileOnly(libs.adventure.text.minimessage)
// drivers
- runtimeOnly("com.h2database:h2:2.4.240")
- runtimeOnly("com.mysql:mysql-connector-j:9.5.0")
- runtimeOnly("org.postgresql:postgresql:42.7.8")
- runtimeOnly("org.xerial:sqlite-jdbc:3.51.1.0")
+ runtimeOnly(libs.h2)
+ runtimeOnly(libs.mysql)
+ runtimeOnly(libs.postgresql)
+ runtimeOnly(libs.sqlite.jdbc)
// litecommands core (compileOnly because it is platform-dependent)
compileOnly(libs.litecommands.core)
// qr code generation
- api("com.google.zxing:core:3.5.4")
+ api(libs.zxing.core)
// tests
- testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
- testImplementation("io.kotest:kotest-assertions-core:5.8.0")
- testImplementation("io.mockk:mockk:1.13.8")
+ testImplementation(libs.kotest.runner.junit5)
+ testImplementation(libs.kotest.assertions.core)
+ testImplementation(libs.mockk)
}
\ No newline at end of file
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResFailureReason.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResFailureReason.kt
index eb1745de..ab7c7a91 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResFailureReason.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResFailureReason.kt
@@ -24,9 +24,10 @@ sealed class UsernameResFailureReason {
data class UsernameMigrationFailedUsernameAlreadyTaken(val username: String) :
UsernameResFailureReason()
- data class NonPremiumWithPremiumConflict(val premiumUsername: String) :
- UsernameResFailureReason()
+ data class NonPremiumWithPremiumConflict(val premiumUsername: String) : UsernameResFailureReason()
data class NonPremiumUsernameNotIdentical(val requiredUsername: String) :
UsernameResFailureReason()
+
+ data object ProfileAPIFailure : UsernameResFailureReason()
}
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResolutionService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResolutionService.kt
index 6a0043e8..3b3738a3 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResolutionService.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/auth/username/UsernameResolutionService.kt
@@ -19,6 +19,7 @@
package pl.spcode.navauth.common.application.auth.username
import com.google.inject.Inject
+import pl.spcode.navauth.common.application.mojang.ProfileApiFetchException
import pl.spcode.navauth.common.application.mojang.ProfileService
import pl.spcode.navauth.common.application.user.UserService
import pl.spcode.navauth.common.application.user.UsernameAlreadyTakenException
@@ -34,7 +35,12 @@ constructor(private val userService: UserService, private val profileService: Pr
connUsername: Username,
existingUserIgnoreCase: User?,
): UsernameResResult {
- val correspondingPremiumProfile = profileService.fetchProfileInfo(connUsername)
+ val correspondingPremiumProfile =
+ try {
+ profileService.fetchProfileInfo(connUsername, useNotFoundCache = true)
+ } catch (e: ProfileApiFetchException) {
+ return failure(UsernameResFailureReason.ProfileAPIFailure)
+ }
val isPremiumNickname = correspondingPremiumProfile != null
// check if the user changed their nickname (does not check letter cases)
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/UserCredentialsService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/UserCredentialsService.kt
index a1632650..e32fe360 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/UserCredentialsService.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/UserCredentialsService.kt
@@ -20,9 +20,11 @@ package pl.spcode.navauth.common.application.credentials
import com.google.inject.Inject
import com.google.inject.Singleton
+import com.google.inject.name.Named
import java.util.UUID
import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ThreadPoolExecutor
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import pl.spcode.navauth.common.application.credentials.queue.EncryptionQueueService
@@ -41,6 +43,7 @@ constructor(
val transactionService: TransactionService,
val credentialsHasherFactory: CredentialsHasherFactory,
val encryptionQueueService: EncryptionQueueService,
+ @param:Named("db") val dbExecutor: ThreadPoolExecutor,
) {
private val logger: Logger = LoggerFactory.getLogger(javaClass)
@@ -81,6 +84,7 @@ constructor(
credentialsRepository.deleteByUser(user)
}
+ // TODO: add full link qualifier for exception (requires new formatting plugin)
/**
* @param password the raw (not hashed) password
* @throws
@@ -100,10 +104,11 @@ constructor(
val future = CompletableFuture()
encryptionQueueService.submitTask(
playerId = playerId,
- operation = {
+ operation = { finishTask ->
try {
val result = hasher.verify(password, passwordHash)
future.complete(result)
+ finishTask()
} catch (ex: Exception) {
logger.error(
"Unexpected error occurred while trying to verify user id='${playerId}' password",
@@ -121,21 +126,21 @@ constructor(
return future
}
+ // TODO: add full link qualifier for exception (requires new formatting plugin)
/**
* @param password the raw (not hashed) password
* @param playerId the id of the player for queue tracking
- * @throws
- * pl.spcode.navauth.common.application.credentials.queue.EncryptionTaskAlreadyQueuedException
- * if task is already queued
+ * @throws EncryptionTaskAlreadyQueuedException if task is already queued
*/
fun enqueueHashPassword(password: String, playerId: UUID): CompletableFuture {
val hasher = credentialsHasherFactory.createDefaultHasher()
val future = CompletableFuture()
encryptionQueueService.submitTask(
playerId = playerId,
- operation = {
+ operation = { finishTask ->
try {
val result = hasher.hash(password)
+ finishTask()
future.complete(result)
} catch (ex: Exception) {
logger.error(
@@ -163,16 +168,21 @@ constructor(
*
* @param user the user whose password needs to be updated
* @param newPassword the new raw password to be hashed and stored
+ * @throws EncryptionTaskAlreadyQueuedException if task is already queued
*/
- fun updatePassword(user: User, newPassword: String) {
- val hashedPassword = enqueueHashPassword(newPassword, user.uuid.value).join()
-
- transactionService.inTransaction {
- val credentials = findCredentials(user)
- require(credentials != null) { "user does not have credentials" }
-
- val newCredentials = credentials.withNewPassword(hashedPassword)
- storeUserCredentials(user, newCredentials)
- }
+ fun updatePassword(user: User, newPassword: String): CompletableFuture {
+ return enqueueHashPassword(newPassword, user.uuid.value)
+ .thenApplyAsync(
+ { hashedPassword ->
+ transactionService.inTransaction {
+ val credentials =
+ findCredentials(user)
+ ?: throw IllegalArgumentException("user does not have credentials")
+ val newCredentials = credentials.withNewPassword(hashedPassword)
+ storeUserCredentials(user, newCredentials)
+ }
+ },
+ dbExecutor,
+ )
}
}
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/queue/EncryptionQueueService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/queue/EncryptionQueueService.kt
index a6c2d906..14db5456 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/queue/EncryptionQueueService.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/queue/EncryptionQueueService.kt
@@ -23,10 +23,29 @@ import java.util.UUID
interface EncryptionQueueService {
/**
+ * Submits an encryption task for the given player.
+ *
+ * The player's slot in the active-task registry is held from the moment this method returns until
+ * [operation] invokes the `finishTask` callback it is given (or, as a fallback, until the task
+ * finishes running). Calling `finishTask` early — e.g. right before completing a future with the
+ * result — frees the slot for a new [submitTask] call for the same [playerId] while this task's
+ * continuations are still unwinding. Calling `finishTask` more than once is safe and has no
+ * effect after the first call.
+ *
+ * @param playerId the player this task is associated with; only one task per player may be queued
+ * or running at a time
+ * @param operation the work to perform, given a `finishTask` callback that must be invoked just
+ * before committing the operation's result (e.g. right before completing a future), so the
+ * player's slot is released before any reentrant work triggered by that commit (such as a
+ * completion callback) runs
* @param onCancelled called when the task is dequeued before execution
* @throws EncryptionTaskAlreadyQueuedException if player has existing task queued
*/
- fun submitTask(playerId: UUID, operation: () -> Unit, onCancelled: () -> Unit)
+ fun submitTask(
+ playerId: UUID,
+ operation: (finishTask: () -> Unit) -> Unit,
+ onCancelled: () -> Unit,
+ )
fun dequeueTask(playerId: UUID): Boolean
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/queue/EncryptionTask.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/queue/EncryptionTask.kt
index 9e65a717..4b31b68a 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/queue/EncryptionTask.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/credentials/queue/EncryptionTask.kt
@@ -22,18 +22,18 @@ import java.util.UUID
class EncryptionTask(
val playerId: UUID?,
- private val operation: () -> Unit,
+ private val operation: (finishTask: () -> Unit) -> Unit,
val onCancelled: () -> Unit,
-) : Runnable {
+) {
@Volatile var cancelled = false
- override fun run() {
+ fun run(finishTask: () -> Unit) {
if (cancelled) {
onCancelled.invoke()
return
}
- operation()
+ operation(finishTask)
}
}
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/mojang/ProfileApiFetchException.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/mojang/ProfileApiFetchException.kt
new file mode 100644
index 00000000..c8dc4ad1
--- /dev/null
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/mojang/ProfileApiFetchException.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.application.mojang
+
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import pl.spcode.navauth.common.domain.user.Username
+
+class ProfileApiFetchException(username: Username, val causes: List) :
+ RuntimeException("All APIs failed to fetch profile for $username") {
+
+ init {
+ val logger: Logger = LoggerFactory.getLogger(ProfileApiFetchException::class.java)
+ logger.error(
+ "All APIs failed to fetch profile for {}. Following errors occurred:",
+ username.value,
+ )
+ causes.forEach { logger.error(" - {}", it.message, it) }
+ }
+}
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/mojang/ProfileService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/mojang/ProfileService.kt
index ad27cca4..4d90ee2a 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/mojang/ProfileService.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/mojang/ProfileService.kt
@@ -22,5 +22,8 @@ import pl.spcode.navauth.common.domain.mojang.MojangProfile
import pl.spcode.navauth.common.domain.user.Username
interface ProfileService {
- fun fetchProfileInfo(usernameCaseIgnored: Username): MojangProfile?
+ fun fetchProfileInfo(
+ usernameCaseIgnored: Username,
+ useNotFoundCache: Boolean = false,
+ ): MojangProfile?
}
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 fe86d18f..25f60372 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
@@ -98,24 +98,23 @@ constructor(
* @return The updated user with premium account status.
*/
fun migrateToPremium(user: User, mojangId: MojangId): User {
- val user =
- txService.inTransaction {
- val credentials = userCredentialsService.findCredentials(user)!!
- // require credentials only if there's 2FA enabled
- val requireCredentials = credentials.isTwoFactorEnabled
- val premiumUser = User.premium(user.uuid, user.username, mojangId, requireCredentials)
-
- userRepository.save(premiumUser)
- if (requireCredentials) {
- val newCredentials = credentials.withoutPassword()
- userCredentialsService.storeUserCredentials(premiumUser, newCredentials)
- } else {
- userCredentialsService.deleteUserCredentials(premiumUser)
- }
-
- return@inTransaction premiumUser
+ val user = txService.inTransaction {
+ val credentials = userCredentialsService.findCredentials(user)!!
+ // require credentials only if there's 2FA enabled
+ val requireCredentials = credentials.isTwoFactorEnabled
+ val premiumUser = User.premium(user.uuid, user.username, mojangId, requireCredentials)
+
+ userRepository.save(premiumUser)
+ if (requireCredentials) {
+ val newCredentials = credentials.withoutPassword()
+ userCredentialsService.storeUserCredentials(premiumUser, newCredentials)
+ } else {
+ userCredentialsService.deleteUserCredentials(premiumUser)
}
+ return@inTransaction premiumUser
+ }
+
eventBus as NavAuthEventBusInternal
eventBus.post(UserPremiumMigrationEvent(user.toAuthUser()))
@@ -135,19 +134,18 @@ constructor(
fun migrateToNonPremium(user: User, newPassword: HashedPassword): User {
require(user.isPremium) { "cannot migrate non-premium user to non-premium" }
- val user =
- txService.inTransaction {
- // make sure the user has credentials required
- val nonPremiumUser = user.toNonPremium()
- userRepository.save(nonPremiumUser)
+ val user = txService.inTransaction {
+ // make sure the user has credentials required
+ val nonPremiumUser = user.toNonPremium()
+ userRepository.save(nonPremiumUser)
- val newCredentials =
- userCredentialsService.findCredentials(nonPremiumUser)?.withNewPassword(newPassword)
- ?: UserCredentials.create(nonPremiumUser, newPassword, null)
- userCredentialsService.storeUserCredentials(nonPremiumUser, newCredentials)
+ val newCredentials =
+ userCredentialsService.findCredentials(nonPremiumUser)?.withNewPassword(newPassword)
+ ?: UserCredentials.create(nonPremiumUser, newPassword, null)
+ userCredentialsService.storeUserCredentials(nonPremiumUser, newCredentials)
- return@inTransaction nonPremiumUser
- }
+ return@inTransaction nonPremiumUser
+ }
eventBus as NavAuthEventBusInternal
eventBus.post(UserNonPremiumMigrationEvent(user.toAuthUser()))
@@ -167,10 +165,9 @@ constructor(
*/
fun migrateUsername(user: User, newUsername: Username): User {
val oldUsername = user.username
- val user =
- txService.inTransaction {
- return@inTransaction migrateUsernameNoTx(user, newUsername)
- }
+ val user = txService.inTransaction {
+ return@inTransaction migrateUsernameNoTx(user, newUsername)
+ }
eventBus as NavAuthEventBusInternal
eventBus.post(UserUsernameMigrationEvent(user.toAuthUser(), oldUsername.value))
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/command/configurer/CommandConfig.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/command/configurer/CommandConfig.kt
new file mode 100644
index 00000000..794fe893
--- /dev/null
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/command/configurer/CommandConfig.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.command.configurer
+
+import eu.okaeri.configs.OkaeriConfig
+import java.util.Collections.emptyList
+
+class CommandConfig(
+ var name: String? = null, // new command name
+ var enabled: Boolean = true,
+ var aliases: List = emptyList(),
+) : OkaeriConfig() {}
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/CommandsConfig.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/CommandsConfig.kt
new file mode 100644
index 00000000..57e0bc38
--- /dev/null
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/CommandsConfig.kt
@@ -0,0 +1,42 @@
+/*
+ * 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
+import pl.spcode.navauth.common.command.configurer.CommandConfig
+
+class CommandsConfig : OkaeriConfig() {
+
+ @Comment(
+ "This property allows you to configure commands definitions.",
+ "You can toggle the command and update its name or aliases.",
+ "",
+ "You can find command names in docs:",
+ "https://navio1430.github.io/NavAuth/docs/general/commands.html#available-commands",
+ "Remember to use command names without the leading slash '/'.",
+ )
+ var commands: MutableMap =
+ mutableMapOf(
+ "navauth" to CommandConfig(name = "navauth", aliases = mutableListOf("na"), enabled = true),
+ "login" to CommandConfig(name = "login", aliases = mutableListOf("l"), enabled = true),
+ "register" to
+ CommandConfig(name = "register", aliases = mutableListOf("reg"), enabled = true),
+ )
+}
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 021898c6..66db35d9 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
@@ -55,6 +55,12 @@ open class MessagesConfig : OkaeriConfig() {
"Username '%USERNAME%' is already taken! Administrator needs to resolve the conflict. "
)
+ @Comment("Usually caused by API rate limit.")
+ var profileApiFailureKickMessage =
+ TextComponent(
+ "We couldn't verify your profile with Mojang APIs. Please try again later. "
+ )
+
var loginTimeExceededError =
TextComponent("You've exceeded login time, please try again ")
@@ -87,7 +93,7 @@ open class MessagesConfig : OkaeriConfig() {
class NoticesConfig : OkaeriConfig() {
- val unexpectedErrorOccurred: Notice = Notice.chat("An unexpected error occurred.")
+ var unexpectedErrorOccurred: Notice = Notice.chat("An unexpected error occurred.")
var processAlreadyInProgressError: Notice =
Notice.chat("Process is already in progress. Please wait...")
@@ -110,6 +116,10 @@ open class MessagesConfig : OkaeriConfig() {
)
var accountAlreadyPremiumError: Notice =
Notice.chat("Account is already set as a premium one.")
+ var accountNotNonPremiumError: Notice =
+ Notice.chat(
+ "Can't execute this command right now: your account is not a non-premium one."
+ )
var alreadyTryingToLoginError: Notice =
Notice.chat(
@@ -170,7 +180,7 @@ open class MessagesConfig : OkaeriConfig() {
YOUR SECRET:
⚠ NEVER share this - even with admins!
%SECRET%
-
+
CLICK HERE TO GENERATE QR CODE
⏱ Time left: %REMAINING_SECONDS%s
@@ -198,6 +208,8 @@ open class MessagesConfig : OkaeriConfig() {
"Can't find '%USERNAME%' user in Mojang database. This player can't be migrated to premium mode."
)
+ var adminCmdPasswordSetUpdating: Notice =
+ Notice.chat("Updating password, please wait...")
var adminCmdPasswordSetSuccess: Notice =
Notice.chat("Success! User '%USERNAME%' password was set.")
var adminCmdAccountMigratedToNonPremiumSuccess: Notice =
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/concurrent/NamedThreadFactory.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/concurrent/NamedThreadFactory.kt
new file mode 100644
index 00000000..2a625c03
--- /dev/null
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/concurrent/NamedThreadFactory.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.infra.concurrent
+
+import java.util.concurrent.ThreadFactory
+import java.util.concurrent.atomic.AtomicInteger
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+class NamedThreadFactory(private val prefix: String) : ThreadFactory {
+
+ private val counter = AtomicInteger(0)
+
+ override fun newThread(r: Runnable): Thread {
+ return Thread(r, "$prefix-${counter.getAndIncrement()}").apply {
+ isDaemon = false
+ priority = Thread.NORM_PRIORITY
+ setUncaughtExceptionHandler { thread, ex ->
+ logger.error("Uncaught exception in thread ${thread.name}", ex)
+ }
+ }
+ }
+
+ companion object {
+ private val logger: Logger = LoggerFactory.getLogger(NamedThreadFactory::class.java)
+ }
+}
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/crypto/queue/EncryptionQueueServiceImpl.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/crypto/queue/EncryptionQueueServiceImpl.kt
index 75992055..84d4b24b 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/crypto/queue/EncryptionQueueServiceImpl.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/crypto/queue/EncryptionQueueServiceImpl.kt
@@ -67,7 +67,11 @@ class EncryptionQueueServiceImpl @Inject constructor(private val config: Encrypt
}
/** @throws EncryptionTaskAlreadyQueuedException if task is already queued */
- override fun submitTask(playerId: UUID, operation: () -> Unit, onCancelled: () -> Unit) {
+ override fun submitTask(
+ playerId: UUID,
+ operation: (finishTask: () -> Unit) -> Unit,
+ onCancelled: () -> Unit,
+ ) {
val task = EncryptionTask(playerId, operation, onCancelled)
if (activeTasks.containsKey(playerId)) {
@@ -75,7 +79,12 @@ class EncryptionQueueServiceImpl @Inject constructor(private val config: Encrypt
}
activeTasks[playerId] = task
- executor.execute(EncryptionTaskRunner(playerId, task, activeTasks, logger))
+ val finishTask = {
+ activeTasks.remove(playerId, task)
+ Unit
+ }
+
+ executor.execute(EncryptionTaskRunner(playerId, task, finishTask, logger))
}
override fun isTaskQueued(playerId: UUID): Boolean {
@@ -83,8 +92,9 @@ class EncryptionQueueServiceImpl @Inject constructor(private val config: Encrypt
}
override fun dequeueTask(playerId: UUID): Boolean {
- val cancelledFromQueue =
- workQueue.removeIf { it is EncryptionTaskRunner && it.playerId == playerId }
+ val cancelledFromQueue = workQueue.removeIf {
+ it is EncryptionTaskRunner && it.playerId == playerId
+ }
val task = activeTasks.remove(playerId)
if (task != null) {
@@ -101,15 +111,15 @@ class EncryptionQueueServiceImpl @Inject constructor(private val config: Encrypt
private class EncryptionTaskRunner(
val playerId: UUID,
val task: EncryptionTask,
- private val activeTasks: ConcurrentHashMap,
+ private val finishTask: () -> Unit,
private val logger: Logger,
) : Runnable {
override fun run() {
- val result = runCatching { task.run() }
+ val result = runCatching { task.run(finishTask) }
if (result.isFailure) {
logger.warn("Player id='${playerId}' EncryptionTask failed", result.exceptionOrNull())
}
- activeTasks.remove(playerId)
+ finishTask()
}
}
}
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/CompositeProfileService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/CompositeProfileService.kt
index c3efad7c..eb3e042f 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/CompositeProfileService.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/CompositeProfileService.kt
@@ -20,6 +20,7 @@ package pl.spcode.navauth.common.infra.mojang
import com.google.inject.Inject
import com.google.inject.Singleton
+import pl.spcode.navauth.common.application.mojang.ProfileApiFetchException
import pl.spcode.navauth.common.application.mojang.ProfileService
import pl.spcode.navauth.common.config.MojangAPIConfig
import pl.spcode.navauth.common.domain.mojang.MojangProfile
@@ -36,13 +37,21 @@ constructor(
private val profileCache: ProfileCache,
) : ProfileService {
- override fun fetchProfileInfo(usernameCaseIgnored: Username): MojangProfile? {
- profileCache.get(usernameCaseIgnored)?.let {
- return it
+ override fun fetchProfileInfo(
+ usernameCaseIgnored: Username,
+ useNotFoundCache: Boolean,
+ ): MojangProfile? {
+ val username = Username(usernameCaseIgnored.value.lowercase())
+ when (val cached = profileCache.get(username)) {
+ is CachedProfile.Found -> return cached.profile
+ is CachedProfile.NotFound ->
+ if (useNotFoundCache) {
+ return null
+ }
+ null -> {}
}
- var lastError: Exception? = null
-
+ val exceptions: MutableList = mutableListOf()
for (api in config.apiOrder) {
try {
val profile =
@@ -52,15 +61,20 @@ constructor(
MojangProfileApi.MOJANG -> mojangProfileService.fetchProfileInfo(usernameCaseIgnored)
}
if (profile != null) {
- profileCache.put(usernameCaseIgnored, profile)
+ profileCache.putFound(username, profile)
return profile
}
- } catch (e: Exception) {
- lastError = e
+ } catch (ex: Exception) {
+ exceptions.add(ex)
}
}
- if (lastError != null) throw lastError
+ if (exceptions.isNotEmpty()) {
+ throw ProfileApiFetchException(usernameCaseIgnored, exceptions)
+ }
+ if (useNotFoundCache) {
+ profileCache.putNotFound(username)
+ }
return null
}
}
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/MineToolsProfileService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/MineToolsProfileService.kt
index ed556fe5..d2a850ee 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/MineToolsProfileService.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/MineToolsProfileService.kt
@@ -40,7 +40,10 @@ constructor(val httpClient: HttpClient, val gson: Gson, val config: MojangAPICon
private data class MineToolsProfileDto(val id: String?, val name: String?, val status: String?)
- override fun fetchProfileInfo(usernameCaseIgnored: Username): MojangProfile? {
+ override fun fetchProfileInfo(
+ usernameCaseIgnored: Username,
+ useNotFoundCache: Boolean,
+ ): MojangProfile? {
val requestUri = URI.create("https://api.minetools.eu/uuid/${usernameCaseIgnored.value}")
val request = HttpRequest.newBuilder(requestUri).timeout(config.apiTimeout).GET().build()
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/MojangProfileServiceImpl.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/MojangProfileServiceImpl.kt
index 85ecfbf2..fec415a2 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/MojangProfileServiceImpl.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/MojangProfileServiceImpl.kt
@@ -50,7 +50,10 @@ constructor(val httpClient: HttpClient, val gson: Gson, val config: MojangAPICon
}
}
- override fun fetchProfileInfo(usernameCaseIgnored: Username): MojangProfile? {
+ override fun fetchProfileInfo(
+ usernameCaseIgnored: Username,
+ useNotFoundCache: Boolean,
+ ): MojangProfile? {
val requestUri =
URI.create(
"https://api.minecraftservices.com/minecraft/profile/lookup/name/${usernameCaseIgnored.value}"
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/ProfileCache.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/ProfileCache.kt
index 72665a10..4df82eeb 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/ProfileCache.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/infra/mojang/ProfileCache.kt
@@ -26,18 +26,28 @@ import pl.spcode.navauth.common.config.MojangAPIConfig
import pl.spcode.navauth.common.domain.mojang.MojangProfile
import pl.spcode.navauth.common.domain.user.Username
+sealed interface CachedProfile {
+ data class Found(val profile: MojangProfile) : CachedProfile
+
+ data object NotFound : CachedProfile
+}
+
@Singleton
class ProfileCache @Inject constructor(config: MojangAPIConfig) {
- private val cache: Cache =
+ private val cache: Cache =
CacheBuilder.newBuilder().expireAfterWrite(config.profileCacheTTL).build()
- fun get(username: Username): MojangProfile? {
+ fun get(username: Username): CachedProfile? {
return cache.getIfPresent(username.value)
}
- fun put(username: Username, profile: MojangProfile) {
- cache.put(username.value, profile)
+ fun putFound(username: Username, profile: MojangProfile) {
+ cache.put(username.value, CachedProfile.Found(profile))
+ }
+
+ fun putNotFound(username: Username) {
+ cache.put(username.value, CachedProfile.NotFound)
}
fun invalidate(username: Username) {
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/migrate/error/MigrationException.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/migrate/error/MigrationException.kt
index ecf94d9a..ea5527df 100644
--- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/migrate/error/MigrationException.kt
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/migrate/error/MigrationException.kt
@@ -18,5 +18,4 @@
package pl.spcode.navauth.common.migrate.error
-open class MigrationException(message: String, error: Throwable? = null) :
- Exception(message, error)
+open class MigrationException(message: String, error: Throwable? = null) : Exception(message, error)
diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/module/ExecutorsModule.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/module/ExecutorsModule.kt
new file mode 100644
index 00000000..a936a1c4
--- /dev/null
+++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/module/ExecutorsModule.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.module
+
+import com.google.inject.AbstractModule
+import com.google.inject.Provides
+import com.google.inject.Singleton
+import com.google.inject.name.Named
+import java.util.concurrent.SynchronousQueue
+import java.util.concurrent.ThreadPoolExecutor
+import java.util.concurrent.TimeUnit
+import pl.spcode.navauth.common.infra.concurrent.NamedThreadFactory
+
+class ExecutorsModule : AbstractModule() {
+
+ @Provides
+ @Singleton
+ @Named("db")
+ fun provideDbExecutor(): ThreadPoolExecutor {
+ return ThreadPoolExecutor(
+ 0,
+ 2,
+ 60L,
+ TimeUnit.SECONDS,
+ SynchronousQueue(),
+ NamedThreadFactory("navauth-db"),
+ )
+ }
+}
diff --git a/navauth-common/src/test/kotlin/extension/app/ApplicationTestExtension.kt b/navauth-common/src/test/kotlin/extension/app/ApplicationTestExtension.kt
index 7fbe77b9..a2f707e9 100644
--- a/navauth-common/src/test/kotlin/extension/app/ApplicationTestExtension.kt
+++ b/navauth-common/src/test/kotlin/extension/app/ApplicationTestExtension.kt
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.TestInstancePostProcessor
import pl.spcode.navauth.common.module.DataPersistenceModule
import pl.spcode.navauth.common.module.EventsModule
+import pl.spcode.navauth.common.module.ExecutorsModule
import pl.spcode.navauth.common.module.HttpClientModule
import pl.spcode.navauth.common.module.ServicesModule
import utils.GuiceUtils
@@ -37,6 +38,7 @@ class ApplicationTestExtension : TestInstancePostProcessor {
TestsConfigModule(),
HttpClientModule(),
DataPersistenceModule(),
+ ExecutorsModule(),
ServicesModule(),
)
diff --git a/navauth-common/src/test/kotlin/extension/app/DataPersistenceTestExtension.kt b/navauth-common/src/test/kotlin/extension/app/DataPersistenceTestExtension.kt
index 10d02390..f777f124 100644
--- a/navauth-common/src/test/kotlin/extension/app/DataPersistenceTestExtension.kt
+++ b/navauth-common/src/test/kotlin/extension/app/DataPersistenceTestExtension.kt
@@ -25,10 +25,12 @@ import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.TestInstancePostProcessor
import pl.spcode.navauth.common.infra.database.DatabaseManager
import pl.spcode.navauth.common.module.DataPersistenceModule
+import pl.spcode.navauth.common.module.ExecutorsModule
import utils.GuiceUtils
class DataPersistenceTestExtension : TestInstancePostProcessor {
- var injector: Injector = Guice.createInjector(TestsConfigModule(), DataPersistenceModule())
+ var injector: Injector =
+ Guice.createInjector(TestsConfigModule(), DataPersistenceModule(), ExecutorsModule())
init {
injector.getInstance(DatabaseManager::class.java).connectAndInit()
diff --git a/navauth-common/src/test/kotlin/extension/app/UsernameResolutionTestExtension.kt b/navauth-common/src/test/kotlin/extension/app/UsernameResolutionTestExtension.kt
index 4f1cde2d..9b021f3e 100644
--- a/navauth-common/src/test/kotlin/extension/app/UsernameResolutionTestExtension.kt
+++ b/navauth-common/src/test/kotlin/extension/app/UsernameResolutionTestExtension.kt
@@ -30,6 +30,7 @@ import pl.spcode.navauth.common.application.mojang.ProfileService
import pl.spcode.navauth.common.infra.database.DatabaseManager
import pl.spcode.navauth.common.module.DataPersistenceModule
import pl.spcode.navauth.common.module.EventsModule
+import pl.spcode.navauth.common.module.ExecutorsModule
import pl.spcode.navauth.common.module.HttpClientModule
import pl.spcode.navauth.common.module.ServicesModule
import utils.GuiceUtils
@@ -45,6 +46,7 @@ class UsernameResolutionTestExtension : TestInstancePostProcessor {
TestsConfigModule(),
HttpClientModule(),
DataPersistenceModule(),
+ ExecutorsModule(),
ServicesModule(),
)
.with(
diff --git a/navauth-common/src/test/kotlin/fake/FakeProfileService.kt b/navauth-common/src/test/kotlin/fake/FakeProfileService.kt
index 1099f3e1..76721742 100644
--- a/navauth-common/src/test/kotlin/fake/FakeProfileService.kt
+++ b/navauth-common/src/test/kotlin/fake/FakeProfileService.kt
@@ -29,7 +29,10 @@ class FakeProfileService : ProfileService {
profiles[username] = profile
}
- override fun fetchProfileInfo(usernameCaseIgnored: Username): MojangProfile? {
+ override fun fetchProfileInfo(
+ usernameCaseIgnored: Username,
+ useNotFoundCache: Boolean,
+ ): MojangProfile? {
return profiles[usernameCaseIgnored]
}
diff --git a/navauth-common/src/test/kotlin/unit/auth/UsernameResolutionServiceTests.kt b/navauth-common/src/test/kotlin/unit/auth/UsernameResolutionServiceTests.kt
index b09c20e8..57e1a671 100644
--- a/navauth-common/src/test/kotlin/unit/auth/UsernameResolutionServiceTests.kt
+++ b/navauth-common/src/test/kotlin/unit/auth/UsernameResolutionServiceTests.kt
@@ -28,6 +28,7 @@ import pl.spcode.navauth.common.application.auth.username.PostUsernameResolution
import pl.spcode.navauth.common.application.auth.username.UsernameResFailureReason
import pl.spcode.navauth.common.application.auth.username.UsernameResResult
import pl.spcode.navauth.common.application.auth.username.UsernameResolutionService
+import pl.spcode.navauth.common.application.mojang.ProfileApiFetchException
import pl.spcode.navauth.common.application.user.UserService
import pl.spcode.navauth.common.domain.auth.handshake.EncryptionType
import pl.spcode.navauth.common.domain.mojang.MojangProfile
@@ -54,7 +55,7 @@ class UsernameResolutionServiceTests :
test("new premium user returns success, premium encryption type") {
val username = Username(generateRandomString(10))
val premiumProfile = MojangProfile(MojangId(UUID.randomUUID()), username)
- every { mockProfileService.fetchProfileInfo(username) } returns premiumProfile
+ every { mockProfileService.fetchProfileInfo(username, any()) } returns premiumProfile
every { mockUserService.findUserByMojangUuid(MojangId(any())) } returns null
val result = service.resolveUsernameConflicts(username, null)
@@ -68,7 +69,7 @@ class UsernameResolutionServiceTests :
test("new nonpremium user returns success, none encryption type") {
val username = Username(generateRandomString(10))
- every { mockProfileService.fetchProfileInfo(username) } returns null
+ every { mockProfileService.fetchProfileInfo(username, any()) } returns null
every { mockUserService.findUserByMojangUuid(MojangId(any())) } returns null
val result = service.resolveUsernameConflicts(username, null)
@@ -77,10 +78,32 @@ class UsernameResolutionServiceTests :
UsernameResResult.Success(EncryptionType.NONE, PostUsernameResolutionState.NEW_ACCOUNT)
}
+ test("returns ProfileAPIFailure when profile API fails for new user") {
+ val username = Username(generateRandomString(10))
+ every { mockProfileService.fetchProfileInfo(username, any()) } throws
+ ProfileApiFetchException(username, listOf(RuntimeException("minetools error")))
+
+ val result = service.resolveUsernameConflicts(username, null)
+
+ result shouldBe UsernameResResult.Failure(UsernameResFailureReason.ProfileAPIFailure)
+ }
+
+ test("returns ProfileAPIFailure when profile API fails for existing premium user") {
+ val username = Username(generateRandomString(10))
+ every { mockProfileService.fetchProfileInfo(username, any()) } throws
+ ProfileApiFetchException(username, listOf(RuntimeException("mojang error")))
+ val existingUser =
+ User.premium(UserUuid(UUID.randomUUID()), username, MojangId(UUID.randomUUID()))
+
+ val result = service.resolveUsernameConflicts(username, existingUser)
+
+ result shouldBe UsernameResResult.Failure(UsernameResFailureReason.ProfileAPIFailure)
+ }
+
test("existing premium user same connection username returns success, premium encryption") {
val username = Username(generateRandomString(10))
val premiumProfile = MojangProfile(MojangId(UUID.randomUUID()), username)
- every { mockProfileService.fetchProfileInfo(username) } returns premiumProfile
+ every { mockProfileService.fetchProfileInfo(username, any()) } returns premiumProfile
val existingUser =
User.premium(UserUuid(premiumProfile.uuid.value), username, premiumProfile.uuid, false)
@@ -95,7 +118,7 @@ class UsernameResolutionServiceTests :
test("existing nonpremium user same connection username returns success, none encryption") {
val username = Username(generateRandomString(10))
- every { mockProfileService.fetchProfileInfo(username) } returns null
+ every { mockProfileService.fetchProfileInfo(username, any()) } returns null
val existingUser = User.nonPremium(UserUuid(UUID.randomUUID()), username)
val result = service.resolveUsernameConflicts(username, existingUser)
@@ -109,7 +132,7 @@ class UsernameResolutionServiceTests :
// make sure the premium profile has a different username case
val premiumUsername = Username(invertCase(username.value))
val premiumProfile = MojangProfile(MojangId(UUID.randomUUID()), premiumUsername)
- every { mockProfileService.fetchProfileInfo(username) } returns premiumProfile
+ every { mockProfileService.fetchProfileInfo(username, any()) } returns premiumProfile
val existingUser = User.nonPremium(UserUuid(premiumProfile.uuid.value), username)
val result = service.resolveUsernameConflicts(username, existingUser)
@@ -123,7 +146,7 @@ class UsernameResolutionServiceTests :
test("premium username with existing nonpremium user and same username returns success") {
val username = Username(generateRandomString(10))
val premiumProfile = MojangProfile(MojangId(UUID.randomUUID()), username)
- every { mockProfileService.fetchProfileInfo(username) } returns premiumProfile
+ every { mockProfileService.fetchProfileInfo(username, any()) } returns premiumProfile
val existingUser = User.nonPremium(UserUuid(premiumProfile.uuid.value), username)
val result = service.resolveUsernameConflicts(username, existingUser)
@@ -138,7 +161,7 @@ class UsernameResolutionServiceTests :
test("existing nonpremium user different connection username failure") {
val username = Username(generateRandomString(10))
val connUsername = Username(generateRandomString(10))
- every { mockProfileService.fetchProfileInfo(connUsername) } returns null
+ every { mockProfileService.fetchProfileInfo(connUsername, any()) } returns null
val existingUser = User.nonPremium(UserUuid(UUID.randomUUID()), username)
val result = service.resolveUsernameConflicts(connUsername, existingUser)
@@ -153,7 +176,7 @@ class UsernameResolutionServiceTests :
val username = Username(generateRandomString(10))
val connUsername = Username(generateRandomString(10))
val premiumProfile = MojangProfile(MojangId(UUID.randomUUID()), username)
- every { mockProfileService.fetchProfileInfo(connUsername) } returns premiumProfile
+ every { mockProfileService.fetchProfileInfo(connUsername, any()) } returns premiumProfile
every { mockUserService.findUserByMojangUuid(MojangId(any())) } returns null
val existingUser =
User.premium(UserUuid(UUID.randomUUID()), username, premiumProfile.uuid, false)
@@ -172,7 +195,7 @@ class UsernameResolutionServiceTests :
updatedUsername: Username,
): MojangProfile {
val updatedProfile = MojangProfile(mojangId, updatedUsername)
- every { mockProfileService.fetchProfileInfo(updatedUsername) } returns updatedProfile
+ every { mockProfileService.fetchProfileInfo(updatedUsername, any()) } returns updatedProfile
every { mockUserService.findUserByMojangUuid(mojangId) } returns existingUser
every { mockUserService.migrateUsername(existingUser, updatedUsername) } returns
existingUser.withNewUsername(updatedUsername)
diff --git a/navauth-common/src/test/kotlin/unit/mojang/CompositeProfileServiceTest.kt b/navauth-common/src/test/kotlin/unit/mojang/CompositeProfileServiceTest.kt
index e98daf92..170283b8 100644
--- a/navauth-common/src/test/kotlin/unit/mojang/CompositeProfileServiceTest.kt
+++ b/navauth-common/src/test/kotlin/unit/mojang/CompositeProfileServiceTest.kt
@@ -20,6 +20,7 @@ package unit.mojang
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.shouldBe
import io.mockk.every
@@ -27,11 +28,13 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import java.util.UUID
+import pl.spcode.navauth.common.application.mojang.ProfileApiFetchException
import pl.spcode.navauth.common.config.MojangAPIConfig
import pl.spcode.navauth.common.domain.mojang.MojangProfile
import pl.spcode.navauth.common.domain.mojang.MojangProfileApi
import pl.spcode.navauth.common.domain.user.MojangId
import pl.spcode.navauth.common.domain.user.Username
+import pl.spcode.navauth.common.infra.mojang.CachedProfile
import pl.spcode.navauth.common.infra.mojang.CompositeProfileService
import pl.spcode.navauth.common.infra.mojang.MineToolsProfileService
import pl.spcode.navauth.common.infra.mojang.MojangProfileServiceImpl
@@ -54,12 +57,13 @@ class CompositeProfileServiceTest :
mineTools = mockk()
mojang = mockk()
cache = mockk()
- every { cache.put(any(), any()) } just runs
+ every { cache.putFound(any(), any()) } just runs
+ every { cache.putNotFound(any()) } just runs
service = CompositeProfileService(config, mojang, mineTools, cache)
}
test("returns cached profile without calling APIs") {
- every { cache.get(username) } returns minetoolsProfile
+ every { cache.get(username) } returns CachedProfile.Found(minetoolsProfile)
val result = service.fetchProfileInfo(username)
@@ -110,13 +114,48 @@ class CompositeProfileServiceTest :
every { mineTools.fetchProfileInfo(username) } throws RuntimeException("minetools error")
every { mojang.fetchProfileInfo(username) } throws RuntimeException("mojang error")
- shouldThrow { service.fetchProfileInfo(username) }
+ val exception = shouldThrow { service.fetchProfileInfo(username) }
+
+ exception.causes
+ .map { it.message }
+ .shouldContainExactlyInAnyOrder("minetools error", "mojang error")
+ }
+
+ test("throws when minetools returns null and mojang throws") {
+ every { cache.get(username) } returns null
+ every { mineTools.fetchProfileInfo(username) } returns null
+ every { mojang.fetchProfileInfo(username) } throws RuntimeException("mojang timeout")
+
+ val exception = shouldThrow { service.fetchProfileInfo(username) }
+
+ exception.causes.map { it.message }.shouldContainExactlyInAnyOrder("mojang timeout")
+ }
+
+ test("throws when minetools throws and mojang returns null") {
+ every { cache.get(username) } returns null
+ every { mineTools.fetchProfileInfo(username) } throws RuntimeException("minetools error")
+ every { mojang.fetchProfileInfo(username) } returns null
+
+ val exception = shouldThrow { service.fetchProfileInfo(username) }
+
+ exception.causes.map { it.message }.shouldContainExactlyInAnyOrder("minetools error")
+ }
+
+ test("throws when first API throws and second returns null") {
+ config.apiOrder = listOf(MojangProfileApi.MOJANG, MojangProfileApi.MINETOOLS)
+ every { cache.get(username) } returns null
+ every { mojang.fetchProfileInfo(username) } throws RuntimeException("mojang error")
+ every { mineTools.fetchProfileInfo(username) } returns null
+
+ val exception = shouldThrow { service.fetchProfileInfo(username) }
+
+ exception.causes.map { it.message }.shouldContainExactlyInAnyOrder("mojang error")
}
test("returns minetools result and caches it") {
every { cache.get(username) } returns null
every { mineTools.fetchProfileInfo(username) } returns minetoolsProfile
- every { cache.put(username, minetoolsProfile) } returns Unit
+ every { cache.putFound(username, minetoolsProfile) } returns Unit
val result = service.fetchProfileInfo(username)
@@ -127,7 +166,7 @@ class CompositeProfileServiceTest :
every { cache.get(username) } returns null
every { mineTools.fetchProfileInfo(username) } throws RuntimeException("minetools error")
every { mojang.fetchProfileInfo(username) } returns mojangProfile
- every { cache.put(username, mojangProfile) } returns Unit
+ every { cache.putFound(username, mojangProfile) } returns Unit
val result = service.fetchProfileInfo(username)
@@ -153,4 +192,52 @@ class CompositeProfileServiceTest :
result shouldBe mojangProfile
}
+
+ test("returns null from cached NotFound without calling APIs when useNotFoundCache") {
+ every { cache.get(username) } returns CachedProfile.NotFound
+
+ val result = service.fetchProfileInfo(username, useNotFoundCache = true)
+
+ result.shouldBeNull()
+ }
+
+ test("ignores cached NotFound and calls APIs when useNotFoundCache is false") {
+ every { cache.get(username) } returns CachedProfile.NotFound
+ every { mineTools.fetchProfileInfo(username) } returns minetoolsProfile
+
+ val result = service.fetchProfileInfo(username)
+
+ result shouldBe minetoolsProfile
+ }
+
+ test("caches NotFound when all APIs return null and useNotFoundCache is true") {
+ every { cache.get(username) } returns null
+ every { mineTools.fetchProfileInfo(username) } returns null
+ every { mojang.fetchProfileInfo(username) } returns null
+ every { cache.putNotFound(username) } returns Unit
+
+ val result = service.fetchProfileInfo(username, useNotFoundCache = true)
+
+ result.shouldBeNull()
+ }
+
+ test("does not cache NotFound when useNotFoundCache is false") {
+ every { cache.get(username) } returns null
+ every { mineTools.fetchProfileInfo(username) } returns null
+ every { mojang.fetchProfileInfo(username) } returns null
+
+ val result = service.fetchProfileInfo(username)
+
+ result.shouldBeNull()
+ }
+
+ test("does not cache NotFound when APIs throw") {
+ every { cache.get(username) } returns null
+ every { mineTools.fetchProfileInfo(username) } throws RuntimeException("minetools error")
+ every { mojang.fetchProfileInfo(username) } throws RuntimeException("mojang error")
+
+ shouldThrow {
+ service.fetchProfileInfo(username, useNotFoundCache = true)
+ }
+ }
})
diff --git a/navauth-common/src/test/kotlin/unit/mojang/ProfileCacheTest.kt b/navauth-common/src/test/kotlin/unit/mojang/ProfileCacheTest.kt
index 74f96d22..a448a7e8 100644
--- a/navauth-common/src/test/kotlin/unit/mojang/ProfileCacheTest.kt
+++ b/navauth-common/src/test/kotlin/unit/mojang/ProfileCacheTest.kt
@@ -26,6 +26,7 @@ import pl.spcode.navauth.common.config.MojangAPIConfig
import pl.spcode.navauth.common.domain.mojang.MojangProfile
import pl.spcode.navauth.common.domain.user.MojangId
import pl.spcode.navauth.common.domain.user.Username
+import pl.spcode.navauth.common.infra.mojang.CachedProfile
import pl.spcode.navauth.common.infra.mojang.ProfileCache
class ProfileCacheTest :
@@ -38,20 +39,43 @@ class ProfileCacheTest :
test("get returns null for uncached username") { cache.get(username).shouldBeNull() }
- test("get returns profile after put") {
- cache.put(username, profile)
- cache.get(username) shouldBe profile
+ test("get returns Found after putFound") {
+ cache.putFound(username, profile)
+ cache.get(username) shouldBe CachedProfile.Found(profile)
}
- test("put overwrites existing entry") {
+ test("get returns NotFound after putNotFound") {
+ cache.putNotFound(username)
+ cache.get(username) shouldBe CachedProfile.NotFound
+ }
+
+ test("putFound overwrites existing entry") {
val newProfile = MojangProfile(MojangId(UUID.randomUUID()), username)
- cache.put(username, profile)
- cache.put(username, newProfile)
- cache.get(username) shouldBe newProfile
+ cache.putFound(username, profile)
+ cache.putFound(username, newProfile)
+ cache.get(username) shouldBe CachedProfile.Found(newProfile)
+ }
+
+ test("putFound overwrites NotFound entry") {
+ cache.putNotFound(username)
+ cache.putFound(username, profile)
+ cache.get(username) shouldBe CachedProfile.Found(profile)
+ }
+
+ test("putNotFound overwrites Found entry") {
+ cache.putFound(username, profile)
+ cache.putNotFound(username)
+ cache.get(username) shouldBe CachedProfile.NotFound
}
test("invalidate removes entry") {
- cache.put(username, profile)
+ cache.putFound(username, profile)
+ cache.invalidate(username)
+ cache.get(username).shouldBeNull()
+ }
+
+ test("invalidate removes NotFound entry") {
+ cache.putNotFound(username)
cache.invalidate(username)
cache.get(username).shouldBeNull()
}
@@ -60,7 +84,7 @@ class ProfileCacheTest :
test("get returns null for different usernames") {
val other = Username("other")
- cache.put(username, profile)
+ cache.putFound(username, profile)
cache.get(other).shouldBeNull()
}
@@ -69,7 +93,7 @@ class ProfileCacheTest :
config.profileCacheTTL = java.time.Duration.ofHours(1)
val longTtlCache = ProfileCache(config)
- longTtlCache.put(username, profile)
- longTtlCache.get(username) shouldBe profile
+ longTtlCache.putFound(username, profile)
+ longTtlCache.get(username) shouldBe CachedProfile.Found(profile)
}
})
diff --git a/navauth-docs-gen/build.gradle.kts b/navauth-docs-gen/build.gradle.kts
index 3569ad44..8ae2bba1 100644
--- a/navauth-docs-gen/build.gradle.kts
+++ b/navauth-docs-gen/build.gradle.kts
@@ -31,7 +31,3 @@ tasks.register("generate") {
classpath = sourceSets.main.get().runtimeClasspath
mainClass.set("pl.spcode.navauth.docsgen.MainKt")
}
-
-kotlin {
- jvmToolchain(21)
-}
\ No newline at end of file
diff --git a/navauth-velocity/build.gradle.kts b/navauth-velocity/build.gradle.kts
index b691b1c2..e830a742 100644
--- a/navauth-velocity/build.gradle.kts
+++ b/navauth-velocity/build.gradle.kts
@@ -54,10 +54,10 @@ dependencies {
kapt(libs.velocitypowered.velocity.api)
// bstats
- implementation("org.bstats:bstats-velocity:3.1.0")
+ implementation(libs.bstats.velocity)
// Tribufu-Rcon used in itzg containers
- compileOnly("com.tribufu:Tribufu-VelocityRcon:1.2.0")
+ compileOnly(libs.tribufu.velocity.rcon)
}
tasks.withType {
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/NavAuthVelocity.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/NavAuthVelocity.kt
index ed447432..bfe350df 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/NavAuthVelocity.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/NavAuthVelocity.kt
@@ -43,12 +43,14 @@ import pl.spcode.navauth.common.command.exception.UserResolveException
import pl.spcode.navauth.common.command.handler.UserResolveExceptionHandler
import pl.spcode.navauth.common.command.user.UsernameOrUuidParser
import pl.spcode.navauth.common.command.user.UsernameOrUuidRaw
+import pl.spcode.navauth.common.config.CommandsConfig
import pl.spcode.navauth.common.config.GeneralConfig
import pl.spcode.navauth.common.config.MessagesConfig
import pl.spcode.navauth.common.config.MigrationConfig
import pl.spcode.navauth.common.infra.database.DatabaseManager
import pl.spcode.navauth.common.module.*
import pl.spcode.navauth.velocity.command.CommandsRegistry
+import pl.spcode.navauth.velocity.command.configurer.CommandConfigurer
import pl.spcode.navauth.velocity.infra.command.VelocityInvalidUsageHandler
import pl.spcode.navauth.velocity.infra.command.VelocityMissingPermissionExceptionHandler
import pl.spcode.navauth.velocity.infra.command.VelocityMissingPermissionHandler
@@ -105,6 +107,9 @@ constructor(
val migrationConfigModule =
YamlConfigModule(MigrationConfig::class, dataDirectory.resolve("migration.yml").toFile())
+ val commandsConfigModule =
+ YamlConfigModule(CommandsConfig::class, dataDirectory.resolve("commands.yml").toFile())
+
injector =
parentInjector.createChildInjector(
PluginDirectoryModule(dataDirectory),
@@ -112,6 +117,8 @@ constructor(
generalConfigModule,
messagesConfigModule,
migrationConfigModule,
+ commandsConfigModule,
+ ExecutorsModule(),
EventsModule(),
VelocityMultificationsModule(velocityViewerProvider),
VelocityCommandsModule(),
@@ -162,6 +169,7 @@ constructor(
UserResolveException::class.java,
UserResolveExceptionHandler(VelocityAudienceProvider(proxyServer)),
)
+ .editorGlobal(injector.getInstance(CommandConfigurer::class.java))
.exception(
MissingPermissionException::class.java,
injector.getInstance(VelocityMissingPermissionExceptionHandler::class.java),
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 18f0cd5e..b4035a0f 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
@@ -28,7 +28,7 @@ import pl.spcode.navauth.common.application.credentials.UserCredentialsService
import pl.spcode.navauth.common.config.GeneralConfig
import pl.spcode.navauth.common.config.MessagesConfig
import pl.spcode.navauth.common.domain.user.User
-import pl.spcode.navauth.velocity.application.event.VelocityEventDispatcher
+import pl.spcode.navauth.velocity.application.server.VelocityServerConnectService
import pl.spcode.navauth.velocity.infra.auth.VelocityAutoLoginAuthSession
import pl.spcode.navauth.velocity.infra.auth.VelocityLoginAuthSession
import pl.spcode.navauth.velocity.infra.auth.VelocityRegisterAuthSession
@@ -44,7 +44,7 @@ constructor(
val authSessionService: AuthSessionService,
val userCredentialsService: UserCredentialsService,
val scheduler: NavAuthScheduler,
- val velocityEventDispatcher: VelocityEventDispatcher,
+ val serverConnectService: VelocityServerConnectService,
val multification: VelocityMultification,
val generalConfig: GeneralConfig,
val messagesConfig: MessagesConfig,
@@ -68,7 +68,7 @@ constructor(
credentials,
userCredentialsService,
scheduler,
- velocityEventDispatcher,
+ serverConnectService,
multification,
generalConfig,
messagesConfig,
@@ -85,7 +85,7 @@ constructor(
VelocityRegisterAuthSession(
player,
scheduler,
- velocityEventDispatcher,
+ serverConnectService,
multification,
messagesConfig,
generalConfig,
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/event/VelocityEventDispatcher.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/event/VelocityEventDispatcher.kt
deleted file mode 100644
index d7b8ae43..00000000
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/event/VelocityEventDispatcher.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * NavAuth
- * Copyright © 2025 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.event
-
-import com.google.inject.Inject
-import com.google.inject.Singleton
-import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent
-import com.velocitypowered.api.proxy.ConnectionRequestBuilder
-import com.velocitypowered.api.proxy.Player
-import com.velocitypowered.api.proxy.ProxyServer
-import java.util.concurrent.CompletableFuture
-
-@Singleton
-class VelocityEventDispatcher @Inject constructor(val proxyServer: ProxyServer) {
-
- /** Invoked with initial server as the one player is currently connected to. */
- fun fireVelocityChooseInitialServerEventAsync(
- player: Player
- ): CompletableFuture {
- val currentServer = player.currentServer.get().server
- return proxyServer.eventManager
- .fire(PlayerChooseInitialServerEvent(player, currentServer))
- .thenApply { player.createConnectionRequest(it.initialServer.get()).connect().get() }
- }
-}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/server/VelocityServerConnectService.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/server/VelocityServerConnectService.kt
new file mode 100644
index 00000000..7c535635
--- /dev/null
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/server/VelocityServerConnectService.kt
@@ -0,0 +1,54 @@
+/*
+ * NavAuth
+ * Copyright © 2025 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.server
+
+import com.google.inject.Inject
+import com.google.inject.Singleton
+import com.velocitypowered.api.proxy.Player
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+@Singleton
+class VelocityServerConnectService
+@Inject
+constructor(val serverSelectionService: VelocityServerSelectionService) {
+
+ companion object {
+ val logger: Logger = LoggerFactory.getLogger(VelocityServerConnectService::class.java)
+ }
+
+ /** Sends the player directly to the initial server selected for an authenticated user. */
+ fun sendPlayerToInitialServer(player: Player) {
+ val initialServer = serverSelectionService.getInitialServer(player)
+ if (initialServer == null) {
+ logger.warn(
+ "sendPlayerToInitialServer: no initial server found for user '{}'",
+ player.username,
+ )
+ return
+ }
+
+ logger.debug(
+ "sendPlayerToInitialServer: sending user '{}' to initial server '{}'",
+ player.username,
+ initialServer.serverInfo.name,
+ )
+ player.createConnectionRequest(initialServer).connect()
+ }
+}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceChangePasswordAdminCommand.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceChangePasswordAdminCommand.kt
index 6b3c7f78..8de50f8d 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceChangePasswordAdminCommand.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceChangePasswordAdminCommand.kt
@@ -26,8 +26,11 @@ 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 org.slf4j.Logger
+import org.slf4j.LoggerFactory
import pl.spcode.navauth.common.annotation.Description
import pl.spcode.navauth.common.application.credentials.UserCredentialsService
+import pl.spcode.navauth.common.application.credentials.queue.EncryptionTaskAlreadyQueuedException
import pl.spcode.navauth.common.application.user.UserService
import pl.spcode.navauth.common.command.user.UserArgumentResolver
import pl.spcode.navauth.common.command.user.UsernameOrUuidRaw
@@ -45,6 +48,8 @@ constructor(
val multification: VelocityMultification,
) {
+ private val logger: Logger = LoggerFactory.getLogger(ForceChangePasswordAdminCommand::class.java)
+
@Async
@Execute
@Description(
@@ -65,10 +70,22 @@ constructor(
return
}
- userCredentialsService.updatePassword(user, password)
- multification
- .create(sender) { it.multification.adminCmdPasswordSetSuccess }
- .placeholder("%USERNAME%", user.username.value)
- .send()
+ multification.create(sender) { it.multification.adminCmdPasswordSetUpdating }.send()
+
+ try {
+ userCredentialsService.updatePassword(user, password).whenComplete { _, throwable ->
+ if (throwable != null) {
+ logger.error("Failed to update password for user '${user.username.value}'", throwable)
+ multification.send(sender) { it.multification.unexpectedErrorOccurred }
+ } else {
+ multification
+ .create(sender) { it.multification.adminCmdPasswordSetSuccess }
+ .placeholder("%USERNAME%", user.username.value)
+ .send()
+ }
+ }
+ } catch (_: EncryptionTaskAlreadyQueuedException) {
+ multification.send(sender) { it.multification.processAlreadyInProgressError }
+ }
}
}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/configurer/CommandConfigurer.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/configurer/CommandConfigurer.kt
new file mode 100644
index 00000000..5a6ec875
--- /dev/null
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/configurer/CommandConfigurer.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.configurer
+
+import com.google.inject.Inject
+import com.velocitypowered.api.command.CommandSource
+import dev.rollczi.litecommands.command.builder.CommandBuilder
+import dev.rollczi.litecommands.editor.Editor
+import pl.spcode.navauth.common.config.CommandsConfig
+
+class CommandConfigurer @Inject constructor(private val commandConfiguration: CommandsConfig) :
+ Editor {
+
+ override fun edit(context: CommandBuilder): CommandBuilder {
+ val command = commandConfiguration.commands[context.name()] ?: return context
+
+ var newContext = context
+
+ if (!command.name.isNullOrEmpty()) {
+ newContext = context.name(command.name)
+ }
+
+ val aliasesFiltered = command.aliases.filter { it.isNotEmpty() }
+
+ return newContext.aliases(aliasesFiltered).enabled(command.enabled)
+ }
+}
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/user/ChangePasswordCommand.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/user/ChangePasswordCommand.kt
index 9c3804bd..41d5316d 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/user/ChangePasswordCommand.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/user/ChangePasswordCommand.kt
@@ -26,6 +26,9 @@ 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 java.util.concurrent.CompletableFuture
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
import pl.spcode.navauth.common.annotation.Description
import pl.spcode.navauth.common.application.credentials.UserCredentialsService
import pl.spcode.navauth.common.application.credentials.queue.EncryptionTaskAlreadyQueuedException
@@ -43,6 +46,10 @@ constructor(
val multification: VelocityMultification,
) {
+ companion object {
+ private val logger: Logger = LoggerFactory.getLogger(javaClass)
+ }
+
@Async
@Execute
@Description("Changes account password to new one. Requires current password.")
@@ -52,6 +59,12 @@ constructor(
@Arg(value = "new_password") newPassword: String,
) {
val user = userService.findUserByExactUsername(sender.username)!!
+
+ if (user.isPremium) {
+ multification.send(sender) { it.multification.accountNotNonPremiumError }
+ return
+ }
+
val credentials = userCredentialsService.findCredentials(user)!!
if (!credentials.isPasswordRequired) {
@@ -62,18 +75,23 @@ constructor(
try {
userCredentialsService
.enqueueVerifyPassword(credentials, currentPassword, sender.uniqueId)
- .whenComplete { isCorrect, throwable ->
- if (throwable != null) {
- multification.send(sender) { it.multification.unexpectedErrorOccurred }
- return@whenComplete
- }
+ .thenCompose { isCorrect ->
if (!isCorrect) {
multification.send(sender) { it.multification.wrongCredentialsError }
- return@whenComplete
+ CompletableFuture.completedFuture(false)
+ } else {
+ userCredentialsService.updatePassword(user, newPassword).thenApply { true }
}
-
- userCredentialsService.updatePassword(user, newPassword)
- multification.send(sender) { it.multification.newPasswordSetSuccess }
+ }
+ .thenAccept { updated ->
+ if (updated) {
+ multification.send(sender) { it.multification.newPasswordSetSuccess }
+ }
+ }
+ .exceptionally { ex ->
+ logger.error("Unexpected error occurred while trying to change user password", ex)
+ multification.send(sender) { it.multification.unexpectedErrorOccurred }
+ null
}
} catch (_: EncryptionTaskAlreadyQueuedException) {
multification.send(sender) { it.multification.processAlreadyInProgressError }
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/user/PremiumAccountCommand.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/user/PremiumAccountCommand.kt
index 581ee9ef..b018c549 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/user/PremiumAccountCommand.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/user/PremiumAccountCommand.kt
@@ -73,6 +73,6 @@ constructor(
}
userService.migrateToPremium(user, mojangProfile.uuid)
- multification.create(sender) { it.multification.accountMigrationSuccess }
+ multification.create(sender) { it.multification.accountMigrationSuccess }.send()
}
}
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 f557f641..633c9b5c 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
@@ -27,7 +27,7 @@ import pl.spcode.navauth.common.config.GeneralConfig
import pl.spcode.navauth.common.config.MessagesConfig
import pl.spcode.navauth.common.domain.credentials.UserCredentials
import pl.spcode.navauth.common.infra.auth.LoginAuthSession
-import pl.spcode.navauth.velocity.application.event.VelocityEventDispatcher
+import pl.spcode.navauth.velocity.application.server.VelocityServerConnectService
import pl.spcode.navauth.velocity.extension.PlayerDisconnectExtension.Companion.disconnectIfActive
import pl.spcode.navauth.velocity.infra.player.VelocityPlayerAdapter
import pl.spcode.navauth.velocity.multification.VelocityMultification
@@ -38,7 +38,7 @@ class VelocityLoginAuthSession(
userCredentials: UserCredentials,
userCredentialsService: UserCredentialsService,
scheduler: NavAuthScheduler,
- private val velocityEventDispatcher: VelocityEventDispatcher,
+ private val serverConnectService: VelocityServerConnectService,
private val multification: VelocityMultification,
generalConfig: GeneralConfig,
private val messagesConfig: MessagesConfig,
@@ -92,7 +92,7 @@ class VelocityLoginAuthSession(
.notice(messagesConfig.multification.loginSuccess)
.player(player.uniqueId)
.send()
- velocityEventDispatcher.fireVelocityChooseInitialServerEventAsync(player)
+ serverConnectService.sendPlayerToInitialServer(player)
}
override fun onInvalidate() {
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 73f77bb0..feee7292 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
@@ -25,7 +25,7 @@ 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.infra.auth.RegisterAuthSession
-import pl.spcode.navauth.velocity.application.event.VelocityEventDispatcher
+import pl.spcode.navauth.velocity.application.server.VelocityServerConnectService
import pl.spcode.navauth.velocity.extension.PlayerDisconnectExtension.Companion.disconnectIfActive
import pl.spcode.navauth.velocity.infra.player.VelocityPlayerAdapter
import pl.spcode.navauth.velocity.multification.VelocityMultification
@@ -34,7 +34,7 @@ import pl.spcode.navauth.velocity.scheduler.NavAuthScheduler
class VelocityRegisterAuthSession(
val player: Player,
scheduler: NavAuthScheduler,
- val velocityEventDispatcher: VelocityEventDispatcher,
+ val serverConnectService: VelocityServerConnectService,
val multification: VelocityMultification,
val messagesConfig: MessagesConfig,
val generalConfig: GeneralConfig,
@@ -78,7 +78,7 @@ class VelocityRegisterAuthSession(
.notice(messagesConfig.multification.registerSuccess)
.player(player.uniqueId)
.send()
- velocityEventDispatcher.fireVelocityChooseInitialServerEventAsync(player)
+ serverConnectService.sendPlayerToInitialServer(player)
}
override fun onInvalidate() {
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/ConnectionListeners.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/ConnectionListeners.kt
index f7b15caa..f2c778cf 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/ConnectionListeners.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/ConnectionListeners.kt
@@ -19,7 +19,6 @@
package pl.spcode.navauth.velocity.listener.velocity
import com.google.inject.Inject
-import com.velocitypowered.api.event.PostOrder
import com.velocitypowered.api.event.Subscribe
import com.velocitypowered.api.event.connection.DisconnectEvent
import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent
@@ -59,8 +58,9 @@ constructor(
userActivitySessionService.storePlayerSessionOnLeave(VelocityPlayerAdapter(event.player))
}
- @Subscribe(order = PostOrder.FIRST)
- fun onServerConnect(event: ServerPreConnectEvent) {
+ /** Priority is Short.MIN_VALUE to always have the final say */
+ @Subscribe(priority = Short.MIN_VALUE)
+ fun onServerPreConnect(event: ServerPreConnectEvent) {
val player = event.player
disconnectOnUnexpectedError(player) {
val uniqueSessionId = VelocityUniqueSessionId(player)
@@ -109,7 +109,8 @@ constructor(
}
}
- @Subscribe(order = PostOrder.FIRST)
+ /** Priority is Short.MIN_VALUE to always have the final say */
+ @Subscribe(priority = Short.MIN_VALUE)
fun onPlayerChooseInitialServer(event: PlayerChooseInitialServerEvent) {
val player = event.player
disconnectOnUnexpectedError(player) {
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 366ff91a..411ff62e 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
@@ -123,6 +123,9 @@ constructor(
is UsernameResFailureReason.UsernameMigrationFailedUsernameAlreadyTaken -> {
usernameMigrationFailedUsernameAlreadyTakenConflictResult(failureReason.username)
}
+ is UsernameResFailureReason.ProfileAPIFailure -> {
+ profileApiFailureKickResult()
+ }
}
}
}
@@ -307,6 +310,11 @@ constructor(
return PreLoginEvent.PreLoginComponentResult.denied(comp)
}
+ private fun profileApiFailureKickResult(): PreLoginEvent.PreLoginComponentResult {
+ val comp = withSupportFooter(messagesConfig.profileApiFailureKickMessage.toComponent())
+ return PreLoginEvent.PreLoginComponentResult.denied(comp)
+ }
+
private fun invalidSessionReconnectDeniedResult(
username: String
): PreLoginEvent.PreLoginComponentResult {
diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/module/VelocityServicesModule.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/module/VelocityServicesModule.kt
index 9c7c5aa9..4ed6ceac 100644
--- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/module/VelocityServicesModule.kt
+++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/module/VelocityServicesModule.kt
@@ -21,7 +21,7 @@ package pl.spcode.navauth.velocity.module
import com.google.inject.AbstractModule
import com.google.inject.Singleton
import pl.spcode.navauth.velocity.application.auth.session.VelocityAuthSessionFactory
-import pl.spcode.navauth.velocity.application.event.VelocityEventDispatcher
+import pl.spcode.navauth.velocity.application.server.VelocityServerConnectService
import pl.spcode.navauth.velocity.application.server.VelocityServerSelectionService
class VelocityServicesModule : AbstractModule() {
@@ -31,6 +31,6 @@ class VelocityServicesModule : AbstractModule() {
bind(VelocityServerSelectionService::class.java).`in`(Singleton::class.java)
- bind(VelocityEventDispatcher::class.java).`in`(Singleton::class.java)
+ bind(VelocityServerConnectService::class.java).`in`(Singleton::class.java)
}
}