""" 芋道认证兼容路由 提供与芋道主平台一致的认证接口,便于前端统一对接。 测试阶段 (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, }, ], } ]