feat: 新增用户管理(用户增删改查、密码重置、角色权限、会话认证)与 API 指南

- 新增 app/api/deps.py、app/core/users.py、app/core/sessions.py:会话鉴权依赖、
  用户存储(PBKDF2-HMAC-SHA256 + 随机 salt,Redis/内存降级)、会话签发与校验(TTL 12h)
- auth.py 新增用户管理端点(列表/创建/重置密码/删除)与 admin/user 角色权限边界,
  user 访问用户管理返回 1006,禁删自己与最后一个 admin
- admin.html 新增用户管理面板(仅 admin 挂载)与 API 指南在线测试台
- Dockerfile 将 uv 放入 PATH;docker-compose 调整 qdrant 依赖为 service_started
  并移除依赖 curl 的 healthcheck(官方镜像不含 curl)
- 新增用户管理测试(users/sessions/auth_api/auth_integration),全量 461 项测试通过

Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
This commit is contained in:
2026-07-31 21:29:02 +08:00
parent 2ab8b56a01
commit 92b062c048
24 changed files with 2771 additions and 350 deletions
+7 -11
View File
@@ -10,12 +10,13 @@ import structlog
from fastapi import APIRouter, Depends, File, Form, Query, UploadFile
from fastapi.responses import JSONResponse
from app.api.deps import get_current_user
from app.api.response import ApiError, ok
from app.config import Settings, settings
from app.core.auth import AuthUser, get_current_user, require_admin
from app.core.file_parser import parse_file
from app.core.ingest_tasks import IngestTaskManager
from app.core.ingestion import Ingester
from app.core.users import UserRecord
from app.models.document import DocumentInput
from app.services.qdrant import QdrantService
from app.services.redis import RedisCache, get_cache
@@ -74,7 +75,7 @@ def _allowed_extensions() -> set[str]:
@router.post("/documents")
async def ingest_document(
doc: DocumentInput, user: AuthUser = Depends(get_current_user)
doc: DocumentInput, user: UserRecord = Depends(get_current_user)
) -> JSONResponse:
"""文档入库入口:登记异步任务并返回 202 + task_id,入库结果经任务查询端点获取"""
if not doc.text.strip():
@@ -95,7 +96,7 @@ async def upload_document(
metadata: str = Form(
default="", description='可选元数据 JSON 字符串,如 \'{"author":"x"}\''
),
user: AuthUser = Depends(get_current_user),
user: UserRecord = Depends(get_current_user),
) -> JSONResponse:
"""文件上传入库入口:校验 → 提取文本 → 落盘 → 提交异步入库流水线
@@ -183,9 +184,7 @@ async def upload_document(
@router.get("/documents/tasks/{task_id}")
async def get_ingest_task(
task_id: str, user: AuthUser = Depends(get_current_user)
) -> dict[str, Any]:
async def get_ingest_task(task_id: str) -> dict[str, Any]:
"""查询入库任务状态:含 task_id/status/created_at/updated_atdone 附 resultfailed 附 error"""
task = await _get_task_manager().get(task_id)
if task is None:
@@ -197,7 +196,6 @@ async def get_ingest_task(
async def list_documents(
limit: int = Query(default=20, ge=1, le=100),
offset: str | None = None,
user: AuthUser = Depends(get_current_user),
) -> dict[str, Any]:
"""分页列出文档(L1 摘要),返回 items 与下一页游标 next_offset"""
try:
@@ -209,9 +207,7 @@ async def list_documents(
@router.get("/documents/{doc_id}")
async def get_document(
doc_id: str, user: AuthUser = Depends(get_current_user)
) -> dict[str, Any]:
async def get_document(doc_id: str) -> dict[str, Any]:
"""获取文档详情:L1 记录 + L2/L3 节点 + chunks 数量"""
try:
detail = await _get_qdrant().get_doc_detail(doc_id)
@@ -225,7 +221,7 @@ async def get_document(
@router.delete("/documents/{doc_id}")
async def delete_document(
doc_id: str, user: AuthUser = Depends(require_admin)
doc_id: str, user: UserRecord = Depends(get_current_user)
) -> dict[str, Any]:
"""删除文档:四层集合中该 doc_id 的所有点;幂等,不存在也返回成功(删除数全 0)"""
try: