feat: 소셜로그인 및 유저 등급
This commit is contained in:
@@ -57,7 +57,7 @@ public class TokenProvider {
|
|||||||
/** 서명/만료/issuer 검증 */
|
/** 서명/만료/issuer 검증 */
|
||||||
public boolean validate(String token) {
|
public boolean validate(String token) {
|
||||||
try {
|
try {
|
||||||
parser.parseClaimsJws(stripBearer(token));
|
parser.parseClaimsJws(token);
|
||||||
return true;
|
return true;
|
||||||
} catch (JwtException | IllegalArgumentException e) {
|
} catch (JwtException | IllegalArgumentException e) {
|
||||||
return false;
|
return false;
|
||||||
@@ -94,7 +94,7 @@ public class TokenProvider {
|
|||||||
|
|
||||||
/** Claims 추출(검증 포함) */
|
/** Claims 추출(검증 포함) */
|
||||||
public Claims getClaims(String token) {
|
public Claims getClaims(String token) {
|
||||||
return parser.parseClaimsJws(stripBearer(token)).getBody();
|
return parser.parseClaimsJws(token).getBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSubject(String token) {
|
public String getSubject(String token) {
|
||||||
@@ -108,9 +108,4 @@ public class TokenProvider {
|
|||||||
public List<String> getRoles(String token) {
|
public List<String> getRoles(String token) {
|
||||||
return getClaims(token).get("roles", List.class);
|
return getClaims(token).get("roles", List.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String stripBearer(String token) {
|
|
||||||
if (token == null || !token.startsWith("Bearer ")) return null;
|
|
||||||
return token.substring(7);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,11 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
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
|
@Configuration
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -34,7 +39,8 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
|
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))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers(allAuthorizedUrls).permitAll()
|
.requestMatchers(allAuthorizedUrls).permitAll()
|
||||||
@@ -58,6 +64,21 @@ public class SecurityConfig {
|
|||||||
return http.build();
|
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
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
|
|||||||
@@ -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.LoginRequest;
|
||||||
import com.boilerplate.core.auth.api.dto.req.SignUpRequest;
|
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.LoginResponse;
|
||||||
import com.boilerplate.core.auth.api.dto.res.SignUpResponse;
|
import com.boilerplate.core.auth.api.dto.res.SignUpResponse;
|
||||||
import com.boilerplate.core.auth.application.LoginUseCase;
|
import com.boilerplate.core.auth.application.LoginUseCase;
|
||||||
import com.boilerplate.core.auth.application.RegisterUseCase;
|
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.command.RegisterUseCaseCommand;
|
||||||
import com.boilerplate.core.auth.application.dto.result.LoginResult;
|
import com.boilerplate.core.auth.application.dto.result.LoginResult;
|
||||||
import com.boilerplate.core.auth.application.dto.result.RegisterUseCaseResult;
|
import com.boilerplate.core.auth.application.dto.result.RegisterUseCaseResult;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -23,13 +26,16 @@ public class AuthController {
|
|||||||
|
|
||||||
private final RegisterUseCase registerUseCase;
|
private final RegisterUseCase registerUseCase;
|
||||||
private final LoginUseCase loginUseCase;
|
private final LoginUseCase loginUseCase;
|
||||||
|
private final SocialLoginUseCase socialLoginUseCase;
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<SignUpResponse> register(
|
public ResponseEntity<SignUpResponse> register(
|
||||||
@RequestBody SignUpRequest request
|
@RequestBody SignUpRequest request
|
||||||
) {
|
) {
|
||||||
RegisterUseCaseResult result = registerUseCase.execute(
|
RegisterUseCaseResult result = registerUseCase.execute(RegisterUseCaseCommand.builder()
|
||||||
RegisterUseCaseCommand.builder().loginId(request.getLoginId()).password(request.getPassword()).build());
|
.email(request.getEmail())
|
||||||
|
.password(request.getPassword())
|
||||||
|
.build());
|
||||||
|
|
||||||
SignUpResponse response = SignUpResponse.builder()
|
SignUpResponse response = SignUpResponse.builder()
|
||||||
.accessToken(result.getAccessToken())
|
.accessToken(result.getAccessToken())
|
||||||
@@ -43,7 +49,7 @@ public class AuthController {
|
|||||||
public ResponseEntity<LoginResponse> login(
|
public ResponseEntity<LoginResponse> login(
|
||||||
@RequestBody LoginRequest request
|
@RequestBody LoginRequest request
|
||||||
) {
|
) {
|
||||||
LoginResult result = loginUseCase.execute(request.getLoginId(), request.getPassword());
|
LoginResult result = loginUseCase.execute(request.getEmail(), request.getPassword());
|
||||||
LoginResponse response = LoginResponse.builder()
|
LoginResponse response = LoginResponse.builder()
|
||||||
.accessToken(result.getAccessToken())
|
.accessToken(result.getAccessToken())
|
||||||
.refreshToken(result.getRefreshToken())
|
.refreshToken(result.getRefreshToken())
|
||||||
@@ -51,4 +57,15 @@ public class AuthController {
|
|||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/social")
|
||||||
|
public ResponseEntity<LoginResponse> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import lombok.Setter;
|
|||||||
@Setter
|
@Setter
|
||||||
public class LoginRequest {
|
public class LoginRequest {
|
||||||
|
|
||||||
private String loginId;
|
private String email;
|
||||||
|
|
||||||
private String password;
|
private String password;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ import lombok.NoArgsConstructor;
|
|||||||
@Getter
|
@Getter
|
||||||
public class SignUpRequest {
|
public class SignUpRequest {
|
||||||
|
|
||||||
private String loginId;
|
private String email;
|
||||||
private String password;
|
private String password;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,8 +15,8 @@ public class LoginUseCase {
|
|||||||
private final AccountService accountService;
|
private final AccountService accountService;
|
||||||
private final TokenProvider tokenProvider;
|
private final TokenProvider tokenProvider;
|
||||||
|
|
||||||
public LoginResult execute(String loginId, String password) {
|
public LoginResult execute(String email, String password) {
|
||||||
long accountId = accountService.login(loginId, password);
|
long accountId = accountService.login(email, password);
|
||||||
|
|
||||||
String accessToken = tokenProvider.createUserAccessToken(accountId);
|
String accessToken = tokenProvider.createUserAccessToken(accountId);
|
||||||
String refreshToken = tokenProvider.createUserRefreshToken(accountId);
|
String refreshToken = tokenProvider.createUserRefreshToken(accountId);
|
||||||
|
|||||||
@@ -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.command.RegisterUseCaseCommand;
|
||||||
import com.boilerplate.core.auth.application.dto.result.RegisterUseCaseResult;
|
import com.boilerplate.core.auth.application.dto.result.RegisterUseCaseResult;
|
||||||
import com.boilerplate.core.auth.domain.service.AccountService;
|
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.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -18,15 +18,15 @@ public final class RegisterUseCase {
|
|||||||
private final TokenProvider tokenProvider;
|
private final TokenProvider tokenProvider;
|
||||||
|
|
||||||
public RegisterUseCaseResult execute(RegisterUseCaseCommand command) {
|
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) {
|
if (isExistEmail) {
|
||||||
throw new ExistLoginIdException(loginId);
|
throw new ExistEmailException(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
long accountId = accountService.register(loginId, command.getPassword());
|
long accountId = accountService.register(email, command.getPassword());
|
||||||
String accessToken = tokenProvider.createUserAccessToken(accountId);
|
String accessToken = tokenProvider.createUserAccessToken(accountId);
|
||||||
String refreshToken = tokenProvider.createUserRefreshToken(accountId);
|
String refreshToken = tokenProvider.createUserRefreshToken(accountId);
|
||||||
|
|
||||||
|
|||||||
@@ -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<SocialProviderClient> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
|
|||||||
@Getter
|
@Getter
|
||||||
public class RegisterUseCaseCommand {
|
public class RegisterUseCaseCommand {
|
||||||
|
|
||||||
private String loginId;
|
private String email;
|
||||||
|
|
||||||
private String password;
|
private String password;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ public final class User implements UserDetails {
|
|||||||
return accountId;
|
return accountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return accountId;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.boilerplate.core.auth.domain.entity;
|
package com.boilerplate.core.auth.domain.entity;
|
||||||
|
|
||||||
|
import com.boilerplate.core.auth.api.dto.req.SocialProvider;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.GenerationType;
|
import jakarta.persistence.GenerationType;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
@@ -24,10 +27,17 @@ public class Account {
|
|||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
private String loginId;
|
private String email;
|
||||||
private String passwordHash;
|
private String passwordHash;
|
||||||
private String name;
|
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
|
@Builder.Default
|
||||||
private LocalDateTime createdAt = LocalDateTime.now();
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.boilerplate.core.auth.domain.repository;
|
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 com.boilerplate.core.auth.domain.entity.Account;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
@@ -7,5 +8,6 @@ import org.springframework.stereotype.Repository;
|
|||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface AccountRepository extends JpaRepository<Account, Long> {
|
public interface AccountRepository extends JpaRepository<Account, Long> {
|
||||||
Optional<Account> findByLoginId(String loginId);
|
Optional<Account> findByEmail(String email);
|
||||||
|
Optional<Account> findByProviderAndProviderId(SocialProvider provider, String providerId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<GradeHistory, Long> {
|
||||||
|
|
||||||
|
@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<GradeHistory> findCurrentActiveGrade(@Param("accountId") Long accountId);
|
||||||
|
}
|
||||||
@@ -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<Grade, Long> {
|
||||||
|
Optional<Grade> findByName(String name);
|
||||||
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
package com.boilerplate.core.auth.domain.service;
|
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.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.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.auth.InvalidCredentialsException;
|
||||||
import com.boilerplate.core.exception.common.NotExistEntityException;
|
import com.boilerplate.core.exception.common.NotExistEntityException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -16,37 +22,78 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
public class AccountService {
|
public class AccountService {
|
||||||
|
|
||||||
private final AccountRepository accountRepository;
|
private final AccountRepository accountRepository;
|
||||||
|
private final GradeRepository gradeRepository;
|
||||||
|
private final GradeHistoryRepository gradeHistoryRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public long register(String loginId, String password) {
|
public long register(String email, String password) {
|
||||||
log.info("회원 등록: {}", loginId);
|
log.info("회원 등록: {}", email);
|
||||||
|
|
||||||
String hashedPassword = passwordEncoder.encode(password);
|
String hashedPassword = passwordEncoder.encode(password);
|
||||||
|
Grade normalGrade = gradeRepository.findByName("NORMAL")
|
||||||
|
.orElseThrow(() -> new NotExistEntityException(Grade.class.getName()));
|
||||||
|
|
||||||
Account account = Account.builder()
|
Account account = Account.builder()
|
||||||
.loginId(loginId)
|
.email(email)
|
||||||
.name("임시 이름")
|
.name("임시 이름")
|
||||||
.passwordHash(hashedPassword)
|
.passwordHash(hashedPassword)
|
||||||
|
.provider(SocialProvider.LOCAL)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
log.info("account: {}", account);
|
log.info("account: {}", account);
|
||||||
|
|
||||||
accountRepository.save(account);
|
accountRepository.save(account);
|
||||||
|
|
||||||
|
// 등급 이력 생성
|
||||||
|
GradeHistory gradeHistory = GradeHistory.builder()
|
||||||
|
.accountId(account.getId())
|
||||||
|
.grade(normalGrade)
|
||||||
|
.status("ACTIVE")
|
||||||
|
.build();
|
||||||
|
gradeHistoryRepository.save(gradeHistory);
|
||||||
|
|
||||||
return account.getId();
|
return account.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isExistLoginId(String loginId) {
|
@Transactional
|
||||||
log.info("로그인 아이디 존재 여부 조회");
|
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("로그인");
|
log.info("로그인");
|
||||||
|
|
||||||
Account account = accountRepository.findByLoginId(loginId)
|
Account account = accountRepository.findByEmail(email)
|
||||||
.orElseThrow(() -> new NotExistEntityException(Account.class.getName()));
|
.orElseThrow(() -> new NotExistEntityException(Account.class.getName()));
|
||||||
|
|
||||||
if (!passwordEncoder.matches(password, account.getPasswordHash())) {
|
if (!passwordEncoder.matches(password, account.getPasswordHash())) {
|
||||||
|
|||||||
@@ -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<String> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ResponseEntity<Map> response = restTemplate.exchange(
|
||||||
|
GOOGLE_USERINFO_URL,
|
||||||
|
HttpMethod.GET,
|
||||||
|
entity,
|
||||||
|
Map.class
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.http.ProblemDetail;
|
import org.springframework.http.ProblemDetail;
|
||||||
import org.springframework.web.ErrorResponseException;
|
import org.springframework.web.ErrorResponseException;
|
||||||
|
|
||||||
public class ExistLoginIdException extends ErrorResponseException {
|
public class ExistEmailException extends ErrorResponseException {
|
||||||
|
|
||||||
public ExistLoginIdException(String loginId) {
|
public ExistEmailException(String email) {
|
||||||
super(
|
super(
|
||||||
HttpStatus.CONFLICT,
|
HttpStatus.CONFLICT,
|
||||||
ProblemDetail.forStatusAndDetail(
|
ProblemDetail.forStatusAndDetail(
|
||||||
HttpStatus.CONFLICT,
|
HttpStatus.CONFLICT,
|
||||||
"이미 존재하는 loginId 입니다: " + loginId
|
"이미 존재하는 email 입니다: " + email
|
||||||
),
|
),
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@@ -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<UserMeResponse> getMe(@AuthenticationPrincipal User user) {
|
||||||
|
UserMeResponse response = getUserMeUseCase.execute(user.getId());
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user