Skip to content

Commit bc3e16d

Browse files
authored
이메일 인덱스 추가 및 collation 사용 (#517)
1 parent 17a3fe1 commit bc3e16d

4 files changed

Lines changed: 52 additions & 20 deletions

File tree

core/src/main/kotlin/users/data/User.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@ import org.springframework.data.mongodb.core.index.Indexed
66
import org.springframework.data.mongodb.core.mapping.Document
77
import java.time.LocalDateTime
88

9+
const val EMAIL_CASE_INSENSITIVE_COLLATION = "{ 'locale': 'en', 'strength': 2 }"
10+
911
@Document("users")
1012
data class User(
1113
@Id
1214
val id: String? = null,
15+
@Indexed(
16+
unique = true,
17+
partialFilter = $$"{ 'active': true, 'isEmailVerified': true, 'email': { '$type': 'string' } }",
18+
collation = EMAIL_CASE_INSENSITIVE_COLLATION,
19+
)
1320
var email: String?,
1421
@Indexed(unique = true, sparse = true)
1522
var nickname: String,

core/src/main/kotlin/users/repository/UserRepository.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.wafflestudio.snutt.users.repository
22

3+
import com.wafflestudio.snutt.users.data.EMAIL_CASE_INSENSITIVE_COLLATION
34
import com.wafflestudio.snutt.users.data.User
45
import kotlinx.coroutines.flow.Flow
6+
import org.springframework.data.mongodb.repository.Query
57
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
68

79
interface UserRepository : CoroutineCrudRepository<User, String> {
@@ -30,10 +32,23 @@ interface UserRepository : CoroutineCrudRepository<User, String> {
3032

3133
suspend fun findAllByIdInAndActiveTrue(ids: List<String>): List<User>
3234

33-
suspend fun existsByEmailIgnoreCaseAndIsEmailVerifiedTrueAndActiveTrue(email: String): Boolean
34-
35-
suspend fun findByEmailIgnoreCaseAndIsEmailVerifiedTrueAndActiveTrue(email: String): User?
36-
35+
@Query(
36+
value = "{ 'email': ?0, 'isEmailVerified': true, 'active': true }",
37+
exists = true,
38+
collation = EMAIL_CASE_INSENSITIVE_COLLATION,
39+
)
40+
suspend fun existsByEmailAndIsEmailVerifiedTrueAndActiveTrue(email: String): Boolean
41+
42+
@Query(
43+
value = "{ 'email': ?0, 'isEmailVerified': true, 'active': true }",
44+
collation = EMAIL_CASE_INSENSITIVE_COLLATION,
45+
)
46+
suspend fun findByEmailAndIsEmailVerifiedTrueAndActiveTrue(email: String): User?
47+
48+
@Query(
49+
value = "{ 'email': ?0 }",
50+
collation = EMAIL_CASE_INSENSITIVE_COLLATION,
51+
)
3752
suspend fun findAllByEmail(email: String): List<User>
3853

3954
suspend fun existsByCredentialFbIdAndActiveTrue(fbId: String): Boolean

core/src/main/kotlin/users/service/AuthService.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,12 @@ class AuthServiceImpl(
6767

6868
override fun isValidPassword(password: String) = password.matches(passwordRegex)
6969

70-
override fun isValidEmail(email: String) = email.matches(emailRegex)
70+
override fun isValidEmail(email: String) = email.trim().matches(emailRegex)
7171

72-
override fun isValidSnuMail(email: String) = email.matches(emailRegex) && email.endsWith("@snu.ac.kr")
72+
override fun isValidSnuMail(email: String): Boolean {
73+
val email = email.trim()
74+
return email.matches(emailRegex) && email.endsWith("@snu.ac.kr", ignoreCase = true)
75+
}
7376

7477
override fun isMatchedPassword(
7578
user: User,

core/src/main/kotlin/users/service/UserService.kt

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import com.wafflestudio.snutt.users.repository.UserRepository
4040
import org.slf4j.LoggerFactory
4141
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding
4242
import org.springframework.context.ApplicationEventPublisher
43+
import org.springframework.dao.DuplicateKeyException
4344
import org.springframework.data.redis.core.ReactiveStringRedisTemplate
4445
import org.springframework.data.redis.core.getAndAwait
4546
import org.springframework.stereotype.Service
@@ -181,7 +182,7 @@ class UserServiceImpl(
181182
override suspend fun registerLocal(localRegisterRequest: LocalRegisterRequest): LoginResponse {
182183
val localId = localRegisterRequest.id
183184
val password = localRegisterRequest.password
184-
val email = localRegisterRequest.email
185+
val email = localRegisterRequest.email?.trim()
185186

186187
val cacheKey = CacheKey.LOCK_REGISTER_LOCAL.build(localId)
187188

@@ -191,7 +192,7 @@ class UserServiceImpl(
191192
if (!authService.isValidPassword(password)) throw InvalidPasswordException
192193
email?.let {
193194
if (!authService.isValidEmail(email)) throw InvalidEmailException
194-
userRepository.findByEmailIgnoreCaseAndIsEmailVerifiedTrueAndActiveTrue(email)?.let {
195+
userRepository.findByEmailAndIsEmailVerifiedTrueAndActiveTrue(email)?.let {
195196
throw DuplicateEmailException(getAttachedAuthProviders(it))
196197
}
197198
}
@@ -235,7 +236,7 @@ class UserServiceImpl(
235236
val credential = authService.buildFacebookCredential(oauth2UserResponse)
236237

237238
if (oauth2UserResponse.email != null) {
238-
userRepository.findByEmailIgnoreCaseAndIsEmailVerifiedTrueAndActiveTrue(oauth2UserResponse.email)?.let {
239+
userRepository.findByEmailAndIsEmailVerifiedTrueAndActiveTrue(oauth2UserResponse.email)?.let {
239240
throw DuplicateEmailException(getAttachedAuthProviders(it))
240241
}
241242
} else {
@@ -259,7 +260,7 @@ class UserServiceImpl(
259260
}
260261

261262
checkNotNull(oauth2UserResponse.email) { "google email is null: $oauth2UserResponse" }
262-
userRepository.findByEmailIgnoreCaseAndIsEmailVerifiedTrueAndActiveTrue(oauth2UserResponse.email)?.let {
263+
userRepository.findByEmailAndIsEmailVerifiedTrueAndActiveTrue(oauth2UserResponse.email)?.let {
263264
throw DuplicateEmailException(getAttachedAuthProviders(it))
264265
}
265266

@@ -282,7 +283,7 @@ class UserServiceImpl(
282283
}
283284

284285
checkNotNull(oauth2UserResponse.email) { "kakao email is null: $oauth2UserResponse" }
285-
userRepository.findByEmailIgnoreCaseAndIsEmailVerifiedTrueAndActiveTrue(oauth2UserResponse.email)?.let {
286+
userRepository.findByEmailAndIsEmailVerifiedTrueAndActiveTrue(oauth2UserResponse.email)?.let {
286287
throw DuplicateEmailException(getAttachedAuthProviders(it))
287288
}
288289

@@ -309,7 +310,7 @@ class UserServiceImpl(
309310
}
310311

311312
checkNotNull(oauth2UserResponse.email) { "apple email is null: $oauth2UserResponse" }
312-
userRepository.findByEmailIgnoreCaseAndIsEmailVerifiedTrueAndActiveTrue(oauth2UserResponse.email)?.let {
313+
userRepository.findByEmailAndIsEmailVerifiedTrueAndActiveTrue(oauth2UserResponse.email)?.let {
313314
throw DuplicateEmailException(getAttachedAuthProviders(it))
314315
}
315316

@@ -349,6 +350,7 @@ class UserServiceImpl(
349350
isEmailVerified: Boolean,
350351
): LoginResponse {
351352
val credentialHash = authService.generateCredentialHash(credential)
353+
val email = email?.trim()
352354

353355
val randomNickname = userNicknameService.generateUniqueRandomNickname()
354356

@@ -384,12 +386,10 @@ class UserServiceImpl(
384386
user: User,
385387
email: String,
386388
) {
389+
val email = email.trim()
387390
if (user.isEmailVerified == true) throw EmailAlreadyVerifiedException
388391
if (!authService.isValidSnuMail(email)) throw InvalidEmailException
389-
if (userRepository.existsByEmailIgnoreCaseAndIsEmailVerifiedTrueAndActiveTrue(
390-
email,
391-
)
392-
) {
392+
if (userRepository.existsByEmailAndIsEmailVerifiedTrueAndActiveTrue(email)) {
393393
throw DuplicateEmailException(getAttachedAuthProviders(user))
394394
}
395395
val key = VERIFICATION_CODE_PREFIX + user.id
@@ -408,7 +408,12 @@ class UserServiceImpl(
408408
email = value.email
409409
isEmailVerified = true
410410
}
411-
userRepository.save(user)
411+
try {
412+
userRepository.save(user)
413+
} catch (e: DuplicateKeyException) {
414+
val presentUser = userRepository.findByEmailAndIsEmailVerifiedTrueAndActiveTrue(value.email)
415+
throw DuplicateEmailException(getAttachedAuthProviders(presentUser ?: user))
416+
}
412417
redisTemplate.delete(key).subscribe()
413418
}
414419

@@ -445,7 +450,7 @@ class UserServiceImpl(
445450
val token = socialLoginRequest.token
446451
val oauth2UserResponse = authService.socialLoginWithAccessToken(authProvider, token)
447452
if (oauth2UserResponse.email != null) {
448-
val presentUser = userRepository.findByEmailIgnoreCaseAndIsEmailVerifiedTrueAndActiveTrue(oauth2UserResponse.email)
453+
val presentUser = userRepository.findByEmailAndIsEmailVerifiedTrueAndActiveTrue(oauth2UserResponse.email)
449454
if (presentUser != null && presentUser.id != user.id) {
450455
throw DuplicateEmailException(getAttachedAuthProviders(presentUser))
451456
}
@@ -572,6 +577,7 @@ class UserServiceImpl(
572577
}
573578

574579
override suspend fun sendLocalIdToEmail(email: String) {
580+
val email = email.trim()
575581
val users = userRepository.findAllByEmail(email).filter { it.active }
576582
if (users.isEmpty()) throw UserNotFoundException
577583

@@ -608,8 +614,9 @@ class UserServiceImpl(
608614
}
609615

610616
override suspend fun sendResetPasswordCode(email: String) {
617+
val email = email.trim()
611618
if (email.replace(emailMaskRegex, "*") == email) throw UpdateAppVersionException
612-
val user = userRepository.findByEmailIgnoreCaseAndIsEmailVerifiedTrueAndActiveTrue(email) ?: throw UserNotFoundException
619+
val user = userRepository.findByEmailAndIsEmailVerifiedTrueAndActiveTrue(email) ?: throw UserNotFoundException
613620
val key = RESET_PASSWORD_CODE_PREFIX + user.id
614621
val code = Base64.getUrlEncoder().encodeToString(Random.nextBytes(6))
615622
saveNewVerificationValue(email, code, key)
@@ -648,7 +655,7 @@ class UserServiceImpl(
648655
redisTemplate.delete(RESET_PASSWORD_CODE_PREFIX + user.id).subscribe()
649656
}
650657

651-
override suspend fun getUsersByEmail(email: String): List<User> = userRepository.findAllByEmail(email)
658+
override suspend fun getUsersByEmail(email: String): List<User> = userRepository.findAllByEmail(email.trim())
652659

653660
private suspend fun saveNewVerificationValue(
654661
email: String,

0 commit comments

Comments
 (0)