123 lines
3.8 KiB
Python
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)))
|