diff --git a/Dockerfile b/Dockerfile index 9660585..a972f50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,6 @@ RUN apt-get update \ COPY --from=build /app/build/libs/user-0.0.1-SNAPSHOT.jar . -EXPOSE 8080 +EXPOSE 8081 ENTRYPOINT ["java", "-jar", "user-0.0.1-SNAPSHOT.jar"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..61dd6ea --- /dev/null +++ b/README.md @@ -0,0 +1,236 @@ +# πŸ“’ FlipNote β€” User Service + +**FlipNote μ„œλΉ„μŠ€μ˜ μœ μ € 도메인 λ°±μ—”λ“œ λ ˆν¬μ§€ν† λ¦¬μž…λ‹ˆλ‹€.** + +![Spring Boot](https://img.shields.io/badge/Spring_Boot-6DB33F?logo=springboot&logoColor=white) +![Java](https://img.shields.io/badge/Java_21-007396?logo=openjdk&logoColor=white) +![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white) +![Redis](https://img.shields.io/badge/Redis-FF4438?logo=redis&logoColor=white) +![Deploy](https://img.shields.io/badge/Deploy-GHCR%20%2B%20Docker-2496ED?logo=docker&logoColor=white) + +--- + +## πŸ“‘ λͺ©μ°¨ + +- [μ‹œμž‘ν•˜κΈ°](#μ‹œμž‘ν•˜κΈ°) +- [ν™˜κ²½ λ³€μˆ˜](#ν™˜κ²½-λ³€μˆ˜) +- [μ‹€ν–‰ 및 배포](#μ‹€ν–‰-및-배포) +- [ν”„λ‘œμ νŠΈ ꡬ쑰](#ν”„λ‘œμ νŠΈ-ꡬ쑰) + +--- + + + +## πŸš€ μ‹œμž‘ν•˜κΈ° + +### 사전 μš”κ΅¬μ‚¬ν•­ + +- **Java** 21 이상 +- **Gradle** 8 이상 +- **MySQL** 8 이상 +- **Redis** 6 이상 +- Google OAuth2 ν΄λΌμ΄μ–ΈνŠΈ 생성 및 API ν‚€ λ°œκΈ‰ +- Resend 계정 생성 및 API ν‚€ λ°œκΈ‰ + +### μ„€μΉ˜ + +```bash +# μ˜μ‘΄μ„± μ„€μΉ˜ 및 λΉŒλ“œ +./gradlew build -x test +``` + +--- + + + +## πŸ” ν™˜κ²½ λ³€μˆ˜ + +`application.yml`μ—μ„œ μ°Έμ‘°ν•˜λŠ” ν™˜κ²½ λ³€μˆ˜ λͺ©λ‘μž…λ‹ˆλ‹€. 둜컬 μ‹€ν–‰ μ‹œ `.env` λ˜λŠ” IDE μ‹€ν–‰ ꡬ성에 μ•„λž˜ λ³€μˆ˜λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€. + +```text +# ─── Database ─────────────────────────────────────────── +DB_URL=jdbc:mysql://localhost:3306/flipnote_user +DB_USERNAME= +DB_PASSWORD= + +# ─── Redis ────────────────────────────────────────────── +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# ─── JPA ──────────────────────────────────────────────── +# create | create-drop | update | validate | none +DDL_AUTO=update + +# ─── gRPC ─────────────────────────────────────────────── +GRPC_PORT=9092 + +# ─── JWT ──────────────────────────────────────────────── +JWT_SECRET= +# μ•‘μ„ΈμŠ€ 토큰 만료 μ‹œκ°„ (ms), κΈ°λ³Έκ°’ 900000 (15λΆ„) +JWT_ACCESS_EXPIRATION=900000 +# λ¦¬ν”„λ ˆμ‹œ 토큰 만료 μ‹œκ°„ (ms), κΈ°λ³Έκ°’ 604800000 (7일) +JWT_REFRESH_EXPIRATION=604800000 + +# ─── Email (Resend) ───────────────────────────────────── +APP_RESEND_API_KEY= + +# ─── Client ───────────────────────────────────────────── +# ν”„λ‘ νŠΈμ—”λ“œ URL (CORS, λ¦¬λ‹€μ΄λ ‰νŠΈμ— μ‚¬μš©) +APP_CLIENT_URL=http://localhost:3000 + +# ─── Google OAuth2 ────────────────────────────────────── +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +``` + +> **⚠️ 주의**: ν™˜κ²½ λ³€μˆ˜ νŒŒμΌμ€ μ ˆλŒ€ git에 μ»€λ°‹ν•˜μ§€ λ§ˆμ„Έμš”. `.gitignore`에 ν¬ν•¨λ˜μ–΄ μžˆλŠ”μ§€ λ°˜λ“œμ‹œ ν™•μΈν•˜μ„Έμš”. + +--- + + + +## πŸ–₯️ μ‹€ν–‰ 및 배포 + +### 둜컬 개발 μ„œλ²„ μ‹€ν–‰ + +```bash +./gradlew bootRun +``` + +기본적으둜 `http://localhost:8081`μ—μ„œ μ‹€ν–‰λ©λ‹ˆλ‹€. +Swagger UIλŠ” `http://localhost:8081/users/swagger-ui.html`μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. + +### ν”„λ‘œλ•μ…˜ λΉŒλ“œ + +```bash +./gradlew bootJar +``` + +`build/libs/user-0.0.1-SNAPSHOT.jar` 파일이 μƒμ„±λ©λ‹ˆλ‹€. + +### ν…ŒμŠ€νŠΈ μ‹€ν–‰ + +```bash +./gradlew test +``` + +### Docker 이미지 λΉŒλ“œ 및 μ‹€ν–‰ + +```bash +# 이미지 λΉŒλ“œ +docker build -t flipnote-user . + +# μ»¨ν…Œμ΄λ„ˆ μ‹€ν–‰ +docker run -p 8081:8081 \ + -e DB_URL=... \ + -e JWT_SECRET=... \ + flipnote-user +``` + +### 배포 (GitHub Actions) + +`main` λΈŒλžœμΉ˜μ— push μ‹œ GitHub Actionsκ°€ μžλ™μœΌλ‘œ μ•„λž˜ 과정을 μ‹€ν–‰ν•©λ‹ˆλ‹€. + +**CI** (`push` / `pull_request` β†’ `main`) +1. JDK 21 μ„€μΉ˜ +2. `./gradlew build -x test` β€” λΉŒλ“œ 검증 +3. `./gradlew test` β€” ν…ŒμŠ€νŠΈ μ‹€ν–‰ +4. Dependency-Check β€” 취약점 뢄석 리포트 생성 + +**CD** (`push` β†’ `main`) +1. GitHub Container Registry(GHCR) 둜그인 +2. Docker 이미지 λΉŒλ“œ +3. `ghcr.io/dungbik/flipnote-user` 이미지 Push + +> 배포에 ν•„μš”ν•œ μ‹œν¬λ¦Ώ(`ORG_PAT`)은 GitHub Repository β†’ Settings β†’ Secrets and variables β†’ Actions에 등둝해야 ν•©λ‹ˆλ‹€. + +--- + + + +## πŸ“ ν”„λ‘œμ νŠΈ ꡬ쑰 + +- κ°„λž΅ν™” 버전 + + ```text + src/main/java/flipnote/user/ + β”œβ”€β”€ domain/ # 도메인 λ ˆμ΄μ–΄ (μ—”ν‹°ν‹°, λ ˆν¬μ§€ν† λ¦¬, μ—λŸ¬μ½”λ“œ, 이벀트) + β”œβ”€β”€ application/ # μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ ˆμ΄μ–΄ (μ„œλΉ„μŠ€) + β”œβ”€β”€ infrastructure/ # 인프라 λ ˆμ΄μ–΄ (JWT, Redis, 메일, OAuth, μ„€μ •) + └── interfaces/ # μΈν„°νŽ˜μ΄μŠ€ λ ˆμ΄μ–΄ (HTTP, gRPC μ§„μž…μ ) + ``` + +```text +FlipNote-User/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ main/ +β”‚ β”‚ β”œβ”€β”€ java/flipnote/user/ +β”‚ β”‚ β”‚ β”œβ”€β”€ UserApplication.java +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”œβ”€β”€ domain/ # 도메인 λ ˆμ΄μ–΄ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ common/ # 도메인 곡톡 +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ErrorCode.java +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ BizException.java +β”‚ β”‚ β”‚ β”‚ β”‚ └── EmailSendException.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ entity/ # JPA μ—”ν‹°ν‹° +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ User.java +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ OAuthLink.java +β”‚ β”‚ β”‚ β”‚ β”‚ └── BaseEntity.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ repository/ # λ ˆν¬μ§€ν† λ¦¬ μΈν„°νŽ˜μ΄μŠ€ +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ UserRepository.java +β”‚ β”‚ β”‚ β”‚ β”‚ └── OAuthLinkRepository.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ event/ # 도메인 이벀트 +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ EmailVerificationSendEvent.java +β”‚ β”‚ β”‚ β”‚ β”‚ └── PasswordResetCreateEvent.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ AuthErrorCode.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ UserErrorCode.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ ImageErrorCode.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ TokenClaims.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ TokenPair.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ PasswordResetConstants.java +β”‚ β”‚ β”‚ β”‚ └── VerificationConstants.java +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”œβ”€β”€ application/ # μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ ˆμ΄μ–΄ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ AuthService.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ OAuthService.java +β”‚ β”‚ β”‚ β”‚ └── UserService.java +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”œβ”€β”€ infrastructure/ # 인프라 λ ˆμ΄μ–΄ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ config/ # λ²”μš© μ„€μ • (App, JPA, Swagger, gRPC ν΄λΌμ΄μ–ΈνŠΈ) +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ jwt/ # JWT λ°œκΈ‰/검증 + μ„€μ • +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ mail/ # 메일 λ°œμ†‘ μ„œλΉ„μŠ€ + μ„€μ • +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ oauth/ # Google OAuth2 ν΄λΌμ΄μ–ΈνŠΈ + μ„€μ • +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ redis/ # Redis μ €μž₯μ†Œ (토큰, μΈμ¦μ½”λ“œ λ“±) +β”‚ β”‚ β”‚ β”‚ └── listener/ # 도메인 이벀트 λ¦¬μŠ€λ„ˆ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ └── interfaces/ # μΈν„°νŽ˜μ΄μŠ€ λ ˆμ΄μ–΄ +β”‚ β”‚ β”‚ β”œβ”€β”€ http/ # HTTP μ§„μž…μ  +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ AuthController.java # 인증 (νšŒμ›κ°€μž…, 둜그인, λΉ„λ°€λ²ˆν˜Έ λ“±) +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ OAuthController.java # μ†Œμ…œ 둜그인 (Google OAuth2) +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ UserController.java # μœ μ € 정보 쑰회/μˆ˜μ • +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ dto/ +β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ request/ # Request DTO +β”‚ β”‚ β”‚ β”‚ β”‚ └── response/ # Response DTO +β”‚ β”‚ β”‚ β”‚ └── common/ # ApiResponse, μ˜ˆμ™Έ 처리, μΏ ν‚€ μœ ν‹Έ +β”‚ β”‚ β”‚ └── grpc/ # gRPC μ§„μž…μ  +β”‚ β”‚ β”‚ β”œβ”€β”€ GrpcUserQueryService.java # μœ μ € 쑰회 gRPC μ„œλΉ„μŠ€ +β”‚ β”‚ β”‚ └── GrpcExceptionHandlerImpl.java # gRPC μ „μ—­ μ˜ˆμ™Έ 처리 +β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ proto/ # gRPC proto 파일 +β”‚ β”‚ β”‚ β”œβ”€β”€ user_query.proto +β”‚ β”‚ β”‚ └── image.proto +β”‚ β”‚ β”‚ +β”‚ β”‚ └── resources/ +β”‚ β”‚ β”œβ”€β”€ application.yml +β”‚ β”‚ └── templates/email/ # 이메일 HTML ν…œν”Œλ¦Ώ (Thymeleaf) +β”‚ β”‚ β”œβ”€β”€ email-verification.html +β”‚ β”‚ └── password-reset.html +β”‚ β”‚ +β”‚ └── test/ +β”‚ └── java/flipnote/user/ +β”‚ +β”œβ”€β”€ Dockerfile +β”œβ”€β”€ build.gradle.kts +└── settings.gradle.kts +``` \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index aa6c6c5..b1caa57 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,13 +23,6 @@ repositories { extra["springGrpcVersion"] = "1.0.2" - -dependencyManagement { - imports { - mavenBom("org.springframework.grpc:spring-grpc-dependencies:1.0.2") - } -} - dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") diff --git a/src/main/java/flipnote/user/application/AuthService.java b/src/main/java/flipnote/user/application/AuthService.java new file mode 100644 index 0000000..acfdd4e --- /dev/null +++ b/src/main/java/flipnote/user/application/AuthService.java @@ -0,0 +1,232 @@ +package flipnote.user.application; + +import java.util.List; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import flipnote.user.application.command.ChangePasswordCommand; +import flipnote.user.application.command.LoginCommand; +import flipnote.user.application.command.SignupCommand; +import flipnote.user.application.result.SocialLinksResult; +import flipnote.user.application.result.TokenValidateResult; +import flipnote.user.application.result.UserRegisterResult; +import flipnote.user.domain.AuthErrorCode; +import flipnote.user.domain.TokenClaims; +import flipnote.user.domain.TokenPair; +import flipnote.user.domain.UserErrorCode; +import flipnote.user.domain.common.BizException; +import flipnote.user.domain.entity.OAuthLink; +import flipnote.user.domain.entity.User; +import flipnote.user.domain.event.EmailVerificationSendEvent; +import flipnote.user.domain.event.PasswordResetCreateEvent; +import flipnote.user.domain.repository.OAuthLinkRepository; +import flipnote.user.domain.repository.UserRepository; +import flipnote.user.infrastructure.config.ClientProperties; +import flipnote.user.infrastructure.jwt.JwtProvider; +import flipnote.user.infrastructure.mail.PasswordResetTokenGenerator; +import flipnote.user.infrastructure.mail.VerificationCodeGenerator; +import flipnote.user.infrastructure.redis.EmailVerificationRepository; +import flipnote.user.infrastructure.redis.PasswordResetRepository; +import flipnote.user.infrastructure.redis.SessionInvalidationRepository; +import flipnote.user.infrastructure.redis.TokenBlacklistRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuthService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtProvider jwtProvider; + private final TokenBlacklistRepository tokenBlacklistRepository; + private final EmailVerificationRepository emailVerificationRepository; + private final PasswordResetRepository passwordResetRepository; + private final OAuthLinkRepository oAuthLinkRepository; + private final SessionInvalidationRepository sessionInvalidationRepository; + private final VerificationCodeGenerator verificationCodeGenerator; + private final PasswordResetTokenGenerator passwordResetTokenGenerator; + private final ClientProperties clientProperties; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public UserRegisterResult register(SignupCommand command) { + if (!emailVerificationRepository.isVerified(command.email())) { + throw new BizException(AuthErrorCode.UNVERIFIED_EMAIL); + } + + if (userRepository.existsByEmail(command.email())) { + throw new BizException(AuthErrorCode.EMAIL_ALREADY_EXISTS); + } + + User user = User.builder() + .email(command.email()) + .password(passwordEncoder.encode(command.password())) + .name(command.name()) + .nickname(command.nickname()) + .phone(command.phone()) + .smsAgree(command.smsAgree()) + .build(); + + User savedUser = userRepository.save(user); + return UserRegisterResult.from(savedUser); + } + + public TokenPair login(LoginCommand command) { + User user = userRepository.findByEmailAndStatus(command.email(), User.Status.ACTIVE) + .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); + + if (!passwordEncoder.matches(command.password(), user.getPassword())) { + throw new BizException(AuthErrorCode.INVALID_CREDENTIALS); + } + + return jwtProvider.generateTokenPair(user); + } + + public void logout(String refreshToken) { + if (refreshToken != null && jwtProvider.isTokenValid(refreshToken)) { + long remaining = jwtProvider.getRemainingExpiration(refreshToken); + if (remaining > 0) { + tokenBlacklistRepository.add(refreshToken, remaining); + } + } + } + + public TokenPair refreshToken(String refreshToken) { + if (refreshToken == null || !jwtProvider.isTokenValid(refreshToken)) { + throw new BizException(AuthErrorCode.INVALID_TOKEN); + } + + if (tokenBlacklistRepository.isBlacklisted(refreshToken)) { + throw new BizException(AuthErrorCode.BLACKLISTED_TOKEN); + } + + TokenClaims claims = jwtProvider.extractClaims(refreshToken); + + sessionInvalidationRepository.getInvalidatedAtMillis(claims.userId()).ifPresent(invalidatedAtMillis -> { + if (jwtProvider.getIssuedAt(refreshToken).getTime() < invalidatedAtMillis) { + throw new BizException(AuthErrorCode.INVALIDATED_SESSION); + } + }); + + User user = findActiveUser(claims.userId()); + + long remaining = jwtProvider.getRemainingExpiration(refreshToken); + if (remaining > 0) { + tokenBlacklistRepository.add(refreshToken, remaining); + } + + return jwtProvider.generateTokenPair(user); + } + + @Transactional + public void changePassword(Long userId, ChangePasswordCommand command) { + User user = findActiveUser(userId); + + if (!passwordEncoder.matches(command.currentPassword(), user.getPassword())) { + throw new BizException(AuthErrorCode.PASSWORD_MISMATCH); + } + + user.changePassword(passwordEncoder.encode(command.newPassword())); + sessionInvalidationRepository.invalidate(user.getId(), jwtProvider.getRefreshTokenExpiration()); + } + + public TokenValidateResult validateToken(String token) { + if (!jwtProvider.isTokenValid(token)) { + throw new BizException(AuthErrorCode.INVALID_TOKEN); + } + + if (tokenBlacklistRepository.isBlacklisted(token)) { + throw new BizException(AuthErrorCode.BLACKLISTED_TOKEN); + } + + TokenClaims claims = jwtProvider.extractClaims(token); + + sessionInvalidationRepository.getInvalidatedAtMillis(claims.userId()).ifPresent(invalidatedAtMillis -> { + if (jwtProvider.getIssuedAt(token).getTime() < invalidatedAtMillis) { + throw new BizException(AuthErrorCode.INVALIDATED_SESSION); + } + }); + + findActiveUser(claims.userId()); + + return new TokenValidateResult(claims.userId(), claims.email(), claims.role()); + } + + public void sendEmailVerificationCode(String email) { + if (emailVerificationRepository.hasCode(email)) { + throw new BizException(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE); + } + + String code = verificationCodeGenerator.generate(); + emailVerificationRepository.saveCode(email, code); + eventPublisher.publishEvent(new EmailVerificationSendEvent(email, code)); + } + + public void verifyEmail(String email, String code) { + if (!emailVerificationRepository.hasCode(email)) { + throw new BizException(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE); + } + + String savedCode = emailVerificationRepository.getCode(email); + if (!code.equals(savedCode)) { + throw new BizException(AuthErrorCode.INVALID_VERIFICATION_CODE); + } + + emailVerificationRepository.deleteCode(email); + emailVerificationRepository.markVerified(email); + } + + public void requestPasswordReset(String email) { + if (!userRepository.existsByEmail(email)) { + return; + } + + if (passwordResetRepository.hasToken(email)) { + throw new BizException(AuthErrorCode.ALREADY_SENT_PASSWORD_RESET_LINK); + } + + String token = passwordResetTokenGenerator.generate(); + passwordResetRepository.save(token, email); + + String link = clientProperties.getUrl() + clientProperties.getPaths().getPasswordReset() + + "?token=" + token; + eventPublisher.publishEvent(new PasswordResetCreateEvent(email, link)); + } + + @Transactional + public void resetPassword(String token, String newPassword) { + String email = passwordResetRepository.findEmailByToken(token); + if (email == null) { + throw new BizException(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN); + } + + User user = userRepository.findByEmailAndStatus(email, User.Status.ACTIVE) + .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); + + user.changePassword(passwordEncoder.encode(newPassword)); + sessionInvalidationRepository.invalidate(user.getId(), jwtProvider.getRefreshTokenExpiration()); + passwordResetRepository.delete(token, email); + } + + public SocialLinksResult getSocialLinks(Long userId) { + List links = oAuthLinkRepository.findByUser_Id(userId); + return SocialLinksResult.from(links); + } + + @Transactional + public void deleteSocialLink(Long userId, Long socialLinkId) { + if (!oAuthLinkRepository.existsByIdAndUser_Id(socialLinkId, userId)) { + throw new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT); + } + oAuthLinkRepository.deleteById(socialLinkId); + } + + private User findActiveUser(Long userId) { + return userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) + .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); + } +} diff --git a/src/main/java/flipnote/user/application/OAuthService.java b/src/main/java/flipnote/user/application/OAuthService.java new file mode 100644 index 0000000..b6aabbd --- /dev/null +++ b/src/main/java/flipnote/user/application/OAuthService.java @@ -0,0 +1,113 @@ +package flipnote.user.application; + +import java.util.Map; +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import flipnote.user.domain.AuthErrorCode; +import flipnote.user.domain.TokenPair; +import flipnote.user.domain.UserErrorCode; +import flipnote.user.domain.common.BizException; +import flipnote.user.domain.entity.OAuthLink; +import flipnote.user.domain.entity.User; +import flipnote.user.domain.repository.OAuthLinkRepository; +import flipnote.user.domain.repository.UserRepository; +import flipnote.user.infrastructure.jwt.JwtProvider; +import flipnote.user.infrastructure.oauth.OAuth2UserInfo; +import flipnote.user.infrastructure.oauth.OAuthApiClient; +import flipnote.user.infrastructure.oauth.OAuthProperties; +import flipnote.user.infrastructure.oauth.PkceUtil; +import flipnote.user.infrastructure.redis.SocialLinkTokenRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class OAuthService { + + private final PkceUtil pkceUtil; + private final OAuthApiClient oAuthApiClient; + private final OAuthLinkRepository oAuthLinkRepository; + private final UserRepository userRepository; + private final SocialLinkTokenRepository socialLinkTokenRepository; + private final JwtProvider jwtProvider; + private final OAuthProperties oAuthProperties; + + public record AuthorizationRedirect(String authorizeUri, String codeVerifier) { + } + + public AuthorizationRedirect getAuthorizationUri(String providerName, Long userId) { + OAuthProperties.Provider provider = resolveProvider(providerName); + + String codeVerifier = pkceUtil.generateCodeVerifier(); + String codeChallenge = pkceUtil.generateCodeChallenge(codeVerifier); + + String state = null; + if (userId != null) { + state = UUID.randomUUID().toString(); + socialLinkTokenRepository.save(userId, state); + } + + String authorizeUri = oAuthApiClient.buildAuthorizeUri(provider, codeChallenge, state); + + return new AuthorizationRedirect(authorizeUri, codeVerifier); + } + + public TokenPair socialLogin(String providerName, String code, String codeVerifier) { + OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier); + + OAuthLink oAuthLink = oAuthLinkRepository + .findByProviderAndProviderIdWithUser(userInfo.getProvider(), userInfo.getProviderId()) + .orElseThrow(() -> new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT)); + + return jwtProvider.generateTokenPair(oAuthLink.getUser()); + } + + @Transactional + public void linkSocialAccount(String providerName, String code, String state, String codeVerifier) { + Long userId = socialLinkTokenRepository.findUserIdByState(state) + .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_SOCIAL_LINK_TOKEN)); + + socialLinkTokenRepository.delete(state); + + OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier); + + if (oAuthLinkRepository.existsByProviderAndProviderId(userInfo.getProvider(), userInfo.getProviderId())) { + throw new BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT); + } + + User user = userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) + .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); + + OAuthLink link = OAuthLink.builder() + .provider(userInfo.getProvider()) + .providerId(userInfo.getProviderId()) + .user(user) + .build(); + oAuthLinkRepository.save(link); + } + + private OAuth2UserInfo getOAuth2UserInfo(String providerName, String code, String codeVerifier) { + OAuthProperties.Provider provider = resolveProvider(providerName); + String accessToken = oAuthApiClient.requestAccessToken(provider, code, codeVerifier); + Map attributes = oAuthApiClient.requestUserInfo(provider, accessToken); + return oAuthApiClient.createUserInfo(providerName, attributes); + } + + private OAuthProperties.Provider resolveProvider(String providerName) { + Map providers = oAuthProperties.getProviders(); + if (providers == null) { + throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); + } + OAuthProperties.Provider provider = providers.get(providerName.toLowerCase()); + if (provider == null) { + log.warn("μ§€μ›ν•˜μ§€ μ•ŠλŠ” OAuth Provider: {}", providerName); + throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); + } + return provider; + } +} diff --git a/src/main/java/flipnote/user/application/UserService.java b/src/main/java/flipnote/user/application/UserService.java new file mode 100644 index 0000000..6d33ab1 --- /dev/null +++ b/src/main/java/flipnote/user/application/UserService.java @@ -0,0 +1,122 @@ +package flipnote.user.application; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import flipnote.image.grpc.v1.ActivateImageRequest; +import flipnote.image.grpc.v1.ActivateImageResponse; +import flipnote.image.grpc.v1.ChangeImageRequest; +import flipnote.image.grpc.v1.ChangeImageResponse; +import flipnote.image.grpc.v1.ImageCommandServiceGrpc; +import flipnote.image.grpc.v1.Type; +import flipnote.user.application.command.UpdateProfileCommand; +import flipnote.user.application.result.MyInfoResult; +import flipnote.user.application.result.UserInfoResult; +import flipnote.user.application.result.UserResult; +import flipnote.user.application.result.UserUpdateResult; +import flipnote.user.domain.AuthErrorCode; +import flipnote.user.domain.ImageErrorCode; +import flipnote.user.domain.TokenClaims; +import flipnote.user.domain.UserErrorCode; +import flipnote.user.domain.common.BizException; +import flipnote.user.domain.entity.User; +import flipnote.user.domain.repository.UserRepository; +import flipnote.user.infrastructure.jwt.JwtProvider; +import flipnote.user.infrastructure.redis.SessionInvalidationRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository userRepository; + private final SessionInvalidationRepository sessionInvalidationRepository; + private final JwtProvider jwtProvider; + private final ImageCommandServiceGrpc.ImageCommandServiceBlockingStub imageCommandServiceStub; + + public MyInfoResult getMyInfo(Long userId) { + User user = findActiveUser(userId); + return MyInfoResult.from(user); + } + + public UserInfoResult getUserInfo(Long userId) { + User user = userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) + .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); + return UserInfoResult.from(user); + } + + @Transactional + public UserUpdateResult updateProfile(Long userId, UpdateProfileCommand command) { + User user = findActiveUser(userId); + + String profileImageUrl = null; + if (command.imageRefId() != null) { + try { + if (User.DEFAULT_PROFILE_IMAGE_URL.equals(user.getProfileImageUrl())) { + ActivateImageResponse activateImageResponse = imageCommandServiceStub.activateImage( + ActivateImageRequest.newBuilder() + .setReferenceType(Type.USER) + .setReferenceId(userId) + .setImageRefId(command.imageRefId()) + .build()); + profileImageUrl = activateImageResponse.getUrl(); + } else { + ChangeImageResponse changeImageResponse = imageCommandServiceStub.changeImage( + ChangeImageRequest.newBuilder() + .setReferenceType(Type.USER) + .setReferenceId(userId) + .setImageRefId(command.imageRefId()) + .build()); + profileImageUrl = changeImageResponse.getUrl(); + } + } catch (Exception ex) { + log.error("updateProfile", ex); + throw new BizException(ImageErrorCode.IMAGE_SERVICE_ERROR); + } + } + + user.updateProfile(command.nickname(), command.phone(), Boolean.TRUE.equals(command.smsAgree()), profileImageUrl); + return UserUpdateResult.from(user, command.imageRefId()); + } + + @Transactional + public void withdraw(Long userId) { + User user = findActiveUser(userId); + user.withdraw(); + sessionInvalidationRepository.invalidate(userId, jwtProvider.getRefreshTokenExpiration()); + } + + public Optional findActiveUserById(Long userId) { + return userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) + .map(UserResult::from); + } + + public List findActiveUsersByIds(List userIds) { + return userRepository.findByIdInAndStatus(userIds, User.Status.ACTIVE) + .stream().map(UserResult::from).toList(); + } + + public Optional findActiveUserByEmail(String email) { + return userRepository.findByEmailAndStatus(email, User.Status.ACTIVE) + .map(UserResult::from); + } + + public UserResult findUserByToken(String token) { + if (!jwtProvider.isTokenValid(token)) { + throw new BizException(AuthErrorCode.INVALID_TOKEN); + } + TokenClaims claims = jwtProvider.extractClaims(token); + return UserResult.from(findActiveUser(claims.userId())); + } + + private User findActiveUser(Long userId) { + return userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) + .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); + } +} diff --git a/src/main/java/flipnote/user/application/command/ChangePasswordCommand.java b/src/main/java/flipnote/user/application/command/ChangePasswordCommand.java new file mode 100644 index 0000000..ecc1ae6 --- /dev/null +++ b/src/main/java/flipnote/user/application/command/ChangePasswordCommand.java @@ -0,0 +1,3 @@ +package flipnote.user.application.command; + +public record ChangePasswordCommand(String currentPassword, String newPassword) {} diff --git a/src/main/java/flipnote/user/application/command/LoginCommand.java b/src/main/java/flipnote/user/application/command/LoginCommand.java new file mode 100644 index 0000000..f824ee9 --- /dev/null +++ b/src/main/java/flipnote/user/application/command/LoginCommand.java @@ -0,0 +1,3 @@ +package flipnote.user.application.command; + +public record LoginCommand(String email, String password) {} diff --git a/src/main/java/flipnote/user/application/command/SignupCommand.java b/src/main/java/flipnote/user/application/command/SignupCommand.java new file mode 100644 index 0000000..024f3f7 --- /dev/null +++ b/src/main/java/flipnote/user/application/command/SignupCommand.java @@ -0,0 +1,10 @@ +package flipnote.user.application.command; + +public record SignupCommand( + String email, + String password, + String name, + String nickname, + String phone, + boolean smsAgree +) {} diff --git a/src/main/java/flipnote/user/application/command/UpdateProfileCommand.java b/src/main/java/flipnote/user/application/command/UpdateProfileCommand.java new file mode 100644 index 0000000..218635f --- /dev/null +++ b/src/main/java/flipnote/user/application/command/UpdateProfileCommand.java @@ -0,0 +1,8 @@ +package flipnote.user.application.command; + +public record UpdateProfileCommand( + String nickname, + String phone, + Boolean smsAgree, + Long imageRefId +) {} diff --git a/src/main/java/flipnote/user/user/presentation/dto/response/MyInfoResponse.java b/src/main/java/flipnote/user/application/result/MyInfoResult.java similarity index 68% rename from src/main/java/flipnote/user/user/presentation/dto/response/MyInfoResponse.java rename to src/main/java/flipnote/user/application/result/MyInfoResult.java index 0798946..90fc081 100644 --- a/src/main/java/flipnote/user/user/presentation/dto/response/MyInfoResponse.java +++ b/src/main/java/flipnote/user/application/result/MyInfoResult.java @@ -1,7 +1,6 @@ -package flipnote.user.user.presentation.dto.response; +package flipnote.user.application.result; -import com.fasterxml.jackson.annotation.JsonFormat; -import flipnote.user.user.domain.User; +import flipnote.user.domain.entity.User; import lombok.AllArgsConstructor; import lombok.Getter; @@ -9,7 +8,7 @@ @Getter @AllArgsConstructor -public class MyInfoResponse { +public class MyInfoResult { private Long userId; private String email; @@ -19,15 +18,11 @@ public class MyInfoResponse { private Boolean smsAgree; private String profileImageUrl; private Long imageRefId; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createdAt; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime modifiedAt; - public static MyInfoResponse from(User user) { - return new MyInfoResponse( + public static MyInfoResult from(User user) { + return new MyInfoResult( user.getId(), user.getEmail(), user.getNickname(), diff --git a/src/main/java/flipnote/user/application/result/SocialLinkResult.java b/src/main/java/flipnote/user/application/result/SocialLinkResult.java new file mode 100644 index 0000000..e5e088e --- /dev/null +++ b/src/main/java/flipnote/user/application/result/SocialLinkResult.java @@ -0,0 +1,20 @@ +package flipnote.user.application.result; + +import flipnote.user.domain.entity.OAuthLink; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class SocialLinkResult { + + private Long socialLinkId; + private String provider; + private LocalDateTime linkedAt; + + public static SocialLinkResult from(OAuthLink link) { + return new SocialLinkResult(link.getId(), link.getProvider(), link.getLinkedAt()); + } +} diff --git a/src/main/java/flipnote/user/application/result/SocialLinksResult.java b/src/main/java/flipnote/user/application/result/SocialLinksResult.java new file mode 100644 index 0000000..f2c71d0 --- /dev/null +++ b/src/main/java/flipnote/user/application/result/SocialLinksResult.java @@ -0,0 +1,18 @@ +package flipnote.user.application.result; + +import flipnote.user.domain.entity.OAuthLink; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class SocialLinksResult { + + private List socialLinks; + + public static SocialLinksResult from(List links) { + return new SocialLinksResult(links.stream().map(SocialLinkResult::from).toList()); + } +} diff --git a/src/main/java/flipnote/user/application/result/TokenValidateResult.java b/src/main/java/flipnote/user/application/result/TokenValidateResult.java new file mode 100644 index 0000000..8a35b85 --- /dev/null +++ b/src/main/java/flipnote/user/application/result/TokenValidateResult.java @@ -0,0 +1,3 @@ +package flipnote.user.application.result; + +public record TokenValidateResult(Long userId, String email, String role) {} diff --git a/src/main/java/flipnote/user/application/result/UserInfoResult.java b/src/main/java/flipnote/user/application/result/UserInfoResult.java new file mode 100644 index 0000000..e9db900 --- /dev/null +++ b/src/main/java/flipnote/user/application/result/UserInfoResult.java @@ -0,0 +1,19 @@ +package flipnote.user.application.result; + +import flipnote.user.domain.entity.User; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UserInfoResult { + + private Long userId; + private String nickname; + private String profileImageUrl; + private Long imageRefId; + + public static UserInfoResult from(User user) { + return new UserInfoResult(user.getId(), user.getNickname(), user.getProfileImageUrl(), null); + } +} diff --git a/src/main/java/flipnote/user/application/result/UserRegisterResult.java b/src/main/java/flipnote/user/application/result/UserRegisterResult.java new file mode 100644 index 0000000..fc2ef22 --- /dev/null +++ b/src/main/java/flipnote/user/application/result/UserRegisterResult.java @@ -0,0 +1,10 @@ +package flipnote.user.application.result; + +import flipnote.user.domain.entity.User; + +public record UserRegisterResult(Long userId) { + + public static UserRegisterResult from(User user) { + return new UserRegisterResult(user.getId()); + } +} diff --git a/src/main/java/flipnote/user/application/result/UserResult.java b/src/main/java/flipnote/user/application/result/UserResult.java new file mode 100644 index 0000000..ea23b5f --- /dev/null +++ b/src/main/java/flipnote/user/application/result/UserResult.java @@ -0,0 +1,16 @@ +package flipnote.user.application.result; + +import flipnote.user.domain.entity.User; + +public record UserResult(Long id, String email, String nickname, String profileImageUrl, String role) { + + public static UserResult from(User user) { + return new UserResult( + user.getId(), + user.getEmail(), + user.getNickname(), + user.getProfileImageUrl() != null ? user.getProfileImageUrl() : "", + user.getRole().name() + ); + } +} diff --git a/src/main/java/flipnote/user/application/result/UserUpdateResult.java b/src/main/java/flipnote/user/application/result/UserUpdateResult.java new file mode 100644 index 0000000..8c9e7c6 --- /dev/null +++ b/src/main/java/flipnote/user/application/result/UserUpdateResult.java @@ -0,0 +1,28 @@ +package flipnote.user.application.result; + +import flipnote.user.domain.entity.User; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UserUpdateResult { + + private Long userId; + private String nickname; + private String phone; + private Boolean smsAgree; + private String profileImageUrl; + private Long imageRefId; + + public static UserUpdateResult from(User user, Long imageRefId) { + return new UserUpdateResult( + user.getId(), + user.getNickname(), + user.getPhone(), + user.isSmsAgree(), + user.getProfileImageUrl(), + imageRefId + ); + } +} diff --git a/src/main/java/flipnote/user/auth/application/AuthService.java b/src/main/java/flipnote/user/auth/application/AuthService.java deleted file mode 100644 index 0b81523..0000000 --- a/src/main/java/flipnote/user/auth/application/AuthService.java +++ /dev/null @@ -1,232 +0,0 @@ -package flipnote.user.auth.application; - -import flipnote.user.auth.domain.AuthErrorCode; -import flipnote.user.auth.domain.TokenClaims; -import flipnote.user.auth.domain.TokenPair; -import flipnote.user.auth.domain.event.EmailVerificationSendEvent; -import flipnote.user.auth.domain.event.PasswordResetCreateEvent; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import flipnote.user.auth.infrastructure.redis.EmailVerificationRepository; -import flipnote.user.auth.infrastructure.redis.PasswordResetRepository; -import flipnote.user.auth.infrastructure.redis.PasswordResetTokenGenerator; -import flipnote.user.auth.infrastructure.redis.SessionInvalidationRepository; -import flipnote.user.auth.infrastructure.redis.TokenBlacklistRepository; -import flipnote.user.auth.infrastructure.redis.VerificationCodeGenerator; -import flipnote.user.auth.presentation.dto.request.ChangePasswordRequest; -import flipnote.user.auth.presentation.dto.request.LoginRequest; -import flipnote.user.auth.presentation.dto.request.SignupRequest; -import flipnote.user.auth.presentation.dto.response.SocialLinksResponse; -import flipnote.user.auth.presentation.dto.response.TokenValidateResponse; -import flipnote.user.auth.presentation.dto.response.UserResponse; -import flipnote.user.global.config.ClientProperties; -import flipnote.user.global.exception.BizException; -import flipnote.user.user.domain.OAuthLink; -import flipnote.user.user.domain.OAuthLinkRepository; -import flipnote.user.user.domain.User; -import flipnote.user.user.domain.UserErrorCode; -import flipnote.user.user.domain.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class AuthService { - - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final JwtProvider jwtProvider; - private final TokenBlacklistRepository tokenBlacklistRepository; - private final EmailVerificationRepository emailVerificationRepository; - private final PasswordResetRepository passwordResetRepository; - private final OAuthLinkRepository oAuthLinkRepository; - private final SessionInvalidationRepository sessionInvalidationRepository; - private final VerificationCodeGenerator verificationCodeGenerator; - private final PasswordResetTokenGenerator passwordResetTokenGenerator; - private final ClientProperties clientProperties; - private final ApplicationEventPublisher eventPublisher; - - @Transactional - public UserResponse register(SignupRequest request) { - if (!emailVerificationRepository.isVerified(request.getEmail())) { - throw new BizException(AuthErrorCode.UNVERIFIED_EMAIL); - } - - if (userRepository.existsByEmail(request.getEmail())) { - throw new BizException(AuthErrorCode.EMAIL_ALREADY_EXISTS); - } - - User user = User.builder() - .email(request.getEmail()) - .password(passwordEncoder.encode(request.getPassword())) - .name(request.getName()) - .nickname(request.getNickname()) - .phone(request.getPhone()) - .smsAgree(Boolean.TRUE.equals(request.getSmsAgree())) - .build(); - - User savedUser = userRepository.save(user); - return UserResponse.from(savedUser); - } - - public TokenPair login(LoginRequest request) { - User user = userRepository.findByEmailAndStatus(request.getEmail(), User.Status.ACTIVE) - .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); - - if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { - throw new BizException(AuthErrorCode.INVALID_CREDENTIALS); - } - - return jwtProvider.generateTokenPair(user); - } - - public void logout(String refreshToken) { - if (refreshToken != null && jwtProvider.isTokenValid(refreshToken)) { - long remaining = jwtProvider.getRemainingExpiration(refreshToken); - if (remaining > 0) { - tokenBlacklistRepository.add(refreshToken, remaining); - } - } - } - - public TokenPair refreshToken(String refreshToken) { - if (refreshToken == null || !jwtProvider.isTokenValid(refreshToken)) { - throw new BizException(AuthErrorCode.INVALID_TOKEN); - } - - if (tokenBlacklistRepository.isBlacklisted(refreshToken)) { - throw new BizException(AuthErrorCode.BLACKLISTED_TOKEN); - } - - TokenClaims claims = jwtProvider.extractClaims(refreshToken); - - sessionInvalidationRepository.getInvalidatedAtMillis(claims.userId()).ifPresent(invalidatedAtMillis -> { - if (jwtProvider.getIssuedAt(refreshToken).getTime() < invalidatedAtMillis) { - throw new BizException(AuthErrorCode.INVALIDATED_SESSION); - } - }); - - User user = findActiveUser(claims.userId()); - - long remaining = jwtProvider.getRemainingExpiration(refreshToken); - if (remaining > 0) { - tokenBlacklistRepository.add(refreshToken, remaining); - } - - return jwtProvider.generateTokenPair(user); - } - - @Transactional - public void changePassword(Long userId, ChangePasswordRequest request) { - User user = findActiveUser(userId); - - if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) { - throw new BizException(AuthErrorCode.PASSWORD_MISMATCH); - } - - user.changePassword(passwordEncoder.encode(request.getNewPassword())); - sessionInvalidationRepository.invalidate(user.getId(), jwtProvider.getRefreshTokenExpiration()); - } - - public TokenValidateResponse validateToken(String token) { - if (!jwtProvider.isTokenValid(token)) { - throw new BizException(AuthErrorCode.INVALID_TOKEN); - } - - if (tokenBlacklistRepository.isBlacklisted(token)) { - throw new BizException(AuthErrorCode.BLACKLISTED_TOKEN); - } - - TokenClaims claims = jwtProvider.extractClaims(token); - - sessionInvalidationRepository.getInvalidatedAtMillis(claims.userId()).ifPresent(invalidatedAtMillis -> { - if (jwtProvider.getIssuedAt(token).getTime() < invalidatedAtMillis) { - throw new BizException(AuthErrorCode.INVALIDATED_SESSION); - } - }); - - findActiveUser(claims.userId()); - - return new TokenValidateResponse(claims.userId(), claims.email(), claims.role()); - } - - public void sendEmailVerificationCode(String email) { - if (emailVerificationRepository.hasCode(email)) { - throw new BizException(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE); - } - - String code = verificationCodeGenerator.generate(); - emailVerificationRepository.saveCode(email, code); - eventPublisher.publishEvent(new EmailVerificationSendEvent(email, code)); - } - - public void verifyEmail(String email, String code) { - if (!emailVerificationRepository.hasCode(email)) { - throw new BizException(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE); - } - - String savedCode = emailVerificationRepository.getCode(email); - if (!code.equals(savedCode)) { - throw new BizException(AuthErrorCode.INVALID_VERIFICATION_CODE); - } - - emailVerificationRepository.deleteCode(email); - emailVerificationRepository.markVerified(email); - } - - public void requestPasswordReset(String email) { - // μ‚¬μš©μžκ°€ 없어도 정상 λ°˜ν™˜ (이메일 쑴재 μ—¬λΆ€ λ…ΈμΆœ λ°©μ§€) - if (!userRepository.existsByEmail(email)) { - return; - } - - if (passwordResetRepository.hasToken(email)) { - throw new BizException(AuthErrorCode.ALREADY_SENT_PASSWORD_RESET_LINK); - } - - String token = passwordResetTokenGenerator.generate(); - passwordResetRepository.save(token, email); - - String link = clientProperties.getUrl() + clientProperties.getPaths().getPasswordReset() - + "?token=" + token; - eventPublisher.publishEvent(new PasswordResetCreateEvent(email, link)); - } - - @Transactional - public void resetPassword(String token, String newPassword) { - String email = passwordResetRepository.findEmailByToken(token); - if (email == null) { - throw new BizException(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN); - } - - User user = userRepository.findByEmailAndStatus(email, User.Status.ACTIVE) - .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); - - user.changePassword(passwordEncoder.encode(newPassword)); - sessionInvalidationRepository.invalidate(user.getId(), jwtProvider.getRefreshTokenExpiration()); - passwordResetRepository.delete(token, email); - } - - public SocialLinksResponse getSocialLinks(Long userId) { - List links = oAuthLinkRepository.findByUser_Id(userId); - return SocialLinksResponse.from(links); - } - - @Transactional - public void deleteSocialLink(Long userId, Long socialLinkId) { - if (!oAuthLinkRepository.existsByIdAndUser_Id(socialLinkId, userId)) { - throw new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT); - } - oAuthLinkRepository.deleteById(socialLinkId); - } - - private User findActiveUser(Long userId) { - return userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) - .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); - } -} diff --git a/src/main/java/flipnote/user/auth/application/OAuthService.java b/src/main/java/flipnote/user/auth/application/OAuthService.java deleted file mode 100644 index fb5eb6c..0000000 --- a/src/main/java/flipnote/user/auth/application/OAuthService.java +++ /dev/null @@ -1,127 +0,0 @@ -package flipnote.user.auth.application; - -import flipnote.user.auth.domain.AuthErrorCode; -import flipnote.user.auth.domain.TokenPair; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import flipnote.user.auth.infrastructure.oauth.OAuthApiClient; -import flipnote.user.auth.infrastructure.oauth.OAuth2UserInfo; -import flipnote.user.auth.infrastructure.oauth.PkceUtil; -import flipnote.user.auth.infrastructure.redis.SocialLinkTokenRepository; -import flipnote.user.global.config.OAuthProperties; -import flipnote.user.global.constants.HttpConstants; -import flipnote.user.global.exception.BizException; -import flipnote.user.user.domain.OAuthLink; -import flipnote.user.user.domain.OAuthLinkRepository; -import flipnote.user.user.domain.User; -import flipnote.user.user.domain.UserErrorCode; -import flipnote.user.user.domain.UserRepository; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseCookie; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Map; -import java.util.UUID; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class OAuthService { - - private final PkceUtil pkceUtil; - private final OAuthApiClient oAuthApiClient; - private final OAuthLinkRepository oAuthLinkRepository; - private final UserRepository userRepository; - private final SocialLinkTokenRepository socialLinkTokenRepository; - private final JwtProvider jwtProvider; - private final OAuthProperties oAuthProperties; - - public record AuthorizationRedirect(String authorizeUri, ResponseCookie verifierCookie) {} - - private static final int VERIFIER_COOKIE_MAX_AGE = 180; - - public AuthorizationRedirect getAuthorizationUri(String providerName, Long userId) { - OAuthProperties.Provider provider = resolveProvider(providerName); - - String codeVerifier = pkceUtil.generateCodeVerifier(); - String codeChallenge = pkceUtil.generateCodeChallenge(codeVerifier); - - String state = null; - if (userId != null) { - state = UUID.randomUUID().toString(); - socialLinkTokenRepository.save(userId, state); - } - - String authorizeUri = oAuthApiClient.buildAuthorizeUri(provider, codeChallenge, state); - - ResponseCookie verifierCookie = ResponseCookie.from(HttpConstants.OAUTH_VERIFIER_COOKIE, codeVerifier) - .httpOnly(true) - .secure(true) - .path("/") - .maxAge(VERIFIER_COOKIE_MAX_AGE) - .sameSite("Lax") - .build(); - - return new AuthorizationRedirect(authorizeUri, verifierCookie); - } - - public TokenPair socialLogin(String providerName, String code, String codeVerifier) { - OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier); - - OAuthLink oAuthLink = oAuthLinkRepository - .findByProviderAndProviderIdWithUser(userInfo.getProvider(), userInfo.getProviderId()) - .orElseThrow(() -> new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT)); - - return jwtProvider.generateTokenPair(oAuthLink.getUser()); - } - - @Transactional - public void linkSocialAccount(String providerName, String code, String state, - String codeVerifier) { - Long userId = socialLinkTokenRepository.findUserIdByState(state) - .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_SOCIAL_LINK_TOKEN)); - - socialLinkTokenRepository.delete(state); - - OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier); - - if (oAuthLinkRepository.existsByUser_IdAndProviderAndProviderId( - userId, userInfo.getProvider(), userInfo.getProviderId())) { - throw new BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT); - } - - User user = userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) - .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); - - OAuthLink link = OAuthLink.builder() - .provider(userInfo.getProvider()) - .providerId(userInfo.getProviderId()) - .user(user) - .build(); - oAuthLinkRepository.save(link); - } - - private OAuth2UserInfo getOAuth2UserInfo(String providerName, String code, - String codeVerifier) { - OAuthProperties.Provider provider = resolveProvider(providerName); - String accessToken = oAuthApiClient.requestAccessToken(provider, code, codeVerifier); - Map attributes = oAuthApiClient.requestUserInfo(provider, accessToken); - return oAuthApiClient.createUserInfo(providerName, attributes); - } - - private OAuthProperties.Provider resolveProvider(String providerName) { - Map providers = oAuthProperties.getProviders(); - if (providers == null) { - throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); - } - OAuthProperties.Provider provider = providers.get(providerName.toLowerCase()); - if (provider == null) { - log.warn("μ§€μ›ν•˜μ§€ μ•ŠλŠ” OAuth Provider: {}", providerName); - throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); - } - return provider; - } -} diff --git a/src/main/java/flipnote/user/auth/domain/AuthErrorCode.java b/src/main/java/flipnote/user/auth/domain/AuthErrorCode.java deleted file mode 100644 index 6ff29e5..0000000 --- a/src/main/java/flipnote/user/auth/domain/AuthErrorCode.java +++ /dev/null @@ -1,37 +0,0 @@ -package flipnote.user.auth.domain; - -import flipnote.user.global.error.ErrorCode; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@Getter -@RequiredArgsConstructor -public enum AuthErrorCode implements ErrorCode { - - INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH_001", "이메일 λ˜λŠ” λΉ„λ°€λ²ˆν˜Έκ°€ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), - ALREADY_ISSUED_VERIFICATION_CODE(HttpStatus.TOO_MANY_REQUESTS, "AUTH_002", "이미 μΈμ¦μ½”λ“œκ°€ λ°œμ†‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”."), - NOT_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_003", "μΈμ¦μ½”λ“œκ°€ λ°œμ†‘λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."), - INVALID_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_004", "μΈμ¦μ½”λ“œκ°€ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), - EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "AUTH_005", "이미 μ‚¬μš© 쀑인 μ΄λ©”μΌμž…λ‹ˆλ‹€."), - INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_006", "μœ νš¨ν•˜μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€."), - UNVERIFIED_EMAIL(HttpStatus.FORBIDDEN, "AUTH_007", "이메일 인증이 μ™„λ£Œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."), - ALREADY_SENT_PASSWORD_RESET_LINK(HttpStatus.TOO_MANY_REQUESTS, "AUTH_008", "이미 λΉ„λ°€λ²ˆν˜Έ μž¬μ„€μ • 링크가 λ°œμ†‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”."), - INVALID_PASSWORD_RESET_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_009", "μœ νš¨ν•˜μ§€ μ•Šμ€ λΉ„λ°€λ²ˆν˜Έ μž¬μ„€μ • ν† ν°μž…λ‹ˆλ‹€."), - INVALID_SOCIAL_LINK_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_010", "μœ νš¨ν•˜μ§€ μ•Šμ€ μ†Œμ…œ 연동 ν† ν°μž…λ‹ˆλ‹€."), - ALREADY_LINKED_SOCIAL_ACCOUNT(HttpStatus.CONFLICT, "AUTH_011", "이미 μ—°κ²°λœ μ†Œμ…œ κ³„μ •μž…λ‹ˆλ‹€."), - NOT_REGISTERED_SOCIAL_ACCOUNT(HttpStatus.NOT_FOUND, "AUTH_012", "μ—°κ²°λœ μ†Œμ…œ 계정이 μ—†μŠ΅λ‹ˆλ‹€."), - INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH_013", "μ§€μ›ν•˜μ§€ μ•ŠλŠ” OAuth μ œκ³΅μžμž…λ‹ˆλ‹€."), - BLACKLISTED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_015", "λ¬΄νš¨ν™”λœ ν† ν°μž…λ‹ˆλ‹€."), - INVALIDATED_SESSION(HttpStatus.UNAUTHORIZED, "AUTH_016", "μ„Έμ…˜μ΄ λ¬΄νš¨ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄ μ£Όμ„Έμš”."), - PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH_017", "ν˜„μž¬ λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); - - private final HttpStatus httpStatus; - private final String code; - private final String message; - - @Override - public int getStatus() { - return httpStatus.value(); - } -} diff --git a/src/main/java/flipnote/user/auth/infrastructure/oauth/OAuthApiClient.java b/src/main/java/flipnote/user/auth/infrastructure/oauth/OAuthApiClient.java deleted file mode 100644 index 4417b43..0000000 --- a/src/main/java/flipnote/user/auth/infrastructure/oauth/OAuthApiClient.java +++ /dev/null @@ -1,96 +0,0 @@ -package flipnote.user.auth.infrastructure.oauth; - -import java.util.Map; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Service; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestClient; -import org.springframework.web.util.UriComponentsBuilder; - -import flipnote.user.global.config.OAuthProperties; - -import lombok.RequiredArgsConstructor; -import tools.jackson.core.type.TypeReference; -import tools.jackson.databind.ObjectMapper; - -@Service -@RequiredArgsConstructor -public class OAuthApiClient { - - private final RestClient restClient; - private final ObjectMapper objectMapper; - private final OAuthProperties oAuthProperties; - - public String requestAccessToken(OAuthProperties.Provider provider, String code, - String codeVerifier) { - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "authorization_code"); - params.add("client_id", provider.getClientId()); - params.add("client_secret", provider.getClientSecret()); - params.add("redirect_uri", buildRedirectUri(provider.getRedirectUri())); - params.add("code", code); - params.add("code_verifier", codeVerifier); - - try { - String responseBody = restClient.post() - .uri(provider.getTokenUri()) - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body(params) - .retrieve() - .body(String.class); - - Map responseMap = objectMapper.readValue(responseBody, new TypeReference<>() {}); - return (String) responseMap.get("access_token"); - } catch (Exception e) { - throw new RuntimeException("Failed to get OAuth access token", e); - } - } - - public Map requestUserInfo(OAuthProperties.Provider provider, String accessToken) { - try { - String responseBody = restClient.get() - .uri(provider.getUserInfoUri()) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) - .retrieve() - .body(String.class); - - return objectMapper.readValue(responseBody, new TypeReference<>() {}); - } catch (Exception e) { - throw new RuntimeException("Failed to get OAuth user info", e); - } - } - - public OAuth2UserInfo createUserInfo(String providerName, Map attributes) { - return switch (providerName.toLowerCase()) { - case "google" -> new GoogleUserInfo(attributes); - default -> throw new IllegalArgumentException("Unsupported OAuth provider: " + providerName); - }; - } - - public String buildAuthorizeUri(OAuthProperties.Provider provider, - String codeChallenge, String state) { - UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(provider.getAuthorizationUri()) - .queryParam("client_id", provider.getClientId()) - .queryParam("redirect_uri", buildRedirectUri(provider.getRedirectUri())) - .queryParam("response_type", "code") - .queryParam("scope", String.join(" ", provider.getScope())) - .queryParam("code_challenge", codeChallenge) - .queryParam("code_challenge_method", "S256"); - - if (state != null) { - builder.queryParam("state", state); - } - - return builder.toUriString(); - } - - private String buildRedirectUri(String path) { - return UriComponentsBuilder.fromUriString(oAuthProperties.getBaseUrl()) - .path(path) - .build() - .toUriString(); - } -} diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinkResponse.java b/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinkResponse.java deleted file mode 100644 index 1c7f250..0000000 --- a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinkResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package flipnote.user.auth.presentation.dto.response; - -import com.fasterxml.jackson.annotation.JsonFormat; -import flipnote.user.user.domain.OAuthLink; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.time.LocalDateTime; - -@Getter -@AllArgsConstructor -public class SocialLinkResponse { - - private Long socialLinkId; - private String provider; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private LocalDateTime linkedAt; - - public static SocialLinkResponse from(OAuthLink link) { - return new SocialLinkResponse(link.getId(), link.getProvider(), link.getLinkedAt()); - } -} diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinksResponse.java b/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinksResponse.java deleted file mode 100644 index d5f3bbd..0000000 --- a/src/main/java/flipnote/user/auth/presentation/dto/response/SocialLinksResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package flipnote.user.auth.presentation.dto.response; - -import flipnote.user.user.domain.OAuthLink; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.List; - -@Getter -@AllArgsConstructor -public class SocialLinksResponse { - - private List socialLinks; - - public static SocialLinksResponse from(List links) { - List socialLinks = links.stream() - .map(SocialLinkResponse::from) - .toList(); - return new SocialLinksResponse(socialLinks); - } -} diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/TokenValidateResponse.java b/src/main/java/flipnote/user/auth/presentation/dto/response/TokenValidateResponse.java deleted file mode 100644 index 6b799b5..0000000 --- a/src/main/java/flipnote/user/auth/presentation/dto/response/TokenValidateResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package flipnote.user.auth.presentation.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class TokenValidateResponse { - - private Long userId; - private String email; - private String role; -} diff --git a/src/main/java/flipnote/user/auth/presentation/dto/response/UserResponse.java b/src/main/java/flipnote/user/auth/presentation/dto/response/UserResponse.java deleted file mode 100644 index 66a9541..0000000 --- a/src/main/java/flipnote/user/auth/presentation/dto/response/UserResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package flipnote.user.auth.presentation.dto.response; - -import flipnote.user.user.domain.User; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class UserResponse { - - private Long userId; - - public static UserResponse from(User user) { - return new UserResponse(user.getId()); - } -} diff --git a/src/main/java/flipnote/user/domain/AuthErrorCode.java b/src/main/java/flipnote/user/domain/AuthErrorCode.java new file mode 100644 index 0000000..479b460 --- /dev/null +++ b/src/main/java/flipnote/user/domain/AuthErrorCode.java @@ -0,0 +1,39 @@ +package flipnote.user.domain; + +import org.springframework.http.HttpStatus; + +import flipnote.user.domain.common.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AuthErrorCode implements ErrorCode { + + INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH_001", "이메일 λ˜λŠ” λΉ„λ°€λ²ˆν˜Έκ°€ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), + ALREADY_ISSUED_VERIFICATION_CODE(HttpStatus.TOO_MANY_REQUESTS, "AUTH_002", "이미 μΈμ¦μ½”λ“œκ°€ λ°œμ†‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”."), + NOT_ISSUED_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_003", "μΈμ¦μ½”λ“œκ°€ λ°œμ†‘λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."), + INVALID_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "AUTH_004", "μΈμ¦μ½”λ“œκ°€ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), + EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "AUTH_005", "이미 μ‚¬μš© 쀑인 μ΄λ©”μΌμž…λ‹ˆλ‹€."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_006", "μœ νš¨ν•˜μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€."), + UNVERIFIED_EMAIL(HttpStatus.FORBIDDEN, "AUTH_007", "이메일 인증이 μ™„λ£Œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."), + ALREADY_SENT_PASSWORD_RESET_LINK(HttpStatus.TOO_MANY_REQUESTS, "AUTH_008", "이미 λΉ„λ°€λ²ˆν˜Έ μž¬μ„€μ • 링크가 λ°œμ†‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”."), + INVALID_PASSWORD_RESET_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_009", "μœ νš¨ν•˜μ§€ μ•Šμ€ λΉ„λ°€λ²ˆν˜Έ μž¬μ„€μ • ν† ν°μž…λ‹ˆλ‹€."), + INVALID_SOCIAL_LINK_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_010", "μœ νš¨ν•˜μ§€ μ•Šμ€ μ†Œμ…œ 연동 ν† ν°μž…λ‹ˆλ‹€."), + ALREADY_LINKED_SOCIAL_ACCOUNT(HttpStatus.CONFLICT, "AUTH_011", "이미 μ—°κ²°λœ μ†Œμ…œ κ³„μ •μž…λ‹ˆλ‹€."), + NOT_REGISTERED_SOCIAL_ACCOUNT(HttpStatus.NOT_FOUND, "AUTH_012", "μ—°κ²°λœ μ†Œμ…œ 계정이 μ—†μŠ΅λ‹ˆλ‹€."), + INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH_013", "μ§€μ›ν•˜μ§€ μ•ŠλŠ” OAuth μ œκ³΅μžμž…λ‹ˆλ‹€."), + OAUTH_COMMUNICATION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_014", "OAuth μ„œλ²„μ™€μ˜ 톡신 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."), + BLACKLISTED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_015", "λ¬΄νš¨ν™”λœ ν† ν°μž…λ‹ˆλ‹€."), + INVALIDATED_SESSION(HttpStatus.UNAUTHORIZED, "AUTH_016", "μ„Έμ…˜μ΄ λ¬΄νš¨ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄ μ£Όμ„Έμš”."), + PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH_017", "ν˜„μž¬ λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public int getStatus() { + return httpStatus.value(); + } +} diff --git a/src/main/java/flipnote/user/global/error/ImageErrorCode.java b/src/main/java/flipnote/user/domain/ImageErrorCode.java similarity index 86% rename from src/main/java/flipnote/user/global/error/ImageErrorCode.java rename to src/main/java/flipnote/user/domain/ImageErrorCode.java index 16fc7b1..57670dd 100644 --- a/src/main/java/flipnote/user/global/error/ImageErrorCode.java +++ b/src/main/java/flipnote/user/domain/ImageErrorCode.java @@ -1,8 +1,10 @@ -package flipnote.user.global.error; +package flipnote.user.domain; +import org.springframework.http.HttpStatus; + +import flipnote.user.domain.common.ErrorCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor diff --git a/src/main/java/flipnote/user/auth/domain/PasswordResetConstants.java b/src/main/java/flipnote/user/domain/PasswordResetConstants.java similarity index 85% rename from src/main/java/flipnote/user/auth/domain/PasswordResetConstants.java rename to src/main/java/flipnote/user/domain/PasswordResetConstants.java index 94878de..9767a83 100644 --- a/src/main/java/flipnote/user/auth/domain/PasswordResetConstants.java +++ b/src/main/java/flipnote/user/domain/PasswordResetConstants.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain; +package flipnote.user.domain; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/src/main/java/flipnote/user/auth/domain/TokenClaims.java b/src/main/java/flipnote/user/domain/TokenClaims.java similarity index 73% rename from src/main/java/flipnote/user/auth/domain/TokenClaims.java rename to src/main/java/flipnote/user/domain/TokenClaims.java index 52bc265..44a252a 100644 --- a/src/main/java/flipnote/user/auth/domain/TokenClaims.java +++ b/src/main/java/flipnote/user/domain/TokenClaims.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain; +package flipnote.user.domain; public record TokenClaims( Long userId, diff --git a/src/main/java/flipnote/user/auth/domain/TokenPair.java b/src/main/java/flipnote/user/domain/TokenPair.java similarity index 66% rename from src/main/java/flipnote/user/auth/domain/TokenPair.java rename to src/main/java/flipnote/user/domain/TokenPair.java index 81cb9d4..48ec06a 100644 --- a/src/main/java/flipnote/user/auth/domain/TokenPair.java +++ b/src/main/java/flipnote/user/domain/TokenPair.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain; +package flipnote.user.domain; public record TokenPair(String accessToken, String refreshToken) { } diff --git a/src/main/java/flipnote/user/user/domain/UserErrorCode.java b/src/main/java/flipnote/user/domain/UserErrorCode.java similarity index 85% rename from src/main/java/flipnote/user/user/domain/UserErrorCode.java rename to src/main/java/flipnote/user/domain/UserErrorCode.java index d98aa0c..dd4a612 100644 --- a/src/main/java/flipnote/user/user/domain/UserErrorCode.java +++ b/src/main/java/flipnote/user/domain/UserErrorCode.java @@ -1,9 +1,10 @@ -package flipnote.user.user.domain; +package flipnote.user.domain; -import flipnote.user.global.error.ErrorCode; +import org.springframework.http.HttpStatus; + +import flipnote.user.domain.common.ErrorCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor diff --git a/src/main/java/flipnote/user/auth/domain/VerificationConstants.java b/src/main/java/flipnote/user/domain/VerificationConstants.java similarity index 70% rename from src/main/java/flipnote/user/auth/domain/VerificationConstants.java rename to src/main/java/flipnote/user/domain/VerificationConstants.java index d92bc97..cc8d5d3 100644 --- a/src/main/java/flipnote/user/auth/domain/VerificationConstants.java +++ b/src/main/java/flipnote/user/domain/VerificationConstants.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain; +package flipnote.user.domain; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -7,4 +7,5 @@ public final class VerificationConstants { public static final int CODE_TTL_MINUTES = 5; + public static final int VERIFIED_TTL_MINUTES = 10; } diff --git a/src/main/java/flipnote/user/global/exception/BizException.java b/src/main/java/flipnote/user/domain/common/BizException.java similarity index 66% rename from src/main/java/flipnote/user/global/exception/BizException.java rename to src/main/java/flipnote/user/domain/common/BizException.java index f31adef..cfed271 100644 --- a/src/main/java/flipnote/user/global/exception/BizException.java +++ b/src/main/java/flipnote/user/domain/common/BizException.java @@ -1,6 +1,6 @@ -package flipnote.user.global.exception; +package flipnote.user.domain.common; -import flipnote.user.global.error.ErrorCode; +import flipnote.user.domain.common.ErrorCode; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/flipnote/user/global/exception/EmailSendException.java b/src/main/java/flipnote/user/domain/common/EmailSendException.java similarity index 77% rename from src/main/java/flipnote/user/global/exception/EmailSendException.java rename to src/main/java/flipnote/user/domain/common/EmailSendException.java index e8a11b5..dfe342f 100644 --- a/src/main/java/flipnote/user/global/exception/EmailSendException.java +++ b/src/main/java/flipnote/user/domain/common/EmailSendException.java @@ -1,4 +1,4 @@ -package flipnote.user.global.exception; +package flipnote.user.domain.common; public class EmailSendException extends RuntimeException { diff --git a/src/main/java/flipnote/user/global/error/ErrorCode.java b/src/main/java/flipnote/user/domain/common/ErrorCode.java similarity index 73% rename from src/main/java/flipnote/user/global/error/ErrorCode.java rename to src/main/java/flipnote/user/domain/common/ErrorCode.java index d043924..a12fbc6 100644 --- a/src/main/java/flipnote/user/global/error/ErrorCode.java +++ b/src/main/java/flipnote/user/domain/common/ErrorCode.java @@ -1,4 +1,4 @@ -package flipnote.user.global.error; +package flipnote.user.domain.common; public interface ErrorCode { diff --git a/src/main/java/flipnote/user/global/entity/BaseEntity.java b/src/main/java/flipnote/user/domain/entity/BaseEntity.java similarity index 94% rename from src/main/java/flipnote/user/global/entity/BaseEntity.java rename to src/main/java/flipnote/user/domain/entity/BaseEntity.java index 61b9181..1c1ca7e 100644 --- a/src/main/java/flipnote/user/global/entity/BaseEntity.java +++ b/src/main/java/flipnote/user/domain/entity/BaseEntity.java @@ -1,4 +1,4 @@ -package flipnote.user.global.entity; +package flipnote.user.domain.entity; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; diff --git a/src/main/java/flipnote/user/user/domain/OAuthLink.java b/src/main/java/flipnote/user/domain/entity/OAuthLink.java similarity index 96% rename from src/main/java/flipnote/user/user/domain/OAuthLink.java rename to src/main/java/flipnote/user/domain/entity/OAuthLink.java index 893b542..07abe1c 100644 --- a/src/main/java/flipnote/user/user/domain/OAuthLink.java +++ b/src/main/java/flipnote/user/domain/entity/OAuthLink.java @@ -1,4 +1,4 @@ -package flipnote.user.user.domain; +package flipnote.user.domain.entity; import jakarta.persistence.*; import lombok.AccessLevel; diff --git a/src/main/java/flipnote/user/user/domain/User.java b/src/main/java/flipnote/user/domain/entity/User.java similarity index 96% rename from src/main/java/flipnote/user/user/domain/User.java rename to src/main/java/flipnote/user/domain/entity/User.java index d09f98e..01546d7 100644 --- a/src/main/java/flipnote/user/user/domain/User.java +++ b/src/main/java/flipnote/user/domain/entity/User.java @@ -1,6 +1,5 @@ -package flipnote.user.user.domain; +package flipnote.user.domain.entity; -import flipnote.user.global.entity.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/flipnote/user/auth/domain/event/EmailVerificationSendEvent.java b/src/main/java/flipnote/user/domain/event/EmailVerificationSendEvent.java similarity index 68% rename from src/main/java/flipnote/user/auth/domain/event/EmailVerificationSendEvent.java rename to src/main/java/flipnote/user/domain/event/EmailVerificationSendEvent.java index 0373988..399867a 100644 --- a/src/main/java/flipnote/user/auth/domain/event/EmailVerificationSendEvent.java +++ b/src/main/java/flipnote/user/domain/event/EmailVerificationSendEvent.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain.event; +package flipnote.user.domain.event; public record EmailVerificationSendEvent( String to, diff --git a/src/main/java/flipnote/user/auth/domain/event/PasswordResetCreateEvent.java b/src/main/java/flipnote/user/domain/event/PasswordResetCreateEvent.java similarity index 67% rename from src/main/java/flipnote/user/auth/domain/event/PasswordResetCreateEvent.java rename to src/main/java/flipnote/user/domain/event/PasswordResetCreateEvent.java index b1c6daf..d0c415a 100644 --- a/src/main/java/flipnote/user/auth/domain/event/PasswordResetCreateEvent.java +++ b/src/main/java/flipnote/user/domain/event/PasswordResetCreateEvent.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.domain.event; +package flipnote.user.domain.event; public record PasswordResetCreateEvent( String to, diff --git a/src/main/java/flipnote/user/user/domain/OAuthLinkRepository.java b/src/main/java/flipnote/user/domain/repository/OAuthLinkRepository.java similarity index 81% rename from src/main/java/flipnote/user/user/domain/OAuthLinkRepository.java rename to src/main/java/flipnote/user/domain/repository/OAuthLinkRepository.java index fbc55c8..1286797 100644 --- a/src/main/java/flipnote/user/user/domain/OAuthLinkRepository.java +++ b/src/main/java/flipnote/user/domain/repository/OAuthLinkRepository.java @@ -1,4 +1,4 @@ -package flipnote.user.user.domain; +package flipnote.user.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -7,6 +7,8 @@ import java.util.List; import java.util.Optional; +import flipnote.user.domain.entity.OAuthLink; + public interface OAuthLinkRepository extends JpaRepository { @Query(""" @@ -19,7 +21,7 @@ Optional findByProviderAndProviderIdWithUser( @Param("providerId") String providerId ); - boolean existsByUser_IdAndProviderAndProviderId(Long userId, String provider, String providerId); + boolean existsByProviderAndProviderId(String provider, String providerId); boolean existsByIdAndUser_Id(Long id, Long userId); diff --git a/src/main/java/flipnote/user/user/domain/UserRepository.java b/src/main/java/flipnote/user/domain/repository/UserRepository.java similarity index 85% rename from src/main/java/flipnote/user/user/domain/UserRepository.java rename to src/main/java/flipnote/user/domain/repository/UserRepository.java index e59c10d..384e675 100644 --- a/src/main/java/flipnote/user/user/domain/UserRepository.java +++ b/src/main/java/flipnote/user/domain/repository/UserRepository.java @@ -1,10 +1,12 @@ -package flipnote.user.user.domain; +package flipnote.user.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; import java.util.Optional; +import flipnote.user.domain.entity.User; + public interface UserRepository extends JpaRepository { Optional findByEmail(String email); diff --git a/src/main/java/flipnote/user/global/config/OAuthProperties.java b/src/main/java/flipnote/user/global/config/OAuthProperties.java deleted file mode 100644 index 8024ce6..0000000 --- a/src/main/java/flipnote/user/global/config/OAuthProperties.java +++ /dev/null @@ -1,30 +0,0 @@ -package flipnote.user.global.config; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.List; -import java.util.Map; - -@Getter -@RequiredArgsConstructor -@ConfigurationProperties(prefix = "app.oauth2") -public class OAuthProperties { - - private final String baseUrl; - private final Map providers; - - @Getter - @Setter - public static class Provider { - private String clientId; - private String clientSecret; - private String redirectUri; - private String authorizationUri; - private String tokenUri; - private String userInfoUri; - private List scope; - } -} diff --git a/src/main/java/flipnote/user/global/config/AppConfig.java b/src/main/java/flipnote/user/infrastructure/config/AppConfig.java similarity index 95% rename from src/main/java/flipnote/user/global/config/AppConfig.java rename to src/main/java/flipnote/user/infrastructure/config/AppConfig.java index c8b5cfe..90b7f0d 100644 --- a/src/main/java/flipnote/user/global/config/AppConfig.java +++ b/src/main/java/flipnote/user/infrastructure/config/AppConfig.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/flipnote/user/global/config/ClientProperties.java b/src/main/java/flipnote/user/infrastructure/config/ClientProperties.java similarity index 93% rename from src/main/java/flipnote/user/global/config/ClientProperties.java rename to src/main/java/flipnote/user/infrastructure/config/ClientProperties.java index 5334472..3edf7d5 100644 --- a/src/main/java/flipnote/user/global/config/ClientProperties.java +++ b/src/main/java/flipnote/user/infrastructure/config/ClientProperties.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.config; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/flipnote/user/global/config/GrpcClientConfig.java b/src/main/java/flipnote/user/infrastructure/config/GrpcClientConfig.java similarity index 92% rename from src/main/java/flipnote/user/global/config/GrpcClientConfig.java rename to src/main/java/flipnote/user/infrastructure/config/GrpcClientConfig.java index 2b5eed3..c7bc08d 100644 --- a/src/main/java/flipnote/user/global/config/GrpcClientConfig.java +++ b/src/main/java/flipnote/user/infrastructure/config/GrpcClientConfig.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.config; import flipnote.image.grpc.v1.ImageCommandServiceGrpc; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/flipnote/user/global/config/JpaAuditingConfig.java b/src/main/java/flipnote/user/infrastructure/config/JpaAuditingConfig.java similarity index 82% rename from src/main/java/flipnote/user/global/config/JpaAuditingConfig.java rename to src/main/java/flipnote/user/infrastructure/config/JpaAuditingConfig.java index 6e3060c..d55f976 100644 --- a/src/main/java/flipnote/user/global/config/JpaAuditingConfig.java +++ b/src/main/java/flipnote/user/infrastructure/config/JpaAuditingConfig.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.config; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; diff --git a/src/main/java/flipnote/user/global/config/SwaggerConfig.java b/src/main/java/flipnote/user/infrastructure/config/SwaggerConfig.java similarity index 92% rename from src/main/java/flipnote/user/global/config/SwaggerConfig.java rename to src/main/java/flipnote/user/infrastructure/config/SwaggerConfig.java index 186cc42..4bf33fe 100644 --- a/src/main/java/flipnote/user/global/config/SwaggerConfig.java +++ b/src/main/java/flipnote/user/infrastructure/config/SwaggerConfig.java @@ -1,6 +1,6 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.config; -import flipnote.user.global.constants.HttpConstants; +import flipnote.user.interfaces.http.common.HttpConstants; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; diff --git a/src/main/java/flipnote/user/global/config/JwtProperties.java b/src/main/java/flipnote/user/infrastructure/jwt/JwtProperties.java similarity index 89% rename from src/main/java/flipnote/user/global/config/JwtProperties.java rename to src/main/java/flipnote/user/infrastructure/jwt/JwtProperties.java index 6feaa72..a7d6f34 100644 --- a/src/main/java/flipnote/user/global/config/JwtProperties.java +++ b/src/main/java/flipnote/user/infrastructure/jwt/JwtProperties.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.jwt; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/flipnote/user/auth/infrastructure/jwt/JwtProvider.java b/src/main/java/flipnote/user/infrastructure/jwt/JwtProvider.java similarity index 93% rename from src/main/java/flipnote/user/auth/infrastructure/jwt/JwtProvider.java rename to src/main/java/flipnote/user/infrastructure/jwt/JwtProvider.java index 9cd7a6e..393d530 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/jwt/JwtProvider.java +++ b/src/main/java/flipnote/user/infrastructure/jwt/JwtProvider.java @@ -1,9 +1,8 @@ -package flipnote.user.auth.infrastructure.jwt; +package flipnote.user.infrastructure.jwt; -import flipnote.user.auth.domain.TokenClaims; -import flipnote.user.auth.domain.TokenPair; -import flipnote.user.user.domain.User; -import flipnote.user.global.config.JwtProperties; +import flipnote.user.domain.TokenClaims; +import flipnote.user.domain.TokenPair; +import flipnote.user.domain.entity.User; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; diff --git a/src/main/java/flipnote/user/auth/infrastructure/listener/EmailVerificationEventListener.java b/src/main/java/flipnote/user/infrastructure/listener/EmailVerificationEventListener.java similarity index 70% rename from src/main/java/flipnote/user/auth/infrastructure/listener/EmailVerificationEventListener.java rename to src/main/java/flipnote/user/infrastructure/listener/EmailVerificationEventListener.java index d16fdd6..4dcbefe 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/listener/EmailVerificationEventListener.java +++ b/src/main/java/flipnote/user/infrastructure/listener/EmailVerificationEventListener.java @@ -1,14 +1,14 @@ -package flipnote.user.auth.infrastructure.listener; +package flipnote.user.infrastructure.listener; import org.springframework.context.event.EventListener; import org.springframework.resilience.annotation.Retryable; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import flipnote.user.auth.domain.VerificationConstants; -import flipnote.user.auth.domain.event.EmailVerificationSendEvent; -import flipnote.user.auth.infrastructure.mail.MailService; -import flipnote.user.global.exception.EmailSendException; +import flipnote.user.domain.VerificationConstants; +import flipnote.user.domain.common.EmailSendException; +import flipnote.user.domain.event.EmailVerificationSendEvent; +import flipnote.user.infrastructure.mail.MailService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/flipnote/user/auth/infrastructure/listener/PasswordResetEventListener.java b/src/main/java/flipnote/user/infrastructure/listener/PasswordResetEventListener.java similarity index 70% rename from src/main/java/flipnote/user/auth/infrastructure/listener/PasswordResetEventListener.java rename to src/main/java/flipnote/user/infrastructure/listener/PasswordResetEventListener.java index 2518e79..4257c89 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/listener/PasswordResetEventListener.java +++ b/src/main/java/flipnote/user/infrastructure/listener/PasswordResetEventListener.java @@ -1,14 +1,14 @@ -package flipnote.user.auth.infrastructure.listener; +package flipnote.user.infrastructure.listener; import org.springframework.context.event.EventListener; import org.springframework.resilience.annotation.Retryable; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import flipnote.user.auth.domain.PasswordResetConstants; -import flipnote.user.auth.domain.event.PasswordResetCreateEvent; -import flipnote.user.auth.infrastructure.mail.MailService; -import flipnote.user.global.exception.EmailSendException; +import flipnote.user.domain.PasswordResetConstants; +import flipnote.user.domain.common.EmailSendException; +import flipnote.user.domain.event.PasswordResetCreateEvent; +import flipnote.user.infrastructure.mail.MailService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/flipnote/user/auth/infrastructure/mail/MailService.java b/src/main/java/flipnote/user/infrastructure/mail/MailService.java similarity index 77% rename from src/main/java/flipnote/user/auth/infrastructure/mail/MailService.java rename to src/main/java/flipnote/user/infrastructure/mail/MailService.java index da8c47c..15df452 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/mail/MailService.java +++ b/src/main/java/flipnote/user/infrastructure/mail/MailService.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.mail; +package flipnote.user.infrastructure.mail; public interface MailService { diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetTokenGenerator.java b/src/main/java/flipnote/user/infrastructure/mail/PasswordResetTokenGenerator.java similarity index 81% rename from src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetTokenGenerator.java rename to src/main/java/flipnote/user/infrastructure/mail/PasswordResetTokenGenerator.java index 1e8797f..53074b5 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetTokenGenerator.java +++ b/src/main/java/flipnote/user/infrastructure/mail/PasswordResetTokenGenerator.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.mail; import org.springframework.stereotype.Component; diff --git a/src/main/java/flipnote/user/global/config/ResendConfig.java b/src/main/java/flipnote/user/infrastructure/mail/ResendConfig.java similarity index 90% rename from src/main/java/flipnote/user/global/config/ResendConfig.java rename to src/main/java/flipnote/user/infrastructure/mail/ResendConfig.java index ed0b74e..c52409f 100644 --- a/src/main/java/flipnote/user/global/config/ResendConfig.java +++ b/src/main/java/flipnote/user/infrastructure/mail/ResendConfig.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.mail; import com.resend.Resend; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/flipnote/user/auth/infrastructure/mail/ResendMailService.java b/src/main/java/flipnote/user/infrastructure/mail/ResendMailService.java similarity index 93% rename from src/main/java/flipnote/user/auth/infrastructure/mail/ResendMailService.java rename to src/main/java/flipnote/user/infrastructure/mail/ResendMailService.java index 4e57af1..4d44bc6 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/mail/ResendMailService.java +++ b/src/main/java/flipnote/user/infrastructure/mail/ResendMailService.java @@ -1,10 +1,10 @@ -package flipnote.user.auth.infrastructure.mail; +package flipnote.user.infrastructure.mail; import com.resend.Resend; import com.resend.core.exception.ResendException; import com.resend.services.emails.model.CreateEmailOptions; -import flipnote.user.global.config.ResendProperties; -import flipnote.user.global.exception.EmailSendException; +import flipnote.user.domain.common.EmailSendException; +import flipnote.user.infrastructure.mail.ResendProperties; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/src/main/java/flipnote/user/global/config/ResendProperties.java b/src/main/java/flipnote/user/infrastructure/mail/ResendProperties.java similarity index 91% rename from src/main/java/flipnote/user/global/config/ResendProperties.java rename to src/main/java/flipnote/user/infrastructure/mail/ResendProperties.java index b1f28c6..7ab8b09 100644 --- a/src/main/java/flipnote/user/global/config/ResendProperties.java +++ b/src/main/java/flipnote/user/infrastructure/mail/ResendProperties.java @@ -1,4 +1,4 @@ -package flipnote.user.global.config; +package flipnote.user.infrastructure.mail; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/VerificationCodeGenerator.java b/src/main/java/flipnote/user/infrastructure/mail/VerificationCodeGenerator.java similarity index 87% rename from src/main/java/flipnote/user/auth/infrastructure/redis/VerificationCodeGenerator.java rename to src/main/java/flipnote/user/infrastructure/mail/VerificationCodeGenerator.java index 301156d..bc61620 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/VerificationCodeGenerator.java +++ b/src/main/java/flipnote/user/infrastructure/mail/VerificationCodeGenerator.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.mail; import org.springframework.stereotype.Component; diff --git a/src/main/java/flipnote/user/auth/infrastructure/oauth/GoogleUserInfo.java b/src/main/java/flipnote/user/infrastructure/oauth/GoogleUserInfo.java similarity index 92% rename from src/main/java/flipnote/user/auth/infrastructure/oauth/GoogleUserInfo.java rename to src/main/java/flipnote/user/infrastructure/oauth/GoogleUserInfo.java index b9e5b01..a1e282e 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/oauth/GoogleUserInfo.java +++ b/src/main/java/flipnote/user/infrastructure/oauth/GoogleUserInfo.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.oauth; +package flipnote.user.infrastructure.oauth; import java.util.Map; diff --git a/src/main/java/flipnote/user/auth/infrastructure/oauth/OAuth2UserInfo.java b/src/main/java/flipnote/user/infrastructure/oauth/OAuth2UserInfo.java similarity index 74% rename from src/main/java/flipnote/user/auth/infrastructure/oauth/OAuth2UserInfo.java rename to src/main/java/flipnote/user/infrastructure/oauth/OAuth2UserInfo.java index 5a394a5..339581e 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/oauth/OAuth2UserInfo.java +++ b/src/main/java/flipnote/user/infrastructure/oauth/OAuth2UserInfo.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.oauth; +package flipnote.user.infrastructure.oauth; public interface OAuth2UserInfo { diff --git a/src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java b/src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java new file mode 100644 index 0000000..a3f8c28 --- /dev/null +++ b/src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java @@ -0,0 +1,101 @@ +package flipnote.user.infrastructure.oauth; + +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; + +import flipnote.user.domain.AuthErrorCode; +import flipnote.user.domain.common.BizException; +import lombok.RequiredArgsConstructor; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; + +@Service +@RequiredArgsConstructor +public class OAuthApiClient { + + private final RestClient restClient; + private final ObjectMapper objectMapper; + private final OAuthProperties oAuthProperties; + + public String requestAccessToken( + OAuthProperties.Provider provider, + String code, + String codeVerifier + ) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", provider.getClientId()); + params.add("client_secret", provider.getClientSecret()); + params.add("redirect_uri", buildRedirectUri(provider.getRedirectUri())); + params.add("code", code); + params.add("code_verifier", codeVerifier); + + try { + String responseBody = restClient.post() + .uri(provider.getTokenUri()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(params) + .retrieve() + .body(String.class); + + Map responseMap = objectMapper.readValue(responseBody, new TypeReference<>() { + }); + return (String)responseMap.get("access_token"); + } catch (Exception e) { + throw new BizException(AuthErrorCode.OAUTH_COMMUNICATION_ERROR); + } + } + + public Map requestUserInfo(OAuthProperties.Provider provider, String accessToken) { + try { + String responseBody = restClient.get() + .uri(provider.getUserInfoUri()) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .retrieve() + .body(String.class); + + return objectMapper.readValue(responseBody, new TypeReference<>() { + }); + } catch (Exception e) { + throw new BizException(AuthErrorCode.OAUTH_COMMUNICATION_ERROR); + } + } + + public OAuth2UserInfo createUserInfo(String providerName, Map attributes) { + return switch (providerName.toLowerCase()) { + case "google" -> new GoogleUserInfo(attributes); + default -> throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); + }; + } + + public String buildAuthorizeUri(OAuthProperties.Provider provider, + String codeChallenge, String state) { + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(provider.getAuthorizationUri()) + .queryParam("client_id", provider.getClientId()) + .queryParam("redirect_uri", buildRedirectUri(provider.getRedirectUri())) + .queryParam("response_type", "code") + .queryParam("scope", String.join(" ", provider.getScope())) + .queryParam("code_challenge", codeChallenge) + .queryParam("code_challenge_method", "S256"); + + if (state != null) { + builder.queryParam("state", state); + } + + return builder.toUriString(); + } + + private String buildRedirectUri(String path) { + return UriComponentsBuilder.fromUriString(oAuthProperties.getBaseUrl()) + .replacePath(path) + .build() + .toUriString(); + } +} diff --git a/src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java b/src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java new file mode 100644 index 0000000..5b0260c --- /dev/null +++ b/src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java @@ -0,0 +1,46 @@ +package flipnote.user.infrastructure.oauth; + +import java.util.List; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Validated +@Getter +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "app.oauth2") +public class OAuthProperties { + + @NotBlank + private final String baseUrl; + + @Valid + private final Map providers; + + @Getter + @Setter + public static class Provider { + @NotBlank + private String clientId; + @NotBlank + private String clientSecret; + @NotBlank + private String redirectUri; + @NotBlank + private String authorizationUri; + @NotBlank + private String tokenUri; + @NotBlank + private String userInfoUri; + @NotEmpty + private List scope; + } +} diff --git a/src/main/java/flipnote/user/auth/infrastructure/oauth/PkceUtil.java b/src/main/java/flipnote/user/infrastructure/oauth/PkceUtil.java similarity index 95% rename from src/main/java/flipnote/user/auth/infrastructure/oauth/PkceUtil.java rename to src/main/java/flipnote/user/infrastructure/oauth/PkceUtil.java index 544266c..cf4fe6e 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/oauth/PkceUtil.java +++ b/src/main/java/flipnote/user/infrastructure/oauth/PkceUtil.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.oauth; +package flipnote.user.infrastructure.oauth; import org.springframework.stereotype.Component; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/EmailVerificationRepository.java b/src/main/java/flipnote/user/infrastructure/redis/EmailVerificationRepository.java similarity index 86% rename from src/main/java/flipnote/user/auth/infrastructure/redis/EmailVerificationRepository.java rename to src/main/java/flipnote/user/infrastructure/redis/EmailVerificationRepository.java index 6218947..e3cf08c 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/EmailVerificationRepository.java +++ b/src/main/java/flipnote/user/infrastructure/redis/EmailVerificationRepository.java @@ -1,5 +1,6 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; +import flipnote.user.domain.VerificationConstants; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; @@ -12,8 +13,6 @@ public class EmailVerificationRepository { private static final String CODE_KEY_PREFIX = "email:verification:code:"; private static final String VERIFIED_KEY_PREFIX = "email:verification:verified:"; - private static final long CODE_TTL_MINUTES = 5; - private static final long VERIFIED_TTL_MINUTES = 10; private final StringRedisTemplate redisTemplate; @@ -21,7 +20,7 @@ public void saveCode(String email, String code) { redisTemplate.opsForValue().set( CODE_KEY_PREFIX + email, code, - CODE_TTL_MINUTES, + VerificationConstants.CODE_TTL_MINUTES, TimeUnit.MINUTES ); } @@ -42,7 +41,7 @@ public void markVerified(String email) { redisTemplate.opsForValue().set( VERIFIED_KEY_PREFIX + email, "verified", - VERIFIED_TTL_MINUTES, + VerificationConstants.VERIFIED_TTL_MINUTES, TimeUnit.MINUTES ); } diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetRepository.java b/src/main/java/flipnote/user/infrastructure/redis/PasswordResetRepository.java similarity index 85% rename from src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetRepository.java rename to src/main/java/flipnote/user/infrastructure/redis/PasswordResetRepository.java index c821d5d..0d636f7 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/PasswordResetRepository.java +++ b/src/main/java/flipnote/user/infrastructure/redis/PasswordResetRepository.java @@ -1,5 +1,6 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; +import flipnote.user.domain.PasswordResetConstants; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; @@ -12,7 +13,6 @@ public class PasswordResetRepository { private static final String TOKEN_KEY_PREFIX = "password:reset:token:"; private static final String EMAIL_KEY_PREFIX = "password:reset:email:"; - private static final long TTL_MINUTES = 30; private final StringRedisTemplate redisTemplate; @@ -24,13 +24,13 @@ public void save(String token, String email) { redisTemplate.opsForValue().set( TOKEN_KEY_PREFIX + token, email, - TTL_MINUTES, + PasswordResetConstants.TOKEN_TTL_MINUTES, TimeUnit.MINUTES ); redisTemplate.opsForValue().set( EMAIL_KEY_PREFIX + email, token, - TTL_MINUTES, + PasswordResetConstants.TOKEN_TTL_MINUTES, TimeUnit.MINUTES ); } diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/SessionInvalidationRepository.java b/src/main/java/flipnote/user/infrastructure/redis/SessionInvalidationRepository.java similarity index 95% rename from src/main/java/flipnote/user/auth/infrastructure/redis/SessionInvalidationRepository.java rename to src/main/java/flipnote/user/infrastructure/redis/SessionInvalidationRepository.java index e494496..887147e 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/SessionInvalidationRepository.java +++ b/src/main/java/flipnote/user/infrastructure/redis/SessionInvalidationRepository.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/SocialLinkTokenRepository.java b/src/main/java/flipnote/user/infrastructure/redis/SocialLinkTokenRepository.java similarity index 95% rename from src/main/java/flipnote/user/auth/infrastructure/redis/SocialLinkTokenRepository.java rename to src/main/java/flipnote/user/infrastructure/redis/SocialLinkTokenRepository.java index 1c4222b..4fb47cd 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/SocialLinkTokenRepository.java +++ b/src/main/java/flipnote/user/infrastructure/redis/SocialLinkTokenRepository.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; diff --git a/src/main/java/flipnote/user/auth/infrastructure/redis/TokenBlacklistRepository.java b/src/main/java/flipnote/user/infrastructure/redis/TokenBlacklistRepository.java similarity index 94% rename from src/main/java/flipnote/user/auth/infrastructure/redis/TokenBlacklistRepository.java rename to src/main/java/flipnote/user/infrastructure/redis/TokenBlacklistRepository.java index 9a1d899..210f8ab 100644 --- a/src/main/java/flipnote/user/auth/infrastructure/redis/TokenBlacklistRepository.java +++ b/src/main/java/flipnote/user/infrastructure/redis/TokenBlacklistRepository.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.infrastructure.redis; +package flipnote.user.infrastructure.redis; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; diff --git a/src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java b/src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java new file mode 100644 index 0000000..fc6c241 --- /dev/null +++ b/src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java @@ -0,0 +1,51 @@ +package flipnote.user.interfaces.grpc; + +import flipnote.user.domain.common.BizException; +import flipnote.user.domain.common.ErrorCode; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class GrpcExceptionHandlerImpl implements GrpcExceptionHandler { + + @Override + public StatusException handleException(Throwable t) { + if (t instanceof BizException e) { + ErrorCode errorCode = e.getErrorCode(); + log.warn("gRPC BizException: code={}, status={}, message={}", + errorCode.getCode(), errorCode.getStatus(), errorCode.getMessage()); + return toGrpcStatus(errorCode) + .withDescription(errorCode.getMessage()) + .asException(); + } + if (t instanceof StatusException e) { + log.warn("gRPC StatusException: status={}, description={}", + e.getStatus().getCode(), e.getStatus().getDescription()); + return e; + } + if (t instanceof StatusRuntimeException e) { + log.warn("gRPC StatusRuntimeException: status={}, description={}", + e.getStatus().getCode(), e.getStatus().getDescription()); + return e.getStatus().asException(e.getTrailers()); + } + log.error("gRPC Unhandled exception", t); + return Status.INTERNAL.withDescription("Internal server error").asException(); + } + + private Status toGrpcStatus(ErrorCode errorCode) { + return switch (errorCode.getStatus()) { + case 400 -> Status.INVALID_ARGUMENT; + case 401 -> Status.UNAUTHENTICATED; + case 403 -> Status.PERMISSION_DENIED; + case 404 -> Status.NOT_FOUND; + case 409 -> Status.ALREADY_EXISTS; + case 429 -> Status.RESOURCE_EXHAUSTED; + default -> Status.INTERNAL; + }; + } +} diff --git a/src/main/java/flipnote/user/interfaces/grpc/GrpcUserQueryService.java b/src/main/java/flipnote/user/interfaces/grpc/GrpcUserQueryService.java new file mode 100644 index 0000000..20bfcf9 --- /dev/null +++ b/src/main/java/flipnote/user/interfaces/grpc/GrpcUserQueryService.java @@ -0,0 +1,93 @@ +package flipnote.user.interfaces.grpc; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import flipnote.user.application.UserService; +import flipnote.user.application.result.UserResult; +import flipnote.user.grpc.GetUserByEmailRequest; +import flipnote.user.grpc.GetUserByEmailResponse; +import flipnote.user.grpc.GetUserByTokenRequest; +import flipnote.user.grpc.GetUserByTokenResponse; +import flipnote.user.grpc.GetUserRequest; +import flipnote.user.grpc.GetUserResponse; +import flipnote.user.grpc.GetUsersRequest; +import flipnote.user.grpc.GetUsersResponse; +import flipnote.user.grpc.UserQueryServiceGrpc; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class GrpcUserQueryService extends UserQueryServiceGrpc.UserQueryServiceImplBase { + + private final UserService userService; + + @Override + public void getUser(GetUserRequest request, StreamObserver responseObserver) { + UserResult user = userService.findActiveUserById(request.getUserId()) + .orElseThrow(() -> Status.NOT_FOUND + .withDescription("μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.") + .asRuntimeException()); + + responseObserver.onNext(toUserResponse(user)); + responseObserver.onCompleted(); + } + + @Override + public void getUsers(GetUsersRequest request, StreamObserver responseObserver) { + List userIds = request.getUserIdsList(); + List users = userService.findActiveUsersByIds(userIds); + + GetUsersResponse response = GetUsersResponse.newBuilder() + .addAllUsers(users.stream().map(this::toUserResponse).toList()) + .build(); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + + @Override + public void getUserByEmail(GetUserByEmailRequest request, StreamObserver responseObserver) { + userService.findActiveUserByEmail(request.getEmail()) + .ifPresentOrElse( + user -> responseObserver.onNext( + GetUserByEmailResponse.newBuilder() + .setExists(true) + .setUser(toUserResponse(user)) + .build() + ), + () -> responseObserver.onNext( + GetUserByEmailResponse.newBuilder() + .setExists(false) + .build() + ) + ); + + responseObserver.onCompleted(); + } + + @Override + public void getUserByToken(GetUserByTokenRequest request, StreamObserver responseObserver) { + UserResult user = userService.findUserByToken(request.getAccessToken()); + + responseObserver.onNext( + GetUserByTokenResponse.newBuilder() + .setUserId(user.id()) + .setNickname(user.nickname()) + .build() + ); + responseObserver.onCompleted(); + } + + private GetUserResponse toUserResponse(UserResult user) { + return GetUserResponse.newBuilder() + .setId(user.id()) + .setEmail(user.email()) + .setNickname(user.nickname()) + .setProfileImageUrl(user.profileImageUrl()) + .build(); + } +} diff --git a/src/main/java/flipnote/user/auth/presentation/AuthController.java b/src/main/java/flipnote/user/interfaces/http/AuthController.java similarity index 54% rename from src/main/java/flipnote/user/auth/presentation/AuthController.java rename to src/main/java/flipnote/user/interfaces/http/AuthController.java index 060f8d6..aa44fc9 100644 --- a/src/main/java/flipnote/user/auth/presentation/AuthController.java +++ b/src/main/java/flipnote/user/interfaces/http/AuthController.java @@ -1,27 +1,36 @@ -package flipnote.user.auth.presentation; - -import flipnote.user.auth.application.AuthService; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import flipnote.user.auth.domain.TokenPair; -import flipnote.user.auth.presentation.dto.request.ChangePasswordRequest; -import flipnote.user.auth.presentation.dto.request.EmailVerificationRequest; -import flipnote.user.auth.presentation.dto.request.EmailVerifyRequest; -import flipnote.user.auth.presentation.dto.request.LoginRequest; -import flipnote.user.auth.presentation.dto.request.PasswordResetCreateRequest; -import flipnote.user.auth.presentation.dto.request.PasswordResetRequest; -import flipnote.user.auth.presentation.dto.request.SignupRequest; -import flipnote.user.auth.presentation.dto.request.TokenValidateRequest; -import flipnote.user.auth.presentation.dto.response.SocialLinksResponse; -import flipnote.user.auth.presentation.dto.response.TokenValidateResponse; -import flipnote.user.auth.presentation.dto.response.UserResponse; -import flipnote.user.global.constants.HttpConstants; -import flipnote.user.global.util.CookieUtil; +package flipnote.user.interfaces.http; + +import flipnote.user.application.AuthService; +import flipnote.user.application.result.SocialLinksResult; +import flipnote.user.application.result.TokenValidateResult; +import flipnote.user.application.result.UserRegisterResult; +import flipnote.user.domain.TokenPair; +import flipnote.user.infrastructure.jwt.JwtProvider; +import flipnote.user.interfaces.http.common.CookieUtil; +import flipnote.user.interfaces.http.common.HttpConstants; +import flipnote.user.interfaces.http.dto.request.ChangePasswordRequest; +import flipnote.user.interfaces.http.dto.request.EmailVerificationRequest; +import flipnote.user.interfaces.http.dto.request.EmailVerifyRequest; +import flipnote.user.interfaces.http.dto.request.LoginRequest; +import flipnote.user.interfaces.http.dto.request.PasswordResetCreateRequest; +import flipnote.user.interfaces.http.dto.request.PasswordResetRequest; +import flipnote.user.interfaces.http.dto.request.SignupRequest; +import flipnote.user.interfaces.http.dto.request.TokenValidateRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/v1/auth") @@ -32,15 +41,16 @@ public class AuthController { private final JwtProvider jwtProvider; @PostMapping("/register") - public ResponseEntity register(@Valid @RequestBody SignupRequest request) { - UserResponse response = authService.register(request); - return ResponseEntity.status(HttpStatus.CREATED).body(response); + public ResponseEntity register(@Valid @RequestBody SignupRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(authService.register(request.toCommand())); } @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request, - HttpServletResponse response) { - TokenPair tokenPair = authService.login(request); + public ResponseEntity login( + @Valid @RequestBody LoginRequest request, + HttpServletResponse response + ) { + TokenPair tokenPair = authService.login(request.toCommand()); setTokenCookies(response, tokenPair); return ResponseEntity.ok().build(); } @@ -48,7 +58,8 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest request, @PostMapping("/logout") public ResponseEntity logout( @CookieValue(name = HttpConstants.REFRESH_TOKEN_COOKIE, required = false) String refreshToken, - HttpServletResponse response) { + HttpServletResponse response + ) { authService.logout(refreshToken); CookieUtil.deleteCookie(response, HttpConstants.ACCESS_TOKEN_COOKIE); CookieUtil.deleteCookie(response, HttpConstants.REFRESH_TOKEN_COOKIE); @@ -58,47 +69,44 @@ public ResponseEntity logout( @PostMapping("/token/refresh") public ResponseEntity refreshToken( @CookieValue(name = HttpConstants.REFRESH_TOKEN_COOKIE) String refreshToken, - HttpServletResponse response) { + HttpServletResponse response + ) { TokenPair tokenPair = authService.refreshToken(refreshToken); setTokenCookies(response, tokenPair); return ResponseEntity.ok().build(); } @PostMapping("/token/validate") - public ResponseEntity validateToken( - @Valid @RequestBody TokenValidateRequest request) { - TokenValidateResponse result = authService.validateToken(request.getToken()); - return ResponseEntity.ok(result); + public ResponseEntity validateToken(@Valid @RequestBody TokenValidateRequest request) { + return ResponseEntity.ok(authService.validateToken(request.getToken())); } @PatchMapping("/password") public ResponseEntity changePassword( @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId, @Valid @RequestBody ChangePasswordRequest request, - HttpServletResponse response) { - authService.changePassword(userId, request); + HttpServletResponse response + ) { + authService.changePassword(userId, request.toCommand()); CookieUtil.deleteCookie(response, HttpConstants.ACCESS_TOKEN_COOKIE); CookieUtil.deleteCookie(response, HttpConstants.REFRESH_TOKEN_COOKIE); return ResponseEntity.noContent().build(); } @PostMapping("/email-verification/request") - public ResponseEntity sendEmailVerification( - @Valid @RequestBody EmailVerificationRequest request) { + public ResponseEntity sendEmailVerification(@Valid @RequestBody EmailVerificationRequest request) { authService.sendEmailVerificationCode(request.getEmail()); return ResponseEntity.ok().build(); } @PostMapping("/email-verification") - public ResponseEntity verifyEmail( - @Valid @RequestBody EmailVerifyRequest request) { + public ResponseEntity verifyEmail(@Valid @RequestBody EmailVerifyRequest request) { authService.verifyEmail(request.getEmail(), request.getCode()); return ResponseEntity.ok().build(); } @PostMapping("/password-reset/request") - public ResponseEntity requestPasswordReset( - @Valid @RequestBody PasswordResetCreateRequest request) { + public ResponseEntity requestPasswordReset(@Valid @RequestBody PasswordResetCreateRequest request) { authService.requestPasswordReset(request.getEmail()); return ResponseEntity.noContent().build(); } @@ -106,7 +114,8 @@ public ResponseEntity requestPasswordReset( @PostMapping("/password-reset") public ResponseEntity resetPassword( @Valid @RequestBody PasswordResetRequest request, - HttpServletResponse response) { + HttpServletResponse response + ) { authService.resetPassword(request.getToken(), request.getPassword()); CookieUtil.deleteCookie(response, HttpConstants.ACCESS_TOKEN_COOKIE); CookieUtil.deleteCookie(response, HttpConstants.REFRESH_TOKEN_COOKIE); @@ -114,16 +123,16 @@ public ResponseEntity resetPassword( } @GetMapping("/social-links") - public ResponseEntity getSocialLinks( + public ResponseEntity getSocialLinks( @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId) { - SocialLinksResponse response = authService.getSocialLinks(userId); - return ResponseEntity.ok(response); + return ResponseEntity.ok(authService.getSocialLinks(userId)); } @DeleteMapping("/social-links/{socialLinkId}") public ResponseEntity deleteSocialLink( @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId, - @PathVariable Long socialLinkId) { + @PathVariable Long socialLinkId + ) { authService.deleteSocialLink(userId, socialLinkId); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/flipnote/user/auth/presentation/OAuthController.java b/src/main/java/flipnote/user/interfaces/http/OAuthController.java similarity index 80% rename from src/main/java/flipnote/user/auth/presentation/OAuthController.java rename to src/main/java/flipnote/user/interfaces/http/OAuthController.java index e34b016..8c92703 100644 --- a/src/main/java/flipnote/user/auth/presentation/OAuthController.java +++ b/src/main/java/flipnote/user/interfaces/http/OAuthController.java @@ -1,18 +1,19 @@ -package flipnote.user.auth.presentation; +package flipnote.user.interfaces.http; -import flipnote.user.auth.application.OAuthService; -import flipnote.user.auth.domain.AuthErrorCode; -import flipnote.user.global.exception.BizException; -import flipnote.user.auth.domain.TokenPair; -import flipnote.user.global.config.ClientProperties; -import flipnote.user.global.constants.HttpConstants; -import flipnote.user.global.util.CookieUtil; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; +import flipnote.user.application.OAuthService; +import flipnote.user.domain.AuthErrorCode; +import flipnote.user.domain.TokenPair; +import flipnote.user.domain.common.BizException; +import flipnote.user.infrastructure.config.ClientProperties; +import flipnote.user.infrastructure.jwt.JwtProvider; +import flipnote.user.interfaces.http.common.CookieUtil; +import flipnote.user.interfaces.http.common.HttpConstants; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; @@ -28,14 +29,24 @@ public class OAuthController { private final JwtProvider jwtProvider; private final ClientProperties clientProperties; + private static final int VERIFIER_COOKIE_MAX_AGE = 180; + @GetMapping("/oauth2/authorization/{provider}") public ResponseEntity redirectToProvider( @PathVariable String provider, @RequestHeader(value = HttpConstants.USER_ID_HEADER, required = false) Long userId) { OAuthService.AuthorizationRedirect redirect = oAuthService.getAuthorizationUri(provider, userId); + ResponseCookie verifierCookie = ResponseCookie.from(HttpConstants.OAUTH_VERIFIER_COOKIE, redirect.codeVerifier()) + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(VERIFIER_COOKIE_MAX_AGE) + .sameSite("Lax") + .build(); + return ResponseEntity.status(HttpStatus.FOUND) - .header(HttpHeaders.SET_COOKIE, redirect.verifierCookie().toString()) + .header(HttpHeaders.SET_COOKIE, verifierCookie.toString()) .location(URI.create(redirect.authorizeUri())) .build(); } @@ -76,8 +87,7 @@ private ResponseEntity handleSocialLogin(String provider, String code, Str } } - private ResponseEntity handleSocialLink(String provider, String code, String state, - String codeVerifier) { + private ResponseEntity handleSocialLink(String provider, String code, String state, String codeVerifier) { try { oAuthService.linkSocialAccount(provider, code, state, codeVerifier); return ResponseEntity.status(HttpStatus.FOUND) diff --git a/src/main/java/flipnote/user/interfaces/http/UserController.java b/src/main/java/flipnote/user/interfaces/http/UserController.java new file mode 100644 index 0000000..361110c --- /dev/null +++ b/src/main/java/flipnote/user/interfaces/http/UserController.java @@ -0,0 +1,52 @@ +package flipnote.user.interfaces.http; + +import flipnote.user.application.UserService; +import flipnote.user.application.result.MyInfoResult; +import flipnote.user.application.result.UserInfoResult; +import flipnote.user.application.result.UserUpdateResult; +import flipnote.user.interfaces.http.common.HttpConstants; +import flipnote.user.interfaces.http.dto.request.UpdateProfileRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @GetMapping("/me") + public ResponseEntity getMyInfo( + @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId) { + return ResponseEntity.ok(userService.getMyInfo(userId)); + } + + @GetMapping("/{userId}") + public ResponseEntity getUserInfo(@PathVariable Long userId) { + return ResponseEntity.ok(userService.getUserInfo(userId)); + } + + @PutMapping + public ResponseEntity updateProfile( + @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId, + @Valid @RequestBody UpdateProfileRequest request) { + return ResponseEntity.ok(userService.updateProfile(userId, request.toCommand())); + } + + @DeleteMapping + public ResponseEntity withdraw( + @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId) { + userService.withdraw(userId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/flipnote/user/global/error/ApiResponse.java b/src/main/java/flipnote/user/interfaces/http/common/ApiResponse.java similarity index 95% rename from src/main/java/flipnote/user/global/error/ApiResponse.java rename to src/main/java/flipnote/user/interfaces/http/common/ApiResponse.java index 39f2442..968d8db 100644 --- a/src/main/java/flipnote/user/global/error/ApiResponse.java +++ b/src/main/java/flipnote/user/interfaces/http/common/ApiResponse.java @@ -1,5 +1,6 @@ -package flipnote.user.global.error; +package flipnote.user.interfaces.http.common; +import flipnote.user.domain.common.ErrorCode; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/flipnote/user/global/error/ApiResponseAdvice.java b/src/main/java/flipnote/user/interfaces/http/common/ApiResponseAdvice.java similarity index 96% rename from src/main/java/flipnote/user/global/error/ApiResponseAdvice.java rename to src/main/java/flipnote/user/interfaces/http/common/ApiResponseAdvice.java index eddabf1..b2bdb3a 100644 --- a/src/main/java/flipnote/user/global/error/ApiResponseAdvice.java +++ b/src/main/java/flipnote/user/interfaces/http/common/ApiResponseAdvice.java @@ -1,4 +1,4 @@ -package flipnote.user.global.error; +package flipnote.user.interfaces.http.common; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; diff --git a/src/main/java/flipnote/user/global/util/CookieUtil.java b/src/main/java/flipnote/user/interfaces/http/common/CookieUtil.java similarity index 95% rename from src/main/java/flipnote/user/global/util/CookieUtil.java rename to src/main/java/flipnote/user/interfaces/http/common/CookieUtil.java index bfeda9b..6df476e 100644 --- a/src/main/java/flipnote/user/global/util/CookieUtil.java +++ b/src/main/java/flipnote/user/interfaces/http/common/CookieUtil.java @@ -1,4 +1,4 @@ -package flipnote.user.global.util; +package flipnote.user.interfaces.http.common; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseCookie; diff --git a/src/main/java/flipnote/user/global/error/GlobalExceptionHandler.java b/src/main/java/flipnote/user/interfaces/http/common/GlobalExceptionHandler.java similarity index 95% rename from src/main/java/flipnote/user/global/error/GlobalExceptionHandler.java rename to src/main/java/flipnote/user/interfaces/http/common/GlobalExceptionHandler.java index 5d4d142..e50545d 100644 --- a/src/main/java/flipnote/user/global/error/GlobalExceptionHandler.java +++ b/src/main/java/flipnote/user/interfaces/http/common/GlobalExceptionHandler.java @@ -1,6 +1,6 @@ -package flipnote.user.global.error; +package flipnote.user.interfaces.http.common; -import flipnote.user.global.exception.BizException; +import flipnote.user.domain.common.BizException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/flipnote/user/global/constants/HttpConstants.java b/src/main/java/flipnote/user/interfaces/http/common/HttpConstants.java similarity index 89% rename from src/main/java/flipnote/user/global/constants/HttpConstants.java rename to src/main/java/flipnote/user/interfaces/http/common/HttpConstants.java index dd85905..ef64b99 100644 --- a/src/main/java/flipnote/user/global/constants/HttpConstants.java +++ b/src/main/java/flipnote/user/interfaces/http/common/HttpConstants.java @@ -1,4 +1,4 @@ -package flipnote.user.global.constants; +package flipnote.user.interfaces.http.common; import lombok.NoArgsConstructor; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/ChangePasswordRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/ChangePasswordRequest.java similarity index 68% rename from src/main/java/flipnote/user/auth/presentation/dto/request/ChangePasswordRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/ChangePasswordRequest.java index 68fca89..f4c2513 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/ChangePasswordRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/ChangePasswordRequest.java @@ -1,5 +1,6 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; +import flipnote.user.application.command.ChangePasswordCommand; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Getter; @@ -15,4 +16,8 @@ public class ChangePasswordRequest { @NotBlank(message = "μƒˆ λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€") @Size(min = 8, max = 20, message = "λΉ„λ°€λ²ˆν˜ΈλŠ” 8자 이상 20자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€") private String newPassword; + + public ChangePasswordCommand toCommand() { + return new ChangePasswordCommand(currentPassword, newPassword); + } } diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerificationRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerificationRequest.java similarity index 87% rename from src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerificationRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerificationRequest.java index 9914235..a16898a 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerificationRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerificationRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerifyRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerifyRequest.java similarity index 91% rename from src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerifyRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerifyRequest.java index add0490..0e2033d 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/EmailVerifyRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerifyRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/LoginRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/LoginRequest.java similarity index 68% rename from src/main/java/flipnote/user/auth/presentation/dto/request/LoginRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/LoginRequest.java index e84a68e..f812b75 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/LoginRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/LoginRequest.java @@ -1,5 +1,6 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; +import flipnote.user.application.command.LoginCommand; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import lombok.Getter; @@ -15,4 +16,8 @@ public class LoginRequest { @NotBlank(message = "λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€") private String password; + + public LoginCommand toCommand() { + return new LoginCommand(email, password); + } } diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetCreateRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetCreateRequest.java similarity index 87% rename from src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetCreateRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetCreateRequest.java index 805ec9e..d166b03 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetCreateRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetCreateRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetRequest.java similarity index 90% rename from src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetRequest.java index 5bf5a9f..fd57bd8 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/PasswordResetRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/SignupRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/SignupRequest.java similarity index 81% rename from src/main/java/flipnote/user/auth/presentation/dto/request/SignupRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/SignupRequest.java index a59af8e..1a0008c 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/SignupRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/SignupRequest.java @@ -1,5 +1,6 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; +import flipnote.user.application.command.SignupCommand; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -32,4 +33,8 @@ public class SignupRequest { @Pattern(regexp = "^01[0-9]{8,9}$", message = "μ˜¬λ°”λ₯Έ μ „ν™”λ²ˆν˜Έ ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€") private String phone; + + public SignupCommand toCommand() { + return new SignupCommand(email, password, name, nickname, phone, Boolean.TRUE.equals(smsAgree)); + } } diff --git a/src/main/java/flipnote/user/auth/presentation/dto/request/TokenValidateRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/TokenValidateRequest.java similarity index 82% rename from src/main/java/flipnote/user/auth/presentation/dto/request/TokenValidateRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/TokenValidateRequest.java index 45055ce..3586ecb 100644 --- a/src/main/java/flipnote/user/auth/presentation/dto/request/TokenValidateRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/TokenValidateRequest.java @@ -1,4 +1,4 @@ -package flipnote.user.auth.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; import jakarta.validation.constraints.NotBlank; import lombok.Getter; diff --git a/src/main/java/flipnote/user/user/presentation/dto/request/UpdateProfileRequest.java b/src/main/java/flipnote/user/interfaces/http/dto/request/UpdateProfileRequest.java similarity index 71% rename from src/main/java/flipnote/user/user/presentation/dto/request/UpdateProfileRequest.java rename to src/main/java/flipnote/user/interfaces/http/dto/request/UpdateProfileRequest.java index 2bcbff3..86bf9aa 100644 --- a/src/main/java/flipnote/user/user/presentation/dto/request/UpdateProfileRequest.java +++ b/src/main/java/flipnote/user/interfaces/http/dto/request/UpdateProfileRequest.java @@ -1,5 +1,6 @@ -package flipnote.user.user.presentation.dto.request; +package flipnote.user.interfaces.http.dto.request; +import flipnote.user.application.command.UpdateProfileCommand; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; @@ -20,4 +21,8 @@ public class UpdateProfileRequest { private Boolean smsAgree; private Long imageRefId; + + public UpdateProfileCommand toCommand() { + return new UpdateProfileCommand(nickname, phone, smsAgree, imageRefId); + } } diff --git a/src/main/java/flipnote/user/interfaces/http/dto/response/.gitkeep b/src/main/java/flipnote/user/interfaces/http/dto/response/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/flipnote/user/user/application/UserService.java b/src/main/java/flipnote/user/user/application/UserService.java deleted file mode 100644 index 220bb07..0000000 --- a/src/main/java/flipnote/user/user/application/UserService.java +++ /dev/null @@ -1,95 +0,0 @@ -package flipnote.user.user.application; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import flipnote.image.grpc.v1.ActivateImageRequest; -import flipnote.image.grpc.v1.ActivateImageResponse; -import flipnote.image.grpc.v1.ChangeImageRequest; -import flipnote.image.grpc.v1.ChangeImageResponse; -import flipnote.image.grpc.v1.ImageCommandServiceGrpc; -import flipnote.image.grpc.v1.Type; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import flipnote.user.auth.infrastructure.redis.SessionInvalidationRepository; -import flipnote.user.global.error.ImageErrorCode; -import flipnote.user.global.exception.BizException; -import flipnote.user.user.domain.User; -import flipnote.user.user.domain.UserErrorCode; -import flipnote.user.user.domain.UserRepository; -import flipnote.user.user.presentation.dto.request.UpdateProfileRequest; -import flipnote.user.user.presentation.dto.response.MyInfoResponse; -import flipnote.user.user.presentation.dto.response.UserInfoResponse; -import flipnote.user.user.presentation.dto.response.UserUpdateResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class UserService { - - private final UserRepository userRepository; - private final SessionInvalidationRepository sessionInvalidationRepository; - private final JwtProvider jwtProvider; - private final ImageCommandServiceGrpc.ImageCommandServiceBlockingStub imageCommandServiceStub; - - public MyInfoResponse getMyInfo(Long userId) { - User user = findActiveUser(userId); - return MyInfoResponse.from(user); - } - - public UserInfoResponse getUserInfo(Long userId) { - User user = userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) - .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); - return UserInfoResponse.from(user); - } - - @Transactional - public UserUpdateResponse updateProfile(Long userId, UpdateProfileRequest request) { - User user = findActiveUser(userId); - - String profileImageUrl = null; - if (request.getImageRefId() != null) { - try { - if (User.DEFAULT_PROFILE_IMAGE_URL.equals(user.getProfileImageUrl())) { - ActivateImageResponse activateImageResponse = imageCommandServiceStub.activateImage( - ActivateImageRequest.newBuilder() - .setReferenceType(Type.USER) - .setReferenceId(userId) - .setImageRefId(request.getImageRefId()) - .build()); - - profileImageUrl = activateImageResponse.getUrl(); - } else { - ChangeImageResponse changeImageResponse = imageCommandServiceStub.changeImage( - ChangeImageRequest.newBuilder() - .setReferenceType(Type.USER) - .setReferenceId(userId) - .setImageRefId(request.getImageRefId()) - .build()); - - profileImageUrl = changeImageResponse.getUrl(); - } - } catch (Exception ex) { - log.error("updateProfile", ex); - throw new BizException(ImageErrorCode.IMAGE_SERVICE_ERROR); - } - } - - user.updateProfile(request.getNickname(), request.getPhone(), request.getSmsAgree(), profileImageUrl); - return UserUpdateResponse.from(user, request.getImageRefId()); - } - - @Transactional - public void withdraw(Long userId) { - User user = findActiveUser(userId); - user.withdraw(); - sessionInvalidationRepository.invalidate(userId, jwtProvider.getRefreshTokenExpiration()); - } - - private User findActiveUser(Long userId) { - return userRepository.findByIdAndStatus(userId, User.Status.ACTIVE) - .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); - } -} diff --git a/src/main/java/flipnote/user/user/presentation/UserController.java b/src/main/java/flipnote/user/user/presentation/UserController.java deleted file mode 100644 index fc53b14..0000000 --- a/src/main/java/flipnote/user/user/presentation/UserController.java +++ /dev/null @@ -1,48 +0,0 @@ -package flipnote.user.user.presentation; - -import flipnote.user.user.application.UserService; -import flipnote.user.user.presentation.dto.request.UpdateProfileRequest; -import flipnote.user.user.presentation.dto.response.MyInfoResponse; -import flipnote.user.user.presentation.dto.response.UserInfoResponse; -import flipnote.user.user.presentation.dto.response.UserUpdateResponse; -import flipnote.user.global.constants.HttpConstants; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/v1/users") -@RequiredArgsConstructor -public class UserController { - - private final UserService userService; - - @GetMapping("/me") - public ResponseEntity getMyInfo( - @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId) { - MyInfoResponse response = userService.getMyInfo(userId); - return ResponseEntity.ok(response); - } - - @GetMapping("/{userId}") - public ResponseEntity getUserInfo(@PathVariable Long userId) { - UserInfoResponse response = userService.getUserInfo(userId); - return ResponseEntity.ok(response); - } - - @PutMapping - public ResponseEntity updateProfile( - @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId, - @Valid @RequestBody UpdateProfileRequest request) { - UserUpdateResponse response = userService.updateProfile(userId, request); - return ResponseEntity.ok(response); - } - - @DeleteMapping - public ResponseEntity withdraw( - @RequestHeader(HttpConstants.USER_ID_HEADER) Long userId) { - userService.withdraw(userId); - return ResponseEntity.noContent().build(); - } -} diff --git a/src/main/java/flipnote/user/user/presentation/dto/response/UserInfoResponse.java b/src/main/java/flipnote/user/user/presentation/dto/response/UserInfoResponse.java deleted file mode 100644 index fd08f81..0000000 --- a/src/main/java/flipnote/user/user/presentation/dto/response/UserInfoResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package flipnote.user.user.presentation.dto.response; - -import flipnote.user.user.domain.User; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class UserInfoResponse { - - private Long userId; - private String nickname; - private String profileImageUrl; - private Long imageRefId; - - public static UserInfoResponse from(User user) { - return new UserInfoResponse(user.getId(), user.getNickname(), user.getProfileImageUrl(), null); - } -} diff --git a/src/main/java/flipnote/user/user/presentation/dto/response/UserUpdateResponse.java b/src/main/java/flipnote/user/user/presentation/dto/response/UserUpdateResponse.java deleted file mode 100644 index 1880087..0000000 --- a/src/main/java/flipnote/user/user/presentation/dto/response/UserUpdateResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -package flipnote.user.user.presentation.dto.response; - -import flipnote.user.user.domain.User; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class UserUpdateResponse { - - private Long userId; - private String nickname; - private String phone; - private Boolean smsAgree; - private String profileImageUrl; - private Long imageRefId; - - public static UserUpdateResponse from(User user, Long imageRefId) { - return new UserUpdateResponse( - user.getId(), - user.getNickname(), - user.getPhone(), - user.isSmsAgree(), - user.getProfileImageUrl(), - imageRefId - ); - } -} diff --git a/src/main/java/flipnote/user/user/presentation/grpc/GrpcUserQueryService.java b/src/main/java/flipnote/user/user/presentation/grpc/GrpcUserQueryService.java deleted file mode 100644 index e38d3e6..0000000 --- a/src/main/java/flipnote/user/user/presentation/grpc/GrpcUserQueryService.java +++ /dev/null @@ -1,135 +0,0 @@ -package flipnote.user.user.presentation.grpc; - -import flipnote.user.auth.domain.TokenClaims; -import flipnote.user.auth.infrastructure.jwt.JwtProvider; -import flipnote.user.user.domain.User; -import flipnote.user.user.domain.UserRepository; -import flipnote.user.grpc.GetUserByEmailRequest; -import flipnote.user.grpc.GetUserByEmailResponse; -import flipnote.user.grpc.GetUserByTokenRequest; -import flipnote.user.grpc.GetUserByTokenResponse; -import flipnote.user.grpc.GetUserRequest; -import flipnote.user.grpc.GetUserResponse; -import flipnote.user.grpc.GetUsersRequest; -import flipnote.user.grpc.GetUsersResponse; -import flipnote.user.grpc.UserQueryServiceGrpc; -import io.grpc.Status; -import io.grpc.stub.StreamObserver; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class GrpcUserQueryService extends UserQueryServiceGrpc.UserQueryServiceImplBase { - - private final UserRepository userRepository; - private final JwtProvider jwtProvider; - - @Override - public void getUser(GetUserRequest request, StreamObserver responseObserver) { - try { - User user = userRepository.findByIdAndStatus(request.getUserId(), User.Status.ACTIVE) - .orElse(null); - - if (user == null) { - responseObserver.onError( - Status.NOT_FOUND.withDescription("μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.").asRuntimeException() - ); - return; - } - - responseObserver.onNext(toResponse(user)); - responseObserver.onCompleted(); - } catch (Exception e) { - log.error("gRPC getUser error. userId: {}", request.getUserId(), e); - responseObserver.onError(Status.INTERNAL.withDescription("Internal error").asRuntimeException()); - } - } - - @Override - public void getUsers(GetUsersRequest request, StreamObserver responseObserver) { - try { - List userIds = request.getUserIdsList(); - List users = userRepository.findByIdInAndStatus(userIds, User.Status.ACTIVE); - - GetUsersResponse response = GetUsersResponse.newBuilder() - .addAllUsers(users.stream().map(this::toResponse).toList()) - .build(); - - responseObserver.onNext(response); - responseObserver.onCompleted(); - } catch (Exception e) { - log.error("gRPC getUsers error. userIds: {}", request.getUserIdsList(), e); - responseObserver.onError(Status.INTERNAL.withDescription("Internal error").asRuntimeException()); - } - } - - @Override - public void getUserByEmail(GetUserByEmailRequest request, StreamObserver responseObserver) { - try { - User user = userRepository.findByEmailAndStatus(request.getEmail(), User.Status.ACTIVE) - .orElse(null); - - GetUserByEmailResponse.Builder responseBuilder = GetUserByEmailResponse.newBuilder(); - - if (user != null) { - responseBuilder.setExists(true).setUser(toResponse(user)); - } else { - responseBuilder.setExists(false); - } - - responseObserver.onNext(responseBuilder.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - log.error("gRPC getUserByEmail error. email: {}", request.getEmail(), e); - responseObserver.onError(Status.INTERNAL.withDescription("Internal error").asRuntimeException()); - } - } - - @Override - public void getUserByToken(GetUserByTokenRequest request, StreamObserver responseObserver) { - try { - if (!jwtProvider.isTokenValid(request.getAccessToken())) { - responseObserver.onError( - Status.UNAUTHENTICATED.withDescription("μœ νš¨ν•˜μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€.").asRuntimeException() - ); - return; - } - - TokenClaims claims = jwtProvider.extractClaims(request.getAccessToken()); - User user = userRepository.findByIdAndStatus(claims.userId(), User.Status.ACTIVE) - .orElse(null); - - if (user == null) { - responseObserver.onError( - Status.NOT_FOUND.withDescription("μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.").asRuntimeException() - ); - return; - } - - GetUserByTokenResponse response = GetUserByTokenResponse.newBuilder() - .setUserId(user.getId()) - .setNickname(user.getNickname()) - .build(); - - responseObserver.onNext(response); - responseObserver.onCompleted(); - } catch (Exception e) { - log.error("gRPC getUserByToken error", e); - responseObserver.onError(Status.INTERNAL.withDescription("Internal error").asRuntimeException()); - } - } - - private GetUserResponse toResponse(User user) { - return GetUserResponse.newBuilder() - .setId(user.getId()) - .setEmail(user.getEmail()) - .setNickname(user.getNickname()) - .setProfileImageUrl(user.getProfileImageUrl() != null ? user.getProfileImageUrl() : "") - .build(); - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index be7129b..8ed9735 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,11 @@ spring: application: name: user + jackson: + date-format: yyyy-MM-dd HH:mm:ss + serialization: + write-dates-as-timestamps: false + datasource: url: ${DB_URL:jdbc:mysql://localhost:3306/flipnote_user} username: ${DB_USERNAME:root}