- Reemplaza emojis por iconos SVG estilo Lucide en cards landing y descargas - Paleta refinada: slate grays + navy + accent amber-700 (B45309) - Hero con grid pattern sutil y gradients radiales - Cards con sombras suaves y borders, hover lift - Header sticky con backdrop-filter - Tipografía Inter con tracking ajustado - Botones con flechas SVG inline
137 lines
4.4 KiB
Python
137 lines
4.4 KiB
Python
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": "code",
|
|
"title": "Material técnico",
|
|
"description": "Specs OpenAPI, configs y artefactos para importar a watsonx Orchestrate.",
|
|
},
|
|
{
|
|
"filename": "taller-wox-funcional.zip",
|
|
"icon": "book",
|
|
"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",
|
|
)
|