Skip to content

Commit dc8d843

Browse files
authored
production release : member_team initiation and refresh_tokens auto saving (#428)
* add response field (memberId, cohortId) (#391) * initiation of new member and new announcement (#393) * appleOAuth name resolution (#395) * whitelist email -> memberId parameter changing (#398) * whitelist email login (parameter email -> memberId) (#399) * whitelist email -> memberId parameter changing * whitelist email -> memberId parameter changing * refactor/#397 (#400) * whitelist email -> memberId parameter changing * whitelist email -> memberId parameter changing * whitelist email -> memberId parameter changing * whitelist email -> memberId parameter changing (#402) * member_roles auto align (#405) * re-sync member_roles after whitelist (#407) * filtered member overview by cohort (#412) * filtered member overview by cohort * after_party_invitees 보상로직 * AFTERPARTY_COMPENSATIONRESPONSE * feat: member overview true/false of latest cohort (#414) * feat: member overview true/false of latest cohort * feat: member overview true/false of latest cohort * feat: invitees invitation iniation (#417) * feat: part null safe (#420) * refactor : Session url open and Cohort value adding (#425) * refactor: member relink and member_team initiation (#426) * PENDING initiation (#427) * refactor: member relink and member_team initiation * refactor : PENDING refresh
1 parent bf87b46 commit dc8d843

File tree

12 files changed

+162
-25
lines changed

12 files changed

+162
-25
lines changed

application/src/main/kotlin/core/application/cohort/application/service/CohortQueryService.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ class CohortQueryService(
1515
private val cohortPersistencePort: CohortPersistencePort,
1616
private val cohortProperties: CohortProperties,
1717
) : CohortQueryUseCase {
18+
fun getLatestCohort(): Cohort = getCohort(cohortProperties.value)
19+
1820
override fun getLatestCohortId(): CohortId =
19-
getCohort(cohortProperties.value).id
21+
getLatestCohort().id
2022
?: throw CohortNotFoundException()
2123

22-
override fun getLatestCohortValue(): String = getCohort(cohortProperties.value).value
24+
override fun getLatestCohortValue(): String = getLatestCohort().value
2325

2426
private fun getCohort(value: String): Cohort =
2527
cohortPersistencePort.findByValue(value)

application/src/main/kotlin/core/application/cohort/presentation/controller/CohortController.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,23 @@ package core.application.cohort.presentation.controller
33
import core.application.cohort.application.service.CohortQueryService
44
import core.application.cohort.presentation.response.CohortNumberResponse
55
import core.application.common.exception.CustomResponse
6+
import org.springframework.security.access.prepost.PreAuthorize
67
import org.springframework.web.bind.annotation.GetMapping
78
import org.springframework.web.bind.annotation.RestController
89

910
@RestController
1011
class CohortController(
1112
private val cohortQueryService: CohortQueryService,
1213
) : CohortApi {
14+
@PreAuthorize("permitAll()")
1315
@GetMapping("/v1/cohort")
1416
override fun latestCohort(): CustomResponse<CohortNumberResponse> {
15-
val cohort = cohortQueryService.getLatestCohortValue()
16-
return CustomResponse.ok(CohortNumberResponse(cohort))
17+
val cohort = cohortQueryService.getLatestCohort()
18+
return CustomResponse.ok(
19+
CohortNumberResponse(
20+
cohortId = cohort.id?.value ?: error("Latest cohort id must not be null"),
21+
cohortNumber = cohort.value,
22+
),
23+
)
1724
}
1825
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package core.application.cohort.presentation.response
22

33
data class CohortNumberResponse(
4+
val cohortId: Long,
45
val cohortNumber: String,
56
)

application/src/main/kotlin/core/application/member/application/service/MemberCommandService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ class MemberCommandService(
153153
}
154154

155155
val memberId = requireNotNull(member.id) { "Active member must have id" }
156+
memberTeamService.ensureMemberTeamInitialized(memberId)
156157
val latestCohortId = cohortQueryUseCase.getLatestCohortId()
157158
memberCohortService.addMemberToCohort(memberId, latestCohortId)
158159
publishMemberActivatedEvent(memberId, latestCohortId)

application/src/main/kotlin/core/application/member/application/service/MemberLoginService.kt

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@ import core.application.common.constant.Profile
44
import core.application.member.application.exception.MemberIdRequiredException
55
import core.application.member.application.service.oauth.MemberOAuthService
66
import core.application.member.application.service.role.MemberRoleService
7+
import core.application.member.application.service.team.MemberTeamService
78
import core.application.security.oauth.token.JwtTokenProvider
89
import core.application.security.properties.SecurityProperties
910
import core.application.security.redirect.handler.LoginRedirectHandler
1011
import core.domain.member.aggregate.Member
12+
import core.domain.member.enums.MemberStatus
1113
import core.domain.member.port.inbound.HandleMemberLoginUseCase
1214
import core.domain.member.port.outbound.MemberPersistencePort
1315
import core.domain.member.vo.MemberId
1416
import core.domain.refreshToken.aggregate.RefreshToken
1517
import core.domain.refreshToken.port.outbound.RefreshTokenPersistencePort
1618
import core.domain.security.oauth.dto.LoginResult
1719
import core.domain.security.oauth.dto.OAuthAttributes
18-
import org.springframework.dao.DataIntegrityViolationException
1920
import org.springframework.core.env.Environment
21+
import org.springframework.dao.DataIntegrityViolationException
2022
import org.springframework.stereotype.Service
2123
import org.springframework.transaction.annotation.Transactional
2224

@@ -30,6 +32,7 @@ class MemberLoginService(
3032
private val environment: Environment,
3133
private val redirectHandler: LoginRedirectHandler,
3234
private val memberRoleService: MemberRoleService,
35+
private val memberTeamService: MemberTeamService,
3336
) : HandleMemberLoginUseCase {
3437
@Transactional
3538
override fun handleLoginSuccess(
@@ -46,7 +49,9 @@ class MemberLoginService(
4649
if (memberOAuth != null) {
4750
val member =
4851
memberPersistencePort.findById(memberOAuth.memberId)
49-
?: throw IllegalStateException("MemberOAuth exists but Member not found")
52+
?: recoverOrCreateMemberForOrphanedOAuth(authAttributes).also {
53+
memberOAuthService.relinkMemberOAuthProvider(it, authAttributes)
54+
}
5055
return handleExistingMemberLogin(requestUrl, member)
5156
}
5257

@@ -75,12 +80,23 @@ class MemberLoginService(
7580
): LoginResult {
7681
val memberId = member.id ?: return LoginResult(null, securityProperties.redirect.restrictedRedirectUrl)
7782

78-
if (!member.isAllowed() || memberPersistencePort.existsDeletedMemberById(memberId.value)) {
83+
if (member.deletedAt == null) {
84+
memberTeamService.ensureMemberTeamInitialized(memberId)
85+
}
86+
87+
if (memberPersistencePort.existsDeletedMemberById(memberId.value) || member.status == MemberStatus.WITHDRAWN) {
7988
return LoginResult(null, securityProperties.redirect.restrictedRedirectUrl)
8089
}
8190

8291
memberRoleService.ensureGuestRoleAssigned(memberId)
8392

93+
if (member.status == MemberStatus.PENDING) {
94+
return generateLoginResult(
95+
memberId,
96+
securityProperties.redirect.restrictedRedirectUrl,
97+
)
98+
}
99+
84100
return generateLoginResult(
85101
memberId,
86102
redirectHandler.determineRedirectUrl(
@@ -113,11 +129,24 @@ class MemberLoginService(
113129
}
114130

115131
private fun selectLoginCandidate(members: List<Member>): Member {
116-
val activeCandidates = members.filter { it.isAllowed() }
117-
val eligible = if (activeCandidates.isNotEmpty()) activeCandidates else members
132+
val availableMembers = members.filter { it.deletedAt == null }
133+
val activeCandidates = availableMembers.filter { it.isAllowed() }
134+
val eligible = if (activeCandidates.isNotEmpty()) activeCandidates else availableMembers
118135
return eligible.maxByOrNull { it.id?.value ?: 0L } ?: throw MemberIdRequiredException()
119136
}
120137

138+
private fun recoverOrCreateMemberForOrphanedOAuth(authAttributes: OAuthAttributes): Member {
139+
val existingMembers = memberPersistencePort.findAllBySignupEmail(authAttributes.getEmail())
140+
return if (existingMembers.isNotEmpty()) {
141+
selectLoginCandidate(existingMembers)
142+
} else {
143+
createOrFindMemberBySignupEmail(
144+
email = authAttributes.getEmail(),
145+
name = authAttributes.getName(),
146+
)
147+
}
148+
}
149+
121150
private fun createOrFindMemberBySignupEmail(
122151
email: String,
123152
name: String,

application/src/main/kotlin/core/application/member/application/service/auth/AppleAuthService.kt

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package core.application.member.application.service.auth
22

3-
import core.application.common.constant.Profile
43
import core.application.member.application.service.role.MemberRoleService
4+
import core.application.member.application.service.team.MemberTeamService
55
import core.application.security.oauth.apple.AppleTokenExchangeService
66
import core.application.security.oauth.token.JwtTokenProvider
77
import core.domain.member.aggregate.Member
@@ -24,6 +24,7 @@ class AppleAuthService(
2424
private val refreshTokenPersistencePort: RefreshTokenPersistencePort,
2525
private val appleIdTokenValidator: core.application.security.oauth.apple.AppleIdTokenValidator,
2626
private val memberRoleService: MemberRoleService,
27+
private val memberTeamService: MemberTeamService,
2728
) {
2829
@Transactional
2930
fun login(
@@ -36,22 +37,23 @@ class AppleAuthService(
3637

3738
val claims = appleIdTokenValidator.verify(tokenResponse.id_token)
3839
val externalId = claims.subject ?: throw IllegalArgumentException("Invalid ID Token: sub missing")
40+
val email =
41+
claims["email"] as? String
42+
?: throw IllegalArgumentException("Invalid ID Token: email missing")
3943

4044
val memberOAuth =
4145
memberOAuthPersistencePort.findByProviderAndExternalId(OAuthProvider.APPLE, externalId)
4246

4347
val member =
4448
if (memberOAuth == null) {
45-
val email =
46-
claims["email"] as? String
47-
?: throw IllegalArgumentException("Invalid ID Token: emailassignments missing")
48-
4949
val existingMembers = memberPersistencePort.findAllBySignupEmail(email)
5050
val existingMember = selectLoginCandidate(existingMembers)
51+
val resolvedFullName =
52+
fullName?.trim()?.takeIf { it.isNotBlank() }
53+
?: (claims["name"] as? String)?.trim()?.takeIf { it.isNotBlank() }
5154
val memberName =
5255
resolveMemberName(
53-
fullName = fullName?.trim()?.takeIf { it.isNotBlank() }
54-
?: (claims["name"] as? String)?.trim()?.takeIf { it.isNotBlank() },
56+
fullName = resolvedFullName,
5557
familyName = familyName,
5658
givenName = givenName,
5759
email = email,
@@ -75,12 +77,22 @@ class AppleAuthService(
7577

7678
targetMember
7779
} else {
78-
// Existing member
7980
memberPersistencePort.findById(memberOAuth.memberId)
80-
?: throw IllegalStateException("MemberOAuth exists but Member not found")
81+
?: recoverOrCreateMemberForOrphanedOAuth(
82+
externalId = externalId,
83+
email = email,
84+
name =
85+
resolveMemberName(
86+
fullName = fullName,
87+
familyName = familyName,
88+
givenName = givenName,
89+
email = email,
90+
),
91+
)
8192
}
8293

8394
memberRoleService.ensureGuestRoleAssigned(member.id!!)
95+
memberTeamService.ensureMemberTeamInitialized(member.id!!)
8496

8597
// 4. Issue App Tokens
8698
val accessToken = jwtTokenProvider.generateAccessToken(member.id!!.toString())
@@ -114,15 +126,37 @@ class AppleAuthService(
114126
}
115127

116128
private fun selectLoginCandidate(members: List<Member>): Member? {
117-
if (members.isEmpty()) {
129+
val availableMembers = members.filter { it.deletedAt == null }
130+
if (availableMembers.isEmpty()) {
118131
return null
119132
}
120133

121-
val activeCandidates = members.filter { it.isAllowed() }
122-
val eligible = if (activeCandidates.isNotEmpty()) activeCandidates else members
134+
val activeCandidates = availableMembers.filter { it.isAllowed() }
135+
val eligible = if (activeCandidates.isNotEmpty()) activeCandidates else availableMembers
123136
return eligible.maxByOrNull { it.id?.value ?: 0L }
124137
}
125138

139+
private fun recoverOrCreateMemberForOrphanedOAuth(
140+
externalId: String,
141+
email: String,
142+
name: String,
143+
): Member {
144+
val targetMember =
145+
selectLoginCandidate(memberPersistencePort.findAllBySignupEmail(email))
146+
?: createOrFindMemberBySignupEmail(
147+
email = email,
148+
name = name,
149+
)
150+
151+
memberOAuthPersistencePort.relinkToMember(
152+
provider = OAuthProvider.APPLE,
153+
externalId = externalId,
154+
member = targetMember,
155+
)
156+
157+
return targetMember
158+
}
159+
126160
private fun createOrFindMemberBySignupEmail(
127161
email: String,
128162
name: String,

application/src/main/kotlin/core/application/member/application/service/auth/EmailPasswordAuthService.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import core.application.member.application.exception.MemberAllowedException
66
import core.application.member.application.exception.MemberDeletedException
77
import core.application.member.application.exception.MemberNotFoundException
88
import core.application.member.application.service.role.MemberRoleService
9+
import core.application.member.application.service.team.MemberTeamService
910
import core.application.security.oauth.token.JwtTokenProvider
1011
import core.domain.member.aggregate.Member
1112
import core.domain.member.port.outbound.MemberPersistencePort
@@ -33,6 +34,7 @@ class EmailPasswordAuthService(
3334
private val memberPersistencePort: MemberPersistencePort,
3435
private val roleQueryService: RoleQueryService,
3536
private val memberRoleService: MemberRoleService,
37+
private val memberTeamService: MemberTeamService,
3638
private val jwtTokenProvider: JwtTokenProvider,
3739
private val refreshTokenPersistencePort: RefreshTokenPersistencePort,
3840
private val passwordEncoder: PasswordEncoder,
@@ -96,6 +98,7 @@ class EmailPasswordAuthService(
9698
validateMemberForLogin(member)
9799

98100
memberRoleService.ensureGuestRoleAssigned(member.id!!)
101+
memberTeamService.ensureMemberTeamInitialized(member.id!!)
99102

100103
// Generate JWT tokens
101104
val permissionStrings = roleQueryService.getPermissionsByMemberId(member.id!!)
@@ -141,6 +144,7 @@ class EmailPasswordAuthService(
141144

142145
// 2. Ensure default member role (GUEST)
143146
memberRoleService.ensureGuestRoleAssigned(newMember.id!!)
147+
memberTeamService.ensureMemberTeamInitialized(newMember.id!!)
144148

145149
// 3. Create MemberCredential with encoded password
146150
val encodedPassword = passwordEncoder.encode(password)
@@ -201,8 +205,9 @@ class EmailPasswordAuthService(
201205
}
202206

203207
private fun selectLoginCandidate(members: List<Member>): Member {
204-
val activeCandidates = members.filter { it.isAllowed() }
205-
val eligible = if (activeCandidates.isNotEmpty()) activeCandidates else members
208+
val availableMembers = members.filter { it.deletedAt == null }
209+
val activeCandidates = availableMembers.filter { it.isAllowed() }
210+
val eligible = if (activeCandidates.isNotEmpty()) activeCandidates else availableMembers
206211
return eligible.maxByOrNull { it.id?.value ?: 0L } ?: throw MemberNotFoundException()
207212
}
208213

application/src/main/kotlin/core/application/member/application/service/oauth/MemberOAuthService.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ class MemberOAuthService(
1616
externalId: String,
1717
): MemberOAuth? = memberOAuthPersistencePort.findByProviderAndExternalId(provider, externalId)
1818

19+
fun relinkMemberOAuthProvider(
20+
member: Member,
21+
authAttribute: OAuthAttributes,
22+
) {
23+
memberOAuthPersistencePort.relinkToMember(
24+
provider = authAttribute.getProvider(),
25+
externalId = authAttribute.getExternalId(),
26+
member = member,
27+
)
28+
}
29+
1930
/**
2031
* 멤버 가입 시, OAuth 클라이언트로부터 전달받은 제공자 정보를 저장함.
2132
*

application/src/main/kotlin/core/application/member/application/service/team/MemberTeamService.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package core.application.member.application.service.team
22

33
import core.domain.member.aggregate.MemberTeam
4+
import core.domain.member.port.outbound.MemberPersistencePort
45
import core.domain.member.port.outbound.MemberTeamPersistencePort
56
import core.domain.member.vo.MemberId
67
import core.domain.team.vo.TeamId
8+
import org.springframework.beans.factory.annotation.Value
79
import org.springframework.stereotype.Service
810

911
@Service
1012
class MemberTeamService(
1113
private val memberTeamPersistencePort: MemberTeamPersistencePort,
14+
private val memberPersistencePort: MemberPersistencePort,
15+
@Value("\${member.default-team-id:0}")
16+
private val defaultTeamId: Int,
1217
) {
1318
/**
1419
* 팀에 멤버를 추가함.
@@ -23,6 +28,23 @@ class MemberTeamService(
2328
memberTeamPersistencePort.save(MemberTeam.of(memberId, teamId))
2429
}
2530

31+
fun ensureMemberTeamInitialized(memberId: MemberId) {
32+
if (defaultTeamId <= 0) {
33+
return
34+
}
35+
36+
if (memberPersistencePort.findMemberTeamByMemberId(memberId) != null) {
37+
return
38+
}
39+
40+
memberTeamPersistencePort.save(
41+
MemberTeam.of(
42+
memberId = memberId,
43+
teamId = TeamId(defaultTeamId.toLong()),
44+
),
45+
)
46+
}
47+
2648
/**
2749
* 멤버 탈퇴 시에, 멤버를 팀에서 제거(Hard Delete)하여 팀 정보 조회 시 노출되지 않도록 함.
2850
*

application/src/main/kotlin/core/application/session/presentation/controller/SessionQueryController.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import java.time.LocalDateTime
2323
class SessionQueryController(
2424
private val sessionQueryService: SessionQueryService,
2525
) : SessionQueryApi {
26-
@PreAuthorize("hasAuthority('read:session')")
26+
@PreAuthorize("permitAll()")
2727
@GetMapping("/next")
2828
override fun getNextSession(): CustomResponse<NextSessionResponse> {
2929
val response =
@@ -34,7 +34,7 @@ class SessionQueryController(
3434
return CustomResponse.ok(response)
3535
}
3636

37-
@PreAuthorize("hasAuthority('read:session')")
37+
@PreAuthorize("permitAll()")
3838
@GetMapping
3939
override fun getAllSessions(): CustomResponse<SessionListResponse> {
4040
val response =

0 commit comments

Comments
 (0)