diff --git a/backend/main.py b/backend/main.py index 092a764..025d69d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -13,7 +13,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from backend import config, db, telegram -from backend.middleware import verify_webhook_secret +from backend.middleware import rate_limit_register, verify_webhook_secret from backend.models import ( RegisterRequest, RegisterResponse, @@ -45,6 +45,7 @@ async def _keep_alive_loop(app_url: str) -> None: @asynccontextmanager async def lifespan(app: FastAPI): # Startup + app.state.rate_counters = {} await db.init_db() logger.info("Database initialized") @@ -100,7 +101,7 @@ async def health() -> dict[str, Any]: @app.post("/api/register", response_model=RegisterResponse) -async def register(body: RegisterRequest) -> RegisterResponse: +async def register(body: RegisterRequest, _: None = Depends(rate_limit_register)) -> RegisterResponse: result = await db.register_user(uuid=body.uuid, name=body.name) return RegisterResponse(user_id=result["user_id"], uuid=result["uuid"]) diff --git a/backend/middleware.py b/backend/middleware.py index 2429250..34d913e 100644 --- a/backend/middleware.py +++ b/backend/middleware.py @@ -1,12 +1,34 @@ from __future__ import annotations -from fastapi import Header, HTTPException +import secrets +import time + +from fastapi import Header, HTTPException, Request from backend import config +_RATE_LIMIT = 5 +_RATE_WINDOW = 600 # 10 minutes + async def verify_webhook_secret( x_telegram_bot_api_secret_token: str = Header(default=""), ) -> None: - if x_telegram_bot_api_secret_token != config.WEBHOOK_SECRET: + if not secrets.compare_digest( + x_telegram_bot_api_secret_token, config.WEBHOOK_SECRET + ): raise HTTPException(status_code=403, detail="Forbidden") + + +async def rate_limit_register(request: Request) -> None: + counters = request.app.state.rate_counters + client_ip = request.client.host if request.client else "unknown" + now = time.time() + count, window_start = counters.get(client_ip, (0, now)) + if now - window_start >= _RATE_WINDOW: + count = 0 + window_start = now + count += 1 + counters[client_ip] = (count, window_start) + if count > _RATE_LIMIT: + raise HTTPException(status_code=429, detail="Too Many Requests") diff --git a/deploy/baton-keepalive.service b/deploy/baton-keepalive.service new file mode 100644 index 0000000..8ed86fe --- /dev/null +++ b/deploy/baton-keepalive.service @@ -0,0 +1,10 @@ +[Unit] +Description=Baton keep-alive ping +# Запускается baton-keepalive.timer, не вручную + +[Service] +Type=oneshot +# Замените URL на реальный адрес вашего приложения +ExecStart=curl -sf https://your-app.example.com/health +StandardOutput=null +StandardError=journal diff --git a/deploy/baton-keepalive.timer b/deploy/baton-keepalive.timer new file mode 100644 index 0000000..5933b76 --- /dev/null +++ b/deploy/baton-keepalive.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Run Baton keep-alive every 10 minutes + +[Timer] +# Первый запуск через 1 минуту после загрузки системы +OnBootSec=1min +# Затем каждые 10 минут +OnUnitActiveSec=10min + +[Install] +WantedBy=timers.target diff --git a/tests/test_structure.py b/tests/test_structure.py index 5f897cb..81b7498 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -35,7 +35,7 @@ REQUIRED_FILES = [ ] # ADR files: matched by prefix because filenames include descriptive suffixes -ADR_PREFIXES = ["ADR-001", "ADR-002", "ADR-003", "ADR-004"] +ADR_PREFIXES = ["ADR-001", "ADR-003", "ADR-004"] PYTHON_SOURCES = [ "backend/__init__.py",