# Esqueci minha senha (login.html) — OTP no WhatsApp

Criado em 2026-10-17. Vale para os **dois** logins: `site_3_paginas_gerais/login.html` (site.semecol.com.br) e `site_4_chamado/login.html` (chamado.semecol.com.br). Os dois sites falam com o **mesmo** Supabase, por isso o SQL e a policy são um só e ficam aqui; o `login-redefinir-senha.js` é uma cópia **idêntica** nas duas raízes.

## Como a pessoa usa

1. No login, abaixo da senha: **Esqueci minha senha**.
2. **Usuário** (só `nome.sobrenome`; completa `@edu.colider.mt.gov.br`) → **Enviar código**. O Supabase manda um código de 6 dígitos para o **WhatsApp** cadastrado (o mesmo caminho do app antigo: `recover` → hook de e-mail do GoTrue → n8n).
3. **Código** de 6 dígitos (aceita colar o código inteiro e o preenchimento automático do celular; confere sozinho ao completar). Reenvio depois de 60 s; 5 códigos errados = espera de 1 min.
4. Código certo → o Supabase devolve a sessão e a tela abre direto em **Nova senha**: mostra a **foto e o nome do cadastro** (`usuarios_cadastrados.user_foto`) com **Trocar foto** (escolher imagem, arrastar, zoom, girar), medidor de complexidade, 5 critérios, confirmação e a opção (marcada) **Desconectar de todas as outras sessões ativas**.
5. **Confirmar e redefinir senha** → a sessão da recuperação é encerrada e volta ao login com o aviso verde e o usuário já preenchido; a pessoa entra com a senha nova (o login de sempre confere a permissão do módulo). **Voltar para o Login** em qualquer etapa; depois do código, voltar encerra a sessão aberta.

Também: tema claro/escuro e cores da paleta do login; o nome do sistema («Sistema Integrado de Gestão Eletrônica para Educação Unificada - SIGEEDU») continua no rodapé do cartão e **aparece em todas as etapas**.

## Arquivos

| Arquivo | O que é | Publicar? |
|---|---|---|
| `site_3_paginas_gerais/login-redefinir-senha.js` e `site_4_chamado/login-redefinir-senha.js` | toda a lógica (cópias idênticas; mexeu em um, copie para o outro) | **sim**, 1º |
| `site_3_paginas_gerais/login.html` e `site_4_chamado/login.html` | markup e CSS novos (só acréscimos); ajudantes `lerImagensLogin`/`lerPermissoes` no lugar das 2 leituras diretas de tabela (com reserva se o SQL 002 não existir); 1 linha no trecho «sessão existente» | **sim**, depois do `.js` |
| `login-redefinir-senha/sql/001_login_redefinir_senha.sql` | funções e log da redefinição de senha (rodar no banco **uma vez**; vale para os dois sites) | não (é do banco) |
| `login-redefinir-senha/sql/002_login_leituras_por_funcao.sql` | funções que substituem as 2 leituras diretas de tabela do login (imagens e nível de permissão); rodar depois do 001, em qualquer momento | não (é do banco) |
| `login-redefinir-senha/docs/policies_storage_foto_usuario.sql` | policy SUGERIDA para a troca de foto (você cria à mão) | não |
| `login-redefinir-senha/README.md` | este arquivo | não |

Sem o `.js` publicado o botão não aparece e o login fica como era; sem o SQL a redefinição funciona igual, só não mostra foto/nome, não troca a foto e não registra o evento (a tela avisa «Atualização do banco pendente»). Depois de publicar: **Ctrl+F5**.

## Banco (nada de tabela direto)

A tela de redefinição **não lê nem grava tabela nenhuma**: só chama o Supabase Auth, o Storage (foto) e estas funções (`sql/001`, idempotente, aditivo, **sem `CREATE POLICY`**):

| Função / objeto | Para quê |
|---|---|
| `main_login_meu_perfil()` → jsonb | devolve **só os dados da própria pessoa** logada (`nome, email, foto, iniciais`); sem sessão, `null` |
| `main_login_trocar_minha_foto(p_url)` → jsonb | grava `usuarios_cadastrados.user_foto` **da própria pessoa**, e só se o endereço for `https://…/storage/v1/object/public/fotos/foto_funcionario/<MEU uuid>_<número>.jpg` (não aceita site de fora nem arquivo de outra pessoa) |
| `main_login_registrar_evento(acao, detalhes)` | auditoria: `otp_verificado`, `senha_redefinida`, `redefinicao_cancelada` (lista fechada; 30/hora por pessoa; só as chaves `outras_sessoes` e `pagina`) |
| `main_login_log` (tabela) | **só inclusão**: UPDATE/DELETE/TRUNCATE recusados; RLS ligado **sem policy** e sem `GRANT` (pela API ninguém lê nem grava); guarda quem (do banco, `auth.uid()`), quando, ação, site (Origin), IP (1º do `x-forwarded-for`) e agente. **Nunca** senha, código ou token. Consulta só por SQL: `select created_at, acao, origem, user_email, site, ip from main_login_log order by id desc limit 100;` |

Todas `SECURITY DEFINER` com `search_path` fixo; `anon` **não** executa nenhuma. Testado em Postgres de teste (pglite): `node F:\CLAUDE\_docs\teste-ponte-pglite\pg\teste-login-redefinir-senha.mjs` → **64 verificações**, 0 falha (identidade forjada, 20 endereços de foto recusados, imutabilidade do log, limite por hora, grants, idempotência, nenhuma policy).

### Leituras do login por função (`sql/002`, 2026-10-17)

O login **já lia duas tabelas direto** (era o código original). Agora o `login.html` (os dois) chama funções, com o **mesmo resultado**:

| Antes (direto na tabela) | Agora (função) |
|---|---|
| `kim_locais` (id = 1): `imagem_fundo`, `img_mes` — papel `anon`, antes de entrar | `main_login_imagens()` → `{imagem_fundo, img_mes}` (só essas 2 colunas da linha 1; `anon` e `authenticated` executam) |
| `main_permissao_usuarios`: nível do próprio usuário no(s) módulo(s) da página | `main_login_meus_niveis(p_modulos)` → `[{modulo, nivel_permissao}]` **só da própria pessoa** (`auth.uid()`; não existe parâmetro «quem»), só dos módulos pedidos (máx. 20); sem sessão = `[]`; `anon` **não** executa |

Quem decide «tem permissão?» continua sendo a tela, com as mesmas regras de antes: **site_3** = nível preenchido e diferente de 5 (mais de uma linha do mesmo módulo ou erro = sem permissão, como o antigo `maybeSingle`); **site_4** = nível de 1 a 4 em algum dos módulos da página. Mesmas mensagens, mesmo `signOut` ao negar, mesmo comportamento com sessão já existente.

**Publicar em qualquer ordem:** se o `login.html` novo chegar antes do SQL 002 (função inexistente: `PGRST202`/`42883`), ele **cai sozinho na leitura direta antiga** (aviso no console: «ainda não existe no banco»). Qualquer outro erro da função vale como antes: sem imagem (imagens) ou sem permissão (nível).

Observações: (1) o `SELECT` do papel `anon` em `kim_locais` **não pode sair**: o kiosk público das TVs (`kimape-modo-kiosk/index.html`) lê essa tabela sem login; (2) a policy «cada usuário vê as próprias linhas» de `main_permissao_usuarios` segue necessária (o permus e o código de cada módulo — `dados.js`, `gestao.js`, `solicitacoes.js`, `app.js`… — ainda leem a tabela direto para mostrar nível e locais na tela; isso **não** foi alterado).

### Guards dos módulos (`auth-guard.js`) — mesma função, 2026-10-17

Os guards de **todos** os módulos também liam `main_permissao_usuarios` direto (só o nível, para liberar ou bloquear a tela). Em cada um foi trocado **apenas o comando que lê a tabela** por uma chamada a `main_login_meus_niveis` (ajudante `lerNivelDoModulo`, ou `lerNiveis` no site_4, logo antes de `conferirPermissao`); **tudo o que vem depois é o código original**: mesma regra (nível 1–4 libera; vazio, 5 ou sem linha = «Sem permissão»; erro = «Não foi possível verificar sua permissão», falha fechado), mesmo tratamento de `JWT expired`, mesma reconferência a cada 5 min, mesmas variáveis (`__nivelPermissaoPagina`, `__modulosPermissao`, `__acessoNegado`). Sem o SQL 002 no banco (`PGRST202`/`42883`) o guard **usa a leitura direta antiga**, então publicar na ordem que quiser não derruba módulo nenhum.

| Guards alterados (15 + o modelo do site_4) | |
|---|---|
| site_3 (11 idênticos no trecho) | `apeges-gestao-alunos-arquivo-permanente`, `apettd-arquivo-oraculo`, `apettd-visualiza-arquivos`, `docali-gestao-pedidos`, `docali1-enviar-solicitacoes`, `escatr-gestao-de-atribuicao`, `funiat-gestao`, `kimape-modo-kiosk`, `main-relatorios`, `permus-permissoes-acesso-sigeedu`, `remanejamento` — cada um em `js/auth-guard.js` |
| site_3 (variação) | `gdrive-visualiza-arquivo/js/auth-guard.js` (módulo vem do código da página) |
| site_4 | `lista_chamados`, `atendimento`, `justificativa_ponto` (`js/auth-guard.js`) e o modelo `_compartilhado/js/auth-guard.modelo.js` (`scripts/sincronizar.js --verificar` = cópias iguais ao modelo) |
| **Não** alterados (não liam a tabela) | `site_3_paginas_gerais/auth-guard.js` (raiz) e `gdrive-portal/js/auth-guard.js` |

Em cada página que carrega o guard, o `?v=` do `<script src="js/auth-guard.js?v=N">` subiu para `N+1` (16 HTML: o `index.html`/`gestao.html` do módulo e `docali-gestao-pedidos/imprimir.html`) — só esse número mudou. **Publicar:** o `js/auth-guard.js` de cada módulo e a página dele; se publicar só o `.js`, o navegador troca na próxima vez que expirar o cache (as duas versões funcionam).

**Teste (comparação antigo × novo):** `node F:\CLAUDE\_docs\teste-login\teste-guards.js` roda cada guard original (`_docs/teste-login/guards-originais/`) e o novo em um navegador simulado com os mesmos 13–20 cenários por guard (níveis 1–4, 5, nulo, sem linha, outro módulo, duas linhas, JWT expirado, sem rede, erro genérico, sem sessão; no site_4 as combinações de `oraose`/`orasim`) e exige resultado idêntico (liberou ou não, nível, tela de bloqueio, redirecionamento, mensagens de erro); confere também que com a função presente o guard novo **não** lê a tabela e que com a função ausente ele cai na leitura antiga e fica igual. **15 guards · 895 verificações · 0 falha** (e o teste pega mutações propositais: nível 5 aceito, duas linhas aceitas, reserva quebrada, módulo errado, faixa de nível maior). **Voltar atrás:** os 18 originais estão em `_docs/teste-login/guards-originais/` (nome = caminho com `__`), basta copiá-los de volta.

### Policy da foto (você cria)

Pessoa comum não consegue enviar arquivo ao bucket `fotos` (só a policy do permus libera). `docs/policies_storage_foto_usuario.sql` traz a policy que libera **só** o envio de `foto_funcionario/<meu uuid>_<número>.jpg`, sem UPDATE/DELETE. Sem ela a redefinição de senha funciona; só a **troca de foto** falha, com aviso claro («peça ao administrador»). A foto antiga continua no bucket (limpeza é do administrador).

## Segurança — o que foi decidido

- **Não revela quem tem conta:** o pedido do código responde igual para usuário que existe e que não existe (o app antigo consultava `view_usuarios_email` e dizia «nenhum usuário encontrado»; isso foi **removido** de propósito). Se o servidor responder «aguarde N segundos», a tela leva à etapa do código (o anterior ainda vale).
- **Senha:** 8 a 72 bytes (limite do bcrypt), maiúscula e minúscula, número, caractere especial e **sem** espaços, sem o próprio usuário (ex.: `ana.silva` → «silva»), sem sequências (`1234`, `qwer`…), sem repetição (`aaaa`) e sem palavras óbvias (`senha`, `sigeedu`, `colider`…). **É só conferência da tela**; o servidor só exige o mínimo dele (veja abaixo).
- **Sessão da recuperação:** o código certo dá uma sessão de verdade. Ao terminar (ou ao voltar), ela é encerrada; se a pessoa **recarregar** a página no meio, a tela volta para a nova senha e o login **não** a encaminha ao sistema (marca na `sessionStorage` desta aba). Se a pessoa **fechar a aba** sem terminar, a sessão fica no navegador como um login comum — é como o Supabase funciona; vale lembrar a quem usa computador compartilhado.
- **Outras sessões:** marcado por padrão; usa `signOut({ scope: 'global' })` depois de trocar a senha (derruba todas as sessões, inclusive esta, e a pessoa entra de novo).
- A senha nunca vai para URL, `localStorage`, `sessionStorage` nem log; os campos são limpos ao concluir/voltar; `autocomplete="new-password"` (gerenciador de senhas oferece salvar).
- O bloqueio de 5 tentativas na tela é só conforto; quem barra de verdade é o limite do servidor.

## O que confirmar no servidor Supabase (não dá para ver daqui)

- O **hook de e-mail** (`GOTRUE_HOOK_SEND_EMAIL_*`, já ligado no `docker-compose`) trata `recovery` e manda o `email_data.token` (6 dígitos) por WhatsApp — o app antigo já dependia disso.
- `GOTRUE_MAILER_OTP_LENGTH` = 6 (padrão). Se mudar, ajuste `OTP_TAMANHO` no `.js`.
- Recomendado: `GOTRUE_PASSWORD_MIN_LENGTH=8` (hoje o padrão do GoTrue é 6; a tela já exige 8, mas quem chama a API direto não). `GOTRUE_PASSWORD_REQUIRED_CHARACTERS` só se quiserem impor as classes no servidor — **afeta todos os fluxos** (criação de usuário no permus, app antigo).
- `GOTRUE_SMTP_MAX_FREQUENCY` (padrão 60 s) é o que a contagem de reenvio espera.
- Quem não tem WhatsApp cadastrado não recebe o código (e a tela não pode dizer isso sem revelar quem tem conta): a mensagem manda confirmar na Secretaria.

## Testes feitos (2026-10-17)

SQL em pglite (`teste-login-redefinir-senha.mjs`: 64 verificações; `teste-login-leituras.mjs`, do 002: **27** — só as linhas da própria pessoa, só os módulos pedidos, nada além das 2 colunas das imagens, `anon` sem acesso à de níveis, limite de 20, texto estranho tratado como dado, mesma resposta que a consulta direta antiga) e a tela no navegador com Supabase simulado (`F:\CLAUDE\_docs\teste-login`, veja o `LEIA-ME.md` de lá): fluxo completo nos dois logins; erros de envio (500/429/sem rede); código errado, bloqueio e reenvio; foto (recorte, envio, erro de Storage, banco sem as funções); recarregar na etapa 3; senha igual à atual; cancelar; login com a senha nova; sessão normal ainda vai ao sistema; publicação parcial; claro e escuro (auditoria de contraste: 0 falhas; nenhuma classe de cor nova, então o `tema.js` não mudou); celular 375 px. **Não testado contra o Supabase/n8n/WhatsApp reais.**

## Limites e decisões

- Depois de redefinir, a pessoa **entra de novo** (não segue direto ao sistema): prova que a senha nova funciona e passa pela checagem de permissão do login.
- O código OTP vale o tempo que o GoTrue configurar; a tela não mostra validade.
- O log (`main_login_log`) não tem tela nem policy de leitura; quem decide quem pode ver é o administrador (sugestão: níveis 3/4 de um módulo, como no grupo L).
- As leituras de `main_permissao_usuarios` e `kim_locais` do login **e dos guards** passaram para funções no `sql/002` (seções acima); o caminho direto antigo só roda como reserva enquanto o SQL 002 não existe no banco. O código de cada módulo que lê a tabela para mostrar nível/locais na tela (`dados.js`, `gestao.js`, `solicitacoes.js`, `app.js`, `menu.js`/`util.js` do site_4…) **não** foi alterado.
