diff --git a/src/main/java/org/atdev/artrip/domain/admin/exhibit/web/AdminExhibitController.java b/src/main/java/org/atdev/artrip/domain/admin/exhibit/web/AdminExhibitController.java index 634d7af..c3c217f 100644 --- a/src/main/java/org/atdev/artrip/domain/admin/exhibit/web/AdminExhibitController.java +++ b/src/main/java/org/atdev/artrip/domain/admin/exhibit/web/AdminExhibitController.java @@ -16,7 +16,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("admin/exhibits") +@RequestMapping("/admin/exhibits") @Slf4j @Tag(name = "Admin - Exhibit", description = "관리자 전시 관리 API") public class AdminExhibitController { diff --git a/src/main/java/org/atdev/artrip/domain/auth/data/User.java b/src/main/java/org/atdev/artrip/domain/auth/data/User.java index dbe9608..f3d9762 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/data/User.java +++ b/src/main/java/org/atdev/artrip/domain/auth/data/User.java @@ -48,7 +48,8 @@ public class User { @Column(name = "push_token") private String pushToken; - + @Column(nullable = false) + private boolean onboardingCompleted=false; @Email @Column(name = "email",nullable = true) diff --git a/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtAuthenticationFilter.java b/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtAuthenticationFilter.java index 2ce1b94..519921f 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtAuthenticationFilter.java @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StringUtils; @@ -12,6 +13,7 @@ import java.io.IOException; +@Slf4j @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -26,6 +28,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (token != null && jwtProvider.validateToken(token)) { Authentication authentication = jwtProvider.getAuthentication(token); + log.info("Authentication principal: {}", authentication.getPrincipal()); + log.info("Authorities: {}", authentication.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } diff --git a/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtGenerator.java b/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtGenerator.java index dc286ba..29956ec 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtGenerator.java +++ b/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtGenerator.java @@ -33,7 +33,7 @@ public JwtToken generateToken(User user, Role roles) { long now = (new Date()).getTime(); - String authorities = roles.name(); + String authorities = "ROLE_" + roles.name(); String accessToken = Jwts.builder() .setIssuer(jwtIssuer) @@ -60,6 +60,8 @@ public JwtToken generateToken(User user, Role roles) { public String createAccessToken(String subject, String roles) {// refresh 재발행때 사용 long now = System.currentTimeMillis(); +// String authorities = "Role_" + roles; + return Jwts.builder() .setIssuer(jwtIssuer) .setSubject(subject) diff --git a/src/main/java/org/atdev/artrip/domain/auth/service/AuthService.java b/src/main/java/org/atdev/artrip/domain/auth/service/AuthService.java index d7b2970..87220ff 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/service/AuthService.java +++ b/src/main/java/org/atdev/artrip/domain/auth/service/AuthService.java @@ -159,18 +159,22 @@ public SocialLoginResponse loginWithSocial(String provider, String idToken) { ? socialUser.getEmail() : provider.toLowerCase() + socialUser.getProviderId() + "@example.com"; - Optional optionalUser = userRepository.findByEmail(email); - User user; - boolean isFirstLogin; - - if (optionalUser.isPresent()) { - user = optionalUser.get(); - isFirstLogin = false; // 기존 사용자 - } else { - user = createNewUser(socialUser, email, provider); - isFirstLogin = true; // 신규 생성 - } - log.info("user:{}",user); +// Optional optionalUser = userRepository.findByEmail(email); + + User user = userRepository.findByEmail(email). + orElseGet(()->createNewUser(socialUser, email, provider)); + + + boolean isFirstLogin= !user.isOnboardingCompleted(); + +// if (optionalUser.isPresent()) { +// user = optionalUser.get(); +// isFirstLogin = false; // 기존 사용자 +// } else { +// user = createNewUser(socialUser, email, provider); +// isFirstLogin = true; // 신규 생성 +// } +// log.info("user:{}",user); JwtToken jwt = jwtGenerator.generateToken(user, user.getRole()); @@ -187,6 +191,14 @@ public SocialLoginResponse loginWithSocial(String provider, String idToken) { ); } + @Transactional + public void completeOnboarding(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(); + + user.setOnboardingCompleted(!user.isOnboardingCompleted()); + } + private User createNewUser(SocialUserInfo info, String email, String providerStr) { Provider provider = switch (providerStr.toUpperCase()) { @@ -199,6 +211,7 @@ private User createNewUser(SocialUserInfo info, String email, String providerStr .email(email) .name(info.getNickname()) .role(Role.USER) + .onboardingCompleted(false) .build(); SocialAccounts social = SocialAccounts.builder() diff --git a/src/main/java/org/atdev/artrip/domain/auth/web/controller/AuthController.java b/src/main/java/org/atdev/artrip/domain/auth/web/controller/AuthController.java index 036b13f..79416a6 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/web/controller/AuthController.java +++ b/src/main/java/org/atdev/artrip/domain/auth/web/controller/AuthController.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.atdev.artrip.domain.auth.jwt.JwtToken; @@ -18,6 +19,8 @@ import org.atdev.artrip.global.apipayload.code.status.UserError; import org.atdev.artrip.global.swagger.ApiErrorResponses; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; @RestController @@ -27,7 +30,7 @@ public class AuthController { private final AuthService authService; - + @PermitAll @Operation(summary = "토큰 재발행 (웹 전용)", description = "refresh토큰으로 access토큰을 재발행합니다") @ApiErrorResponses( user = {UserError._USER_NOT_FOUND, UserError._INVALID_REFRESH_TOKEN, UserError._INVALID_USER_REFRESH_TOKEN}, @@ -43,6 +46,7 @@ public ResponseEntity> webReissue( return ResponseEntity.ok(CommonResponse.onSuccess(newAccessToken)); } + @PermitAll @Operation(summary = "토큰 재발행 (앱 전용)", description = "refresh토큰으로 access토큰을 재발행합니다") @ApiErrorResponses( user = { @@ -61,6 +65,7 @@ public ResponseEntity> appReissue(@RequestBo return ResponseEntity.ok(CommonResponse.onSuccess(jwt)); } + @PermitAll @Operation(summary = "로그아웃 (웹 전용)", description = "refresh, access 토큰을 제거합니다.") @ApiErrorResponses( user = {UserError._INVALID_REFRESH_TOKEN}, @@ -75,6 +80,7 @@ public ResponseEntity webLogout(@CookieValue(value = "refreshToken", req return ResponseEntity.ok("로그아웃 완료"); } + @PermitAll @Operation(summary = "로그아웃 (앱 전용)", description = "refresh, access 토큰을 제거합니다.") @ApiErrorResponses( user = {UserError._INVALID_REFRESH_TOKEN}, @@ -88,6 +94,7 @@ public ResponseEntity appLogout(@RequestBody(required = false) ReissueRe return ResponseEntity.ok("로그아웃 완료"); } + @PermitAll @Operation(summary = "소셜 SDK 토큰 검증 후 jwt 발급", description = "만료일 : refresh: 7일 , access: 15분 ,isFirstLogin true:회원가입 false:로그인") @ApiErrorResponses( user = { @@ -108,4 +115,17 @@ public ResponseEntity> socialLogin(@RequestB return ResponseEntity.ok(CommonResponse.onSuccess(jwt)); } + @Operation(summary = "isFirstLogin값 반전 api") + @PostMapping("/complete") + public ResponseEntity completeOnboarding( + @AuthenticationPrincipal UserDetails userDetails) { + + long userId = Long.parseLong(userDetails.getUsername()); + + authService.completeOnboarding(userId); + + return ResponseEntity.noContent().build(); + } + + } diff --git a/src/main/java/org/atdev/artrip/domain/keyword/service/KeywordService.java b/src/main/java/org/atdev/artrip/domain/keyword/service/KeywordService.java index 0566744..08a74b8 100644 --- a/src/main/java/org/atdev/artrip/domain/keyword/service/KeywordService.java +++ b/src/main/java/org/atdev/artrip/domain/keyword/service/KeywordService.java @@ -12,6 +12,7 @@ import org.atdev.artrip.global.apipayload.code.status.UserError; import org.atdev.artrip.global.apipayload.exception.GeneralException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; @@ -25,6 +26,7 @@ public class KeywordService { private final UserKeywordRepository userKeywordRepository; private final UserRepository userRepository; + @Transactional public void saveUserKeywords(Long userId, List keywordIds) { User user = userRepository.findById(userId) @@ -45,6 +47,7 @@ public void saveUserKeywords(Long userId, List keywordIds) { userKeywordRepository.saveAll(userKeywords); } + @Transactional public List getAllKeywords() { return keywordRepository.findAll() .stream() @@ -52,6 +55,7 @@ public List getAllKeywords() { .collect(Collectors.toList()); } + @Transactional public List getUserKeywords(Long userId) { return userKeywordRepository.findAllByUserUserId(userId) // UserKeyword 테이블에서 조회 .stream() diff --git a/src/main/java/org/atdev/artrip/global/apipayload/code/status/FavoriteError.java b/src/main/java/org/atdev/artrip/global/apipayload/code/status/FavoriteError.java index efc7a89..ffc8d1e 100644 --- a/src/main/java/org/atdev/artrip/global/apipayload/code/status/FavoriteError.java +++ b/src/main/java/org/atdev/artrip/global/apipayload/code/status/FavoriteError.java @@ -11,7 +11,7 @@ public enum FavoriteError implements BaseErrorCode { _FAVORITE_NOT_FOUND(HttpStatus.NOT_FOUND, "FAVORITE404-NOT_FOUND", "즐겨찾기를 찾을 수 없습니다."), _FAVORITE_ALREADY_EXISTS(HttpStatus.CONFLICT, "FAVORITE409-ALREADY_EXISTS", "이미 즐겨찾기에 추가된 전시입니다."), - _FAVORITE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "FAVORITE4002-LIMIT_EXCEEDED", "즐겨찾기는 최대 100개까지 추가할 수 있습니다."), + _FAVORITE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "FAVORITE400-LIMIT_EXCEEDED", "즐겨찾기는 최대 100개까지 추가할 수 있습니다."), _EXHIBIT_NOT_FOUND_FOR_FAVORITE(HttpStatus.NOT_FOUND, "FAVORITE404-EXHIBIT_NOT_FOUND", "즐겨찾기하려는 전시를 찾을 수 없습니다."), _FAVORITE_UNAUTHORIZED(HttpStatus.FORBIDDEN, "FAVORITE403-UNAUTHORIZED", "다른 사용자의 즐겨찾기에 접근할 수 없습니다."); diff --git a/src/main/java/org/atdev/artrip/global/apipayload/code/status/HomeError.java b/src/main/java/org/atdev/artrip/global/apipayload/code/status/HomeError.java index 4e7407c..b43d24f 100644 --- a/src/main/java/org/atdev/artrip/global/apipayload/code/status/HomeError.java +++ b/src/main/java/org/atdev/artrip/global/apipayload/code/status/HomeError.java @@ -9,10 +9,10 @@ @AllArgsConstructor public enum HomeError implements BaseErrorCode { - _HOME_INVALID_DATE_RANGE(HttpStatus.BAD_REQUEST, "HOME401-INVALID_DATE_RANGE", "전시 기간 설정이 올바르지 않습니다. 종료일이 시작일보다 빠릅니다."), - _HOME_UNRECOGNIZED_REGION(HttpStatus.BAD_REQUEST, "HOME402-UNRECOGNIZED_REGION", "요청하신 국가 또는 지역 정보를 인식할 수 없습니다."), - _HOME_EXHIBIT_NOT_FOUND(HttpStatus.BAD_REQUEST, "HOME404-EXHIBIT_NOT_FOUND", "해당 ID의 전시 상세 정보를 찾을 수 없습니다."), - _HOME_GENRE_NOT_FOUND(HttpStatus.BAD_REQUEST, "HOME404-GENRE_NOT_FOUND", "요청하신 장르에 해당하는 전시 데이터가 없습니다."); + _HOME_INVALID_DATE_RANGE(HttpStatus.BAD_REQUEST, "HOME400-INVALID_DATE_RANGE", "전시 기간 설정이 올바르지 않습니다. 종료일이 시작일보다 빠릅니다."), + _HOME_UNRECOGNIZED_REGION(HttpStatus.BAD_REQUEST, "HOME400-UNRECOGNIZED_REGION", "요청하신 국가 또는 지역 정보를 인식할 수 없습니다."), + _HOME_EXHIBIT_NOT_FOUND(HttpStatus.NOT_FOUND, "HOME404-EXHIBIT_NOT_FOUND", "해당 ID의 전시 상세 정보를 찾을 수 없습니다."), + _HOME_GENRE_NOT_FOUND(HttpStatus.NOT_FOUND, "HOME404-GENRE_NOT_FOUND", "요청하신 장르에 해당하는 전시 데이터가 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/org/atdev/artrip/global/apipayload/code/status/KeywordError.java b/src/main/java/org/atdev/artrip/global/apipayload/code/status/KeywordError.java index 3172d78..eec43d0 100644 --- a/src/main/java/org/atdev/artrip/global/apipayload/code/status/KeywordError.java +++ b/src/main/java/org/atdev/artrip/global/apipayload/code/status/KeywordError.java @@ -10,8 +10,8 @@ public enum KeywordError implements BaseErrorCode { _KEYWORD_NOT_FOUND(HttpStatus.NOT_FOUND, "KEYWORD404-NOT_FOUND", "존재하지 않거나 유효하지 않은 키워드 ID가 요청되었습니다."), - _KEYWORD_SELECTION_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "KEYWORD401-SELECTION_LIMIT_EXCEEDED", "키워드 선택 최대 개수를 초과했습니다."), - _KEYWORD_INVALID_REQUEST(HttpStatus.BAD_REQUEST, "KEYWORD402-INVALID_REQUEST", "키워드 요청 데이터가 올바르지 않습니다."); + _KEYWORD_SELECTION_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "KEYWORD400-SELECTION_LIMIT_EXCEEDED", "키워드 선택 최대 개수를 초과했습니다."), + _KEYWORD_INVALID_REQUEST(HttpStatus.BAD_REQUEST, "KEYWORD400-INVALID_REQUEST", "키워드 요청 데이터가 올바르지 않습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/org/atdev/artrip/global/apipayload/code/status/ReviewError.java b/src/main/java/org/atdev/artrip/global/apipayload/code/status/ReviewError.java index c3defae..b306150 100644 --- a/src/main/java/org/atdev/artrip/global/apipayload/code/status/ReviewError.java +++ b/src/main/java/org/atdev/artrip/global/apipayload/code/status/ReviewError.java @@ -10,7 +10,7 @@ public enum ReviewError implements BaseErrorCode { _REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404-NOT_FOUND", "리뷰 정보를 찾을 수 없습니다."), - _REVIEW_USER_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW403-NO_PERMISSION", "해당 유저에게 리뷰 수정권한이 없습니다."); + _REVIEW_USER_NOT_FOUND(HttpStatus.FORBIDDEN, "REVIEW403-NO_PERMISSION", "해당 유저에게 리뷰 수정권한이 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/org/atdev/artrip/global/apipayload/code/status/UserError.java b/src/main/java/org/atdev/artrip/global/apipayload/code/status/UserError.java index 00e81a2..b61ca9a 100644 --- a/src/main/java/org/atdev/artrip/global/apipayload/code/status/UserError.java +++ b/src/main/java/org/atdev/artrip/global/apipayload/code/status/UserError.java @@ -11,6 +11,7 @@ public enum UserError implements BaseErrorCode { // User Errors _USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404-NOT_FOUND", "존재하지 않는 회원입니다."), + _USER_FORBIDDEN(HttpStatus.FORBIDDEN, "USER403-FORBIDDEN", "접근 권한이 없습니다."), // JWT Errors _JWT_EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "JWT401-EXPIRED_ACCESS", "만료된 엑세스 토큰입니다."), @@ -22,6 +23,7 @@ public enum UserError implements BaseErrorCode { _JWT_EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "JWT401-EXPIRED_REFRESH", "만료된 리프레시 토큰입니다."), _INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "JWT401-INVALID_REFRESH", "리프레시 토큰이 유효하지 않습니다."), _INVALID_USER_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "JWT401-INVALID_USER_REFRESH", "리프레시 토큰에 유저ID가 유효하지 않습니다."), + // Social Login Errors _SOCIAL_VERIFICATION_FAILED(HttpStatus.UNAUTHORIZED, "SOCIAL401-VERIFICATION_FAILED", "소셜 토큰 검증 중 오류가 발생했습니다."), _SOCIAL_ID_TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "IDTOKEN401-INVALID", "소셜 ID 토큰이 유효하지 않습니다."), diff --git a/src/main/java/org/atdev/artrip/global/apipayload/exception/handler/JwtAccessDeniedHandler.java b/src/main/java/org/atdev/artrip/global/apipayload/exception/handler/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..84b4f72 --- /dev/null +++ b/src/main/java/org/atdev/artrip/global/apipayload/exception/handler/JwtAccessDeniedHandler.java @@ -0,0 +1,41 @@ +package org.atdev.artrip.global.apipayload.exception.handler; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.atdev.artrip.global.apipayload.code.status.UserError; +import org.atdev.artrip.global.apipayload.code.ErrorReasonDTO; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + + UserError errorCode = UserError._USER_FORBIDDEN; + + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + + ErrorReasonDTO errorResponse = errorCode.getReasonHttpStatus(); + + response.getWriter().write( + objectMapper.writeValueAsString(errorResponse) + ); + } +} + diff --git a/src/main/java/org/atdev/artrip/global/apipayload/exception/handler/JwtAuthenticationEntryPoint.java b/src/main/java/org/atdev/artrip/global/apipayload/exception/handler/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..9a296bc --- /dev/null +++ b/src/main/java/org/atdev/artrip/global/apipayload/exception/handler/JwtAuthenticationEntryPoint.java @@ -0,0 +1,39 @@ +package org.atdev.artrip.global.apipayload.exception.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.atdev.artrip.global.apipayload.code.status.UserError; +import org.atdev.artrip.global.apipayload.code.ErrorReasonDTO; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + + UserError errorCode = UserError._JWT_INVALID_TOKEN; + + response.setStatus(errorCode.getHttpStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + + ErrorReasonDTO errorResponse = errorCode.getReasonHttpStatus(); + + response.getWriter().write( + objectMapper.writeValueAsString(errorResponse) + ); + } +} diff --git a/src/main/java/org/atdev/artrip/global/config/SecurityConfig.java b/src/main/java/org/atdev/artrip/global/config/SecurityConfig.java index 95eae5a..3c555ff 100644 --- a/src/main/java/org/atdev/artrip/global/config/SecurityConfig.java +++ b/src/main/java/org/atdev/artrip/global/config/SecurityConfig.java @@ -7,6 +7,8 @@ import org.atdev.artrip.domain.auth.jwt.JwtAuthenticationFilter; import org.atdev.artrip.domain.auth.jwt.JwtProvider; import org.atdev.artrip.domain.auth.jwt.exception.JwtExceptionFilter; +import org.atdev.artrip.global.apipayload.exception.handler.JwtAccessDeniedHandler; +import org.atdev.artrip.global.apipayload.exception.handler.JwtAuthenticationEntryPoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -31,6 +33,8 @@ public class SecurityConfig { private final JwtProvider jwtProvider; private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; private final ObjectMapper objectMapper; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final JwtAccessDeniedHandler jwtAccessDeniedHandler; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -43,12 +47,17 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth + .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers("/","/a","/login/**", "/oauth2/**", "/error", "/swagger-ui/**", "/v3/api-docs/**","/auth/web/reissue","/auth/app/reissue","/s3/**","/auth/social").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/swagger-ui.html", "/webjars/**").permitAll()//스웨거 에러 .anyRequest().authenticated() ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(jwtAuthenticationEntryPoint) + .accessDeniedHandler(jwtAccessDeniedHandler) + ) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .userService(oAuth2UserService) diff --git a/src/main/java/org/atdev/artrip/global/swagger/GlobalOperationCustomizer.java b/src/main/java/org/atdev/artrip/global/swagger/GlobalOperationCustomizer.java index 4521b8a..4b72c74 100644 --- a/src/main/java/org/atdev/artrip/global/swagger/GlobalOperationCustomizer.java +++ b/src/main/java/org/atdev/artrip/global/swagger/GlobalOperationCustomizer.java @@ -7,7 +7,10 @@ import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; +import jakarta.annotation.security.PermitAll; import org.atdev.artrip.global.apipayload.code.BaseErrorCode; +import org.atdev.artrip.global.apipayload.code.status.CommonError; +import org.atdev.artrip.global.apipayload.code.status.UserError; import org.springdoc.core.customizers.OperationCustomizer; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; @@ -19,17 +22,39 @@ @Component public class GlobalOperationCustomizer implements OperationCustomizer { + private boolean isSecuredApi(HandlerMethod handlerMethod) { + return !( + handlerMethod.hasMethodAnnotation(PermitAll.class) + || handlerMethod.getBeanType().isAnnotationPresent(PermitAll.class) + ); + } + @Override public Operation customize(Operation operation, HandlerMethod handlerMethod) { ApiResponses responses = operation.getResponses(); + if (isSecuredApi(handlerMethod)) { + addOrMergeErrorResponseWithExamples( + responses, + "401", + List.of( + UserError._JWT_INVALID_TOKEN, + UserError._JWT_EXPIRED_ACCESS_TOKEN, + UserError._SOCIAL_TOKEN_INVALID_SIGNATURE, + UserError._JWT_UNSUPPORTED_TOKEN + ) + ); + } + ApiErrorResponses annotation = handlerMethod.getMethodAnnotation(ApiErrorResponses.class); if (annotation != null) { processAllErrorAttributes(responses, annotation); } + return operation; } + private void processAllErrorAttributes(ApiResponses responses, ApiErrorResponses annotation) { List allErrors = new ArrayList<>(); @@ -52,40 +77,45 @@ private void processAllErrorAttributes(ApiResponses responses, ApiErrorResponses )); errorsByStatusCode.forEach((statusCode, errors) -> { - addErrorResponseWithMultipleExamples(responses, statusCode, errors); + addOrMergeErrorResponseWithExamples(responses, statusCode, errors); }); } - private void addErrorResponseWithMultipleExamples(ApiResponses responses, String statusCode, List errors) { - ApiResponse apiResponse = new ApiResponse(); - - String description = errors.get(0).getHttpStatus().getReasonPhrase(); - apiResponse.setDescription(description); - - Content content = new Content(); - MediaType mediaType = new MediaType(); + private void addOrMergeErrorResponseWithExamples( + ApiResponses responses, + String statusCode, + List errorsToAdd + ) { + ApiResponse apiResponse = responses.get(statusCode); + if (apiResponse == null) { + apiResponse = new ApiResponse(); + apiResponse.setDescription(errorsToAdd.get(0).getHttpStatus().getReasonPhrase()); + apiResponse.setContent(new Content()); + responses.addApiResponse(statusCode, apiResponse); + } - Schema schema = new Schema<>(); - schema.set$ref("#/components/schemas/CommonResponse"); - mediaType.setSchema(schema); + MediaType mediaType = apiResponse.getContent().get("application/json"); + if (mediaType == null) { + mediaType = new MediaType(); + mediaType.setSchema(new Schema<>().$ref("#/components/schemas/CommonResponse")); + apiResponse.getContent().addMediaType("application/json", mediaType); + } - Map exampleMap = new LinkedHashMap<>(); - for (BaseErrorCode error :errors) { - Example example = new Example(); - example.setValue(createErrorExample(error)); - example.setDescription(error.getMessage()); + Map exampleMap = mediaType.getExamples(); + if (exampleMap == null) { + exampleMap = new LinkedHashMap<>(); + } - exampleMap.put(error.getCode(), example); + for (BaseErrorCode error : errorsToAdd) { + exampleMap.putIfAbsent(error.getCode(), new Example() + .value(createErrorExample(error)) + .description(error.getMessage())); } mediaType.setExamples(exampleMap); - content.addMediaType("application/json", mediaType); - apiResponse.setContent(content); - - responses.addApiResponse(statusCode, apiResponse); - } + private Map createErrorExample(BaseErrorCode error) { Map example = new LinkedHashMap<>(); example.put("isSuccess", false); diff --git a/src/main/resources/db/migration/V2__add_onboarding_completed_to_user.sql b/src/main/resources/db/migration/V2__add_onboarding_completed_to_user.sql new file mode 100644 index 0000000..9923d9c --- /dev/null +++ b/src/main/resources/db/migration/V2__add_onboarding_completed_to_user.sql @@ -0,0 +1,3 @@ +ALTER TABLE `user` + ADD COLUMN `onboarding_completed` TINYINT(1) +NOT NULL DEFAULT 0; \ No newline at end of file