Modelo do encurtador em FastAPI

Gerado a partir de deploy-templates/fastapi (dado.cloud ab2f4c0).
This commit is contained in:
dado.cloud
2026-10-21 14:40:18 +00:00
parent 7006615b91
commit e0c5d12f3f
5 changed files with 251 additions and 14 deletions
+40 -4
View File
@@ -7,6 +7,7 @@ import html
import json
import os
import platform
import re
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
@@ -15,7 +16,7 @@ import fastapi
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
from links import FORMATO_CODIGO, ErroDeValidacao, criar_link, listar_links, registrar_clique
from links import FORMATO_CODIGO, ErroDeValidacao, buscar_link, criar_link, host_do_link, listar_links, registrar_clique
# Variável de ambiente de exemplo: na Dado, ela se configura no painel do app.
MENSAGEM = os.environ.get("MENSAGEM_BOAS_VINDAS") or "Defina a variável MENSAGEM_BOAS_VINDAS para trocar este texto."
@@ -36,6 +37,19 @@ PAGINA_INICIAL = (
.replace("{{MENSAGEM}}", html.escape(MENSAGEM))
)
PAGINA_NAO_ENCONTRADA = (PASTA_PAGINAS / "nao-encontrado.html").read_text(encoding="utf-8")
MODELO_AVISO = (PASTA_PAGINAS / "aviso.html").read_text(encoding="utf-8")
# A página de aviso não roda script nenhum: o navegador só aceita o CSS dela.
CSP_AVISO = "default-src 'none'; style-src 'unsafe-inline'; img-src data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
MARCADORES_AVISO = re.compile(r"\{\{(HOST|URL|CODIGO)\}\}")
def pagina_aviso(link: dict) -> str:
"""Página de aviso antes de sair: mostra para onde o link leva. Todo valor é
escapado (o link foi digitado por alguém) e entra numa passada só, para um
valor nunca ser lido como outro marcador."""
valores = {"HOST": host_do_link(link["url"]), "URL": link["url"], "CODIGO": link["codigo"]}
return MARCADORES_AVISO.sub(lambda m: html.escape(valores[m.group(1)]), MODELO_AVISO)
# Freio simples contra abuso: no máximo 30 links novos a cada 10 minutos por IP.
LIMITE_POR_IP = 30
@@ -171,14 +185,36 @@ async def criar(request: Request):
return JSONResponse(status_code=201, content=para_json(link, request))
# O redirecionamento é feito pelo servidor: GET /<código> → 302 para o link.
# Fica por último: as rotas fixas acima têm prioridade.
# GET /<código> não redireciona direto: mostra antes uma página de aviso com o
# destino. Sem ela, qualquer pessoa usaria o seu endereço para disfarçar um
# link de golpe. A página não conta clique — quem só olha não foi ao destino.
# Estas rotas ficam por último: as rotas fixas acima têm prioridade.
@app.get("/{codigo}", include_in_schema=False)
def aviso(codigo: str):
link = buscar_link(codigo) if FORMATO_CODIGO.match(codigo) else None
if link is None:
return HTMLResponse(PAGINA_NAO_ENCONTRADA, status_code=404)
return HTMLResponse(
pagina_aviso(link),
headers={
"Cache-Control": "no-store",
"Referrer-Policy": "no-referrer",
"X-Robots-Tag": "noindex, nofollow",
"Content-Security-Policy": CSP_AVISO,
},
)
# O botão "Continuar" leva para GET /<código>/ir: aqui o servidor soma o
# clique e redireciona (302) para o link.
@app.get("/{codigo}/ir", include_in_schema=False)
def redirecionar(codigo: str):
link = registrar_clique(codigo) if FORMATO_CODIGO.match(codigo) else None
if link is None:
return HTMLResponse(PAGINA_NAO_ENCONTRADA, status_code=404)
return RedirectResponse(link["url"], status_code=302, headers={"Cache-Control": "no-store"})
return RedirectResponse(
link["url"], status_code=302, headers={"Cache-Control": "no-store", "Referrer-Policy": "no-referrer"}
)
if __name__ == "__main__":