P1+P2 complete: pgvector, Casdoor SSO, CLI, KB router, K8s, monitoring, UI fixes (Promotion rewrite, Storage/Automation btn fix, membership configs mojibake fix)

This commit is contained in:
qyyfw committed 2026-07-08 12:10:32 +08:00
1 parent 1f47951895
commit 177e345539
7 files changed
+3197 -2702

No files matched your search

+214
View File
@@ -0,0 +1,214 @@
"""
Casdoor SSO 集成模块
==================
实现 Casdoor OIDC 单点登录集成,
支持 JWT + Casdoor 双认证模式。
使用方式:
from app.casdoor_auth import casdoor_router, get_casdoor_login_url, handle_casdoor_callback
"""
import os
import logging
import httpx
from typing import Optional
from datetime import datetime
from urllib.parse import urlencode
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from pydantic import BaseModel
from .database import get_db
from .models import User, gen_uuid
from .auth import create_access_token, hash_password
logger = logging.getLogger("aiflow.casdoor")
router = APIRouter()
# Casdoor 配置
CASDOOR_ENDPOINT = os.environ.get("CASDOOR_ENDPOINT", "http://casdoor:8000")
CASDOOR_CLIENT_ID = os.environ.get("CASDOOR_CLIENT_ID", "aiflow")
CASDOOR_CLIENT_SECRET = os.environ.get("CASDOOR_CLIENT_SECRET", "")
CASDOOR_ORG_NAME = os.environ.get("CASDOOR_ORG_NAME", "aiflow")
CASDOOR_APP_NAME = os.environ.get("CASDOOR_APP_NAME", "aiflow-app")
CASDOOR_REDIRECT_URI = os.environ.get(
"CASDOOR_REDIRECT_URI",
"http://210.16.161.218:12888/app/api/auth/casdoor/callback"
)
APP_FRONTEND_URL = os.environ.get("APP_ACCESS_URL", "http://210.16.161.218:12888")
def get_casdoor_login_url(state: str = "") -> str:
"""获取 Casdoor 登录跳转URL"""
params = {
"client_id": CASDOOR_CLIENT_ID,
"response_type": "code",
"redirect_uri": CASDOOR_REDIRECT_URI,
"scope": "openid profile email",
"state": state or "aiflow",
}
return f"{CASDOOR_ENDPOINT}/login/oauth/authorize?{urlencode(params)}"
async def exchange_code_for_token(code: str) -> Optional[dict]:
"""用授权码换取 access_token 和用户信息"""
try:
async with httpx.AsyncClient(timeout=30) as client:
# 获取 token
token_resp = await client.post(
f"{CASDOOR_ENDPOINT}/api/login/oauth/access_token",
json={
"grant_type": "authorization_code",
"client_id": CASDOOR_CLIENT_ID,
"client_secret": CASDOOR_CLIENT_SECRET,
"code": code,
"redirect_uri": CASDOOR_REDIRECT_URI,
},
)
if token_resp.status_code != 200:
logger.error(f"Casdoor token exchange failed: {token_resp.status_code} {token_resp.text}")
return None
token_data = token_resp.json()
access_token = token_data.get("access_token")
if not access_token:
logger.error("No access_token in Casdoor response")
return None
# 获取用户信息
user_resp = await client.get(
f"{CASDOOR_ENDPOINT}/api/account",
headers={"Authorization": f"Bearer {access_token}"},
)
if user_resp.status_code != 200:
# 尝试备用端点
user_resp = await client.get(
f"{CASDOOR_ENDPOINT}/api/userinfo",
headers={"Authorization": f"Bearer {access_token}"},
)
if user_resp.status_code != 200:
logger.error(f"Casdoor userinfo failed: {user_resp.status_code}")
return {"access_token": access_token, "user": None}
user_info = user_resp.json()
return {"access_token": access_token, "user": user_info}
except Exception as e:
logger.error(f"Casdoor token exchange error: {e}")
return None
async def get_or_create_user(db: Session, casdoor_user: dict) -> User:
"""根据 Casdoor 用户信息获取或创建本地用户"""
casdoor_id = casdoor_user.get("id", casdoor_user.get("name", ""))
casdoor_name = casdoor_user.get("name", casdoor_user.get("preferred_username", ""))
casdoor_email = casdoor_user.get("email", "")
casdoor_display = casdoor_user.get("displayName", casdoor_user.get("name", ""))
# 通过 Casdoor ID 查找已有用户
existing = db.query(User).filter(User.casdoor_id == casdoor_id).first() if hasattr(User, "casdoor_id") else None
# 通过用户名查找
if not existing and casdoor_name:
existing = db.query(User).filter(User.username == f"sso_{casdoor_name}").first()
if existing and hasattr(existing, "casdoor_id"):
existing.casdoor_id = casdoor_id
if existing:
# 更新最后登录时间
existing.last_login = datetime.now()
db.commit()
return existing
# 创建新用户
username = f"sso_{casdoor_name}" if casdoor_name else f"sso_{casdoor_id[:8]}"
# 避免用户名冲突
base_username = username
counter = 1
while db.query(User).filter(User.username == username).first():
username = f"{base_username}_{counter}"
counter += 1
new_user = User(
id=gen_uuid(),
username=username,
email=casdoor_email or None,
password_hash=hash_password(casdoor_id + datetime.now().isoformat()), # 随机密码
role="user",
status="active",
display_name=casdoor_display or username,
)
# 如果 User 模型有 casdoor_id 字段则设置
if hasattr(new_user, "casdoor_id"):
new_user.casdoor_id = casdoor_id
db.add(new_user)
db.commit()
db.refresh(new_user)
logger.info(f"Created SSO user: {username} (casdoor_id={casdoor_id})")
return new_user
# ==================== API 路由 ====================
@router.get("/casdoor/login")
async def casdoor_login(state: str = ""):
"""跳转到 Casdoor 登录页面"""
login_url = get_casdoor_login_url(state)
return RedirectResponse(url=login_url)
@router.get("/casdoor/callback")
async def casdoor_callback(
code: str,
state: str = "",
db: Session = Depends(get_db),
):
"""Casdoor OAuth 回调"""
result = await exchange_code_for_token(code)
if not result or not result.get("user"):
# 重定向到前端登录页,带错误参数
return RedirectResponse(url=f"{APP_FRONTEND_URL}/#/login?error=casdoor_failed")
casdoor_user = result["user"]
user = await get_or_create_user(db, casdoor_user)
# 生成平台 JWT token
token_data = {"sub": user.id, "username": user.username, "role": user.role}
access_token = create_access_token(token_data)
# 重定向到前端,带 token
redirect_url = f"{APP_FRONTEND_URL}/#/login?token={access_token}&sso=casdoor"
return RedirectResponse(url=redirect_url)
@router.get("/casdoor/status")
async def casdoor_status():
"""检查 Casdoor SSO 是否可用"""
try:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get(f"{CASDOOR_ENDPOINT}/api/health")
if resp.status_code == 200:
return {"enabled": True, "endpoint": CASDOOR_ENDPOINT}
except Exception:
pass
return {"enabled": False, "endpoint": CASDOOR_ENDPOINT}
# 配置说明:
# 1. 在 Casdoor 中创建应用,获取 Client ID 和 Client Secret
# 2. 设置环境变量:
# - CASDOOR_ENDPOINT=http://casdoor:8000
# - CASDOOR_CLIENT_ID=your_client_id
# - CASDOOR_CLIENT_SECRET=your_client_secret
# - CASDOOR_REDIRECT_URI=http://your-domain/app/api/auth/casdoor/callback
# 3. 在 User 模型中添加 casdoor_id 字段 (可选):
# casdoor_id = Column(String(128), unique=True, nullable=True)