-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: provider enum 클래스 정의 * feat: provider converter 정의 * feat: oauth domain 정의 * feat: provider exception 정의 * feat: provider request converter 정의 * feat: web config에 provider converter 등록 * chore: infra 모듈 httpclient 라이브러리 의존성 추가 * feat: oauth 로그인&회원가입 dto 정의 * rename: getter 메서드명 중 oidc 소문자화 * fix: oauth 로그인 시 oauth id 필드 추가 * fix: oauth oidc helper 메서드 추가 * rename: provider exception -> oauth exception * feat: oauth id 불일치 예외 추가 * feat: oauth repository 정의 * feat: oauth repository 소셜 아이디 & 제공자 탐색 메서드 선언 * feat: oauth domain service 정의 * rename: provider converter 예외 이름 수정 * rename: oauth service get -> read * style: oauth exception api 모듈 -> domain 모듈 이전 * rename: oauth error code 주석 포맷 변경 * feat: oauth 매퍼클래스 - 로그인 분기처리 * feat: oauth 로그인 use case 구현 * feat: oauth 로그인 컨트롤러 정의 * fix: cache config 내 불필요한 설정 추가 제거 * chore: infra 모듈 redis 환경 변수 주입 * rename: oauth controller 스웨거 문서 설명 추가 * feat: oauth API 설계 * fix: 전화번호 인증 oauth provider 구분 * fix: 전화 번호 인증 열거 타입 oauth provider 추론 메서드 static으로 변경 * fix: 전화번호 인증 코드 응답 객체 general, oauth 정적 팩토리 분리 * fix: oauth 분기 시나리오에 따른 dto 구분 * fix: oauth api 설계 수정 * fix: aouth use case의 verify code 메서드 반환 타입 verify code res로 수정 * rename: 인증 코드 검증 dto 생성 메서드 변경 * feat: oauth provider signup된 정보 존재 시 반환하는 에러코드 추가 * feat: 사용자 아이디 & provider 기반 데이터 존재 여부 검증 도메인 서비스 메서드 추가 * feat: oauth 회원가입 요청을 phone 검증 dto로 변환하는 정적 팩토리 메서드 추가 * fix: 소셜 회원가입 시 phone, code 필수 입력 필드 추가 * feat: user sync mapper 클래스 내 oauth 회원가입 분기 결정 메서드 추가 * feat: oauth use case 메서드 작성 * feat: 소셜 회원가입 분기 등록 로직 구현 * feat: 소셜 회원가입 Use case 작성 * rename: oauth controller 주석 제거 * rename: oauth api 1, 3번 상세한 설명을 위한 swagger 어노테이션 추가 * style: 프로그래밍 코드와 문서 주석 분리 * docs: 인증 코드 검증 예외 문서 수정 * fix: 휴대폰 만료 혹은 미등록 예외 reason code 401->404 변경 * docs: 전화번호 인증 응답 포맷 수정
- Loading branch information
1 parent
9d910ab
commit 9b7bde2
Showing
30 changed files
with
797 additions
and
29 deletions.
There are no files selected for viewing
140 changes: 140 additions & 0 deletions
140
pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/auth/api/OauthApi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
package kr.co.pennyway.api.apis.auth.api; | ||
|
||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.Parameter; | ||
import io.swagger.v3.oas.annotations.enums.ParameterIn; | ||
import io.swagger.v3.oas.annotations.media.Content; | ||
import io.swagger.v3.oas.annotations.media.ExampleObject; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponses; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import kr.co.pennyway.api.apis.auth.dto.PhoneVerificationDto; | ||
import kr.co.pennyway.api.apis.auth.dto.SignInReq; | ||
import kr.co.pennyway.api.apis.auth.dto.SignUpReq; | ||
import kr.co.pennyway.domain.domains.oauth.type.Provider; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.validation.annotation.Validated; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
|
||
@Tag(name = "[소셜 인증 API]") | ||
public interface OauthApi { | ||
@Operation(summary = "[1] 소셜 로그인", description = "기존에 Provider로 가입한 사용자는 로그인, 가입하지 않은 사용자는 전화번호 인증으로 이동") | ||
@Parameter(name = "provider", description = "소셜 제공자", examples = { | ||
@ExampleObject(name = "카카오", value = "kakao"), @ExampleObject(name = "애플", value = "apple"), @ExampleObject(name = "구글", value = "google") | ||
}, required = true, in = ParameterIn.QUERY) | ||
@ApiResponses({ | ||
@ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", examples = { | ||
@ExampleObject(name = "성공 - 기존 계정 있음", value = """ | ||
{ | ||
"code": "2000", | ||
"data": { | ||
"user": { | ||
"id": 1 | ||
} | ||
} | ||
} | ||
"""), | ||
@ExampleObject(name = "성공 - 기존 계정 없음 (id -1인 경우) - [2]로 진행", value = """ | ||
{ | ||
"code": "2000", | ||
"data": { | ||
"user": { | ||
"id": -1 | ||
} | ||
} | ||
} | ||
""") | ||
})), | ||
@ApiResponse(responseCode = "401", content = @Content(mediaType = "application/json", examples = { | ||
@ExampleObject(name = "실패 - 유효하지 않은 idToken", value = """ | ||
{ | ||
"code": "4013", | ||
"message": "비정상적인 토큰입니다" | ||
} | ||
""") | ||
})) | ||
}) | ||
ResponseEntity<?> signIn(@RequestParam Provider provider, @RequestBody @Validated SignInReq.Oauth request); | ||
|
||
@Operation(summary = "[2] 인증번호 발송", description = "전화번호 입력 후 인증번호 발송") | ||
@Parameter(name = "provider", description = "소셜 제공자", examples = { | ||
@ExampleObject(name = "카카오", value = "kakao"), @ExampleObject(name = "애플", value = "apple"), @ExampleObject(name = "구글", value = "google") | ||
}, required = true, in = ParameterIn.QUERY) | ||
@ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", examples = { | ||
@ExampleObject(name = "발신 성공", value = """ | ||
{ | ||
"code": "2000", | ||
"data": { | ||
"sms": { | ||
"to": "010-1234-5678", | ||
"sendAt": "2024-04-04 00:31:57", | ||
"expiresAt": "2024-04-04 00:36:57" | ||
} | ||
} | ||
} | ||
""") | ||
})) | ||
ResponseEntity<?> sendCode(@RequestParam Provider provider, @RequestBody @Validated PhoneVerificationDto.PushCodeReq request); | ||
|
||
@Operation(summary = "[3] 전화번호 인증", description = "전화번호 인증 후 이미 계정이 존재하면 연동, 없으면 회원가입") | ||
@Parameter(name = "provider", description = "소셜 제공자", examples = { | ||
@ExampleObject(name = "카카오", value = "kakao"), @ExampleObject(name = "애플", value = "apple"), @ExampleObject(name = "구글", value = "google") | ||
}, required = true, in = ParameterIn.QUERY) | ||
@ApiResponses({ | ||
@ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", examples = { | ||
@ExampleObject(name = "성공 - 기존 계정 있음 - [4-1]로 진행", value = """ | ||
{ | ||
"code": "2000", | ||
"data": { | ||
"sms": { | ||
"code": true, | ||
"existUser": true, | ||
"username": "pennyway" | ||
} | ||
} | ||
} | ||
"""), | ||
@ExampleObject(name = "성공 - 기존 계정 없음 - [4-2]로 진행", value = """ | ||
{ | ||
"code": "2000", | ||
"data": { | ||
"sms": { | ||
"code": true, | ||
"existUser": false | ||
} | ||
} | ||
} | ||
""") | ||
})), | ||
@ApiResponse(responseCode = "401", content = @Content(mediaType = "application/json", examples = { | ||
@ExampleObject(name = "인증코드 불일치", value = """ | ||
{ | ||
"code": "4010", | ||
"message": "인증코드가 일치하지 않습니다." | ||
} | ||
""") | ||
})), | ||
@ApiResponse(responseCode = "404", content = @Content(mediaType = "application/json", examples = { | ||
@ExampleObject(name = "만료 혹은 등록되지 않은 휴대폰", value = """ | ||
{ | ||
"code": "4042", | ||
"message": "만료되었거나 등록되지 않은 휴대폰 정보입니다." | ||
} | ||
"""), | ||
})) | ||
}) | ||
ResponseEntity<?> verifyCode(@RequestParam Provider provider, @RequestBody @Validated PhoneVerificationDto.VerifyCodeReq request); | ||
|
||
@Operation(summary = "[4-1] 계정 연동", description = "일반 혹은 소셜 계정이 존재하는 경우 연동") | ||
@Parameter(name = "provider", description = "소셜 제공자", examples = { | ||
@ExampleObject(name = "카카오", value = "kakao"), @ExampleObject(name = "애플", value = "apple"), @ExampleObject(name = "구글", value = "google") | ||
}, required = true, in = ParameterIn.QUERY) | ||
ResponseEntity<?> linkAuth(@RequestParam Provider provider, @RequestBody @Validated SignUpReq.SyncWithAuth request); | ||
|
||
@Operation(summary = "[4-2] 소셜 회원가입", description = "회원 정보 입력 후 회원가입") | ||
@Parameter(name = "provider", description = "소셜 제공자", examples = { | ||
@ExampleObject(name = "카카오", value = "kakao"), @ExampleObject(name = "애플", value = "apple"), @ExampleObject(name = "구글", value = "google") | ||
}, required = true, in = ParameterIn.QUERY) | ||
ResponseEntity<?> signUp(@RequestParam Provider provider, @RequestBody @Validated SignUpReq.Oauth request); | ||
} |
81 changes: 81 additions & 0 deletions
81
...p-external-api/src/main/java/kr/co/pennyway/api/apis/auth/controller/OauthController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package kr.co.pennyway.api.apis.auth.controller; | ||
|
||
import kr.co.pennyway.api.apis.auth.api.OauthApi; | ||
import kr.co.pennyway.api.apis.auth.dto.PhoneVerificationDto; | ||
import kr.co.pennyway.api.apis.auth.dto.SignInReq; | ||
import kr.co.pennyway.api.apis.auth.dto.SignUpReq; | ||
import kr.co.pennyway.api.apis.auth.usecase.OauthUseCase; | ||
import kr.co.pennyway.api.common.response.SuccessResponse; | ||
import kr.co.pennyway.api.common.security.jwt.Jwts; | ||
import kr.co.pennyway.api.common.util.CookieUtil; | ||
import kr.co.pennyway.domain.domains.oauth.type.Provider; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.commons.lang3.tuple.Pair; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.ResponseCookie; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.access.prepost.PreAuthorize; | ||
import org.springframework.validation.annotation.Validated; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
import java.time.Duration; | ||
import java.util.Map; | ||
|
||
@Slf4j | ||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/v1/auth/oauth") | ||
public class OauthController implements OauthApi { | ||
private final OauthUseCase oauthUseCase; | ||
private final CookieUtil cookieUtil; | ||
|
||
@Override | ||
@PostMapping("/sign-in") | ||
@PreAuthorize("isAnonymous()") | ||
public ResponseEntity<?> signIn(@RequestParam Provider provider, @RequestBody @Validated SignInReq.Oauth request) { | ||
Pair<Long, Jwts> userInfo = oauthUseCase.signIn(provider, request); | ||
|
||
if (userInfo.getLeft().equals(-1L)) { | ||
return ResponseEntity.ok(SuccessResponse.from("user", Map.of("id", -1))); | ||
} | ||
return createAuthenticatedResponse(userInfo); | ||
} | ||
|
||
@Override | ||
@PostMapping("/phone") | ||
@PreAuthorize("isAnonymous()") | ||
public ResponseEntity<?> sendCode(@RequestParam Provider provider, @RequestBody @Validated PhoneVerificationDto.PushCodeReq request) { | ||
return ResponseEntity.ok(SuccessResponse.from("sms", oauthUseCase.sendCode(provider, request))); | ||
} | ||
|
||
@Override | ||
@PostMapping("/phone/verification") | ||
@PreAuthorize("isAnonymous()") | ||
public ResponseEntity<?> verifyCode(@RequestParam Provider provider, @RequestBody @Validated PhoneVerificationDto.VerifyCodeReq request) { | ||
return ResponseEntity.ok(SuccessResponse.from("sms", oauthUseCase.verifyCode(provider, request))); | ||
} | ||
|
||
@Override | ||
@PostMapping("/link-auth") | ||
@PreAuthorize("isAnonymous()") | ||
public ResponseEntity<?> linkAuth(@RequestParam Provider provider, @RequestBody @Validated SignUpReq.SyncWithAuth request) { | ||
return createAuthenticatedResponse(oauthUseCase.signUp(provider, request.toOauthInfo())); | ||
} | ||
|
||
@Override | ||
@PostMapping("/sign-up") | ||
@PreAuthorize("isAnonymous()") | ||
public ResponseEntity<?> signUp(@RequestParam Provider provider, @RequestBody @Validated SignUpReq.Oauth request) { | ||
return createAuthenticatedResponse(oauthUseCase.signUp(provider, request.toOauthInfo())); | ||
} | ||
|
||
private ResponseEntity<?> createAuthenticatedResponse(Pair<Long, Jwts> userInfo) { | ||
ResponseCookie cookie = cookieUtil.createCookie("refreshToken", userInfo.getValue().refreshToken(), Duration.ofDays(7).toSeconds()); | ||
return ResponseEntity.ok() | ||
.header(HttpHeaders.SET_COOKIE, cookie.toString()) | ||
.header(HttpHeaders.AUTHORIZATION, userInfo.getValue().accessToken()) | ||
.body(SuccessResponse.from("user", Map.of("id", userInfo.getKey()))) | ||
; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.