diff --git a/src/main/java/com/swyp8team2/auth/application/AuthService.java b/src/main/java/com/swyp8team2/auth/application/AuthService.java index 9b3898f9..470816d7 100644 --- a/src/main/java/com/swyp8team2/auth/application/AuthService.java +++ b/src/main/java/com/swyp8team2/auth/application/AuthService.java @@ -7,11 +7,14 @@ import com.swyp8team2.auth.domain.Provider; import com.swyp8team2.auth.domain.SocialAccount; import com.swyp8team2.auth.domain.SocialAccountRepository; +import com.swyp8team2.crypto.application.CryptoService; import com.swyp8team2.user.application.UserService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.security.NoSuchAlgorithmException; + @Service @RequiredArgsConstructor public class AuthService { @@ -20,6 +23,7 @@ public class AuthService { private final OAuthService oAuthService; private final SocialAccountRepository socialAccountRepository; private final UserService userService; + private final CryptoService cryptoService; @Transactional public TokenPair oauthSignIn(String code, String redirectUri) { @@ -39,4 +43,9 @@ private SocialAccount createUser(OAuthUserInfo oAuthUserInfo) { public TokenPair reissue(String refreshToken) { return jwtService.reissue(refreshToken); } + + public String guestLogin() { + Long guestId = userService.createGuest(); + return cryptoService.encrypt(String.valueOf(guestId)); + } } diff --git a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java index ff12cb20..23f784e7 100644 --- a/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java +++ b/src/main/java/com/swyp8team2/auth/domain/SocialAccount.java @@ -1,6 +1,7 @@ package com.swyp8team2.auth.domain; import com.swyp8team2.auth.application.oauth.dto.OAuthUserInfo; +import com.swyp8team2.common.domain.BaseEntity; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -16,7 +17,7 @@ @Getter @Entity @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) -public class SocialAccount { +public class SocialAccount extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/swyp8team2/auth/domain/UserInfo.java b/src/main/java/com/swyp8team2/auth/domain/UserInfo.java index bd836070..84ac2c45 100644 --- a/src/main/java/com/swyp8team2/auth/domain/UserInfo.java +++ b/src/main/java/com/swyp8team2/auth/domain/UserInfo.java @@ -1,14 +1,17 @@ package com.swyp8team2.auth.domain; +import com.swyp8team2.user.domain.Role; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Collections; +import java.util.List; import static com.swyp8team2.common.util.Validator.validateNull; -public record UserInfo(long userId) implements UserDetails { +public record UserInfo(long userId, Role role) implements UserDetails { public UserInfo { validateNull(userId); @@ -16,7 +19,7 @@ public record UserInfo(long userId) implements UserDetails { @Override public Collection getAuthorities() { - return Collections.emptyList(); + return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); } @Override diff --git a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java index e7e2e908..b14bd9c2 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/AuthController.java +++ b/src/main/java/com/swyp8team2/auth/presentation/AuthController.java @@ -3,6 +3,7 @@ import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.auth.presentation.dto.GuestTokenResponse; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.common.exception.BadRequestException; @@ -53,4 +54,10 @@ public ResponseEntity reissue( response.addCookie(cookie); return ResponseEntity.ok(new TokenResponse(tokenPair.accessToken())); } + + @PostMapping("/guest/token") + public ResponseEntity guestLogin() { + String guestToken = authService.guestLogin(); + return ResponseEntity.ok(new GuestTokenResponse(guestToken)); + } } diff --git a/src/main/java/com/swyp8team2/auth/presentation/dto/GuestTokenResponse.java b/src/main/java/com/swyp8team2/auth/presentation/dto/GuestTokenResponse.java new file mode 100644 index 00000000..ee76d2f6 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/dto/GuestTokenResponse.java @@ -0,0 +1,4 @@ +package com.swyp8team2.auth.presentation.dto; + +public record GuestTokenResponse(String guestToken) { +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java new file mode 100644 index 00000000..4b678645 --- /dev/null +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/GuestAuthFilter.java @@ -0,0 +1,61 @@ +package com.swyp8team2.auth.presentation.filter; + +import com.swyp8team2.auth.domain.UserInfo; +import com.swyp8team2.common.exception.ApplicationException; +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.presentation.CustomHeader; +import com.swyp8team2.crypto.application.CryptoService; +import com.swyp8team2.user.domain.Role; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Objects; + +import static com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint.EXCEPTION_KEY; + +@Slf4j +@RequiredArgsConstructor +public class GuestAuthFilter extends OncePerRequestFilter { + + private final CryptoService cryptoService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + AntPathMatcher matcher = new AntPathMatcher(); + if (!matcher.match("/posts/{postId}/votes/guest", request.getRequestURI())) { + return; + } + String token = request.getHeader(CustomHeader.GUEST_ID); + if (Objects.isNull(token)) { + throw new BadRequestException(ErrorCode.INVALID_GUEST_HEADER); + } + String guestId = cryptoService.decrypt(token); + Authentication authentication = getAuthentication(Long.parseLong(guestId)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (ApplicationException e) { + request.setAttribute(EXCEPTION_KEY, e); + } finally { + doFilter(request, response, filterChain); + } + } + + private Authentication getAuthentication(long userId) { + UserInfo userInfo = new UserInfo(userId, Role.GUEST); + return new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities()); + } +} diff --git a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java index d77b476f..b7f16f08 100644 --- a/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java +++ b/src/main/java/com/swyp8team2/auth/presentation/filter/JwtAuthFilter.java @@ -4,6 +4,7 @@ import com.swyp8team2.auth.application.jwt.JwtProvider; import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.common.exception.ApplicationException; +import com.swyp8team2.user.domain.Role; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -44,7 +45,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } private Authentication getAuthentication(long userId) { - UserInfo userInfo = new UserInfo(userId); + UserInfo userInfo = new UserInfo(userId, Role.USER); return new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities()); } } diff --git a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java index dc36a80b..54e0f7e6 100644 --- a/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java +++ b/src/main/java/com/swyp8team2/comment/domain/CommentRepository.java @@ -14,7 +14,7 @@ public interface CommentRepository extends JpaRepository { SELECT c FROM Comment c WHERE c.postId = :postId - AND (:cursor is null or c.id > :cursor) + AND (:cursor is null or c.id < :cursor) ORDER BY c.createdAt DESC """) Slice findByPostId( diff --git a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java index 4e94ba55..e5de05a9 100644 --- a/src/main/java/com/swyp8team2/common/config/SecurityConfig.java +++ b/src/main/java/com/swyp8team2/common/config/SecurityConfig.java @@ -1,9 +1,12 @@ package com.swyp8team2.common.config; import com.swyp8team2.auth.application.jwt.JwtProvider; +import com.swyp8team2.auth.presentation.filter.GuestAuthFilter; import com.swyp8team2.auth.presentation.filter.HeaderTokenExtractor; import com.swyp8team2.auth.presentation.filter.JwtAuthFilter; import com.swyp8team2.auth.presentation.filter.JwtAuthenticationEntryPoint; +import com.swyp8team2.crypto.application.CryptoService; +import com.swyp8team2.user.domain.Role; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; @@ -32,11 +35,14 @@ public class SecurityConfig { private final HandlerExceptionResolver handlerExceptionResolver; + private final CryptoService cryptoService; public SecurityConfig( - @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver, + CryptoService cryptoService ) { this.handlerExceptionResolver = handlerExceptionResolver; + this.cryptoService = cryptoService; } @Bean @@ -84,11 +90,17 @@ public SecurityFilterChain securityFilterChain( .authorizeHttpRequests(authorize -> authorize .requestMatchers(getWhiteList(introspect)).permitAll() + .requestMatchers(getGuestTokenRequestList(introspect)) + .hasAnyRole(Role.USER.name(), Role.GUEST.name()) .anyRequest().authenticated()) .addFilterBefore( new JwtAuthFilter(jwtProvider, new HeaderTokenExtractor()), UsernamePasswordAuthenticationFilter.class) + .addFilterAfter( + new GuestAuthFilter(cryptoService), + JwtAuthFilter.class + ) .exceptionHandling(exception -> exception.authenticationEntryPoint( new JwtAuthenticationEntryPoint(handlerExceptionResolver))); @@ -99,11 +111,18 @@ public static MvcRequestMatcher[] getWhiteList(HandlerMappingIntrospector intros MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); return new MvcRequestMatcher[]{ mvc.pattern("/auth/reissue"), - mvc.pattern("/guest"), + mvc.pattern("/auth/guest/token"), mvc.pattern(HttpMethod.GET, "/posts/{sharedUrl}"), mvc.pattern(HttpMethod.GET, "/posts/{postId}/comments"), - mvc.pattern("/posts/{postId}/votes/guest/**"), +// mvc.pattern("/posts/{postId}/votes/guest/**"), mvc.pattern("/auth/oauth2/**") }; } + + public static MvcRequestMatcher[] getGuestTokenRequestList(HandlerMappingIntrospector introspect) { + MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspect); + return new MvcRequestMatcher[]{ + mvc.pattern("/posts/{postId}/votes/guest"), + }; + } } diff --git a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java index c490ac30..9665d462 100644 --- a/src/main/java/com/swyp8team2/common/dev/DataInitializer.java +++ b/src/main/java/com/swyp8team2/common/dev/DataInitializer.java @@ -2,6 +2,8 @@ import com.swyp8team2.auth.application.jwt.JwtService; import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.comment.domain.Comment; +import com.swyp8team2.comment.domain.CommentRepository; import com.swyp8team2.image.domain.ImageFile; import com.swyp8team2.image.domain.ImageFileRepository; import com.swyp8team2.image.presentation.dto.ImageFileDto; @@ -33,6 +35,7 @@ public class DataInitializer { private final PostRepository postRepository; private final JwtService jwtService; private final VoteService voteService; + private final CommentRepository commentRepository; @Transactional public void init() { @@ -52,12 +55,14 @@ public void init() { ImageFile imageFile2 = imageFileRepository.save(ImageFile.create(new ImageFileDto("202502240006030.png", "https://image.photopic.site/images-dev/202502240006030.png", "https://image.photopic.site/images-dev/resized_202502240006030.png"))); posts.add(postRepository.save(Post.create(user.getId(), "description" + j, List.of(PostImage.create("뽀또A", imageFile1.getId()), PostImage.create("뽀또B", imageFile2.getId())), "https://photopic.site/shareurl"))); } + } for (User user : users) { for (Post post : posts) { Random random = new Random(); int num = random.nextInt(2); voteService.vote(user.getId(), post.getId(), post.getImages().get(num).getId()); + commentRepository.save(new Comment(post.getId(), user.getId(), "댓글 내용" + random.nextInt(100))); } } } diff --git a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java index 2796149d..3d2fc530 100644 --- a/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java +++ b/src/main/java/com/swyp8team2/common/exception/ApplicationControllerAdvice.java @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingRequestHeaderException; @@ -33,16 +34,21 @@ public ResponseEntity handle(UnauthorizedException e) { .body(response); } - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handle(MethodArgumentNotValidException e) { - log.debug("MethodArgumentNotValidException {}", e.getMessage()); + @ExceptionHandler({ + MethodArgumentNotValidException.class, + HttpMessageNotReadableException.class, + MissingRequestHeaderException.class, + HandlerMethodValidationException.class + }) + public ResponseEntity invalidArgument(Exception e) { + log.debug("invalidArgument: {}", e.getMessage()); return ResponseEntity.badRequest() .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); } - @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public ResponseEntity handle(HttpRequestMethodNotSupportedException e) { - log.debug("HttpRequestMethodNotSupportedException {}", e.getMessage()); + @ExceptionHandler({HttpRequestMethodNotSupportedException.class, MethodArgumentTypeMismatchException.class}) + public ResponseEntity notFound(HttpRequestMethodNotSupportedException e) { + log.debug("notFound: {}", e.getMessage()); return ResponseEntity.notFound().build(); } @@ -52,11 +58,6 @@ public ResponseEntity handle(NoResourceFoundException e) { return ResponseEntity.notFound().build(); } - @ExceptionHandler(HandlerMethodValidationException.class) - public ResponseEntity handle(HandlerMethodValidationException e) { - return ResponseEntity.badRequest() - .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); - } @ExceptionHandler(AuthenticationException.class) public ResponseEntity handle(AuthenticationException e) { @@ -70,20 +71,6 @@ public ResponseEntity handle(AccessDeniedException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(ErrorCode.INVALID_TOKEN)); } - @ExceptionHandler(MissingRequestHeaderException.class) - public ResponseEntity handle(MissingRequestHeaderException e) { - log.debug("MissingRequestHeaderException {}", e.getMessage()); - return ResponseEntity.badRequest() - .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); - } - - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handle(MethodArgumentTypeMismatchException e) { - log.debug("MethodArgumentTypeMismatchException {}", e.getMessage()); - return ResponseEntity.badRequest() - .body(new ErrorResponse(ErrorCode.INVALID_ARGUMENT)); - } - @ExceptionHandler(Exception.class) public ResponseEntity handle(Exception e) { log.error("Exception", e); diff --git a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java index 3a1f52d5..bbc5ea04 100644 --- a/src/main/java/com/swyp8team2/common/exception/ErrorCode.java +++ b/src/main/java/com/swyp8team2/common/exception/ErrorCode.java @@ -20,6 +20,7 @@ public enum ErrorCode { INVALID_POST_IMAGE_COUNT("게시글 이미지 개수 오류"), NOT_POST_AUTHOR("게시글 작성자가 아님"), POST_ALREADY_CLOSED("이미 마감된 게시글"), + INVALID_GUEST_HEADER("잘못된 게스트 토큰 헤더"), //401 EXPIRED_TOKEN("토큰 만료"), diff --git a/src/main/java/com/swyp8team2/crypto/application/CryptoService.java b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java new file mode 100644 index 00000000..25fddd7b --- /dev/null +++ b/src/main/java/com/swyp8team2/crypto/application/CryptoService.java @@ -0,0 +1,55 @@ +package com.swyp8team2.crypto.application; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.common.exception.InternalServerException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.util.Base64; + +@Slf4j +@Service +public class CryptoService { + + private static final String ALGORITHM = "AES"; + private final SecretKey secretKey; + + public CryptoService() throws Exception { + KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM); + keyGenerator.init(256); + this.secretKey = keyGenerator.generateKey(); + } + + public String encrypt(String data) { + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + byte[] encryptedBytes = cipher.doFinal(data.getBytes()); + return Base64.getEncoder().encodeToString(encryptedBytes); + } catch (Exception e) { + log.error("encrypt error {}", e.getMessage()); + throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } + + public String decrypt(String encryptedData) { + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData)); + return new String(decryptedBytes); + } catch (IllegalBlockSizeException | BadPaddingException e) { + log.debug("decrypt error {}", e.getMessage()); + throw new BadRequestException(ErrorCode.INVALID_TOKEN); + } catch (Exception e) { + log.error("decrypt error {}", e.getMessage()); + throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/swyp8team2/post/application/PostService.java b/src/main/java/com/swyp8team2/post/application/PostService.java index b9279815..5b643b77 100644 --- a/src/main/java/com/swyp8team2/post/application/PostService.java +++ b/src/main/java/com/swyp8team2/post/application/PostService.java @@ -88,7 +88,7 @@ private PostImageResponse createVoteResponseDto(PostImage image, Long userId, Lo private Boolean getVoted(PostImage image, Long userId, Long postId) { User user = userRepository.findById(userId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - return voteRepository.findByUserSeqAndPostId(user.getSeq(), postId) + return voteRepository.findByUserIdAndPostId(user.getId(), postId) .map(vote -> vote.getPostImageId().equals(image.getId())) .orElse(false); } @@ -108,7 +108,7 @@ private SimplePostResponse createSimplePostResponse(Post post) { public CursorBasePaginatedResponse findVotedPosts(Long userId, Long cursor, int size) { User user = userRepository.findById(userId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - List postIds = voteRepository.findByUserSeq(user.getSeq()) + List postIds = voteRepository.findByUserId(user.getId()) .map(Vote::getPostId) .toList(); Slice postSlice = postRepository.findByIdIn(postIds, cursor, PageRequest.ofSize(size)); diff --git a/src/main/java/com/swyp8team2/post/presentation/PostController.java b/src/main/java/com/swyp8team2/post/presentation/PostController.java index 80596126..3d4273a0 100644 --- a/src/main/java/com/swyp8team2/post/presentation/PostController.java +++ b/src/main/java/com/swyp8team2/post/presentation/PostController.java @@ -76,7 +76,7 @@ public ResponseEntity deletePost( return ResponseEntity.ok().build(); } - @GetMapping("/user") + @GetMapping("/user/me") public ResponseEntity> findMyPosts( @RequestParam(name = "cursor", required = false) @Min(0) Long cursor, @RequestParam(name = "size", required = false, defaultValue = "10") @Min(1) int size, diff --git a/src/main/java/com/swyp8team2/user/application/UserService.java b/src/main/java/com/swyp8team2/user/application/UserService.java index 64e5e1c1..b089de58 100644 --- a/src/main/java/com/swyp8team2/user/application/UserService.java +++ b/src/main/java/com/swyp8team2/user/application/UserService.java @@ -42,6 +42,12 @@ private String getNickname(String nickname) { }); } + @Transactional + public Long createGuest() { + User user = userRepository.save(User.createGuest()); + return user.getId(); + } + public UserInfoResponse findById(Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); diff --git a/src/main/java/com/swyp8team2/user/domain/Role.java b/src/main/java/com/swyp8team2/user/domain/Role.java new file mode 100644 index 00000000..3792a4c5 --- /dev/null +++ b/src/main/java/com/swyp8team2/user/domain/Role.java @@ -0,0 +1,5 @@ +package com.swyp8team2.user.domain; + +public enum Role { + GUEST, USER +} diff --git a/src/main/java/com/swyp8team2/user/domain/User.java b/src/main/java/com/swyp8team2/user/domain/User.java index d63e1bd3..85ad7041 100644 --- a/src/main/java/com/swyp8team2/user/domain/User.java +++ b/src/main/java/com/swyp8team2/user/domain/User.java @@ -1,6 +1,8 @@ package com.swyp8team2.user.domain; +import com.swyp8team2.common.domain.BaseEntity; import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -17,7 +19,7 @@ @Entity @Table(name = "users") @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) -public class User { +public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -29,14 +31,28 @@ public class User { private String seq; - public User(Long id, String nickname, String profileUrl, String seq) { + @Enumerated(jakarta.persistence.EnumType.STRING) + public Role role; + + public User(Long id, String nickname, String profileUrl, String seq, Role role) { this.id = id; this.nickname = nickname; this.profileUrl = profileUrl; this.seq = seq; + this.role = role; } public static User create(String nickname, String profileUrl) { - return new User(null, nickname, profileUrl, UUID.randomUUID().toString()); + return new User(null, nickname, profileUrl, UUID.randomUUID().toString(), Role.USER); + } + + public static User createGuest() { + return new User( + null, + "guest_" + System.currentTimeMillis(), + "https://image.photopic.site/images-dev/resized_202502240006030.png", + UUID.randomUUID().toString(), + Role.GUEST + ); } } diff --git a/src/main/java/com/swyp8team2/vote/application/VoteService.java b/src/main/java/com/swyp8team2/vote/application/VoteService.java index 9860e8a7..e7f093d9 100644 --- a/src/main/java/com/swyp8team2/vote/application/VoteService.java +++ b/src/main/java/com/swyp8team2/vote/application/VoteService.java @@ -8,7 +8,6 @@ import com.swyp8team2.user.domain.UserRepository; import com.swyp8team2.vote.domain.Vote; import com.swyp8team2.vote.domain.VoteRepository; -import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,13 +25,13 @@ public class VoteService { public Long vote(Long voterId, Long postId, Long imageId) { User voter = userRepository.findById(voterId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); - deleteVoteIfExisting(postId, voter.getSeq()); - Vote vote = createVote(postId, imageId, voter.getSeq()); + deleteVoteIfExisting(postId, voter.getId()); + Vote vote = createVote(postId, imageId, voter.getId()); return vote.getId(); } - private void deleteVoteIfExisting(Long postId, String userSeq) { - voteRepository.findByUserSeqAndPostId(userSeq, postId) + private void deleteVoteIfExisting(Long postId, Long userId) { + voteRepository.findByUserIdAndPostId(userId, postId) .ifPresent(vote -> { voteRepository.delete(vote); postRepository.findById(postId) @@ -41,18 +40,19 @@ private void deleteVoteIfExisting(Long postId, String userSeq) { }); } - private Vote createVote(Long postId, Long imageId, String userSeq) { + private Vote createVote(Long postId, Long imageId, Long userId) { Post post = postRepository.findById(postId) .orElseThrow(() -> new BadRequestException(ErrorCode.POST_NOT_FOUND)); post.validateProgress(); - Vote vote = voteRepository.save(Vote.of(post.getId(), imageId, userSeq)); + Vote vote = voteRepository.save(Vote.of(post.getId(), imageId, userId)); post.vote(imageId); return vote; } - public Long guestVote(String guestId, Long postId, Long imageId) { - deleteVoteIfExisting(postId, guestId); - Vote vote = createVote(postId, imageId, guestId); + @Transactional + public Long guestVote(Long userId, Long postId, Long imageId) { + deleteVoteIfExisting(postId, userId); + Vote vote = createVote(postId, imageId, userId); return vote.getId(); } } diff --git a/src/main/java/com/swyp8team2/vote/domain/Vote.java b/src/main/java/com/swyp8team2/vote/domain/Vote.java index dde80117..23bd38a9 100644 --- a/src/main/java/com/swyp8team2/vote/domain/Vote.java +++ b/src/main/java/com/swyp8team2/vote/domain/Vote.java @@ -1,5 +1,6 @@ package com.swyp8team2.vote.domain; +import com.swyp8team2.common.domain.BaseEntity; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -13,7 +14,7 @@ @Entity @Table(name = "user_votes") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Vote { +public class Vote extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -23,16 +24,16 @@ public class Vote { private Long postImageId; - private String userSeq; + private Long userId; - public Vote(Long id, Long postId, Long postImageId, String userSeq) { + public Vote(Long id, Long postId, Long postImageId, Long userId) { this.id = id; this.postId = postId; this.postImageId = postImageId; - this.userSeq = userSeq; + this.userId = userId; } - public static Vote of(Long postId, Long postImageId, String userSeq) { - return new Vote(null, postId, postImageId, userSeq); + public static Vote of(Long postId, Long postImageId, Long userId) { + return new Vote(null, postId, postImageId, userId); } } diff --git a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java index 786e845f..05c2ccf5 100644 --- a/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java +++ b/src/main/java/com/swyp8team2/vote/domain/VoteRepository.java @@ -8,7 +8,7 @@ @Repository public interface VoteRepository extends JpaRepository { - Optional findByUserSeqAndPostId(String userSeq, Long postId); + Optional findByUserIdAndPostId(Long userId, Long postId); - Slice findByUserSeq(String seq); + Slice findByUserId(Long userId); } diff --git a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java index 25025afd..51d0caea 100644 --- a/src/main/java/com/swyp8team2/vote/presentation/VoteController.java +++ b/src/main/java/com/swyp8team2/vote/presentation/VoteController.java @@ -37,10 +37,10 @@ public ResponseEntity vote( @PostMapping("/guest") public ResponseEntity guestVote( @PathVariable("postId") Long postId, - @RequestHeader(CustomHeader.GUEST_ID) String guestId, - @Valid @RequestBody VoteRequest request + @Valid @RequestBody VoteRequest request, + @AuthenticationPrincipal UserInfo userInfo ) { - voteService.guestVote(guestId, postId, request.imageId()); + voteService.guestVote(userInfo.userId(), postId, request.imageId()); return ResponseEntity.ok().build(); } diff --git a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java index fc93bafb..676f8231 100644 --- a/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/swyp8team2/auth/presentation/AuthControllerTest.java @@ -2,6 +2,7 @@ import com.swyp8team2.auth.application.AuthService; import com.swyp8team2.auth.application.jwt.TokenPair; +import com.swyp8team2.auth.presentation.dto.GuestTokenResponse; import com.swyp8team2.auth.presentation.dto.OAuthSignInRequest; import com.swyp8team2.auth.presentation.dto.TokenResponse; import com.swyp8team2.common.exception.BadRequestException; @@ -146,4 +147,23 @@ void reissue_refreshTokenMismatched() throws Exception { .andExpect(status().isBadRequest()) .andExpect(content().json(objectMapper.writeValueAsString(response))); } + + @Test + @DisplayName("게스트 토큰 발급") + void guestLogin() throws Exception { + //given + String guestToken = "guestToken"; + given(authService.guestLogin()) + .willReturn(guestToken); + + //when then + mockMvc.perform(post("/auth/guest/token")) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(new GuestTokenResponse(guestToken)))) + .andDo(restDocs.document( + responseFields( + fieldWithPath("guestToken").description("게스트 토큰") + ) + )); + } } diff --git a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java index 29b7f6f0..09d5e374 100644 --- a/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java +++ b/src/test/java/com/swyp8team2/comment/application/CommentServiceTest.java @@ -8,6 +8,7 @@ import com.swyp8team2.common.dto.CursorBasePaginatedResponse; import com.swyp8team2.common.exception.BadRequestException; import com.swyp8team2.common.exception.ErrorCode; +import com.swyp8team2.user.domain.Role; import com.swyp8team2.user.domain.User; import com.swyp8team2.user.domain.UserRepository; import org.junit.jupiter.api.DisplayName; @@ -48,7 +49,7 @@ void createComment() { // given Long postId = 1L; CreateCommentRequest request = new CreateCommentRequest("테스트 댓글"); - UserInfo userInfo = new UserInfo(100L); + UserInfo userInfo = new UserInfo(100L, Role.USER); Comment comment = new Comment(postId, userInfo.userId(), request.content()); // when @@ -69,7 +70,7 @@ void findComments() { Comment comment1 = new Comment(1L, postId, 100L, "첫 번째 댓글"); Comment comment2 = new Comment(2L, postId, 100L, "두 번째 댓글"); SliceImpl commentSlice = new SliceImpl<>(List.of(comment1, comment2), PageRequest.of(0, size), false); - User user = new User(100L, "닉네임","http://example.com/profile.png", "seq"); + User user = new User(100L, "닉네임","http://example.com/profile.png", "seq", Role.USER); // Mock 설정 given(commentRepository.findByPostId(eq(postId), eq(cursor), any(PageRequest.class))).willReturn(commentSlice); diff --git a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java index aaa6f9ca..d8a44e7d 100644 --- a/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java +++ b/src/test/java/com/swyp8team2/comment/domain/CommentRepositoryTest.java @@ -35,6 +35,6 @@ void select_CommentUser() { Slice result2 = commentRepository.findByPostId(1L, 1L, PageRequest.of(0, 10)); // then2 - assertThat(result2.getContent()).hasSize(2); + assertThat(result2.getContent()).hasSize(0); } } \ No newline at end of file diff --git a/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java new file mode 100644 index 00000000..3142366c --- /dev/null +++ b/src/test/java/com/swyp8team2/crypto/application/CryptoServiceTest.java @@ -0,0 +1,61 @@ +package com.swyp8team2.crypto.application; + +import com.swyp8team2.common.exception.BadRequestException; +import com.swyp8team2.common.exception.ErrorCode; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class CryptoServiceTest { + + CryptoService cryptoService; + + @BeforeEach + void setUp() throws Exception { + cryptoService = new CryptoService(); + } + + @Test + @DisplayName("암호화 및 복호화") + void encryptAndDecrypt() { + // given + String plainText = "Hello, World!"; + + // when + String encryptedText = cryptoService.encrypt(plainText); + String decryptedText = cryptoService.decrypt(encryptedText); + + // then + assertThat(decryptedText).isEqualTo(plainText); + } + + @Test + @DisplayName("암호화 및 복호화 - 다른 키") + void encryptAndDecrypt_differentKey() throws Exception { + // given + String plainText = "Hello, World!"; + CryptoService differentCryptoService = new CryptoService(); + String encryptedText = differentCryptoService.encrypt(plainText); + + // when then + assertThatThrownBy(() -> cryptoService.decrypt(encryptedText)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + + @Test + @DisplayName("복호화 - 이상한 토큰") + void decrypt_invalidToken() { + // given + String invalid = "invalidToken"; + + // when then + assertThatThrownBy(() -> cryptoService.decrypt(invalid)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } +} diff --git a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java index 53b927be..4f5118d9 100644 --- a/src/test/java/com/swyp8team2/post/application/PostServiceTest.java +++ b/src/test/java/com/swyp8team2/post/application/PostServiceTest.java @@ -196,7 +196,7 @@ void findVotedPosts() throws Exception { List posts = createPosts(user); for (int i = 0; i < 15; i++) { Post post = posts.get(i); - voteRepository.save(Vote.of(post.getId(), post.getImages().get(0).getId(), user.getSeq())); + voteRepository.save(Vote.of(post.getId(), post.getImages().get(0).getId(), user.getId())); } int size = 10; diff --git a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java index 6041d4d4..0d3701f1 100644 --- a/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java +++ b/src/test/java/com/swyp8team2/post/presentation/PostControllerTest.java @@ -206,7 +206,7 @@ void findMyPost() throws Exception { .willReturn(response); //when then - mockMvc.perform(get("/posts/user") + mockMvc.perform(get("/posts/user/me") .header(HttpHeaders.AUTHORIZATION, "Bearer token")) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(response))) diff --git a/src/test/java/com/swyp8team2/support/WithMockUserInfo.java b/src/test/java/com/swyp8team2/support/WithMockUserInfo.java index bb769e56..5b42ff3e 100644 --- a/src/test/java/com/swyp8team2/support/WithMockUserInfo.java +++ b/src/test/java/com/swyp8team2/support/WithMockUserInfo.java @@ -1,6 +1,7 @@ package com.swyp8team2.support; import com.swyp8team2.support.security.TestSecurityContextFactory; +import com.swyp8team2.user.domain.Role; import org.springframework.security.test.context.support.WithSecurityContext; import java.lang.annotation.Retention; @@ -10,4 +11,5 @@ @WithSecurityContext(factory = TestSecurityContextFactory.class) public @interface WithMockUserInfo { long userId() default 1L; + Role role() default Role.USER; } diff --git a/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java b/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java index 163d7423..2fd6de4a 100644 --- a/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java +++ b/src/test/java/com/swyp8team2/support/config/TestSecurityConfig.java @@ -1,5 +1,6 @@ package com.swyp8team2.support.config; +import com.swyp8team2.user.domain.Role; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -9,6 +10,7 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import static com.swyp8team2.common.config.SecurityConfig.getGuestTokenRequestList; import static com.swyp8team2.common.config.SecurityConfig.getWhiteList; @TestConfiguration @@ -32,8 +34,10 @@ public SecurityFilterChain securityFilterChain( .authorizeHttpRequests(authorize -> authorize - .requestMatchers(getWhiteList(introspect)).permitAll() - .anyRequest().authenticated() +// .requestMatchers(getWhiteList(introspect)).permitAll() +// .requestMatchers(getGuestTokenRequestList(introspect)) +// .hasAnyRole(Role.USER.name(), Role.GUEST.name()) + .anyRequest().permitAll() ); return http.build(); } diff --git a/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java b/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java index 822963d0..e6a06d4a 100644 --- a/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java +++ b/src/test/java/com/swyp8team2/support/security/TestSecurityContextFactory.java @@ -2,6 +2,7 @@ import com.swyp8team2.auth.domain.UserInfo; import com.swyp8team2.support.WithMockUserInfo; +import com.swyp8team2.user.domain.Role; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; @@ -15,7 +16,7 @@ public class TestSecurityContextFactory implements WithSecurityContextFactory assertThat(vote.getUserSeq()).isEqualTo(user.getSeq()), + () -> assertThat(vote.getUserId()).isEqualTo(user.getId()), () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(0).getId()), () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(1) @@ -83,7 +83,7 @@ void vote_change() { Vote vote = voteRepository.findById(voteId).get(); Post findPost = postRepository.findById(post.getId()).get(); assertAll( - () -> assertThat(vote.getUserSeq()).isEqualTo(user.getSeq()), + () -> assertThat(vote.getUserId()).isEqualTo(user.getId()), () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(1).getId()), () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(0), @@ -120,8 +120,8 @@ void vote_alreadyClosed() { @DisplayName("게스트 투표하기") void guestVote() { // given - String guestId = "guestId"; User user = userRepository.save(createUser(1)); + Long guestId = user.getId() + 1L; ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); @@ -133,7 +133,7 @@ void guestVote() { Vote vote = voteRepository.findById(voteId).get(); Post findPost = postRepository.findById(post.getId()).get(); assertAll( - () -> assertThat(vote.getUserSeq()).isEqualTo(guestId), + () -> assertThat(vote.getUserId()).isEqualTo(guestId), () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(0).getId()), () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(1) @@ -144,8 +144,8 @@ void guestVote() { @DisplayName("게스트 투표하기 - 다른 이미지로 투표 변경한 경우") void guestVote_change() { // given - String guestId = "guestId"; User user = userRepository.save(createUser(1)); + Long guestId = user.getId() + 1L; ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); Post post = postRepository.save(createPost(user.getId(), imageFile1, imageFile2, 1)); @@ -158,7 +158,7 @@ void guestVote_change() { Vote vote = voteRepository.findById(voteId).get(); Post findPost = postRepository.findById(post.getId()).get(); assertAll( - () -> assertThat(vote.getUserSeq()).isEqualTo(guestId), + () -> assertThat(vote.getUserId()).isEqualTo(guestId), () -> assertThat(vote.getPostId()).isEqualTo(post.getId()), () -> assertThat(vote.getPostImageId()).isEqualTo(post.getImages().get(1).getId()), () -> assertThat(findPost.getImages().get(0).getVoteCount()).isEqualTo(0), @@ -171,6 +171,7 @@ void guestVote_change() { void guestVote_alreadyClosed() { // given User user = userRepository.save(createUser(1)); + Long guestId = user.getId() + 1L; ImageFile imageFile1 = imageFileRepository.save(createImageFile(1)); ImageFile imageFile2 = imageFileRepository.save(createImageFile(2)); Post post = postRepository.save(new Post( @@ -186,7 +187,7 @@ void guestVote_alreadyClosed() { )); // when - assertThatThrownBy(() -> voteService.guestVote("guestId", post.getId(), post.getImages().get(0).getId())) + assertThatThrownBy(() -> voteService.guestVote(guestId, post.getId(), post.getImages().get(0).getId())) .isInstanceOf(BadRequestException.class) .hasMessage(ErrorCode.POST_ALREADY_CLOSED.getMessage()); } diff --git a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java index c28de442..129d98d2 100644 --- a/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java +++ b/src/test/java/com/swyp8team2/vote/presentation/VoteControllerTest.java @@ -3,6 +3,7 @@ import com.swyp8team2.common.presentation.CustomHeader; import com.swyp8team2.support.RestDocsTest; import com.swyp8team2.support.WithMockUserInfo; +import com.swyp8team2.user.domain.Role; import com.swyp8team2.vote.presentation.dto.ChangeVoteRequest; import com.swyp8team2.vote.presentation.dto.VoteRequest; import org.junit.jupiter.api.DisplayName; @@ -58,7 +59,7 @@ void vote() throws Exception { } @Test - @WithAnonymousUser + @WithMockUserInfo(role = Role.GUEST) @DisplayName("게스트 투표") void guestVote() throws Exception { //given @@ -68,7 +69,7 @@ void guestVote() throws Exception { mockMvc.perform(post("/posts/{postId}/votes/guest", "1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) - .header(CustomHeader.GUEST_ID, UUID.randomUUID().toString())) + .header(CustomHeader.GUEST_ID, "guestToken")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(guestHeader()), @@ -111,7 +112,7 @@ void changeVote() throws Exception { } @Test - @WithAnonymousUser + @WithMockUserInfo(role = Role.GUEST) @DisplayName("게스트 투표 변경") void guestChangeVote() throws Exception { //given @@ -121,7 +122,7 @@ void guestChangeVote() throws Exception { mockMvc.perform(patch("/posts/{postId}/votes/guest", "1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) - .header(CustomHeader.GUEST_ID, UUID.randomUUID().toString())) + .header(CustomHeader.GUEST_ID, "guestToken")) .andExpect(status().isOk()) .andDo(restDocs.document( requestHeaders(guestHeader()),