Mural Olímpico — Aluno e Responsável
Última atualização: 2026-04-06
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
└── CPF + OTP WhatsApp → Seletor de filhos2. 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.
3. Autenticação do Mural
JWT Separado
| Aspecto | Sistema Principal | Mural Olímpico |
|---|---|---|
| Secret | OLP_JWT_SECRET | OLP_JWT_SECRET (unificado) |
| Cookie | olp_auth | olp_mural |
| Expiração | 8h | 2h (PORTAL_JWT_EXPIRY_HOURS em portal-security.ts) |
| Escopo | Completo (CRUD) | portal_readonly |
Claims do Token do Mural
{
"sub": "uuid-aluno-ou-responsavel",
"tipo": "aluno | responsavel",
"nome_completo": "Nome",
"escola_id": "uuid",
"serie_id": "uuid | null",
"turma_id": "uuid | null",
"matricula": "123456",
"escopo": "portal_readonly"
}4. 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) |
Justificativa: Em escolas, milhares de alunos podem compartilhar o mesmo IP público. A defesa principal contra brute force é transferida do IP para o identificador (Matrícula/CPF) via lockout progressivo.
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.
Validação Frontend
- Data de nascimento: campo
maxdefinido como data atual, impedindo datas futuras - Botão de envio: desabilitado até todos os campos serem preenchidos corretamente
Tabelas de Segurança
| Tabela | Função |
|---|---|
portal_otps | OTPs do mural (SHA-256, 5 min validade) |
portal_login_tentativas | Registro de tentativas para rate limiting e lockout |
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" |
Nota: Mensagens ao aluno/responsável são genéricas de propósito — não revelam detalhes financeiros.
5. 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_a, send_otp_aluno, verify_otp_aluno |
| Acesso responsável | portal.acesso_responsavel | false (fail-close) | Bloqueia todas as actions de responsável |
Categorias de Actions
| Categoria | Actions | Gate aplicado |
|---|---|---|
| Neutras | lookup_escola, get_series_escola, check_slug_availability, verificar_sessao, get_aluno_dashboard, logout_portal, get_config, save_config | Nenhum |
| Aluno | login_aluno_a, send_otp_aluno, verify_otp_aluno | portal + portal.acesso_aluno |
| Responsável | send_otp_responsavel, verify_otp_responsavel, verificar_cpf_responsavel, cadastro_enviar_otp_responsavel, cadastro_verificar_otp_responsavel, auto_vincular_matricula, auto_desvincular, update_perfil_responsavel | portal + portal.acesso_responsavel |
Princípio fail-close: Em caso de erro ao buscar flags, todas retornam
false(bloqueia).
6. Diferenças Visuais
| Aspecto | Aluno | Responsável |
|---|---|---|
| Abas | Notícias + Competidor | SOMENTE Competidor |
| Seletor de filho | ❌ | ✅ |
| Textos | "Você competiu..." | "Nível de participação:" |
Nota: A aba "Notícias" é oculta para responsáveis por design (ver memory
guardian-mural-parity). O responsável consome o mesmo snapshot materializado do aluno viatipoVisualizador="responsavel".
7. Liberação de Resultados e Snapshots Materializados
Controlada pela tabela mural_liberacoes, por escola + olimpíada + fase + nível.
Flags de Liberação (exclusivas)
| Flag | Efeito no Mural |
|---|---|
liberar_notas | Materializa pontuação, ranking por nível/série |
liberar_resultados | Materializa situação, premiação, ranking |
Exclusividade: Apenas um dos dois flags pode estar ativo por vez. O backend enforça automaticamente, priorizando o toggle que mudou.
Snapshots Materializados (mural_dados_publicados)
O mural NÃO lê diretamente resultados_aluno. Ao publicar, o backend gera snapshots materializados na tabela mural_dados_publicados via computeAndUpsertSnapshot() (helper _shared/snapshot-publicados.ts).
Fluxo:
- Coordenador ativa
liberar_notasouliberar_resultados - Backend salva em
mural_liberacoes - Backend chama
computeAndUpsertSnapshot()que:- Busca inscrições da escola (com paginação para >1000 registros)
- Busca resultados por fase (em chunks de 80 IDs para evitar estouro de URL PostgREST)
- Computa rankings, empates, posição por série
- Upserta em
mural_dados_publicados(chunks de 100)
- Mural do aluno lê apenas
mural_dados_publicados
Recomputação automática: O helper recomputeSnapshotsIfPublished() é chamado após importações e edições de resultados em gestao-resultados, mantendo os dados do mural sincronizados.
Fallback sem fase_id: Se a liberação não tem fase_id explícito (olimpíada de fase única), o backend itera todas as fases da olimpíada automaticamente.
Erro de snapshot: Se a materialização falhar, o backend retorna snapshot_error: true para que o frontend exiba aviso ao coordenador.
Documentação detalhada: MURAL_LIBERACOES.md
8. Código Principal
Edge Function: portal-escola
SSOT: supabase/functions/portal-escola/index.ts
Actions Públicas (sem auth)
| Action | Descrição |
|---|---|
lookup_escola | Busca escola por slug (valida status, assinatura, mural_ativo) |
get_series_escola | Busca séries ativas da escola (pré-login, para seleção de série) |
login_aluno_a | Login Método A — Matrícula + Data de Nascimento |
send_otp_aluno | Envia OTP para aluno (Método B) via WhatsApp |
verify_otp_aluno | Valida OTP de aluno (Método B) → JWT |
send_otp_responsavel | Envia OTP para responsável via WhatsApp |
verify_otp_responsavel | Valida OTP de responsável → JWT |
verificar_cpf_responsavel | Verifica se CPF já existe no cadastro de responsáveis |
cadastro_enviar_otp_responsavel | Envia OTP para cadastro de novo responsável via WhatsApp |
cadastro_verificar_otp_responsavel | Valida OTP e cadastra responsável → JWT |
check_slug_availability | Verifica disponibilidade de slug (coordenador) |
Actions Autenticadas (Portal — cookie olp_mural)
| Action | Descrição |
|---|---|
verificar_sessao | Verifica validade do token do mural |
get_aluno_dashboard | Dados do dashboard (aluno direto ou filho do responsável via aluno_id) |
auto_vincular_matricula | Vincula aluno ao responsável (matrícula + parentesco) |
auto_desvincular | Desvincula aluno do responsável |
update_perfil_responsavel | Atualiza perfil do responsável (telefone, nome) |
logout_portal | Logout + limpeza de cookies (olp_mural e olp_portal legado) |
Actions Autenticadas (Sistema — cookie olp_auth)
| Action | Descrição |
|---|---|
get_config | Busca config do mural da escola (coordenador) |
save_config | Salva config do mural da escola (coordenador, role escola) |
Hook Frontend: useMuralPortal
SSOT: src/hooks/useMuralPortal.ts (~750 linhas)
import { useMuralPortal } from '@/hooks/useMuralPortal';
const {
// === Autenticação ===
loginAlunoMetodoA, // Método A: matrícula + DN
solicitarOtpAluno, // Método B: enviar OTP via WhatsApp
verificarOtpAluno, // Método B: verificar OTP
solicitarOtpResponsavel, // Responsável: enviar OTP via WhatsApp
verificarOtpResponsavel, // Responsável: verificar OTP
cadastroEnviarOtp, // Cadastro: enviar OTP via WhatsApp
cadastroVerificarOtp, // Cadastro: verificar OTP + cadastrar
verificarCpfResponsavel, // Verificar se CPF já existe
verificarSessao, // Verifica validade do token
logoutPortal, // Logout + limpeza de cookies
// === Vínculos ===
autoVincularMatricula, // Vincular aluno ao responsável
autoDesvincular, // Desvincular aluno do responsável
// === Perfil ===
updatePerfilResponsavel, // Atualizar perfil do responsável
// === Dados ===
escolaData, // Dados da escola (pós-lookup)
seriesEscola, // Séries ativas da escola
metodosAcesso, // Métodos de acesso por série
alunoLogado, // Dados do aluno logado
responsavelLogado, // Dados do responsável logado
dashboardData, // Dados do dashboard logado
// === Estado ===
loading, // Loading geral
error, // Último erro
} = useMuralPortal();Páginas Frontend
| Arquivo | Função |
|---|---|
src/pages/mural/MuralEscolaPage.tsx | Página principal (lookup + seleção aluno/responsável) |
src/pages/mural/MuralLoginAluno.tsx | Formulário de login do aluno (Método A ou B) |
src/pages/mural/MuralLoginResponsavel.tsx | Formulário de login do responsável |
src/pages/mural/MuralCadastroResponsavel.tsx | Cadastro de novo responsável |
src/pages/mural/MuralDashboardAluno.tsx | Dashboard pós-login do aluno |
src/pages/mural/MuralDashboardResponsavel.tsx | Dashboard pós-login do responsável (seletor de filhos) |
9. Referências
- 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
- Troubleshooting — Debug de problemas do mural