kin: BATON-005-backend_dev
This commit is contained in:
parent
68a1c90541
commit
cb95c9928f
7 changed files with 219 additions and 7 deletions
|
|
@ -1,20 +1,25 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from backend import config, db, telegram
|
||||
from backend.middleware import rate_limit_register, verify_webhook_secret
|
||||
from backend.middleware import rate_limit_register, verify_admin_token, verify_webhook_secret
|
||||
from backend.models import (
|
||||
AdminBlockRequest,
|
||||
AdminCreateUserRequest,
|
||||
AdminSetPasswordRequest,
|
||||
RegisterRequest,
|
||||
RegisterResponse,
|
||||
SignalRequest,
|
||||
|
|
@ -24,6 +29,16 @@ from backend.models import (
|
|||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _hash_password(password: str) -> str:
|
||||
"""Hash a password using PBKDF2-HMAC-SHA256 (stdlib, no external deps).
|
||||
|
||||
Stored format: ``<salt_hex>:<dk_hex>``
|
||||
"""
|
||||
salt = os.urandom(16)
|
||||
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 260_000)
|
||||
return f"{salt.hex()}:{dk.hex()}"
|
||||
|
||||
# aggregator = telegram.SignalAggregator(interval=10) # v2.0 feature — отключено в v1 (ADR-004)
|
||||
|
||||
_KEEPALIVE_INTERVAL = 600 # 10 минут
|
||||
|
|
@ -108,6 +123,9 @@ async def register(body: RegisterRequest, _: None = Depends(rate_limit_register)
|
|||
|
||||
@app.post("/api/signal", response_model=SignalResponse)
|
||||
async def signal(body: SignalRequest) -> SignalResponse:
|
||||
if await db.is_user_blocked(body.user_id):
|
||||
raise HTTPException(status_code=403, detail="User is blocked")
|
||||
|
||||
geo = body.geo
|
||||
lat = geo.lat if geo else None
|
||||
lon = geo.lon if geo else None
|
||||
|
|
@ -139,6 +157,44 @@ async def signal(body: SignalRequest) -> SignalResponse:
|
|||
return SignalResponse(status="ok", signal_id=signal_id)
|
||||
|
||||
|
||||
@app.get("/admin/users", dependencies=[Depends(verify_admin_token)])
|
||||
async def admin_list_users() -> list[dict]:
|
||||
return await db.admin_list_users()
|
||||
|
||||
|
||||
@app.post("/admin/users", status_code=201, dependencies=[Depends(verify_admin_token)])
|
||||
async def admin_create_user(body: AdminCreateUserRequest) -> dict:
|
||||
password_hash = _hash_password(body.password) if body.password else None
|
||||
result = await db.admin_create_user(body.uuid, body.name, password_hash)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=409, detail="User with this UUID already exists")
|
||||
return result
|
||||
|
||||
|
||||
@app.put("/admin/users/{user_id}/password", dependencies=[Depends(verify_admin_token)])
|
||||
async def admin_set_password(user_id: int, body: AdminSetPasswordRequest) -> dict:
|
||||
changed = await db.admin_set_password(user_id, _hash_password(body.password))
|
||||
if not changed:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.put("/admin/users/{user_id}/block", dependencies=[Depends(verify_admin_token)])
|
||||
async def admin_block_user(user_id: int, body: AdminBlockRequest) -> dict:
|
||||
changed = await db.admin_set_blocked(user_id, body.is_blocked)
|
||||
if not changed:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
user = await db.admin_get_user_by_id(user_id)
|
||||
return user # type: ignore[return-value]
|
||||
|
||||
|
||||
@app.delete("/admin/users/{user_id}", status_code=204, dependencies=[Depends(verify_admin_token)])
|
||||
async def admin_delete_user(user_id: int) -> None:
|
||||
deleted = await db.admin_delete_user(user_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
|
||||
@app.post("/api/webhook/telegram")
|
||||
async def webhook_telegram(
|
||||
request: Request,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue