恢复被误删的实时视频菜单定义,通过 visible=False 隐藏菜单, 保留路由和组件以备后续启用。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
358 lines
10 KiB
Python
358 lines
10 KiB
Python
"""
|
||
芋道认证兼容路由
|
||
|
||
提供与芋道主平台一致的认证接口,便于前端统一对接。
|
||
|
||
测试阶段 (dev_mode=True):
|
||
- 本地验证 admin/admin
|
||
- 返回模拟的 Token 和用户信息
|
||
|
||
生产阶段 (dev_mode=False):
|
||
- 转发到芋道主平台验证
|
||
- 返回真实的 Token 和用户信息
|
||
|
||
API 路径规范:
|
||
- /admin-api/system/auth/login - 登录
|
||
- /admin-api/system/auth/logout - 登出
|
||
- /admin-api/system/auth/refresh-token - 刷新令牌
|
||
- /admin-api/system/auth/get-permission-info - 获取权限信息
|
||
"""
|
||
|
||
from fastapi import APIRouter, HTTPException, Header, Query
|
||
from pydantic import BaseModel
|
||
from typing import Optional
|
||
import time
|
||
import secrets
|
||
|
||
from app.config import settings
|
||
from app.yudao_compat import YudaoResponse
|
||
|
||
router = APIRouter(prefix="/admin-api/system", tags=["系统认证"])
|
||
|
||
|
||
class LoginRequest(BaseModel):
|
||
"""登录请求"""
|
||
username: str
|
||
password: str
|
||
captchaVerification: Optional[str] = None
|
||
# 社交登录参数(预留)
|
||
socialType: Optional[int] = None
|
||
socialCode: Optional[str] = None
|
||
socialState: Optional[str] = None
|
||
|
||
|
||
@router.post("/auth/login")
|
||
async def login(req: LoginRequest):
|
||
"""
|
||
登录接口
|
||
|
||
测试阶段:验证 admin/admin,返回模拟 Token
|
||
生产阶段:转发到芋道主平台验证
|
||
|
||
Returns:
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"userId": 1,
|
||
"accessToken": "xxx",
|
||
"refreshToken": "xxx",
|
||
"expiresTime": 1234567890000
|
||
},
|
||
"msg": "success"
|
||
}
|
||
"""
|
||
if settings.app.dev_mode:
|
||
# 测试阶段:简单验证 admin/admin
|
||
if req.username == "admin" and req.password == "admin":
|
||
# 生成模拟 Token
|
||
access_token = secrets.token_urlsafe(32)
|
||
refresh_token = secrets.token_urlsafe(32)
|
||
|
||
return YudaoResponse.success({
|
||
"userId": 1,
|
||
"accessToken": access_token,
|
||
"refreshToken": refresh_token,
|
||
"expiresTime": int(time.time() * 1000) + 24 * 60 * 60 * 1000 # 24小时后过期
|
||
})
|
||
|
||
raise HTTPException(status_code=401, detail="用户名或密码错误")
|
||
|
||
# TODO: 生产阶段转发到芋道主平台
|
||
# response = requests.post(
|
||
# f"{settings.yudao.base_url}/system/auth/login",
|
||
# json=req.dict()
|
||
# )
|
||
# return response.json()
|
||
|
||
raise HTTPException(status_code=501, detail="生产模式认证未实现,请配置芋道主平台地址")
|
||
|
||
|
||
@router.get("/auth/get-permission-info")
|
||
async def get_permission_info(
|
||
authorization: Optional[str] = Header(None, alias="Authorization")
|
||
):
|
||
"""
|
||
获取当前用户权限信息
|
||
|
||
测试阶段:返回超级管理员权限
|
||
生产阶段:验证 Token 并获取真实权限
|
||
|
||
Returns:
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"user": { "id": 1, "nickname": "超级管理员", ... },
|
||
"roles": ["super_admin"],
|
||
"permissions": ["*:*:*"]
|
||
},
|
||
"msg": "success"
|
||
}
|
||
"""
|
||
if settings.app.dev_mode:
|
||
return YudaoResponse.success({
|
||
"user": {
|
||
"id": 1,
|
||
"nickname": "超级管理员",
|
||
"avatar": "",
|
||
"deptId": 1,
|
||
"homePath": "/aiot/alarm/list"
|
||
},
|
||
"roles": ["super_admin"],
|
||
"permissions": ["*:*:*"], # 超级权限,拥有所有操作权限
|
||
"menus": _build_aiot_menus()
|
||
})
|
||
|
||
# 生产阶段:验证 Token
|
||
if not authorization or not authorization.startswith("Bearer "):
|
||
raise HTTPException(status_code=401, detail="请先登录")
|
||
|
||
# TODO: 生产阶段调用芋道主平台验证
|
||
# token = authorization.replace("Bearer ", "")
|
||
# response = requests.get(
|
||
# f"{settings.yudao.base_url}/system/auth/get-permission-info",
|
||
# headers={"Authorization": f"Bearer {token}"}
|
||
# )
|
||
# return response.json()
|
||
|
||
raise HTTPException(status_code=501, detail="生产模式认证未实现")
|
||
|
||
|
||
@router.post("/auth/logout")
|
||
async def logout(
|
||
authorization: Optional[str] = Header(None, alias="Authorization")
|
||
):
|
||
"""
|
||
登出接口
|
||
|
||
清除用户登录状态
|
||
"""
|
||
# 测试阶段和生产阶段都直接返回成功
|
||
# 实际的 Token 失效由前端清除 localStorage 完成
|
||
return YudaoResponse.success(True)
|
||
|
||
|
||
@router.post("/auth/refresh-token")
|
||
async def refresh_token(
|
||
refreshToken: str = Query(..., description="刷新令牌")
|
||
):
|
||
"""
|
||
刷新访问令牌
|
||
|
||
使用 refreshToken 获取新的 accessToken
|
||
|
||
Returns:
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"accessToken": "xxx",
|
||
"refreshToken": "xxx",
|
||
"expiresTime": 1234567890000
|
||
},
|
||
"msg": "success"
|
||
}
|
||
"""
|
||
if settings.app.dev_mode:
|
||
# 测试阶段:直接返回新 Token
|
||
new_access_token = secrets.token_urlsafe(32)
|
||
new_refresh_token = secrets.token_urlsafe(32)
|
||
|
||
return YudaoResponse.success({
|
||
"accessToken": new_access_token,
|
||
"refreshToken": new_refresh_token,
|
||
"expiresTime": int(time.time() * 1000) + 24 * 60 * 60 * 1000
|
||
})
|
||
|
||
# TODO: 生产阶段调用芋道主平台刷新
|
||
raise HTTPException(status_code=501, detail="生产模式认证未实现")
|
||
|
||
|
||
# ==================== 租户相关接口(预留) ====================
|
||
|
||
@router.get("/tenant/simple-list")
|
||
async def get_tenant_list():
|
||
"""
|
||
获取租户列表
|
||
|
||
测试阶段:返回空列表(前端 VITE_APP_TENANT_ENABLE=false 不会调用)
|
||
"""
|
||
return YudaoResponse.success([])
|
||
|
||
|
||
@router.get("/tenant/get-by-website")
|
||
async def get_tenant_by_website(website: str = Query(...)):
|
||
"""
|
||
根据域名获取租户
|
||
|
||
测试阶段:返回默认租户
|
||
"""
|
||
return YudaoResponse.success({
|
||
"id": 1,
|
||
"name": "默认租户"
|
||
})
|
||
|
||
|
||
# ==================== 验证码相关接口(预留) ====================
|
||
|
||
@router.post("/captcha/get")
|
||
async def get_captcha(data: dict):
|
||
"""
|
||
获取验证码
|
||
|
||
测试阶段:前端 VITE_APP_CAPTCHA_ENABLE=false 不会调用
|
||
"""
|
||
return YudaoResponse.success({
|
||
"enable": False
|
||
})
|
||
|
||
|
||
@router.post("/captcha/check")
|
||
async def check_captcha(data: dict):
|
||
"""
|
||
校验验证码
|
||
|
||
测试阶段:直接返回成功
|
||
"""
|
||
return YudaoResponse.success(True)
|
||
|
||
|
||
# ==================== 字典数据接口(预留) ====================
|
||
|
||
@router.get("/dict-data/simple-list")
|
||
async def get_dict_data_simple_list():
|
||
"""
|
||
获取字典数据列表
|
||
|
||
测试阶段:返回空列表
|
||
"""
|
||
return YudaoResponse.success([])
|
||
|
||
|
||
@router.get("/notify-message/get-unread-count")
|
||
async def get_unread_message_count():
|
||
"""
|
||
获取未读消息数量
|
||
|
||
测试阶段:返回 0
|
||
"""
|
||
return YudaoResponse.success(0)
|
||
|
||
|
||
# ==================== 辅助函数 ====================
|
||
|
||
def _build_aiot_menus():
|
||
"""
|
||
构建 AIoT 菜单树
|
||
|
||
芋道前端 (accessMode='backend') 期望的菜单格式:
|
||
- id, parentId, name, path, component, icon, sort, visible, keepAlive
|
||
- 顶层 parentId=0, component='Layout'
|
||
- 叶子节点 component 为 views/ 下的路径
|
||
"""
|
||
return [
|
||
{
|
||
"id": 2000,
|
||
"parentId": 0,
|
||
"name": "AIoT 智能平台",
|
||
"path": "aiot",
|
||
"component": "Layout",
|
||
"icon": "ep:monitor",
|
||
"sort": 10,
|
||
"visible": True,
|
||
"keepAlive": True,
|
||
"children": [
|
||
{
|
||
"id": 2010,
|
||
"parentId": 2000,
|
||
"name": "告警列表",
|
||
"path": "alarm/list",
|
||
"component": "aiot/alarm/list/index",
|
||
"componentName": "AiotAlarmList",
|
||
"icon": "ep:warning",
|
||
"sort": 1,
|
||
"visible": True,
|
||
"keepAlive": True,
|
||
},
|
||
{
|
||
"id": 2011,
|
||
"parentId": 2000,
|
||
"name": "摄像头告警汇总",
|
||
"path": "alarm/summary",
|
||
"component": "aiot/alarm/summary/index",
|
||
"componentName": "AiotAlarmSummary",
|
||
"icon": "ep:data-analysis",
|
||
"sort": 2,
|
||
"visible": True,
|
||
"keepAlive": True,
|
||
},
|
||
{
|
||
"id": 2020,
|
||
"parentId": 2000,
|
||
"name": "摄像头管理",
|
||
"path": "device/camera",
|
||
"component": "aiot/device/camera/index",
|
||
"componentName": "AiotDeviceCamera",
|
||
"icon": "ep:camera",
|
||
"sort": 3,
|
||
"visible": True,
|
||
"keepAlive": True,
|
||
},
|
||
{
|
||
"id": 2021,
|
||
"parentId": 2000,
|
||
"name": "ROI 区域配置",
|
||
"path": "device/roi",
|
||
"component": "aiot/device/roi/index",
|
||
"componentName": "AiotDeviceRoi",
|
||
"icon": "ep:aim",
|
||
"sort": 4,
|
||
"visible": True,
|
||
"keepAlive": True,
|
||
},
|
||
{
|
||
"id": 2030,
|
||
"parentId": 2000,
|
||
"name": "实时视频",
|
||
"path": "video/live",
|
||
"component": "aiot/video/live/index",
|
||
"componentName": "AiotVideoLive",
|
||
"icon": "ep:video-camera",
|
||
"sort": 5,
|
||
"visible": False,
|
||
"keepAlive": True,
|
||
},
|
||
{
|
||
"id": 2040,
|
||
"parentId": 2000,
|
||
"name": "边缘节点管理",
|
||
"path": "edge/node",
|
||
"component": "aiot/edge/node/index",
|
||
"componentName": "AiotEdgeNode",
|
||
"icon": "ep:cpu",
|
||
"sort": 6,
|
||
"visible": True,
|
||
"keepAlive": True,
|
||
},
|
||
],
|
||
}
|
||
]
|