Pular para conteúdo

Guia do código

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-14

Onde fica o quê em br.com.xadm.integracao.central. A organização é package-by-feature (padrão da casa — engenharia/stack.md): cada feature reúne seu Controller, Service, Repository e entidades; o transversal vive em comum. O porquê das escolhas está nas Decisões. A referência por classe (Javadoc) é gerada pelo CI de docs em dev/api/.

Mapa de pacotes (feature-first)

A → B na coluna de classes: o controller A delega ao service B.

Pacote Papel Classes principais
comum Transversal (nenhuma feature depende ciclicamente) RateLimiter, config/{CentralSettings,CorsOrigins,ClockFactory}, crypto/AesCipher, email/{EmailSender,ResendEmailSender,SmtpEmailSender}, error/ApiException, jwt/{JwtService,JwtValidator}, tenant/{Cliente,ClienteRepository}
login Autenticação de usuário (o core) AuthController → AuthLoginService, PabastPasswordHasher, FirebaseTokenValidatorService, PabastSenha(+repo), AppUserAccess(+repo — acesso unificado pAbast+oauth), NewAccessRequestNotifier
admin Superfície administrativa AdminController → ClienteAdminService + PabastUsuarioAdminService, OauthFirebaseAdminController → OauthFirebaseAdminService, AdminAuthFilter, AdminApiCorsFilter
centralapps Central de Apps (broker build-time) + auto-governança do cliente SetupController → ProvisioningService, AppCatalogController → AppCatalogService + RolesCatalogSyncService, AppSelfServiceController → UsuariosDoAppService, GarageBrokerController → GaragePresignService, SetupAuthFilter, AppUserAuthFilter, AppApiCorsFilter, RolesCatalogClient, FeatureProvider, App/AppRole/AppProviderResource(+repos)
coolify Client da API do Coolify (integração externa; slice compartilhado) CoolifyClient (injectEnvs/listApplications)
deploy Control-plane de deploy da frota (POST /api/ci/deploy) e painel da frota DeployController → DeployService + DeployStatusService, DeployFleetController → DeployFleetService, DeployReconcileService, DeployAuthFilter/DeployTokenGuard, DeployTarget/DeployAudit(+repos)
monitoramento Monitor de host/apps do Coolify (Fase 1, read-only) MonitoramentoController → MonitoramentoService, DockerStatsClient, HostMetricsReader
integradores Heartbeat do integrador-client: recebimento M2M, job de silêncio e painel admin (0028) HeartbeatController e IntegradoresAdminController → IntegradorInstanciaService, IntegradorAuthFilter/IntegradorTokenGuard, IntegradorInstancia(+repo), SilencioJob, AlertaSilencio/SentryAlertaSilencio, HorasUteis/FeriadosNacionais
diagnostico Smoke-tests rodáveis em produção (/test; o do GlitchTip exige xadm_admin) TestController
docs Poke de sync das docs (M2M do CI, sob o DeployAuthFilter) DocsController, DocsClient

Deps entre features são acíclicas (ex.: admin→login/centralapps, centralapps→login/coolify, monitoramento→coolify; nenhum volta; comum não depende de feature). integradores e diagnostico dependem só de comum: as rotas deles sob credencial admin (/api/admin/integradores, /test/glitchtip) são cobertas pelo AdminAuthFilter por path, sem importar admin. O CoolifyClient mora no slice coolify (não em centralapps) desde que monitoramento também passou a consumi-lo (decisão 0019). Ver também decisão 0010.

Dois efeitos práticos da regra "sem ciclos", que já morderam: o helper corsHeaders mora em comum/config/CorsOrigins, e não no filtro de admin, porque o filtro gêmeo de centralapps precisa dele; e o AdminContato do corpo do 403 é um record local ao pacote login, em vez de reusar o de centralapps.

Camadas dentro da feature

  • Controller (@Controller) é a borda HTTP: rota, Bean Validation do corpo, leitura dos atributos que o filtro pôs no request (o escopo (cliente, app) do claim, o gate de papel) e o mapeamento para os DTOs HTTP — record @Serdeable aninhados no próprio controller. Não injeta repository nem DataSource; injeta um ou dois services.
  • Service (@Singleton, sufixo Service) é a regra: consulta e grava pelos repositories, lança ApiException(status, code, msg) (problem+json, decisão 0016) e devolve entidade ou record próprio — PabastUsuarioAdminService.UsuarioComAcesso, AppCatalogService.AppDetalhe, UsuariosDoAppService.Usuarios —, nunca o DTO do controller. Nenhum service abre transação (@Transactional): cada chamada de repository commita sozinha.
  • Repository (@JdbcRepository) é chamado por service, job (SilencioJob) ou notifier (NewAccessRequestNotifier) — nunca por controller.
  • Entidade (@MappedEntity/@Embeddable) é dado: não conhece settings, container de beans, HTTP nem JDBC cru.

Fronteiras travadas

O ArchitectureTest (ArchUnit, roda no check) cobra as fronteiras de Java/Micronaut, Guarda de fronteiras de arquitetura. As cinco primeiras regras vêm da fábrica RegrasArquitetura da xadm-comum-teste; a de entidade é deste app:

Regra O que trava
IMPORT_NAO_VAZIO import de zero classe falha alto, em vez de deixar as outras regras passarem vácuas
SEM_CICLOS_ENTRE_FEATURES ciclo entre os pacotes de topo
BASE_COMUM_NAO_DEPENDE_DE_FEATURES comum dependendo de qualquer feature
CONTROLLER_NAO_ACESSA_REPOSITORY @Controller dependendo de repository (GenericRepository) ou de javax.sql
DOMINIO_NAO_DEPENDE_DE_CONTROLLER qualquer classe fora do controller dependendo dele ou de um DTO aninhado nele (o código que o Micronaut gera — bean definitions, introspecções, serdes — fica fora)
CONFIG_E_INFRA_NAO_VAZAM_PARA_O_DOMINIO entidade dependendo de comum.config, io.micronaut.context, io.micronaut.http ou javax.sql

Controller e repository moram no mesmo pacote, então as regras de camada são por tipo (anotação, herança), não por pacote. A guarda só morde com ArchUnit ≥ 1.4.1: abaixo disso o ASM não lê bytecode do JDK 25, descarta as classes em silêncio e as regras passam vácuas, verdes — é o que o IMPORT_NAO_VAZIO pega.

Fluxo de uma requisição

AuthController → limita (RateLimiter) → AuthLoginService, que consulta os repositories, confere a identidade (PabastPasswordHasher ou FirebaseTokenValidatorService) e emite o token (JwtService). Em /api/admin/** e no /test/glitchtip, o AdminAuthFilter roda antes do controller, validando o JWT interno com JwtValidator (decisão 0007).

Invariantes que o código mantém

  • alg=RS256 no JWKS — o JwtService adiciona à mão; o nimbus-jose-jwt não inclui por padrão e o PowerSync recusa sem ele (decisão 0002).
  • PabastSenhaRepository só por par (cliente_id, id) — os finders herdados findById/update/deleteById (por id sozinho) não devem ser usados: cruzariam clientes (decisão 0008).
  • Algoritmo pAbast imutável — PabastPasswordHasher é porta literal do integrador/Flutter; mudar a lógica quebra os hashes em produção.
  • Senha em claro nunca sai — as respostas do admin trazem os hashes pAbast (senha/senha_compl), que a UI admin lista; a senha em claro só entra, e o PabastUsuarioAdminService a hasheia antes de gravar.
  • Migrations Flyway congeladas — as V* já aplicadas não se editam; criar nova.