Add SSO
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
# Секретный ключ для JWT (сгенерируйте свой!)
|
||||
SECRET_KEY=your-secret-key-here-change-this-in-production
|
||||
|
||||
# Алгоритм шифрования
|
||||
ALGORITHM=HS256
|
||||
|
||||
# Время жизни токена в минутах
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=43200
|
||||
154
backend/main.py
154
backend/main.py
@@ -1,6 +1,6 @@
|
||||
from fastapi import FastAPI, WebSocket, UploadFile, File, HTTPException, Depends, status
|
||||
from fastapi import FastAPI, WebSocket, UploadFile, File, HTTPException, Depends, status, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
import asyncio
|
||||
import subprocess
|
||||
@@ -14,9 +14,35 @@ import json
|
||||
from passlib.context import CryptContext
|
||||
from jose import JWTError, jwt
|
||||
from datetime import datetime, timedelta
|
||||
from authlib.integrations.starlette_client import OAuth
|
||||
from authlib.common.errors import AuthlibBaseError
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
from oidc_config import get_enabled_providers, get_redirect_uri, OIDC_PROVIDERS
|
||||
|
||||
# Загружаем переменные окружения
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI(title="MC Panel")
|
||||
|
||||
# Инициализация OAuth
|
||||
oauth = OAuth()
|
||||
|
||||
# Регистрация ZITADEL провайдера
|
||||
enabled_providers = get_enabled_providers()
|
||||
if "zitadel" in enabled_providers:
|
||||
config = enabled_providers["zitadel"]
|
||||
oauth.register(
|
||||
name="zitadel",
|
||||
client_id=config["client_id"],
|
||||
client_secret=config["client_secret"],
|
||||
server_metadata_url=config["server_metadata_url"],
|
||||
client_kwargs={"scope": " ".join(config["scopes"])}
|
||||
)
|
||||
print(f"✓ ZITADEL провайдер зарегистрирован: {config['issuer']}")
|
||||
else:
|
||||
print("⚠ ZITADEL провайдер не настроен. Проверьте .env файл.")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
@@ -139,6 +165,130 @@ def check_server_access(user: dict, server_name: str):
|
||||
return server_name in user.get("servers", [])
|
||||
|
||||
# API для аутентификации
|
||||
|
||||
# OpenID Connect endpoints
|
||||
@app.get("/api/auth/oidc/providers")
|
||||
async def get_oidc_providers():
|
||||
"""Получить список доступных OpenID Connect провайдеров"""
|
||||
providers = {}
|
||||
for provider_id, config in get_enabled_providers().items():
|
||||
providers[provider_id] = {
|
||||
"name": config["name"],
|
||||
"icon": config["icon"],
|
||||
"color": config["color"]
|
||||
}
|
||||
return providers
|
||||
|
||||
@app.get("/api/auth/oidc/{provider}/login")
|
||||
async def oidc_login(provider: str, request: Request):
|
||||
"""Начать процесс аутентификации через OpenID Connect"""
|
||||
if provider not in get_enabled_providers():
|
||||
raise HTTPException(404, f"Провайдер {provider} не найден или не настроен")
|
||||
|
||||
try:
|
||||
redirect_uri = get_redirect_uri(provider, os.getenv("BASE_URL", "http://localhost:8000"))
|
||||
return await oauth.create_client(provider).authorize_redirect(request, redirect_uri)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Ошибка инициализации OAuth: {str(e)}")
|
||||
|
||||
@app.get("/api/auth/oidc/{provider}/callback")
|
||||
async def oidc_callback(provider: str, request: Request):
|
||||
"""Обработка callback от OpenID Connect провайдера"""
|
||||
if provider not in get_enabled_providers():
|
||||
raise HTTPException(404, f"Провайдер {provider} не найден или не настроен")
|
||||
|
||||
try:
|
||||
client = oauth.create_client(provider)
|
||||
|
||||
# Получаем токен от провайдера
|
||||
token = await client.authorize_access_token(request)
|
||||
|
||||
# Получаем данные пользователя
|
||||
user_data = token.get("userinfo")
|
||||
if not user_data:
|
||||
# Если userinfo нет в токене, парсим id_token
|
||||
user_data = token.get("id_token")
|
||||
if not user_data:
|
||||
raise HTTPException(400, "Не удалось получить данные пользователя")
|
||||
|
||||
# Создаём или обновляем пользователя
|
||||
username = create_or_update_oidc_user(user_data, provider)
|
||||
|
||||
# Создаём JWT токен для нашей системы
|
||||
users = load_users()
|
||||
user = users[username]
|
||||
access_token = create_access_token({"sub": username, "role": user["role"]})
|
||||
|
||||
# Перенаправляем на фронтенд с токеном
|
||||
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:3000")
|
||||
return RedirectResponse(f"{frontend_url}/?token={access_token}&username={username}")
|
||||
|
||||
except AuthlibBaseError as e:
|
||||
print(f"OAuth ошибка для {provider}: {str(e)}")
|
||||
raise HTTPException(400, f"OAuth ошибка: {str(e)}")
|
||||
except Exception as e:
|
||||
print(f"Ошибка аутентификации для {provider}: {str(e)}")
|
||||
raise HTTPException(500, f"Ошибка аутентификации: {str(e)}")
|
||||
|
||||
def create_or_update_oidc_user(user_data: dict, provider: str) -> str:
|
||||
"""Создать или обновить пользователя из OpenID Connect данных"""
|
||||
users = load_users()
|
||||
|
||||
# Генерируем уникальное имя пользователя
|
||||
email = user_data.get("email", "")
|
||||
name = user_data.get("name", "")
|
||||
sub = user_data.get("sub", "")
|
||||
|
||||
# Пытаемся использовать email как username
|
||||
if email:
|
||||
base_username = email.split("@")[0]
|
||||
elif name:
|
||||
base_username = name.replace(" ", "_").lower()
|
||||
else:
|
||||
base_username = f"{provider}_user"
|
||||
|
||||
# Убираем недопустимые символы
|
||||
import re
|
||||
base_username = re.sub(r'[^a-zA-Z0-9_-]', '', base_username)
|
||||
|
||||
# Ищем существующего пользователя по OIDC ID
|
||||
oidc_id = f"{provider}:{sub}"
|
||||
existing_user = None
|
||||
for username, user_info in users.items():
|
||||
if user_info.get("oidc_id") == oidc_id:
|
||||
existing_user = username
|
||||
break
|
||||
|
||||
if existing_user:
|
||||
# Обновляем существующего пользователя
|
||||
users[existing_user]["email"] = email
|
||||
users[existing_user]["name"] = name
|
||||
users[existing_user]["picture"] = user_data.get("picture")
|
||||
save_users(users)
|
||||
return existing_user
|
||||
else:
|
||||
# Создаём нового пользователя
|
||||
username = base_username
|
||||
counter = 1
|
||||
while username in users:
|
||||
username = f"{base_username}_{counter}"
|
||||
counter += 1
|
||||
|
||||
users[username] = {
|
||||
"username": username,
|
||||
"password": "", # Пустой пароль для OIDC пользователей
|
||||
"role": "user",
|
||||
"servers": [],
|
||||
"oidc_id": oidc_id,
|
||||
"email": email,
|
||||
"name": name,
|
||||
"picture": user_data.get("picture"),
|
||||
"provider": provider,
|
||||
"created_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
save_users(users)
|
||||
return username
|
||||
|
||||
@app.post("/api/auth/register")
|
||||
async def register(data: dict):
|
||||
users = load_users()
|
||||
|
||||
31
backend/oidc_config.py
Normal file
31
backend/oidc_config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Конфигурация OpenID Connect провайдеров
|
||||
"""
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
|
||||
# Конфигурация провайдеров OpenID Connect
|
||||
OIDC_PROVIDERS = {
|
||||
"zitadel": {
|
||||
"name": "ZITADEL",
|
||||
"client_id": os.getenv("ZITADEL_CLIENT_ID", ""),
|
||||
"client_secret": os.getenv("ZITADEL_CLIENT_SECRET", ""),
|
||||
"server_metadata_url": os.getenv("ZITADEL_ISSUER", "") + "/.well-known/openid-configuration",
|
||||
"issuer": os.getenv("ZITADEL_ISSUER", ""),
|
||||
"scopes": ["openid", "email", "profile"],
|
||||
"icon": "🔐",
|
||||
"color": "bg-purple-600 hover:bg-purple-700"
|
||||
}
|
||||
}
|
||||
|
||||
def get_enabled_providers() -> Dict[str, Dict[str, Any]]:
|
||||
"""Получить список включённых провайдеров (с настроенными client_id)"""
|
||||
enabled = {}
|
||||
for provider_id, config in OIDC_PROVIDERS.items():
|
||||
if config.get("client_id") and config.get("issuer"):
|
||||
enabled[provider_id] = config
|
||||
return enabled
|
||||
|
||||
def get_redirect_uri(provider_id: str, base_url: str = "http://localhost:8000") -> str:
|
||||
"""Получить redirect URI для провайдера"""
|
||||
return f"{base_url}/api/auth/oidc/{provider_id}/callback"
|
||||
@@ -7,6 +7,6 @@ python-multipart==0.0.6
|
||||
pydantic==2.5.3
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-jose[cryptography]==3.3.0
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-dotenv==1.0.0
|
||||
authlib==1.3.0
|
||||
httpx==0.26.0
|
||||
|
||||
@@ -44,5 +44,86 @@
|
||||
"timestamp": "2026-01-14T15:22:02.654579"
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"id": "2",
|
||||
"title": "Разраб даун",
|
||||
"description": "помогите разраб минды даун, а киро вообще маньяк на коммиты в гитею",
|
||||
"author": "MihailPrud",
|
||||
"status": "closed",
|
||||
"created_at": "2026-01-15T03:25:33.660528",
|
||||
"updated_at": "2026-01-15T03:27:41.117949",
|
||||
"messages": [
|
||||
{
|
||||
"author": "MihailPrud",
|
||||
"text": "помогите разраб минды даун, а киро вообще маньяк на коммиты в гитею",
|
||||
"timestamp": "2026-01-15T03:25:33.660528"
|
||||
},
|
||||
{
|
||||
"author": "system",
|
||||
"text": "Статус изменён на: В работе",
|
||||
"timestamp": "2026-01-15T03:25:56.445796"
|
||||
},
|
||||
{
|
||||
"author": "Sofa12345",
|
||||
"text": "Дааааа, туда этого бота",
|
||||
"timestamp": "2026-01-15T03:25:58.592839"
|
||||
},
|
||||
{
|
||||
"author": "MihailPrud",
|
||||
"text": "памагете",
|
||||
"timestamp": "2026-01-15T03:26:20.740325"
|
||||
},
|
||||
{
|
||||
"author": "Sofa12345",
|
||||
"text": "чим",
|
||||
"timestamp": "2026-01-15T03:26:29.038071"
|
||||
},
|
||||
{
|
||||
"author": "MihailPrud",
|
||||
"text": "у миня -30 и минет в школу надоть",
|
||||
"timestamp": "2026-01-15T03:26:37.692369"
|
||||
},
|
||||
{
|
||||
"author": "Sofa12345",
|
||||
"text": "пиздец нахуй блять",
|
||||
"timestamp": "2026-01-15T03:26:48.846565"
|
||||
},
|
||||
{
|
||||
"author": "MihailPrud",
|
||||
"text": "согласен",
|
||||
"timestamp": "2026-01-15T03:26:56.324587"
|
||||
},
|
||||
{
|
||||
"author": "Sofa12345",
|
||||
"text": "Nahyi eto school nyxna",
|
||||
"timestamp": "2026-01-15T03:27:15.968192"
|
||||
},
|
||||
{
|
||||
"author": "Sofa12345",
|
||||
"text": "pizdets",
|
||||
"timestamp": "2026-01-15T03:27:21.810953"
|
||||
},
|
||||
{
|
||||
"author": "MihailPrud",
|
||||
"text": "не нужна",
|
||||
"timestamp": "2026-01-15T03:27:24.548623"
|
||||
},
|
||||
{
|
||||
"author": "MihailPrud",
|
||||
"text": "но ходить надоть",
|
||||
"timestamp": "2026-01-15T03:27:31.625634"
|
||||
},
|
||||
{
|
||||
"author": "system",
|
||||
"text": "Статус изменён на: Закрыт",
|
||||
"timestamp": "2026-01-15T03:27:38.480740"
|
||||
},
|
||||
{
|
||||
"author": "MihailPrud",
|
||||
"text": "для баланса вселеннной",
|
||||
"timestamp": "2026-01-15T03:27:41.117949"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user