zioinfo-esn/backend/src/main/java/com/zioinfo/esn/auth/AuthController.java
2026-07-04 08:37:02 +09:00

112 lines
5.1 KiB
Java

package com.zioinfo.esn.auth;
import com.zioinfo.esn.auth.dto.ChangePasswordRequest;
import com.zioinfo.esn.auth.dto.OtpConfirmRequest;
import com.zioinfo.esn.auth.dto.OtpSetupResponse;
import com.zioinfo.esn.auth.dto.OtpVerifyRequest;
import com.zioinfo.esn.common.ApiResponse;
import com.zioinfo.esn.uiws.auth.OtpAuthService;
import com.zioinfo.esn.uiws.auth.TwoFactorService;
import com.zioinfo.esn.uiws.common.UiwsApiException;
import com.zioinfo.esn.uiws.common.UiwsErrorCode;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* ESN 인증 컨트롤러.
* - /login: 2FA off 면 { twofa:"false", token, type }, 2FA on 이면 OTP({twofa,verifyToken,verifyMethod,...}) 또는 이메일코드.
* - /verify: (UIWS 2FA 이식) 이메일 verify-token + 코드 → access 발급.
* - /verify-otp: (TOTP 이식) verify-token + 6자리 → access 발급(최초 로그인이면 등록 확정).
* - /otp/setup·confirm·disable, /change-password: 마이페이지(본인 access 토큰 필요).
* 기존 클라이언트는 응답에 token 필드가 그대로 존재(2FA off 시) → 회귀 0.
*/
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
private final TwoFactorService twoFactorService;
private final OtpAuthService otpAuthService;
private final JwtUtil jwtUtil;
@PostMapping("/login")
public ApiResponse<Map<String, String>> login(@RequestBody LoginRequest req) {
return ApiResponse.ok(authService.login(req.username(), req.password()));
}
/** UIWS 2FA(이메일코드) 이식: 2차 인증 코드 검증 → access 토큰 발급. */
@PostMapping("/verify")
public ApiResponse<Map<String, String>> verify(@RequestBody VerifyRequest req) {
return ApiResponse.ok(twoFactorService.verify(req.verifyToken(), req.code()));
}
/** TOTP 이식: 로그인 2단계 6자리 검증 → access 토큰 발급(최초 로그인이면 등록 확정). */
@PostMapping("/verify-otp")
public ApiResponse<Map<String, String>> verifyOtp(@Valid @RequestBody OtpVerifyRequest req) {
return ApiResponse.ok(otpAuthService.verifyOtp(req.verifyToken(), req.code()));
}
@GetMapping("/me")
public ApiResponse<Map<String, String>> me(@RequestHeader("Authorization") String header) {
String token = header.replace("Bearer ", "");
return ApiResponse.ok(authService.me(token));
}
// ── 마이페이지: OTP 등록/재설정/해제 + 비밀번호 변경 (본인, access 토큰 필요) ──────────
// (/api/auth/** 는 permitAll 이라 필터 인증이 없으므로 여기서 access 토큰을 명시 검증.)
/** 마이페이지 OTP 등록/재설정 시작 → { secret, otpAuthUri, qrImage }(이 응답에서만 시크릿/QR 노출). */
@PostMapping("/otp/setup")
public ApiResponse<OtpSetupResponse> otpSetup(@RequestHeader("Authorization") String header) {
return ApiResponse.ok(otpAuthService.setup(requireUser(header)));
}
/** 마이페이지 OTP 확인·활성화(앱 6자리 코드). */
@PostMapping("/otp/confirm")
public ApiResponse<Map<String, String>> otpConfirm(@RequestHeader("Authorization") String header,
@Valid @RequestBody OtpConfirmRequest req) {
otpAuthService.confirm(requireUser(header), req.code());
return ApiResponse.ok(Map.of("result", "ok"));
}
/** 마이페이지 OTP 해제. */
@PostMapping("/otp/disable")
public ApiResponse<Map<String, String>> otpDisable(@RequestHeader("Authorization") String header) {
otpAuthService.disable(requireUser(header));
return ApiResponse.ok(Map.of("result", "ok"));
}
/** 마이페이지 비밀번호 변경(현재 비번 검증 + BCrypt). 새 비번은 응답/로그 미포함. */
@PostMapping("/change-password")
public ApiResponse<Map<String, String>> changePassword(@RequestHeader("Authorization") String header,
@Valid @RequestBody ChangePasswordRequest req) {
authService.changePassword(requireUser(header), req);
return ApiResponse.ok(Map.of("result", "ok"));
}
/**
* Authorization 헤더에서 인증 사용자를 해석한다. verify-token(2fa 단계용)은 거부(2FA 우회 차단).
*/
private String requireUser(String header) {
String token = header == null ? "" : header.replace("Bearer ", "").trim();
if (token.isEmpty() || jwtUtil.isVerifyToken(token) || !jwtUtil.isValid(token)) {
throw new UiwsApiException(UiwsErrorCode.UNAUTHORIZED);
}
return jwtUtil.getUsername(token);
}
@PostMapping("/logout")
public ApiResponse<Void> logout() {
// JWT stateless — 클라이언트에서 토큰 삭제
return ApiResponse.ok("로그아웃 성공", null);
}
record LoginRequest(String username, String password) {}
record VerifyRequest(String verifyToken, String code) {}
}