diff --git a/src/main/java/com/boilerplate/common/TokenProvider.java b/src/main/java/com/boilerplate/common/TokenProvider.java index 9de1ada..57d85cd 100644 --- a/src/main/java/com/boilerplate/common/TokenProvider.java +++ b/src/main/java/com/boilerplate/common/TokenProvider.java @@ -57,7 +57,7 @@ public class TokenProvider { /** 서명/만료/issuer 검증 */ public boolean validate(String token) { try { - parser.parseClaimsJws(stripBearer(token)); + parser.parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; @@ -94,7 +94,7 @@ public class TokenProvider { /** Claims 추출(검증 포함) */ public Claims getClaims(String token) { - return parser.parseClaimsJws(stripBearer(token)).getBody(); + return parser.parseClaimsJws(token).getBody(); } public String getSubject(String token) { @@ -108,9 +108,4 @@ public class TokenProvider { public List getRoles(String token) { return getClaims(token).get("roles", List.class); } - - private String stripBearer(String token) { - if (token == null || !token.startsWith("Bearer ")) return null; - return token.substring(7); - } } \ No newline at end of file diff --git a/src/main/java/com/boilerplate/config/security/SecurityConfig.java b/src/main/java/com/boilerplate/config/security/SecurityConfig.java index 0a42bcb..2c72ac6 100644 --- a/src/main/java/com/boilerplate/config/security/SecurityConfig.java +++ b/src/main/java/com/boilerplate/config/security/SecurityConfig.java @@ -14,6 +14,11 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; @Configuration @RequiredArgsConstructor @@ -34,7 +39,8 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception { - http.csrf(AbstractHttpConfigurer::disable) + http.cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers(allAuthorizedUrls).permitAll() @@ -58,6 +64,21 @@ public class SecurityConfig { return http.build(); } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(List.of("http://localhost:3000")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setExposedHeaders(List.of("*")); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); diff --git a/src/main/java/com/boilerplate/core/auth/api/controller/AuthController.java b/src/main/java/com/boilerplate/core/auth/api/controller/AuthController.java index 8af60fe..4b1da3f 100644 --- a/src/main/java/com/boilerplate/core/auth/api/controller/AuthController.java +++ b/src/main/java/com/boilerplate/core/auth/api/controller/AuthController.java @@ -2,15 +2,18 @@ package com.boilerplate.core.auth.api.controller; import com.boilerplate.core.auth.api.dto.req.LoginRequest; import com.boilerplate.core.auth.api.dto.req.SignUpRequest; +import com.boilerplate.core.auth.api.dto.req.SocialLoginRequest; import com.boilerplate.core.auth.api.dto.res.LoginResponse; import com.boilerplate.core.auth.api.dto.res.SignUpResponse; import com.boilerplate.core.auth.application.LoginUseCase; import com.boilerplate.core.auth.application.RegisterUseCase; +import com.boilerplate.core.auth.application.SocialLoginUseCase; import com.boilerplate.core.auth.application.dto.command.RegisterUseCaseCommand; import com.boilerplate.core.auth.application.dto.result.LoginResult; import com.boilerplate.core.auth.application.dto.result.RegisterUseCaseResult; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -23,13 +26,16 @@ public class AuthController { private final RegisterUseCase registerUseCase; private final LoginUseCase loginUseCase; + private final SocialLoginUseCase socialLoginUseCase; @PostMapping("/register") public ResponseEntity register( @RequestBody SignUpRequest request ) { - RegisterUseCaseResult result = registerUseCase.execute( - RegisterUseCaseCommand.builder().loginId(request.getLoginId()).password(request.getPassword()).build()); + RegisterUseCaseResult result = registerUseCase.execute(RegisterUseCaseCommand.builder() + .email(request.getEmail()) + .password(request.getPassword()) + .build()); SignUpResponse response = SignUpResponse.builder() .accessToken(result.getAccessToken()) @@ -43,7 +49,7 @@ public class AuthController { public ResponseEntity login( @RequestBody LoginRequest request ) { - LoginResult result = loginUseCase.execute(request.getLoginId(), request.getPassword()); + LoginResult result = loginUseCase.execute(request.getEmail(), request.getPassword()); LoginResponse response = LoginResponse.builder() .accessToken(result.getAccessToken()) .refreshToken(result.getRefreshToken()) @@ -51,4 +57,15 @@ public class AuthController { return ResponseEntity.ok(response); } + + @PostMapping("/social") + public ResponseEntity socialLogin(@RequestBody SocialLoginRequest request) { + LoginResult result = socialLoginUseCase.execute(request.getProvider(), request.getToken()); + + LoginResponse response = LoginResponse.builder() + .accessToken(result.getAccessToken()) + .refreshToken(result.getRefreshToken()) + .build(); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/boilerplate/core/auth/api/dto/req/LoginRequest.java b/src/main/java/com/boilerplate/core/auth/api/dto/req/LoginRequest.java index c0bdd7b..6ee591e 100644 --- a/src/main/java/com/boilerplate/core/auth/api/dto/req/LoginRequest.java +++ b/src/main/java/com/boilerplate/core/auth/api/dto/req/LoginRequest.java @@ -7,7 +7,7 @@ import lombok.Setter; @Setter public class LoginRequest { - private String loginId; + private String email; private String password; } diff --git a/src/main/java/com/boilerplate/core/auth/api/dto/req/SignUpRequest.java b/src/main/java/com/boilerplate/core/auth/api/dto/req/SignUpRequest.java index 3e59377..3d47100 100644 --- a/src/main/java/com/boilerplate/core/auth/api/dto/req/SignUpRequest.java +++ b/src/main/java/com/boilerplate/core/auth/api/dto/req/SignUpRequest.java @@ -11,6 +11,6 @@ import lombok.NoArgsConstructor; @Getter public class SignUpRequest { - private String loginId; + private String email; private String password; } diff --git a/src/main/java/com/boilerplate/core/auth/api/dto/req/SocialLoginRequest.java b/src/main/java/com/boilerplate/core/auth/api/dto/req/SocialLoginRequest.java new file mode 100644 index 0000000..56e6094 --- /dev/null +++ b/src/main/java/com/boilerplate/core/auth/api/dto/req/SocialLoginRequest.java @@ -0,0 +1,16 @@ +package com.boilerplate.core.auth.api.dto.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class SocialLoginRequest { + private SocialProvider provider; + private String token; +} + diff --git a/src/main/java/com/boilerplate/core/auth/api/dto/req/SocialProvider.java b/src/main/java/com/boilerplate/core/auth/api/dto/req/SocialProvider.java new file mode 100644 index 0000000..5e7b68f --- /dev/null +++ b/src/main/java/com/boilerplate/core/auth/api/dto/req/SocialProvider.java @@ -0,0 +1,16 @@ +package com.boilerplate.core.auth.api.dto.req; + +import com.fasterxml.jackson.annotation.JsonCreator; +import java.util.stream.Stream; + +public enum SocialProvider { + LOCAL, GOOGLE, FACEBOOK; + + @JsonCreator + public static SocialProvider from(String value) { + return Stream.of(SocialProvider.values()) + .filter(p -> p.name().equalsIgnoreCase(value)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/com/boilerplate/core/auth/application/LoginUseCase.java b/src/main/java/com/boilerplate/core/auth/application/LoginUseCase.java index 7d921cb..a280e7f 100644 --- a/src/main/java/com/boilerplate/core/auth/application/LoginUseCase.java +++ b/src/main/java/com/boilerplate/core/auth/application/LoginUseCase.java @@ -15,8 +15,8 @@ public class LoginUseCase { private final AccountService accountService; private final TokenProvider tokenProvider; - public LoginResult execute(String loginId, String password) { - long accountId = accountService.login(loginId, password); + public LoginResult execute(String email, String password) { + long accountId = accountService.login(email, password); String accessToken = tokenProvider.createUserAccessToken(accountId); String refreshToken = tokenProvider.createUserRefreshToken(accountId); diff --git a/src/main/java/com/boilerplate/core/auth/application/RegisterUseCase.java b/src/main/java/com/boilerplate/core/auth/application/RegisterUseCase.java index f8f9e77..4fc3e9b 100644 --- a/src/main/java/com/boilerplate/core/auth/application/RegisterUseCase.java +++ b/src/main/java/com/boilerplate/core/auth/application/RegisterUseCase.java @@ -4,7 +4,7 @@ import com.boilerplate.common.TokenProvider; import com.boilerplate.core.auth.application.dto.command.RegisterUseCaseCommand; import com.boilerplate.core.auth.application.dto.result.RegisterUseCaseResult; import com.boilerplate.core.auth.domain.service.AccountService; -import com.boilerplate.core.exception.auth.ExistLoginIdException; +import com.boilerplate.core.exception.auth.ExistEmailException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -18,15 +18,15 @@ public final class RegisterUseCase { private final TokenProvider tokenProvider; public RegisterUseCaseResult execute(RegisterUseCaseCommand command) { - String loginId = command.getLoginId(); + String email = command.getEmail(); - boolean isExistLoginId = accountService.isExistLoginId(loginId); + boolean isExistEmail = accountService.isExistEmail(email); - if (isExistLoginId) { - throw new ExistLoginIdException(loginId); + if (isExistEmail) { + throw new ExistEmailException(email); } - long accountId = accountService.register(loginId, command.getPassword()); + long accountId = accountService.register(email, command.getPassword()); String accessToken = tokenProvider.createUserAccessToken(accountId); String refreshToken = tokenProvider.createUserRefreshToken(accountId); diff --git a/src/main/java/com/boilerplate/core/auth/application/SocialLoginUseCase.java b/src/main/java/com/boilerplate/core/auth/application/SocialLoginUseCase.java new file mode 100644 index 0000000..807b677 --- /dev/null +++ b/src/main/java/com/boilerplate/core/auth/application/SocialLoginUseCase.java @@ -0,0 +1,42 @@ +package com.boilerplate.core.auth.application; + +import com.boilerplate.common.TokenProvider; +import com.boilerplate.core.auth.api.dto.req.SocialProvider; +import com.boilerplate.core.auth.application.dto.result.LoginResult; +import com.boilerplate.core.auth.domain.SocialProviderClient; +import com.boilerplate.core.auth.domain.SocialUserInfo; +import com.boilerplate.core.auth.domain.service.AccountService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class SocialLoginUseCase { + + private final List socialProviderClients; + private final AccountService accountService; + private final TokenProvider tokenProvider; + + @Transactional + public LoginResult execute(SocialProvider provider, String token) { + SocialProviderClient client = socialProviderClients.stream() + .filter(c -> c.getProvider() == provider) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unsupported provider: " + provider)); + + SocialUserInfo userInfo = client.getUserInfo(token); + + long accountId = accountService.getOrCreateSocialAccount(userInfo); + + String accessToken = tokenProvider.createUserAccessToken(accountId); + String refreshToken = tokenProvider.createUserRefreshToken(accountId); + + return LoginResult.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/com/boilerplate/core/auth/application/dto/command/RegisterUseCaseCommand.java b/src/main/java/com/boilerplate/core/auth/application/dto/command/RegisterUseCaseCommand.java index 9e2e11f..20cb0f8 100644 --- a/src/main/java/com/boilerplate/core/auth/application/dto/command/RegisterUseCaseCommand.java +++ b/src/main/java/com/boilerplate/core/auth/application/dto/command/RegisterUseCaseCommand.java @@ -11,7 +11,7 @@ import lombok.NoArgsConstructor; @Getter public class RegisterUseCaseCommand { - private String loginId; + private String email; private String password; } diff --git a/src/main/java/com/boilerplate/core/auth/application/userDetail/User.java b/src/main/java/com/boilerplate/core/auth/application/userDetail/User.java index 5911ff6..6f4518a 100644 --- a/src/main/java/com/boilerplate/core/auth/application/userDetail/User.java +++ b/src/main/java/com/boilerplate/core/auth/application/userDetail/User.java @@ -36,6 +36,10 @@ public final class User implements UserDetails { return accountId; } + public Long getId() { + return accountId; + } + @Override public String getUsername() { return null; diff --git a/src/main/java/com/boilerplate/core/auth/domain/SocialProviderClient.java b/src/main/java/com/boilerplate/core/auth/domain/SocialProviderClient.java new file mode 100644 index 0000000..35d208a --- /dev/null +++ b/src/main/java/com/boilerplate/core/auth/domain/SocialProviderClient.java @@ -0,0 +1,8 @@ +package com.boilerplate.core.auth.domain; + +import com.boilerplate.core.auth.api.dto.req.SocialProvider; + +public interface SocialProviderClient { + SocialUserInfo getUserInfo(String token); + SocialProvider getProvider(); +} diff --git a/src/main/java/com/boilerplate/core/auth/domain/SocialUserInfo.java b/src/main/java/com/boilerplate/core/auth/domain/SocialUserInfo.java new file mode 100644 index 0000000..ea91e88 --- /dev/null +++ b/src/main/java/com/boilerplate/core/auth/domain/SocialUserInfo.java @@ -0,0 +1,14 @@ +package com.boilerplate.core.auth.domain; + +import com.boilerplate.core.auth.api.dto.req.SocialProvider; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class SocialUserInfo { + private final String providerId; + private final String email; + private final String name; + private final SocialProvider provider; +} diff --git a/src/main/java/com/boilerplate/core/auth/domain/entity/Account.java b/src/main/java/com/boilerplate/core/auth/domain/entity/Account.java index ccbfabd..a6bc32b 100644 --- a/src/main/java/com/boilerplate/core/auth/domain/entity/Account.java +++ b/src/main/java/com/boilerplate/core/auth/domain/entity/Account.java @@ -1,6 +1,9 @@ package com.boilerplate.core.auth.domain.entity; +import com.boilerplate.core.auth.api.dto.req.SocialProvider; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -24,10 +27,17 @@ public class Account { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String loginId; + private String email; private String passwordHash; private String name; + @Enumerated(EnumType.STRING) + @jakarta.persistence.Column(name = "auth_provider") + private SocialProvider provider; + + @jakarta.persistence.Column(name = "oauth_id") + private String providerId; + @Builder.Default private LocalDateTime createdAt = LocalDateTime.now(); diff --git a/src/main/java/com/boilerplate/core/auth/domain/entity/Grade.java b/src/main/java/com/boilerplate/core/auth/domain/entity/Grade.java new file mode 100644 index 0000000..64c1ad3 --- /dev/null +++ b/src/main/java/com/boilerplate/core/auth/domain/entity/Grade.java @@ -0,0 +1,27 @@ +package com.boilerplate.core.auth.domain.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@ToString +public class Grade { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private String description; +} diff --git a/src/main/java/com/boilerplate/core/auth/domain/entity/GradeHistory.java b/src/main/java/com/boilerplate/core/auth/domain/entity/GradeHistory.java new file mode 100644 index 0000000..383c596 --- /dev/null +++ b/src/main/java/com/boilerplate/core/auth/domain/entity/GradeHistory.java @@ -0,0 +1,31 @@ +package com.boilerplate.core.auth.domain.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@ToString +public class GradeHistory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long accountId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "grade_id") + private Grade grade; + + private String status; // ACTIVE, EXPIRED, CANCELED + + @Builder.Default + private LocalDateTime purchasedAt = LocalDateTime.now(); + + private LocalDateTime expiresAt; +} diff --git a/src/main/java/com/boilerplate/core/auth/domain/repository/AccountRepository.java b/src/main/java/com/boilerplate/core/auth/domain/repository/AccountRepository.java index 8833087..2c2f1cb 100644 --- a/src/main/java/com/boilerplate/core/auth/domain/repository/AccountRepository.java +++ b/src/main/java/com/boilerplate/core/auth/domain/repository/AccountRepository.java @@ -1,5 +1,6 @@ package com.boilerplate.core.auth.domain.repository; +import com.boilerplate.core.auth.api.dto.req.SocialProvider; import com.boilerplate.core.auth.domain.entity.Account; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -7,5 +8,6 @@ import org.springframework.stereotype.Repository; @Repository public interface AccountRepository extends JpaRepository { - Optional findByLoginId(String loginId); + Optional findByEmail(String email); + Optional findByProviderAndProviderId(SocialProvider provider, String providerId); } diff --git a/src/main/java/com/boilerplate/core/auth/domain/repository/GradeHistoryRepository.java b/src/main/java/com/boilerplate/core/auth/domain/repository/GradeHistoryRepository.java new file mode 100644 index 0000000..6097834 --- /dev/null +++ b/src/main/java/com/boilerplate/core/auth/domain/repository/GradeHistoryRepository.java @@ -0,0 +1,19 @@ +package com.boilerplate.core.auth.domain.repository; + +import com.boilerplate.core.auth.domain.entity.GradeHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface GradeHistoryRepository extends JpaRepository { + + @Query("SELECT gh FROM GradeHistory gh " + + "JOIN FETCH gh.grade " + + "WHERE gh.accountId = :accountId AND gh.status = 'ACTIVE' " + + "ORDER BY gh.purchasedAt DESC LIMIT 1") + Optional findCurrentActiveGrade(@Param("accountId") Long accountId); +} diff --git a/src/main/java/com/boilerplate/core/auth/domain/repository/GradeRepository.java b/src/main/java/com/boilerplate/core/auth/domain/repository/GradeRepository.java new file mode 100644 index 0000000..1f110e1 --- /dev/null +++ b/src/main/java/com/boilerplate/core/auth/domain/repository/GradeRepository.java @@ -0,0 +1,11 @@ +package com.boilerplate.core.auth.domain.repository; + +import com.boilerplate.core.auth.domain.entity.Grade; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface GradeRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/boilerplate/core/auth/domain/service/AccountService.java b/src/main/java/com/boilerplate/core/auth/domain/service/AccountService.java index 2297ed0..9653637 100644 --- a/src/main/java/com/boilerplate/core/auth/domain/service/AccountService.java +++ b/src/main/java/com/boilerplate/core/auth/domain/service/AccountService.java @@ -1,7 +1,13 @@ package com.boilerplate.core.auth.domain.service; +import com.boilerplate.core.auth.api.dto.req.SocialProvider; +import com.boilerplate.core.auth.domain.SocialUserInfo; import com.boilerplate.core.auth.domain.entity.Account; +import com.boilerplate.core.auth.domain.entity.Grade; +import com.boilerplate.core.auth.domain.entity.GradeHistory; import com.boilerplate.core.auth.domain.repository.AccountRepository; +import com.boilerplate.core.auth.domain.repository.GradeHistoryRepository; +import com.boilerplate.core.auth.domain.repository.GradeRepository; import com.boilerplate.core.exception.auth.InvalidCredentialsException; import com.boilerplate.core.exception.common.NotExistEntityException; import lombok.RequiredArgsConstructor; @@ -16,37 +22,78 @@ import org.springframework.transaction.annotation.Transactional; public class AccountService { private final AccountRepository accountRepository; + private final GradeRepository gradeRepository; + private final GradeHistoryRepository gradeHistoryRepository; private final PasswordEncoder passwordEncoder; @Transactional - public long register(String loginId, String password) { - log.info("회원 등록: {}", loginId); + public long register(String email, String password) { + log.info("회원 등록: {}", email); String hashedPassword = passwordEncoder.encode(password); + Grade normalGrade = gradeRepository.findByName("NORMAL") + .orElseThrow(() -> new NotExistEntityException(Grade.class.getName())); Account account = Account.builder() - .loginId(loginId) + .email(email) .name("임시 이름") .passwordHash(hashedPassword) + .provider(SocialProvider.LOCAL) .build(); log.info("account: {}", account); accountRepository.save(account); + // 등급 이력 생성 + GradeHistory gradeHistory = GradeHistory.builder() + .accountId(account.getId()) + .grade(normalGrade) + .status("ACTIVE") + .build(); + gradeHistoryRepository.save(gradeHistory); + return account.getId(); } - public boolean isExistLoginId(String loginId) { - log.info("로그인 아이디 존재 여부 조회"); + @Transactional + public long getOrCreateSocialAccount(SocialUserInfo userInfo) { + return accountRepository.findByProviderAndProviderId(userInfo.getProvider(), userInfo.getProviderId()) + .map(Account::getId) + .orElseGet(() -> { + Grade normalGrade = gradeRepository.findByName("NORMAL") + .orElseThrow(() -> new NotExistEntityException(Grade.class.getName())); - return accountRepository.findByLoginId(loginId).isPresent(); + Account newAccount = Account.builder() + .email(userInfo.getEmail()) + .name(userInfo.getName()) + .provider(userInfo.getProvider()) + .providerId(userInfo.getProviderId()) + .build(); + accountRepository.save(newAccount); + + // 등급 이력 생성 + GradeHistory gradeHistory = GradeHistory.builder() + .accountId(newAccount.getId()) + .grade(normalGrade) + .status("ACTIVE") + .build(); + gradeHistoryRepository.save(gradeHistory); + + return newAccount.getId(); + }); } - public long login(String loginId, String password) { + public boolean isExistEmail(String email) { + log.info("로그인 아이디 존재 여부 조회"); + + return accountRepository.findByEmail(email).isPresent(); + } + + public long login(String email, String password) { log.info("로그인"); - Account account = accountRepository.findByLoginId(loginId) + Account account = accountRepository.findByEmail(email) .orElseThrow(() -> new NotExistEntityException(Account.class.getName())); if (!passwordEncoder.matches(password, account.getPasswordHash())) { diff --git a/src/main/java/com/boilerplate/core/auth/infra/social/GoogleSocialProviderClient.java b/src/main/java/com/boilerplate/core/auth/infra/social/GoogleSocialProviderClient.java new file mode 100644 index 0000000..00552ef --- /dev/null +++ b/src/main/java/com/boilerplate/core/auth/infra/social/GoogleSocialProviderClient.java @@ -0,0 +1,70 @@ +package com.boilerplate.core.auth.infra.social; + +import com.boilerplate.core.auth.api.dto.req.SocialProvider; +import com.boilerplate.core.auth.domain.SocialProviderClient; +import com.boilerplate.core.auth.domain.SocialUserInfo; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +@Slf4j +@RequiredArgsConstructor +public class GoogleSocialProviderClient implements SocialProviderClient { + + private static final String GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"; + + @Override + public SocialUserInfo getUserInfo(String accessToken) { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity entity = new HttpEntity<>(headers); + + try { + ResponseEntity response = restTemplate.exchange( + GOOGLE_USERINFO_URL, + HttpMethod.GET, + entity, + Map.class + ); + + Map body = response.getBody(); + + if (body == null) { + throw new RuntimeException("Failed to get response from Google"); + } + + String userId = (String) body.get("sub"); + String email = (String) body.get("email"); + String name = (String) body.get("name"); + + log.info("google user name: {}", userId); + log.info("email: {}", email); + log.info("name: {}", name); + + return SocialUserInfo.builder() + .providerId(userId) + .email(email) + .name(name) + .provider(SocialProvider.GOOGLE) + .build(); + + } catch (Exception e) { + log.error("Error fetching Google user info with access token", e); + throw new RuntimeException("Failed to fetch Google user info", e); + } + } + + @Override + public SocialProvider getProvider() { + return SocialProvider.GOOGLE; + } +} diff --git a/src/main/java/com/boilerplate/core/exception/auth/ExistLoginIdException.java b/src/main/java/com/boilerplate/core/exception/auth/ExistEmailException.java similarity index 66% rename from src/main/java/com/boilerplate/core/exception/auth/ExistLoginIdException.java rename to src/main/java/com/boilerplate/core/exception/auth/ExistEmailException.java index 905849d..c35ee3e 100644 --- a/src/main/java/com/boilerplate/core/exception/auth/ExistLoginIdException.java +++ b/src/main/java/com/boilerplate/core/exception/auth/ExistEmailException.java @@ -4,14 +4,14 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.web.ErrorResponseException; -public class ExistLoginIdException extends ErrorResponseException { +public class ExistEmailException extends ErrorResponseException { - public ExistLoginIdException(String loginId) { + public ExistEmailException(String email) { super( HttpStatus.CONFLICT, ProblemDetail.forStatusAndDetail( HttpStatus.CONFLICT, - "이미 존재하는 loginId 입니다: " + loginId + "이미 존재하는 email 입니다: " + email ), null ); diff --git a/src/main/java/com/boilerplate/core/user/api/controller/UserController.java b/src/main/java/com/boilerplate/core/user/api/controller/UserController.java new file mode 100644 index 0000000..698efd9 --- /dev/null +++ b/src/main/java/com/boilerplate/core/user/api/controller/UserController.java @@ -0,0 +1,25 @@ +package com.boilerplate.core.user.api.controller; + +import com.boilerplate.core.auth.application.userDetail.User; +import com.boilerplate.core.user.api.dto.res.UserMeResponse; +import com.boilerplate.core.user.application.GetUserMeUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/user") +@RequiredArgsConstructor +public class UserController { + + private final GetUserMeUseCase getUserMeUseCase; + + @GetMapping("/me") + public ResponseEntity getMe(@AuthenticationPrincipal User user) { + UserMeResponse response = getUserMeUseCase.execute(user.getId()); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/boilerplate/core/user/api/dto/res/UserMeResponse.java b/src/main/java/com/boilerplate/core/user/api/dto/res/UserMeResponse.java new file mode 100644 index 0000000..6c6c116 --- /dev/null +++ b/src/main/java/com/boilerplate/core/user/api/dto/res/UserMeResponse.java @@ -0,0 +1,13 @@ +package com.boilerplate.core.user.api.dto.res; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class UserMeResponse { + private final Long id; + private final String name; + private final String email; + private final String grade; +} diff --git a/src/main/java/com/boilerplate/core/user/application/GetUserMeUseCase.java b/src/main/java/com/boilerplate/core/user/application/GetUserMeUseCase.java new file mode 100644 index 0000000..79deb8d --- /dev/null +++ b/src/main/java/com/boilerplate/core/user/application/GetUserMeUseCase.java @@ -0,0 +1,34 @@ +package com.boilerplate.core.user.application; + +import com.boilerplate.core.auth.domain.entity.Account; +import com.boilerplate.core.auth.domain.entity.GradeHistory; +import com.boilerplate.core.auth.domain.repository.AccountRepository; +import com.boilerplate.core.auth.domain.repository.GradeHistoryRepository; +import com.boilerplate.core.exception.common.NotExistEntityException; +import com.boilerplate.core.user.api.dto.res.UserMeResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class GetUserMeUseCase { + + private final AccountRepository accountRepository; + private final GradeHistoryRepository gradeHistoryRepository; + + public UserMeResponse execute(Long accountId) { + Account account = accountRepository.findById(accountId) + .orElseThrow(() -> new NotExistEntityException(Account.class.getName())); + + String gradeName = gradeHistoryRepository.findCurrentActiveGrade(accountId) + .map(gh -> gh.getGrade().getName()) + .orElse("NORMAL"); // 기본값 + + return UserMeResponse.builder() + .id(account.getId()) + .name(account.getName()) + .email(account.getEmail()) + .grade(gradeName) + .build(); + } +}