Skip to content

Commit 18a98ec

Browse files
Anty0claude
andcommitted
fix: improve handling of disabled accounts during login and sign-up
Check for disabled accounts before throwing generic errors. Login now returns a specific error instead of "bad credentials", and sign-up detects disabled accounts instead of causing a constraint violation. Closes #3276 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e144a04 commit 18a98ec

File tree

9 files changed

+68
-2
lines changed

9 files changed

+68
-2
lines changed

backend/app/src/test/kotlin/io/tolgee/controllers/PublicControllerTest.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.tolgee.controllers
33
import com.posthog.server.PostHog
44
import io.tolgee.dtos.misc.CreateProjectInvitationParams
55
import io.tolgee.dtos.request.auth.SignUpDto
6+
import io.tolgee.fixtures.andAssertError
67
import io.tolgee.fixtures.andAssertResponse
78
import io.tolgee.fixtures.andIsBadRequest
89
import io.tolgee.fixtures.andIsOk
@@ -130,6 +131,50 @@ class PublicControllerTest : AbstractControllerTest() {
130131
performPost("/api/public/sign_up", dto).andIsUnauthorized
131132
}
132133

134+
@Test
135+
fun `returns error when signing up with disabled account email`() {
136+
val dto =
137+
SignUpDto(
138+
name = "Pavel Novak",
139+
password = "aaaaaaaaa",
140+
email = "disabled@test.com",
141+
)
142+
performPost("/api/public/sign_up", dto).andIsOk
143+
144+
val user = userAccountService.findActive("disabled@test.com")!!
145+
userAccountService.disable(user.id)
146+
147+
val dto2 =
148+
SignUpDto(
149+
name = "Another User",
150+
password = "bbbbbbbbb",
151+
email = "disabled@test.com",
152+
)
153+
performPost("/api/public/sign_up", dto2)
154+
.andIsBadRequest
155+
.andAssertError
156+
.hasCode("user_account_disabled")
157+
}
158+
159+
@Test
160+
fun `returns error when logging in with disabled account`() {
161+
val dto =
162+
SignUpDto(
163+
name = "Login Disabled",
164+
password = "aaaaaaaaa",
165+
email = "login-disabled@test.com",
166+
)
167+
performPost("/api/public/sign_up", dto).andIsOk
168+
169+
val user = userAccountService.findActive("login-disabled@test.com")!!
170+
userAccountService.disable(user.id)
171+
172+
doAuthentication("login-disabled@test.com", "aaaaaaaaa")
173+
.andIsUnauthorized
174+
.andAssertError
175+
.hasCode("user_account_disabled")
176+
}
177+
133178
@Test
134179
fun testSignUpValidationBlankEmail() {
135180
val dto = SignUpDto(name = "Pavel Novak", password = "aaaa", email = "")

backend/data/src/main/kotlin/io/tolgee/constants/Message.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ enum class Message {
147147
CANNOT_SET_VIEW_LANGUAGES_WITHOUT_FOR_LEVEL_BASED_PERMISSIONS,
148148
CANNOT_SET_DIFFERENT_TRANSLATE_AND_STATE_CHANGE_LANGUAGES_FOR_LEVEL_BASED_PERMISSIONS,
149149
CANNOT_DISABLE_YOUR_OWN_ACCOUNT,
150+
USER_ACCOUNT_DISABLED,
150151
SUBSCRIPTION_NOT_FOUND,
151152
INVOICE_DOES_NOT_HAVE_USAGE,
152153
CUSTOMER_NOT_FOUND,

backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ interface UserAccountRepository : JpaRepository<UserAccount, Long> {
110110
@Query("from UserAccount ua where ua.id = :id and ua.deletedAt is null and ua.disabledAt is null")
111111
fun findActive(id: Long): UserAccount?
112112

113+
@Query("from UserAccount ua where ua.username = :username and ua.deletedAt is null")
114+
fun findActiveOrDisabled(username: String): UserAccount?
115+
113116
@Query("from UserAccount ua left join fetch ua.emailVerification where ua.isInitialUser = true")
114117
fun findInitialUser(): UserAccount?
115118

backend/data/src/main/kotlin/io/tolgee/security/thirdParty/ThirdPartyUserHandler.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ class ThirdPartyUserHandler(
116116
}
117117

118118
private fun createUser(data: ThirdPartyUserDetails): UserAccount {
119-
userAccountService.findActive(data.username)?.let {
119+
userAccountService.findActiveOrDisabled(data.username)?.let {
120+
if (it.disabledAt != null) {
121+
throw AuthenticationException(Message.USER_ACCOUNT_DISABLED)
122+
}
120123
throw AuthenticationException(Message.USERNAME_ALREADY_EXISTS)
121124
}
122125

backend/data/src/main/kotlin/io/tolgee/service/security/SignUpService.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ class SignUpService(
2727
) {
2828
@Transactional
2929
fun signUp(dto: SignUpDto): JwtAuthenticationResponse? {
30-
userAccountService.findActive(dto.email)?.let {
30+
userAccountService.findActiveOrDisabled(dto.email)?.let {
31+
if (it.disabledAt != null) {
32+
throw BadRequestException(Message.USER_ACCOUNT_DISABLED)
33+
}
3134
throw BadRequestException(Message.USERNAME_ALREADY_EXISTS)
3235
}
3336

backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ class UserAccountService(
9292
return userAccountRepository.findActive(username)
9393
}
9494

95+
fun findActiveOrDisabled(username: String): UserAccount? {
96+
return userAccountRepository.findActiveOrDisabled(username)
97+
}
98+
9599
operator fun get(username: String): UserAccount {
96100
return this.findActive(username) ?: throw NotFoundException(Message.USER_NOT_FOUND)
97101
}

backend/data/src/main/kotlin/io/tolgee/service/security/UserCredentialsService.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class UserCredentialsService(
2020
): UserAccount {
2121
val userAccount = userAccountService.findActive(username)
2222
if (userAccount == null) {
23+
userAccountService.findActiveOrDisabled(username)?.let {
24+
throw AuthenticationException(Message.USER_ACCOUNT_DISABLED)
25+
}
2326
throw AuthenticationException(Message.BAD_CREDENTIALS)
2427
}
2528

webapp/src/service/apiSchema.generated.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2717,6 +2717,7 @@ export interface components {
27172717
| "cannot_set_view_languages_without_for_level_based_permissions"
27182718
| "cannot_set_different_translate_and_state_change_languages_for_level_based_permissions"
27192719
| "cannot_disable_your_own_account"
2720+
| "user_account_disabled"
27202721
| "subscription_not_found"
27212722
| "invoice_does_not_have_usage"
27222723
| "customer_not_found"
@@ -6092,6 +6093,7 @@ export interface components {
60926093
| "cannot_set_view_languages_without_for_level_based_permissions"
60936094
| "cannot_set_different_translate_and_state_change_languages_for_level_based_permissions"
60946095
| "cannot_disable_your_own_account"
6096+
| "user_account_disabled"
60956097
| "subscription_not_found"
60966098
| "invoice_does_not_have_usage"
60976099
| "customer_not_found"

webapp/src/translationTools/useErrorTranslation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export function useErrorTranslation() {
1111

1212
case 'bad_credentials':
1313
return t('bad_credentials');
14+
case 'user_account_disabled':
15+
return t('user_account_disabled');
1416
case 'invalid_otp_code':
1517
return t('invalid_otp_code');
1618
case 'invitation_code_does_not_exist_or_expired':

0 commit comments

Comments
 (0)