init
This commit is contained in:
17
backend/Dockerfile
Normal file
17
backend/Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Установка зависимостей
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копирование кода приложения
|
||||
COPY main.py .
|
||||
|
||||
# Открытие порта
|
||||
EXPOSE 8000
|
||||
|
||||
# Запуск приложения
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
226
backend/main.py
Normal file
226
backend/main.py
Normal file
@ -0,0 +1,226 @@
|
||||
import os
|
||||
import psycopg2
|
||||
from fastapi import FastAPI, HTTPException, Request, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from jose import jwt, JWTError
|
||||
import requests
|
||||
from functools import lru_cache
|
||||
|
||||
app = FastAPI(title="Dex Demo Backend")
|
||||
|
||||
# CORS настройки
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Конфигурация из переменных окружения
|
||||
DB_HOST = os.getenv("DB_HOST", "postgres")
|
||||
DB_PORT = os.getenv("DB_PORT", "5432")
|
||||
DB_NAME = os.getenv("DB_NAME", "dexdemo")
|
||||
DB_USER = os.getenv("DB_USER", "dexdemo")
|
||||
DB_PASSWORD = os.getenv("DB_PASSWORD", "dexdemo")
|
||||
DEX_ISSUER = os.getenv("DEX_ISSUER", "https://dex.127.0.0.1.sslip.io/")
|
||||
DEX_JWKS_URL = f"{DEX_ISSUER}keys"
|
||||
|
||||
|
||||
class Organization(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
class Role(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class Link(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
url: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
email: str
|
||||
full_name: str
|
||||
organization: Optional[Organization]
|
||||
roles: List[Role]
|
||||
available_links: List[Link]
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
"""Создание подключения к БД"""
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
database=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD
|
||||
)
|
||||
return conn
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Database connection error: {str(e)}")
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_jwks():
|
||||
"""Получение JWKS от Dex для валидации JWT"""
|
||||
try:
|
||||
response = requests.get(DEX_JWKS_URL, verify=False, timeout=5)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"Error fetching JWKS: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def validate_jwt_token(token: str) -> dict:
|
||||
"""Валидация JWT токена"""
|
||||
try:
|
||||
# Получаем JWKS
|
||||
jwks = get_jwks()
|
||||
if not jwks:
|
||||
raise HTTPException(status_code=500, detail="Cannot fetch JWKS from Dex")
|
||||
|
||||
# Декодируем заголовок токена, чтобы получить kid
|
||||
unverified_header = jwt.get_unverified_header(token)
|
||||
kid = unverified_header.get("kid")
|
||||
|
||||
# Находим нужный ключ
|
||||
key = None
|
||||
for jwk in jwks.get("keys", []):
|
||||
if jwk.get("kid") == kid:
|
||||
key = jwk
|
||||
break
|
||||
|
||||
if not key:
|
||||
raise HTTPException(status_code=401, detail="Unable to find appropriate key")
|
||||
|
||||
# Валидируем токен
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
key,
|
||||
algorithms=["RS256"],
|
||||
audience=None,
|
||||
issuer=DEX_ISSUER,
|
||||
options={
|
||||
"verify_aud": False, # Отключаем проверку audience для демо
|
||||
"verify_at_hash": False
|
||||
}
|
||||
)
|
||||
|
||||
return payload
|
||||
except JWTError as e:
|
||||
raise HTTPException(status_code=401, detail=f"Invalid token: {str(e)}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=401, detail=f"Token validation error: {str(e)}")
|
||||
|
||||
|
||||
def get_user_email(request: Request) -> str:
|
||||
"""Извлечение email пользователя из JWT или заголовков"""
|
||||
# Попытка получить токен из заголовка Authorization
|
||||
auth_header = request.headers.get("Authorization")
|
||||
|
||||
if auth_header and auth_header.startswith("Bearer "):
|
||||
token = auth_header.split(" ")[1]
|
||||
payload = validate_jwt_token(token)
|
||||
|
||||
# Email может быть в разных полях
|
||||
email = payload.get("email") # payload.get("name")
|
||||
if email:
|
||||
return email
|
||||
|
||||
# Или берем из заголока от oauth2-proxy/DexAuthenticator
|
||||
email = request.headers.get("X-Auth-Request-Email") # request.headers.get("X-Auth-Request-User")
|
||||
|
||||
if not email:
|
||||
raise HTTPException(status_code=401, detail="No authentication information found")
|
||||
|
||||
return email
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health_check():
|
||||
"""Проверка здоровья сервиса"""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/api/user-info", response_model=UserInfo)
|
||||
def get_user_info(email: str = Depends(get_user_email)):
|
||||
"""Получение информации о пользователе"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Получаем информацию о пользователе
|
||||
cursor.execute("""
|
||||
SELECT u.email, u.full_name, u.organization_id, o.name as org_name
|
||||
FROM users u
|
||||
LEFT JOIN organizations o ON u.organization_id = o.id
|
||||
WHERE u.email = %s
|
||||
""", (email,))
|
||||
|
||||
user_row = cursor.fetchone()
|
||||
if not user_row:
|
||||
raise HTTPException(status_code=404, detail="User not found in database")
|
||||
|
||||
user_email, full_name, org_id, org_name = user_row
|
||||
|
||||
organization = None
|
||||
if org_id and org_name:
|
||||
organization = Organization(id=org_id, name=org_name)
|
||||
|
||||
# Получаем роли пользователя
|
||||
cursor.execute("""
|
||||
SELECT r.id, r.name, r.description
|
||||
FROM roles r
|
||||
JOIN user_roles ur ON r.id = ur.role_id
|
||||
JOIN users u ON ur.user_id = u.id
|
||||
WHERE u.email = %s
|
||||
""", (email,))
|
||||
|
||||
roles = [
|
||||
Role(id=row[0], name=row[1], description=row[2])
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
|
||||
# Получаем доступные ссылки на основе ролей
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT l.id, l.title, l.url, l.description
|
||||
FROM links l
|
||||
JOIN role_links rl ON l.id = rl.link_id
|
||||
JOIN user_roles ur ON rl.role_id = ur.role_id
|
||||
JOIN users u ON ur.user_id = u.id
|
||||
WHERE u.email = %s
|
||||
ORDER BY l.id
|
||||
""", (email,))
|
||||
|
||||
available_links = [
|
||||
Link(id=row[0], title=row[1], url=row[2], description=row[3])
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
|
||||
return UserInfo(
|
||||
email=user_email,
|
||||
full_name=full_name,
|
||||
organization=organization,
|
||||
roles=roles,
|
||||
available_links=available_links
|
||||
)
|
||||
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
8
backend/requirements.txt
Normal file
8
backend/requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
psycopg2-binary==2.9.9
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
python-jose[cryptography]==3.3.0
|
||||
requests==2.31.0
|
||||
|
||||
Reference in New Issue
Block a user