"""管理端账号安全设置:绑定手机号、重置绑定设备。""" 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 "当前没有已绑定设备", }