diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/security/SecurityConfig.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/security/SecurityConfig.kt index b071b927..e9d8f7cc 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/security/SecurityConfig.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/config/security/SecurityConfig.kt @@ -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( diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt index 6eef298b..a3b37ce1 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/OAuthController.kt @@ -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 @@ -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 { - 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 { + ): ResponseEntity { 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 { + oAuthService.updateUsername(user.getUserId(), request.toServiceDto()) + return ResponseEntity.ok(StringSuccessResponse("OAuth 아이디 설정이 완료되었습니다.")) + } } diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/request/OAuthSignUpRequest.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/request/OAuthUpdateUsernameRequest.kt similarity index 77% rename from photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/request/OAuthSignUpRequest.kt rename to photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/request/OAuthUpdateUsernameRequest.kt index bb4dce3e..bd6684b6 100644 --- a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/request/OAuthSignUpRequest.kt +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/request/OAuthUpdateUsernameRequest.kt @@ -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 = "아이디는 필수 입력입니다.") @@ -20,5 +20,5 @@ data class OAuthSignUpRequest( val username: String, ) { - fun toServiceDto() = OAuthSignUpDto(username) + fun toServiceDto() = OAuthUpdateUsernameDto(username) } diff --git a/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/response/OAuthLoginResponse.kt b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/response/OAuthLoginResponse.kt new file mode 100644 index 00000000..ce279737 --- /dev/null +++ b/photi-apis/enduser/src/main/kotlin/com/photi/apis/enduser/controller/oauth/response/OAuthLoginResponse.kt @@ -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) + } +} \ No newline at end of file diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthLoginDto.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthLoginDto.kt new file mode 100644 index 00000000..4a7dbd28 --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthLoginDto.kt @@ -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) + } +} diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt deleted file mode 100644 index 3debca70..00000000 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthSignUpDto.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.photi.core.domain.user.dto - -import com.photi.core.domain.user.model.OAuthInfo -import com.photi.core.domain.user.model.RoleType -import com.photi.core.domain.user.model.User - -data class OAuthSignUpDto( - val username: String, -) { - - fun toEntity(oAuthInfo: OAuthInfo, email: String, image: String) = - User( - email = email, - oAuthInfo = oAuthInfo, - username = username, - role = RoleType.USER, - isAuthenticated = true, - imageUrl = image, - ) -} diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthUpdateUsernameDto.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthUpdateUsernameDto.kt new file mode 100644 index 00000000..9adcb2da --- /dev/null +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/dto/OAuthUpdateUsernameDto.kt @@ -0,0 +1,5 @@ +package com.photi.core.domain.user.dto + +data class OAuthUpdateUsernameDto( + val username: String, +) diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt index 7bdcf6e4..5bb9bb5f 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/model/User.kt @@ -133,5 +133,9 @@ class User( userValidator.validateDeletedUser(this) } + fun changeUsername(username: String) { + this.username = username + } + private fun isImageNullOrEmpty() = this.imageUrl.isNullOrEmpty() } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt index a4222c1b..440ede98 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/OAuthService.kt @@ -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 @@ -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) + } } diff --git a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt index f08d4a07..059858e2 100644 --- a/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt +++ b/photi-core/domain/src/main/kotlin/com/photi/core/domain/user/service/command/UserCommandService.kt @@ -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 @@ -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, + ) }