feat(web): self-onboarding multi-tenant + auth sesiune (PRD 3.3a)

Canalul web trece de la 100% deschis (hardcodat cont 1) la autentificat si
multi-tenant. Un service nou se inregistreaza din browser, primeste o cheie API
(o singura data) si o sesiune; contul se creeaza "in asteptare" (active=0) si nu
trimite la RAR pana la activarea de catre admin (tools/account.py activate).

- users + app/users.py: parole scrypt (salt per-user, eticheta parametri onorata
  la verify pentru migrare cost), email unic case-insensitive
- sesiune: SessionMiddleware (same_site=strict, https_only config) + app/web/session.py
  (current_account/web_account/require_login->LoginRequired, set_session clear-inainte)
- CSRF (app/web/csrf.py) enforce in prod inclusiv pe login/signup + rate-limit
  in-proces (app/web/ratelimit.py) pe signup si login
- signup/login/logout (app/web/auth_routes.py): signup tranzactie atomica,
  cheie-o-data, log SIGNUP pentru descoperire admin
- dashboard + import scoped pe contul sesiunii (regula NULL->cont 1); toate rutele
  web care ating date sensibile sub require_login; nomenclator ramane global
- banner "cont in asteptare" pentru conturi active=0
- gate worker: claim_one LEFT JOIN accounts COALESCE(active,1)=1 (account_id NULL=activ)

VERIFY context curat (2 runde): leak cross-account /_fragments/mapari prins+reparat.
/code-review high: csrf_token lipsa pe re-randari de eroare, scrypt_params ignorat,
login fara rate-limit -- toate reparate. 361 teste pass (de la 313).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-18 16:43:21 +00:00
parent 748ab8b289
commit 504b490d3b
29 changed files with 2264 additions and 106 deletions

View File

@@ -8,6 +8,7 @@ Pornire dev: uvicorn app.main:app --reload
from __future__ import annotations
import secrets
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from pathlib import Path
@@ -16,6 +17,8 @@ from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, PlainTextResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import RedirectResponse
from . import __version__
from .api.v1.import_router import router as import_v1_router
@@ -24,6 +27,9 @@ from .config import get_settings
from .db import get_connection, init_db, queue_depth, read_heartbeat
from .security import install_log_redaction
from .web.routes import router as web_router
from .web.auth_routes import router as auth_router
from .web.csrf import CsrfError
from .web.session import LoginRequired
@asynccontextmanager
@@ -35,6 +41,26 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="Gateway RAR AUTOPASS", version=__version__, lifespan=lifespan)
settings = get_settings()
_session_secret = settings.session_secret or secrets.token_hex(32)
app.add_middleware(
SessionMiddleware,
secret_key=_session_secret,
session_cookie="autopass_session",
https_only=settings.session_https_only,
same_site="strict",
)
@app.exception_handler(LoginRequired)
async def login_required_handler(request: Request, exc: LoginRequired) -> RedirectResponse:
return RedirectResponse("/login", status_code=303)
@app.exception_handler(CsrfError)
async def csrf_error_handler(request: Request, exc: CsrfError) -> JSONResponse:
return JSONResponse(status_code=403, content={"detail": "CSRF invalid"})
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
@@ -59,6 +85,7 @@ app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
app.include_router(api_v1_router)
app.include_router(import_v1_router)
app.include_router(web_router)
app.include_router(auth_router)
@app.get("/healthz")