Files
000-platform/backend/app/routers/auth.py
111 6a93e05ec3
All checks were successful
continuous-integration/drone/push Build is passing
feat: 应用扁平化与 Token 验证 API
- 新增 /api/auth/verify 接口供外部应用验证 token
- 简化应用管理:移除 tools 字段,每个应用独立存在
- 简化应用配置:移除 allowed_tools,专注于租户订阅
- 优化 Token 展示和复制功能
2026-01-24 10:05:24 +08:00

339 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""认证路由"""
import hmac
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from typing import Optional, List
from sqlalchemy.orm import Session
from ..database import get_db
from ..services.auth import (
authenticate_user,
create_access_token,
decode_token,
update_last_login,
hash_password,
TokenData,
UserInfo
)
from ..services.crypto import decrypt_config
from ..models.user import User
from ..models.tenant_app import TenantApp
from ..models.tenant_wechat_app import TenantWechatApp
router = APIRouter(prefix="/auth", tags=["认证"])
security = HTTPBearer()
class LoginRequest(BaseModel):
"""登录请求"""
username: str
password: str
class LoginResponse(BaseModel):
"""登录响应"""
success: bool
token: Optional[str] = None
user: Optional[UserInfo] = None
error: Optional[str] = None
class ChangePasswordRequest(BaseModel):
"""修改密码请求"""
old_password: str
new_password: str
# 权限依赖
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
) -> User:
"""获取当前用户"""
token = credentials.credentials
token_data = decode_token(token)
if not token_data:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token 无效或已过期"
)
user = db.query(User).filter(User.id == token_data.user_id).first()
if not user or user.status != 1:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户不存在或已禁用"
)
return user
async def require_admin(user: User = Depends(get_current_user)) -> User:
"""要求管理员权限"""
if user.role != 'admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要管理员权限"
)
return user
async def require_operator(user: User = Depends(get_current_user)) -> User:
"""要求操作员以上权限"""
if user.role not in ('admin', 'operator'):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要操作员以上权限"
)
return user
# API 端点
@router.post("/login", response_model=LoginResponse)
async def login(request: LoginRequest, db: Session = Depends(get_db)):
"""用户登录"""
user = authenticate_user(db, request.username, request.password)
if not user:
return LoginResponse(success=False, error="用户名或密码错误")
# 更新登录时间
update_last_login(db, user.id)
# 生成 Token
token = create_access_token({
"user_id": user.id,
"username": user.username,
"role": user.role
})
return LoginResponse(
success=True,
token=token,
user=UserInfo(
id=user.id,
username=user.username,
nickname=user.nickname,
role=user.role
)
)
@router.get("/me", response_model=UserInfo)
async def get_me(user: User = Depends(get_current_user)):
"""获取当前用户信息"""
return UserInfo(
id=user.id,
username=user.username,
nickname=user.nickname,
role=user.role
)
@router.post("/change-password")
async def change_password(
request: ChangePasswordRequest,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""修改密码"""
from ..services.auth import verify_password
if not verify_password(request.old_password, user.password_hash):
raise HTTPException(status_code=400, detail="原密码错误")
new_hash = hash_password(request.new_password)
db.query(User).filter(User.id == user.id).update({"password_hash": new_hash})
db.commit()
return {"success": True, "message": "密码修改成功"}
@router.get("/users")
async def list_users(
user: User = Depends(require_admin),
db: Session = Depends(get_db)
):
"""获取用户列表(仅管理员)"""
users = db.query(User).all()
return [
{
"id": u.id,
"username": u.username,
"nickname": u.nickname,
"role": u.role,
"status": u.status,
"last_login_at": u.last_login_at,
"created_at": u.created_at
}
for u in users
]
class CreateUserRequest(BaseModel):
username: str
password: str
nickname: Optional[str] = None
role: str = "viewer"
@router.post("/users")
async def create_user(
request: CreateUserRequest,
user: User = Depends(require_admin),
db: Session = Depends(get_db)
):
"""创建用户(仅管理员)"""
# 检查用户名是否存在
exists = db.query(User).filter(User.username == request.username).first()
if exists:
raise HTTPException(status_code=400, detail="用户名已存在")
new_user = User(
username=request.username,
password_hash=hash_password(request.password),
nickname=request.nickname,
role=request.role,
status=1
)
db.add(new_user)
db.commit()
db.refresh(new_user)
return {"success": True, "id": new_user.id}
@router.delete("/users/{user_id}")
async def delete_user(
user_id: int,
user: User = Depends(require_admin),
db: Session = Depends(get_db)
):
"""删除用户(仅管理员)"""
if user_id == user.id:
raise HTTPException(status_code=400, detail="不能删除自己")
target = db.query(User).filter(User.id == user_id).first()
if not target:
raise HTTPException(status_code=404, detail="用户不存在")
db.delete(target)
db.commit()
return {"success": True}
# ============ Token 验证 API供外部应用调用 ============
class VerifyTokenRequest(BaseModel):
"""Token 验证请求"""
token: str
app_code: Optional[str] = None # 可选,用于验证 token 是否属于特定应用
class WechatConfig(BaseModel):
"""企微配置"""
corp_id: Optional[str] = None
agent_id: Optional[str] = None
secret: Optional[str] = None
class VerifyTokenResponse(BaseModel):
"""Token 验证响应"""
valid: bool
tenant_id: Optional[str] = None
app_code: Optional[str] = None
wechat_config: Optional[WechatConfig] = None
error: Optional[str] = None
@router.post("/verify", response_model=VerifyTokenResponse)
async def verify_token(
request: VerifyTokenRequest,
db: Session = Depends(get_db)
):
"""
验证 Token 有效性(供外部应用调用,无需登录)
外部应用收到用户请求后,可调用此接口验证 token
1. 验证 token 是否存在且有效
2. 如传入 app_code验证 token 是否属于该应用
3. 返回租户信息和企微配置
Args:
token: 访问令牌
app_code: 应用代码(可选,用于验证 token 是否属于特定应用)
Returns:
valid: 是否有效
tenant_id: 租户ID
app_code: 应用代码
wechat_config: 企微配置(如有)
"""
if not request.token:
return VerifyTokenResponse(valid=False, error="Token 不能为空")
# 根据 token 查询租户应用配置
query = db.query(TenantApp).filter(
TenantApp.access_token == request.token,
TenantApp.status == 1
)
# 如果指定了 app_code验证 token 是否属于该应用
if request.app_code:
query = query.filter(TenantApp.app_code == request.app_code)
tenant_app = query.first()
if not tenant_app:
return VerifyTokenResponse(valid=False, error="Token 无效或已过期")
# 获取关联的企微配置
wechat_config = None
if tenant_app.wechat_app_id:
wechat_app = db.query(TenantWechatApp).filter(
TenantWechatApp.id == tenant_app.wechat_app_id,
TenantWechatApp.status == 1
).first()
if wechat_app:
# 解密 secret
secret = None
if wechat_app.secret_encrypted:
try:
secret = decrypt_config(wechat_app.secret_encrypted)
except:
pass
wechat_config = WechatConfig(
corp_id=wechat_app.corp_id,
agent_id=wechat_app.agent_id,
secret=secret
)
return VerifyTokenResponse(
valid=True,
tenant_id=tenant_app.tenant_id,
app_code=tenant_app.app_code,
wechat_config=wechat_config
)
@router.get("/verify")
async def verify_token_get(
token: str,
app_code: Optional[str] = None,
db: Session = Depends(get_db)
):
"""
验证 TokenGET 方式,便于简单测试)
"""
return await verify_token(
VerifyTokenRequest(token=token, app_code=app_code),
db
)