from pathlib import Path from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from itsdangerous import BadSignature, SignatureExpired from pydantic import EmailStr from app.config import get_settings from app.db import log_download, upsert_lead from app.security import ( create_download_token, is_honeypot_filled, verify_download_token, ) router = APIRouter() _TEMPLATES_DIR = Path(__file__).parent / "templates" templates = Jinja2Templates(directory=str(_TEMPLATES_DIR)) DOWNLOADS = [ { "filename": "taller-wox-tecnico.zip", "icon": "🧩", "title": "Material técnico", "description": "Specs OpenAPI, configs y artefactos para importar a watsonx Orchestrate.", }, { "filename": "taller-wox-funcional.zip", "icon": "📚", "title": "Material funcional", "description": "Manual paso a paso del bootcamp y deck de slides.", }, ] ALLOWED_FILENAMES = {d["filename"] for d in DOWNLOADS} @router.get("/", response_class=HTMLResponse) def landing(request: Request, error: str | None = None): error_msg = None if error == "token-invalido": error_msg = "El link expiró o es inválido. Regístrate de nuevo para descargar el material." return templates.TemplateResponse( "index.html", {"request": request, "error": error_msg}, ) @router.post("/register") def register( request: Request, nombre: str = Form(..., min_length=2, max_length=80), email: EmailStr = Form(...), empresa: str = Form(..., min_length=2, max_length=100), consentimiento: str = Form(...), website: str | None = Form(default=""), ): if consentimiento != "on": raise HTTPException(status_code=400, detail="Consentimiento requerido") if is_honeypot_filled(website): fake_token = create_download_token(email="honeypot@discarded.local", nombre="x") return RedirectResponse(url=f"/descargas?token={fake_token}", status_code=303) client_ip = request.client.host if request.client else None user_agent = request.headers.get("user-agent") upsert_lead( nombre=nombre, email=str(email), empresa=empresa, ip=client_ip, user_agent=user_agent, consent=True, ) token = create_download_token(email=str(email), nombre=nombre) return RedirectResponse(url=f"/descargas?token={token}", status_code=303) @router.get("/descargas", response_class=HTMLResponse) def descargas(request: Request, token: str | None = None): if not token: return RedirectResponse(url="/?error=token-invalido", status_code=307) try: data = verify_download_token(token) except (SignatureExpired, BadSignature): return RedirectResponse(url="/?error=token-invalido", status_code=307) settings = get_settings() material_dir = Path(settings.material_dir) downloads_view = [] for d in DOWNLOADS: path = material_dir / d["filename"] downloads_view.append({ **d, "available": path.exists(), "size_mb": round(path.stat().st_size / (1024 * 1024), 1) if path.exists() else 0, }) return templates.TemplateResponse( "descargas.html", { "request": request, "nombre": data.get("nombre", "amig@"), "token": token, "downloads": downloads_view, }, ) @router.get("/download/{filename}") def download(request: Request, filename: str, token: str | None = None): if not token: raise HTTPException(status_code=401, detail="Missing token") try: data = verify_download_token(token) except (SignatureExpired, BadSignature): raise HTTPException(status_code=401, detail="Invalid or expired token") if filename not in ALLOWED_FILENAMES: raise HTTPException(status_code=404, detail="File not found") path = Path(get_settings().material_dir) / filename if not path.exists(): raise HTTPException(status_code=404, detail="File not available yet") client_ip = request.client.host if request.client else None log_download(lead_email=data["email"], filename=filename, ip=client_ip) return FileResponse( path=str(path), filename=filename, media_type="application/zip", )