Mural Olímpico — Aluno e Responsável
Última atualização: 2026-04-13
SSOT consolidado:docs/features/MURAL.md
⚠️ Este documento é referência secundária. A fonte de verdade canônica para o Mural Olímpico é
docs/features/MURAL.md, que inclui schema completo, fluxos detalhados, RLS, comportamento cross-escola e referência à auditoria LGPD.
1. Visão Geral
O Mural Olímpico é a interface pública acessível por alunos e responsáveis via /escola/:slug. Cada escola possui um slug único configurado na tabela escola_mural_config.
Fluxo Geral
/escola/:slug
├── Lookup escola (slug → escola_mural_config)
│ ├── Verifica status da escola (ativa, suspensa, encerrada)
│ ├── Verifica assinatura (subscription-helper.ts)
│ └── Verifica mural_ativo
├── Seleção: "Sou Aluno" | "Sou Responsável"
├── Fluxo Aluno
│ ├── Método A: Matrícula + Data de Nascimento (séries 6º-7º EF)
│ └── Método B: CPF + OTP WhatsApp (séries 8º-9º EF)
└── Fluxo Responsável
├── Login: CPF + OTP WhatsApp → Seletor de filhos
└── Cadastro: CPF + telefone + nome → OTP WhatsApp → registro global2. Métodos de Acesso
Por Série
A tabela escola_mural_config.metodo_acesso_por_serie define o método por série:
{
"6_ef": "A",
"7_ef": "A",
"8_ef": "B",
"9_ef": "B"
}| Método | Campos | Público-alvo | Justificativa |
|---|---|---|---|
| A | Matrícula + Data de Nascimento | Crianças (6º-7º) | Não possuem CPF/telefone |
| B | CPF + OTP WhatsApp | Adolescentes (8º-9º) | Possuem CPF e telefone |
Responsável
Sempre usa CPF + OTP WhatsApp, independente da série do filho.
Canal de entrega: O OTP é enviado via WhatsApp (Wasender) — provedor exclusivo de mensagens da plataforma.
RLS de escola_mural_config
| Papel | Operações | Filtro |
|---|---|---|
escola (gestor) | SELECT, INSERT, UPDATE, DELETE | escola_id próprio |
coordenador, diretor | SELECT (leitura do slug para copiar link no Mural) | escola_id próprio |
Demais (aluno, responsavel, administrador, etc.) | — | sem acesso direto |
Todas as policies usam lower((auth.jwt()->>'principal_role')) para comparação case-insensitive (padrão do projeto, ver RLS_DESIGN_GUIDE.md).
3. Autenticação do Mural
JWT Separado
| Aspecto | Sistema Principal | Mural — Aluno | Mural — Responsável |
|---|---|---|---|
| Secret | OLP_JWT_SECRET | OLP_JWT_SECRET | OLP_JWT_SECRET |
| Cookie | olp_auth | olp_mural | olp_mural |
| Expiração | 8h | 2h | 2h |
| Escopo | Completo (CRUD) | portal_readonly | portal_readonly |
escola_id | UUID da escola | UUID da escola | null (global) |
Claims do Token do Responsável
{
"sub": "uuid-responsavel",
"tipo": "responsavel",
"nome_completo": "Nome",
"escola_id": null,
"serie_id": null,
"turma_id": null,
"matricula": null,
"escopo": "portal_readonly"
}
escola_id: null— o responsável é uma identidade global por CPF. O acesso aos dados dos filhos é resolvido viaget_alunos_responsavel()(SECURITY DEFINER).
4. Responsável — Modelo de Identidade
Identidade Global por CPF
responsaveis.escola_idé semprenull(campo legado)- CPF é UNIQUE global — um registro por responsável em toda a plataforma
- Vínculo com escolas derivado via
aluno_responsaveis → alunos.escola_id - Um responsável pode ter filhos em múltiplas escolas
Comportamento Cross-Escola
| Cenário | Comportamento |
|---|---|
| Login pelo mural da Escola A | Retorna filhos de todas as escolas |
| Dashboard do filho na Escola B | Visível via mural da Escola A |
| Bloqueio pela Escola A | Impede login pelo mural da Escola A apenas |
| Update de perfil | Altera registro global (impacta todas as escolas) |
Fluxos Detalhados
Para fluxos completos (auto-cadastro, login, auto-vínculo, desvinculação, dashboard, gestão pela escola), ver docs/features/MURAL.md §8.
Lacunas LGPD
Auditoria técnica completa com 9 lacunas identificadas: docs/audits/AUDIT_RESPONSAVEL_LGPD_2026-04-13.md
5. Segurança
Rate Limiting — Estratégia NAT-Aware (Dual Check)
O mural opera em ambientes escolares com alta densidade de tráfego (NAT compartilhado). A estratégia utiliza dual check por IP + escola_id com janelas curtas:
SSOT: Constantes em supabase/functions/_shared/portal-security.ts
| Constante | Limite | Janela | Usado por |
|---|---|---|---|
RATE_LIMIT_LOOKUP_IP | 100/min | 1 min | lookup_escola |
RATE_LIMIT_LOOKUP_ESCOLA | 200/min | 1 min | lookup_escola |
RATE_LIMIT_SERIES_IP | 100/min | 1 min | get_series_escola |
RATE_LIMIT_LOGIN_A_ESCOLA | 200/min | 1 min | login_aluno_a |
RATE_LIMIT_LOGIN_A_IP | 400/5min | 5 min | login_aluno_a |
RATE_LIMIT_OTP_IP | 50/15min | 15 min | send_otp_* |
RATE_LIMIT_OTP_ESCOLA | 100/15min | 15 min | send_otp_* |
RATE_LIMIT_CADASTRO | 30/30min | 30 min | cadastro_* |
RATE_LIMIT_VINCULO | 30/60min | 60 min | auto_vincular_matricula |
RATE_LIMIT_UPDATE_PERFIL | 10/60min | 60 min | update_perfil_responsavel |
RATE_LIMIT_CHECK_SLUG | 60/min | 1 min | check_slug_availability |
BURST_LIMIT_IP | 150/5s | 5s | Todas (in-memory) |
Lockout Progressivo (SSOT: portal-security.ts)
Login (LOCKOUT_THRESHOLDS)
| Falhas Consecutivas | Lockout | Alerta |
|---|---|---|
| 3 | 2 minutos | — |
| 6 | 10 minutos | ntfy high (Tier 2) |
| 10 | 60 minutos | ntfy urgent (Tier 3 — possível brute force) |
Vínculo (LOCKOUT_THRESHOLDS_VINCULO)
| Falhas Consecutivas | Lockout |
|---|---|
| 5 | 10 minutos |
| 8 | 60 minutos |
| 12 | 120 minutos |
Alertas Push (ntfy.sh)
| Trigger | Prioridade | Descrição |
|---|---|---|
| Lockout Tier 2 (≥6 falhas) | high | Monitorar se escala para Tier 3 |
| Lockout Tier 3 (≥10 falhas) | urgent | Possível brute force — verificar logs_transacoes |
| Rate limit OTP escola (70%+) | high | Alerta de possível abuso de custo de mensagens |
Anti-Timing Attack
Todas as respostas de login do mural incluem delay aleatório de 500-1000ms para prevenir enumeração de usuários.
Validação de CPF
Todos os endpoints que recebem CPF validam dígitos verificadores via isValidCPF() antes de qualquer operação de banco.
Verificação de Assinatura
O mural valida a assinatura da escola via subscription-helper.ts antes de permitir login. Códigos de erro:
| Código | Significado | Mensagem ao Usuário |
|---|---|---|
ESCOLA_SUSPENSA | Escola suspensa pelo admin | "Portal temporariamente indisponível" |
ESCOLA_ENCERRADA | Escola encerrada | "Portal temporariamente indisponível" |
PORTAL_DISABLED | Mural desativado pela escola | "Portal não ativo" |
TRIAL_EXPIRADO | Trial expirou | "Portal temporariamente indisponível" |
SEM_ASSINATURA | Sem assinatura ativa | "Portal temporariamente indisponível" |
6. Feature Flags
O mural possui 3 feature flags independentes verificados via fetchPortalFeatureFlags() (helper _shared/portal-feature-check.ts):
| Flag | Chave | Default | Efeito |
|---|---|---|---|
| Portal inteiro | portal | true (fail-open) | Bloqueia TODAS as actions não-neutras |
| Acesso aluno | portal.acesso_aluno | true (fail-open) | Bloqueia login aluno |
| Acesso responsável | portal.acesso_responsavel | false (fail-close) | Bloqueia actions responsável |
7. Diferenças Visuais
| Aspecto | Aluno | Responsável |
|---|---|---|
| Abas | Notícias + Competidor | SOMENTE Competidor |
| Seletor de filho | ❌ | ✅ |
| Textos | "Você competiu..." | "Nível de participação:" |
7.1 Visibilidade de Notícias por Série
Notícias do mural (mural_publicacoes) aplicam regra de cascata por série, controlada pelos campos olimpiada_id e publico_series (TEXT[] de IDs de série, opcional).
| Cenário | publico_series | olimpiada_id | Quem vê |
|---|---|---|---|
| Genérica, sem olimpíada | NULL/vazio | NULL | Todos alunos da escola |
| Vinculada a olimpíada, sem público explícito | NULL/vazio | preenchido | Apenas alunos cuja serie_id ∈ olimpiada_series_participantes |
| Vinculada com público explícito | array | preenchido | Apenas alunos em publico_series (sobrepõe filtro de olimpíada) |
| Sem olimpíada, com público explícito | array | NULL | Apenas alunos em publico_series |
Implementação: filtro server-side em mural-escola/get_publicacoes_ativas (recebe serie_id opcional). Quando ausente, retorna o conjunto completo (uso pelo coordenador). Frontend passa aluno.serie_id via useMuralPublicacoes.
Cadeia de propagação do serie_id (auditada):
- Backend
portal-login-alunoeverificar_sessaoretornamserie_idno payload do aluno. MuralEscolaPagepersiste emalunoLogado.serie_ide o repassa explicitamente em<MuralAlunoDashboard aluno=>— tanto no fluxo direto do aluno quanto na visão do responsável.- Para o responsável,
portal-dashboard.handleVerificarSessaoincluiserie_idno map de cada aluno vinculado (viaturmas.serie_id). MuralAlunoDashboardProps.aluno.serie_idé tipado explicitamente (semas any) e consumido poruseMuralPublicacoes.- Defesa em profundidade: backend emite
console.warnse receber request semserie_idmas houver pubs com restrição (publico_seriesouolimpiada_id), sinalizando regressão de caller.
SSOT do schema: mural_publicacoes.publico_series TEXT[] NULL + índice GIN parcial WHERE publico_series IS NOT NULL.
8. Liberação de Resultados e Snapshots Materializados
Ver detalhes completos em docs/features/MURAL.md §11 e MURAL_LIBERACOES.md.
9. Referências
- SSOT consolidado:
docs/features/MURAL.md - Liberação de Resultados — Snapshots materializados, cache, flags
- Autenticação — JWT customizado e cookies
- Rate Limits — Detalhes do rate limiting NAT-aware e lockout progressivo
- Security Audit — Relatório completo de segurança
- Auditoria LGPD Responsável — Lacunas de privacidade identificadas
- Troubleshooting — Debug de problemas do mural