Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class UserProvisioningFilter(
if (user == null) {
// 이메일은 nullable(미제공 provider 지원, ADR-0017) — placeholder 합성하지 않는다.
repo.save(User(id = principal.userId, email = principal.email, nickname = defaultNickname(principal)))
// 이번 요청에 막 생성된 신규 사용자임을 표시 → AuthController.me 가 isNew 로 반환
// (client 가 닉네임 설정 화면을 띄우는 신호). provisioning 은 보통 첫 /auth/me 요청.
request.setAttribute(NEW_USER_REQUEST_ATTR, true)
} else if (!user.isActive) {
// 탈퇴 사용자는 토큰 만료 전까지 유효한 Supabase JWT 로 접근할 수 있으므로 명시 차단한다.
response.sendError(HttpServletResponse.SC_FORBIDDEN, "비활성화된 계정입니다")
Expand All @@ -39,5 +42,9 @@ class UserProvisioningFilter(
?: principal.email?.substringBefore('@')?.take(NICKNAME_MAX_LENGTH)?.ifBlank { null }
?: "user"

private companion object { const val NICKNAME_MAX_LENGTH = 20 }
companion object {
/** 신규 provisioning 표시용 요청 attribute 키(AuthController.me 가 읽어 isNew 반환). */
const val NEW_USER_REQUEST_ATTR = "app.ieoseo.newUser"
private const val NICKNAME_MAX_LENGTH = 20
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package app.ieoseo.server.user.controller
import app.ieoseo.server.user.dto.*

import app.ieoseo.server.global.security.AuthPrincipal
import app.ieoseo.server.global.security.UserProvisioningFilter
import app.ieoseo.server.user.service.AuthService
import app.ieoseo.server.settings.service.UserSettingsService
import jakarta.servlet.http.HttpServletRequest
import app.ieoseo.server.global.common.ApiResponse
import app.ieoseo.server.settings.dto.SettingsResponse
import app.ieoseo.server.settings.dto.UpdateSettingsRequest
Expand Down Expand Up @@ -34,8 +36,15 @@ class AuthController(
private val userSettingsService: UserSettingsService,
) {
@GetMapping("/me")
fun me(@AuthenticationPrincipal principal: AuthPrincipal): ApiResponse<UserResponse> =
ApiResponse.ok(UserResponse.from(authService.me(principal.userId)))
fun me(
@AuthenticationPrincipal principal: AuthPrincipal,
request: HttpServletRequest,
): ApiResponse<UserResponse> {
// UserProvisioningFilter 가 이번 요청에 사용자를 막 생성했으면 isNew=true 로 알린다
// (client 가 닉네임 설정 화면을 띄움). 기존 사용자면 attribute 없음 → false.
val isNew = request.getAttribute(UserProvisioningFilter.NEW_USER_REQUEST_ATTR) == true
return ApiResponse.ok(UserResponse.from(authService.me(principal.userId), isNew))
}

/** 프로필(닉네임) 수정(이슈 #56). 인증 필요. 검증 실패 → 400, 사용자 없음 → 404. */
@PatchMapping("/me")
Expand Down
5 changes: 4 additions & 1 deletion src/main/kotlin/app/ieoseo/server/user/dto/AuthDtos.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ data class UserResponse(
val id: UUID,
val email: String?,
val nickname: String,
/** 이번 요청에 막 provisioning 된 신규 사용자인지(client 닉네임 설정 화면 트리거용). */
val isNew: Boolean = false,
) {
companion object {
fun from(user: User): UserResponse = UserResponse(id = user.id, email = user.email, nickname = user.nickname)
fun from(user: User, isNew: Boolean = false): UserResponse =
UserResponse(id = user.id, email = user.email, nickname = user.nickname, isNew = isNew)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,33 @@ class UserProvisioningFilterTest {
assertEquals("user", user.nickname)
}

@Test
fun `신규 생성 시 요청에 신규 표시 attribute 를 설정한다`() {
authenticate(AuthPrincipal(userId, email = "jiwoo@ieoseo.app"))
`when`(repo.findById(userId)).thenReturn(Optional.empty())

filter.doFilter(request, response, chain)

verify(request).setAttribute(
eq(UserProvisioningFilter.NEW_USER_REQUEST_ATTR),
eq(true),
)
}

@Test
fun `기존 사용자면 신규 표시 attribute 를 설정하지 않는다`() {
authenticate(AuthPrincipal(userId, email = "jiwoo@ieoseo.app"))
`when`(repo.findById(userId))
.thenReturn(Optional.of(User(id = userId, email = "jiwoo@ieoseo.app", nickname = "jiwoo")))

filter.doFilter(request, response, chain)

verify(request, never()).setAttribute(
eq(UserProvisioningFilter.NEW_USER_REQUEST_ATTR),
org.mockito.ArgumentMatchers.any(),
)
}

@Test
fun `활성 사용자가 이미 존재하면 저장하지 않고 체인을 진행한다`() {
authenticate(AuthPrincipal(userId, email = "jiwoo@ieoseo.app"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.ieoseo.server.user.controller
import app.ieoseo.server.user.domain.User
import app.ieoseo.server.global.security.AuthPrincipal
import app.ieoseo.server.global.security.SecurityConfig
import app.ieoseo.server.global.security.UserProvisioningFilter
import app.ieoseo.server.user.service.AuthService
import app.ieoseo.server.settings.service.UpdateUserSettingsCommand
import app.ieoseo.server.settings.service.UserSettingsService
Expand Down Expand Up @@ -97,6 +98,20 @@ class AuthControllerWebMvcTest {
.andExpect(jsonPath("$.data.id").value(userId.toString()))
.andExpect(jsonPath("$.data.email").value("jiwoo@ieoseo.app"))
.andExpect(jsonPath("$.data.nickname").value("지우"))
.andExpect(jsonPath("$.data.isNew").value(false))
}

@Test
fun `me 는 신규 provisioning 요청이면 isNew true 를 반환한다`() {
`when`(authService.me(userId)).thenReturn(user())

mockMvc.perform(
get("/api/v1/auth/me")
.with(authentication(asUser()))
.requestAttr(UserProvisioningFilter.NEW_USER_REQUEST_ATTR, true),
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.isNew").value(true))
}

@Test
Expand Down
Loading