Files
border-red-post-nalati/backend/app/api/routes/auth.py
T

123 lines
3.8 KiB
Python

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)))