Static API Key for security

This commit is contained in:
Артём Кокос
2026-02-21 13:06:02 +07:00
parent 3fe2be5514
commit b30ce9ce2a

70
main.py
View File

@@ -1,13 +1,21 @@
import asyncio
import logging
from contextlib import asynccontextmanager
import os
from typing import Optional, List
from datetime import datetime, timedelta
from contextlib import asynccontextmanager
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI, Depends, HTTPException, Security, APIRouter
from fastapi.staticfiles import StaticFiles
from fastapi.security import APIKeyHeader
from starlette.status import HTTP_403_FORBIDDEN
from sqlalchemy import select
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from app.core.discovery import DiscoveryService
from app.core.state import state_manager
from app.core.scheduler import start_scheduler, scheduler, execute_lamp_command
@@ -16,8 +24,11 @@ from app.core.database import init_db, async_session
from app.models.device import GroupModel, DeviceModel, GroupCreateSchema
from app.models.schedule import ScheduleTask
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
load_dotenv()
API_KEY = os.getenv("IGNIS_API_KEY")
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
logging.basicConfig(
level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s"
@@ -28,6 +39,21 @@ discovery = DiscoveryService()
wiz = WizDriver()
async def verify_token(header_value: str = Depends(api_key_header)):
# Если ключ не задан в .env, пускаем (для локальной разработки),
# либо строго требуем, если мы в продакшене
if not API_KEY:
return None
if header_value == API_KEY:
return header_value
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
)
api_router = APIRouter(dependencies=[Depends(verify_token)])
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Жизненный цикл приложения."""
@@ -75,24 +101,24 @@ app = FastAPI(title="Ignis Core API", lifespan=lifespan)
# --- Эндпоинты Устройств ---
@app.get("/devices")
@api_router.get("/devices")
async def get_all_devices():
"""Список всех обнаруженных ламп."""
return state_manager.devices
@app.get("/groups")
@api_router.get("/groups")
async def get_groups():
"""Список всех созданных люстр/групп."""
return state_manager.groups
@app.get("/scenes")
@api_router.get("/scenes")
async def get_scenes():
return wiz.SCENES
@app.post("/groups")
@api_router.post("/groups")
async def create_group(data: GroupCreateSchema):
async with async_session() as session:
existing = await session.get(GroupModel, data.id)
@@ -109,7 +135,7 @@ async def create_group(data: GroupCreateSchema):
return {"status": "created", "group": data.name}
@app.post("/control/device/{device_id}")
@api_router.post("/control/device/{device_id}")
async def control_device(
device_id: str,
state: Optional[bool] = None,
@@ -144,7 +170,7 @@ async def control_device(
return {"device_id": device_id, "applied": params, "result": result}
@app.post("/control/group/{group_id}")
@api_router.post("/control/group/{group_id}")
async def control_group(
group_id: str,
state: Optional[bool] = None,
@@ -182,7 +208,7 @@ async def control_group(
return {"status": "ok", "applied": params, "sent_to": ips}
@app.delete("/groups/{group_id}")
@api_router.delete("/groups/{group_id}")
async def delete_group(group_id: str):
async with async_session() as session:
# Ищем в базе
@@ -204,7 +230,7 @@ async def delete_group(group_id: str):
return {"status": "deleted", "id": group_id}
@app.post("/discovery/rescan")
@api_router.post("/discovery/rescan")
async def rescan_network():
logger.info("🔄 Ручной перезапуск сканирования сети...")
@@ -215,7 +241,7 @@ async def rescan_network():
return {"status": "ok", "found": len(state_manager.devices)}
@app.post("/control/device/{device_id}/blink")
@api_router.post("/control/device/{device_id}/blink")
async def blink_device(device_id: str):
device = state_manager.devices.get(device_id)
if not device:
@@ -231,7 +257,7 @@ async def blink_device(device_id: str):
return {"status": "blink_sent"}
@app.post("/schedules/once")
@api_router.post("/schedules/once")
async def add_once_task(device_id: str, minutes: int, state: bool):
device = state_manager.devices.get(device_id)
if not device:
@@ -250,7 +276,7 @@ async def add_once_task(device_id: str, minutes: int, state: bool):
return {"status": "scheduled", "job_id": job.id, "run_at": run_time.isoformat()}
@app.post("/schedules/group/once")
@api_router.post("/schedules/group/once")
async def add_group_once_task(group_id: str, minutes: int, state: bool):
group = state_manager.groups.get(group_id)
if not group:
@@ -276,7 +302,7 @@ async def add_group_once_task(group_id: str, minutes: int, state: bool):
}
@app.get("/schedules/active")
@api_router.get("/schedules/active")
async def get_active_jobs():
jobs = []
for job in scheduler.get_jobs():
@@ -291,7 +317,7 @@ async def get_active_jobs():
return {"active_jobs": jobs}
@app.delete("/schedules/{job_id}")
@api_router.delete("/schedules/{job_id}")
async def cancel_task(job_id: str):
try:
scheduler.remove_job(job_id)
@@ -300,7 +326,7 @@ async def cancel_task(job_id: str):
raise HTTPException(status_code=404, detail="Задача не найдена")
@app.post("/schedules/at")
@api_router.post("/schedules/at")
async def add_task_at_time(
target_id: str,
run_at: datetime,
@@ -344,7 +370,7 @@ async def add_task_at_time(
return {"status": "scheduled", "jobs": job_ids, "run_at": run_at}
@app.post("/schedules/cron")
@api_router.post("/schedules/cron")
async def add_cron_task(
target_id: str,
hour: str,
@@ -384,9 +410,9 @@ async def add_cron_task(
return {"status": "cron_scheduled", "jobs": job_ids}
@app.get("/schedules/tasks")
@api_router.get("/schedules/tasks")
async def get_all_tasks():
"""Тот самый расширенный список задач для бота или фронта"""
"""Список задач для бота или фронта"""
jobs = []
for job in scheduler.get_jobs():
jobs.append(
@@ -402,6 +428,8 @@ async def get_all_tasks():
return {"tasks": jobs}
app.include_router(api_router)
# --- МОНТИРОВАНИЕ СТАТИКИ (ДОЛЖНО БЫТЬ ПОСЛЕ ВСЕХ API МАРШРУТОВ) ---
app.mount("/", StaticFiles(directory="static", html=True), name="static")