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:
41
app/security.py
Normal file
41
app/security.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import secrets
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
|
||||
_basic = HTTPBasic()
|
||||
|
||||
|
||||
def _serializer() -> URLSafeTimedSerializer:
|
||||
return URLSafeTimedSerializer(get_settings().secret_key, salt="download")
|
||||
|
||||
|
||||
def create_download_token(email: str, nombre: str) -> str:
|
||||
return _serializer().dumps({"email": email, "nombre": nombre})
|
||||
|
||||
|
||||
def verify_download_token(token: str) -> dict:
|
||||
settings = get_settings()
|
||||
max_age_seconds = max(1, settings.token_expiry_hours * 3600)
|
||||
return _serializer().loads(token, max_age=max_age_seconds)
|
||||
|
||||
|
||||
def is_honeypot_filled(value: str | None) -> bool:
|
||||
return bool(value)
|
||||
|
||||
|
||||
def require_admin(credentials: HTTPBasicCredentials = Depends(_basic)) -> str:
|
||||
settings = get_settings()
|
||||
user_ok = secrets.compare_digest(credentials.username, settings.admin_user)
|
||||
pass_ok = secrets.compare_digest(credentials.password, settings.admin_pass)
|
||||
if not (user_ok and pass_ok):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid credentials",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
return credentials.username
|
||||
Reference in New Issue
Block a user