rbacuri.dpdns.org — status: online

Trocando de domínio em produção: quando a ordem dos passos quebra tudo

rbacuri.dpdns.org era um subdomínio de um provedor de DNS dinâmico gratuito — prático pra subir um home lab rápido, péssimo pra reputação de email: bastou testar envio transacional via um ESP de verdade pra levar vários bloqueios seguidos, simplesmente por estar num domínio compartilhado que qualquer um pode registrar um subdomínio. Comprei um domínio próprio (neojr.com) pra resolver isso na raiz. A parte “burocrática” (trocar nameserver, apontar DNS) foi rápida. A parte que exigiu cuidado de verdade foi a ordem em que cada peça podia mudar sem quebrar o que já estava em produção — com usuário real, QR code físico já na rua.

O inventário antes de mexer em qualquer coisa

Antes de tocar em DNS, levantei o que realmente dependia do domínio antigo: 5 hosts no Nginx Proxy Manager (app de QR code, portfólio, e três painéis administrativos), NEXTAUTH_URL fixado no .env do app, duas redirect URIs OAuth cadastradas (Google e Microsoft), um site key do Cloudflare Turnstile travado por hostname, e — o detalhe que quase passou batido — 2 QR codes dinâmicos já impressos e entregues fisicamente, com o domínio antigo codificado direto nos pixels da imagem.

Esse último ponto merece explicação: o conteúdo de um QR dinâmico não fica salvo no banco como uma URL fixa — é montado assim, no momento em que a página é aberta:

`${window.location.origin}/r/${qrCode.slug}`

Ou seja, o domínio nunca é gravado em lugar nenhum; ele é lido do navegador na hora de gerar a imagem. Isso significa duas coisas: QR codes novos, gerados depois da migração, saem automaticamente com o domínio certo — mas os que já foram baixados e impressos carregam o domínio antigo pra sempre, gravado fisicamente. Não tem como “atualizar” uma imagem já impressa.

A ordem que evita quebrar login

O maior risco não era técnico, era de sequenciamento. O NextAuth usa NEXTAUTH_URL pra montar a URL de callback do OAuth — não o host da requisição recebida, o valor fixo do env. Trocar esse valor e recriar o container antes de cadastrar a nova redirect URI no Google Cloud Console e no Azure Portal significa: ninguém loga em lugar nenhum. O domínio antigo já não bate mais com o NEXTAUTH_URL novo, e o domínio novo ainda não está autorizado nos provedores.

A ordem que funciona:

  1. Cadastra a redirect URI nova nos dois provedores, sem apagar a antiga ainda.
  2. Só depois troca NEXTAUTH_URL e recria o container.
  3. Testa login de verdade nos dois provedores antes de mexer em mais nada.

Mesmo seguindo essa ordem, o primeiro teste de login deu Erro 400: redirect_uri_mismatch — o Google compara a URI recebida exatamente, caractere por caractere, e eu tinha esquecido de autorizar o domínio novo (não só a redirect URI) em outro lugar do painel. Comparação de URI não perdoa barra final nem domínio faltando — vale conferir os dois campos, não só um.

Path preservado salva o QR já impresso

Pro QR físico continuar funcionando, o domínio antigo não podia simplesmente sumir — mas também não fazia sentido manter dois sites completos no ar indefinidamente. A solução foi trocar o Proxy Host do domínio antigo por um Redirection Host (recurso separado no Nginx Proxy Manager), com uma opção que é fácil de esquecer: Preserve Path. Sem ela, qualquer request pro domínio antigo cai na home do domínio novo — com ela, dominio-antigo/r/<slug> vira dominio-novo/r/<slug>, o mesmo slug, o mesmo banco, a imagem física continua escaneável.

qrcode.rbacuri.dpdns.org  →  301, Preserve Path  →  https://qrcode.neojr.com

Testei direto pra confirmar:

curl -sI "https://qrcode.rbacuri.dpdns.org/r/<slug>"
HTTP/2 301
location: https://qrcode.neojr.com/r/<slug>

Confirmando que o Cloudflare não quebrou o analytics

Depois de ligar o proxy (nuvem laranja) do Cloudflare na frente do app, sobrou uma dúvida concreta: o app grava um hash do IP de quem escaneia (sha256(salt:ip), truncado), lendo o header X-Forwarded-For. Com o Cloudflare no meio, será que esse header ainda carrega o IP real do visitante, ou passa a carregar o IP do próprio edge do Cloudflare?

Em vez de confiar, testei: fiz um scan de verdade, peguei meu IP público, calculei o hash esperado com o mesmo sal usado pelo app, e comparei com o que ficou gravado no banco:

CALC=$(printf "%s:%s" "$SALT" "$MEU_IP" | sha256sum | cut -c1-32)
DB_HASH=$(psql ... -c "select \"ipHash\" from \"ScanEvent\" order by \"scannedAt\" desc limit 1;")
[ "$CALC" = "$DB_HASH" ] && echo "bateu"

Bateu. O Cloudflare repassa o IP real corretamente na cadeia de X-Forwarded-For — analytics e rate limiting continuam válidos com o proxy ligado.

O quase-erro que fechou o ciclo

No fim da migração, fui atualizar a documentação do projeto pra refletir o domínio novo — e colei o domínio real direto no README, um repositório público. Alguns segundos depois lembrei: semanas atrás, numa auditoria de segurança no mesmo projeto, eu tinha trocado justamente esse domínio real por um placeholder genérico ({seu-dominio}), exatamente pra não amarrar um template reutilizável à minha infraestrutura pessoal. Reverti antes de qualquer commit.

A lição

A causa raiz de todo esse trabalho foi confiar a identidade de um projeto a um domínio que eu não controlava de verdade — reputação de terceiro, decisão de terceiro. Comprar o domínio próprio resolveu isso na raiz. Mas o quase-erro do README é o lembrete de que “isso é público” não é uma verificação que se faz uma vez só: é um hábito que precisa ser reaplicado toda vez que se mexe em documentação, mesmo em tarefas que parecem rotina — como só trocar um nome de domínio.