feat: add cookie sessions and backend role authorization

This commit is contained in:
2026-10-16 07:51:59 +08:00
parent 411eadb431
commit 3f25ffab83
28 changed files with 1186 additions and 31 deletions
+122
View File
@@ -0,0 +1,122 @@
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)))
+2 -1
View File
@@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models import Document, KnowledgeItem, Source
from app.schemas.catalog import AuditRead, DocumentCreate, DocumentRead, DocumentUpdate, Page
from app.services.auth import require_admin
from app.services.catalog import (
ensure_version,
get_item,
@@ -15,7 +16,7 @@ from app.services.catalog import (
update_values,
)
router = APIRouter(prefix="/documents", tags=["documents"])
router = APIRouter(prefix="/documents", tags=["documents"], dependencies=[Depends(require_admin)])
@router.get("", response_model=Page[DocumentRead])
+2 -1
View File
@@ -7,6 +7,7 @@ from app.models import KnowledgeItem
from app.models.catalog import utc_now
from app.schemas.catalog import AuditRead, Page, Status
from app.schemas.knowledge import KnowledgeCreate, KnowledgeRead, KnowledgeReview, KnowledgeUpdate
from app.services.auth import require_admin
from app.services.catalog import (
ensure_version,
get_item,
@@ -18,7 +19,7 @@ from app.services.catalog import (
update_values,
)
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
router = APIRouter(prefix="/knowledge", tags=["knowledge"], dependencies=[Depends(require_admin)])
def filtered_query(q="", category=None, verification_status=None, source_id=None, document_id=None):
+5 -2
View File
@@ -8,6 +8,7 @@ from app.db.session import get_db
from app.models import RetrievalRun
from app.schemas.catalog import Page
from app.schemas.retrieval import RetrievalRequest, RetrievalResponse
from app.services.auth import require_admin
from app.services.retrieval import retrieve
router = APIRouter(prefix="/retrieval", tags=["retrieval"])
@@ -19,7 +20,7 @@ def keyword_retrieval(payload: RetrievalRequest, db: Session = Depends(get_db)):
return retrieve(db, payload)
@router.get("/runs", response_model=Page[RetrievalResponse])
@router.get("/runs", response_model=Page[RetrievalResponse], dependencies=[Depends(require_admin)])
def list_runs(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
@@ -39,7 +40,9 @@ def list_runs(
}
@router.get("/runs/{run_id}", response_model=RetrievalResponse)
@router.get(
"/runs/{run_id}", response_model=RetrievalResponse, dependencies=[Depends(require_admin)]
)
def read_run(run_id: UUID, db: Session = Depends(get_db)):
row = db.get(RetrievalRun, str(run_id))
if row is None:
+2 -1
View File
@@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models import Document, KnowledgeItem, Source
from app.schemas.catalog import AuditRead, Page, SourceCreate, SourceRead, SourceUpdate
from app.services.auth import require_admin
from app.services.catalog import (
ensure_version,
get_item,
@@ -15,7 +16,7 @@ from app.services.catalog import (
update_values,
)
router = APIRouter(prefix="/sources", tags=["sources"])
router = APIRouter(prefix="/sources", tags=["sources"], dependencies=[Depends(require_admin)])
@router.get("", response_model=Page[SourceRead])