Skip to content

Multi-Escola — Documentação Completa

Última atualização: 2026-02-24

Status da Implementação

EtapaDescriçãoStatus
1. select-roleAceita { papel, escola_id }, valida contra usuario_papeis✅ Implementado
2. verify-otpJWT escola_id derivado de usuario_papeis, bloqueios multi-escola✅ Implementado
3. /meBloqueios verificados para todas as escolas, retorna escola_nome por papel✅ Implementado
4. Login UITela de seleção agrupa por perfil + escola, cards distintos por escola✅ Implementado
5. Role SwitcherPopover lista perfis com nome da escola, troca envia escola_id✅ Implementado
6. Admin CRUDadmin-usuarios aceita papel_vinculos[] com N escolas por papel✅ Implementado
7. Gestor CRUDGestor da escola vincula usuário existente (CPF já cadastrado)Pendente
8. Docs + DeprecaçãoDocumentação atualizada, campo legado marcado✅ Este documento

Como Funciona — Fluxos Detalhados

1. Login com Múltiplas Escolas

Usuário digita CPF → send-otp → WhatsApp → verify-otp


                              Busca papéis em usuario_papeis
                              (com escola_nome via JOIN escolas)


                              Verifica bloqueios para CADA escola:
                              - Status da escola (suspensa/encerrada?)
                              - Assinatura (trial expirado?)


                              Agrupa perfis por UI:
                              coordenador+pedagogico+professor = "Coordenação"
                              escola = "Gestão"
                              etc.


                              Chave de agrupamento: perfil_ui::escola_id
                              Ex: "Coordenação::uuid-A" e "Coordenação::uuid-B"
                              são cards DISTINTOS


                         ┌──────────────────────────────┐
                         │  1 perfil único? Login direto │
                         │  N perfis? Tela de seleção    │
                         └──────────────────────────────┘


                              Usuário seleciona perfil
                              Frontend envia: { papel, escola_id }


                              select-role valida par em usuario_papeis
                              Re-assina JWT com escola_id correto
                              Atualiza usuarios.escola_id (write-through)

2. Troca de Perfil In-App (Role Switcher)

Usuário clica no avatar → Popover abre


Lista combinações perfil+escola do JWT roles[]
Cada item mostra: "Coordenação" + "Escola Alpha" (subtexto)


Usuário seleciona outro perfil
Frontend: invokeAction('select-role', 'select', { papel, escola_id })


Backend valida → re-assina JWT → cookie atualizado
Frontend recarrega com novo contexto

3. Admin Criando Usuário Multi-Escola

Admin abre formulário de criar usuário


Seção "Vínculos Papel + Escola":
  [+ Adicionar Vínculo]
  ┌──────────────────────────────────────┐
  │ Papel: [Coordenador ▼]              │
  │ Escola: [Escola Alpha ▼]            │
  │ [Adicionar]                          │
  └──────────────────────────────────────┘

Vínculos adicionados aparecem como badges:
  🏷️ Coordenador — Escola Alpha  [×]
  🏷️ Diretor — Escola Beta       [×]
  🏷️ Administrador (sem escola)  [×]


Salvar → Backend recebe papel_vinculos[]:
  [
    { papel_id: "uuid-coord", escola_id: "uuid-alpha" },
    { papel_id: "uuid-dir",   escola_id: "uuid-beta" },
    { papel_id: "uuid-admin", escola_id: null }
  ]


Backend cria registros em usuario_papeis
Seed de permissões por (escola_id, papel_id)
  → usuarios_escola_permissoes escopado por papel_id
  → usuarios_escola_sub_permissoes escopado por papel_id

4. Bloqueios por Escola

Quando uma escola está suspensa ou com trial expirado, apenas os perfis naquela escola são bloqueados. Exemplo:

Usuário com:
  - Coordenador na Escola A (ativa)     → ✅ Liberado
  - Coordenador na Escola B (suspensa)  → 🔒 Bloqueado
  - Administrador (sem escola)          → ✅ Liberado

Na tela de seleção:
  [Coordenação - Escola Alpha]  ← selecionável
  [Coordenação - Escola Beta]   ← card desabilitado com mensagem
  [Administrador]               ← selecionável

Arquitetura Técnica

Fonte da Verdade (SSOT)

ANTES (pré-Fev/2026): usuarios.escola_id (coluna única, 1:1)
AGORA: usuario_papeis.escola_id (tabela N:N)

A tabela usuario_papeis tem constraint UNIQUE em (usuario_id, papel_id, escola_id) e é a fonte da verdade para vínculos usuário-escola.

Permissões Escopadas por Papel (Abr/2026)

Desde Abr/2026, as tabelas usuarios_escola_permissoes e usuarios_escola_sub_permissoes incluem papel_id como dimensão obrigatória:

ANTES: UNIQUE (usuario_id, escola_id, permissao)
AGORA: UNIQUE (usuario_id, escola_id, papel_id, permissao)

Isso resolve o problema de colisão quando um usuário tem múltiplos papéis na mesma escola (ex: coordenador + diretor). Cada papel tem seu conjunto de permissões e sub-flags isolado.

Impacto no fluxo Multi-Escola:

  • Ao trocar de papel via Role Switcher, o /me resolve o papel_id correto e retorna menu/permissões isolados
  • Ao criar vínculo, o seed de permissões é escopado por papel_id
  • Ao remover vínculo, apenas permissões daquele papel_id são removidas

JWT Claims (Atualizado)

json
{
  "sub": "uuid-usuario",
  "principal_role": "coordenador",
  "escola_id": "uuid-escola-ativa",
  "roles": [
    { "nome": "coordenador", "escola_id": "uuid-escola-A" },
    { "nome": "coordenador", "escola_id": "uuid-escola-B" },
    { "nome": "administrador", "escola_id": null }
  ]
}

O escola_id raiz é o da escola ativa (selecionada). O array roles[] contém todos os vínculos.

RLS — Zero Alterações

As ~60 policies que usam auth.jwt() ->> 'escola_id' continuam funcionando sem alteração. O JWT sempre contém o escola_id correto para o contexto ativo.

Campo Legado usuarios.escola_id

  • Status: Mantido por compatibilidade
  • Atualizado: Via write-through em select-role (reflete última escola ativa)
  • NÃO é SSOT: Não deve ser usado para derivar o JWT
  • Deprecação futura: Pode ser removido quando todas as referências internas forem migradas para usuario_papeis

Trigger de Validação

O trigger fn_validar_escola_usuario_papel foi atualizado para permitir vincular papéis a qualquer escola (não mais restrito ao usuarios.escola_id). Garante que:

  • Papéis com escopo de escola têm escola_id NOT NULL e válido
  • Papéis globais (admin/especialista) têm escola_id = NULL

Arquivos Modificados

ArquivoMudança
supabase/functions/select-role/index.tsAceita { papel, escola_id }, valida contra usuario_papeis
supabase/functions/verify-otp/index.tsJWT escola_id de usuario_papeis, bloqueios multi-escola
supabase/functions/me/index.tsBloqueios em batch, retorna escola_nome por papel
supabase/functions/admin-usuarios/index.tsCRUD com papel_vinculos[]
src/components/login-unified.tsxAgrupamento por perfil::escola_id, cards distintos
src/components/role-switcher.tsxPopover com perfil+escola, envia escola_id
src/components/admin-usuarios.tsxFormulário de vínculos papel+escola
src/hooks/useAdminUsuarios.tsHook com papel_vinculos
src/contexts/auth-context.tsxBloqueios tipados com escola_id
src/types/auth.tsescola_nome no tipo Papel

Pendências (Etapa 7 — Gestor CRUD)

A Etapa 7 foi intencionalmente adiada. Ela trata do cenário onde o Gestor de uma escola cria um usuário cujo CPF já existe no sistema (vinculado a outra escola).

O que precisa ser implementado:

  1. supabase/functions/gestao-usuarios-escola/index.ts:

    • Na action create, antes de inserir em usuarios, verificar se o CPF já existe
    • Se existir: NÃO criar novo registro em usuarios
    • Apenas adicionar novo registro em usuario_papeis com escola_id do gestor
    • Seed de permissões em usuarios_escola_permissoes para o novo vínculo
    • Retornar mensagem informativa: "Usuário já existia e foi vinculado à sua escola"
  2. src/components/usuarios-escola.tsx:

    • Exibir feedback quando usuário é vinculado (vs. criado do zero)
    • Possível alerta: "Este CPF já está cadastrado em outra escola. Deseja vincular?"
  3. Considerações de segurança:

    • O gestor NÃO deve ver dados da outra escola
    • O gestor NÃO deve alterar dados pessoais (nome, telefone) se o usuário já existe
    • Apenas o vínculo usuario_papeis é criado

Outras pendências menores:

  • Testes E2E: Criar cenários de teste para login multi-escola (login → seleção → troca de perfil)
  • gestao-usuarios-escola update/delete: Ao remover um usuário da escola, desativar apenas o usuario_papeis dessa escola (não o usuário inteiro)
  • Migração de dados: Se houver usuários existentes com vínculos implícitos em múltiplas escolas que não estão em usuario_papeis, será necessário um script de migração one-time