diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/config/security/CorsConfig.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/config/security/CorsConfig.java index 4c51b670..da611607 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/config/security/CorsConfig.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/config/security/CorsConfig.java @@ -15,7 +15,7 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용 - config.setAllowedOrigins(List.of("http://localhost:5173","http://localhost:63342", "https://no-wait-fe-nowait-admin-2y5y.vercel.app", "https://nowait-admin.co.kr")); // 허용할 출처 설정 + config.setAllowedOrigins(List.of("http://localhost:5173","http://localhost:63342", "https://no-wait-fe-nowait-admin-2y5y.vercel.app", "https://nowait-admin.co.kr", "https://www.nowait-admin.com")); // 허용할 출처 설정 config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용 config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더 config.setExposedHeaders(List.of("Authorization")); //클라이언트(브라우저)가 접근할 수 있는 헤더 지정 diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/dto/ManagerSignupRequestDto.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/dto/ManagerSignupRequestDto.java index 7f807bea..0dd4fbcd 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/dto/ManagerSignupRequestDto.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/dto/ManagerSignupRequestDto.java @@ -1,5 +1,7 @@ package com.nowait.applicationadmin.user.dto; +import java.time.LocalDateTime; + import com.nowait.common.enums.Role; import com.nowait.common.enums.SocialType; import com.nowait.domaincorerdb.user.entity.User; @@ -7,7 +9,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.Getter; import lombok.NoArgsConstructor; @@ -39,10 +40,13 @@ public class ManagerSignupRequestDto { public User toEntity() { return User.builder() .email(email) + .phoneNumber("") .password(password) .nickname(nickname) .socialType(SocialType.LOCAL) .role(Role.MANAGER) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) .build(); } diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/CustomOAuth2UserService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/CustomOAuth2UserService.java index a7e12685..048dc43b 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/CustomOAuth2UserService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/CustomOAuth2UserService.java @@ -1,5 +1,6 @@ package com.nowait.applicationuser.oauth.oauth2; +import java.time.LocalDateTime; import java.util.Optional; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; @@ -50,11 +51,16 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic User user = User.builder() .email(oAuth2Response.getEmail()) + .phoneNumber("") .nickname(oAuth2Response.getNickName()) .profileImage(oAuth2Response.getProfileImage()) .socialType(SocialType.KAKAO) .role(Role.USER) // 일반 유저 설정 .storeId(0L) + .phoneEntered(false) + .isMarketingAgree(false) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) .build(); userRepository.save(user); diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/OAuth2LoginSuccessHandler.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/OAuth2LoginSuccessHandler.java index 4a843688..c6e2dcf7 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/OAuth2LoginSuccessHandler.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/OAuth2LoginSuccessHandler.java @@ -38,13 +38,14 @@ public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHan public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal(); + CustomOAuth2User customUserDetails = (CustomOAuth2User)authentication.getPrincipal(); User user = customUserDetails.getUser(); Long userId = customUserDetails.getUserId(); String role = authentication.getAuthorities().iterator().next().getAuthority(); // JWT 발급 - String accessToken = jwtUtil.createAccessToken("accessToken", userId, role, 30 * 60 * 1000L); // 30분 + String accessToken = jwtUtil.createAccessToken("accessToken", userId, role, + Boolean.TRUE.equals(user.getPhoneEntered()), Boolean.TRUE.equals(user.getIsMarketingAgree()),60 * 60 * 1000L); // 1시간 String refreshToken = jwtUtil.createRefreshToken("refreshToken", userId, 30L * 24 * 60 * 60 * 1000L); // 30일 // 1. refreshToken을 DB에 저장 or update diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/security/jwt/JwtUtil.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/security/jwt/JwtUtil.java index 763c320a..3df48aab 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/security/jwt/JwtUtil.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/security/jwt/JwtUtil.java @@ -23,11 +23,14 @@ public JwtUtil(@Value("${jwt.secret}") String secret) { ); } - public String createAccessToken(String tokenCategory, Long userId, String role, Long expiredMs) { + public String createAccessToken(String tokenCategory, Long userId, String role, boolean phoneEntered, + boolean marketingAgree, Long expiredMs) { return Jwts.builder() .claim("tokenCategory", tokenCategory) // accessToken .claim("userId", userId) .claim("role", role) + .claim("phoneEntered", phoneEntered) + .claim("marketingAgree", marketingAgree) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + expiredMs)) .signWith(secretKey) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java index fc6a99e2..53368747 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/token/controller/TokenController.java @@ -11,6 +11,9 @@ import com.nowait.applicationuser.security.jwt.JwtUtil; import com.nowait.applicationuser.token.dto.AuthenticationResponse; import com.nowait.applicationuser.token.service.TokenService; +import com.nowait.domaincorerdb.user.entity.User; +import com.nowait.domaincorerdb.user.exception.UserNotFoundException; +import com.nowait.domaincorerdb.user.repository.UserRepository; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -22,37 +25,57 @@ @RequestMapping("/api/refresh-token") @Slf4j public class TokenController { - private final JwtUtil jwtUtil; - private final TokenService tokenService; - @Value("${jwt.access-token-expiration-ms}") - private long accessTokenExpiration; - @Value("${jwt.refresh-token-expiration-ms}") - private long refreshTokenExpiration; - - @PostMapping - @Operation(summary = "리프레시 토큰", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급합니다.") - @ApiResponse(responseCode = "200", description = "새로운 액세스 토큰과 리프레시 토큰 발급 성공") - public ResponseEntity refreshToken( - @CookieValue(value = "refreshToken", required = false) String refreshToken) { - - if (refreshToken == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token not found in cookies"); - } - - // 리프레시 토큰 검증 - Long userId = jwtUtil.getUserId(refreshToken); - String role = jwtUtil.getRole(refreshToken); - - if (tokenService.validateToken(refreshToken, userId)){ - String newAccessToken = jwtUtil.createAccessToken("accessToken", userId, role, accessTokenExpiration); - String newRefreshToken = jwtUtil.createRefreshToken("refreshToken", userId, refreshTokenExpiration); - - tokenService.updateRefreshToken(userId, refreshToken, newRefreshToken); - - AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, newRefreshToken); - return ResponseEntity.ok().body(authenticationResponse); - } - - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired refresh token"); - } + private final JwtUtil jwtUtil; + private final TokenService tokenService; + private final UserRepository userRepository; + @Value("${jwt.access-token-expiration-ms}") + private long accessTokenExpiration; + @Value("${jwt.refresh-token-expiration-ms}") + private long refreshTokenExpiration; + + @PostMapping + @Operation(summary = "리프레시 토큰", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급합니다.") + @ApiResponse(responseCode = "200", description = "새로운 액세스 토큰과 리프레시 토큰 발급 성공") + public ResponseEntity refreshToken( + @CookieValue(value = "refreshToken", required = false) String refreshToken) { + + if (refreshToken == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token not found in cookies"); + } + + // 리프레시 토큰 검증 + Long userId; + String role; + try { + userId = jwtUtil.getUserId(refreshToken); + role = jwtUtil.getRole(refreshToken); + } catch (RuntimeException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired refresh token"); + } + + User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); + + if (tokenService.validateToken(refreshToken, userId)) { + String newAccessToken = jwtUtil.createAccessToken( + "accessToken", + userId, + role, + Boolean.TRUE.equals(user.getPhoneEntered()), + Boolean.TRUE.equals(user.getIsMarketingAgree()), + accessTokenExpiration + ); + String newRefreshToken = jwtUtil.createRefreshToken( + "refreshToken", + userId, + refreshTokenExpiration + ); + + tokenService.updateRefreshToken(userId, refreshToken, newRefreshToken); + + AuthenticationResponse authenticationResponse = new AuthenticationResponse(newAccessToken, newRefreshToken); + return ResponseEntity.ok().body(authenticationResponse); + } + + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired refresh token"); + } } diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/user/controller/UserController.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/user/controller/UserController.java new file mode 100644 index 00000000..fcb05808 --- /dev/null +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/user/controller/UserController.java @@ -0,0 +1,42 @@ +package com.nowait.applicationuser.user.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.nowait.applicationuser.user.dto.UserUpdateRequest; +import com.nowait.applicationuser.user.service.UserService; +import com.nowait.common.api.ApiUtils; +import com.nowait.domainuserrdb.oauth.dto.CustomOAuth2User; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @PutMapping("/optional-info") + public ResponseEntity putOptional( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, + @Valid @RequestBody UserUpdateRequest req) { + + String newAccessToken = userService.putOptional(customOAuth2User.getUserId(), req.phoneNumber(), + Boolean.TRUE.equals(req.consent())); + + return ResponseEntity + .status(HttpStatus.OK) + .body( + ApiUtils.success( + newAccessToken + ) + ); + } +} diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/user/dto/UserUpdateRequest.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/user/dto/UserUpdateRequest.java new file mode 100644 index 00000000..f9993f19 --- /dev/null +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/user/dto/UserUpdateRequest.java @@ -0,0 +1,10 @@ +package com.nowait.applicationuser.user.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public record UserUpdateRequest( + @NotBlank + @Pattern(regexp = "^010-\\d{4}-\\d{4}$", message = "휴대폰 번호는 010-0000-0000 형식이어야 합니다.") + String phoneNumber, + boolean consent) { } diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/user/service/UserService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/user/service/UserService.java new file mode 100644 index 00000000..bdd65d56 --- /dev/null +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/user/service/UserService.java @@ -0,0 +1,42 @@ +package com.nowait.applicationuser.user.service; + +import java.time.LocalDateTime; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.nowait.applicationuser.security.jwt.JwtUtil; +import com.nowait.domaincorerdb.user.entity.User; +import com.nowait.domaincorerdb.user.exception.UserNotFoundException; +import com.nowait.domaincorerdb.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + private final JwtUtil jwtUtil; + + @Transactional + public String putOptional(Long userId, String phoneNumber, boolean consent) { + + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + if (userRepository.existsByPhoneNumberAndIdNot(phoneNumber, userId)) { + throw new IllegalArgumentException("이미 사용 중인 휴대폰 번호입니다."); + } + + user.setPhoneNumberAndMarkEntered(phoneNumber, LocalDateTime.now()); + user.setIsMarketingAgree(consent, LocalDateTime.now()); + + String role = "ROLE_" + user.getRole().name(); + + return jwtUtil.createAccessToken("accessToken", user.getId(), role, + Boolean.TRUE.equals(user.getPhoneEntered()), + Boolean.TRUE.equals(user.getIsMarketingAgree()), + 60 * 60 * 1000L); + } +} diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/entity/Store.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/entity/Store.java index a635dab2..109c9c45 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/entity/Store.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/entity/Store.java @@ -40,13 +40,13 @@ public class Store extends BaseTimeEntity { @Column(nullable = true, length = 200) private String location; - @Column(nullable = true, length = 200) + @Column(nullable = true, length = 250) private String description; @Column(nullable = true, length = 200) private String noticeTitle; - @Column(nullable = true, length = 200) + @Column(nullable = true, length = 500) private String noticeContent; @Column(nullable = true, length = 200) diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/entity/User.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/entity/User.java index 86d45d9b..bac239a2 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/entity/User.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/entity/User.java @@ -1,9 +1,13 @@ package com.nowait.domaincorerdb.user.entity; +import java.time.LocalDateTime; + +import org.springframework.data.annotation.LastModifiedDate; import org.springframework.security.crypto.password.PasswordEncoder; import com.nowait.common.enums.Role; import com.nowait.common.enums.SocialType; +import com.nowait.domaincorerdb.base.entity.BaseTimeEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -14,21 +18,25 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "users") @Getter -public class User { +@SuperBuilder +public class User extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 생성 private Long id; @Column(nullable = false, unique = true) private String email; // 카카오 이메일 + @Column(nullable = true) + private String phoneNumber; // 사용자 전화번호 + @Column(nullable = false) private String password; // 관리자 패스워드 @@ -44,11 +52,22 @@ public class User { @Enumerated(EnumType.STRING) private Role role; + @Column(nullable = false) + private Boolean isMarketingAgree = false; + + @Column(nullable = false) + private Boolean phoneEntered = false; + private Long storeId; - @Builder - public User(String email,String password, String nickname, String profileImage, SocialType socialType, - Role role, Long storeId) { + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime updatedAt; + + + public User(LocalDateTime createdAt, String email,String password, String nickname, String profileImage, SocialType socialType, + Role role, Long storeId, LocalDateTime updatedAt ) { + super(createdAt); this.email = email; this.password = password; this.nickname = nickname; @@ -56,6 +75,7 @@ public User(String email,String password, String nickname, String profileImage, this.socialType = socialType; this.role = role; this.storeId = storeId; + this.updatedAt = updatedAt; } public static User createUserWithId(Long userId, String email, String nickname, String profileImage, @@ -67,6 +87,8 @@ public static User createUserWithId(Long userId, String email, String nickname, .socialType(socialType) .role(role) .storeId(storeId) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) .build(); user.id = userId; @@ -77,7 +99,19 @@ public static User createUserWithId(Long userId, String email, String nickname, public void updateNickname(String nickname){ this.nickname = nickname; } + public void encodePassword(PasswordEncoder passwordEncoder) { password = passwordEncoder.encode(password); } + + public void setPhoneNumberAndMarkEntered(String phoneNumber, LocalDateTime ts) { + this.phoneNumber = phoneNumber; + this.phoneEntered = true; + this.updatedAt = ts; + } + + public void setIsMarketingAgree(boolean agree, LocalDateTime ts) { + this.isMarketingAgree = agree; + this.updatedAt = ts; + } } diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/repository/UserRepository.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/repository/UserRepository.java index b22b9fc9..5653ee4b 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/repository/UserRepository.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/repository/UserRepository.java @@ -11,4 +11,5 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Optional findByNickname(String nickName); + boolean existsByPhoneNumberAndIdNot(String phoneNumber, Long id); }