-
Notifications
You must be signed in to change notification settings - Fork 82
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[1 - 2단계 방탈출 예약 대기] 이든(최승준) 미션 제출합니다. (#86)
* init: 이전 미션 코드 반영 * refactor: Jdbc -> Jpa 마이그레이션 * fix: 인기테마 조회 API Endpoint 변경 * feat: 내 예약목록 조회 API 구현 * style: 처리한 TODO 제거 * style: 처리한 TODO 제거 * refactor: LAZY 로딩 적용 * refactor: Spring-validation 적용 * fix: 중복 메서드 제거 * fix: 코드라인 정리 * refactor: JPA Column Nullable 설정 * refactor: AuthInterceptor 분리 * refactor: 인증 Interceptor 코드 라인 정리 * feat: 회원가입 API 구현 * feat: Cookie MaxAge 설정 * feat: Logout API 구현 * refactor: 적용하지 않은 TODO 제거 * docs: auth API 관련 문서 작성 * refactor: Entity 객체 원시타입 필드에 VO 적용 * fix: 검증 기준 획일화 * feat: Reservation 객체 검증 로직 추가 * test: Member, Theme 객체 검증 로직 작성 * refactor: nativeQuery 사용 로직 제거 * fix: 누락된 Embeddable 에노테이션 추가 * docs: ERD 추가 * refactor: VO 필드명 도메인적으로 변경 - AttributeOverride 애노테이션 제거를 위한 목적 * test: Auth 관련 기능 테스트 구현 * refactor: ReservationTime에서 지난 시간인지 직접 검증하도록 수정 * refactor: 내 예약목록 조회 API Endpoint를 RESTful하게 변경 * test: 내 예약목록 조회 API 테스트 작성
- Loading branch information
Showing
117 changed files
with
7,113 additions
and
18 deletions.
There are no files selected for viewing
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
98 changes: 98 additions & 0 deletions
98
src/main/java/roomescape/auth/controller/AuthController.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,98 @@ | ||
package roomescape.auth.controller; | ||
|
||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import jakarta.validation.Valid; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import roomescape.auth.dto.LoginCheckResponse; | ||
import roomescape.auth.dto.LoginRequest; | ||
import roomescape.auth.dto.SignUpRequest; | ||
import roomescape.auth.service.AuthService; | ||
import roomescape.global.auth.annotation.Auth; | ||
import roomescape.global.auth.annotation.MemberId; | ||
import roomescape.global.auth.jwt.JwtHandler; | ||
import roomescape.global.auth.jwt.dto.TokenDto; | ||
import roomescape.global.dto.response.ApiResponse; | ||
|
||
@RestController | ||
public class AuthController { | ||
private final AuthService authService; | ||
|
||
public AuthController(final AuthService authService) { | ||
this.authService = authService; | ||
} | ||
|
||
@PostMapping("/signup") | ||
public ApiResponse<Void> signup(@Valid @RequestBody final SignUpRequest signupRequest, final HttpServletResponse response) { | ||
TokenDto tokenDto = authService.signUp(signupRequest); | ||
addTokensToCookie(tokenDto, response); | ||
|
||
return ApiResponse.success(); | ||
} | ||
|
||
@PostMapping("/login") | ||
public ApiResponse<Void> login(@Valid @RequestBody final LoginRequest loginRequest, final HttpServletResponse response) { | ||
TokenDto tokenDto = authService.login(loginRequest); | ||
addTokensToCookie(tokenDto, response); | ||
|
||
return ApiResponse.success(); | ||
} | ||
|
||
@PostMapping("/logout") | ||
public ApiResponse<Void> logout(final HttpServletResponse response) { | ||
TokenDto logoutTokenDto = authService.logout(); | ||
addTokensToCookie(logoutTokenDto, response); | ||
|
||
return ApiResponse.success(); | ||
} | ||
|
||
@Auth | ||
@GetMapping("/login/check") | ||
public ApiResponse<LoginCheckResponse> checkLogin(@MemberId final Long memberId) { | ||
LoginCheckResponse response = authService.checkLogin(memberId); | ||
return ApiResponse.success(response); | ||
} | ||
|
||
// TODO: 토큰 재발급 자동화 로직 구현 | ||
@GetMapping("/token-reissue") | ||
public ApiResponse<Void> reissueToken(final HttpServletRequest request, final HttpServletResponse response) { | ||
TokenDto requestToken = getTokenFromCookie(request); | ||
|
||
TokenDto tokenInfo = authService.reissueToken(requestToken.accessToken(), requestToken.refreshToken()); | ||
addTokensToCookie(tokenInfo, response); | ||
|
||
return ApiResponse.success(); | ||
} | ||
|
||
private TokenDto getTokenFromCookie(final HttpServletRequest request) { | ||
String accessToken = ""; | ||
String refreshToken = ""; | ||
for (Cookie cookie : request.getCookies()) { | ||
if (cookie.getName().equals(JwtHandler.ACCESS_TOKEN_HEADER_KEY)) { | ||
accessToken = cookie.getValue(); | ||
} | ||
if (cookie.getName().equals(JwtHandler.REFRESH_TOKEN_HEADER_KEY)) { | ||
refreshToken = cookie.getValue(); | ||
} | ||
} | ||
|
||
return new TokenDto(accessToken, refreshToken); | ||
} | ||
|
||
private void addTokensToCookie(TokenDto tokenInfo, HttpServletResponse response) { | ||
addTokenToCookie(JwtHandler.ACCESS_TOKEN_HEADER_KEY, tokenInfo.accessToken(), response, JwtHandler.ACCESS_TOKEN_EXPIRE_TIME); | ||
addTokenToCookie(JwtHandler.REFRESH_TOKEN_HEADER_KEY, tokenInfo.refreshToken(), response, JwtHandler.REFRESH_TOKEN_EXPIRE_TIME); | ||
} | ||
|
||
private void addTokenToCookie(String cookieName, String token, HttpServletResponse response, int maxAge) { | ||
Cookie cookie = new Cookie(cookieName, token); | ||
cookie.setHttpOnly(true); | ||
cookie.setMaxAge(maxAge); | ||
|
||
response.addCookie(cookie); | ||
} | ||
} |
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,6 @@ | ||
package roomescape.auth.dto; | ||
|
||
public record LoginCheckResponse( | ||
String name | ||
) { | ||
} |
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,13 @@ | ||
package roomescape.auth.dto; | ||
|
||
import jakarta.validation.constraints.Email; | ||
import jakarta.validation.constraints.NotBlank; | ||
|
||
public record LoginRequest( | ||
@NotBlank(message = "이메일은 null 또는 공백일 수 없습니다.") | ||
@Email(message = "이메일 형식이 일치하지 않습니다. (xxx@xxx.xxx)") | ||
String email, | ||
@NotBlank(message = "비밀번호는 null 또는 공백일 수 없습니다.") | ||
String password | ||
) { | ||
} |
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,27 @@ | ||
package roomescape.auth.dto; | ||
|
||
import jakarta.validation.constraints.Email; | ||
import jakarta.validation.constraints.NotBlank; | ||
import jakarta.validation.constraints.Size; | ||
import roomescape.member.domain.Member; | ||
import roomescape.member.domain.MemberName; | ||
|
||
public record SignUpRequest( | ||
@NotBlank(message = "회원명은 null 또는 공백일 수 없습니다.") | ||
@Size(min = MemberName.MIN_LENGTH, max = MemberName.MAX_LENGTH, message = "회원명은 " + MemberName.MIN_LENGTH + "~" + MemberName.MAX_LENGTH + "글자 사이여야 합니다.") | ||
String name, | ||
@NotBlank(message = "이메일은 null 또는 공백일 수 없습니다.") | ||
@Email(message = "이메일 형식이 일치하지 않습니다. (xxx@xxx.xxx)") | ||
String email, | ||
@NotBlank(message = "비밀번호는 null 또는 공백일 수 없습니다.") | ||
String password | ||
) { | ||
public Member toMemberEntity() { | ||
return new Member( | ||
name, | ||
email, | ||
password | ||
); | ||
} | ||
|
||
} |
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,56 @@ | ||
package roomescape.auth.service; | ||
|
||
import org.springframework.stereotype.Service; | ||
import roomescape.auth.dto.LoginCheckResponse; | ||
import roomescape.auth.dto.LoginRequest; | ||
import roomescape.auth.dto.SignUpRequest; | ||
import roomescape.global.auth.jwt.JwtHandler; | ||
import roomescape.global.auth.jwt.dto.TokenDto; | ||
import roomescape.global.exception.error.ErrorType; | ||
import roomescape.global.exception.model.UnauthorizedException; | ||
import roomescape.member.domain.Member; | ||
import roomescape.member.service.MemberService; | ||
|
||
@Service | ||
public class AuthService { | ||
private final MemberService memberService; | ||
private final JwtHandler jwtHandler; | ||
|
||
public AuthService(final MemberService memberService, final JwtHandler jwtHandler) { | ||
this.memberService = memberService; | ||
this.jwtHandler = jwtHandler; | ||
} | ||
|
||
public TokenDto signUp(final SignUpRequest signupRequest) { | ||
Member member = memberService.addMember(signupRequest); | ||
|
||
return jwtHandler.createToken(member.getId()); | ||
} | ||
|
||
public TokenDto login(final LoginRequest request) { | ||
Member member = memberService.findMemberByEmailAndPassword(request); | ||
|
||
return jwtHandler.createToken(member.getId()); | ||
} | ||
|
||
public LoginCheckResponse checkLogin(final Long memberId) { | ||
Member member = memberService.findMemberById(memberId); | ||
|
||
return new LoginCheckResponse(member.getName()); | ||
} | ||
|
||
public TokenDto reissueToken(final String accessToken, final String refreshToken) { | ||
try { | ||
jwtHandler.validateToken(refreshToken); | ||
} catch (UnauthorizedException e) { | ||
throw new UnauthorizedException(ErrorType.INVALID_REFRESH_TOKEN, ErrorType.INVALID_REFRESH_TOKEN.getDescription(), e); | ||
} | ||
|
||
Long memberId = jwtHandler.getMemberIdFromTokenWithNotValidate(accessToken); | ||
return jwtHandler.createToken(memberId); | ||
} | ||
|
||
public TokenDto logout() { | ||
return jwtHandler.createLogoutToken(); | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
src/main/java/roomescape/global/auth/annotation/Admin.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,11 @@ | ||
package roomescape.global.auth.annotation; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Target(ElementType.METHOD) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface Admin { | ||
} |
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,11 @@ | ||
package roomescape.global.auth.annotation; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Target(ElementType.METHOD) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface Auth { | ||
} |
11 changes: 11 additions & 0 deletions
11
src/main/java/roomescape/global/auth/annotation/MemberId.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,11 @@ | ||
package roomescape.global.auth.annotation; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Target(ElementType.PARAMETER) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface MemberId { | ||
} |
67 changes: 67 additions & 0 deletions
67
src/main/java/roomescape/global/auth/interceptor/AdminInterceptor.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,67 @@ | ||
package roomescape.global.auth.interceptor; | ||
|
||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.method.HandlerMethod; | ||
import org.springframework.web.servlet.HandlerInterceptor; | ||
import roomescape.global.auth.annotation.Admin; | ||
import roomescape.global.auth.jwt.JwtHandler; | ||
import roomescape.global.auth.jwt.constant.JwtKey; | ||
import roomescape.global.exception.error.ErrorType; | ||
import roomescape.global.exception.model.ForbiddenException; | ||
import roomescape.global.exception.model.UnauthorizedException; | ||
import roomescape.member.domain.Member; | ||
import roomescape.member.domain.Role; | ||
import roomescape.member.service.MemberService; | ||
|
||
@Component | ||
public class AdminInterceptor implements HandlerInterceptor { | ||
private final MemberService memberService; | ||
private final JwtHandler jwtHandler; | ||
|
||
public AdminInterceptor(final MemberService memberService, final JwtHandler jwtHandler) { | ||
this.memberService = memberService; | ||
this.jwtHandler = jwtHandler; | ||
} | ||
|
||
@Override | ||
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) { | ||
if (!(handler instanceof HandlerMethod)) { | ||
return true; | ||
} | ||
HandlerMethod handlerMethod = (HandlerMethod) handler; | ||
if (handlerMethod.getMethodAnnotation(Admin.class) == null) { | ||
return true; | ||
} | ||
|
||
request.setAttribute(JwtKey.MEMBER_ID.getValue(), parseMemberIdFromRequest(request)); | ||
return true; | ||
} | ||
|
||
private Long parseMemberIdFromRequest(final HttpServletRequest request) { | ||
String cookieHeader = request.getHeader("Cookie"); | ||
if (cookieHeader != null) { | ||
for (Cookie cookie : request.getCookies()) { | ||
if (cookie.getName().equals(JwtHandler.ACCESS_TOKEN_HEADER_KEY)) { | ||
String accessToken = cookie.getValue(); | ||
Long memberId = jwtHandler.getMemberIdFromTokenWithValidate(accessToken); | ||
Member member = memberService.findMemberById(memberId); | ||
checkRole(member); | ||
|
||
return memberId; | ||
} | ||
} | ||
} | ||
throw new UnauthorizedException(ErrorType.INVALID_TOKEN, ErrorType.INVALID_TOKEN.getDescription()); | ||
} | ||
|
||
private boolean checkRole(final Member member) { | ||
if (member.isRole(Role.ADMIN)) { | ||
return true; | ||
} | ||
throw new ForbiddenException(ErrorType.PERMISSION_DOES_NOT_EXIST, | ||
String.format("회원 권한이 존재하지 않아 접근할 수 없습니다. [memberId: %d, Role: %s]", member.getId(), member.getRole())); | ||
} | ||
} |
Oops, something went wrong.