feat(auth): 新增手机号与 DCAuth 设备认证登录

- 登录链路改为用户名 + 密码 + 短信验证码 + DCAuth v2 设备签名
- 新增 login_sms_codes / user_devices / device_challenges 三张表,迁移 0063/0064
- 设备绑定只比对 deviceId + publicKey,每账号一个 active 设备,管理员可重置
- 保留未绑定手机号账号的密码登录兜底,绑定后强制安全登录
- 登录页与用户管理页支持验证码、DCAuth 轮询、手机号绑定与设备重置
This commit is contained in:
yml2213
2026-10-21 22:00:03 +08:00
parent f21362dfe3
commit 2f5a63ae27
24 changed files with 3049 additions and 130 deletions
+28 -51
View File
@@ -9,35 +9,44 @@ from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import require_permission
from ..models import Account, AuditLog, HuyaAccount, User
from ..permissions import PERMISSIONS, get_user_permissions
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()
result = []
for u in users:
result.append(
UserInfo(
id=u.id,
username=u.username,
role=u.role,
is_active=u.is_active,
remark=u.remark or "",
created_at=u.created_at,
permissions=get_user_permissions(u),
custom_permissions=u.custom_permissions,
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()
)
return result
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)
@@ -70,15 +79,7 @@ def create_user(
)
db.commit()
return UserInfo(
id=user.id,
username=user.username,
role=user.role,
is_active=user.is_active,
remark=user.remark or "",
permissions=get_user_permissions(user),
custom_permissions=user.custom_permissions,
)
return to_user_info(user, _active_device(db, user.id))
@router.put("/{user_id}", response_model=UserInfo)
@@ -118,15 +119,7 @@ def update_user(
)
db.commit()
return UserInfo(
id=user.id,
username=user.username,
role=user.role,
is_active=user.is_active,
remark=user.remark or "",
permissions=get_user_permissions(user),
custom_permissions=user.custom_permissions,
)
return to_user_info(user, _active_device(db, user.id))
@router.patch("/{user_id}/username", response_model=UserInfo)
@@ -145,15 +138,7 @@ def rename_user(
if len(username) < 2:
raise HTTPException(status_code=400, detail="用户名至少 2 个字符")
if username == user.username:
return UserInfo(
id=user.id,
username=user.username,
role=user.role,
is_active=user.is_active,
remark=user.remark or "",
permissions=get_user_permissions(user),
custom_permissions=user.custom_permissions,
)
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="用户名已存在")
@@ -173,15 +158,7 @@ def rename_user(
db.rollback()
raise HTTPException(status_code=400, detail="用户名已存在") from exc
db.refresh(user)
return UserInfo(
id=user.id,
username=user.username,
role=user.role,
is_active=user.is_active,
remark=user.remark or "",
permissions=get_user_permissions(user),
custom_permissions=user.custom_permissions,
)
return to_user_info(user, _active_device(db, user.id))
@router.delete("/{user_id}")