Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ build/
!**/src/test/**/build/

### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
.idea/AndroidProjectSystem.xml
.idea/
*.iws
*.iml
*.ipr
Expand Down
2 changes: 1 addition & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file modified gradlew
100644 → 100755
Empty file.
Empty file modified gradlew.bat
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ constructor(private val userService: UserService, private val profileService: Pr
// check if the letter case changed
if (
correspondingPremiumProfile.name != existingUserIgnoreCase.username &&
correspondingPremiumProfile.name.value.equals(existingUserIgnoreCase.username.value, true)
correspondingPremiumProfile.name.value.equals(
existingUserIgnoreCase.username.value,
true,
)
) {
userService.migrateUsername(existingUserIgnoreCase, correspondingPremiumProfile.name)
return success(
Expand All @@ -87,7 +90,13 @@ constructor(private val userService: UserService, private val profileService: Pr

if (existingUserIgnoreCase != null && !existingUserIgnoreCase.isPremium) {
if (isPremiumNickname) {
return if (existingUserIgnoreCase.username == correspondingPremiumProfile.name) {
// Check if usernames match case-insensitively (allow case variants as same account)
val sameNameIgnoreCase =
existingUserIgnoreCase.username.value.equals(
correspondingPremiumProfile.name.value,
ignoreCase = true,
)
return if (sameNameIgnoreCase) {
success(
EncryptionType.NONE,
PostUsernameResolutionState.NONPREMIUM_WITH_SAME_PREMIUM_NICKNAME,
Expand Down Expand Up @@ -131,10 +140,8 @@ constructor(private val userService: UserService, private val profileService: Pr
)
)
}
return success(EncryptionType.ENFORCE_PREMIUM, PostUsernameResolutionState.NEW_ACCOUNT)
} else {
return success(EncryptionType.NONE, PostUsernameResolutionState.NEW_ACCOUNT)
}
return success(EncryptionType.NONE, PostUsernameResolutionState.NEW_ACCOUNT)
}

throw IllegalStateException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,12 @@ constructor(
*
* @param user the user to whom the provided credentials belong
* @param userCredentials the credentials to be stored for the user
* @throws IllegalArgumentException if the provided credentials do not match the user or if the
* user does not require credentials
* @throws IllegalArgumentException if the provided credentials do not match the user
*/
fun storeUserCredentials(user: User, userCredentials: UserCredentials) {
require(userCredentials.userUuid == user.uuid) {
"provided credentials do not belong to the provided user"
}
require(user.credentialsRequired) {
"cannot store credentials for a user without credentials required property"
}

credentialsRepository.save(userCredentials)
}
Expand All @@ -75,12 +71,8 @@ constructor(
* Deletes the credentials associated with the given user.
*
* @param user the user whose credentials are to be deleted
* @throws IllegalArgumentException if the user has the `credentialsRequired` property set to true
*/
fun deleteUserCredentials(user: User) {
require(!user.credentialsRequired) {
"cannot delete credentials for a user with credentials required property"
}
credentialsRepository.deleteByUser(user)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ constructor(
}

fun createAndStoreUserWithNewCredentials(user: User, password: HashedPassword) {
require(user.credentialsRequired) { "cannot store user without credentials required property" }

txService.inTransaction {
userRepository.save(user)
userCredentialsService.storeUserCredentials(
Expand All @@ -83,34 +81,25 @@ constructor(
}

private fun deleteUserCredentialsUpdateUserNoTx(user: User): User {
val user = user.withCredentialsRequired(false)
userRepository.save(user)
userCredentialsService.deleteUserCredentials(user)
return user
}

/**
* Migrates a non-premium user account to a premium account using the provided Mojang ID. This
* updates the user's data and ensures proper handling of authentication credentials.
* updates the user's data and marks the user as premium. The user keeps their password.
*
* @param user The non-premium user to be migrated to a premium account.
* @param mojangId The Mojang ID associated with the premium account to link to the user.
* @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)
val premiumUser = User.premium(user.uuid, user.username, mojangId)

userRepository.save(premiumUser)
if (requireCredentials) {
val newCredentials = credentials.withoutPassword()
userCredentialsService.storeUserCredentials(premiumUser, newCredentials)
} else {
userCredentialsService.deleteUserCredentials(premiumUser)
}
// User keeps their password, no credential changes needed

return@inTransaction premiumUser
}
Expand All @@ -127,23 +116,15 @@ constructor(
* context.
*
* @param user The user to be migrated, which must be a premium user.
* @param newPassword The new hashed password to set for the user.
* @return The updated user with non-premium status and required credentials.
* @throws IllegalArgumentException if the user is already a premium user.
*/
fun migrateToNonPremium(user: User, newPassword: HashedPassword): User {
require(user.isPremium) { "cannot migrate non-premium user to non-premium" }
fun migrateToNonPremium(user: User): User {
require(user.isPremium) { "cannot migrate premium user to non-premium" }

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)

return@inTransaction nonPremiumUser
}

Expand Down Expand Up @@ -185,7 +166,7 @@ constructor(
* @return The updated user with the new username.
* @throws UsernameAlreadyTakenException if another user already takes the new username.
* @throws IllegalArgumentException If the user is a premium user, or the new username belongs to
* a premium profile.
* a premium user.
*/
fun migrateData(user: User, newUsername: Username): User {
require(!user.isPremium) { "cannot migrate premium user data" }
Expand Down Expand Up @@ -226,31 +207,18 @@ constructor(
txService.inTransaction {
val credentials =
userCredentialsService.findCredentials(user)
?: UserCredentials.create(user, null, totpSecret)

val userWithCredentials =
if (!user.credentialsRequired) {
val user = user.withCredentialsRequired()
userRepository.save(user)
user
} else {
user
}
?: throw IllegalStateException("User must have credentials before enabling 2FA")

val newCredentials = credentials.withTotpSecret(totpSecret)
userCredentialsService.storeUserCredentials(userWithCredentials, newCredentials)
userCredentialsService.storeUserCredentials(user, newCredentials)
}
}

fun disableTwoFactorAuth(user: User) {
txService.inTransaction {
val credentials = userCredentialsService.findCredentials(user)!!
if (credentials.isPasswordRequired) {
val newCredentials = credentials.withoutTotpSecret()
userCredentialsService.storeUserCredentials(user, newCredentials)
} else {
deleteUserCredentialsUpdateUserNoTx(user)
}
val newCredentials = credentials.withoutTotpSecret()
userCredentialsService.storeUserCredentials(user, newCredentials)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@ open class MessagesConfig : OkaeriConfig() {
var loginTooManyAttemptsError =
TextComponent("<red>Too many login attempts. Please try again later.")

var adminCopyPasswordText =
"<aqua><bold><click:copy_to_clipboard:%PASSWORD%>CLICK HERE TO COPY</click>"

var yourAccountDataHasBeenMigrated =
TextComponent("<green>Your account data has been migrated to '%USERNAME%'.")

Expand All @@ -97,29 +94,24 @@ open class MessagesConfig : OkaeriConfig() {
var processAlreadyInProgressError: Notice =
Notice.chat("<red>Process is already in progress. Please wait...")

var passwordRequiredError: Notice = Notice.chat("<red>Please provide your current password.")
var twoFactorAlreadyEnabledError: Notice =
Notice.chat("<red>Your account has 2FA enabled already!")

var missingPermissionError: Notice =
Notice.chat("<red>You don't have permission to execute this command.")
var invalidUsageError: Notice = Notice.chat("<red>Invalid command usage!")

@Comment("Invalid usage scheme line (single text component only).")
var invalidUsageLine: TextComponent = TextComponent("<gray> • %SCHEME%")

var cantUseThisCommandNowError: Notice = Notice.chat("<red>Can't use this command right now.")
var commandPasswordNotSetForAccountError: Notice =
Notice.chat("<red>Can't execute this command right now: your account has no password set.")

var commandNoPremiumAccountWithUsername: Notice =
Notice.chat(
"<red>Can't set this account as premium because there's no premium account with username '%USERNAME%'."
)
var accountAlreadyPremiumError: Notice =
Notice.chat("<red>Account is already set as a premium one.")
var accountNotNonPremiumError: Notice =
Notice.chat(
"<red>Can't execute this command right now: your account is not a non-premium one."
)

var alreadyTryingToLoginError: Notice =
Notice.chat(
Expand All @@ -134,8 +126,7 @@ open class MessagesConfig : OkaeriConfig() {

var loginPasswordOnlyInstruction: Notice =
Notice.chat("<green>Please login using \"/login <password>\" command.</green>")
var loginTwoFactorOnlyInstruction: Notice =
Notice.chat("<green>Please login using \"/2fa <code>\" command.</green>")

var loginPasswordAndTwoFactorInstruction: Notice =
Notice.chat("<green>Please login using \"/login <password> <2fa_code>\" command.</green>")
var loggingInInfo: Notice = Notice.chat("<yellow>Logging in, please wait...</yellow>")
Expand Down Expand Up @@ -213,9 +204,7 @@ open class MessagesConfig : OkaeriConfig() {
var adminCmdPasswordSetSuccess: Notice =
Notice.chat("<green>Success! User '%USERNAME%' password was set.")
var adminCmdAccountMigratedToNonPremiumSuccess: Notice =
Notice.chat(
"<green>User '%USERNAME%' has been successfully migrated to non-premium mode. Their new password is: %PASSWORD_TEXT%"
)
Notice.chat("<green>User '%USERNAME%' has been successfully migrated to non-premium mode.")
var adminCmdUserDataMigratedSuccess: Notice =
Notice.chat(
"<green>Success! User '%OLD_USERNAME%' data has been migrated to '%NEW_USERNAME%'."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,9 @@ private constructor(
) {

init {
require(isValid()) { "Credentials must have either password or TOTP secret" }
require(isValid()) { "Credentials must have a password" }
}

val isPasswordRequired: Boolean
get() = hashedPassword != null

val isTwoFactorEnabled: Boolean
get() = totpSecret != null

Expand All @@ -68,18 +65,11 @@ private constructor(

fun withNewPassword(password: HashedPassword): UserCredentials = copy(hashedPassword = password)

fun withoutPassword(): UserCredentials {
require(totpSecret != null) {
"to create credentials without password, at least totpSecret must be set"
}
return copy(hashedPassword = null)
}

fun withTotpSecret(totpSecret: TOTPSecret): UserCredentials = copy(totpSecret = totpSecret)

fun withoutTotpSecret(): UserCredentials = copy(totpSecret = null)

fun isValid(): Boolean {
return hashedPassword != null || totpSecret != null
return hashedPassword != null // Must have password, TOTP is optional
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,33 +41,26 @@ data class User
private constructor(
val uuid: UserUuid,
val username: Username,
val credentialsRequired: Boolean,
val mojangUuid: MojangId? = null, // null = non-premium
) {
val isPremium: Boolean
get() = mojangUuid != null

companion object Factory {
fun nonPremium(id: UserUuid, username: Username): User = User(id, username, true)
fun nonPremium(id: UserUuid, username: Username): User = User(id, username)

fun premium(
id: UserUuid,
username: Username,
mojangUuid: MojangId,
requiresCredentials: Boolean = false,
): User = User(id, username, requiresCredentials, mojangUuid)
fun premium(id: UserUuid, username: Username, mojangUuid: MojangId): User =
User(id, username, mojangUuid)
}

fun withNewUsername(username: Username): User {
return User(this.uuid, username, this.credentialsRequired, this.mojangUuid)
return User(this.uuid, username, this.mojangUuid)
}

fun toNonPremium(): User {
return copy(mojangUuid = null, credentialsRequired = true)
return copy(mojangUuid = null)
}

fun withCredentialsRequired(required: Boolean = true) = copy(credentialsRequired = required)

fun toAuthUser(): AuthUser {
return object : AuthUser {
override fun getUUID(): UUID {
Expand All @@ -85,6 +78,6 @@ private constructor(
}

override fun toString(): String {
return "User(id=$uuid, username=$username, mojangUuid=$mojangUuid, credentialsRequired=$credentialsRequired)"
return "User(id=$uuid, username=$username, mojangUuid=$mojangUuid)"
}
}
Loading