Skip to content

Commit

Permalink
Merge pull request #82 from DEPthes/develop
Browse files Browse the repository at this point in the history
V.2.2.1 Deploy
  • Loading branch information
phonil authored Aug 15, 2024
2 parents 6c6a6fa + 03dbb76 commit a6f1b9e
Show file tree
Hide file tree
Showing 29 changed files with 382 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@

import mvp.deplog.domain.auth.dto.request.LoginReq;
import mvp.deplog.domain.auth.dto.request.JoinReq;
import mvp.deplog.domain.auth.dto.request.LogoutReq;
import mvp.deplog.domain.auth.dto.request.ModifyPasswordReq;
import mvp.deplog.domain.auth.dto.response.EmailDuplicateCheckRes;
import mvp.deplog.domain.auth.dto.response.LoginRes;
import mvp.deplog.domain.auth.dto.response.ReissueRes;
import mvp.deplog.global.common.Message;
import mvp.deplog.global.common.SuccessResponse;
import mvp.deplog.global.security.UserDetailsImpl;

public interface AuthService {

SuccessResponse<Message> join(JoinReq joinReq);

SuccessResponse<LoginRes> login(LoginReq loginReq);

SuccessResponse<Message> logout(UserDetailsImpl userDetails, LogoutReq logoutReq);

SuccessResponse<ReissueRes> reissue(String refreshToken);

SuccessResponse<EmailDuplicateCheckRes> checkEmailDuplicate(String email);

SuccessResponse<Message> modifyPassword(ModifyPasswordReq modifyPasswordReq);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package mvp.deplog.domain.auth.application;

import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.RequiredArgsConstructor;
import mvp.deplog.domain.auth.dto.mapper.MemberAuthMapper;
import mvp.deplog.domain.auth.dto.request.LoginReq;
import mvp.deplog.domain.auth.dto.request.LogoutReq;
import mvp.deplog.domain.auth.dto.request.ModifyPasswordReq;
import mvp.deplog.domain.auth.dto.response.EmailDuplicateCheckRes;
import mvp.deplog.domain.auth.dto.response.LoginRes;
import mvp.deplog.domain.auth.dto.request.JoinReq;
import mvp.deplog.domain.auth.dto.response.ReissueRes;
import mvp.deplog.global.common.Message;
import mvp.deplog.global.common.SuccessResponse;
import mvp.deplog.domain.member.domain.Member;
import mvp.deplog.domain.member.domain.repository.MemberRepository;
import mvp.deplog.global.security.UserDetailsImpl;
import mvp.deplog.global.security.jwt.JwtTokenProvider;
import mvp.deplog.infrastructure.redis.RedisUtil;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -23,6 +29,11 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
Expand All @@ -32,6 +43,7 @@ public class AuthServiceImpl implements AuthService{
private long refreshTokenValidityInSeconds;

private static String RT_PREFIX = "RT_";
private static String BL_AT_PREFIX = "BL_AT_";

private final RedisUtil redisUtil;
private final AuthenticationManager authenticationManager;
Expand Down Expand Up @@ -61,7 +73,6 @@ public SuccessResponse<Message> join(JoinReq joinReq) {
}

@Override
@Transactional
public SuccessResponse<LoginRes> login(LoginReq loginReq) {
String email = loginReq.getEmail();
String password = loginReq.getPassword();
Expand All @@ -78,8 +89,7 @@ public SuccessResponse<LoginRes> login(LoginReq loginReq) {
memberRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("해당 이메일로 유저를 찾을 수 없습니다: " + email));

String findRefreshToken = redisUtil.getData(RT_PREFIX + email);
redisUtil.setDataExpire(RT_PREFIX + email, refreshToken, refreshTokenValidityInSeconds);
redisUtil.setDataExpire(RT_PREFIX + refreshToken, email, refreshTokenValidityInSeconds);

LoginRes loginRes = LoginRes.builder()
.accessToken(accessToken)
Expand All @@ -89,6 +99,46 @@ public SuccessResponse<LoginRes> login(LoginReq loginReq) {
return SuccessResponse.of(loginRes);
}

@Override
public SuccessResponse<Message> logout(UserDetailsImpl userDetails, LogoutReq logoutReq) {
String email = userDetails.getMember().getEmail();
String findEmail = redisUtil.getData(RT_PREFIX + logoutReq.getRefreshToken());
if (!email.equals(findEmail))
throw new IllegalArgumentException("본인의 리프레시 토큰만 삭제할 수 있습니다.");
redisUtil.deleteData(RT_PREFIX + logoutReq.getRefreshToken());

// 남은 시간을 초 단위로 계산
DecodedJWT decodedJWT = JWT.decode(logoutReq.getAccessToken());
Instant expiresAt = decodedJWT.getExpiresAt().toInstant();
Instant now = Instant.now();
long between = ChronoUnit.SECONDS.between(now, expiresAt);
System.out.println("남은 시간: " + between);

// 남은 만료시간만큼 access token blacklist
redisUtil.setDataExpire(BL_AT_PREFIX + logoutReq.getAccessToken(), "black list token", between);

Message message = Message.builder()
.message("로그아웃이 완료되었습니다.")
.build();

return SuccessResponse.of(message);
}

@Override
public SuccessResponse<ReissueRes> reissue(String refreshToken) {
if (!jwtTokenProvider.isTokenValid(refreshToken))
throw new TokenExpiredException("유효하지 않은 리프레시 토큰입니다.");

String email = redisUtil.getData(RT_PREFIX + refreshToken);
String accessToken = jwtTokenProvider.createAccessToken(email);

ReissueRes reissueRes = ReissueRes.builder()
.accessToken(accessToken)
.build();

return SuccessResponse.of(reissueRes);
}

@Override
public SuccessResponse<EmailDuplicateCheckRes> checkEmailDuplicate(String email) {
EmailDuplicateCheckRes emailDuplicateCheckRes = EmailDuplicateCheckRes.builder()
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/mvp/deplog/domain/auth/dto/request/LogoutReq.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package mvp.deplog.domain.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
public class LogoutReq {

@Schema(
type = "string",
example = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBY2Nlc3NUb2tlbiIsInJvbGUiOiJjdnZ6M0BuYXZlci5jb20iLCJpZCI6ImN2dnozQG5hdmVyLmNvbSIsImV4cCI6MTcyMjE4NTU5OSwiZW1haWwiOiJjdnZ6M0BuYXZlci5jb20ifQ.6b3gEhqolM3PcdeDpaT1ExTuNV0_PSQQhGDFEk1IvDnPePbjtV2ZX3ds48_nWx77ci4nSEbS1XsajcV9yW_vBQ",
description="access token을 출력합니다."
)
private String accessToken;

@Schema(
type = "string",
example = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJSZWZyZXNoVG9rZW4iLCJleHAiOjE3MjIxODU2MDl9.hQckv2VUkHeKE2GOIe08xUbvKVwPPV6XoKo0xM5ZgppcIrIHeCCXUSOqhgZtsenMyryYNAgxCFeYDLA30-SfgQ",
description="refresh token을 출력합니다."
)
private String refreshToken;
}
17 changes: 17 additions & 0 deletions src/main/java/mvp/deplog/domain/auth/dto/response/ReissueRes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package mvp.deplog.domain.auth.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ReissueRes {

@Schema(
type = "string",
example = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBY2Nlc3NUb2tlbiIsInJvbGUiOiJjdnZ6M0BuYXZlci5jb20iLCJpZCI6ImN2dnozQG5hdmVyLmNvbSIsImV4cCI6MTcyMzYyODQ2MiwiZW1haWwiOiJjdnZ6M0BuYXZlci5jb20ifQ.b8_v-GTWTFxQVmhH1jg-JUERpVXGe_tFg4-Tjv6F8DtymMMKgxicCF6dWlsHxJEhyKL3k-Z4qnCeVq_EcH54eg",
description= "새로 발급된 access token을 출력합니다."
)
private String accessToken;
}
41 changes: 39 additions & 2 deletions src/main/java/mvp/deplog/domain/auth/presentation/AuthApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@
import jakarta.validation.Valid;
import mvp.deplog.domain.auth.dto.request.LoginReq;
import mvp.deplog.domain.auth.dto.request.JoinReq;
import mvp.deplog.domain.auth.dto.request.LogoutReq;
import mvp.deplog.domain.auth.dto.request.ModifyPasswordReq;
import mvp.deplog.domain.auth.dto.response.EmailDuplicateCheckRes;
import mvp.deplog.domain.auth.dto.response.LoginRes;
import mvp.deplog.domain.auth.dto.response.ReissueRes;
import mvp.deplog.global.common.Message;
import mvp.deplog.global.common.SuccessResponse;
import mvp.deplog.global.exception.ErrorResponse;
import mvp.deplog.global.security.UserDetailsImpl;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "Auth API", description = "인증 관련 API입니다.")
Expand All @@ -35,7 +39,7 @@ public interface AuthApi {
})
@PostMapping(value = "/join")
ResponseEntity<SuccessResponse<Message>> join(
@Parameter(description = "Schemas의 SignUpRequest를 참고해주세요.", required = true) @Valid @RequestBody JoinReq joinReq
@Parameter(description = "Schemas의 JoinReq를 참고해주세요.", required = true) @Valid @RequestBody JoinReq joinReq
);

@Operation(summary = "로그인 API", description = "로그인을 진행합니다.")
Expand All @@ -51,7 +55,40 @@ ResponseEntity<SuccessResponse<Message>> join(
})
@PostMapping(value = "/login")
ResponseEntity<SuccessResponse<LoginRes>> login(
@Parameter(description = "Schemas의 SignUpRequest를 참고해주세요.", required = true) @Valid @RequestBody LoginReq loginReq
@Parameter(description = "Schemas의 LoginReq를 참고해주세요.", required = true) @Valid @RequestBody LoginReq loginReq
);

@Operation(summary = "로그아웃 API", description = "로그아웃을 진행합니다.")
@ApiResponses(value = {
@ApiResponse(
responseCode = "200", description = "로그아웃 성공",
content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Message.class))}
),
@ApiResponse(
responseCode = "400", description = "로그아웃 실패",
content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}
)
})
@DeleteMapping(value = "/logout")
ResponseEntity<SuccessResponse<Message>> logout(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @AuthenticationPrincipal UserDetailsImpl userDetails,
@Parameter(description = "Schemas의 LogoutReq를 참고해주세요.", required = true) @RequestBody LogoutReq logoutReq
);

@Operation(summary = "토큰 재발급 API", description = "리프레시 토큰으로 액세스 토큰 재발급을 진행합니다.")
@ApiResponses(value = {
@ApiResponse(
responseCode = "200", description = "토큰 재발급 성공",
content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ReissueRes.class))}
),
@ApiResponse(
responseCode = "400", description = "토큰 재발급 실패",
content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}
)
})
@GetMapping(value = "/reissue")
ResponseEntity<SuccessResponse<ReissueRes>> reissue(
@Parameter(description = "리프레시 토큰을 입력해주세요.", required = true) @RequestParam(value = "refreshToken") String refreshToken
);

@Operation(summary = "이메일 중복 체크 API", description = "이메일 중복 여부를 체크합니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package mvp.deplog.domain.auth.presentation;

import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import mvp.deplog.domain.auth.application.AuthService;
import mvp.deplog.domain.auth.application.AuthServiceImpl;
import mvp.deplog.domain.auth.dto.request.LoginReq;
import mvp.deplog.domain.auth.dto.request.JoinReq;
import mvp.deplog.domain.auth.dto.request.LogoutReq;
import mvp.deplog.domain.auth.dto.request.ModifyPasswordReq;
import mvp.deplog.domain.auth.dto.response.EmailDuplicateCheckRes;
import mvp.deplog.domain.auth.dto.response.LoginRes;
import mvp.deplog.domain.auth.dto.response.ReissueRes;
import mvp.deplog.global.common.Message;
import mvp.deplog.global.common.SuccessResponse;
import mvp.deplog.global.security.UserDetailsImpl;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
Expand All @@ -36,6 +41,21 @@ public ResponseEntity<SuccessResponse<LoginRes>> login(@Valid @RequestBody Login
return ResponseEntity.ok(authService.login(loginReq));
}

@Override
@DeleteMapping(value = "/logout")
public ResponseEntity<SuccessResponse<Message>> logout(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@RequestBody LogoutReq logoutReq
) {
return ResponseEntity.ok(authService.logout(userDetails, logoutReq));
}

@Override
@GetMapping(value = "/reissue")
public ResponseEntity<SuccessResponse<ReissueRes>> reissue(@RequestParam(value = "refreshToken") String refreshToken) {
return ResponseEntity.ok(authService.reissue(refreshToken));
}

@Override
@GetMapping(value = "/emails")
public ResponseEntity<SuccessResponse<EmailDuplicateCheckRes>> checkEmailDuplicate(@RequestParam(value = "email") String email) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,41 +24,6 @@ public class CommentService {
private final CommentRepository commentRepository;
private final PostRepository postRepository;

@Transactional
public SuccessResponse<Message> createComment(CreateCommentReq createCommentReq) {
Long postId = createCommentReq.getPostId();
Post post = postRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("해당 아이디의 게시글을 찾을 수 없습니다: " + postId));

Comment comment;
if(createCommentReq.getParentCommentId() == null){
comment = Comment.commentBuilder()
.post(post)
.content(createCommentReq.getContent())
.nickname(createCommentReq.getNickname())
.build();
} else{
Long parentCommentId = createCommentReq.getParentCommentId();
Comment parentComment = commentRepository.findById(parentCommentId)
.orElseThrow(() -> new IllegalArgumentException("해당 아이디의 부모 댓글을 찾을 수 없습니다: " + parentCommentId));
comment = Comment.replyBuilder()
.post(post)
.parentComment(parentComment)
.content(createCommentReq.getContent())
.nickname(createCommentReq.getNickname())
.build();
}

// 댓글 저장
commentRepository.save(comment);

Message message = Message.builder()
.message("댓글 작성이 완료되었습니다.")
.build();

return SuccessResponse.of(message);
}

public SuccessResponse<List<CommentListRes>> getCommentList(Long postId){
Post post = postRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("해당하는 아이디의 게시글이 없습니다: " + postId));
Expand All @@ -70,7 +35,7 @@ public SuccessResponse<List<CommentListRes>> getCommentList(Long postId){
if(comment.getParentComment() == null){
CommentListRes commentListRes = CommentListRes.builder()
.commentId(comment.getId())
// .avatarImage() // 아바타 이미지 url
.avatarImage(comment.getAvatarImage()) // 아바타 이미지 url
.nickname(comment.getNickname())
.createdDate(comment.getCreatedDate().toLocalDate())
.content(comment.getContent())
Expand All @@ -82,7 +47,7 @@ public SuccessResponse<List<CommentListRes>> getCommentList(Long postId){
ReplyListRes replyListRes = ReplyListRes.builder()
.commentId(comment.getId())
.parentCommentId(comment.getParentComment().getId())
// .avatarImage() // 아바타 이미지 url
.avatarImage(comment.getAvatarImage()) // 아바타 이미지 url
.nickname(comment.getNickname())
.createdDate(comment.getCreatedDate().toLocalDate())
.content(comment.getContent())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package mvp.deplog.domain.comment.application;

import mvp.deplog.domain.comment.dto.request.CreateCommentReq;
import mvp.deplog.global.common.Message;
import mvp.deplog.global.common.SuccessResponse;

public interface CreateCommentService {

boolean supports(Long parentCommentId);

SuccessResponse<Message> createComment(CreateCommentReq createCommentReq);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package mvp.deplog.domain.comment.application;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.List;

@RequiredArgsConstructor
@Component
public class CreateCommentServiceFactory {

private final List<CreateCommentService> createCommentServiceList;

public CreateCommentService find(Long parentCommentId) {
return createCommentServiceList.stream()
.filter(v -> v.supports(parentCommentId))
.findFirst()
.orElseThrow();
}
}
Loading

0 comments on commit a6f1b9e

Please sign in to comment.