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
| Coluna | Tipo | Nullable | Default | Observações |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
escola_id | uuid | NULL | — | FK→escolas ON DELETE CASCADE. Sempre null na prática — identidade global por CPF |
nome_completo | text | NOT NULL | — | |
cpf | text | NOT NULL | — | UNIQUE (responsaveis_cpf_key) |
email | text | NULL | — | |
telefone | text | NOT NULL | — | |
telefone_secundario | text | NULL | — | |
ativo | boolean | NOT NULL | true | Soft-disable (não soft-delete) |
criado_em | timestamptz | NOT NULL | now() | |
atualizado_em | timestamptz | NOT NULL | now() |
RLS: Habilitado.
Constraints notáveis:
- CPF é único globalmente — não pode existir dois registros com o mesmo CPF.
escola_idexiste na coluna mas é sempre inserido comonull(verportal-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
| Coluna | Tipo | Nullable | Default | Observações |
|---|---|---|---|---|
aluno_id | uuid | NOT NULL | — | FK→alunos ON DELETE CASCADE. PK composta |
responsavel_id | uuid | NOT NULL | — | FK→responsaveis ON DELETE CASCADE. PK composta |
parentesco | text | NOT NULL | 'responsavel' | Valores aceitos no portal: pai, mae, responsavel |
principal | boolean | NOT NULL | false | |
criado_em | timestamptz | NOT NULL | now() | |
bloqueado_escola | boolean | NOT NULL | false | Bloqueio institucional por escola |
bloqueado_em | timestamptz | NULL | — | Timestamp do bloqueio |
bloqueado_por | uuid | NULL | — | UUID 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:
responsaveisaluno_responsaveisalunosportal_otpsportal_login_tentativas
2. Fluxos
2.1 Auto-cadastro do Responsável
Arquivos: supabase/functions/_shared/portal-cadastro.ts
Etapa 1: handleCadastroEnviarOtpResponsavel (linhas 21-87)
- Valida CPF (
isValidCPF) e telefone (11 dígitos) - Rate limit:
checkRateLimit(ip, "cadastro_responsavel", RATE_LIMIT_CADASTRO) - Verifica duplicata global:
supabaseSystem.from("responsaveis").select("id").eq("cpf", cpf)— se existe, retorna 409 - Gera OTP 6 dígitos, hash SHA-256, salva em
portal_otpscomtipo_entidade: "cadastro_responsavel" - Envia OTP via WhatsApp (Wasender)
- Registra métrica e tentativa
Etapa 2: handleCadastroVerificarOtpResponsavel (linhas 89-163)
- Verifica lockout progressivo
- Busca OTP não usado e não expirado em
portal_otps - Comparação timing-safe (
crypto.subtle.timingSafeEqual) - Re-verifica duplicata de CPF (double-check)
- Cria registro:
supabaseSystem.from("responsaveis").insert({ nome_completo, cpf, telefone, escola_id: null, ativo: true }) - Emite JWT:
gerarPortalJWT({ id, tipo: "responsavel", nome, escolaId: null }) - Set cookie:
olp_mural(HttpOnly, Secure, SameSite=None, Max-Age conformePORTAL_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)
- Valida CPF
- Rate limit duplo: por escola (
RATE_LIMIT_OTP_ESCOLA) e por IP (RATE_LIMIT_OTP_IP) - Lockout progressivo por identificador
- Busca responsável:
supabaseSystem.from("responsaveis").select(...).eq("cpf", cpf).eq("ativo", true).single()- CPF deve existir e estar ativo
- 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 > 0EvinculosNaoBloqueados.length === 0→ bloqueia login (HTTP 403) - Se
totalVinculosEscola === 0(sem vínculos naquela escola) → login é permitido (não há bloqueio)
- Se
- 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)
- Lockout e validação
- Verifica OTP (timing-safe)
- Retorna TODOS os alunos vinculados (todas as escolas):— Sem filtro por
supabaseSystem.from("aluno_responsaveis") .select("alunos (id, nome_completo, matricula, escola_id, escolas!inner(...), turmas!inner(...))") .eq("responsavel_id", responsavel.id)escola_idoubloqueado_escola - JWT emitido com
escolaId: null(responsável é global) - Cookie
olp_muraldefinido
2.3 Auto-vínculo (Matrícula)
Arquivo: supabase/functions/_shared/portal-cadastro.ts, função handleAutoVincularMatricula (linhas 165-242)
- Rate limit:
RATE_LIMIT_VINCULOpor IP - Lockout progressivo:
LOCKOUT_THRESHOLDS_VINCULO - Valida parentesco: deve ser
pai,maeouresponsavel - Exige
data_nascimento— obrigatório para o match - Busca aluno:— Não filtra por escola_id → aluno pode ser de qualquer escola do OLP
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) - Verifica escola ativa:
escola.status !== "ativa"→ bloqueia - Verifica vínculo duplicado
- 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)
- Rate limit: 5 desvinculações por hora (
{ max: 5, windowMinutes: 60 }) porresponsavel_id - Verifica existência do vínculo
- Deleta vínculo:
supabaseSystem.from("aluno_responsaveis").delete().eq("aluno_id", aluno_id).eq("responsavel_id", payload.sub) - Validação SEC-6:
.select("aluno_id").maybeSingle()para confirmar deleção - 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çãodesvincular_aluno)
2.5 Dashboard do Responsável
Arquivo: supabase/functions/_shared/portal-dashboard.ts, função handleGetAlunoDashboard (linhas 115-229)
- Extrai e verifica token do portal
- SEC-7: Para responsáveis, verifica vínculo explícito via
supabaseSystem:— Não verificasupabaseSystem.from("aluno_responsaveis") .select("aluno_id").eq("aluno_id", aluno_id).eq("responsavel_id", payload.sub)bloqueado_escola— responsável bloqueado que ainda tem vínculo pode ver dados - Consulta
alunosviasupabasePortal(com RLS) - Consulta
inscricoes_olimpiadaviasupabasePortal(com RLS) - Consulta
mural_dados_publicadosviasupabasePortal(com RLS)
Dados retornados:
olimpiadas_inscritas: id, status, ano_edicao, nivel, sigla, nome, arearesultados: 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)
- Valida formato CPF
- Rate limit:
RATE_LIMIT_CADASTRO - Consulta:
supabaseSystem.from("responsaveis").select("id").eq("cpf", cpf).maybeSingle() - 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)
- Verifica token do portal (tipo
responsavel) - Rate limit:
RATE_LIMIT_UPDATE_PERFIL - Campos editáveis:
nome_completo,email,telefone,telefone_secundario - Atualiza registro GLOBAL:
supabaseSystem.from("responsaveis").update(dados).eq("id", payload.sub) - 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):
| Action | Operação | Descrição |
|---|---|---|
list | SELECT | Lista responsáveis com vínculos, filtros e paginação |
get | SELECT | Busca responsável por ID com vínculos |
create | INSERT | Cria responsável (com escola_id ou null) |
update | UPDATE | Atualiza dados do responsável (registro global) |
bloquear | UPDATE | Define bloqueado_escola=true em aluno_responsaveis |
desbloquear | UPDATE | Define bloqueado_escola=false em aluno_responsaveis |
vincular_aluno | INSERT | Cria vínculo aluno_responsaveis |
desvincular_aluno | DELETE | Remove vínculo aluno_responsaveis |
listar_alunos_responsavel | SELECT | Lista 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
| Policy | Operação | Papel/Escopo | Filtro |
|---|---|---|---|
portal_responsavel_own_data | SELECT | Portal responsável | portal_type='responsavel' AND id=sub |
responsaveis_escola_select | SELECT | escola, diretor, pedagogico | portal_type IS NULL AND principal_role IN (...) AND responsavel_tem_aluno_na_escola(id) |
responsaveis_escola_insert | INSERT | escola | principal_role='escola' |
responsaveis_escola_update | UPDATE | escola | principal_role='escola' AND responsavel_tem_aluno_na_escola(id) |
responsaveis_escola_delete | DELETE | escola | principal_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
| Policy | Operação | Papel/Escopo | Filtro |
|---|---|---|---|
aluno_resp_escola_select | SELECT | escola, diretor, pedagogico | portal_type IS NULL AND aluno_pertence_escola(aluno_id) |
aluno_resp_escola_insert | INSERT | escola | aluno.escola_id = jwt.escola_id AND principal_role='escola' |
aluno_resp_escola_update_bloqueio | UPDATE | escola | portal_type IS NULL AND aluno_pertence_escola(aluno_id) AND principal_role='escola' |
aluno_resp_escola_delete | DELETE | escola | portal_type IS NULL AND aluno_pertence_escola(aluno_id) AND principal_role='escola' |
portal_responsavel_vinculos | SELECT | Portal responsável | portal_type='responsavel' AND responsavel_id=sub |
portal_responsavel_auto_vincular | INSERT | Portal responsável | portal_type='responsavel' AND responsavel_id=sub |
portal_responsavel_auto_desvincular | DELETE | Portal responsável | portal_type='responsavel' AND responsavel_id=sub |
3.3 Tabelas Consultadas pelo Dashboard do Responsável
| Tabela | Policy | Filtro |
|---|---|---|
alunos | portal_responsavel_filhos_alunos | portal_type='responsavel' AND id IN (get_alunos_responsavel()) |
inscricoes_olimpiada | portal_responsavel_inscricoes | portal_type='responsavel' AND aluno_id IN (get_alunos_responsavel()) |
mural_dados_publicados | portal_dados_pub_responsavel | portal_type='responsavel' AND aluno_id IN (get_alunos_responsavel()) |
3.4 Funções SECURITY DEFINER
get_alunos_responsavel()
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)
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)
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)
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 → escolas4.2 O que cada conexão habilita
| Conexão | Habilita | Restringe |
|---|---|---|
Responsável existe em responsaveis | Login no portal (qualquer escola), auto-vínculo | — |
Vínculo em aluno_responsaveis | Ver dashboard do aluno, ver inscrições, ver resultados publicados | — |
bloqueado_escola=true | — | Impede login no portal daquela escola específica (se todos os vínculos daquela escola estão bloqueados) |
| Escola A bloqueia | — | Não impede acesso ao dashboard do filho via portal de Escola B (se há vínculo com Escola B) |
ativo=false em responsaveis | — | Impede 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_escolaealuno_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 osaluno_idsem filtrarbloqueado_escolaportal-dashboard.ts:142-153verifica vínculo mas não verificabloqueado_escolahandleVerifyOtpResponsavel(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_responsaveispersiste após bloqueio — apenas o flag muda get_alunos_responsavel()ignora o flagbloqueado_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
responsaveismesmo 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:
- Auto-cadastro via portal (
portal-cadastro.ts:handleCadastroVerificarOtpResponsavel) - Cadastro manual pela escola (
gestao-responsaveis/index.ts, actioncreate)
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
| Caminho | Arquivo | Quem executa |
|---|---|---|
| Auto-cadastro | portal-cadastro.ts:handleCadastroVerificarOtpResponsavel | Próprio responsável |
| Cadastro manual | gestao-responsaveis/index.ts, action create | Papel escola |
6.2 Caminhos de Atualização
| Caminho | Arquivo | Quem executa | Campos |
|---|---|---|---|
| Portal self-service | portal-config.ts:handleUpdatePerfilResponsavel | Próprio responsável | nome, email, telefone, tel_secundario |
| Gestão interna | gestao-responsaveis/index.ts, action update | Papel escola | nome, 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_deleteexiste no RLS, permitindo DELETE pelo papelescolaquandoresponsavel_tem_aluno_na_escola(id), mas não há interface ou edge function que execute essa operação. - Se todos os vínculos
aluno_responsaveisforem removidos, o registro emresponsaveispersiste 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_responsaveldetalhes: 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
| Dado | Obrigatório | Fonte | Finalidade |
|---|---|---|---|
| CPF | Sim | Auto-cadastro | Identificação única, login |
| Nome completo | Sim | Auto-cadastro | Identificação |
| Telefone | Sim | Auto-cadastro | Envio de OTP via WhatsApp |
| Não | Auto-cadastro ou edição | Contato (não usado para autenticação) | |
| Telefone secundário | Não | Edição | Contato alternativo |
| IP | Automático | Todas as requisições | Rate limit, segurança, log de auditoria |
| User-Agent | Automático | Login/cadastro | Log de auditoria |
Bases Legais a Considerar
- Consentimento (Art. 7º, I) — atualmente não coletado
- Legítimo interesse (Art. 7º, IX) — possível para segurança (logs, rate limit)
- Proteção da vida (Art. 7º, VII) — possível para dados de contato de emergência
Compartilhamento de Dados
| Dado | Compartilhado com | Via |
|---|---|---|
| Nome, telefone, email | Escola(s) do(s) filho(s) | Visibilidade via RLS para papéis escola, diretor, pedagogico |
| CPF | Não compartilhado | Apenas 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:
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_mascaradodo telefone real).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).handleVerificarCpfResponsavel: alterar para sempre retornar{ success: true, existe: false }independente do resultado real. Manter rate limit como defesa em profundidade.Frontend (
MuralLoginResponsavel.tsx): remover a chamadaverificarCpfResponsaveldo 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.