chore: 完成全量功能迭代与部署准备
- 移除冗余依赖包 - 新增账号禁用校验与用户管理能力 - 新增文档下载与管理页面文件展示 - 新增API文档页面与用户管理前端页面 - 重构时区处理与docker-compose部署配置 - 完善测试用例与项目文档
This commit is contained in:
+4
-4
@@ -4,7 +4,7 @@
|
||||
鉴权(get_current_user)以 JWT 自包含信息为主,Redis 不可用时降级可用。
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import bcrypt
|
||||
import jwt
|
||||
@@ -67,7 +67,7 @@ def verify_password(password: str, hashed: str) -> bool:
|
||||
|
||||
def create_access_token(username: str, role: str) -> tuple[str, int]:
|
||||
"""签发 JWT,返回 (token, expires_in_seconds)"""
|
||||
now = datetime.now(UTC)
|
||||
now = datetime.now(timezone.utc)
|
||||
expire = now + timedelta(minutes=settings.jwt_expire_minutes)
|
||||
payload = {
|
||||
"sub": username,
|
||||
@@ -136,7 +136,7 @@ class UserStore:
|
||||
user = StoredUser(
|
||||
username=username,
|
||||
role=role,
|
||||
created_at=datetime.now(UTC),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
hashed_password=hash_password(password),
|
||||
)
|
||||
try:
|
||||
@@ -203,7 +203,7 @@ async def get_current_user(
|
||||
)
|
||||
# Redis 不可用或用户不存在(可能已删除):降级用 JWT payload
|
||||
logger.warning("用户存储查询未命中,降级使用 JWT payload", username=username)
|
||||
return AuthUser(username=username, role=role, created_at=datetime.now(UTC))
|
||||
return AuthUser(username=username, role=role, created_at=datetime.now(timezone.utc))
|
||||
|
||||
|
||||
async def require_admin(user: AuthUser = Depends(get_current_user)) -> AuthUser:
|
||||
|
||||
+44
-4
@@ -11,7 +11,7 @@ import hmac
|
||||
import json
|
||||
import re
|
||||
import secrets
|
||||
from dataclasses import asdict, dataclass
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from redis import asyncio as redis_async
|
||||
@@ -41,6 +41,10 @@ class UserStoreError(Exception):
|
||||
"""用户存储后端读写异常"""
|
||||
|
||||
|
||||
class UserNotFoundError(Exception):
|
||||
"""用户不存在"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserRecord:
|
||||
"""用户记录(不含明文密码)"""
|
||||
@@ -50,7 +54,9 @@ class UserRecord:
|
||||
password_hash: str # hex
|
||||
salt: str # hex, 16 字节
|
||||
must_change_password: bool
|
||||
created_at: str # UTC ISO8601
|
||||
enabled: bool = True
|
||||
# created_at 紧随 enabled 之后;二者均有默认值以满足 dataclass 字段顺序约束
|
||||
created_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
||||
|
||||
|
||||
class UserStore:
|
||||
@@ -76,6 +82,7 @@ class UserStore:
|
||||
password: str,
|
||||
role: str = "user",
|
||||
must_change_password: bool = False,
|
||||
enabled: bool = True,
|
||||
) -> UserRecord:
|
||||
"""创建用户
|
||||
|
||||
@@ -94,6 +101,7 @@ class UserStore:
|
||||
password_hash=self.hash_password(password, salt),
|
||||
salt=salt.hex(),
|
||||
must_change_password=must_change_password,
|
||||
enabled=enabled,
|
||||
created_at=datetime.now(UTC).isoformat(),
|
||||
)
|
||||
await self._write(record)
|
||||
@@ -104,7 +112,7 @@ class UserStore:
|
||||
raw = await self._read_raw(username)
|
||||
if raw is None:
|
||||
return None
|
||||
return UserRecord(**json.loads(raw))
|
||||
return self._from_raw(raw)
|
||||
|
||||
async def list(self) -> list[UserRecord]:
|
||||
"""列出全部用户(扫描 user:* 键)"""
|
||||
@@ -115,7 +123,15 @@ class UserStore:
|
||||
raws = [await self._redis.get(key) async for key in self._redis.scan_iter(match=f"{_USER_KEY_PREFIX}*")]
|
||||
except Exception as e:
|
||||
raise UserStoreError("列出用户失败") from e
|
||||
return [UserRecord(**json.loads(raw)) for raw in raws if raw is not None]
|
||||
return [self._from_raw(raw) for raw in raws if raw is not None]
|
||||
|
||||
@staticmethod
|
||||
def _from_raw(raw: str) -> UserRecord:
|
||||
"""从 JSON 反序列化 UserRecord;存量记录无 enabled 字段时按 True 兼容"""
|
||||
data = json.loads(raw)
|
||||
if "enabled" not in data:
|
||||
data["enabled"] = True
|
||||
return UserRecord(**data)
|
||||
|
||||
async def delete(self, username: str) -> bool:
|
||||
"""删除用户,返回是否删除成功(幂等:不存在返回 False)"""
|
||||
@@ -158,6 +174,30 @@ class UserStore:
|
||||
"""统计 admin 角色用户数"""
|
||||
return sum(1 for record in await self.list() if record.role == "admin")
|
||||
|
||||
async def update_user(
|
||||
self,
|
||||
username: str,
|
||||
*,
|
||||
role: str | None = None,
|
||||
enabled: bool | None = None,
|
||||
) -> UserRecord:
|
||||
"""更新用户角色或启用状态
|
||||
|
||||
用户不存在抛 UserNotFoundError;role 非 admin/user 抛 ValueError。
|
||||
role/enabled 为 None 表示不修改对应字段。更新后写回并返回最新记录。
|
||||
"""
|
||||
record = await self.get(username)
|
||||
if record is None:
|
||||
raise UserNotFoundError(f"用户不存在: {username}")
|
||||
if role is not None and role not in ("admin", "user"):
|
||||
raise ValueError(f"非法角色: {role!r}(须为 admin/user)")
|
||||
if role is not None:
|
||||
record.role = role
|
||||
if enabled is not None:
|
||||
record.enabled = enabled
|
||||
await self._write(record)
|
||||
return record
|
||||
|
||||
async def _read_raw(self, username: str) -> str | None:
|
||||
if self._redis is None:
|
||||
return self._memory.get(username)
|
||||
|
||||
Reference in New Issue
Block a user