feat: initial implementation taller-wox.fitlabs.dev
Portal FastAPI + 5 endpoints REST para Bootcamp Agentic AI con watsonx Orchestrate (FactorIT). Single container, Coolify-ready. - Landing brandeado FIT con formulario de registro (honeypot anti-bot) - Tokens itsdangerous para descargas (24h expiry) - 5 endpoints API: historical/available procedures, member-insights, schedule, generate-report (Jinja2 + Plotly) - SQLite con upsert-on-email para leads + log de descargas - Admin endpoints (HTTP Basic): leads.json, leads.csv, stats - 23 tests pytest pasando - Dockerfile listo para Coolify con volúmenes persistentes (/app/leads.db, /app/app/data/reports_output, /app/material) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
42
app/admin.py
Normal file
42
app/admin.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import csv
|
||||
import io
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.responses import Response
|
||||
|
||||
from app.db import list_leads, stats
|
||||
from app.security import require_admin
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
|
||||
@router.get("/leads.json")
|
||||
def admin_leads_json(
|
||||
_user: str = Depends(require_admin),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
return list_leads(limit=limit, offset=offset)
|
||||
|
||||
|
||||
@router.get("/leads.csv")
|
||||
def admin_leads_csv(_user: str = Depends(require_admin)):
|
||||
rows = list_leads(limit=10_000, offset=0)
|
||||
buf = io.StringIO()
|
||||
if rows:
|
||||
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()))
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
else:
|
||||
buf.write("(no leads)\n")
|
||||
payload = "" + buf.getvalue()
|
||||
return Response(
|
||||
content=payload,
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": 'attachment; filename="leads.csv"'},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
def admin_stats(_user: str = Depends(require_admin)):
|
||||
return stats()
|
||||
Reference in New Issue
Block a user