Files
rar-autopass/app/web/admin_routes.py
Claude Agent b92055eb01 feat(web): self-service cheie/creds + admin web + email signup (PRD 3.3b)
US-007: rute web proprii /cont/roteste-cheie + /cont/rar-creds scoped pe
sesiune (C13), sectiune "Contul meu" cu cheie afisata o data.
US-010: rol admin (users.is_admin) + require_admin->403 + CLI set-admin +
bootstrap primul cont=admin (count_admins in BEGIN IMMEDIATE, anti-race).
US-011: panou /admin (activare/dezactivare conturi, CSRF + PRG), link admin
+ logout pe dashboard.
US-012: app/email.py notify_signup best-effort degradat fara SMTP + config smtp_*.
Fix: migrare defensiva users.is_admin/email_verified in _migrate.

VERIFY x2 context curat (PASS) + /code-review high. 393 teste pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:19:06 +00:00

130 lines
4.0 KiB
Python

"""Panou admin web /admin. US-011 PRD 3.3b.
Rute:
GET /admin — listeaza conturi in asteptare + active (require_admin)
POST /admin/activate — activeaza un cont (require_admin + CSRF, PRG)
POST /admin/deactivate — dezactiveaza un cont, nu permite id=1 (require_admin + CSRF, PRG)
"""
from __future__ import annotations
from pathlib import Path
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from .. import __version__
from ..accounts import list_accounts, set_active
from ..config import get_settings
from ..db import get_connection
from ..web.csrf import get_csrf_token, verify_csrf
from ..web.session import require_admin
router = APIRouter()
_TMPL = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "templates"))
def _ctx(request: Request, **extra) -> dict:
settings = get_settings()
return {"rar_env": settings.rar_env, "version": __version__, **extra}
def _emails_by_account(conn) -> dict[int, str | None]:
"""Intoarce primul email per account_id, intr-un singur query (fara N+1)."""
rows = conn.execute(
"SELECT account_id, email FROM users ORDER BY id"
).fetchall()
result: dict[int, str | None] = {}
for row in rows:
acc_id = int(row["account_id"])
if acc_id not in result:
result[acc_id] = row["email"]
return result
def _render_admin(request: Request, conn, *, error: str | None = None, status_code: int = 200):
"""Randeaza pagina admin.html cu lista de conturi si optional un mesaj de eroare."""
accounts = list_accounts(conn)
emails = _emails_by_account(conn)
for acct in accounts:
acct["email"] = emails.get(acct["id"])
pending = [a for a in accounts if not a["active"] and a["id"] != 1]
active = [a for a in accounts if a["active"] and a["id"] != 1]
default = next((a for a in accounts if a["id"] == 1), None)
return _TMPL.TemplateResponse(request, "admin.html", _ctx(
request,
csrf_token=get_csrf_token(request),
pending=pending,
active=active,
default_account=default,
error=error,
), status_code=status_code)
@router.get("/admin", response_class=HTMLResponse)
async def admin_get(request: Request):
"""Panou admin: conturi in asteptare + active."""
require_admin(request)
conn = get_connection()
try:
return _render_admin(request, conn)
finally:
conn.close()
@router.post("/admin/activate", response_class=HTMLResponse)
async def admin_activate(
request: Request,
account_id: int = Form(...),
csrf_token: str = Form(default=""),
):
"""Activeaza un cont. PRG: redirect 303 la /admin dupa succes."""
require_admin(request)
verify_csrf(request, csrf_token)
conn = get_connection()
try:
try:
set_active(conn, account_id, True)
except ValueError as exc:
return _render_admin(request, conn, error=str(exc), status_code=422)
finally:
conn.close()
return RedirectResponse("/admin", status_code=303)
@router.post("/admin/deactivate", response_class=HTMLResponse)
async def admin_deactivate(
request: Request,
account_id: int = Form(...),
csrf_token: str = Form(default=""),
):
"""Dezactiveaza un cont. Nu permite dezactivarea contului default id=1. PRG: redirect 303."""
require_admin(request)
verify_csrf(request, csrf_token)
if account_id == 1:
conn = get_connection()
try:
return _render_admin(
request, conn,
error="Contul default (id=1) nu poate fi dezactivat.",
status_code=422,
)
finally:
conn.close()
conn = get_connection()
try:
try:
set_active(conn, account_id, False)
except ValueError as exc:
return _render_admin(request, conn, error=str(exc), status_code=422)
finally:
conn.close()
return RedirectResponse("/admin", status_code=303)