250 lines
7.6 KiB
Python
250 lines
7.6 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
设备管理 API
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
from utils.redis import RedisClient
|
|
from utils.client_info import get_device_id, get_client_info
|
|
from utils.security import get_current_user
|
|
from app.base_schema import ResponseModel
|
|
from core.device.schema import DeviceInfo, DeviceListResponse, DeviceRenameRequest
|
|
from datetime import datetime, timezone
|
|
|
|
router = APIRouter(prefix="/devices", tags=["设备管理"])
|
|
|
|
# Redis中存储refresh token的key前缀
|
|
REFRESH_TOKEN_PREFIX = "refresh_token:"
|
|
# Redis中存储设备信息的key前缀
|
|
DEVICE_INFO_PREFIX = "device_info:"
|
|
|
|
|
|
@router.get("", response_model=DeviceListResponse, summary="获取设备列表")
|
|
async def get_devices(
|
|
request: Request,
|
|
current_user=Depends(get_current_user)
|
|
):
|
|
"""
|
|
获取当前用户的所有登录设备
|
|
|
|
返回:
|
|
- current_device: 当前设备信息
|
|
- online_devices: 其他在线设备列表
|
|
- total_count: 设备总数
|
|
"""
|
|
user_id = current_user.id
|
|
current_device_id = get_device_id(request)
|
|
|
|
redis = await RedisClient.get_client()
|
|
|
|
# 查找该用户的所有refresh token
|
|
pattern = f"{REFRESH_TOKEN_PREFIX}{user_id}:*"
|
|
cursor = 0
|
|
devices = []
|
|
|
|
while True:
|
|
cursor, keys = await redis.scan(cursor, match=pattern, count=100)
|
|
|
|
for key in keys:
|
|
# 提取device_id
|
|
device_id = key.split(":")[-1]
|
|
|
|
# 检查refresh token是否存在
|
|
token_exists = await redis.exists(key)
|
|
if not token_exists:
|
|
continue
|
|
|
|
# 获取设备信息
|
|
device_info_key = f"{DEVICE_INFO_PREFIX}{user_id}:{device_id}"
|
|
device_data = await redis.hgetall(device_info_key)
|
|
|
|
# 检查 access token 是否存在(判断在线状态)
|
|
access_token_key = f"access_token:{user_id}:{device_id}"
|
|
is_online = await redis.exists(access_token_key)
|
|
|
|
# 构建设备信息
|
|
device = DeviceInfo(
|
|
device_id=device_id,
|
|
device_name=device_data.get("device_name"),
|
|
device_type=device_data.get("device_type"),
|
|
browser_type=device_data.get("browser_type"),
|
|
os_type=device_data.get("os_type"),
|
|
ip_address=device_data.get("ip_address"),
|
|
last_active_time=datetime.fromisoformat(device_data["last_active_time"]) if device_data.get("last_active_time") else None,
|
|
is_current=(device_id == current_device_id),
|
|
is_online=bool(is_online)
|
|
)
|
|
devices.append(device)
|
|
|
|
if cursor == 0:
|
|
break
|
|
|
|
# 分离当前设备和其他设备
|
|
current_device = None
|
|
online_devices = []
|
|
|
|
for device in devices:
|
|
if device.is_current:
|
|
current_device = device
|
|
else:
|
|
online_devices.append(device)
|
|
|
|
# 按最后活跃时间排序
|
|
online_devices.sort(key=lambda x: x.last_active_time or datetime.min, reverse=True)
|
|
|
|
return DeviceListResponse(
|
|
current_device=current_device,
|
|
online_devices=online_devices,
|
|
total_count=len(devices)
|
|
)
|
|
|
|
|
|
@router.delete("/{device_id}", response_model=ResponseModel, summary="强制登出指定设备")
|
|
async def logout_device(
|
|
device_id: str,
|
|
request: Request,
|
|
current_user=Depends(get_current_user)
|
|
):
|
|
"""
|
|
强制登出指定设备
|
|
|
|
删除该设备的refresh token,使其下次刷新时失效
|
|
"""
|
|
user_id = current_user.id
|
|
current_device_id = get_device_id(request)
|
|
|
|
# 不能登出当前设备
|
|
if device_id == current_device_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="不能登出当前设备,请使用登出功能"
|
|
)
|
|
|
|
redis = await RedisClient.get_client()
|
|
|
|
# 删除指定设备的 refresh token 和 access token
|
|
token_key = f"{REFRESH_TOKEN_PREFIX}{user_id}:{device_id}"
|
|
access_token_key = f"access_token:{user_id}:{device_id}"
|
|
deleted = await redis.delete(token_key)
|
|
await redis.delete(access_token_key)
|
|
|
|
if deleted == 0:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="设备不存在或已登出"
|
|
)
|
|
|
|
# 删除设备信息
|
|
device_info_key = f"{DEVICE_INFO_PREFIX}{user_id}:{device_id}"
|
|
await redis.delete(device_info_key)
|
|
|
|
return ResponseModel(message="设备已强制登出")
|
|
|
|
|
|
@router.delete("/logout-others", response_model=ResponseModel, summary="登出其他所有设备")
|
|
async def logout_other_devices(
|
|
request: Request,
|
|
current_user=Depends(get_current_user)
|
|
):
|
|
"""
|
|
登出除当前设备外的所有其他设备
|
|
"""
|
|
user_id = current_user.id
|
|
current_device_id = get_device_id(request)
|
|
|
|
redis = await RedisClient.get_client()
|
|
|
|
# 查找该用户的所有refresh token
|
|
pattern = f"{REFRESH_TOKEN_PREFIX}{user_id}:*"
|
|
cursor = 0
|
|
deleted_count = 0
|
|
|
|
while True:
|
|
cursor, keys = await redis.scan(cursor, match=pattern, count=100)
|
|
|
|
for key in keys:
|
|
# 提取device_id
|
|
device_id = key.split(":")[-1]
|
|
|
|
# 跳过当前设备
|
|
if device_id == current_device_id:
|
|
continue
|
|
|
|
# 删除其他设备的 refresh token 和 access token
|
|
await redis.delete(key)
|
|
access_token_key = f"access_token:{user_id}:{device_id}"
|
|
await redis.delete(access_token_key)
|
|
|
|
# 删除设备信息
|
|
device_info_key = f"{DEVICE_INFO_PREFIX}{user_id}:{device_id}"
|
|
await redis.delete(device_info_key)
|
|
|
|
deleted_count += 1
|
|
|
|
if cursor == 0:
|
|
break
|
|
|
|
return ResponseModel(message=f"已登出 {deleted_count} 台其他设备")
|
|
|
|
|
|
@router.post("/{device_id}/rename", response_model=ResponseModel, summary="重命名设备")
|
|
async def rename_device(
|
|
device_id: str,
|
|
data: DeviceRenameRequest,
|
|
current_user=Depends(get_current_user)
|
|
):
|
|
"""
|
|
为设备设置自定义名称
|
|
"""
|
|
user_id = current_user.id
|
|
|
|
redis = await RedisClient.get_client()
|
|
|
|
# 检查设备是否存在
|
|
token_key = f"{REFRESH_TOKEN_PREFIX}{user_id}:{device_id}"
|
|
exists = await redis.exists(token_key)
|
|
|
|
if not exists:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="设备不存在"
|
|
)
|
|
|
|
# 更新设备名称
|
|
device_info_key = f"{DEVICE_INFO_PREFIX}{user_id}:{device_id}"
|
|
await redis.hset(device_info_key, "device_name", data.device_name)
|
|
|
|
return ResponseModel(message="设备名称已更新")
|
|
|
|
|
|
@router.get("/statistics", response_model=ResponseModel, summary="获取设备统计信息")
|
|
async def get_device_statistics(
|
|
current_user=Depends(get_current_user)
|
|
):
|
|
"""
|
|
获取设备统计信息
|
|
"""
|
|
user_id = current_user.id
|
|
|
|
redis = await RedisClient.get_client()
|
|
|
|
# 统计在线设备数量
|
|
pattern = f"{REFRESH_TOKEN_PREFIX}{user_id}:*"
|
|
cursor = 0
|
|
online_count = 0
|
|
|
|
while True:
|
|
cursor, keys = await redis.scan(cursor, match=pattern, count=100)
|
|
online_count += len(keys)
|
|
|
|
if cursor == 0:
|
|
break
|
|
|
|
return ResponseModel(
|
|
message="获取成功",
|
|
data={
|
|
"online_count": online_count,
|
|
"total_count": online_count # 暂时只统计在线设备
|
|
}
|
|
)
|