package com.zioinfo.mes.auth; import com.zioinfo.mes.auth.dto.ChangePasswordRequest; import com.zioinfo.mes.auth.dto.OtpConfirmRequest; import com.zioinfo.mes.auth.dto.OtpSetupResponse; import com.zioinfo.mes.auth.dto.OtpVerifyRequest; import com.zioinfo.mes.common.ApiResponse; import com.zioinfo.mes.uiws.auth.OtpAuthService; import com.zioinfo.mes.uiws.auth.TwoFactorService; import com.zioinfo.mes.uiws.common.UiwsApiException; import com.zioinfo.mes.uiws.common.UiwsErrorCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.Map; /** * MES 인증 컨트롤러. * - /login: 2FA off 면 { token, type, twofa:"false" }, 2FA on 이면 { twofa:"true", verifyToken, verifyMethod|step, ... }. * - /verify: (UIWS 2FA 이식) verify-token + 이메일코드 → access/refresh 발급. * - /verify-otp: (TOTP 이식) verify-token + 6자리 → access/refresh 발급(최초 로그인이면 등록 확정). * 기존 클라이언트(2FA off)는 응답 형태 token 보존 → 회귀 0. */ @RestController @RequestMapping("/api/mes/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> login(@RequestBody LoginRequest req) { return ApiResponse.ok(authService.login(req.username(), req.password())); } /** UIWS 2FA(이메일코드) 이식: 2차 인증 코드 검증 → access/refresh 발급. */ @PostMapping("/verify") public ApiResponse> verify(@RequestBody VerifyRequest req) { return ApiResponse.ok(twoFactorService.verify(req.verifyToken(), req.code())); } /** TOTP 이식: 로그인 2단계 6자리 검증 → access/refresh 발급(최초 로그인이면 등록 확정). */ @PostMapping("/verify-otp") public ApiResponse> verifyOtp(@Valid @RequestBody OtpVerifyRequest req) { return ApiResponse.ok(otpAuthService.verifyOtp(req.verifyToken(), req.code())); } @GetMapping("/me") public ApiResponse> me(@RequestHeader("Authorization") String header) { String token = header.replace("Bearer ", ""); return ApiResponse.ok(authService.me(token)); } // ── 마이페이지: OTP 등록/재설정/해제 + 비밀번호 변경 (본인, access 토큰 필요) ────────── /** 마이페이지 OTP 등록/재설정 시작 → { secret, otpAuthUri, qrImage }(이 응답에서만 시크릿/QR 노출). */ @PostMapping("/otp/setup") public ApiResponse otpSetup(@RequestHeader("Authorization") String header) { return ApiResponse.ok(otpAuthService.setup(requireUser(header))); } /** 마이페이지 OTP 확인·활성화(앱 6자리 코드). */ @PostMapping("/otp/confirm") public ApiResponse> 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> otpDisable(@RequestHeader("Authorization") String header) { otpAuthService.disable(requireUser(header)); return ApiResponse.ok(Map.of("result", "ok")); } /** 마이페이지 비밀번호 변경(현재 비번 검증 + BCrypt). 새 비번은 응답/로그 미포함. */ @PostMapping("/change-password") public ApiResponse> 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 단계용)은 거부. * (/api/mes/auth/** 는 permitAll 이라 필터 인증이 없으므로 여기서 access 토큰을 명시 검증.) */ 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); } record LoginRequest(String username, String password) {} record VerifyRequest(String verifyToken, String code) {} }