Skip to content

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 global

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.

RLS de escola_mural_config

PapelOperaçõesFiltro
escola (gestor)SELECT, INSERT, UPDATE, DELETEescola_id próprio
coordenador, diretorSELECT (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

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

Claims do Token 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 — o responsável é uma identidade global por CPF. O acesso aos dados dos filhos é resolvido via get_alunos_responsavel() (SECURITY DEFINER).


4. Responsável — Modelo de Identidade

Identidade Global por CPF

  • responsaveis.escola_id é sempre null (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árioComportamento
Login pelo mural da Escola ARetorna filhos de todas as escolas
Dashboard do filho na Escola BVisível via mural da Escola A
Bloqueio pela Escola AImpede login pelo mural da Escola A apenas
Update de perfilAltera 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

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)

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.

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"

6. 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
Acesso responsávelportal.acesso_responsavelfalse (fail-close)Bloqueia actions responsável

7. Diferenças Visuais

AspectoAlunoResponsável
AbasNotícias + CompetidorSOMENTE 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áriopublico_seriesolimpiada_idQuem vê
Genérica, sem olimpíadaNULL/vazioNULLTodos alunos da escola
Vinculada a olimpíada, sem público explícitoNULL/vaziopreenchidoApenas alunos cuja serie_id ∈ olimpiada_series_participantes
Vinculada com público explícitoarraypreenchidoApenas alunos em publico_series (sobrepõe filtro de olimpíada)
Sem olimpíada, com público explícitoarrayNULLApenas 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):

  1. Backend portal-login-aluno e verificar_sessao retornam serie_id no payload do aluno.
  2. MuralEscolaPage persiste em alunoLogado.serie_id e o repassa explicitamente em <MuralAlunoDashboard aluno=> — tanto no fluxo direto do aluno quanto na visão do responsável.
  3. Para o responsável, portal-dashboard.handleVerificarSessao inclui serie_id no map de cada aluno vinculado (via turmas.serie_id).
  4. MuralAlunoDashboardProps.aluno.serie_id é tipado explicitamente (sem as any) e consumido por useMuralPublicacoes.
  5. Defesa em profundidade: backend emite console.warn se receber request sem serie_id mas houver pubs com restrição (publico_series ou olimpiada_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