Skip to content

Auditoria Técnica: Responsável no OLP — Base LGPD

Data: 2026-04-13
Objetivo: Documentar modelo de dados, fluxos, permissões e lacunas do módulo Responsável para subsidiar redação de termos de consentimento e política de privacidade.
Método: Exploração exaustiva do codebase (Edge Functions, RLS, schema) — sem inferências.


1. Schema

1.1 Tabela responsaveis

ColunaTipoNullableDefaultObservações
iduuidNOT NULLgen_random_uuid()PK
escola_iduuidNULLFK→escolas ON DELETE CASCADE. Sempre null na prática — identidade global por CPF
nome_completotextNOT NULL
cpftextNOT NULLUNIQUE (responsaveis_cpf_key)
emailtextNULL
telefonetextNOT NULL
telefone_secundariotextNULL
ativobooleanNOT NULLtrueSoft-disable (não soft-delete)
criado_emtimestamptzNOT NULLnow()
atualizado_emtimestamptzNOT NULLnow()

RLS: Habilitado.

Constraints notáveis:

  • CPF é único globalmente — não pode existir dois registros com o mesmo CPF.
  • escola_id existe na coluna mas é sempre inserido como null (ver portal-cadastro.ts:144).

Tabelas que referenciam responsaveis.id:

  • aluno_responsaveis.responsavel_id (FK, ON DELETE CASCADE)
  • inscricoes_olimpiada.autorizado_por (FK — campo de autorização de inscrição pelo responsável)

1.2 Tabela aluno_responsaveis

ColunaTipoNullableDefaultObservações
aluno_iduuidNOT NULLFK→alunos ON DELETE CASCADE. PK composta
responsavel_iduuidNOT NULLFK→responsaveis ON DELETE CASCADE. PK composta
parentescotextNOT NULL'responsavel'Valores aceitos no portal: pai, mae, responsavel
principalbooleanNOT NULLfalse
criado_emtimestamptzNOT NULLnow()
bloqueado_escolabooleanNOT NULLfalseBloqueio institucional por escola
bloqueado_emtimestamptzNULLTimestamp do bloqueio
bloqueado_poruuidNULLUUID do usuário que bloqueou

RLS: Habilitado.

1.3 Campos de Consentimento

NÃO EXISTEM campos de consentimento, aceite de termos, timestamp de aceite LGPD ou qualquer mecanismo de coleta de consentimento em nenhuma tabela do sistema. Isso inclui:

  • responsaveis
  • aluno_responsaveis
  • alunos
  • portal_otps
  • portal_login_tentativas

2. Fluxos

2.1 Auto-cadastro do Responsável

Arquivos: supabase/functions/_shared/portal-cadastro.ts

Etapa 1: handleCadastroEnviarOtpResponsavel (linhas 21-87)

  1. Valida CPF (isValidCPF) e telefone (11 dígitos)
  2. Rate limit: checkRateLimit(ip, "cadastro_responsavel", RATE_LIMIT_CADASTRO)
  3. Verifica duplicata global: supabaseSystem.from("responsaveis").select("id").eq("cpf", cpf) — se existe, retorna 409
  4. Gera OTP 6 dígitos, hash SHA-256, salva em portal_otps com tipo_entidade: "cadastro_responsavel"
  5. Envia OTP via WhatsApp (Wasender)
  6. Registra métrica e tentativa

Etapa 2: handleCadastroVerificarOtpResponsavel (linhas 89-163)

  1. Verifica lockout progressivo
  2. Busca OTP não usado e não expirado em portal_otps
  3. Comparação timing-safe (crypto.subtle.timingSafeEqual)
  4. Re-verifica duplicata de CPF (double-check)
  5. Cria registro: supabaseSystem.from("responsaveis").insert({ nome_completo, cpf, telefone, escola_id: null, ativo: true })
  6. Emite JWT: gerarPortalJWT({ id, tipo: "responsavel", nome, escolaId: null })
  7. Set cookie: olp_mural (HttpOnly, Secure, SameSite=None, Max-Age conforme PORTAL_JWT_EXPIRY_HOURS)

⚠️ Nenhum consentimento é coletado. Nenhum aceite de termos, nenhuma flag LGPD, nenhum timestamp de aceite.

⚠️ Nenhum vínculo aluno_responsaveis é criado no cadastro. O responsável é criado "solto" — vínculos são feitos separadamente via auto-vínculo.

2.2 Login do Responsável

Arquivo: supabase/functions/_shared/portal-login-responsavel.ts

handleSendOtpResponsavel (linhas 19-139)

  1. Valida CPF
  2. Rate limit duplo: por escola (RATE_LIMIT_OTP_ESCOLA) e por IP (RATE_LIMIT_OTP_IP)
  3. Lockout progressivo por identificador
  4. Busca responsável: supabaseSystem.from("responsaveis").select(...).eq("cpf", cpf).eq("ativo", true).single()
    • CPF deve existir e estar ativo
  5. Verificação de bloqueio por escola (linhas 80-93):
    supabaseSystem.from("aluno_responsaveis")
      .select("aluno_id, bloqueado_escola, alunos!inner(escola_id)")
      .eq("responsavel_id", responsavel.id)
      .eq("alunos.escola_id", params.escolaId)
    • Se totalVinculosEscola > 0 E vinculosNaoBloqueados.length === 0bloqueia login (HTTP 403)
    • Se totalVinculosEscola === 0 (sem vínculos naquela escola) → login é permitido (não há bloqueio)
  6. Gera e envia OTP via WhatsApp

Cenário importante: Se o responsável acessa o mural da Escola A mas tem vínculos apenas com alunos da Escola B, o login é permitido — a verificação de bloqueio só se aplica quando há vínculos naquela escola específica.

handleVerifyOtpResponsavel (linhas 141-233)

  1. Lockout e validação
  2. Verifica OTP (timing-safe)
  3. Retorna TODOS os alunos vinculados (todas as escolas):
    supabaseSystem.from("aluno_responsaveis")
      .select("alunos (id, nome_completo, matricula, escola_id, escolas!inner(...), turmas!inner(...))")
      .eq("responsavel_id", responsavel.id)
    — Sem filtro por escola_id ou bloqueado_escola
  4. JWT emitido com escolaId: null (responsável é global)
  5. Cookie olp_mural definido

2.3 Auto-vínculo (Matrícula)

Arquivo: supabase/functions/_shared/portal-cadastro.ts, função handleAutoVincularMatricula (linhas 165-242)

  1. Rate limit: RATE_LIMIT_VINCULO por IP
  2. Lockout progressivo: LOCKOUT_THRESHOLDS_VINCULO
  3. Valida parentesco: deve ser pai, mae ou responsavel
  4. Exige data_nascimento — obrigatório para o match
  5. Busca aluno:
    supabaseSystem.from("alunos")
      .select("id, nome_completo, matricula, escola_id, ativo, escolas!inner(...), turmas(...)")
      .eq("matricula", matricula).eq("data_nascimento", data_nascimento).eq("ativo", true)
    Não filtra por escola_id → aluno pode ser de qualquer escola do OLP
  6. Verifica escola ativa: escola.status !== "ativa" → bloqueia
  7. Verifica vínculo duplicado
  8. Cria vínculo: supabaseSystem.from("aluno_responsaveis").insert({ aluno_id, responsavel_id, parentesco, principal: false })

⚠️ Nenhuma aprovação da escola. O vínculo é criado imediatamente, sem confirmação, notificação ou aprovação institucional.

⚠️ Sem limite de vínculos. Não há verificação do número de vínculos que o responsável já possui.

2.4 Auto-desvinculação

Arquivo: supabase/functions/_shared/portal-cadastro.ts, função handleAutoDesvincular (linhas 244-281)

  1. Rate limit: 5 desvinculações por hora ({ max: 5, windowMinutes: 60 }) por responsavel_id
  2. Verifica existência do vínculo
  3. Deleta vínculo: supabaseSystem.from("aluno_responsaveis").delete().eq("aluno_id", aluno_id).eq("responsavel_id", payload.sub)
  4. Validação SEC-6: .select("aluno_id").maybeSingle() para confirmar deleção
  5. Log de auditoria em logs_transacoes

Quem pode desvincular:

  • O próprio responsável (via portal, esta função)
  • A escola (via gestao-responsaveis, ação desvincular_aluno)

2.5 Dashboard do Responsável

Arquivo: supabase/functions/_shared/portal-dashboard.ts, função handleGetAlunoDashboard (linhas 115-229)

  1. Extrai e verifica token do portal
  2. SEC-7: Para responsáveis, verifica vínculo explícito via supabaseSystem:
    supabaseSystem.from("aluno_responsaveis")
      .select("aluno_id").eq("aluno_id", aluno_id).eq("responsavel_id", payload.sub)
    Não verifica bloqueado_escola — responsável bloqueado que ainda tem vínculo pode ver dados
  3. Consulta alunos via supabasePortal (com RLS)
  4. Consulta inscricoes_olimpiada via supabasePortal (com RLS)
  5. Consulta mural_dados_publicados via supabasePortal (com RLS)

Dados retornados:

  • olimpiadas_inscritas: id, status, ano_edicao, nivel, sigla, nome, area
  • resultados: por inscrição → por fase → pontuação, classificação, premiação, observações

⚠️ Sem filtro por escola_id no dashboard. Os dados vêm de todas as olimpíadas do aluno, independente de qual escola o responsável está acessando.

2.6 Verificação de CPF (Público)

Arquivo: supabase/functions/_shared/portal-config.ts, função handleVerificarCpfResponsavel (linhas 106-128)

  1. Valida formato CPF
  2. Rate limit: RATE_LIMIT_CADASTRO
  3. Consulta: supabaseSystem.from("responsaveis").select("id").eq("cpf", cpf).maybeSingle()
  4. Retorna: { success: true, existe: true/false }

⚠️ Permite enumeração de CPFs — qualquer pessoa pode verificar se um CPF está cadastrado. Mitigado por rate limit, mas sem CAPTCHA.

2.7 Atualização de Perfil (Portal)

Arquivo: supabase/functions/_shared/portal-config.ts, função handleUpdatePerfilResponsavel (linhas 130-206)

  1. Verifica token do portal (tipo responsavel)
  2. Rate limit: RATE_LIMIT_UPDATE_PERFIL
  3. Campos editáveis: nome_completo, email, telefone, telefone_secundario
  4. Atualiza registro GLOBAL: supabaseSystem.from("responsaveis").update(dados).eq("id", payload.sub)
  5. Log com diff (antes/depois) em logs_transacoes

⚠️ Update global cross-escola: Se o responsável altera seu nome ou telefone, a alteração afeta o registro único global — impactando a visão de TODAS as escolas que compartilham esse responsável.

2.8 Gestão pela Escola (Sistema Interno)

Arquivo: supabase/functions/gestao-responsaveis/index.ts

Actions disponíveis (papel escola obrigatório):

ActionOperaçãoDescrição
listSELECTLista responsáveis com vínculos, filtros e paginação
getSELECTBusca responsável por ID com vínculos
createINSERTCria responsável (com escola_id ou null)
updateUPDATEAtualiza dados do responsável (registro global)
bloquearUPDATEDefine bloqueado_escola=true em aluno_responsaveis
desbloquearUPDATEDefine bloqueado_escola=false em aluno_responsaveis
vincular_alunoINSERTCria vínculo aluno_responsaveis
desvincular_alunoDELETERemove vínculo aluno_responsaveis
listar_alunos_responsavelSELECTLista alunos vinculados a um responsável

⚠️ Update pela escola também é global: Quando a escola edita nome/telefone de um responsável, modifica o mesmo registro global — impactando outras escolas.


3. Matriz de Permissões (RLS)

3.1 Tabela responsaveis

PolicyOperaçãoPapel/EscopoFiltro
portal_responsavel_own_dataSELECTPortal responsávelportal_type='responsavel' AND id=sub
responsaveis_escola_selectSELECTescola, diretor, pedagogicoportal_type IS NULL AND principal_role IN (...) AND responsavel_tem_aluno_na_escola(id)
responsaveis_escola_insertINSERTescolaprincipal_role='escola'
responsaveis_escola_updateUPDATEescolaprincipal_role='escola' AND responsavel_tem_aluno_na_escola(id)
responsaveis_escola_deleteDELETEescolaprincipal_role='escola' AND responsavel_tem_aluno_na_escola(id)

Observações:

  • Administrador NÃO tem acesso via RLS — sem policy de SELECT para admin
  • Coordenador NÃO tem acesso direto — apenas escola, diretor, pedagogico
  • Portal do aluno NÃO tem acesso à tabela responsaveis

3.2 Tabela aluno_responsaveis

PolicyOperaçãoPapel/EscopoFiltro
aluno_resp_escola_selectSELECTescola, diretor, pedagogicoportal_type IS NULL AND aluno_pertence_escola(aluno_id)
aluno_resp_escola_insertINSERTescolaaluno.escola_id = jwt.escola_id AND principal_role='escola'
aluno_resp_escola_update_bloqueioUPDATEescolaportal_type IS NULL AND aluno_pertence_escola(aluno_id) AND principal_role='escola'
aluno_resp_escola_deleteDELETEescolaportal_type IS NULL AND aluno_pertence_escola(aluno_id) AND principal_role='escola'
portal_responsavel_vinculosSELECTPortal responsávelportal_type='responsavel' AND responsavel_id=sub
portal_responsavel_auto_vincularINSERTPortal responsávelportal_type='responsavel' AND responsavel_id=sub
portal_responsavel_auto_desvincularDELETEPortal responsávelportal_type='responsavel' AND responsavel_id=sub

3.3 Tabelas Consultadas pelo Dashboard do Responsável

TabelaPolicyFiltro
alunosportal_responsavel_filhos_alunosportal_type='responsavel' AND id IN (get_alunos_responsavel())
inscricoes_olimpiadaportal_responsavel_inscricoesportal_type='responsavel' AND aluno_id IN (get_alunos_responsavel())
mural_dados_publicadosportal_dados_pub_responsavelportal_type='responsavel' AND aluno_id IN (get_alunos_responsavel())

3.4 Funções SECURITY DEFINER

get_alunos_responsavel()

sql
CREATE OR REPLACE FUNCTION public.get_alunos_responsavel()
 RETURNS SETOF uuid
 LANGUAGE sql
 STABLE SECURITY DEFINER
 SET search_path TO 'public'
AS $function$
  SELECT aluno_id FROM aluno_responsaveis
  WHERE responsavel_id::text = auth.jwt()->>'sub';
$function$

Observação: Retorna TODOS os aluno_id vinculados ao responsável, sem filtrar por escola_id, bloqueado_escola ou status da escola.

responsavel_vinculado_aluno(p_aluno_id uuid)

sql
CREATE OR REPLACE FUNCTION public.responsavel_vinculado_aluno(p_aluno_id uuid)
 RETURNS boolean
 LANGUAGE sql
 STABLE SECURITY DEFINER
 SET search_path TO 'public'
AS $function$
  SELECT EXISTS (
    SELECT 1 FROM aluno_responsaveis
    WHERE aluno_id = p_aluno_id
      AND responsavel_id::text = auth.jwt()->>'sub'
  );
$function$

Observação: Não verifica bloqueado_escola.

responsavel_tem_aluno_na_escola(p_responsavel_id uuid)

sql
CREATE OR REPLACE FUNCTION public.responsavel_tem_aluno_na_escola(p_responsavel_id uuid)
 RETURNS boolean
 LANGUAGE sql
 STABLE SECURITY DEFINER
 SET search_path TO 'public'
AS $function$
  SELECT 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::text = auth.jwt()->>'escola_id'
  );
$function$

Observação: Verifica vínculo ativo na escola do JWT — usada para scoping de visibilidade institucional. Não verifica bloqueado_escola.

aluno_pertence_escola(p_aluno_id uuid)

sql
CREATE OR REPLACE FUNCTION public.aluno_pertence_escola(p_aluno_id uuid)
 RETURNS boolean
 LANGUAGE sql
 STABLE SECURITY DEFINER
 SET search_path TO 'public'
AS $function$
  SELECT EXISTS (
    SELECT 1 FROM alunos
    WHERE id = p_aluno_id
      AND escola_id::text = auth.jwt()->>'escola_id'
  );
$function$

4. Vínculos: Responsável ↔ Aluno ↔ Escola

4.1 Modelo de Relacionamento

responsaveis (global, CPF único)

    │ N:N via aluno_responsaveis
    │   ├── parentesco (pai/mae/responsavel)
    │   ├── principal (boolean)
    │   └── bloqueado_escola (boolean, por escola)


alunos (scoped a escola_id)

    └── escola_id → escolas

4.2 O que cada conexão habilita

ConexãoHabilitaRestringe
Responsável existe em responsaveisLogin no portal (qualquer escola), auto-vínculo
Vínculo em aluno_responsaveisVer dashboard do aluno, ver inscrições, ver resultados publicados
bloqueado_escola=trueImpede login no portal daquela escola específica (se todos os vínculos daquela escola estão bloqueados)
Escola A bloqueiaNão impede acesso ao dashboard do filho via portal de Escola B (se há vínculo com Escola B)
ativo=false em responsaveisImpede login em qualquer portal (verificação eq("ativo", true))

4.3 Isolamento Cross-Escola

  • A escola só vê responsáveis vinculados a seus próprios alunos (via responsavel_tem_aluno_na_escola e aluno_pertence_escola).
  • O responsável vê dados de TODOS os filhos (via get_alunos_responsavel() sem filtro de escola).
  • O bloqueio é por vínculo-escola, não global. Bloquear na Escola A não afeta acesso via Escola B.

5. Lacunas Identificadas

5.1 Ausência Total de Consentimento LGPD

Cenário: O responsável se cadastra e fornece CPF, nome, telefone. Não há tela de aceite de termos, checkbox de consentimento, ou qualquer registro temporal de aceite.

Evidência: portal-cadastro.ts:142-145 — INSERT direto sem campos de consentimento. Nenhum campo aceite_termos, consentimento_em, ou versao_termos existe em responsaveis ou qualquer outra tabela.

Implicação LGPD: Art. 7º e 8º da LGPD exigem base legal para tratamento. Sem registro de consentimento, não há comprovação de base legal para o tratamento dos dados pessoais.

5.2 Update Global Cross-Escola

Cenário: O responsável João tem filhos na Escola A e Escola B. A Escola A edita o telefone de João via gestão interna. A Escola B passa a ver o telefone alterado.

Evidência: portal-config.ts:186 e gestao-responsaveis/index.ts — UPDATE em responsaveis filtrado por id, sem scoping por escola.

Implicação: Escola A modifica dados visíveis para Escola B sem conhecimento ou consentimento do responsável ou da Escola B.

5.3 Auto-vínculo Sem Aprovação Institucional

Cenário: Qualquer pessoa com CPF cadastrado pode vincular-se a qualquer aluno de qualquer escola ativa do OLP, desde que conheça a matrícula e data de nascimento.

Evidência: portal-cadastro.ts:197-200 — query em alunos sem filtro de escola_id, apenas matricula + data_nascimento + ativo.

Implicação: Risco de vínculo indevido. Matrícula e data de nascimento não são informações secretas — podem ser conhecidas por terceiros.

5.4 Dashboard Cross-Escola Sem Filtro

Cenário: O responsável é bloqueado pela Escola A (bloqueado_escola=true). Ele acessa o portal da Escola B (onde não tem vínculos) e consegue ver o dashboard dos filhos da Escola A.

Evidência:

  • get_alunos_responsavel() retorna todos os aluno_id sem filtrar bloqueado_escola
  • portal-dashboard.ts:142-153 verifica vínculo mas não verifica bloqueado_escola
  • handleVerifyOtpResponsavel (login) retorna todos os alunos sem filtrar bloqueio

Implicação: O bloqueio institucional é parcialmente efetivo — impede o login na escola que bloqueou, mas não impede o acesso aos dados se o responsável tem qualquer outro ponto de entrada.

5.5 Bloqueio Parcial — Escopo Limitado

Cenário: bloqueado_escola em aluno_responsaveis impede login no portal da escola que bloqueou. Porém:

  • Se o responsável tem vínculos com alunos de múltiplas escolas, pode logar pelo portal de outra escola
  • O vínculo aluno_responsaveis persiste após bloqueio — apenas o flag muda
  • get_alunos_responsavel() ignora o flag bloqueado_escola

Evidência: portal-login-responsavel.ts:80-93 — verificação somente para a escola do portal acessado.

5.6 Ausência de Exclusão/Portabilidade de Dados

Cenário: O responsável quer exercer direito de exclusão (Art. 18, VI LGPD) ou portabilidade (Art. 18, V).

Evidência: Não existe nenhuma:

  • Função de exclusão de responsável
  • Fluxo de anonimização
  • Endpoint de exportação de dados
  • Tela ou interface de solicitação de exclusão
  • O registro persiste em responsaveis mesmo que todos os vínculos sejam removidos

Implicação: Sem mecanismo técnico para atender solicitações de direitos do titular.

5.7 Sem Limite de Vínculos

Cenário: Um responsável pode vincular-se a um número ilimitado de alunos.

Evidência: portal-cadastro.ts:219-224 — verifica apenas duplicata do vínculo específico, sem contar total de vínculos.

Implicação: Risco de abuso — um agente malicioso com CPF cadastrado poderia tentar vincular-se a muitos alunos (mitigado por rate limit e exigência de matrícula+data_nascimento, mas sem hard limit).

5.8 Responsáveis Apenas Via Auto-Cadastro ou Cadastro Manual

Cenário: Não existe importação em massa de responsáveis.

Evidência: As únicas rotas de criação são:

  1. Auto-cadastro via portal (portal-cadastro.ts:handleCadastroVerificarOtpResponsavel)
  2. Cadastro manual pela escola (gestao-responsaveis/index.ts, action create)

Implicação: Não é um risco de segurança, mas limita a operacionalidade para escolas com muitos responsáveis.

5.9 Enumeração de CPF via Endpoint Público

Cenário: Qualquer pessoa pode verificar se um CPF está cadastrado no sistema.

Evidência: portal-config.ts:106-128 — endpoint verificar_cpf_responsavel retorna { existe: true/false } para qualquer CPF válido. Protegido por RATE_LIMIT_CADASTRO mas sem CAPTCHA.

Implicação: Permite a um atacante determinar se uma pessoa específica tem cadastro no OLP, o que constitui vazamento de informação pessoal (dado relacional).


6. Ciclo de Vida dos Dados

6.1 Caminhos de Criação

CaminhoArquivoQuem executa
Auto-cadastroportal-cadastro.ts:handleCadastroVerificarOtpResponsavelPróprio responsável
Cadastro manualgestao-responsaveis/index.ts, action createPapel escola

6.2 Caminhos de Atualização

CaminhoArquivoQuem executaCampos
Portal self-serviceportal-config.ts:handleUpdatePerfilResponsavelPróprio responsávelnome, email, telefone, tel_secundario
Gestão internagestao-responsaveis/index.ts, action updatePapel escolanome, email, telefone, tel_secundario, cpf, ativo

6.3 Exclusão

  • Não implementada. Não existe função de exclusão de responsável (hard ou soft delete).
  • A policy responsaveis_escola_delete existe no RLS, permitindo DELETE pelo papel escola quando responsavel_tem_aluno_na_escola(id), mas não há interface ou edge function que execute essa operação.
  • Se todos os vínculos aluno_responsaveis forem removidos, o registro em responsaveis persiste indefinidamente.
  • Não há fluxo de anonimização, exportação ou portabilidade.

6.4 Dados em logs_transacoes

Todas as operações de CUD (Create, Update, Delete) sobre responsáveis e vínculos são registradas em logs_transacoes com:

  • acao: portal.auto_cadastro_responsavel, portal.auto_vincular_matricula, portal.auto_desvincular, portal.update_perfil_responsavel, portal.login_responsavel
  • detalhes: JSON com diff de alterações, IDs, CPF parcial, IP
  • Retenção: sem política de retenção definida (dados persistem indefinidamente)

7. Resumo para Redação de Termos

Dados Coletados

DadoObrigatórioFonteFinalidade
CPFSimAuto-cadastroIdentificação única, login
Nome completoSimAuto-cadastroIdentificação
TelefoneSimAuto-cadastroEnvio de OTP via WhatsApp
EmailNãoAuto-cadastro ou ediçãoContato (não usado para autenticação)
Telefone secundárioNãoEdiçãoContato alternativo
IPAutomáticoTodas as requisiçõesRate limit, segurança, log de auditoria
User-AgentAutomáticoLogin/cadastroLog de auditoria

Bases Legais a Considerar

  1. Consentimento (Art. 7º, I) — atualmente não coletado
  2. Legítimo interesse (Art. 7º, IX) — possível para segurança (logs, rate limit)
  3. Proteção da vida (Art. 7º, VII) — possível para dados de contato de emergência

Compartilhamento de Dados

DadoCompartilhado comVia
Nome, telefone, emailEscola(s) do(s) filho(s)Visibilidade via RLS para papéis escola, diretor, pedagogico
CPFNão compartilhadoApenas na tabela responsaveis — não retornado para a escola via RLS (exceto UPDATE/GET pelo papel escola)

8. Implementações Pendentes — Aguardando Aprovação Jurídica

8.1 Correção de Enumeração de CPF no Cadastro (§5.9)

Status: ⏳ Aguardando aprovação — pendente reunião e alinhamento jurídico
Data do levantamento: 2026-04-15
Severidade: Média (mitigada por rate limit, sem CAPTCHA)

Problema: O endpoint verificar_cpf_responsavel e a rejeição 409 em cadastro_enviar_otp_responsavel permitem que terceiros descubram se um CPF está cadastrado na plataforma — vazamento de dado relacional (Art. 6º, VII LGPD — segurança).

Solução planejada:

  1. handleCadastroEnviarOtpResponsavel: quando CPF já existe, enviar OTP para o telefone do registro existente (ignorando o telefone do formulário) e retornar resposta genérica idêntica ao fluxo de cadastro novo (mesmo shape, mesmo status 200, telefone_mascarado do telefone real).

  2. handleCadastroVerificarOtpResponsavel: quando CPF já existia, emitir JWT de login usando o registro existente em vez de rejeitar com 409. Buscar alunos vinculados para retornar no response (paridade com login normal).

  3. handleVerificarCpfResponsavel: alterar para sempre retornar { success: true, existe: false } independente do resultado real. Manter rate limit como defesa em profundidade.

  4. Frontend (MuralLoginResponsavel.tsx): remover a chamada verificarCpfResponsavel do fluxo de cadastro e a exibição da mensagem "CPF já cadastrado".

Arquivos impactados:

  • supabase/functions/_shared/portal-cadastro.ts (linhas 46-48 e 138-141)
  • supabase/functions/_shared/portal-config.ts (linhas 106-128)
  • src/pages/mural/MuralLoginResponsavel.tsx (linhas 232-245)

Logs de auditoria: diferenciar internamente cadastro_novo de login_via_cadastro em registrarLog() para rastreabilidade sem expor ao frontend.

Princípio: O usuário chega autenticado sem saber se "já tinha cadastro" — resposta idêntica nos dois cenários.


Documento gerado por exploração exaustiva do codebase em 2026-04-13. Atualizado em 2026-04-15 com implementações pendentes. Todas as referências apontam para arquivos e funções exatas no repositório.