Visão geral do sistema
Como as peças se encaixam — atores, portões de entrada, cálculo de escopo e ciclo de vida do documento.
Documento de visão geral para discussão: mostra o caminho que um dado percorre do login até o banco, onde ficam as decisões de acesso e quais pontos ainda estão em aberto. Para o detalhe de cada rota, veja Endpoints; para as camadas de código, Arquitetura.
1. O que o sistema é
O Profile Server é o cadastro central de cooperativas (organizations) e seus documentos, compartilhado por vários aplicativos e vários clientes. Ele resolve três perguntas:
- Quem é você — autenticação delegada ao Keycloak.
- O que você alcança — um usuário só enxerga as cooperativas alcançadas pelos tenants de que participa.
- Os documentos estão em dia — upload, revisão (aprovado/rejeitado) e validade.
2. O fluxo completo em uma imagem
Do login até a resposta, com os dois caminhos de entrada lado a lado. O traço verde é o caminho feliz do usuário final, o roxo é o app-to-app, e os vermelhos finos são as saídas de erro. Para slide ou impressão, use os arquivos exportados: PNG · SVG · fonte .mmd.
Bloco 2 simplificado — pré-datava a persona por tenant
O ramo "X-Tenant-Id informado?" abaixo ainda mostra a lógica anterior a 2026-07-30 (header
ausente → sempre união). Hoje isso só vale pra persona global; quem não é global e não
manda o header é resolvido sozinho se tiver 1 tenant só, ou toma 403 com 0 ou 2+. A
resolução de persona também acontece no mesmo passo, não depois. Lógica exata e atualizada em
Autenticação. Os arquivos
exportados (PNG/SVG/.mmd) ainda não foram regerados.
3. Panorama — quem fala com quem
O painel nunca fala com o banco: tudo passa pela API. O Keycloak é a única fonte de
identidade — desde 2026-07-30 (PLANO-PERSONA-POR-TENANT.md) nenhuma persona vem mais de lá.
O banco guarda quem é o usuário e o que ele pode fazer: UserTenant.persona (por tenant) e
User.type === 'Global' (bypass total, não é por tenant). O Keycloak só carrega as roles de
exceção (modulo:acao[:deny]) além da identidade.
4. Os dois portões de entrada
Toda rota nasce sob um de dois prefixos, com middlewares e modelos de autorização diferentes. Essa separação é a decisão estrutural mais importante do sistema.
/api/* | /service/* | |
|---|---|---|
| Consumidor | pessoa no painel | outro sistema |
| Token | JWT de usuário | client credentials |
| Quem é o ator | req.user (IUser) | req.service (tenant resolvido pelo azp) |
| Autorização | RolesGuard (persona × módulo × ação) | apenas "o client existe e está ativo" |
| Escopo de dados | cooperativas dos tenants do usuário | tenant único do próprio client |
Ponto de atenção: o
allowedScopesdo tenant existe no schema mas é passado como lista vazia — não há autorização granular em/service/*. Qualquer client cadastrado e ativo chega em qualquer rota de serviço. Ver §10.
5. Ciclo de vida de uma requisição /api
Dois pontos que costumam surpreender:
- A autorização (persona) e o escopo (quais cooperativas) são independentes — e vêm de fontes
diferentes. O
RolesGuarddecide se você pode chamar a rota olhando a persona (banco,UserTenant.personaouUser.typese global) + roles de exceção do Keycloak; o escopo decide quais linhas voltam e é injetado no Prisma a partir doUser.type(Admin/Globalnão são limitados;Cooperative/Technician/Coordinatorsão). Um403nunca vem do tenant, e uma lista vazia nunca vem da persona. - O filtro é injetado na camada de banco, não nos use cases. Nenhum repositório de domínio sabe que existe multi-tenancy — o que torna impossível esquecer o filtro em uma query nova, mas também torna o comportamento invisível na leitura do código do use case.
6. Como o escopo é calculado
Este é o coração do multi-tenant: transformar "quem é o usuário" em uma lista de cooperativas.
Simplificado — pré-datava a persona por tenant
Igual ao bloco 2: o ramo abaixo (header ausente → sempre união) é o comportamento anterior a
2026-07-30. Hoje "ausente" só vira união pra persona global; pra quem não é global, vira
inferência automática (1 tenant só) ou 403 (0 ou 2+ tenants). O mesmo passo também resolve a
persona (UserTenant.persona), não só o escopo de organizations. Lógica exata em
Autenticação.
A lista resultante escopa, de uma vez só, Organization (pelo próprio id) e Document,
OrganizationAddress e OrganizationContact (por organizationId). É por isso que trocar o
tenant ativo encolhe a tela inteira, e não uma listagem só.
Hierarquia de dados
Tenant é o cruzamento de um App com um Customer — é a unidade que amarra tudo:
usuários entram por UserTenant, cooperativas por OrganizationTenant. Os dois vínculos são
M:N, então a mesma cooperativa pode ser alcançada por tenants diferentes (e é isso que torna o
seletor de tenant ativo necessário).
7. Ciclo de vida de um documento
O DocumentType define as regras (obrigatório, exige aprovação, tem validade, dias de validade
padrão, exige código de autenticidade); o Document é a instância daquela regra para uma
cooperativa. Vencimento é calculado, não persistido: não existe status EXPIRED nem
agendador que reavalie documentos — hoje, um documento vencido continua APPROVED no banco até
alguém olhar.
8. Camadas do código
Regra de ouro: domain nunca importa de infra. Use cases devolvem Either em vez de lançar
exceções, e o controller traduz Left em status HTTP.
9. O painel
Uma rota por área do menu, cada uma protegida por RequireRole no cliente — proteção de
usabilidade, não de segurança: a decisão real é sempre do backend.
| Rota | Módulo | Quem vê |
|---|---|---|
/customers, /apps, /tenants | plataforma | só global |
/users | usuários | só global |
/organizations, /documents, /document-types | operação | todas as personas (com ações variando) |
O painel busca em GET /api/me/permissions a lista de modulo:acao liberados e usa isso para
esconder menu e botão. O seletor de tenant ativo fica no rodapé da barra lateral e só aparece
para quem tem dois ou mais tenants.
10. Pontos em aberto — pauta sugerida
Levantados do código e dos planos já escritos no repositório.
Segurança e autorização
| Ponto | Situação | Impacto |
|---|---|---|
PermissionGuard (UMA) | importado mas nunca registrado — o APP_GUARD está comentado | @Permissions(...) está em todo controller e não executa. Ou se remove, ou se decide ativar |
allowedScopes em /service/* | passado como [], nunca validado | qualquer client ativo alcança qualquer rota de serviço |
| Restrição de cooperativas por usuário | especificado em PLANO-RESTRICAO-ORGANIZACAO-POR-USUARIO.md, não implementado | hoje todo usuário de um tenant vê todas as cooperativas dele |
users fora do tenant-admin | exclusão temporária desde 2026-07-17, revisitada e mantida em 2026-07-30 | tenant-admin não administra os próprios usuários, nem atribui persona a eles no tenant |
Operação e confiabilidade
| Ponto | Situação | Impacto |
|---|---|---|
| Cache de autenticação | gravação comentada no auth.middleware | toda requisição faz verificação de JWT, userinfo no Keycloak e consulta ao banco |
Cache de tenant em /service/* | lê a chave tenant_scope1: e grava tenant_scope: | o cache nunca acerta; toda chamada de serviço vai ao banco |
console.log de userInfo e roles | ativos no middleware | dados pessoais do usuário indo para o log a cada requisição |
| Testes ponta a ponta | 266 testes unitários, zero e2e | o encaixe controller + guard + Prisma não é exercitado automaticamente |
| Vencimento de documento | sem status próprio e sem rotina | ninguém é avisado quando um documento obrigatório vence |
Produto
| Ponto | Situação |
|---|---|
| Padronização de erros | PLANO-PADRONIZACAO-ERROS.md aprovado, não implementado (código estável + envelope + tradução no painel) |
| Criação de usuário via serviço | PLANO-CRIACAO-USUARIO-SERVICE.md |
| Tenant ativo | implementado (seletor "Representando" no painel + header X-Tenant-Id) |
| Persona por tenant | implementado (2026-07-30) — PLANO-PERSONA-POR-TENANT.md. Persona deixou de vir do Keycloak: UserTenant.persona por tenant, User.type === 'Global' pro bypass total |