Files
2026-03-13 00:39:46 +08:00

118 lines
4.5 KiB
Python

from __future__ import annotations
import logging
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from app.api.dependencies import AppState, get_current_user, get_db, get_state
from app.core.db import utc_now
from app.models.user import User
from app.schemas.api import (
LoginRequest,
LoginResponse,
PasswordChangeRequest,
PasswordResetConfirmRequest,
PasswordResetRequest,
PasswordResetRequestResponse,
StatusResponse,
)
router = APIRouter(prefix="/auth", tags=["auth"])
logger = logging.getLogger(__name__)
@router.post("/login", response_model=LoginResponse)
def login(payload: LoginRequest, state: AppState = Depends(get_state), db=Depends(get_db)) -> LoginResponse:
user = db.execute(select(User).where(User.username == payload.username)).scalar_one_or_none()
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误")
auth_service = state.auth_service
if auth_service.is_locked(user):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已锁定,请稍后再试")
if not auth_service.verify_password(payload.password, user.password_hash):
auth_service.register_failed_login(user)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误")
auth_service.clear_login_failures(user)
token_pair = auth_service.issue_token(user, remember=payload.remember_me)
return LoginResponse(
access_token=token_pair.access_token,
expires_at=token_pair.expires_at,
force_password_change=user.must_change_password,
)
@router.post("/change-password")
def change_password(
payload: PasswordChangeRequest,
state: AppState = Depends(get_state),
user: User = Depends(get_current_user),
) -> StatusResponse:
auth_service = state.auth_service
if not auth_service.verify_password(payload.current_password, user.password_hash):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="当前密码不正确")
if payload.new_password != payload.confirm_password:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="新密码不一致")
try:
auth_service.validate_new_password(user.username, payload.new_password)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
user.password_hash = auth_service.hash_password(payload.new_password)
user.must_change_password = False
user.last_login = utc_now()
return StatusResponse(status="ok")
@router.post("/reset-password/request", response_model=PasswordResetRequestResponse)
def request_password_reset(
payload: PasswordResetRequest,
state: AppState = Depends(get_state),
db=Depends(get_db),
) -> PasswordResetRequestResponse:
username = payload.username.strip()
user = db.execute(select(User).where(User.username == username)).scalar_one_or_none()
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
challenge = state.auth_service.create_password_reset_challenge(user.username)
logger.warning(
"Password reset verification code for user %s: %s (expires_at=%s, attempts=%s)",
user.username,
challenge.code,
challenge.expires_at.isoformat(),
challenge.attempts_remaining,
)
return PasswordResetRequestResponse(status="ok", expires_in_seconds=300)
@router.post("/reset-password/confirm", response_model=StatusResponse)
def confirm_password_reset(
payload: PasswordResetConfirmRequest,
state: AppState = Depends(get_state),
db=Depends(get_db),
) -> StatusResponse:
username = payload.username.strip()
user = db.execute(select(User).where(User.username == username)).scalar_one_or_none()
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
try:
state.auth_service.verify_password_reset_challenge(user.username, payload.verification_code)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
new_password = state.auth_service.generate_random_password()
user.password_hash = state.auth_service.hash_password(new_password)
user.must_change_password = True
state.auth_service.clear_lock_state(user)
logger.warning("Password reset success for user %s, new temporary password: %s", user.username, new_password)
return StatusResponse(status="ok")