feat(system): 完成后端系统管理(审计/角色/操作日志/日志查询)
- 操作日志审计机制:audit 上下文(成功与失败都记、独立提交、操作人标量 固化、commit 后查询前置约定),用户/角色全部写操作接入;request_data 按键名的结构化脱敏 + UTF-8 字节边界截断 - 角色管理:CRUD 与启停;admin 禁删禁停、有绑定禁删(FOR UPDATE 锁行)、 编码创建后不可改、停用角色不可再分配 - 日志查询:操作/登录两端点,列表与详情两段式(详情含已脱敏请求参数); 时间参数统一转本地 naive(修混用时区 500 与偏移丢弃) - 用户管理:补齐 reset_password 操作人、编辑空串归一 NULL、并发唯一键兜底 400 - 测试 301 例全绿(单元 223 / 集成 78) Co-Authored-By: Claude Code <noreply@anthropic.com>
这个提交包含在:
1 父节点
e22c4bcdc9
当前提交
c83287819a
32 个文件已更改
+3158
-137
没有与搜索条件相匹配的文件
+25
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.core.security import decode_token
|
||||
from app.models import User
|
||||
from app.repositories import user_repository
|
||||
from app.services import token_blacklist
|
||||
from app.services.audit_service import RequestMeta
|
||||
|
||||
# auto_error=False:令牌缺失时由我们自己抛 AuthError,
|
||||
# 才能返回统一响应信封,而不是 FastAPI 默认的 {"detail": "Not authenticated"}
|
||||
@@ -22,6 +23,29 @@ BearerCredentials = Annotated[HTTPAuthorizationCredentials | None, Depends(_bear
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
def client_ip(request: Request) -> str | None:
|
||||
"""取客户端 IP。
|
||||
|
||||
当前取直连地址。生产环境经 Nginx 反代后直连地址会变成代理地址,
|
||||
届时需改为读取 `X-Forwarded-For` 的**第一个**值 —— 且必须确保该头
|
||||
由网关覆写,否则客户端可以伪造。此改动留到部署阶段一并处理。
|
||||
"""
|
||||
return request.client.host if request.client else None
|
||||
|
||||
|
||||
def get_request_meta(request: Request) -> RequestMeta:
|
||||
"""提取 HTTP 请求元信息,供操作日志审计使用。"""
|
||||
return RequestMeta(
|
||||
ip=client_ip(request),
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
)
|
||||
|
||||
|
||||
RequestMetaDep = Annotated[RequestMeta, Depends(get_request_meta)]
|
||||
|
||||
|
||||
def get_raw_token(credentials: BearerCredentials) -> str:
|
||||
"""取原始令牌字符串。登出时需要它来解析 jti。"""
|
||||
if credentials is None or not credentials.credentials:
|
||||
|
||||
在新工单中引用
屏蔽一个用户