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