Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class SecurityConfig(
POST,
"/api/v2/challenges",
"/api/v2/challenges/{challengeId}/join",
"/api/v2/oauth/username",
).authenticated()
it.requestMatchers(DELETE, "/api/v2/challenges/{challengeId}").authenticated()
it.requestMatchers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,26 @@ package com.photi.apis.enduser.controller.oauth

import com.photi.apis.enduser.common.exception.GlobalApiErrorResponses
import com.photi.apis.enduser.common.exception.UserApiErrorResponses
import com.photi.apis.enduser.common.success.dto.StringSuccessResponse
import com.photi.apis.enduser.config.security.AuthUser
import com.photi.apis.enduser.config.security.CustomUserDetails
import com.photi.apis.enduser.config.security.JwtTokenProvider
import com.photi.apis.enduser.controller.auth.dto.response.LoginResponse
import com.photi.apis.enduser.controller.auth.dto.response.SignUpResponse
import com.photi.apis.enduser.controller.oauth.request.OAuthSignUpRequest
import com.photi.core.domain.common.exception.GlobalErrorCode.EXPIRED_TOKEN
import com.photi.core.domain.common.exception.GlobalErrorCode.INVALID_TOKEN
import com.photi.core.domain.user.exception.UserErrorCode.*
import com.photi.apis.enduser.config.security.getUserId
import com.photi.apis.enduser.controller.oauth.request.OAuthUpdateUsernameRequest
import com.photi.apis.enduser.controller.oauth.response.OAuthLoginResponse
import com.photi.core.domain.common.consts.SwaggerKey.ACCESS_TOKEN_KEY
import com.photi.core.domain.common.exception.GlobalErrorCode.*
import com.photi.core.domain.user.exception.UserErrorCode.DELETED_USER
import com.photi.core.domain.user.exception.UserErrorCode.USER_NOT_FOUND
import com.photi.core.domain.user.model.OAuthProviderType
import com.photi.core.domain.user.model.RoleType
import com.photi.core.domain.user.model.RoleType.Companion.getRole
import com.photi.core.domain.user.service.OAuthService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.http.HttpStatus.CREATED
import org.springframework.http.HttpStatus.OK
import org.springframework.http.ResponseEntity
import org.springframework.validation.annotation.Validated
Expand All @@ -33,34 +36,31 @@ class OAuthController(
private val jwtTokenProvider: JwtTokenProvider,
) {

@PostMapping("/{provider}/signup")
@Operation(summary = "OAuth 회원가입")
@ApiResponse(responseCode = "201")
@UserApiErrorResponses([EXISTING_USER])
@GlobalApiErrorResponses([INVALID_TOKEN, EXPIRED_TOKEN])
fun signUp(
@PathVariable provider: OAuthProviderType,
@RequestParam("id_token") @Parameter(description = "ID 토큰") idToken: String,
@RequestBody @Valid request: OAuthSignUpRequest,
): ResponseEntity<SignUpResponse> {
val signUpUser = oAuthService.signUp(provider, idToken, request.toServiceDto())
val response = SignUpResponse.of(signUpUser)
val headers = jwtTokenProvider.createToken(response.userId, RoleType.USER)
return ResponseEntity.status(CREATED).headers(headers).body(response)
}

@GetMapping("/{provider}/login")
@Operation(summary = "OAuth 로그인")
@ApiResponse(responseCode = "200")
@UserApiErrorResponses([USER_NOT_FOUND, DELETED_USER])
@UserApiErrorResponses([DELETED_USER])
@GlobalApiErrorResponses([INVALID_TOKEN, EXPIRED_TOKEN])
fun login(
@PathVariable provider: OAuthProviderType,
@RequestParam("id_token") @Parameter(description = "ID 토큰") idToken: String,
): ResponseEntity<LoginResponse> {
): ResponseEntity<OAuthLoginResponse> {
val loginUser = oAuthService.login(provider, idToken)
val response = LoginResponse.of(loginUser)
val headers = jwtTokenProvider.createToken(response.userId, getRole(response.username))
val response = OAuthLoginResponse.of(loginUser)
val headers = jwtTokenProvider.createToken(loginUser.userId, RoleType.USER)
return ResponseEntity.status(OK).headers(headers).body(response)
}

@PostMapping("/username")
@Operation(summary = "OAuth 아이디 설정", security = [SecurityRequirement(name = ACCESS_TOKEN_KEY)])
@ApiResponse(responseCode = "200")
@UserApiErrorResponses([USER_NOT_FOUND])
@GlobalApiErrorResponses([TOKEN_UNAUTHENTICATED, INVALID_TOKEN, EXPIRED_TOKEN])
fun updateUsername(
@AuthUser user: CustomUserDetails,
@RequestBody @Valid request: OAuthUpdateUsernameRequest,
): ResponseEntity<StringSuccessResponse> {
oAuthService.updateUsername(user.getUserId(), request.toServiceDto())
return ResponseEntity.ok(StringSuccessResponse("OAuth 아이디 설정이 완료되었습니다."))
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.photi.apis.enduser.controller.oauth.request

import com.photi.core.domain.common.consts.RegexPattern.LOWERCASE_NUMBER_UNDERSCORE
import com.photi.core.domain.user.dto.OAuthSignUpDto
import com.photi.core.domain.user.dto.OAuthUpdateUsernameDto
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Size

@Schema(description = "OAuth 회원가입 요청 객체")
data class OAuthSignUpRequest(
@Schema(description = "OAuth 아이디 설정 요청 객체")
data class OAuthUpdateUsernameRequest(

@Schema(description = "아이디", example = "photi")
@field:NotBlank(message = "아이디는 필수 입력입니다.")
Expand All @@ -20,5 +20,5 @@ data class OAuthSignUpRequest(
val username: String,
) {

fun toServiceDto() = OAuthSignUpDto(username)
fun toServiceDto() = OAuthUpdateUsernameDto(username)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.photi.apis.enduser.controller.oauth.response

import com.photi.core.domain.user.dto.OAuthLoginDto
import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "OAuth 로그인 응답 객체")
data class OAuthLoginResponse(

@Schema(description = "아이디", example = "null 또는 설정된 아이디")
val username: String?,
) {

companion object {

fun of(user: OAuthLoginDto) = OAuthLoginResponse(user.username)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.photi.core.domain.user.dto

import com.photi.core.domain.user.model.User

data class OAuthLoginDto(
val userId: Long,
val username: String?,
) {

companion object {

fun of(user: User) = OAuthLoginDto(user.id!!, user.username)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.photi.core.domain.user.dto

data class OAuthUpdateUsernameDto(
val username: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,9 @@ class User(
userValidator.validateDeletedUser(this)
}

fun changeUsername(username: String) {
this.username = username
}

private fun isImageNullOrEmpty() = this.imageUrl.isNullOrEmpty()
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.photi.core.domain.user.service

import com.photi.core.domain.user.dto.LoginDto
import com.photi.core.domain.user.dto.OAuthSignUpDto
import com.photi.core.domain.user.dto.OAuthLoginDto
import com.photi.core.domain.user.dto.OAuthUpdateUsernameDto
import com.photi.core.domain.user.dto.OidcPayload
import com.photi.core.domain.user.dto.SignUpDto
import com.photi.core.domain.user.exception.UserException
import com.photi.core.domain.user.model.OAuthInfo
import com.photi.core.domain.user.model.OAuthProviderType
import com.photi.core.domain.user.port.OAuthFactoryPort
import com.photi.core.domain.user.port.OAuthPort
Expand All @@ -24,32 +24,34 @@ class OAuthService(
) {

@Transactional
fun signUp(provider: OAuthProviderType, idToken: String, dto: OAuthSignUpDto): SignUpDto {
fun login(provider: OAuthProviderType, idToken: String): OAuthLoginDto {
val oAuthPort = oAuthFactory.getOAuthAdapter(provider)
val idTokenPayload = getOidcPayload(idToken, oAuthPort)
val oAuthInfo = oAuthPort.createOAuthInfo(idTokenPayload.sub)
if (userQueryService.existsBy(oAuthInfo)) throw UserException.ExistsUserException()
val user = userCommandService.createUser(
dto,
oAuthInfo,
idTokenPayload.email,
idTokenPayload.image,
)
return SignUpDto.of(user)
val user =
userQueryService.getLoginUserBy(oAuthInfo) ?: return newUser(oAuthInfo, idTokenPayload)
user.login(userValidator)
return OAuthLoginDto.of(user)
}

fun login(provider: OAuthProviderType, idToken: String): LoginDto {
val oAuthPort = oAuthFactory.getOAuthAdapter(provider)
val idTokenPayload = getOidcPayload(idToken, oAuthPort)
val oAuthInfo = oAuthPort.createOAuthInfo(idTokenPayload.sub)
val user = userQueryService.getLoginUserBy(oAuthInfo)
?: throw UserException.NotFoundUserException()
user.login(userValidator)
return LoginDto.of(user)
@Transactional
fun updateUsername(id: Long, dto: OAuthUpdateUsernameDto) {
val user = userQueryService.getUserBy(id)
.orElseThrow { throw UserException.NotFoundUserException() }
user.changeUsername(dto.username)
}

private fun getOidcPayload(idToken: String, oAuthPort: OAuthPort): OidcPayload {
val properties = oAuthPort.getProperties()
return oAuthPort.getIdTokenPayload(idToken, properties.baseUrl, properties.nativeAppKey)
}

private fun newUser(oAuthInfo: OAuthInfo, idTokenPayload: OidcPayload) =
userCommandService.createUser(
oAuthInfo,
idTokenPayload.email,
idTokenPayload.image,
).run {
OAuthLoginDto.of(this)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.photi.core.domain.user.service.command

import com.photi.core.domain.user.dto.OAuthSignUpDto
import com.photi.core.domain.user.dto.SendEmailAuthenticationCodeDto
import com.photi.core.domain.user.model.OAuthInfo
import com.photi.core.domain.user.model.RoleType
import com.photi.core.domain.user.model.User
import com.photi.core.domain.user.model.repository.UserRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -17,6 +18,15 @@ class UserCommandService(
userRepository.save(dto.toEntity(authenticationCode))
}

fun createUser(dto: OAuthSignUpDto, oAuthInfo: OAuthInfo, email: String, image: String) =
userRepository.save(dto.toEntity(oAuthInfo, email, image))
fun createUser(oAuthInfo: OAuthInfo, email: String, image: String) =
userRepository.save(toUserEntity(oAuthInfo, email, image))

private fun toUserEntity(oAuthInfo: OAuthInfo, email: String, image: String) =
User(
email = email,
oAuthInfo = oAuthInfo,
role = RoleType.USER,
isAuthenticated = true,
imageUrl = image,
)
}