from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response from sqlalchemy import delete, select from sqlalchemy.orm import Session from app.db.session import get_db from app.models.auth import AuthSession, User from app.schemas.auth import ( LoginInput, PasswordChangeInput, ProfileInput, RegisterInput, SessionRead, UserRead, ) from app.services.auth import ( PASSWORD_HASHER, CurrentSession, check_password, clear_cookie, dummy_hash, issue_session, limit_auth_requests, require_admin, require_origin, require_session, ) router = APIRouter(tags=["auth"]) @router.post("/auth/register", response_model=UserRead, status_code=201) def register( payload: RegisterInput, request: Request, db: Session = Depends(get_db), origin=Depends(require_origin), ): """Registration never accepts role, ID, password hash or credential version fields.""" limit_auth_requests(db, request) user = User( username=payload.username, password_hash=PASSWORD_HASHER.hash(payload.password.get_secret_value()), nickname=payload.nickname, email=str(payload.email) if payload.email else None, role="user", ) db.add(user) db.commit() return user @router.post("/auth/login", response_model=SessionRead) def login( payload: LoginInput, request: Request, response: Response, db: Session = Depends(get_db), origin=Depends(require_origin), ): limit_auth_requests(db, request, payload.username) user = db.scalar(select(User).where(User.username == payload.username).with_for_update()) encoded = user.password_hash if user else dummy_hash() if not check_password(encoded, payload.password.get_secret_value()) or user is None: raise HTTPException(401, "用户名或密码错误") if PASSWORD_HASHER.check_needs_rehash(user.password_hash): user.password_hash = PASSWORD_HASHER.hash(payload.password.get_secret_value()) return issue_session(db, user, request, response) @router.get("/auth/me", response_model=SessionRead) def me(current: CurrentSession = Depends(require_session)): return current.response() @router.post("/auth/logout", status_code=204) def logout( response: Response, current: CurrentSession = Depends(require_session), db: Session = Depends(get_db), ): db.delete(current.session) db.commit() clear_cookie(response) @router.patch("/users/me", response_model=UserRead) def update_profile( payload: ProfileInput, current: CurrentSession = Depends(require_session), db: Session = Depends(get_db), ): if not payload.model_fields_set: raise HTTPException(422, "请至少填写一个可修改字段") for key, value in payload.model_dump(exclude_unset=True, mode="json").items(): setattr(current.user, key, value) db.commit() return current.user @router.post("/auth/change-password", status_code=204) def change_password( payload: PasswordChangeInput, response: Response, current: CurrentSession = Depends(require_session), db: Session = Depends(get_db), ): if not check_password(current.user.password_hash, payload.current_password.get_secret_value()): raise HTTPException(400, "当前密码不正确") current.user.password_hash = PASSWORD_HASHER.hash(payload.new_password.get_secret_value()) current.user.credential_version += 1 db.execute(delete(AuthSession).where(AuthSession.user_id == current.user.id)) db.commit() clear_cookie(response) @router.get("/admin/users", response_model=list[UserRead], dependencies=[Depends(require_admin)]) def list_users( limit: int = Query(100, ge=1, le=100), offset: int = Query(0, ge=0), db: Session = Depends(get_db), ): return list(db.scalars(select(User).order_by(User.id).offset(offset).limit(limit)))