feat(auth): 新增手机号与 DCAuth 设备认证登录
- 登录链路改为用户名 + 密码 + 短信验证码 + DCAuth v2 设备签名 - 新增 login_sms_codes / user_devices / device_challenges 三张表,迁移 0063/0064 - 设备绑定只比对 deviceId + publicKey,每账号一个 active 设备,管理员可重置 - 保留未绑定手机号账号的密码登录兜底,绑定后强制安全登录 - 登录页与用户管理页支持验证码、DCAuth 轮询、手机号绑定与设备重置
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""管理端账号安全设置:绑定手机号、重置绑定设备。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
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 ..errors import ApiError
|
||||
from ..models import AuditLog, User, UserDevice
|
||||
from ..schemas import UserInfo, UserPhoneBindRequest
|
||||
from ..services.dc_auth_service import mask_device_id
|
||||
from ..services.login_sms_service import mask_phone, normalize_phone
|
||||
from ..services.user_profile_service import to_user_info
|
||||
|
||||
router = APIRouter(prefix="/api/admin/users", tags=["用户管理"])
|
||||
|
||||
|
||||
def _get_user(db: Session, user_id: int) -> User:
|
||||
user = db.query(User).filter(User.id == user_id, User.deleted_at.is_(None)).first()
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return user
|
||||
|
||||
|
||||
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.post("/{user_id}/phone", response_model=UserInfo)
|
||||
def bind_user_phone(
|
||||
user_id: int,
|
||||
req: UserPhoneBindRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("user:edit")),
|
||||
):
|
||||
user = _get_user(db, user_id)
|
||||
phone = normalize_phone(req.phone)
|
||||
if not phone:
|
||||
raise ApiError(400, "请输入手机号", "phone_required")
|
||||
|
||||
existing = db.query(User).filter(User.phone == phone, User.id != user.id).first()
|
||||
if existing is not None:
|
||||
raise ApiError(409, "该手机号已绑定其他用户", "phone_exists")
|
||||
|
||||
now = datetime.now(UTC)
|
||||
user.phone = phone
|
||||
user.phone_bound_at = now
|
||||
user.phone_bound_by = current.id
|
||||
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="user:phone_bind",
|
||||
target=user.username,
|
||||
detail=f"phone={mask_phone(phone)}",
|
||||
)
|
||||
)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError as exc:
|
||||
db.rollback()
|
||||
raise ApiError(409, "该手机号已绑定其他用户", "phone_exists") from exc
|
||||
|
||||
db.refresh(user)
|
||||
return to_user_info(user, _active_device(db, user.id))
|
||||
|
||||
|
||||
@router.post("/{user_id}/device/reset")
|
||||
def reset_user_device(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("user:edit")),
|
||||
):
|
||||
user = _get_user(db, user_id)
|
||||
now = datetime.now(UTC)
|
||||
active = _active_device(db, user.id)
|
||||
revoked = (
|
||||
db.query(UserDevice)
|
||||
.filter(UserDevice.user_id == user.id, UserDevice.status == "active")
|
||||
.update(
|
||||
{
|
||||
UserDevice.status: "revoked",
|
||||
UserDevice.revoked_at: now,
|
||||
UserDevice.revoked_by: current.id,
|
||||
UserDevice.updated_at: now,
|
||||
},
|
||||
synchronize_session=False,
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="user:device_reset",
|
||||
target=user.username,
|
||||
detail=(
|
||||
f"revoked={revoked}; device={mask_device_id(active.device_id)}"
|
||||
if active is not None
|
||||
else "revoked=0"
|
||||
),
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"reset": revoked > 0,
|
||||
"revoked": revoked,
|
||||
"message": "已重置设备,下次登录将重新绑定"
|
||||
if revoked
|
||||
else "当前没有已绑定设备",
|
||||
}
|
||||
Reference in New Issue
Block a user