Skip to content

Mural Olímpico — Aluno e Responsável

Última atualização: 2026-04-13
Consolidado de: docs/mural/MURAL_OVERVIEW.md + docs/mural/MURAL_LIBERACOES.md


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. O coordenador controla a publicação de resultados via flags de liberação, que geram snapshots materializados para performance e controle de acesso granular.

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
│   ├── Seleção de série (dropdown agrupado: Médio → Fund. Finais → Fund. Iniciais, ordem decrescente)
│   ├── Método A: Matrícula + Data de Nascimento (input texto com máscara DD/MM/AAAA, inputMode numeric)
│   └── Método B: CPF + OTP WhatsApp
└── Fluxo Responsável
    ├── Login: CPF + OTP WhatsApp → Seletor de filhos
    └── Cadastro: CPF + telefone + nome → OTP WhatsApp → registro global

2. Arquitetura

text
Frontend                            Backend                              Banco
────────                            ───────                              ─────
MuralEscolaPage ─┐                  portal-escola                        escola_mural_config
MuralLoginAluno ─┤                  ├── lookup_escola                    portal_otps
MuralLoginResp. ─┤── useMuralPortal ├── login_aluno_a                    portal_login_tentativas
MuralCadastro   ─┤                  ├── send/verify_otp_*                alunos, responsaveis
MuralDashAluno ──┤                  ├── cadastro_*_otp_responsavel       aluno_responsaveis
MuralDashResp. ──┘                  ├── get_aluno_dashboard              mural_liberacoes
                                    ├── auto_vincular/desvincular        mural_dados_publicados
                                    ├── update_perfil_responsavel        resultados_aluno (read)
                                    └── get/save_config

Coordenador (publicação):           mural-escola
MuralLiberacoes ── useMuralEscola ─── toggle_liberacao ───────────────► mural_dados_publicados
                                                                        (snapshot materializado)

Gestão pela escola:                 gestao-responsaveis
GestaoResponsaveis ───────────────── list/create/update/block ────────► responsaveis
                                     vincular/desvincular              aluno_responsaveis

3. Schema (Tabelas)

TabelaPropósitoRLS
escola_mural_configConfig do mural (slug, métodos de acesso por série, ativo/inativo)coordenador (SELECT, UPDATE)
portal_otpsOTPs do mural (SHA-256, 5 min validade)service_role only
portal_login_tentativasRegistro de tentativas para rate limiting e lockoutservice_role only
mural_liberacoesFlags de publicação por fase/nível (liberar_notas, liberar_resultados)coordenador (SELECT, INSERT, UPDATE)
mural_dados_publicadosSnapshots materializados para o muralservice_role (write), portal_readonly (read)
mural_publicacoesNotícias do mural. Inclui publico_series TEXT[] NULL (filtro opcional por série; ver §7.1 do MURAL_OVERVIEW) e olimpiada_id (filtro implícito por séries participantes da olimpíada)coordenador (CRUD na própria escola), portal_readonly (SELECT por escola_id, filtrado por série server-side)
responsaveisIdentidade global do responsável (CPF único)escola (SELECT, INSERT, UPDATE); portal (SELECT, UPDATE próprio)
aluno_responsaveisVínculos aluno↔responsável com bloqueio institucionalescola (SELECT, INSERT, UPDATE, DELETE); portal (SELECT, INSERT, DELETE próprio)

responsaveis — Schema Completo

ColunaTipoConstraintsObservação
iduuidPK, gen_random_uuid
escola_iduuidFK→escolas, nullable, ON DELETE CASCADESempre null — identidade global por CPF
nome_completotextNOT NULLEditável pelo responsável e pela escola (global)
cpftextNOT NULL, UNIQUE (responsaveis_cpf_key)Identificador único global
emailtextnullable
telefonetextNOT NULLUsado para OTP WhatsApp
telefone_secundariotextnullable
ativobooleanNOT NULL, default trueSoft-disable (não é soft-delete)
criado_emtimestamptzdefault now()
atualizado_emtimestamptzdefault now()

Modelo de identidade: O responsável é uma entidade global identificada por CPF. O campo escola_id existe por legado mas é sempre null. O vínculo com escolas é derivado exclusivamente via aluno_responsaveis → alunos.escola_id.

aluno_responsaveis — Schema Completo

ColunaTipoConstraintsObservação
aluno_iduuidPK (composta), FK→alunos ON DELETE CASCADE
responsavel_iduuidPK (composta), FK→responsaveis ON DELETE CASCADE
parentescotextNOT NULL, default 'responsavel'Valores: pai, mae, responsavel
principalbooleandefault falseFlag informativo, não afeta permissões
criado_emtimestamptzdefault now()
bloqueado_escolabooleandefault falseBloqueio institucional (impede login no portal da escola)
bloqueado_emtimestamptznullableTimestamp do bloqueio
bloqueado_poruuidnullableID do usuário que bloqueou

Sem campo de consentimento: Não existe campo de aceite de termos, consentimento LGPD ou timestamp de aceite em nenhuma das tabelas. Ver Auditoria LGPD.


4. Métodos de Acesso

Por Série (escola_mural_config.metodo_acesso_por_serie)

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 CPF + OTP WhatsApp.


5. Autenticação

AspectoSistema PrincipalMural — AlunoMural — Responsável
SecretOLP_JWT_SECRETOLP_JWT_SECRETOLP_JWT_SECRET
Cookieolp_autholp_muralolp_mural
Expiração8h2h2h
EscopoCompleto (CRUD)portal_readonlyportal_readonly
escola_id no JWTUUID da escolaUUID da escolanull (identidade global)

Claims do JWT do Responsável

json
{
  "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 é intencional. O responsável é global — não pertence a nenhuma escola específica. O acesso aos dados dos filhos é resolvido via get_alunos_responsavel() (SECURITY DEFINER).


6. Segurança

Rate Limiting — NAT-Aware (Dual Check)

SSOT: Constantes em supabase/functions/_shared/portal-security.ts

Limites Gerais

ConstanteLimiteJanela
RATE_LIMIT_LOOKUP_IP100/min1 min
RATE_LIMIT_LOGIN_A_IP400/5min5 min
RATE_LIMIT_OTP_IP50/15min15 min
BURST_LIMIT_IP150/5s5s (in-memory)

Limites Específicos do Responsável

ConstanteLimiteJanelaUsado por
RATE_LIMIT_CADASTRO30/30min30 mincadastro_enviar_otp_responsavel
RATE_LIMIT_VINCULO30/60min60 minauto_vincular_matricula
RATE_LIMIT_UPDATE_PERFIL10/60min60 minupdate_perfil_responsavel

Lockout Progressivo

Login (LOCKOUT_THRESHOLDS)

FalhasLockoutAlerta
32 min
610 minntfy high
1060 minntfy urgent (possível brute force)

Vínculo (LOCKOUT_THRESHOLDS_VINCULO)

FalhasLockout
510 min
860 min
12120 min

Outras Proteções

  • Anti-timing attack: delay aleatório 500-1000ms em todas as respostas de login
  • Validação de CPF: isValidCPF() antes de qualquer operação de banco
  • Verificação de assinatura: subscription-helper.ts antes de permitir login
  • Parentescos válidos: PARENTESCOS_VALIDOS_PORTAL = ['pai', 'mae', 'responsavel']

7. Edge Functions

portal-escola

Actions Públicas (sem auth)

ActionDescrição
lookup_escolaBusca escola por slug (valida status, assinatura, mural_ativo)
get_series_escolaSéries ativas da escola (pré-login)
login_aluno_aLogin Método A — Matrícula + DN
send_otp_alunoEnvia OTP (Método B) via WhatsApp
verify_otp_alunoValida OTP (Método B) → JWT
send_otp_responsavelEnvia OTP para responsável
verify_otp_responsavelValida OTP responsável → JWT
verificar_cpf_responsavelVerifica se CPF já existe
cadastro_enviar_otp_responsavelOTP para cadastro novo
cadastro_verificar_otp_responsavelValida OTP + cadastra → JWT
check_slug_availabilityVerifica disponibilidade de slug

Actions Autenticadas (Portal — olp_mural)

ActionDescrição
verificar_sessaoVerifica validade do token
get_aluno_dashboardDados do dashboard
auto_vincular_matriculaVincula aluno ao responsável
auto_desvincularDesvincula aluno
update_perfil_responsavelAtualiza perfil do responsável
logout_portalLogout + limpeza de cookies

Actions Autenticadas (Sistema — olp_auth)

ActionDescrição
get_configConfig do mural da escola (coordenador)
save_configSalva config do mural (coordenador)

mural-escola

ActionTipoDescrição
toggle_liberacaoWritePublica/despublica resultados → gera snapshots

gestao-responsaveis

ActionTipoPapelDescrição
listReadescolaLista responsáveis vinculados a alunos da escola
getReadescolaDetalhe de um responsável
createWriteescolaCadastra responsável manualmente
updateWriteescolaAtualiza dados do responsável (global)
bloquearWriteescolaSeta bloqueado_escola=true em aluno_responsaveis
desbloquearWriteescolaSeta bloqueado_escola=false em aluno_responsaveis
vincularWriteescolaVincula responsável a aluno da escola
desvincularWriteescolaRemove vínculo responsável↔aluno

8. Responsável — Modelo de Identidade e Fluxos

Identidade Global

O responsável é uma entidade global identificada pelo CPF. Não pertence a nenhuma escola específica:

  • responsaveis.escola_id é sempre null (campo legado)
  • O vínculo com escolas é derivado via aluno_responsaveis → alunos.escola_id
  • Um mesmo responsável pode ter filhos em múltiplas escolas
  • O JWT do responsável tem escolaId: null

Fluxo de Auto-Cadastro

Handlers: portal-cadastro.tshandleCadastroEnviarOtpResponsavel, handleCadastroVerificarOtpResponsavel

CPF + telefone + nome_completo
  → Valida CPF (isValidCPF)
  → Verifica duplicata global (responsaveis.cpf)
  → Se CPF já existe → erro "CPF já cadastrado"
  → Envia OTP via WhatsApp
  → Verifica OTP
  → INSERT em responsaveis (escola_id: null, ativo: true)
  → Emite JWT com escolaId: null
  → NENHUM vínculo aluno_responsaveis criado

Sem consentimento: Não há coleta de aceite de termos, consentimento LGPD ou qualquer flag de aceite neste fluxo.

Fluxo de Login

Handlers: portal-login-responsavel.tshandleSendOtpResponsavel, handleVerifyOtpResponsavel

CPF
  → CPF deve existir em responsaveis + ativo=true
  → Verifica vínculos na escola do mural:
      aluno_responsaveis JOIN alunos WHERE alunos.escola_id = escola_do_slug
  → Se TODOS os vínculos naquela escola têm bloqueado_escola=true → BLOQUEADO
  → Se nenhum vínculo com a escola → PERMITIDO (login sem restrição)
  → Envia OTP via WhatsApp
  → Verifica OTP
  → JWT com escolaId: null
  → Retorna lista de TODOS os alunos vinculados (TODAS as escolas)

Cross-escola: O login pelo mural da Escola A retorna filhos da Escola A e da Escola B. O JWT não restringe por escola.

Auto-Vínculo

Handler: portal-cadastro.tshandleAutoVincularMatricula

matrícula + data_nascimento + parentesco
  → Busca aluno por matrícula + data_nascimento (qualquer escola ativa)
  → Verifica escola do aluno está ativa
  → Verifica vínculo não existe
  → Valida parentesco ∈ PARENTESCOS_VALIDOS_PORTAL
  → INSERT em aluno_responsaveis (principal: false, bloqueado_escola: false)
  → Rate limit: RATE_LIMIT_VINCULO (30/60min)
  → Lockout: LOCKOUT_THRESHOLDS_VINCULO

Sem aprovação institucional: O vínculo é criado sem aprovação da escola. A escola pode bloquear depois via gestao-responsaveis.bloquear.

Auto-Desvinculação

Handler: portal-cadastro.tshandleAutoDesvincular

aluno_id (do token do responsável)
  → Verifica vínculo existe
  → DELETE de aluno_responsaveis
  → Rate limit: 5 operações/hora

Dashboard do Responsável

Handler: portal-dashboard.tshandleGetAlunoDashboard

aluno_id (parâmetro) + responsavel_id (do JWT)
  → Verifica vínculo explícito: aluno_responsaveis WHERE aluno_id AND responsavel_id
  → NÃO verifica bloqueado_escola
  → Busca dados do aluno (alunos)
  → Busca inscrições (inscricoes_olimpiada)
  → Busca snapshots (mural_dados_publicados)
  → Retorna dados de TODAS as escolas/olimpíadas do aluno

Sem filtro por escola: O dashboard retorna dados de todas as escolas do aluno, independente de qual mural o responsável acessou.

Atualização de Perfil

Handler: portal-escola/index.tshandleUpdatePerfilResponsavel

nome_completo, email, telefone, telefone_secundario (opcionais)
  → UPDATE em responsaveis WHERE id = sub do JWT
  → Afeta o registro GLOBAL — impacta todas as escolas vinculadas
  → Rate limit: RATE_LIMIT_UPDATE_PERFIL (10/60min)

Impacto cross-escola: Se a Escola A e a Escola B compartilham o mesmo responsável, um update de nome pelo portal da Escola A altera o nome visto pela Escola B.

Gestão pela Escola

Edge Function: gestao-responsaveis/index.ts — papel escola obrigatório

  • CRUD: list, get, create, update — scoped a alunos da escola via aluno_responsaveis → alunos.escola_id
  • Bloqueio: seta bloqueado_escola=true no vínculo específico (não global)
  • Vínculo/Desvínculo: escola pode vincular/desvincular responsáveis de seus alunos
  • Update: altera registro GLOBAL em responsaveis (mesmo impacto cross-escola)

9. RLS e Funções SECURITY DEFINER

Funções SECURITY DEFINER

get_alunos_responsavel()

sql
-- Retorna todos os aluno_ids vinculados ao responsável autenticado
-- NÃO filtra por escola_id, NÃO filtra por bloqueado_escola
SELECT aluno_id FROM aluno_responsaveis
WHERE responsavel_id = (auth.jwt()->>'sub')::uuid

Implicação: Responsável bloqueado na Escola A ainda vê dados do aluno via portal da Escola B (se tiver vínculo não-bloqueado lá).

responsavel_vinculado_aluno(p_aluno_id uuid)

sql
-- Verifica se o responsável autenticado tem vínculo com o aluno
EXISTS (
  SELECT 1 FROM aluno_responsaveis
  WHERE aluno_id = p_aluno_id
    AND responsavel_id = (auth.jwt()->>'sub')::uuid
)

responsavel_tem_aluno_na_escola(p_responsavel_id uuid)

sql
-- Verifica se o responsável tem pelo menos um aluno na escola do JWT
EXISTS (
  SELECT 1 FROM aluno_responsaveis ar
  JOIN alunos a ON a.id = ar.aluno_id
  WHERE ar.responsavel_id = p_responsavel_id
    AND a.escola_id = (auth.jwt()->>'escola_id')::uuid
)

Visibilidade por Papel

Papelresponsaveis SELECTaluno_responsaveis SELECTFiltro
escolaresponsavel_tem_aluno_na_escola() — apenas responsáveis com filhos na escola
coordenadoridem escola
diretoridem escola
administradorSem acesso (por design — proteção de privacidade)
portal (responsável)✅ próprio✅ próprios vínculossub do JWT = responsavel_id

10. Comportamento Cross-Escola

O modelo de identidade global cria comportamentos cross-escola que devem ser considerados:

CenárioComportamentoReferência
Login pelo mural da Escola ARetorna filhos de TODAS as escolashandleVerifyOtpResponsavel
Dashboard do filho na Escola BDados visíveis via mural da Escola AhandleGetAlunoDashboard
Bloqueio pela Escola AImpede login pelo mural da Escola A; NÃO impede acesso via Escola BhandleSendOtpResponsavel
Update de perfil pelo portalAltera registro GLOBALhandleUpdatePerfilResponsavel
Update pela Escola AAltera registro GLOBAL (impacta Escola B)gestao-responsaveis/update
Desvinculação pelo responsávelRemove vínculo; se último vínculo, registro persiste em responsaveishandleAutoDesvincular

11. Liberação de Resultados e Snapshots

Flags de Liberação (mural_liberacoes)

FlagEfeitoExclusividade
liberar_notasMaterializa pontuação, rankingMutuamente exclusivo com liberar_resultados
liberar_resultadosMaterializa situação, premiação, rankingMutuamente exclusivo com liberar_notas

Snapshots Materializados (mural_dados_publicados)

O mural NÃO lê diretamente resultados_aluno. Ao publicar, o backend gera snapshots via computeAndUpsertSnapshot() (helper _shared/snapshot-publicados.ts):

  1. Busca inscrições em chunks de 80 IDs (limite PostgREST)
  2. Resolve resultados da fase correspondente
  3. Calcula empates por nível e por série
  4. Resolve pontuação máxima via fallback
  5. Upsert em mural_dados_publicados (chunks de 100)

Recomputação Automática

Snapshots são recomputados em: inserção/importação de resultados, deleção, atualização de prêmios/nota de corte, botão "Republicar snapshot" (🔄).

Limpeza

Quando ambos liberar_notas=false e liberar_resultados=false, snapshots da fase/nível são deletados.


12. Hook Frontend: useMuralPortal

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

Exports principais: loginAlunoMetodoA, solicitarOtpAluno, verificarOtpAluno, solicitarOtpResponsavel, verificarOtpResponsavel, cadastroEnviarOtp, cadastroVerificarOtp, verificarSessao, logoutPortal, autoVincularMatricula, autoDesvincular, updatePerfilResponsavel.


13. Componentes

text
src/pages/mural/
├── MuralEscolaPage.tsx              Lookup + seleção aluno/responsável
├── MuralLoginAluno.tsx              Login aluno (Método A ou B)
├── MuralLoginResponsavel.tsx        Login responsável
├── MuralCadastroResponsavel.tsx     Cadastro novo responsável
├── MuralDashboardAluno.tsx          Dashboard pós-login aluno
└── MuralDashboardResponsavel.tsx    Dashboard responsável (seletor de filhos)

src/components/mural-olimpico/
├── mural-liberacoes.tsx             Cards de liberação (coordenador)
└── fase-content.tsx                 Conteúdo por fase (aluno/responsável)

Diferenças Visuais

AspectoAlunoResponsável
AbasNotícias + CompetidorSOMENTE Competidor
Seletor de filho

14. Cache e Invalidação (Coordenador)

KeyConteúdo
['mural-liberacoes']Lista de flags de liberação
['mural-liberacao-stats', 'batch', ...]Stats por fase/nível
['gestao-resultados', ...]Resultados do coordenador

Toda operação que altera resultados ou liberações DEVE invalidar stats + liberacoes com refetchType: 'all'.


15. Feature Flags

FlagChaveDefaultEfeito
Portal inteiroportaltrue (fail-open)Bloqueia actions não-neutras
Acesso alunoportal.acesso_alunotrue (fail-open)Bloqueia login aluno
Acesso responsávelportal.acesso_responsavelfalse (fail-close)Bloqueia actions responsável

16. Referências

  • docs/security/AUTHENTICATION.md — JWT customizado e cookies
  • docs/security/RATE_LIMITS.md — Rate limiting e lockout
  • docs/security/SECURITY_AUDIT_2026-02-28.md — Relatório de segurança
  • docs/operations/TROUBLESHOOTING.md — Debug de problemas do mural
  • docs/audits/AUDIT_RESPONSAVEL_LGPD_2026-04-13.mdAuditoria LGPD do módulo responsável (lacunas de consentimento, cross-escola, portabilidade)

17. Padrões Aplicados

PadrãoReferência
Autenticação JWT separadadocs/security/AUTHENTICATION.md
Rate Limiting NAT-Awaredocs/security/RATE_LIMITS.md
Snapshots Materializados_shared/snapshot-publicados.ts
Anti-Timing Attackdocs/security/RATE_LIMITS.md
Identidade Global Responsável→ §8 deste documento
Bloqueio Institucional→ §8 + §10 deste documento