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.gcsc.guide.service.ActivityService; import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; 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 @Tag(name = "01. 인증", description = "Google OAuth2 로그인 및 JWT 토큰 관리") 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; private final ActivityService activityService; @Operation(summary = "Google 로그인", description = "Google ID Token을 검증하고 JWT를 발급합니다. " + "신규 사용자는 PENDING 상태로 생성되며, htlee@gcsc.co.kr은 자동 ACTIVE + 관리자 부여됩니다.", security = {}) @ApiResponses({ @ApiResponse(responseCode = "200", description = "로그인 성공, JWT 토큰 발급", content = @Content(schema = @Schema(implementation = AuthResponse.class))), @ApiResponse(responseCode = "401", description = "유효하지 않은 Google 토큰 또는 허용되지 않은 이메일 도메인", content = @Content) }) @PostMapping("/google") public ResponseEntity googleLogin( @Valid @RequestBody GoogleLoginRequest request, HttpServletRequest httpRequest) { 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(); activityService.recordLogin( userWithRoles.getId(), httpRequest.getRemoteAddr(), httpRequest.getHeader("User-Agent")); String token = jwtTokenProvider.generateToken( userWithRoles.getId(), userWithRoles.getEmail(), userWithRoles.isAdmin()); return ResponseEntity.ok(new AuthResponse(token, UserResponse.from(userWithRoles))); } @Operation(summary = "현재 사용자 정보 조회", description = "JWT 토큰으로 인증된 현재 사용자의 상세 정보와 롤 목록을 반환합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "사용자 정보 조회 성공"), @ApiResponse(responseCode = "401", description = "인증 실패 (토큰 없음/만료)", content = @Content), @ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음", content = @Content) }) @SecurityRequirement(name = "Bearer JWT") @GetMapping("/me") public ResponseEntity 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)); } @Operation(summary = "로그아웃", description = "Stateless JWT 방식이므로 서버 측 처리 없이 204를 반환합니다. 클라이언트에서 토큰을 삭제하세요.") @ApiResponse(responseCode = "204", description = "로그아웃 성공") @SecurityRequirement(name = "Bearer JWT") @PostMapping("/logout") public ResponseEntity 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); } }