Files
live-hub-py/web/backend/routers/users.py
T
yml2213 2f5a63ae27 feat(auth): 新增手机号与 DCAuth 设备认证登录
- 登录链路改为用户名 + 密码 + 短信验证码 + DCAuth v2 设备签名
- 新增 login_sms_codes / user_devices / device_challenges 三张表,迁移 0063/0064
- 设备绑定只比对 deviceId + publicKey,每账号一个 active 设备,管理员可重置
- 保留未绑定手机号账号的密码登录兜底,绑定后强制安全登录
- 登录页与用户管理页支持验证码、DCAuth 轮询、手机号绑定与设备重置
2026-10-21 22:00:03 +08:00

223 lines
7.0 KiB
Python

"""用户管理路由(超管)"""
from datetime import UTC, datetime
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import require_permission
from ..models import Account, AuditLog, HuyaAccount, User, UserDevice
from ..permissions import PERMISSIONS
from ..schemas import UserCreate, UserInfo, UserRename, UserUpdate
from ..security import hash_password
from ..services.user_profile_service import to_user_info
router = APIRouter(prefix="/api/users", tags=["用户管理"])
def _active_device(db: Session, user_id: int) -> UserDevice | None:
return (
db.query(UserDevice)
.filter(UserDevice.user_id == user_id, UserDevice.status == "active")
.order_by(UserDevice.id.desc())
.first()
)
@router.get("", response_model=list[UserInfo])
def list_users(
db: Session = Depends(get_db),
_: User = Depends(require_permission("user:view")),
):
users = db.query(User).filter(User.deleted_at.is_(None)).order_by(User.id).all()
devices: dict[int, UserDevice] = {}
if users:
device_rows = (
db.query(UserDevice)
.filter(
UserDevice.user_id.in_([user.id for user in users]),
UserDevice.status == "active",
)
.order_by(UserDevice.id.desc())
.all()
)
for device in device_rows:
devices.setdefault(device.user_id, device)
return [to_user_info(user, devices.get(user.id)) for user in users]
@router.post("", response_model=UserInfo)
def create_user(
req: UserCreate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("user:create")),
):
if db.query(User).filter(User.username == req.username).first():
raise HTTPException(status_code=400, detail="用户名已存在")
user = User(
username=req.username,
password_hash=hash_password(req.password),
role=req.role,
remark=req.remark,
is_active=True,
)
db.add(user)
db.commit()
db.refresh(user)
db.add(
AuditLog(
user_id=current.id,
username=current.username,
action="user:create",
target=user.username,
)
)
db.commit()
return to_user_info(user, _active_device(db, user.id))
@router.put("/{user_id}", response_model=UserInfo)
def update_user(
user_id: int,
req: UserUpdate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("user:edit")),
):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if user.deleted_at is not None:
raise HTTPException(status_code=404, detail="用户不存在")
if req.password:
user.password_hash = hash_password(req.password)
if req.role is not None:
user.role = req.role
if req.is_active is not None:
user.is_active = req.is_active
if req.remark is not None:
user.remark = req.remark
if "custom_permissions" in req.model_fields_set:
user.custom_permissions = req.custom_permissions
db.commit()
db.refresh(user)
db.add(
AuditLog(
user_id=current.id,
username=current.username,
action="user:edit",
target=user.username,
)
)
db.commit()
return to_user_info(user, _active_device(db, user.id))
@router.patch("/{user_id}/username", response_model=UserInfo)
def rename_user(
user_id: int,
req: UserRename,
db: Session = Depends(get_db),
current: User = Depends(require_permission("user:rename")),
):
"""仅变更用户名,保留用户 ID 和全部关联业务数据。"""
user = db.query(User).filter(User.id == user_id, User.deleted_at.is_(None)).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
username = req.username.strip()
if len(username) < 2:
raise HTTPException(status_code=400, detail="用户名至少 2 个字符")
if username == user.username:
return to_user_info(user, _active_device(db, user.id))
if db.query(User.id).filter(User.username == username).first():
raise HTTPException(status_code=400, detail="用户名已存在")
old_username = user.username
user.username = username
db.add(
AuditLog(
user_id=current.id,
username=current.username,
action="user:rename",
target=f"{old_username} -> {username}",
)
)
try:
db.commit()
except IntegrityError as exc:
db.rollback()
raise HTTPException(status_code=400, detail="用户名已存在") from exc
db.refresh(user)
return to_user_info(user, _active_device(db, user.id))
@router.delete("/{user_id}")
def delete_user(
user_id: int,
db: Session = Depends(get_db),
current: User = Depends(require_permission("user:delete")),
):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if user.deleted_at is not None:
raise HTTPException(status_code=404, detail="用户不存在")
if user.role == "super_admin":
raise HTTPException(status_code=400, detail="不能删除超级管理员")
if user.id == current.id:
raise HTTPException(status_code=400, detail="不能删除当前登录用户")
# 保留任务与审计记录的创建者 ID,避免删除用户时破坏历史数据。
released_accounts = (
db.query(Account)
.filter(Account.assigned_to == user.id)
.update({Account.assigned_to: None}, synchronize_session=False)
)
released_huya_accounts = (
db.query(HuyaAccount)
.filter(HuyaAccount.assigned_to == user.id)
.update({HuyaAccount.assigned_to: None}, synchronize_session=False)
)
deleted_username = user.username
db.add(
AuditLog(
user_id=current.id,
username=current.username,
action="user:delete",
target=deleted_username,
)
)
user.is_active = False
user.deleted_at = datetime.now(UTC)
user.deleted_username = deleted_username
# 释放用户名供新用户使用,同时不改变历史任务通过 user.id 保存的归属。
user.username = f"deleted-{user.id}-{uuid4().hex[:12]}"
db.commit()
return {
"message": "已删除",
"success": True,
"released_accounts": released_accounts,
"released_huya_accounts": released_huya_accounts,
}
@router.get("/permissions/list")
def list_permissions(_: User = Depends(require_permission("user:assign_permissions"))):
"""返回所有可用权限点及角色默认权限映射。"""
from ..permissions import ROLE_PERMISSIONS
return {
"permissions": PERMISSIONS,
"role_permissions": ROLE_PERMISSIONS,
}