"""认证接口:验证码、登录、登出、当前用户。""" from typing import Annotated from fastapi import APIRouter, Depends, Request from app.api.deps import DbSession, client_ip, get_current_user, get_raw_token from app.core.config import get_settings from app.core.response import ApiResponse, ok from app.models import User from app.schemas.auth import CaptchaOut, CurrentUserOut, LoginIn, LoginOut from app.services import auth_service, captcha_service router = APIRouter(prefix="/auth", tags=["认证"]) @router.get("/captcha", response_model=ApiResponse[CaptchaOut], summary="获取图形验证码") def get_captcha() -> ApiResponse[CaptchaOut]: """签发图形验证码。无需认证。 图片以 base64 内联返回而非单独 URL:保证 id 与图片成对产生, 避免前端分两次请求时拿到不匹配的组合。 """ captcha_id, image_base64, expires_in = captcha_service.issue() return ok(CaptchaOut(captcha_id=captcha_id, image_base64=image_base64, expires_in=expires_in)) @router.post("/login", response_model=ApiResponse[LoginOut], summary="登录") def login(payload: LoginIn, request: Request, session: DbSession) -> ApiResponse[LoginOut]: """账号 + 密码 + 图形验证码登录。 失败一律返回 **400 或 403,绝不使用 401**:前端的 401 拦截器会清空令牌 并跳转登录页,若登录接口自身返回 401,用户输错一次密码就会触发 「清空 + 跳转 + 报错」的组合动作,登录页反复重挂载、验证码反复刷新。 另注意:验证码经 GETDEL 消费后即失效,**前端必须在登录失败后刷新验证码**, 否则第二次提交必然报「验证码错误」。 """ result = auth_service.login( session, username=payload.username, password=payload.password, captcha_id=payload.captcha_id, captcha_code=payload.captcha_code, ip=client_ip(request), user_agent=request.headers.get("user-agent"), ) return ok(result, message="登录成功") @router.post("/logout", response_model=ApiResponse[None], summary="登出") def logout( token: Annotated[str, Depends(get_raw_token)], _current_user: Annotated[User, Depends(get_current_user)], ) -> ApiResponse[None]: """登出:把当前令牌加入黑名单,使其立即失效。 同时依赖 `get_current_user` 做真实鉴权 —— 仅凭原始令牌字符串不能证明 它是有效的(可能已过期或已被拉黑)。 """ auth_service.logout(token) return ok(message="已退出登录") @router.get("/me", response_model=ApiResponse[CurrentUserOut], summary="当前登录用户") def me(current_user: Annotated[User, Depends(get_current_user)]) -> ApiResponse[CurrentUserOut]: """返回当前登录用户的信息与角色。前端刷新页面后用它恢复会话。""" from app.services.authorization_service import session_info, user_session authorization = session_info(user_session(current_user), current_user) return ok( CurrentUserOut( id=current_user.id, username=current_user.username, real_name=current_user.real_name, **authorization, payroll_approval_enabled=get_settings().payroll_approval_enabled, last_login_time=current_user.last_login_time, ) )