From d68cf37493790ac3412dc3b197e83431eea242d2 Mon Sep 17 00:00:00 2001 From: deploy Date: Thu, 24 Sep 2026 20:38:05 +0000 Subject: [PATCH] feat(plataforma): SMTP pela Hostinger, e o molde do realm no repositorio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O dominio athleticmap.com tem MX na Hostinger e SPF que so autoriza a Hostinger. A credencial foi medida: aceita em smtp.hostinger.com:587, recusada com 535 no Gmail e no Titan. O molde do realm nunca tinha sido empurrado — PROVISIONAMENTO_REALM_TEMPLATE estava vazio, e a etapa REALM_CRIADO teria parado nele logo depois do SMTP. --- moldes/realm-do-tenant.LEIA-ME.md | 53 +++++++++++++ moldes/realm-do-tenant.json | 87 +++++++++++++++++++++ tenants/plataforma/11-sealed-smtp.yaml | 15 ++++ tenants/plataforma/82-servico-captacao.yaml | 25 ++++++ 4 files changed, 180 insertions(+) create mode 100644 moldes/realm-do-tenant.LEIA-ME.md create mode 100644 moldes/realm-do-tenant.json create mode 100644 tenants/plataforma/11-sealed-smtp.yaml diff --git a/moldes/realm-do-tenant.LEIA-ME.md b/moldes/realm-do-tenant.LEIA-ME.md new file mode 100644 index 0000000..bff2d76 --- /dev/null +++ b/moldes/realm-do-tenant.LEIA-ME.md @@ -0,0 +1,53 @@ +———————————————————————————————————————————————————————————————————————————== +P5.4 — o realm de um tenant novo. + +DERIVADO de `tenants/plataforma/realm/athleticmap-realm.json`, que e o realm +que existe hoje. Os nove papeis e o cliente `spa` sao os de la — NAO inventei +nenhum, e um papel a mais aqui seria um papel que nenhum codigo verifica. + +── OS MARCADORES ─────────────────────────────────────────────────────────── + + {{REALM}} `athleticmap-` — decidido em 2026-09-19 + {{TENANT}} o nome comercial, que vira o subdominio + +SAO COISAS DIFERENTES desde aquela decisao. O realm e numerado porque uma +instancia so de Keycloak nao comporta quatro realms chamados `athleticmap`; o +subdominio e o nome que o cliente ve. + +── O DOMINIO DOS REDIRECTS ───────────────────────────────────────────────── + +`athleticmap.com`, e nao `athleticmap.influxdigital.com.br` — o canonico foi +decidido em 2026-09-16 (P0.9). O tenant `plataforma` ainda usa o antigo porque +o corte de dominio nao aconteceu; um tenant NOVO ja nasce no certo. + +Se o corte nao tiver acontecido quando o primeiro tenant for provisionado, ESTE +arquivo precisa mudar junto — e o sintoma de esquecer e um login que redireciona +para um host que nao resolve. + +── NAO HA USUARIO AQUI ───────────────────────────────────────────────────── + +O administrador do cliente e criado pela Admin API, DEPOIS do realm, porque ele +precisa de senha temporaria e de `UPDATE_PASSWORD` — e nenhum dos dois cabe num +arquivo versionado. Ver EtapaCriarRealm. +———————————————————————————————————————————————————————————————————————————== + + +— O SMTP, E POR QUE ELE VIVE AQUI ———————————————————————————————————— + +Quem manda o e-mail de boas-vindas e o PROPRIO KEYCLOAK, pela Admin API +(`execute-actions-email`), e nao o `servico-captacao`. + +Isso tem tres consequencias boas: + + 1. o nosso servico nao precisa de biblioteca de e-mail nem de configuracao de + SMTP — uma dependencia a menos e um segredo a menos por onde vazar; + 2. o link de definicao de senha e gerado e assinado pelo proprio Keycloak, com + validade propria. Nao ha token nosso a inventar, guardar nem expirar; + 3. "fechar a aba e voltar depois pelo link do e-mail" — criterio do card — + funciona de graca, porque o link nao depende de sessao nenhuma. + +A SENHA DO SMTP NAO FICA NESTE ARQUIVO. Os marcadores sao preenchidos em memoria, +no momento de criar o realm, a partir da configuracao do servico — o realm e +criado pela API, e nao commitado. Um `{{SMTP_SENHA}}` literal que chegasse ao +Gitea seria um segredo versionado, que e exatamente o que a regra do repositorio +proibe. diff --git a/moldes/realm-do-tenant.json b/moldes/realm-do-tenant.json new file mode 100644 index 0000000..7dc5a2e --- /dev/null +++ b/moldes/realm-do-tenant.json @@ -0,0 +1,87 @@ +{ + "realm": "{{REALM}}", + "enabled": true, + "displayName": "Athletic Map — {{TENANT}}", + "loginWithEmailAllowed": true, + "roles": { + "realm": [ + { + "name": "admin" + }, + { + "name": "atm_athlete" + }, + { + "name": "atm_trainer" + }, + { + "name": "atm_team_admin" + }, + { + "name": "atm_fed_admin" + }, + { + "name": "atm_admin" + }, + { + "name": "atm_cadastro" + }, + { + "name": "atm_financeiro" + }, + { + "name": "atm_financeiro-leitura" + } + ] + }, + "clients": [ + { + "clientId": "spa", + "name": "Athletic Map SPA", + "publicClient": true, + "protocol": "openid-connect", + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "redirectUris": [ + "https://{{TENANT}}.athleticmap.com/*" + ], + "webOrigins": [ + "https://{{TENANT}}.athleticmap.com" + ], + "attributes": { + "pkce.code.challenge.method": "S256", + "post.logout.redirect.uris": "https://{{TENANT}}.athleticmap.com/*" + }, + "protocolMappers": [ + { + "name": "funcionario_id", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "funcionario_id", + "claim.name": "funcionario_id", + "jsonType.label": "String", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + } + ], + "smtpServer": { + "host": "{{SMTP_HOST}}", + "port": "{{SMTP_PORTA}}", + "from": "{{SMTP_REMETENTE}}", + "fromDisplayName": "Athletic Map", + "replyTo": "{{SMTP_RESPONDER_PARA}}", + "replyToDisplayName": "Athletic Map", + "auth": "{{SMTP_AUTH}}", + "user": "{{SMTP_USUARIO}}", + "password": "{{SMTP_SENHA}}", + "starttls": "{{SMTP_STARTTLS}}", + "ssl": "false" + } +} diff --git a/tenants/plataforma/11-sealed-smtp.yaml b/tenants/plataforma/11-sealed-smtp.yaml new file mode 100644 index 0000000..c184a26 --- /dev/null +++ b/tenants/plataforma/11-sealed-smtp.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: smtp-credentials + namespace: plataforma-prod +spec: + encryptedData: + senha: AgCUr1cwmhKTRJG9TwQtxofrXy7jrLDuXUlSQWLeULng6/4jlyK5kDr75iFXNjYaensLAavBgc3ydp0SHe7D/cTIUurPbEWeWRbxZEjBjEOo1nL2Z5NHBV8dTmQ2XLIDGgIFFGXZlHeWQmPghdWTuL4r3TsNuMAialDcOrPcQTmii6LDqLF80kbftx6fk79YGHOe5qLwMhZeLm7mG6Jz91r1P+Pm0gpVD5b0vLL6ePCUuAb10DZlj3efzM9tEPs/46GnO9kQZ0GGgHAC8EekUzNckj1ztGzAMsFtLCmMwI2bU9M+HrlwkqcA57pK/4aQHhhh+O2HhNbLN3xQXU8zoA/MeHdVGV0WdGaqGZhrHVTrnEL3iEv4ybreCtw7+O95VkS7xQWrkFUYz5hdnab2t+dLhAYtwEsvH/ZdkWpz9P5lxg/z+YvW04Sl4uYjU710Y5lFg9o0udPqFT5mwcEr8i2Y8yLERfjDZVemlYl6FS80qaKQIZ4qRCtYqUM/jGbHxp3KY3j1wzEiQbXXwsBTSpuktVQevyHg1+5maj14tJmeDCx13PXVGkwbfQUqBchgpzbKWDBnQLh/pgaPElFfpEYeg+uIiEaEc/KlKPFBPh19D4AiNNZbJ6J4MSICgUxzQUwe6X+4ePgbODhvdH2CS0gPfiB7nX9Q3e/UFa5WdE5/DCAZ1ql2xy2chwKgcto1pbWqeU5onfMfy0f8Fl+jkSWO6JY6 + template: + metadata: + creationTimestamp: null + name: smtp-credentials + namespace: plataforma-prod diff --git a/tenants/plataforma/82-servico-captacao.yaml b/tenants/plataforma/82-servico-captacao.yaml index 7d730d4..5329a3d 100644 --- a/tenants/plataforma/82-servico-captacao.yaml +++ b/tenants/plataforma/82-servico-captacao.yaml @@ -210,6 +210,31 @@ spec: # Destino do aviso ao comercial. Vazio = lead e gravado e o # aviso vira log sem PII. Preencher quando houver SMTP. - { name: CAPTACAO_EMAIL_COMERCIAL, value: "" } + # ── SMTP ───────────────────────────────── + # + # HOSTINGER, e nao Google. O dominio `athleticmap.com` tem MX em + # mx1/mx2.hostinger.com e SPF `include:_spf.mail.hostinger.com`: + # mandar pelo Gmail falharia SPF justamente no e-mail que leva o + # link de senha de quem acabou de pagar. Medido em 2026-09-24 — a + # credencial e ACEITA em smtp.hostinger.com:587 e recusada (535) + # em smtp.gmail.com e em smtp.titan.email. + - { name: SMTP_HOST, value: "smtp.hostinger.com" } + - { name: SMTP_PORTA, value: "587" } + - { name: SMTP_USUARIO, value: "nao-responda@athleticmap.com" } + # `nao-responda@` de proposito: a senha dela vive no cluster, e uma + # caixa que nao guarda nada limita o estrago se o segredo vazar. Sem + # o RESPONDER_PARA, quem acabou de pagar e responde fala com o vazio. + - { name: SMTP_RESPONDER_PARA, value: "contato@athleticmap.com" } + - { name: SMTP_STARTTLS, value: "true" } + - { name: SMTP_AUTENTICAR, value: "true" } + # Vazia, o realm nasceria com um SMTP que so falha na hora de ENVIAR + # — depois de o cliente ter conta. A etapa recusa antes disso. + - name: SMTP_SENHA + valueFrom: + secretKeyRef: { name: smtp-credentials, key: senha } + # O molde do realm. Sem ele a etapa recusa nomeando a propriedade; o + # arquivo vive no mesmo clone do GitOps que o /gitops monta. + - { name: PROVISIONAMENTO_REALM_TEMPLATE, value: "/gitops/moldes/realm-do-tenant.json" } resources: requests: { cpu: 150m, memory: 320Mi } limits: { cpu: "1", memory: 768Mi }