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.

[![Velocity](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/supported/velocity_vector.svg)](https://modrinth.com/plugin/navauth) - [![Modrinth](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/modrinth_vector.svg)](https://modrinth.com/plugin/navauth) [![Github](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/github_vector.svg)](https://github.com/Navio1430/NavAuth) +[//]: # ( [![Modrinth](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/modrinth_vector.svg)](https://modrinth.com/plugin/navauth)) + [![Gradle](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/built-with/gradle_vector.svg)](https://gradle.org/) [![Kotlin](assets/built_with_kotlin.svg)](https://kotlinlang.org/docs/home.html) - ![Java](assets/built_with_java.svg) [![Documentation](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/documentation/generic_vector.svg)](https://navio1430.github.io/NavAuth/docs) [![Kotlin](assets/read_javadoc.svg)](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 [![GNU AGPL Logo](https://www.gnu.org/graphics/agplv3-155x51.png)](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) } }