kin: BATON-SEC-003-backend_dev
This commit is contained in:
parent
097b7af949
commit
f17ee79edb
13 changed files with 593 additions and 125 deletions
|
|
@ -29,6 +29,7 @@ async def init_db() -> None:
|
|||
name TEXT NOT NULL,
|
||||
is_blocked INTEGER NOT NULL DEFAULT 0,
|
||||
password_hash TEXT DEFAULT NULL,
|
||||
api_key_hash TEXT DEFAULT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
|
|
@ -64,6 +65,7 @@ async def init_db() -> None:
|
|||
for stmt in [
|
||||
"ALTER TABLE users ADD COLUMN is_blocked INTEGER NOT NULL DEFAULT 0",
|
||||
"ALTER TABLE users ADD COLUMN password_hash TEXT DEFAULT NULL",
|
||||
"ALTER TABLE users ADD COLUMN api_key_hash TEXT DEFAULT NULL",
|
||||
]:
|
||||
try:
|
||||
await conn.execute(stmt)
|
||||
|
|
@ -73,12 +75,21 @@ async def init_db() -> None:
|
|||
await conn.commit()
|
||||
|
||||
|
||||
async def register_user(uuid: str, name: str) -> dict:
|
||||
async def register_user(uuid: str, name: str, api_key_hash: Optional[str] = None) -> dict:
|
||||
async with _get_conn() as conn:
|
||||
await conn.execute(
|
||||
"INSERT OR IGNORE INTO users (uuid, name) VALUES (?, ?)",
|
||||
(uuid, name),
|
||||
)
|
||||
if api_key_hash is not None:
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO users (uuid, name, api_key_hash) VALUES (?, ?, ?)
|
||||
ON CONFLICT(uuid) DO UPDATE SET api_key_hash = excluded.api_key_hash
|
||||
""",
|
||||
(uuid, name, api_key_hash),
|
||||
)
|
||||
else:
|
||||
await conn.execute(
|
||||
"INSERT OR IGNORE INTO users (uuid, name) VALUES (?, ?)",
|
||||
(uuid, name),
|
||||
)
|
||||
await conn.commit()
|
||||
async with conn.execute(
|
||||
"SELECT id, uuid FROM users WHERE uuid = ?", (uuid,)
|
||||
|
|
@ -87,6 +98,15 @@ async def register_user(uuid: str, name: str) -> dict:
|
|||
return {"user_id": row["id"], "uuid": row["uuid"]}
|
||||
|
||||
|
||||
async def get_api_key_hash_by_uuid(uuid: str) -> Optional[str]:
|
||||
async with _get_conn() as conn:
|
||||
async with conn.execute(
|
||||
"SELECT api_key_hash FROM users WHERE uuid = ?", (uuid,)
|
||||
) as cur:
|
||||
row = await cur.fetchone()
|
||||
return row["api_key_hash"] if row else None
|
||||
|
||||
|
||||
async def save_signal(
|
||||
user_uuid: str,
|
||||
timestamp: int,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class RegisterRequest(BaseModel):
|
|||
class RegisterResponse(BaseModel):
|
||||
user_id: int
|
||||
uuid: str
|
||||
api_key: str
|
||||
|
||||
|
||||
class GeoData(BaseModel):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue