feat(auth): JWT 기반 Google 로그인 인증 API 구현 #1
96
src/main/java/com/gcsc/guide/auth/AuthController.java
Normal file
96
src/main/java/com/gcsc/guide/auth/AuthController.java
Normal file
@ -0,0 +1,96 @@
|
||||
package com.gcsc.guide.auth;
|
||||
|
||||
import com.gcsc.guide.dto.AuthResponse;
|
||||
import com.gcsc.guide.dto.GoogleLoginRequest;
|
||||
import com.gcsc.guide.dto.UserResponse;
|
||||
import com.gcsc.guide.entity.User;
|
||||
import com.gcsc.guide.repository.UserRepository;
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private static final String AUTO_ADMIN_EMAIL = "htlee@gcsc.co.kr";
|
||||
|
||||
private final GoogleTokenVerifier googleTokenVerifier;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* Google ID Token으로 로그인/회원가입 처리 후 JWT 발급
|
||||
*/
|
||||
@PostMapping("/google")
|
||||
public ResponseEntity<AuthResponse> googleLogin(@Valid @RequestBody GoogleLoginRequest request) {
|
||||
GoogleIdToken.Payload payload = googleTokenVerifier.verify(request.idToken());
|
||||
if (payload == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "유효하지 않은 Google 토큰입니다");
|
||||
}
|
||||
|
||||
String email = payload.getEmail();
|
||||
String name = (String) payload.get("name");
|
||||
String avatarUrl = (String) payload.get("picture");
|
||||
|
||||
userRepository.findByEmail(email)
|
||||
.ifPresentOrElse(
|
||||
existingUser -> {
|
||||
existingUser.updateProfile(name, avatarUrl);
|
||||
existingUser.updateLastLogin();
|
||||
userRepository.save(existingUser);
|
||||
},
|
||||
() -> createNewUser(email, name, avatarUrl)
|
||||
);
|
||||
|
||||
User userWithRoles = userRepository.findByEmailWithRoles(email)
|
||||
.orElseThrow();
|
||||
|
||||
String token = jwtTokenProvider.generateToken(
|
||||
userWithRoles.getId(), userWithRoles.getEmail(), userWithRoles.isAdmin());
|
||||
|
||||
return ResponseEntity.ok(new AuthResponse(token, UserResponse.from(userWithRoles)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자 정보 조회
|
||||
*/
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<UserResponse> getCurrentUser(Authentication authentication) {
|
||||
Long userId = (Long) authentication.getPrincipal();
|
||||
|
||||
User user = userRepository.findByIdWithRoles(userId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다"));
|
||||
|
||||
return ResponseEntity.ok(UserResponse.from(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃 (Stateless JWT이므로 서버 측 처리 없음, 프론트에서 토큰 삭제)
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<Void> logout() {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private User createNewUser(String email, String name, String avatarUrl) {
|
||||
User newUser = new User(email, name, avatarUrl);
|
||||
|
||||
if (AUTO_ADMIN_EMAIL.equals(email)) {
|
||||
newUser.activate();
|
||||
newUser.grantAdmin();
|
||||
log.info("관리자 자동 승인: {}", email);
|
||||
}
|
||||
|
||||
newUser.updateLastLogin();
|
||||
return userRepository.save(newUser);
|
||||
}
|
||||
}
|
||||
57
src/main/java/com/gcsc/guide/auth/GoogleTokenVerifier.java
Normal file
57
src/main/java/com/gcsc/guide/auth/GoogleTokenVerifier.java
Normal file
@ -0,0 +1,57 @@
|
||||
package com.gcsc.guide.auth;
|
||||
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
|
||||
import com.google.api.client.http.javanet.NetHttpTransport;
|
||||
import com.google.api.client.json.gson.GsonFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GoogleTokenVerifier {
|
||||
|
||||
private final GoogleIdTokenVerifier verifier;
|
||||
private final String allowedEmailDomain;
|
||||
|
||||
public GoogleTokenVerifier(
|
||||
@Value("${app.google.client-id}") String clientId,
|
||||
@Value("${app.allowed-email-domain}") String allowedEmailDomain
|
||||
) {
|
||||
this.verifier = new GoogleIdTokenVerifier.Builder(
|
||||
new NetHttpTransport(), GsonFactory.getDefaultInstance())
|
||||
.setAudience(Collections.singletonList(clientId))
|
||||
.build();
|
||||
this.allowedEmailDomain = allowedEmailDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google ID Token을 검증하고 페이로드를 반환한다.
|
||||
* 검증 실패 또는 허용되지 않은 이메일 도메인이면 null을 반환한다.
|
||||
*/
|
||||
public GoogleIdToken.Payload verify(String idTokenString) {
|
||||
try {
|
||||
GoogleIdToken idToken = verifier.verify(idTokenString);
|
||||
if (idToken == null) {
|
||||
log.warn("Google ID Token 검증 실패: 유효하지 않은 토큰");
|
||||
return null;
|
||||
}
|
||||
|
||||
GoogleIdToken.Payload payload = idToken.getPayload();
|
||||
String email = payload.getEmail();
|
||||
|
||||
if (email == null || !email.endsWith("@" + allowedEmailDomain)) {
|
||||
log.warn("허용되지 않은 이메일 도메인: {}", email);
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (Exception e) {
|
||||
log.error("Google ID Token 검증 중 오류: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package com.gcsc.guide.auth;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
|
||||
String token = extractToken(request);
|
||||
|
||||
if (token != null && jwtTokenProvider.validateToken(token)) {
|
||||
Long userId = jwtTokenProvider.getUserIdFromToken(token);
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(userId, token, List.of(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String extractToken(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
|
||||
return bearerToken.substring(BEARER_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
66
src/main/java/com/gcsc/guide/auth/JwtTokenProvider.java
Normal file
66
src/main/java/com/gcsc/guide/auth/JwtTokenProvider.java
Normal file
@ -0,0 +1,66 @@
|
||||
package com.gcsc.guide.auth;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
private final SecretKey secretKey;
|
||||
private final long expirationMs;
|
||||
|
||||
public JwtTokenProvider(
|
||||
@Value("${app.jwt.secret}") String secret,
|
||||
@Value("${app.jwt.expiration-ms}") long expirationMs
|
||||
) {
|
||||
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
this.expirationMs = expirationMs;
|
||||
}
|
||||
|
||||
public String generateToken(Long userId, String email, boolean isAdmin) {
|
||||
Date now = new Date();
|
||||
Date expiry = new Date(now.getTime() + expirationMs);
|
||||
|
||||
return Jwts.builder()
|
||||
.subject(userId.toString())
|
||||
.claim("email", email)
|
||||
.claim("isAdmin", isAdmin)
|
||||
.issuedAt(now)
|
||||
.expiration(expiry)
|
||||
.signWith(secretKey)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Long getUserIdFromToken(String token) {
|
||||
Claims claims = parseToken(token);
|
||||
return Long.parseLong(claims.getSubject());
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
parseToken(token);
|
||||
return true;
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
log.debug("JWT 토큰 검증 실패: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Claims parseToken(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
}
|
||||
@ -1,28 +1,65 @@
|
||||
package com.gcsc.guide.config;
|
||||
|
||||
import com.gcsc.guide.auth.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
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
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Value("${app.cors.allowed-origins:http://localhost:5173,https://guide.gc-si.dev}")
|
||||
private List<String> allowedOrigins;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session ->
|
||||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/auth/**", "/actuator/health", "/h2-console/**").permitAll()
|
||||
.requestMatchers(
|
||||
"/api/auth/**",
|
||||
"/api/health",
|
||||
"/actuator/health",
|
||||
"/h2-console/**"
|
||||
).permitAll()
|
||||
.requestMatchers("/api/admin/**").authenticated()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));
|
||||
.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()))
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowedOrigins(allowedOrigins);
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*"));
|
||||
config.setAllowCredentials(true);
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/api/**", config);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
7
src/main/java/com/gcsc/guide/dto/AuthResponse.java
Normal file
7
src/main/java/com/gcsc/guide/dto/AuthResponse.java
Normal file
@ -0,0 +1,7 @@
|
||||
package com.gcsc.guide.dto;
|
||||
|
||||
public record AuthResponse(
|
||||
String token,
|
||||
UserResponse user
|
||||
) {
|
||||
}
|
||||
8
src/main/java/com/gcsc/guide/dto/GoogleLoginRequest.java
Normal file
8
src/main/java/com/gcsc/guide/dto/GoogleLoginRequest.java
Normal file
@ -0,0 +1,8 @@
|
||||
package com.gcsc.guide.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record GoogleLoginRequest(
|
||||
@NotBlank String idToken
|
||||
) {
|
||||
}
|
||||
27
src/main/java/com/gcsc/guide/dto/RoleResponse.java
Normal file
27
src/main/java/com/gcsc/guide/dto/RoleResponse.java
Normal file
@ -0,0 +1,27 @@
|
||||
package com.gcsc.guide.dto;
|
||||
|
||||
import com.gcsc.guide.entity.Role;
|
||||
import com.gcsc.guide.entity.RoleUrlPattern;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record RoleResponse(
|
||||
Long id,
|
||||
String name,
|
||||
String description,
|
||||
List<String> urlPatterns
|
||||
) {
|
||||
|
||||
public static RoleResponse from(Role role) {
|
||||
List<String> patterns = role.getUrlPatterns().stream()
|
||||
.map(RoleUrlPattern::getUrlPattern)
|
||||
.toList();
|
||||
|
||||
return new RoleResponse(
|
||||
role.getId(),
|
||||
role.getName(),
|
||||
role.getDescription(),
|
||||
patterns
|
||||
);
|
||||
}
|
||||
}
|
||||
37
src/main/java/com/gcsc/guide/dto/UserResponse.java
Normal file
37
src/main/java/com/gcsc/guide/dto/UserResponse.java
Normal file
@ -0,0 +1,37 @@
|
||||
package com.gcsc.guide.dto;
|
||||
|
||||
import com.gcsc.guide.entity.User;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record UserResponse(
|
||||
Long id,
|
||||
String email,
|
||||
String name,
|
||||
String avatarUrl,
|
||||
String status,
|
||||
boolean isAdmin,
|
||||
List<RoleResponse> roles,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime lastLoginAt
|
||||
) {
|
||||
|
||||
public static UserResponse from(User user) {
|
||||
List<RoleResponse> roles = user.getRoles().stream()
|
||||
.map(RoleResponse::from)
|
||||
.toList();
|
||||
|
||||
return new UserResponse(
|
||||
user.getId(),
|
||||
user.getEmail(),
|
||||
user.getName(),
|
||||
user.getAvatarUrl(),
|
||||
user.getStatus().name(),
|
||||
user.isAdmin(),
|
||||
roles,
|
||||
user.getCreatedAt(),
|
||||
user.getLastLoginAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
47
src/main/java/com/gcsc/guide/entity/Role.java
Normal file
47
src/main/java/com/gcsc/guide/entity/Role.java
Normal file
@ -0,0 +1,47 @@
|
||||
package com.gcsc.guide.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "roles")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class Role {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String name;
|
||||
|
||||
@Column(length = 255)
|
||||
private String description;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@OneToMany(mappedBy = "role", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<RoleUrlPattern> urlPatterns = new ArrayList<>();
|
||||
|
||||
public Role(String name, String description) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public void update(String name, String description) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
38
src/main/java/com/gcsc/guide/entity/RoleUrlPattern.java
Normal file
38
src/main/java/com/gcsc/guide/entity/RoleUrlPattern.java
Normal file
@ -0,0 +1,38 @@
|
||||
package com.gcsc.guide.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "role_url_patterns")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class RoleUrlPattern {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "role_id", nullable = false)
|
||||
private Role role;
|
||||
|
||||
@Column(name = "url_pattern", nullable = false, length = 255)
|
||||
private String urlPattern;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public RoleUrlPattern(Role role, String urlPattern) {
|
||||
this.role = role;
|
||||
this.urlPattern = urlPattern;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
103
src/main/java/com/gcsc/guide/entity/User.java
Normal file
103
src/main/java/com/gcsc/guide/entity/User.java
Normal file
@ -0,0 +1,103 @@
|
||||
package com.gcsc.guide.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String name;
|
||||
|
||||
@Column(name = "avatar_url", length = 500)
|
||||
private String avatarUrl;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 20)
|
||||
private UserStatus status = UserStatus.PENDING;
|
||||
|
||||
@Column(name = "is_admin", nullable = false)
|
||||
private boolean isAdmin = false;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "last_login_at")
|
||||
private LocalDateTime lastLoginAt;
|
||||
|
||||
@ManyToMany(fetch = FetchType.LAZY)
|
||||
@JoinTable(
|
||||
name = "user_roles",
|
||||
joinColumns = @JoinColumn(name = "user_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "role_id")
|
||||
)
|
||||
private Set<Role> roles = new HashSet<>();
|
||||
|
||||
public User(String email, String name, String avatarUrl) {
|
||||
this.email = email;
|
||||
this.name = name;
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public void activate() {
|
||||
this.status = UserStatus.ACTIVE;
|
||||
}
|
||||
|
||||
public void reject() {
|
||||
this.status = UserStatus.REJECTED;
|
||||
}
|
||||
|
||||
public void disable() {
|
||||
this.status = UserStatus.DISABLED;
|
||||
}
|
||||
|
||||
public void grantAdmin() {
|
||||
this.isAdmin = true;
|
||||
}
|
||||
|
||||
public void revokeAdmin() {
|
||||
this.isAdmin = false;
|
||||
}
|
||||
|
||||
public void updateLastLogin() {
|
||||
this.lastLoginAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void updateProfile(String name, String avatarUrl) {
|
||||
this.name = name;
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public void updateRoles(Set<Role> roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
8
src/main/java/com/gcsc/guide/entity/UserStatus.java
Normal file
8
src/main/java/com/gcsc/guide/entity/UserStatus.java
Normal file
@ -0,0 +1,8 @@
|
||||
package com.gcsc.guide.entity;
|
||||
|
||||
public enum UserStatus {
|
||||
PENDING,
|
||||
ACTIVE,
|
||||
REJECTED,
|
||||
DISABLED
|
||||
}
|
||||
19
src/main/java/com/gcsc/guide/repository/RoleRepository.java
Normal file
19
src/main/java/com/gcsc/guide/repository/RoleRepository.java
Normal file
@ -0,0 +1,19 @@
|
||||
package com.gcsc.guide.repository;
|
||||
|
||||
import com.gcsc.guide.entity.Role;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface RoleRepository extends JpaRepository<Role, Long> {
|
||||
|
||||
Optional<Role> findByName(String name);
|
||||
|
||||
@Query("SELECT DISTINCT r FROM Role r LEFT JOIN FETCH r.urlPatterns")
|
||||
List<Role> findAllWithUrlPatterns();
|
||||
|
||||
@Query("SELECT r FROM Role r LEFT JOIN FETCH r.urlPatterns WHERE r.id = :id")
|
||||
Optional<Role> findByIdWithUrlPatterns(Long id);
|
||||
}
|
||||
24
src/main/java/com/gcsc/guide/repository/UserRepository.java
Normal file
24
src/main/java/com/gcsc/guide/repository/UserRepository.java
Normal file
@ -0,0 +1,24 @@
|
||||
package com.gcsc.guide.repository;
|
||||
|
||||
import com.gcsc.guide.entity.User;
|
||||
import com.gcsc.guide.entity.UserStatus;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles r LEFT JOIN FETCH r.urlPatterns WHERE u.id = :id")
|
||||
Optional<User> findByIdWithRoles(Long id);
|
||||
|
||||
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles r LEFT JOIN FETCH r.urlPatterns WHERE u.email = :email")
|
||||
Optional<User> findByEmailWithRoles(String email);
|
||||
|
||||
List<User> findByStatus(UserStatus status);
|
||||
|
||||
long countByStatus(UserStatus status);
|
||||
}
|
||||
@ -8,10 +8,15 @@ spring:
|
||||
|
||||
jpa:
|
||||
open-in-view: false
|
||||
defer-datasource-initialization: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
|
||||
jackson:
|
||||
serialization:
|
||||
write-dates-as-timestamps: false
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:8080}
|
||||
|
||||
@ -23,6 +28,8 @@ app:
|
||||
google:
|
||||
client-id: ${GOOGLE_CLIENT_ID:}
|
||||
allowed-email-domain: gcsc.co.kr
|
||||
cors:
|
||||
allowed-origins: ${CORS_ORIGINS:http://localhost:5173,https://guide.gc-si.dev}
|
||||
|
||||
# Actuator
|
||||
management:
|
||||
|
||||
11
src/main/resources/data.sql
Normal file
11
src/main/resources/data.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- 초기 롤 시드 데이터
|
||||
INSERT INTO roles (name, description, created_at) VALUES
|
||||
('ADMIN', '전체 접근 권한 (관리자 페이지 포함)', NOW()),
|
||||
('DEVELOPER', '전체 개발 가이드 접근', NOW()),
|
||||
('FRONT_DEV', '프론트엔드 개발 가이드만', NOW());
|
||||
|
||||
-- 롤별 URL 패턴
|
||||
INSERT INTO role_url_patterns (role_id, url_pattern, created_at) VALUES
|
||||
((SELECT id FROM roles WHERE name = 'ADMIN'), '/**', NOW()),
|
||||
((SELECT id FROM roles WHERE name = 'DEVELOPER'), '/dev/**', NOW()),
|
||||
((SELECT id FROM roles WHERE name = 'FRONT_DEV'), '/dev/front/**', NOW());
|
||||
불러오는 중...
Reference in New Issue
Block a user