diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java index ea9418eab..ba2c10d03 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java @@ -6,10 +6,8 @@ import com.gdschongik.gdsc.domain.member.dto.request.MemberPaymentRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; -import com.gdschongik.gdsc.domain.member.dto.response.MemberFindAllResponse; +import com.gdschongik.gdsc.domain.member.dto.response.AdminMemberResponse; import com.gdschongik.gdsc.domain.member.dto.response.MemberGrantResponse; -import com.gdschongik.gdsc.domain.member.dto.response.MemberPaymentFindAllResponse; -import com.gdschongik.gdsc.domain.member.dto.response.MemberPendingFindAllResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -36,8 +34,8 @@ public class AdminMemberController { @Operation(summary = "전체 회원 목록 조회", description = "전체 회원 목록을 조회합니다.") @GetMapping - public ResponseEntity> getMembers(MemberQueryRequest queryRequest, Pageable pageable) { - Page response = adminMemberService.findAll(queryRequest, pageable); + public ResponseEntity> getMembers(MemberQueryRequest queryRequest, Pageable pageable) { + Page response = adminMemberService.findAll(queryRequest, pageable); return ResponseEntity.ok().body(response); } @@ -50,9 +48,9 @@ public ResponseEntity withdrawMember(@PathVariable Long memberId) { @Operation(summary = "대기중인 회원 목록 조회", description = "대기중인 회원 목록을 조회합니다.") @GetMapping("/pending") - public ResponseEntity> getPendingMembers( + public ResponseEntity> getPendingMembers( MemberQueryRequest queryRequest, Pageable pageable) { - Page response = adminMemberService.findAllPendingMembers(queryRequest, pageable); + Page response = adminMemberService.findAllPendingMembers(queryRequest, pageable); return ResponseEntity.ok().body(response); } @@ -73,19 +71,19 @@ public ResponseEntity grantMember(@Valid @RequestBody Membe @Operation(summary = "승인 가능 회원 전체 조회", description = "승인 가능한 회원 전체를 조회합니다.") @GetMapping("/grantable") - public ResponseEntity> getGrantableMembers( + public ResponseEntity> getGrantableMembers( MemberQueryRequest queryRequest, Pageable pageable) { - Page response = adminMemberService.getGrantableMembers(queryRequest, pageable); + Page response = adminMemberService.getGrantableMembers(queryRequest, pageable); return ResponseEntity.ok().body(response); } @Operation(summary = "회비 납부 상태에 따른 회원 전체 조회", description = "회비 납부 상태에 따라 회원 목록을 조회합니다.") @GetMapping("/payment") - public ResponseEntity> getMembersByPaymentStatus( + public ResponseEntity> getMembersByPaymentStatus( MemberQueryRequest queryRequest, @RequestParam(name = "status", required = false) RequirementStatus paymentStatus, Pageable pageable) { - Page response = + Page response = adminMemberService.getMembersByPaymentStatus(queryRequest, paymentStatus, pageable); return ResponseEntity.ok().body(response); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java index c463d1742..7ea57a036 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java @@ -10,10 +10,8 @@ import com.gdschongik.gdsc.domain.member.dto.request.MemberPaymentRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberQueryRequest; import com.gdschongik.gdsc.domain.member.dto.request.MemberUpdateRequest; -import com.gdschongik.gdsc.domain.member.dto.response.MemberFindAllResponse; +import com.gdschongik.gdsc.domain.member.dto.response.AdminMemberResponse; import com.gdschongik.gdsc.domain.member.dto.response.MemberGrantResponse; -import com.gdschongik.gdsc.domain.member.dto.response.MemberPaymentFindAllResponse; -import com.gdschongik.gdsc.domain.member.dto.response.MemberPendingFindAllResponse; import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.exception.ErrorCode; import java.util.List; @@ -31,9 +29,9 @@ public class AdminMemberService { private final MemberRepository memberRepository; - public Page findAll(MemberQueryRequest queryRequest, Pageable pageable) { + public Page findAll(MemberQueryRequest queryRequest, Pageable pageable) { Page members = memberRepository.findAll(queryRequest, pageable); - return members.map(MemberFindAllResponse::of); + return members.map(AdminMemberResponse::from); } @Transactional @@ -56,10 +54,9 @@ public void updateMember(Long memberId, MemberUpdateRequest request) { request.nickname()); } - public Page findAllPendingMembers( - MemberQueryRequest queryRequest, Pageable pageable) { + public Page findAllPendingMembers(MemberQueryRequest queryRequest, Pageable pageable) { Page members = memberRepository.findAllByRole(queryRequest, MemberRole.GUEST, pageable); - return members.map(MemberPendingFindAllResponse::of); + return members.map(AdminMemberResponse::from); } @Transactional @@ -70,15 +67,15 @@ public MemberGrantResponse grantMember(MemberGrantRequest request) { return MemberGrantResponse.from(classifiedMember); } - public Page getGrantableMembers(MemberQueryRequest queryRequest, Pageable pageable) { + public Page getGrantableMembers(MemberQueryRequest queryRequest, Pageable pageable) { Page members = memberRepository.findAllGrantable(queryRequest, pageable); - return members.map(MemberFindAllResponse::of); + return members.map(AdminMemberResponse::from); } - public Page getMembersByPaymentStatus( + public Page getMembersByPaymentStatus( MemberQueryRequest queryRequest, RequirementStatus paymentStatus, Pageable pageable) { Page members = memberRepository.findAllByPaymentStatus(queryRequest, paymentStatus, pageable); - return members.map(MemberPaymentFindAllResponse::from); + return members.map(AdminMemberResponse::from); } @Transactional diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java index 3afb101bc..a0f55aa2a 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dao/MemberCustomRepositoryImpl.java @@ -177,12 +177,16 @@ private BooleanBuilder queryOption(MemberQueryRequest queryRequest) { .and(eqStudentId(queryRequest.studentId())) .and(eqName(queryRequest.name())) .and(eqPhone(queryRequest.phone())) - .and(eqDepartment(queryRequest.department())) + .and(inDepartmentList(Department.getDepartmentCodes(queryRequest.department()))) .and(eqEmail(queryRequest.email())) .and(eqDiscordUsername(queryRequest.discordUsername())) .and(eqNickname(queryRequest.nickname())); } + private BooleanExpression inDepartmentList(List departmentCodes) { + return departmentCodes != null ? member.department.in(departmentCodes) : null; + } + private BooleanExpression eqStudentId(String studentId) { return studentId != null ? member.studentId.containsIgnoreCase(studentId) : null; } @@ -195,10 +199,6 @@ private BooleanExpression eqPhone(String phone) { return phone != null ? member.phone.contains(phone.replaceAll("-", "")) : null; } - private BooleanExpression eqDepartment(Department department) { - return department != null ? member.department.eq(department) : null; - } - private BooleanExpression eqEmail(String email) { return email != null ? member.email.containsIgnoreCase(email) : null; } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Department.java b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Department.java index aadaf5c74..184a77168 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/domain/Department.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/domain/Department.java @@ -1,5 +1,8 @@ package com.gdschongik.gdsc.domain.member.domain; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; import lombok.AllArgsConstructor; import lombok.Getter; @@ -85,4 +88,12 @@ public enum Department { D077("영어교육과"); private String departmentName; + + public static List getDepartmentCodes(String keyword) { + return Optional.ofNullable(keyword) + .map(s -> Arrays.stream(Department.values()) + .filter(department -> department.getDepartmentName().contains(s)) + .toList()) + .orElse(null); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java index ac3d23799..56ac95fa8 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/request/MemberQueryRequest.java @@ -2,14 +2,13 @@ import static com.gdschongik.gdsc.global.common.constant.RegexConstant.*; -import com.gdschongik.gdsc.domain.member.domain.Department; import io.swagger.v3.oas.annotations.media.Schema; public record MemberQueryRequest( @Schema(description = "학번", pattern = STUDENT_ID) String studentId, @Schema(description = "이름") String name, @Schema(description = "전화번호", pattern = PHONE_WITHOUT_HYPHEN) String phone, - @Schema(description = "학과") Department department, + @Schema(description = "학과") String department, @Schema(description = "이메일") String email, @Schema(description = "디스코드 유저네임") String discordUsername, @Schema(description = "커뮤니티 닉네임", pattern = NICKNAME) String nickname) {} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/AdminMemberResponse.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/AdminMemberResponse.java new file mode 100644 index 000000000..312a6f216 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/AdminMemberResponse.java @@ -0,0 +1,52 @@ +package com.gdschongik.gdsc.domain.member.dto.response; + +import com.gdschongik.gdsc.domain.member.domain.Department; +import com.gdschongik.gdsc.domain.member.domain.Member; +import com.gdschongik.gdsc.domain.member.domain.Requirement; +import java.util.Optional; + +public record AdminMemberResponse( + Long memberId, + String studentId, + String name, + String phone, + DepartmentDto department, + String email, + String discordUsername, + String nickname, + RequirementDto requirement) { + + public static AdminMemberResponse from(Member member) { + return new AdminMemberResponse( + member.getId(), + member.getStudentId(), + member.getName(), + Optional.ofNullable(member.getPhone()) + .map(phone -> String.format( + "%s-%s-%s", phone.substring(0, 3), phone.substring(3, 7), phone.substring(7))) + .orElse(null), + DepartmentDto.from(member.getDepartment()), + member.getEmail(), + member.getDiscordUsername(), + member.getNickname(), + RequirementDto.from(member.getRequirement())); + } + + record DepartmentDto(Department code, String name) { + public static DepartmentDto from(Department department) { + return Optional.ofNullable(department) + .map(code -> new DepartmentDto(code, code.getDepartmentName())) + .orElse(new DepartmentDto(null, null)); + } + } + + record RequirementDto(String univStatus, String discordStatus, String paymentStatus, String bevyStatus) { + public static RequirementDto from(Requirement requirement) { + return new RequirementDto( + requirement.getUnivStatus().name(), + requirement.getDiscordStatus().name(), + requirement.getPaymentStatus().name(), + requirement.getBevyStatus().name()); + } + } +} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberFindAllResponse.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberFindAllResponse.java deleted file mode 100644 index a0bf28b93..000000000 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberFindAllResponse.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.gdschongik.gdsc.domain.member.dto.response; - -import com.gdschongik.gdsc.domain.member.domain.Department; -import com.gdschongik.gdsc.domain.member.domain.Member; -import java.util.Optional; - -public record MemberFindAllResponse( - Long memberId, - String studentId, - String name, - String phone, - String department, - String email, - String discordUsername, - String nickname) { - - public static MemberFindAllResponse of(Member member) { - return new MemberFindAllResponse( - member.getId(), - member.getStudentId(), - member.getName(), - Optional.ofNullable(member.getPhone()) - .map(phone -> String.format( - "%s-%s-%s", phone.substring(0, 3), phone.substring(3, 7), phone.substring(7))) - .orElse(null), - Optional.ofNullable(member.getDepartment()) - .map(Department::getDepartmentName) - .orElse(null), - member.getEmail(), - member.getDiscordUsername(), - member.getNickname()); - } -} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberPaymentFindAllResponse.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberPaymentFindAllResponse.java deleted file mode 100644 index 97d3b9482..000000000 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberPaymentFindAllResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.gdschongik.gdsc.domain.member.dto.response; - -import com.gdschongik.gdsc.domain.member.domain.Member; -import com.gdschongik.gdsc.domain.member.domain.RequirementStatus; - -public record MemberPaymentFindAllResponse( - Long memberId, - String studentId, - String name, - String phone, - String discordUsername, - String nickname, - RequirementStatus paymentStatus) { - public static MemberPaymentFindAllResponse from(Member member) { - return new MemberPaymentFindAllResponse( - member.getId(), - member.getStudentId(), - member.getName(), - member.getPhone(), - member.getDiscordUsername(), - member.getNickname(), - member.getRequirement().getPaymentStatus()); - } -} diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberPendingFindAllResponse.java b/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberPendingFindAllResponse.java deleted file mode 100644 index 4f216d151..000000000 --- a/src/main/java/com/gdschongik/gdsc/domain/member/dto/response/MemberPendingFindAllResponse.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.gdschongik.gdsc.domain.member.dto.response; - -import com.gdschongik.gdsc.domain.member.domain.Department; -import com.gdschongik.gdsc.domain.member.domain.Member; -import com.gdschongik.gdsc.domain.member.domain.Requirement; -import java.util.Optional; - -public record MemberPendingFindAllResponse( - Long memberId, - String studentId, - String name, - String phone, - String department, - String email, - String discordUsername, - String nickname, - Requirement requirement) { - - public static MemberPendingFindAllResponse of(Member member) { - return new MemberPendingFindAllResponse( - member.getId(), - member.getStudentId(), - member.getName(), - Optional.ofNullable(member.getPhone()) - .map(phone -> String.format( - "%s-%s-%s", phone.substring(0, 3), phone.substring(3, 7), phone.substring(7))) - .orElse(null), - Optional.ofNullable(member.getDepartment()) - .map(Department::getDepartmentName) - .orElse(null), - member.getEmail(), - member.getDiscordUsername(), - member.getNickname(), - member.getRequirement()); - } -} diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java index 77feb0f8a..ebb0770c3 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/SecurityConstant.java @@ -3,10 +3,14 @@ public class SecurityConstant { public static final String LANDING_STATUS_PARAM = "landing-status"; + public static final String ACCESS_TOKEN_PARAM = "access"; + public static final String REFRESH_TOKEN_PARAM = "refresh"; public static final String TOKEN_ROLE_NAME = "role"; public static final String GITHUB_NAME_ATTR_KEY = "id"; public static final String ACCESS_TOKEN_HEADER_PREFIX = "Bearer "; public static final String ACCESS_TOKEN_HEADER_NAME = "Authorization"; + public static final String OAUTH_BASE_URI_COOKIE_NAME = "oauth-base-uri"; + public static final String OAUTH_REDIRECT_PATH_SEGMENT = "/social-login/redirect"; private SecurityConstant() {} } diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java index d824dfeb2..07d575690 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java @@ -2,20 +2,29 @@ import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.*; +import java.util.List; + public class UrlConstant { private UrlConstant() {} - public static final String PROD_CLIENT_URL = "https://onboarding.gdschongik.com"; - public static final String DEV_CLIENT_URL = "https://dev-onboarding.gdschongik.com"; + // 클라이언트 URL + public static final String PROD_CLIENT_ONBOARDING_URL = "https://onboarding.gdschongik.com"; + public static final String PROD_CLIENT_ADMIN_URL = "https://admin.gdschongik.com"; + public static final String DEV_CLIENT_ONBOARDING_URL = "https://dev-onboarding.gdschongik.com"; + public static final String DEV_CLIENT_ADMIN_URL = "https://dev-admin.gdschongik.com"; public static final String LOCAL_REACT_CLIENT_URL = "http://localhost:3000"; public static final String LOCAL_REACT_CLIENT_SECURE_URL = "https://localhost:3000"; public static final String LOCAL_VITE_CLIENT_URL = "http://localhost:5173"; public static final String LOCAL_VITE_CLIENT_SECURE_URL = "https://localhost:5173"; + public static final List LOCAL_CLIENT_URLS = List.of( + LOCAL_REACT_CLIENT_URL, LOCAL_REACT_CLIENT_SECURE_URL, LOCAL_VITE_CLIENT_URL, LOCAL_VITE_CLIENT_SECURE_URL); + // 서버 URL public static final String PROD_SERVER_URL = "https://api.gdschongik.com"; public static final String DEV_SERVER_URL = "https://dev-api.gdschongik.com"; public static final String LOCAL_SERVER_URL = "http://localhost:8080"; - public static final String SOCIAL_LOGIN_REDIRECT_URL = "%ssocial-login/redirect?%s=%s&%s=%s&%s=%s"; + // 기타 + public static final String ROOT_DOMAIN = "gdschongik.com"; } diff --git a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java index 6790053f6..b22ac5abb 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java @@ -146,11 +146,13 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); if (environmentUtil.isProdProfile()) { - configuration.addAllowedOriginPattern(PROD_CLIENT_URL); + configuration.addAllowedOriginPattern(PROD_CLIENT_ONBOARDING_URL); + configuration.addAllowedOriginPattern(PROD_CLIENT_ADMIN_URL); } if (environmentUtil.isDevProfile()) { - configuration.addAllowedOriginPattern(DEV_CLIENT_URL); + configuration.addAllowedOriginPattern(DEV_CLIENT_ONBOARDING_URL); + configuration.addAllowedOriginPattern(DEV_CLIENT_ADMIN_URL); configuration.addAllowedOriginPattern(LOCAL_REACT_CLIENT_URL); configuration.addAllowedOriginPattern(LOCAL_REACT_CLIENT_SECURE_URL); configuration.addAllowedOriginPattern(LOCAL_VITE_CLIENT_URL); diff --git a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java index c5a3b3663..d096fcfb3 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -17,6 +17,8 @@ public enum ErrorCode { EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 JWT 토큰입니다."), AUTH_NOT_EXIST(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보가 존재하지 않습니다."), AUTH_NOT_PARSABLE(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보 파싱에 실패했습니다."), + BASE_URI_COOKIE_NOT_FOUND(HttpStatus.NOT_FOUND, "Base URI 쿠키가 존재하지 않습니다."), + NOT_ALLOWED_BASE_URI(HttpStatus.FORBIDDEN, "허용되지 않은 Base URI입니다."), // Parameter INVALID_QUERY_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 쿼리 파라미터입니다."), diff --git a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java index 8e65eadd7..e60e28240 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomSuccessHandler.java @@ -2,18 +2,22 @@ import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.*; import static com.gdschongik.gdsc.global.common.constant.UrlConstant.*; +import static com.gdschongik.gdsc.global.exception.ErrorCode.*; import com.gdschongik.gdsc.domain.auth.application.JwtService; import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto; import com.gdschongik.gdsc.domain.auth.dto.RefreshTokenDto; +import com.gdschongik.gdsc.global.exception.CustomException; import com.gdschongik.gdsc.global.util.CookieUtil; import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.web.util.UriComponentsBuilder; @Slf4j public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @@ -24,13 +28,13 @@ public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler public CustomSuccessHandler(JwtService jwtService, CookieUtil cookieUtil) { this.jwtService = jwtService; this.cookieUtil = cookieUtil; - this.setUseReferer(true); } @Override public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { + String baseUri = determineTargetUrl(request, response); CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); @@ -43,19 +47,36 @@ public void onAuthenticationSuccess( // 임시로 헤더에 엑세스 토큰 추가 response.addHeader(ACCESS_TOKEN_HEADER_NAME, ACCESS_TOKEN_HEADER_PREFIX + accessTokenDto.tokenValue()); - // 랜딩 상태를 파라미터로 추가하여 리다이렉트 - // String baseUrl = determineTargetUrl(request, response); - String baseUrl = PROD_CLIENT_URL + "/"; - String redirectUrl = String.format( - SOCIAL_LOGIN_REDIRECT_URL, - baseUrl, - LANDING_STATUS_PARAM, - oAuth2User.getLandingStatus().name(), - "access", - accessTokenDto.tokenValue(), - "refresh", - refreshTokenDto.tokenValue()); - - getRedirectStrategy().sendRedirect(request, response, redirectUrl); + String redirectUri = UriComponentsBuilder.fromHttpUrl(baseUri) + .path(OAUTH_REDIRECT_PATH_SEGMENT) + .queryParam(LANDING_STATUS_PARAM, oAuth2User.getLandingStatus().name()) + .queryParam(ACCESS_TOKEN_PARAM, accessTokenDto.tokenValue()) + .queryParam(REFRESH_TOKEN_PARAM, refreshTokenDto.tokenValue()) + .toUriString(); + + getRedirectStrategy().sendRedirect(request, response, redirectUri); + } + + @Override + protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) { + Cookie baseUriCookie = cookieUtil + .findCookie(request, OAUTH_BASE_URI_COOKIE_NAME) + .orElseThrow(() -> new CustomException(BASE_URI_COOKIE_NOT_FOUND)); + + String baseUri = baseUriCookie.getValue(); + validateBaseUri(baseUri); + + cookieUtil.deleteCookie(response, baseUriCookie); + + return baseUri; + } + + private void validateBaseUri(String baseUri) { + if (baseUri.endsWith(ROOT_DOMAIN) || LOCAL_CLIENT_URLS.contains(baseUri)) { + return; + } + + log.error("허용되지 않은 BASE URI로의 리다이렉트 요청 발생: {}", baseUri); + throw new CustomException(NOT_ALLOWED_BASE_URI); } } diff --git a/src/main/java/com/gdschongik/gdsc/global/security/CustomUserService.java b/src/main/java/com/gdschongik/gdsc/global/security/CustomUserService.java index 4ab53c807..076c317b7 100644 --- a/src/main/java/com/gdschongik/gdsc/global/security/CustomUserService.java +++ b/src/main/java/com/gdschongik/gdsc/global/security/CustomUserService.java @@ -1,5 +1,7 @@ package com.gdschongik.gdsc.global.security; +import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.*; + import com.gdschongik.gdsc.domain.member.dao.MemberRepository; import com.gdschongik.gdsc.domain.member.domain.Member; import lombok.RequiredArgsConstructor; @@ -18,8 +20,11 @@ public class CustomUserService extends DefaultOAuth2UserService { @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2User oAuth2User = super.loadUser(userRequest); + Member member = fetchOrCreate(oAuth2User); member.updateLastLoginAt(); + memberRepository.save(member); + return new CustomOAuth2User(oAuth2User, member); } diff --git a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java index 21556abca..b8722d1b9 100644 --- a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java +++ b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java @@ -1,7 +1,10 @@ package com.gdschongik.gdsc.global.util; import com.gdschongik.gdsc.global.common.constant.JwtConstant; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.boot.web.server.Cookie; import org.springframework.http.HttpHeaders; @@ -19,16 +22,16 @@ public void addTokenCookies(HttpServletResponse response, String accessToken, St String sameSite = determineSameSitePolicy(); ResponseCookie accessTokenCookie = - generateCookie(JwtConstant.ACCESS_TOKEN.getCookieName(), accessToken, sameSite); + generateTokenCookie(JwtConstant.ACCESS_TOKEN.getCookieName(), accessToken, sameSite); ResponseCookie refreshTokenCookie = - generateCookie(JwtConstant.REFRESH_TOKEN.getCookieName(), refreshToken, sameSite); + generateTokenCookie(JwtConstant.REFRESH_TOKEN.getCookieName(), refreshToken, sameSite); response.addHeader(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); } - private ResponseCookie generateCookie(String cookieName, String tokenValue, String sameSite) { + private ResponseCookie generateTokenCookie(String cookieName, String tokenValue, String sameSite) { return ResponseCookie.from(cookieName, tokenValue) .path("/") .secure(true) @@ -43,4 +46,17 @@ private String determineSameSitePolicy() { } return Cookie.SameSite.NONE.attributeValue(); } + + public Optional findCookie(HttpServletRequest request, String cookieName) { + return Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals(cookieName)) + .findFirst(); + } + + public void deleteCookie(HttpServletResponse response, jakarta.servlet.http.Cookie cookie) { + cookie.setPath("/"); + cookie.setValue(""); + cookie.setMaxAge(0); + response.addCookie(cookie); + } } diff --git a/src/main/java/com/gdschongik/gdsc/global/util/email/VerificationLinkUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/email/VerificationLinkUtil.java index ce90b32b2..39452a0d9 100644 --- a/src/main/java/com/gdschongik/gdsc/global/util/email/VerificationLinkUtil.java +++ b/src/main/java/com/gdschongik/gdsc/global/util/email/VerificationLinkUtil.java @@ -4,9 +4,9 @@ import static com.gdschongik.gdsc.global.common.constant.EmailConstant.VERIFY_EMAIL_REQUEST_PARAMETER_KEY; import static com.gdschongik.gdsc.global.common.constant.EnvironmentConstant.Constants.DEV_ENV; import static com.gdschongik.gdsc.global.common.constant.EnvironmentConstant.Constants.PROD_ENV; -import static com.gdschongik.gdsc.global.common.constant.UrlConstant.DEV_CLIENT_URL; +import static com.gdschongik.gdsc.global.common.constant.UrlConstant.DEV_CLIENT_ONBOARDING_URL; import static com.gdschongik.gdsc.global.common.constant.UrlConstant.LOCAL_VITE_CLIENT_SECURE_URL; -import static com.gdschongik.gdsc.global.common.constant.UrlConstant.PROD_CLIENT_URL; +import static com.gdschongik.gdsc.global.common.constant.UrlConstant.PROD_CLIENT_ONBOARDING_URL; import com.gdschongik.gdsc.global.util.EnvironmentUtil; import lombok.RequiredArgsConstructor; @@ -25,8 +25,8 @@ public String createLink(String verificationCode) { private String getClientUrl() { return switch (environmentUtil.getCurrentProfile()) { - case PROD_ENV -> PROD_CLIENT_URL; - case DEV_ENV -> DEV_CLIENT_URL; + case PROD_ENV -> PROD_CLIENT_ONBOARDING_URL; + case DEV_ENV -> DEV_CLIENT_ONBOARDING_URL; default -> LOCAL_VITE_CLIENT_SECURE_URL; }; }