Files
ignis-core/app/api/deps.py
Artem Kokos b6b25fa2a1 BBFbyOpus
2026-04-01 22:51:24 +07:00

81 lines
2.8 KiB
Python

import os
import hmac
import logging
from dataclasses import dataclass
from typing import Optional
from fastapi import Depends, HTTPException
from fastapi.security import APIKeyHeader
from starlette.status import HTTP_403_FORBIDDEN
from dotenv import load_dotenv
from sqlalchemy import select
from app.core.database import async_session
from app.models.api_key import ApiKeyModel
load_dotenv()
logger = logging.getLogger(__name__)
MASTER_KEY = os.getenv("IGNIS_API_KEY")
if not MASTER_KEY:
logger.warning("IGNIS_API_KEY не задан -- авторизация отключена!")
API_KEY_NAME = "X-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
@dataclass
class AuthContext:
"""Результат авторизации -- передаётся в роуты через Depends."""
is_master: bool # мастер-ключ из .env
is_admin: bool # право на CRUD групп, расписания, ресканирование
key_name: str # имя ключа (для логов)
async def verify_token(header_value: str = Depends(api_key_header)) -> AuthContext:
"""
Проверка API-ключа:
1. Если IGNIS_API_KEY не задан -- авторизация отключена, полный доступ
2. Мастер-ключ из .env -- полный доступ
3. Ключ из БД (api_keys) -- проверяем active и is_admin
4. Иначе -- 403
"""
# Авторизация отключена
if not MASTER_KEY:
return AuthContext(is_master=True, is_admin=True, key_name="no-auth")
if not header_value:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="API-ключ не передан"
)
# Мастер-ключ (timing-safe сравнение)
if hmac.compare_digest(header_value, MASTER_KEY):
return AuthContext(is_master=True, is_admin=True, key_name="master")
# Ищем в БД
async with async_session() as session:
result = await session.execute(
select(ApiKeyModel).where(ApiKeyModel.key == header_value)
)
api_key = result.scalar_one_or_none()
if api_key and api_key.active:
return AuthContext(
is_master=False,
is_admin=api_key.is_admin,
key_name=api_key.name,
)
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Неверный или деактивированный ключ"
)
def require_admin(auth: AuthContext = Depends(verify_token)) -> AuthContext:
"""Dependency для роутов, требующих админских прав."""
if not auth.is_admin:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Недостаточно прав")
return auth