fix: 恢复 rsync 误删的近期开发(agents/skills/mcp/knowledge/workflow/tenant/billing/storage/privacy/promotion/achievement/automation/code-exec/developer/distribution/im-bot/portal/casdoor 等后端模块 + 对应前端页面路由, 补回 main.py 挂载与 App.tsx 路由
This commit is contained in:
1 parent
d95b847fee
commit
e8e5ffa1a9
65 files changed
+35624
-339
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user