Skip to content

Commit

Permalink
[Feat #29] API 누락 Swagger 작성 및 임시 토큰 발급 API (#30)
Browse files Browse the repository at this point in the history
* [fix] : 토큰 재발급 시 accessToken을 검증하는 로직 삭제

* [feat] : 로컬 스웨거 경로 인가 허용 추가

* [feat] : swagger 관려 기능 추가

* [feat] : cors config 추가

* [feat] : 임시 토큰 발급 api 및 로직 추가

* [feat] : 소셜 이메일을 통한 회원 존재 확인 쿼리 메서드 추가

* [test] : 실제 로직 변경으로 인해 불필요해진 stub 제거

* [fix] : 스웨거 명세 제거
  • Loading branch information
dudxo authored Aug 12, 2024
1 parent 402f213 commit 22a3146
Show file tree
Hide file tree
Showing 18 changed files with 113 additions and 6 deletions.
22 changes: 22 additions & 0 deletions src/main/java/com/dnd/gongmuin/auth/cotroller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,44 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.dnd.gongmuin.auth.dto.LoginRequest;
import com.dnd.gongmuin.auth.service.AuthService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@Tag(name = "Social Login API", description = "소셜 로그인 요청 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthController {

private final AuthService authService;

@Operation(summary = "카카오 로그인 API", description = "카카오 로그인 페이지로 이동 요청한다.(사용불가!!)")
@ApiResponse(useReturnTypeSchema = true)
@GetMapping("/signin/kakao")
public ResponseEntity<?> kakaoLoginRedirect() {
HttpHeaders httpHeaders = new HttpHeaders();
// 카카오 로그인 페이지로 리다이렉트
httpHeaders.setLocation(URI.create("/oauth2/authorization/kakao"));
return new ResponseEntity<>(httpHeaders, HttpStatus.MOVED_PERMANENTLY);
}

@Operation(summary = "임시 로그인/회원가입(토큰 발급) API", description = "로그인 또는 회원가입 후 토큰을 발급한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/token")
public ResponseEntity<String> swaggerToken(@RequestBody LoginRequest loginRequest) {
String accessToken = authService.swaggerToken(loginRequest);
return ResponseEntity.ok(accessToken);

}
}

9 changes: 9 additions & 0 deletions src/main/java/com/dnd/gongmuin/auth/dto/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.gongmuin.auth.dto;

public record LoginRequest(

String socialName,
String socialEmail

) {
}
28 changes: 28 additions & 0 deletions src/main/java/com/dnd/gongmuin/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package com.dnd.gongmuin.auth.service;

import java.util.Date;
import java.util.Objects;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.dnd.gongmuin.auth.domain.Auth;
import com.dnd.gongmuin.auth.domain.AuthStatus;
import com.dnd.gongmuin.auth.domain.Provider;
import com.dnd.gongmuin.auth.dto.LoginRequest;
import com.dnd.gongmuin.auth.exception.AuthErrorCode;
import com.dnd.gongmuin.auth.repository.AuthRepository;
import com.dnd.gongmuin.common.exception.runtime.NotFoundException;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.member.exception.MemberErrorCode;
import com.dnd.gongmuin.member.repository.MemberRepository;
import com.dnd.gongmuin.member.service.MemberService;
import com.dnd.gongmuin.security.jwt.util.TokenProvider;
import com.dnd.gongmuin.security.oauth2.AuthInfo;
import com.dnd.gongmuin.security.oauth2.CustomOauth2User;

import lombok.RequiredArgsConstructor;

Expand All @@ -21,6 +29,8 @@ public class AuthService {

private final AuthRepository authRepository;
private final MemberService memberService;
private final TokenProvider tokenProvider;
private final MemberRepository memberRepository;

public void saveOrUpdate(Member savedMember) {
Auth findedOrCreatedAuth = authRepository.findByMember(savedMember)
Expand Down Expand Up @@ -48,4 +58,22 @@ private Auth createAuth(Member savedMember) {
return Auth.of(provider, AuthStatus.NEW, savedMember);
}

@Transactional
public String swaggerToken(LoginRequest loginRequest) {
Date now = new Date();
Member member = Member.of(loginRequest.socialName(), "kakao/" + loginRequest.socialEmail(), 10000);

if (memberRepository.existsBySocialEmail(member.getSocialEmail())) {
throw new NotFoundException(MemberErrorCode.NOT_FOUND_NEW_MEMBER);
}
memberRepository.save(member);
saveOrUpdate(member);

AuthInfo authInfo = AuthInfo.of(member.getSocialName(), member.getSocialEmail());
CustomOauth2User customOauth2User = new CustomOauth2User(authInfo);

tokenProvider.generateRefreshToken(customOauth2User, now);
return tokenProvider.generateAccessToken(customOauth2User, now);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,32 @@
import com.dnd.gongmuin.mail.dto.response.SendMailResponse;
import com.dnd.gongmuin.mail.service.MailService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "Mail API", description = "메일 관련 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth/check-email")
public class MailController {

private final MailService mailService;

@Operation(summary = "공무원 이메일 인증 코드 요청 API",
description = "요청 받은 이메일 중복 가입 여부를 검증하고, 해당 이메일로 인증 코드 이메일 발송한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping
public ResponseEntity<SendMailResponse> sendAuthCodeToMail(
@RequestBody @Valid SendMailRequest sendMailRequest) {
SendMailResponse targetEmail = mailService.sendEmail(sendMailRequest);
return ResponseEntity.ok(targetEmail);
}

@Operation(summary = "공무원 이메일 인증 코드 검증 API", description = "인증 코드의 일치 여부를 검증한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/authCode")
public ResponseEntity<AuthCodeResponse> verifyMailAuthCode(
@RequestBody @Valid AuthCodeRequest authCodeRequest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,30 @@
import com.dnd.gongmuin.member.dto.response.ValidateNickNameResponse;
import com.dnd.gongmuin.member.service.MemberService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "Member API", description = "회원 관련 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class MemberController {

private final MemberService memberService;

@Operation(summary = "닉네임 중복 검증 API", description = "닉네임 중복을 검증한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/check-nickname")
public ResponseEntity<ValidateNickNameResponse> checkNickName(
@RequestBody @Valid ValidateNickNameRequest validateNickNameRequest) {
return ResponseEntity.ok(memberService.isDuplicatedNickname(validateNickNameRequest));
}

@Operation(summary = "추가정보 API", description = "추가 정보를 저장한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/member")
public ResponseEntity<SignUpResponse> signUp(
@RequestBody @Valid AdditionalInfoRequest request,
Expand All @@ -43,12 +51,16 @@ public ResponseEntity<SignUpResponse> signUp(
return ResponseEntity.ok(response);
}

@Operation(summary = "로그아웃 API", description = "로그아웃한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/logout")
public ResponseEntity<LogoutResponse> logout(@RequestBody @Valid LogoutRequest request) {
LogoutResponse response = memberService.logout(request);
return ResponseEntity.ok(response);
}

@Operation(summary = "토큰 재발급 API", description = "토큰을 재발급한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/reissue/token")
public ResponseEntity<ReissueResponse> reissue(@RequestBody @Valid ReissueRequest request) {
ReissueResponse response = memberService.reissue(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ public record AdditionalInfoRequest(

@NotBlank(message = "공무원 이메일은 필수 입력 항목입니다.")
String officialEmail,

@NotBlank(message = "닉네임은 필수 입력 항목입니다.")
@Size(min = 2, max = 12, message = "닉네임은 최소 2자리 이상 최대 12자 이하입니다.")
String nickname,

@NotBlank(message = "직군은 필수 입력 항목입니다.")
String jobGroup,

@NotBlank(message = "직렬은 필수 입력 항목입니다.")
String jobCategory
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.validation.constraints.NotEmpty;

public record LogoutRequest(

@NotEmpty(message = "AccessToken을 입력해주세요.")
String accessToken
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.validation.constraints.NotEmpty;

public record ReissueRequest(

@NotEmpty(message = "AccessToken을 입력해주세요.")
String accessToken
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import jakarta.validation.constraints.Size;

public record ValidateNickNameRequest(

@NotBlank(message = "닉네임은 필수 입력 항목입니다.")
@Size(min = 2, max = 12, message = "닉네임은 최소 2자리 이상 최대 12자 이하입니다.")
String nickname
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dnd.gongmuin.member.dto.response;

public record LogoutResponse(

boolean result
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dnd.gongmuin.member.dto.response;

public record ReissueResponse(

String accessToken
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dnd.gongmuin.member.dto.response;

public record SignUpResponse(

String nickName
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dnd.gongmuin.member.dto.response;

public record ValidateNickNameResponse(

boolean isDuplicated
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
boolean existsByNickname(String nickname);

boolean existsByOfficialEmail(String officialEmail);

boolean existsBySocialEmail(String socialEmail);
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,6 @@ public LogoutResponse logout(LogoutRequest request) {
public ReissueResponse reissue(ReissueRequest request) {
String accessToken = request.accessToken().substring(TOKEN_PREFIX.length());

if (!tokenProvider.validateToken(accessToken, new Date())) {
throw new ValidationException(AuthErrorCode.UNAUTHORIZED_TOKEN);
}

// 로그아웃 토큰 처리
if ("logout".equals(redisUtil.getValues(accessToken))) {
throw new ValidationException(AuthErrorCode.UNAUTHORIZED_TOKEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
(auth) -> auth
.requestMatchers("/").permitAll()
.requestMatchers("/api/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll()
.requestMatchers("/api/auth/signin/kakao").permitAll()
.requestMatchers("/api/auth/member").permitAll()
.requestMatchers("/api/auth/check-email").permitAll()
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/dnd/gongmuin/security/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.dnd.gongmuin.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000", "https://gongmuin.netlify.app/",
"https://gongmuin.site/", "http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Set-Cookie", "Authorization")
.maxAge(3000);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ void reissue() {
Member principal = MemberFixture.member();
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, "test");

given(tokenProvider.validateToken(anyString(), any(Date.class))).willReturn(true);
given(tokenProvider.getAuthentication(anyString())).willReturn(authentication);

given(redisUtil.getValues(anyString())).willReturn("refreshToken");
Expand Down

0 comments on commit 22a3146

Please sign in to comment.