Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
10 commits
Select commit Hold shift + click to select a range
cc5349d
feat: νšŒμ› νƒˆν‡΄ ν›„ 처리 λ‘œμ§μ— νŠΈλžœμž­μ…˜ 동기화 등둝 μΆ”κ°€
popeye0618 May 14, 2026
811390e
test: νŠΈλžœμž­μ…˜ 동기화 μ—†λŠ” νƒˆν‡΄ ν›„μ²˜λ¦¬ ν…ŒμŠ€νŠΈ μΆ”κ°€
popeye0618 May 14, 2026
7695a7a
test: RefreshToken Repository의 PESSIMISTIC_WRITE 잠금 λ™μž‘ ν…ŒμŠ€νŠΈ μΆ”κ°€
popeye0618 May 14, 2026
b6317ad
feat: 이메일 μ•”ν˜Έν™” 및 λ³΅ν˜Έν™” 둜직 μΆ”κ°€
popeye0618 May 14, 2026
f79f27a
feat: 학ꡐ 이메일 인증 μš”μ²­ μ‹œ 학ꡐ 인증 λŒ€κΈ° μƒνƒœ 검증 둜직 μΆ”κ°€
popeye0618 May 14, 2026
5d99fcb
feat: νŠΈλžœμž­μ…˜ 컀밋 ν›„ 이메일 λ°œμ†‘ 둜직 μΆ”κ°€
popeye0618 May 14, 2026
704c338
fix: μ‹€νŒ¨ μ‹œλ„ 증가 둜직 μœ„μΉ˜ μ‘°μ • 및 검증 ν…ŒμŠ€νŠΈ μˆ˜μ •
popeye0618 May 14, 2026
6f0370f
fix: 이메일 ν•΄μ‹œ 생성 μ „ 곡백 및 μ†Œλ¬Έμž λ³€ν™˜ 둜직 μΆ”κ°€
popeye0618 May 14, 2026
ab30156
fix: νšŒμ› νƒˆν‡΄ ν›„μ²˜λ¦¬ μ‹€νŒ¨ μ‹œ 둜그 μΆ”κ°€ 및 μ •μ±… 처리 둜직 뢄리
popeye0618 May 14, 2026
bd78f62
feat: νšŒμ› νƒˆν‡΄ μ‹œ λ™μ˜ 철회 둜직 μΆ”κ°€
popeye0618 May 14, 2026
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ OAUTH_KAKAO_CLIENT_ID=
OAUTH_KAKAO_CLIENT_SECRET=
OAUTH_NAVER_CLIENT_ID=
OAUTH_NAVER_CLIENT_SECRET=

EMAIL_ENCRYPTION_KEY_BASE64=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.unit.member.config

import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration

@Configuration
@EnableConfigurationProperties(EmailEncryptionProperties::class)
class EmailEncryptionConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.unit.member.config

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "unit.security.email-encryption")
data class EmailEncryptionProperties(
val keyBase64: String,
)
2 changes: 2 additions & 0 deletions src/main/kotlin/com/unit/member/dto/MemberResponse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.unit.member.enums.UserSchoolVerificationStatus

data class MemberMeResponse(
val memberId: Long,
val email: String?,
val nickname: String,
val profileImageUrl: String?,
val status: MemberStatus,
Expand All @@ -16,4 +17,5 @@ data class MemberSchoolResponse(
val schoolId: Long,
val name: String,
val verificationStatus: UserSchoolVerificationStatus,
val verifiedEmail: String?,
)
6 changes: 5 additions & 1 deletion src/main/kotlin/com/unit/member/entity/MemberConsent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ class MemberConsent(
@Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime? = null
protected set
}

fun withdraw(now: LocalDateTime) {
this.withdrawnAt = now
}
}
15 changes: 13 additions & 2 deletions src/main/kotlin/com/unit/member/entity/UserSchoolVerification.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ class UserSchoolVerification(
val method: UserSchoolVerificationMethod,

@Column(name = "verified_email_hash", columnDefinition = "BINARY(32)")
val verifiedEmailHash: ByteArray? = null,
var verifiedEmailHash: ByteArray? = null,

@Column(name = "verified_email_encrypted", columnDefinition = "VARBINARY(512)")
val verifiedEmailEncrypted: ByteArray? = null,
var verifiedEmailEncrypted: ByteArray? = null,

@Column(name = "student_number_hash", columnDefinition = "BINARY(32)")
val studentNumberHash: ByteArray? = null,
Expand Down Expand Up @@ -74,4 +74,15 @@ class UserSchoolVerification(
fun expire() {
this.status = UserSchoolVerificationStatus.EXPIRED
}

fun verifyByEmail(
now: LocalDateTime,
emailHash: ByteArray,
emailEncrypted: ByteArray? = null,
) {
this.status = UserSchoolVerificationStatus.VERIFIED
this.verifiedAt = now
this.verifiedEmailHash = emailHash
this.verifiedEmailEncrypted = emailEncrypted
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ package com.unit.member.repository
import com.unit.member.entity.MemberConsent
import org.springframework.data.jpa.repository.JpaRepository

interface MemberConsentRepository : JpaRepository<MemberConsent, Long>
interface MemberConsentRepository : JpaRepository<MemberConsent, Long> {

fun findAllByMemberId(memberId: Long): List<MemberConsent>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package com.unit.member.repository

import com.unit.member.entity.RefreshToken
import com.unit.member.enums.RefreshTokenStatus
import jakarta.persistence.LockModeType
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Lock

interface RefreshTokenRepository : JpaRepository<RefreshToken, Long> {

@Lock(LockModeType.PESSIMISTIC_WRITE)
fun findByTokenHashAndStatus(
tokenHash: ByteArray,
status: RefreshTokenStatus = RefreshTokenStatus.ACTIVE,
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/com/unit/member/service/AuthLoginService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class AuthLoginService(

override fun login(request: AuthLoginRequest): AuthLoginResponse {

val emailHash = emailHasher.hash(request.email)
val email = request.email.trim().lowercase()
val emailHash = emailHasher.hash(email)
Comment on lines +28 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge κΈ°μ‘΄ 이메일 ν•΄μ‹œμ™€ ν˜Έν™˜λ˜κ²Œ 둜그인 쑰회λ₯Ό μœ μ§€ν•˜μ„Έμš”

μ—¬κΈ°μ„œ 이메일을 trim().lowercase()둜 μ •κ·œν™”ν•œ λ’€ ν•΄μ‹œλ₯Ό μ‘°νšŒν•˜λ©΄, 이 컀밋 이전에 λŒ€μ†Œλ¬Έμž κ·ΈλŒ€λ‘œ ν•΄μ‹œ μ €μž₯된 νšŒμ›(예: User@school.ac.kr)은 동일 μ£Όμ†Œλ‘œ λ‘œκ·ΈμΈν•΄λ„ 항상 λ‹€λ₯Έ ν•΄μ‹œκ°€ λ˜μ–΄ INVALID_LOGIN_CREDENTIALS둜 μ‹€νŒ¨ν•©λ‹ˆλ‹€. 즉 배포 μ‹œμ λΆ€ν„° 일뢀 κΈ°μ‘΄ 계정이 μ¦‰μ‹œ 둜그인 λΆˆκ°€ μƒνƒœκ°€ 될 수 μžˆμœΌλ―€λ‘œ, μ΅œμ†Œν•œ 데이터 λ°±ν•„(backfill) μ™„λ£Œ μ „κΉŒμ§€λŠ” κΈ°μ‘΄ ν•΄μ‹œ 포맷도 ν•¨κ»˜ μ‘°νšŒν•˜κ±°λ‚˜, λ§ˆμ΄κ·Έλ ˆμ΄μ…˜μ„ μ„ ν–‰ν•΄ ν•΄μ‹œ 일관성을 λ§žμΆ°μ•Ό ν•©λ‹ˆλ‹€.

Useful? React with πŸ‘Β / πŸ‘Ž.


val member = memberRepository.findByEmailHashAndDeletedAtIsNull(emailHash)
?: memberRepository.findTopByEmailHashAndDeletedAtIsNotNullOrderByDeletedAtDescIdDesc(emailHash)
Expand Down
4 changes: 4 additions & 0 deletions src/main/kotlin/com/unit/member/service/MemberQueryService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.unit.member.exception.MemberErrorCode
import com.unit.member.repository.MemberRepository
import com.unit.member.repository.SchoolRepository
import com.unit.member.repository.UserSchoolVerificationRepository
import com.unit.member.util.EmailEncryptor
import com.unit.platform.error.BusinessException
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
Expand All @@ -18,6 +19,7 @@ class MemberQueryService(
private val memberRepository: MemberRepository,
private val userSchoolVerificationRepository: UserSchoolVerificationRepository,
private val schoolRepository: SchoolRepository,
private val emailEncryptor: EmailEncryptor
) : MemberQueryUseCase {

override fun getMe(memberId: Long): MemberMeResponse {
Expand All @@ -35,11 +37,13 @@ class MemberQueryService(
schoolId = requireNotNull(school.id),
name = school.name,
verificationStatus = it.status,
verifiedEmail = it.verifiedEmailEncrypted?.let(emailEncryptor::decrypt),
)
}

return MemberMeResponse(
memberId = requireNotNull(member.id),
email = member.emailEncrypted?.let(emailEncryptor::decrypt),
nickname = member.nickname,
profileImageUrl = member.profileImageUrl,
status = member.status,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.unit.member.repository.MemberConsentRepository
import com.unit.member.repository.MemberRepository
import com.unit.member.repository.SchoolRepository
import com.unit.member.repository.UserSchoolVerificationRepository
import com.unit.member.util.EmailEncryptor
import com.unit.member.util.EmailHasher
import com.unit.platform.error.BusinessException
import org.springframework.security.crypto.password.PasswordEncoder
Expand All @@ -32,10 +33,12 @@ class MemberSignupService(
private val emailHasher: EmailHasher,
private val memberConsentRepository: MemberConsentRepository,
private val memberConsentProperties: MemberConsentProperties,
private val emailEncryptor: EmailEncryptor,
) : MemberSignupUseCase {

override fun signup(request: MemberSignupRequest): MemberSignupResponse {
val emailHash = emailHasher.hash(request.email)
val email = request.email.trim().lowercase()
val emailHash = emailHasher.hash(email)
Comment on lines +40 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 둜그인과 같은 이메일 μ •κ·œν™”λ₯Ό μ μš©ν•˜λΌ

이번 μ»€λ°‹μ—μ„œ νšŒμ›κ°€μž…μ€ request.email.trim().lowercase()둜 ν•΄μ‹œλ₯Ό λ§Œλ“€λ„λ‘ λ°”λ€Œμ—ˆμ§€λ§Œ, 둜그인(AuthLoginService.login)은 μ—¬μ „νžˆ 원문 request.email을 κ·ΈλŒ€λ‘œ ν•΄μ‹œν•©λ‹ˆλ‹€. κ·Έ κ²°κ³Ό μ‚¬μš©μžκ°€ κ°€μž… μ‹œ Test@Unit.com처럼 λŒ€μ†Œλ¬Έμž/곡백이 μ„žμΈ 이메일을 μž…λ ₯ν•˜λ©΄ DBμ—λŠ” μ •κ·œν™”λœ ν•΄μ‹œκ°€ μ €μž₯되고, 이후 같은 μž…λ ₯κ°’μœΌλ‘œ λ‘œκ·ΈμΈν•  λ•Œ ν•΄μ‹œκ°€ 달라져 INVALID_LOGIN_CREDENTIALSκ°€ λ°œμƒν•©λ‹ˆλ‹€. κ°€μž…κ³Ό λ‘œκ·ΈμΈμ—μ„œ λ™μΌν•œ μ •κ·œν™” κ·œμΉ™μ„ κ³΅μœ ν•˜λ„λ‘ λ§žμΆ°μ•Ό ν•©λ‹ˆλ‹€.

Useful? React with πŸ‘Β / πŸ‘Ž.

Comment on lines +40 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 이메일 쀑볡 검사에 λ ˆκ±°μ‹œ ν•΄μ‹œ ν˜Έν™˜ 경둜λ₯Ό μΆ”κ°€ν•˜μ„Έμš”

νšŒμ›κ°€μž…μ—μ„œ 이메일을 μ†Œλ¬Έμž μ •κ·œν™”ν•΄ ν•΄μ‹œλ₯Ό λ§Œλ“€κΈ° μ‹œμž‘ν–ˆμ§€λ§Œ κΈ°μ‘΄ λ°μ΄ν„°λŠ” λΉ„μ •κ·œν™” ν•΄μ‹œλ₯Ό κ·ΈλŒ€λ‘œ κ°€μ§€κ³  μžˆμ–΄, 과거에 λŒ€λ¬Έμžλ‘œ κ°€μž…λœ μ£Όμ†Œμ™€ λ™μΌν•œ λ©”μΌλ°•μŠ€κ°€ μƒˆλ‘œμš΄ μ†Œλ¬Έμž ν•΄μ‹œλ‘œ 쀑볡 κ°€μž…λ  수 μžˆμŠ΅λ‹ˆλ‹€. 이 경우 existsByEmailHashAndDeletedAtIsNullκ°€ falseκ°€ λ˜μ–΄ 동일 μ‹€μ‚¬μš© 이메일에 λŒ€ν•΄ ν™œμ„± 계정이 2개 생길 수 μžˆμœΌλ―€λ‘œ, μ •κ·œν™” μ „/ν›„ ν•΄μ‹œλ₯Ό λͺ¨λ‘ μ°¨λ‹¨ν•˜κ±°λ‚˜ 사전 λ§ˆμ΄κ·Έλ ˆμ΄μ…˜μœΌλ‘œ κΈ°μ‘΄ ν•΄μ‹œλ₯Ό 톡합해야 ν•©λ‹ˆλ‹€.

Useful? React with πŸ‘Β / πŸ‘Ž.

val nickname = request.nickname.trim()
val schoolId = request.schoolId!!

Expand All @@ -54,6 +57,7 @@ class MemberSignupService(
val member = memberRepository.save(
Member(
emailHash = emailHash,
emailEncrypted = emailEncryptor.encrypt(email),
passwordHash = passwordEncoder.encode(request.password),
nickname = nickname,
status = MemberStatus.PENDING,
Expand Down
46 changes: 44 additions & 2 deletions src/main/kotlin/com/unit/member/service/MemberWithdrawalService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@ package com.unit.member.service

import com.unit.member.enums.MemberStatus
import com.unit.member.exception.MemberErrorCode
import com.unit.member.repository.MemberConsentRepository
import com.unit.member.repository.MemberRepository
import com.unit.member.withdrawal.MemberWithdrawalContext
import com.unit.member.withdrawal.MemberWithdrawalPolicy
import com.unit.platform.error.BusinessException
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.support.TransactionSynchronization
import org.springframework.transaction.support.TransactionSynchronizationManager
import java.time.LocalDateTime

@Service
@Transactional
class MemberWithdrawalService(
private val memberRepository: MemberRepository,
private val memberConsentRepository: MemberConsentRepository,
private val refreshTokenUseCase: RefreshTokenUseCase,
private val withdrawalPolicies: List<MemberWithdrawalPolicy>,
) : MemberWithdrawalUseCase {
Expand All @@ -33,8 +38,45 @@ class MemberWithdrawalService(
withdrawalPolicies.forEach { it.validate(context) }

member.withdraw(now)
memberConsentRepository.findAllByMemberId(context.memberId)
.forEach { it.withdraw(now) }
refreshTokenUseCase.revokeAll(context.memberId)

withdrawalPolicies.forEach { it.apply(context) }
registerAfterCommit {
applyWithdrawalPoliciesAfterCommit(context)
}
}
}

private fun registerAfterCommit(action: () -> Unit) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
action()
return
}

TransactionSynchronizationManager.registerSynchronization(
object : TransactionSynchronization {
override fun afterCommit() {
action()
}
Comment on lines +58 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge afterCommit 콜백 μ˜ˆμ™Έλ₯Ό νƒˆν‡΄ 응닡과 λΆ„λ¦¬ν•˜λΌ

withdrawalPolicies.applyλ₯Ό afterCommitμ—μ„œ κ·ΈλŒ€λ‘œ μ‹€ν–‰ν•˜λ©΄, 콜백 λ‚΄λΆ€ μ˜ˆμ™Έκ°€ ν˜ΈμΆœμžκΉŒμ§€ μ „νŒŒλ˜λŠ” λ™μ•ˆ DB 컀밋은 이미 μ™„λ£Œλ˜μ–΄ νšŒμ›μ€ DELETED μƒνƒœμΈλ° APIλŠ” μ‹€νŒ¨(5xx)둜 보일 수 μžˆμŠ΅λ‹ˆλ‹€. 이 μ»€λ°‹μ—μ„œ ν›„μ²˜λ¦¬λ₯Ό νŠΈλžœμž­μ…˜ λ°–μœΌλ‘œ μ΄λ™ν–ˆκΈ° λ•Œλ¬Έμ— μƒˆλ‘œ 생긴 뢈일치이며, ν΄λΌμ΄μ–ΈνŠΈ μž¬μ‹œλ„/운영 λͺ¨λ‹ˆν„°λ§μ—μ„œ "μ‹€νŒ¨μ²˜λŸΌ λ³΄μ΄μ§€λ§Œ μ‹€μ œλ‘œλŠ” νƒˆν‡΄ μ™„λ£Œ" μƒνƒœλ₯Ό λ§Œλ“­λ‹ˆλ‹€. 콜백 μ˜ˆμ™Έλ₯Ό 별도 처리(try-catch + λ‘œκΉ…/비동기 재처리)ν•΄ 컀밋 결과와 응닡 의미λ₯Ό λΆ„λ¦¬ν•˜λŠ” 편이 μ•ˆμ „ν•©λ‹ˆλ‹€.

Useful? React with πŸ‘Β / πŸ‘Ž.

},
)
}

private fun applyWithdrawalPoliciesAfterCommit(context: MemberWithdrawalContext) {
withdrawalPolicies.forEach { policy ->
try {
policy.apply(context)
} catch (e: Exception) {
log.error(
"Member withdrawal post-processing failed. memberId=${context.memberId}, policy=${policy.javaClass.name}",
e,
)
}
}
}

companion object {
private val log = LoggerFactory.getLogger(MemberWithdrawalService::class.java)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.unit.member.repository.SchoolEmailDomainRepository
import com.unit.member.repository.SchoolEmailVerificationCodeRepository
import com.unit.member.repository.SchoolRepository
import com.unit.member.repository.UserSchoolVerificationRepository
import com.unit.member.util.EmailEncryptor
import com.unit.member.util.EmailHasher
import com.unit.member.util.SchoolEmailVerificationCodeGenerator
import com.unit.member.util.SchoolEmailVerificationFailureRecorder
Expand All @@ -22,6 +23,8 @@ import com.unit.platform.mail.EmailSender
import com.unit.platform.mail.EmailTemplateRenderer
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.support.TransactionSynchronization
import org.springframework.transaction.support.TransactionSynchronizationManager
import java.time.LocalDateTime

@Service
Expand All @@ -38,7 +41,7 @@ class SchoolEmailVerificationService(
private val failureRecorder: SchoolEmailVerificationFailureRecorder,
private val emailSender: EmailSender,
private val emailTemplateRenderer: EmailTemplateRenderer,

private val emailEncryptor: EmailEncryptor,
) : SchoolEmailVerificationUseCase {

private val verificationExpiresInSeconds = 300L
Expand All @@ -51,6 +54,18 @@ class SchoolEmailVerificationService(
schoolRepository.findByIdAndStatus(schoolId)
?: throw BusinessException(MemberErrorCode.SCHOOL_NOT_FOUND)

val hasPendingSchoolVerification =
userSchoolVerificationRepository.existsByMemberIdAndSchoolIdAndStatus(
memberId = memberId,
schoolId = schoolId,
status = UserSchoolVerificationStatus.PENDING,
)

if (!hasPendingSchoolVerification) {
throw BusinessException(MemberErrorCode.SCHOOL_VERIFICATION_NOT_FOUND)
}


val email = request.email.trim().lowercase()
val domain = extractDomain(email)

Expand Down Expand Up @@ -91,15 +106,17 @@ class SchoolEmailVerificationService(
),
)

emailSender.send(
EmailMessage(
to = email,
subject = "[UniT] 학ꡐ 이메일 인증 μ½”λ“œ",
body = html,
html = true,
),
val message = EmailMessage(
to = email,
subject = "[UniT] 학ꡐ 이메일 인증 μ½”λ“œ",
body = html,
html = true,
)

registerAfterCommit {
emailSender.send(message)
}

return SchoolEmailVerificationResponse(
schoolId = schoolId,
email = email,
Expand Down Expand Up @@ -134,27 +151,27 @@ class SchoolEmailVerificationService(
throw BusinessException(MemberErrorCode.SCHOOL_EMAIL_VERIFICATION_CODE_EXPIRED)
}

failureRecorder.increaseAttempt(requireNotNull(verificationCode.id))

if (!tokenHasher.matches(code, verificationCode.codeHash)) {
failureRecorder.increaseAttempt(requireNotNull(verificationCode.id))
throw BusinessException(MemberErrorCode.SCHOOL_EMAIL_VERIFICATION_CODE_MISMATCHED)
}

verificationCode.verify(now)

val schoolVerification = userSchoolVerificationRepository.findByMemberIdAndSchoolId(
memberId = memberId,
schoolId = schoolId,
) ?: throw BusinessException(MemberErrorCode.SCHOOL_VERIFICATION_NOT_FOUND)

schoolVerification.status = UserSchoolVerificationStatus.VERIFIED
schoolVerification.verifiedAt = now

val member = memberRepository.findByIdAndStatusAndDeletedAtIsNull(
id = memberId,
status = MemberStatus.PENDING,
) ?: throw BusinessException(MemberErrorCode.MEMBER_LOGIN_FORBIDDEN)

verificationCode.verify(now)
schoolVerification.verifyByEmail(
now = now,
emailHash = emailHash,
emailEncrypted = emailEncryptor.encrypt(email),
)
member.activate()

return SchoolEmailVerificationConfirmResponse(
Expand All @@ -170,4 +187,20 @@ class SchoolEmailVerificationService(
private fun SchoolEmailVerificationCode.memberIdEquals(memberId: Long): Boolean {
return this.memberId == memberId
}
}

private fun registerAfterCommit(action: () -> Unit) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
action()
return
}

TransactionSynchronizationManager.registerSynchronization(
object : TransactionSynchronization {
override fun afterCommit() {
action()
}
},
)
}

}
Loading
Loading