feat(plataforma): SMTP pela Hostinger, e o molde do realm no repositorio

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.
This commit is contained in:
deploy
2026-09-24 20:38:05 +00:00
parent 5a60e81b1f
commit d68cf37493
4 changed files with 180 additions and 0 deletions
+53
View File
@@ -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-<N>` — 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.
+87
View File
@@ -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"
}
}
+15
View File
@@ -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
@@ -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 }