kin: BATON-SEC-003-backend_dev

This commit is contained in:
Gros Frumos 2026-03-21 08:12:01 +02:00
parent 8629f3e40b
commit 3a2ec11cc7
13 changed files with 593 additions and 125 deletions

View file

@ -4,14 +4,16 @@ import asyncio
import hashlib
import logging
import os
import secrets
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from typing import Any
from typing import Any, Optional
import httpx
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from backend import config, db, telegram
from backend.middleware import rate_limit_register, rate_limit_signal, verify_admin_token, verify_webhook_secret
@ -25,10 +27,17 @@ from backend.models import (
SignalResponse,
)
_api_key_bearer = HTTPBearer(auto_error=False)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def _hash_api_key(key: str) -> str:
"""SHA-256 хэш для API-ключа (без соли — для быстрого сравнения)."""
return hashlib.sha256(key.encode()).hexdigest()
def _hash_password(password: str) -> str:
"""Hash a password using PBKDF2-HMAC-SHA256 (stdlib, no external deps).
@ -105,7 +114,7 @@ app.add_middleware(
CORSMiddleware,
allow_origins=[config.FRONTEND_ORIGIN],
allow_methods=["POST"],
allow_headers=["Content-Type"],
allow_headers=["Content-Type", "Authorization"],
)
@ -117,12 +126,24 @@ async def health() -> dict[str, Any]:
@app.post("/api/register", response_model=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"])
api_key = secrets.token_hex(32)
result = await db.register_user(uuid=body.uuid, name=body.name, api_key_hash=_hash_api_key(api_key))
return RegisterResponse(user_id=result["user_id"], uuid=result["uuid"], api_key=api_key)
@app.post("/api/signal", response_model=SignalResponse)
async def signal(body: SignalRequest, _: None = Depends(rate_limit_signal)) -> SignalResponse:
async def signal(
body: SignalRequest,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(_api_key_bearer),
_: None = Depends(rate_limit_signal),
) -> SignalResponse:
if credentials is None:
raise HTTPException(status_code=401, detail="Unauthorized")
key_hash = _hash_api_key(credentials.credentials)
stored_hash = await db.get_api_key_hash_by_uuid(body.user_id)
if stored_hash is None or not secrets.compare_digest(key_hash, stored_hash):
raise HTTPException(status_code=401, detail="Unauthorized")
if await db.is_user_blocked(body.user_id):
raise HTTPException(status_code=403, detail="User is blocked")