package com.mydreamjob.controller; import com.mydreamjob.auth.AuthService; import com.mydreamjob.auth.AuthSessionService; import com.mydreamjob.auth.InvalidTokenException; import com.mydreamjob.auth.LoginRequest; import com.mydreamjob.auth.SmsVerificationService; import com.mydreamjob.auth.dto.RefreshTokenRequest; import com.mydreamjob.auth.dto.TokenPairResponse; import com.mydreamjob.auth.dto.PasswordChangeRequest; import com.mydreamjob.auth.dto.PasswordLoginRequest; import com.mydreamjob.auth.dto.PasswordOperationResponse; import com.mydreamjob.auth.dto.PasswordResetRequest; import com.mydreamjob.auth.dto.PasswordSetRequest; import com.mydreamjob.auth.password.PasswordAuthenticationService; import com.mydreamjob.auth.company.CompanyRegistrationRequest; import com.mydreamjob.auth.company.CompanyRegistrationResponse; import com.mydreamjob.auth.company.CompanyRegistrationService; import com.mydreamjob.audit.AuditContext; import com.mydreamjob.common.api.ApiResponse; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import com.mydreamjob.security.CurrentUserAccessor; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.Map; /** * 认证相关接口 * *

S02:新增 {@code /auth/refresh} 与 {@code /auth/logout}。Controller 只做参数接收、 * 身份解析和响应包装,不计算摘要、不解析 JWT、不访问 Repository,也不重复实现会话逻辑。

*/ @Tag(name = "认证", description = "登录、验证码、Token 刷新与登出、用户信息") @RestController @RequestMapping("/auth") @RequiredArgsConstructor @Validated public class AuthController { /** R01-A6:requestId 只来自过滤器规范化后的唯一产出点。 */ private final com.mydreamjob.common.logging.RequestIdAccessor requestIdAccessor; private final AuthService authService; private final SmsVerificationService smsVerificationService; private final CurrentUserAccessor currentUserAccessor; private final PasswordAuthenticationService passwordAuthenticationService; private final CompanyRegistrationService companyRegistrationService; @Operation(summary = "企业注册", description = "使用 company-register 短信验证码创建企业账号和企业资料") @PostMapping("/company/register") public ResponseEntity> companyRegister( @Valid @RequestBody CompanyRegistrationRequest body, HttpServletRequest request) { CompanyRegistrationResponse response = companyRegistrationService.register(body, AuditContext.from(request)); return ResponseEntity.ok(ApiResponse.ok(response, requestIdAccessor.get(request))); } @Operation(summary = "发送短信验证码", description = "开发/测试使用 Mock 发送实现;生产环境未接入短信网关时返回服务不可用。验证码不会写入日志。") @PostMapping("/sms/send") public ResponseEntity>> sendSmsCode( @RequestParam @NotBlank(message = "phone is required") @Pattern(regexp = "[0-9]{11}", message = "phone must contain exactly 11 digits") String phone, @RequestParam(defaultValue = "login") String purpose, HttpServletRequest request) { try { smsVerificationService.sendCode(phone, purpose, requestIdAccessor.get(request), request.getRemoteAddr(), request.getHeader("User-Agent")); return ResponseEntity.ok(ApiResponse.ok( Map.of("message", "验证码已发送(Mock)"), requestIdAccessor.get(request))); } catch (ConstraintViolationException e) { return ResponseEntity.badRequest().body( ApiResponse.badRequest(e.getMessage(), requestIdAccessor.get(request))); } catch (Exception e) { return ResponseEntity.badRequest().body( ApiResponse.error(400, e.getMessage(), requestIdAccessor.get(request))); } } @Operation(summary = "Mock 登录", description = "使用手机号+验证码登录,返回 Access/Refresh Token 并创建服务端登录会话") @PostMapping("/login") public ResponseEntity>> mockLogin( @Valid @RequestBody LoginRequest body, HttpServletRequest request) { String phone = body.getPhone(); String code = body.getCode(); String purpose = body.getPurpose() == null || body.getPurpose().isBlank() ? "login" : body.getPurpose(); try { Map result = authService.mockLogin(phone, code, purpose, requestIdAccessor.get(request), request.getRemoteAddr(), request.getHeader("User-Agent")); return ResponseEntity.ok(ApiResponse.ok(result, requestIdAccessor.get(request))); } catch (BusinessException e) { // 已注销手机号:保留 HTTP 409 / 业务 code=9303,交由全局异常处理器映射。 // 其余业务异常(如验证码错误、参数校验)维持既有 400/code=400 行为。 if (e.getErrorCodeInt() == ErrorCode.ACCOUNT_DELETED.getCode()) { throw e; } return ResponseEntity.badRequest().body( ApiResponse.error(400, e.getMessage(), requestIdAccessor.get(request))); } catch (Exception e) { return ResponseEntity.badRequest().body( ApiResponse.error(400, e.getMessage(), requestIdAccessor.get(request))); } } @Operation(summary = "手机号密码登录", description = "使用手机号和密码登录,返回 Access/Refresh Token;不自动创建账号") @PostMapping("/password/login") public ResponseEntity>> passwordLogin( @Valid @RequestBody PasswordLoginRequest body, HttpServletRequest request) { try { return ResponseEntity.ok(ApiResponse.ok(passwordAuthenticationService.login( body.getPhone(), body.getPassword(), requestIdAccessor.get(request), request.getRemoteAddr(), request.getHeader("User-Agent")), requestIdAccessor.get(request))); } catch (BusinessException e) { if (e.getErrorCodeInt() == ErrorCode.SERVICE_UNAVAILABLE.getCode()) { return ResponseEntity.status(503).body(ApiResponse.error(503, "authentication service temporarily unavailable", requestIdAccessor.get(request))); } throw e; } } @Operation(summary = "首次设置密码") @PostMapping("/password/set") public ResponseEntity> setPassword( @Valid @RequestBody PasswordSetRequest body, HttpServletRequest request) { try { Long userId = currentUserAccessor.requireUserId(); PasswordAuthenticationService.PasswordOperationResult result = passwordAuthenticationService.setPassword( userId, body.getSmsCode(), body.getNewPassword(), requestIdAccessor.get(request), request.getRemoteAddr(), request.getHeader("User-Agent")); return ResponseEntity.ok(ApiResponse.ok( new PasswordOperationResponse(result.passwordConfigured(), result.reauthenticationRequired()), requestIdAccessor.get(request))); } catch (BusinessException e) { if (e.getErrorCodeInt() == ErrorCode.SERVICE_UNAVAILABLE.getCode()) { return ResponseEntity.status(503).body(ApiResponse.error(503, "authentication service temporarily unavailable", requestIdAccessor.get(request))); } throw e; } } @Operation(summary = "修改密码") @PostMapping("/password/change") public ResponseEntity> changePassword( @Valid @RequestBody PasswordChangeRequest body, HttpServletRequest request) { try { Long userId = currentUserAccessor.requireUserId(); PasswordAuthenticationService.PasswordOperationResult result = passwordAuthenticationService.changePassword( userId, body.getCurrentPassword(), body.getNewPassword(), requestIdAccessor.get(request), request.getRemoteAddr(), request.getHeader("User-Agent")); return ResponseEntity.ok(ApiResponse.ok( new PasswordOperationResponse(result.passwordConfigured(), result.reauthenticationRequired()), requestIdAccessor.get(request))); } catch (BusinessException e) { if (e.getErrorCodeInt() == ErrorCode.SERVICE_UNAVAILABLE.getCode()) { return ResponseEntity.status(503).body(ApiResponse.error(503, "authentication service temporarily unavailable", requestIdAccessor.get(request))); } throw e; } } @Operation(summary = "短信找回并重置密码") @PostMapping("/password/reset") public ResponseEntity> resetPassword( @Valid @RequestBody PasswordResetRequest body, HttpServletRequest request) { try { PasswordAuthenticationService.PasswordOperationResult result = passwordAuthenticationService.resetPassword( body.getPhone(), body.getSmsCode(), body.getNewPassword(), requestIdAccessor.get(request), request.getRemoteAddr(), request.getHeader("User-Agent")); return ResponseEntity.ok(ApiResponse.ok( new PasswordOperationResponse(result.passwordConfigured(), result.reauthenticationRequired()), requestIdAccessor.get(request))); } catch (BusinessException e) { if (e.getErrorCodeInt() == ErrorCode.SERVICE_UNAVAILABLE.getCode()) { return ResponseEntity.status(503).body(ApiResponse.error(503, "authentication service temporarily unavailable", requestIdAccessor.get(request))); } throw e; } } @Operation(summary = "刷新 Token", description = "使用 Refresh Token 换取新的 Access/Refresh Token 对;旧 Refresh Token 立即失效") @PostMapping("/refresh") public ResponseEntity> refresh( @Valid @RequestBody RefreshTokenRequest body, HttpServletRequest request) { try { AuthSessionService.SessionTokens tokens = authService.refresh( body.getRefreshToken(), requestIdAccessor.get(request), request.getRemoteAddr(), request.getHeader("User-Agent")); return ResponseEntity.ok(ApiResponse.ok( new TokenPairResponse(tokens.accessToken(), tokens.refreshToken(), AuthService.EXPIRES_IN_SECONDS), requestIdAccessor.get(request))); } catch (InvalidTokenException e) { // 统一 401:不说明 Token 不存在、已消费、已撤销还是重放,也不回显任何 Token。 return unauthorized(request); } } @Operation(summary = "登出", description = "撤销当前登录会话;需要 Access Token,不接受任何请求体") @PostMapping("/logout") public ResponseEntity>> logout(HttpServletRequest request) { // 身份与会话一律来自通过校验的 Access Token,不接受客户端传入 userId/sessionId。 Long userId = currentUserAccessor.requireUserId(); String sessionId = currentUserAccessor.requireSessionId(); authService.logout(userId, sessionId, requestIdAccessor.get(request), request.getRemoteAddr(), request.getHeader("User-Agent")); return ResponseEntity.ok(ApiResponse.ok( Map.of("message", "logged out"), requestIdAccessor.get(request))); } @Operation(summary = "当前用户信息", description = "需要 JWT 认证") @GetMapping("/me") public ResponseEntity>> me(HttpServletRequest request) { Long userId = currentUserAccessor.requireUserId(); return ResponseEntity.ok(ApiResponse.ok(authService.getCurrentUser(userId), requestIdAccessor.get(request))); } private ResponseEntity> unauthorized(HttpServletRequest request) { return ResponseEntity.status(401).body( ApiResponse.unauthorized("invalid or expired token", requestIdAccessor.get(request))); } }