Skip to content

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 filhos

2. Métodos de Acesso

Por Série

A tabela escola_mural_config.metodo_acesso_por_serie define o método por série:

json
{
  "6_ef": "A",
  "7_ef": "A",
  "8_ef": "B",
  "9_ef": "B"
}
MétodoCamposPúblico-alvoJustificativa
AMatrícula + Data de NascimentoCrianças (6º-7º)Não possuem CPF/telefone
BCPF + OTP WhatsAppAdolescentes (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

AspectoSistema PrincipalMural Olímpico
SecretOLP_JWT_SECRETOLP_JWT_SECRET (unificado)
Cookieolp_autholp_mural
Expiração8h2h (PORTAL_JWT_EXPIRY_HOURS em portal-security.ts)
EscopoCompleto (CRUD)portal_readonly

Claims do Token do Mural

json
{
  "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

ConstanteLimiteJanelaUsado por
RATE_LIMIT_LOOKUP_IP100/min1 minlookup_escola
RATE_LIMIT_LOOKUP_ESCOLA200/min1 minlookup_escola
RATE_LIMIT_SERIES_IP100/min1 minget_series_escola
RATE_LIMIT_LOGIN_A_ESCOLA200/min1 minlogin_aluno_a
RATE_LIMIT_LOGIN_A_IP400/5min5 minlogin_aluno_a
RATE_LIMIT_OTP_IP50/15min15 minsend_otp_*
RATE_LIMIT_OTP_ESCOLA100/15min15 minsend_otp_*
RATE_LIMIT_CADASTRO30/30min30 mincadastro_*
RATE_LIMIT_VINCULO30/60min60 minauto_vincular_matricula
RATE_LIMIT_UPDATE_PERFIL10/60min60 minupdate_perfil_responsavel
RATE_LIMIT_CHECK_SLUG60/min1 mincheck_slug_availability
BURST_LIMIT_IP150/5s5sTodas (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 ConsecutivasLockoutAlerta
32 minutos
610 minutosntfy high (Tier 2)
1060 minutosntfy urgent (Tier 3 — possível brute force)

Vínculo (LOCKOUT_THRESHOLDS_VINCULO)

Falhas ConsecutivasLockout
510 minutos
860 minutos
12120 minutos

Alertas Push (ntfy.sh)

TriggerPrioridadeDescrição
Lockout Tier 2 (≥6 falhas)highMonitorar se escala para Tier 3
Lockout Tier 3 (≥10 falhas)urgentPossível brute force — verificar logs_transacoes
Rate limit OTP escola (70%+)highAlerta 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 max definido como data atual, impedindo datas futuras
  • Botão de envio: desabilitado até todos os campos serem preenchidos corretamente

Tabelas de Segurança

TabelaFunção
portal_otpsOTPs do mural (SHA-256, 5 min validade)
portal_login_tentativasRegistro 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ódigoSignificadoMensagem ao Usuário
ESCOLA_SUSPENSAEscola suspensa pelo admin"Portal temporariamente indisponível"
ESCOLA_ENCERRADAEscola encerrada"Portal temporariamente indisponível"
PORTAL_DISABLEDMural desativado pela escola"Portal não ativo"
TRIAL_EXPIRADOTrial expirou"Portal temporariamente indisponível"
SEM_ASSINATURASem 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):

FlagChaveDefaultEfeito
Portal inteiroportaltrue (fail-open)Bloqueia TODAS as actions não-neutras
Acesso alunoportal.acesso_alunotrue (fail-open)Bloqueia login_aluno_a, send_otp_aluno, verify_otp_aluno
Acesso responsávelportal.acesso_responsavelfalse (fail-close)Bloqueia todas as actions de responsável

Categorias de Actions

CategoriaActionsGate aplicado
Neutraslookup_escola, get_series_escola, check_slug_availability, verificar_sessao, get_aluno_dashboard, logout_portal, get_config, save_configNenhum
Alunologin_aluno_a, send_otp_aluno, verify_otp_alunoportal + portal.acesso_aluno
Responsávelsend_otp_responsavel, verify_otp_responsavel, verificar_cpf_responsavel, cadastro_enviar_otp_responsavel, cadastro_verificar_otp_responsavel, auto_vincular_matricula, auto_desvincular, update_perfil_responsavelportal + portal.acesso_responsavel

Princípio fail-close: Em caso de erro ao buscar flags, todas retornam false (bloqueia).


6. Diferenças Visuais

AspectoAlunoResponsável
AbasNotícias + CompetidorSOMENTE 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 via tipoVisualizador="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)

FlagEfeito no Mural
liberar_notasMaterializa pontuação, ranking por nível/série
liberar_resultadosMaterializa 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:

  1. Coordenador ativa liberar_notas ou liberar_resultados
  2. Backend salva em mural_liberacoes
  3. 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)
  4. 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)

ActionDescrição
lookup_escolaBusca escola por slug (valida status, assinatura, mural_ativo)
get_series_escolaBusca séries ativas da escola (pré-login, para seleção de série)
login_aluno_aLogin Método A — Matrícula + Data de Nascimento
send_otp_alunoEnvia OTP para aluno (Método B) via WhatsApp
verify_otp_alunoValida OTP de aluno (Método B) → JWT
send_otp_responsavelEnvia OTP para responsável via WhatsApp
verify_otp_responsavelValida OTP de responsável → JWT
verificar_cpf_responsavelVerifica se CPF já existe no cadastro de responsáveis
cadastro_enviar_otp_responsavelEnvia OTP para cadastro de novo responsável via WhatsApp
cadastro_verificar_otp_responsavelValida OTP e cadastra responsável → JWT
check_slug_availabilityVerifica disponibilidade de slug (coordenador)
ActionDescrição
verificar_sessaoVerifica validade do token do mural
get_aluno_dashboardDados do dashboard (aluno direto ou filho do responsável via aluno_id)
auto_vincular_matriculaVincula aluno ao responsável (matrícula + parentesco)
auto_desvincularDesvincula aluno do responsável
update_perfil_responsavelAtualiza perfil do responsável (telefone, nome)
logout_portalLogout + limpeza de cookies (olp_mural e olp_portal legado)
ActionDescrição
get_configBusca config do mural da escola (coordenador)
save_configSalva config do mural da escola (coordenador, role escola)

Hook Frontend: useMuralPortal

SSOT: src/hooks/useMuralPortal.ts (~750 linhas)

typescript
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

ArquivoFunção
src/pages/mural/MuralEscolaPage.tsxPágina principal (lookup + seleção aluno/responsável)
src/pages/mural/MuralLoginAluno.tsxFormulário de login do aluno (Método A ou B)
src/pages/mural/MuralLoginResponsavel.tsxFormulário de login do responsável
src/pages/mural/MuralCadastroResponsavel.tsxCadastro de novo responsável
src/pages/mural/MuralDashboardAluno.tsxDashboard pós-login do aluno
src/pages/mural/MuralDashboardResponsavel.tsxDashboard pós-login do responsável (seletor de filhos)

9. Referências