Commit Graph
86 Commits
Author SHA1 Message Date
GustavoandClaude Sonnet 5 b1f93c2d68 feat: read-only internal endpoint for GTI Fiscal to mirror clients
GET /internal/fiscal/clients returns every CLIENTE (active and inactive)
as {external_client_id, code, display_name, active} — CLIENTE.ID is the
identity (CODIGO is editable, same reasoning as FUNCIONARIO_PAINEL.ID in
SSO). Authenticated by a dedicated bearer token (FISCAL_INTERNAL_API_TOKEN,
never reused from SESSION_SECRET/SSO key/admin password), rate-limited per
IP, returns 503 rather than opening anonymously when the token isn't
configured. GTI Gerenciador stays the single source of truth: the Fiscal
Hub never receives MySQL credentials.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 22:36:40 -03:00
GustavoandClaude Opus 5.5 161da90e02 feat: bind the GTI Fiscal SSO assertion to the browser (state/nonce)
/sso/fiscal now requires the state that GTI Fiscal creates when the flow
starts and signs it as the "nonce" claim; GTI Fiscal checks it against
its own one-time cookie. Without a valid state the panel sends the
browser to GTI Fiscal's /login, so the flow always starts there. This
prevents someone from pushing an assertion for their own account into
another person's browser (login CSRF).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 12:21:27 -03:00
GustavoandClaude Opus 5.5 9139a1f6f3 feat: single sign-on from the panel into GTI Fiscal
The sidebar "Fiscal" entry now goes to /sso/fiscal. For a logged-in,
active employee the panel issues a JWT signed with Ed25519 (the private
key exists only here; GTI Fiscal only holds the public key): iss/aud
fixed, sub = funcionario:<ID> (LOGIN is editable), 60 s lifetime, unique
jti that GTI Fiscal records to reject replays. No password, hash or panel
cookie leaves the panel.

The assertion is delivered in an auto-submitted POST, never in the URL,
so it stays out of proxy logs, browser history and Referer. That page
carries its own CSP allowing form-action only to the configured callback
origin; the security-headers middleware now keeps a route-defined CSP.

Without a session the employee goes through the normal login and comes
back; plain HTTP is redirected to the public HTTPS address; `next` only
accepts a relative path; the .env bootstrap account (no stable ID) is
refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 08:35:35 -03:00
GustavoandClaude Opus 5.5 4f9bea7799 feat: add a "Fiscal" link to the sidebar
Opens the GTI Fiscal Hub (https://gtifiscal.gtigrupo.com.br) in a new
tab. Link only: GTI Fiscal keeps its own login, the panel's
authentication is unchanged. Already deployed to production.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 00:37:50 -03:00
Gustavo 2e23904b99 fix: tabelas nao ficam mais espremidas no mobile (colunas quebrando letra por letra) - min-width em vez de width fixo, permitindo rolagem horizontal 2026-08-20 14:13:25 -03:00
Gustavo ab8912fce7 fix: adiciona cache-busting nos arquivos estaticos (css/js) para evitar layout desatualizado apos deploy 2026-08-20 13:52:07 -03:00
Gustavo 48d17d15b5 feat: ajusta colunas, alinhamento e densidade da tabela de clientes 2026-08-20 13:35:05 -03:00
Gustavo 9b7210d41c docs: plano da melhoria de ui/ux da tabela de clientes 2026-08-20 12:09:55 -03:00
Gustavo 66c50b252a docs: spec da melhoria de ui/ux da tabela de clientes 2026-08-20 12:08:54 -03:00
Gustavo d2135bce5d fix: botao mostrar/ocultar do log nao abria (elemento interativo dentro de summary nao propaga toggle nativo) 2026-08-20 11:48:54 -03:00
Gustavo cf742b4341 feat: secoes da tela de log comecam fechadas, com clique para abrir 2026-08-20 11:39:11 -03:00
Gustavo 565649e50d docs: plano das secoes escondidas na tela de log 2026-08-20 11:34:43 -03:00
Gustavo ca5db5b46f docs: spec das secoes escondidas na tela de log 2026-08-20 11:33:41 -03:00
Gustavo 4ec3c8df88 feat: limite de 20 registros e botao de limpar log (admin) 2026-08-20 10:52:48 -03:00
Gustavo 9fbc57b0ad feat: bolinha de status ativo/inativo clicavel na lista de clientes 2026-08-20 10:51:28 -03:00
Gustavo 0c82170c26 feat: alternar_ativo em CLIENTE e limpar em LOG_PAINEL (camada de banco) 2026-08-20 10:49:43 -03:00
Gustavo 0fdf4ab4e3 feat: campo Ativo no cadastro e filtro de status na listagem de clientes 2026-08-20 09:53:55 -03:00
Gustavo 710d688d38 feat: filtro de status (ativos/inativos/todos) na rota de listagem 2026-08-20 09:52:28 -03:00
Gustavo 4fd4649edd feat: coluna ATIVO em CLIENTE e filtro na camada de banco 2026-08-20 09:49:19 -03:00
Gustavo 53dcd1c9d1 feat: botao de gerar chave de reativacao por linha na lista de clientes 2026-08-20 09:31:13 -03:00
Gustavo b830ce14dc feat: passa csrf_token no contexto da listagem de clientes 2026-08-20 09:30:08 -03:00
GustavoandClaude Sonnet 5 9a7d12f941 fix: achados da revisao final (remove TAMANHO_PAGINA morto, httponly no cookie de paginacao, limpa testes)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 09:11:08 -03:00
GustavoandClaude Sonnet 5 9ce6bcdcad feat: seletor de linhas por pagina e paginacao numerada na listagem de clientes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 09:01:12 -03:00
Gustavo 5a3c4ac092 feat: logout apaga o cookie de preferencia de linhas por pagina 2026-08-20 08:55:56 -03:00
Gustavo 819c1f8e28 feat: GET /clientes le por_pagina da query/cookie e expoe contexto de paginacao 2026-08-20 08:52:17 -03:00
Gustavo 9d5702a01e feat: funcoes puras de resolucao de tamanho de pagina e janela de paginas 2026-08-20 08:48:35 -03:00
GustavoandClaude Sonnet 5 f940ff5768 feat: bloqueia CNPJ/CPF duplicado ao cadastrar ou editar cliente
Mesmo padrão já usado pro CODIGO (db_clientes.codigo_em_uso): nova
db_clientes.cnpj_em_uso(documento, ignorar_id=None) checada em
criar()/editar() antes de gravar. A comparação ignora pontuação dos dois
lados (não só normaliza o valor recebido) porque CLIENTE.CNPJ tem
formatação inconsistente em registros legados (alguns com máscara,
outros sem) — sem isso, um cadastro novo com o mesmo documento de um
legado ainda mascarado passaria batido.

app/templates/cliente_form.html: nova mensagem de erro
'erro_cnpj_duplicado', mesmo estilo visual do erro de código duplicado.

6 testes novos (unitário do db + integração do router pros dois
fluxos), suíte completa (196 testes) passando.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 09:13:43 -03:00
GustavoandClaude Sonnet 5 eed473c5f3 fix: telefone de contato sem DDI e updates/excludes que fingiam sucesso
- app/routers/contatos.py: CLIENTE_CONTATO.TELEFONE passa a ser gravado
  sempre com DDI 55 (normalizar_telefone_com_ddi, mesma normalização já
  usada em CLIENTE.TELEFONE/CELULAR) — confirmado no node '10 - Validar
  Telefones' do workflow n8n de produção (ER6Piy9yu93uOVuY) que o
  telefone é usado exatamente como está gravado, sem nenhuma
  normalização própria. Um contato cadastrado pelo painel sem o
  funcionário digitar '55' na frente nunca recebia a chave automática
  por WhatsApp — falha silenciosa, sem nenhum erro visível.

- app/db/clientes.py, app/db/contatos.py, app/db/funcionarios.py:
  excluir_cliente/atualizar_contato/excluir_contato/redefinir_senha
  passam a retornar bool (cur.rowcount > 0) em vez de None — um
  ID inexistente não gera erro nenhum do MySQL (WHERE só não bate com
  nada), então sem checar rowcount o router não tinha como saber que
  não alterou/excluiu nada.

- app/routers/clientes.py, app/routers/contatos.py,
  app/routers/funcionarios.py: editar/excluir/redefinir-senha/
  alternar-ativo passam a checar essa existência e mostrar 'não
  encontrado' em vez de registrar 'sucesso' no log e redirecionar pra
  uma tela que também vai dizer que não encontrou nada.

- app/templates/clientes_lista.html: busca não estava com urlencode nos
  links de paginação/ordenação — um termo de busca com '&' (comum em
  razão social, ex. 'Fulano & Cia') cortava o resto da query string do
  link, perdendo página/ordenação.

Suíte completa (192 testes) passando, com 13 testes novos cobrindo os
casos de 'não encontrado' e o DDI do telefone de contato.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 11:50:37 -03:00
GustavoandClaude Sonnet 5 d7a08ddd8d security: rate limit no login, cookies Secure condicional, security headers e deps com CVE
- app/rate_limit.py: bloqueia por IP após 5 tentativas de senha erradas
  em 5 minutos em POST /login (rota pré-autenticação, hoje sem nenhuma
  proteção contra força bruta).
- Cookie de sessão (routers/auth.py) e cookie CSRF (main.py) passam a
  sair com Secure quando a requisição chega em HTTPS (request.url.scheme)
  — hoje é sempre False porque produção ainda serve HTTP puro, mas o
  cookie passa a ficar protegido automaticamente assim que um TLS for
  colocado na frente, sem precisar editar isso de novo.
- main.py: novo middleware de security headers (X-Content-Type-Options,
  X-Frame-Options: DENY, Referrer-Policy, Content-Security-Policy,
  Strict-Transport-Security condicional a HTTPS).
- requirements.txt: fastapi 0.115.0 -> 0.115.6 (starlette >=0.40.0,
  corrige DoS por memória não limitada ao parsear multipart/form-data —
  GHSA-f96h-pmfr-66vw), python-multipart 0.0.9 -> 0.0.20 (várias
  vulnerabilidades de DoS corrigidas), jinja2 3.1.4 -> 3.1.6 (escapes de
  sandbox). httpx/uvicorn/PyMySQL já estavam em versões sem CVE
  conhecida — conferido via osv.dev.
- Dockerfile: container não roda mais como root.

Suíte completa (181 testes) passando, incluindo testes novos para o
rate limiter (unitários + integração via POST /login) e smoke test
manual confirmando headers, fluxo de login com CSRF e o bloqueio por
força bruta.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 11:17:41 -03:00
GustavoandClaude Sonnet 5 b0830651f4 feat(painel-clientes-web): favicon com a logomarca da GTI
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 14:23:44 -03:00
GustavoandClaude Sonnet 5 dea9dc4401 fix: corrige achados da revisao final (DDI idempotente, mascara DDI 12/13 digitos, status DV, validacao no load, autofill dispara mascaras)
Endereca 5 achados da revisao final de branch:
- Important #1: normalizar_telefone_com_ddi agora e idempotente (nao duplica 55 independente do tamanho)
- Important #2: mascara de telefone detecta DDI colado por tamanho total (12/13 digitos), nao mais so pelo campo especifico
- Minor #3: buscarCnpj pula lookup na Receita quando DV do CNPJ e invalido, preservando a mensagem de erro
- Minor #4: detectarEValidarDocumento roda uma vez no carregamento da pagina
- Minor #5: autofill da Receita dispara evento input para acionar as mascaras de UF/CEP/TELEFONE

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:31:20 -03:00
GustavoandClaude Sonnet 5 eec7ce21b5 feat: validação de CNPJ e máscaras de telefone/UF/CEP no formulário de cliente
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:21:15 -03:00
GustavoandClaude Sonnet 5 59f15d9ac0 feat: bloquear CNPJ com DV inválido, gravar telefone/UF/CEP normalizados
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:18:24 -03:00
GustavoandClaude Sonnet 5 8b5e7e251e feat: normalizar_telefone_com_ddi/normalizar_uf/normalizar_cep
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:14:39 -03:00
GustavoandClaude Sonnet 5 5c577a644a fix(seguranca): move algoritmo de geracao de chave do JS pro backend
O algoritmo (CHAVE_INTERNA + funcoes gerarReativacao/gerarInstalacao)
ficava 100% legivel no codigo-fonte da pagina — qualquer pessoa com
acesso ao painel conseguia abrir o DevTools e extrair a logica pra
gerar chaves de licenca fora do sistema.

Move tudo pra app/gerador_chave.py (roda so no servidor) e expoe via
POST /gerador-chave/gerar (autenticado, protegido por CSRF). A pagina
agora so manda os dados do formulario e mostra o resultado que volta
do backend. Algoritmo verificado byte-a-byte contra os 3 exemplos
reais conhecidos (cliente 000001, ALUGUEL). Bonus: o byte inicial
agora usa secrets.randbelow (CSPRNG) em vez de Math.random() do JS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 10:03:19 -03:00
GustavoandClaude Sonnet 5 a3a84abd37 fix: campo codigo do cliente so aceita numeros + botoes de tabela nao quebram linha
Gerador de chave: filtra caracteres nao numericos do campo codigo em
tempo real (input event), alem da validacao ja existente no submit.

Tabelas: a.botao/button ganham white-space: nowrap, corrigindo o
botao 'Abrir' (e outros) quebrando em duas linhas depois que as
celulas de dado passaram a permitir quebra de texto longo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 09:19:44 -03:00
GustavoandClaude Sonnet 5 f6cf0f2e53 fix: botao de ver senha no login ficava esticado pra largura total
.login-cartao button { width: 100%; } tinha mais especificidade CSS
que .botao-ver-senha e forcava o botao do olho a ocupar 100% da
largura do campo, jogando o icone (alinhado por padrao a esquerda no
flex) pro canto esquerdo por cima do input. Regra do width:100% agora
so mira o botao de submit; o campo de senha vira um wrapper flex
(input + botao lado a lado), mais robusto que posicionamento absoluto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 08:56:20 -03:00
GustavoandClaude Sonnet 5 1728a2dbdd fix: sidebar fixa na tela (nao sobe com tabelas longas) + mostrar/ocultar senha no login
Sidebar antes esticava junto com o conteudo (align-items:stretch do
flex), entao em telas com tabela longa (Clientes) o rodape (tema/sair/
recolher) ficava fora da area visivel sem rolar a pagina toda. Agora
fica position:sticky, presa ao topo da viewport, com rolagem propria
se a lista de links for maior que a tela.

Login ganha um botao de olho pra mostrar/ocultar a senha digitada.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 08:52:35 -03:00
GustavoandClaude Sonnet 5 7febe7c652 fix: evita rolagem horizontal nas tabelas (celulas com quebra de linha)
O white-space: nowrap em toda celula de tabela impedia o texto de
quebrar linha, entao valores longos de Razao/Fantasia/CNPJ estouravam
a largura da tabela e forcavam rolagem horizontal mesmo em telas
grandes. Cabecalhos continuam sem quebra (nomes curtos); celulas de
dado agora quebram linha quando necessario.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 08:45:11 -03:00
GustavoandClaude Sonnet 5 6aeed3af7e fix: aplica classe ativo nos links da sidebar conforme rota atual
Adiciona class condicional (ativo se request.url.path.startswith(...))
em cada link de app/templates/base.html, usando o estilo .sidebar-nav
a.ativo que ja existia em estilo.css mas nunca era aplicado.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:48:37 -03:00
GustavoandClaude Sonnet 5 9012795973 fix: corrige contraste AA do laranja de acento no tema claro
- --cor-acento-contraste passa de branco pra #17171a (texto escuro sobre
  fundo laranja em botoes, unificando com o tema escuro) - contraste ~7:1
- novo token --cor-acento-texto (#B3510C no tema claro, #F5821F no
  tema escuro) usado para laranja como cor de TEXTO sobre fundo claro
  (link padrao, badge-acento, item ativo da sidebar, chave gerada em
  gerador_chave.html) - contraste 5.12:1 contra branco, acima do
  minimo AA de 4.5:1

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:48:12 -03:00
GustavoandClaude Sonnet 5 b5098a3aed feat: redesign da tela de log (badges de status)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:38:01 -03:00
GustavoandClaude Sonnet 5 9cef2c8d17 feat: redesign de minha-conta e gerador de chave
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:35:05 -03:00
GustavoandClaude Sonnet 5 9114303507 feat: redesign da lista e formulario de funcionarios (badges de perfil/status)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:32:07 -03:00
GustavoandClaude Sonnet 5 18029de9f8 feat: redesign do detalhe de cliente (contatos e status de chaves)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 22:04:31 -03:00
GustavoandClaude Sonnet 5 343f61ade7 feat: redesign da lista e formulario de cliente (cartoes, grid, tabela responsiva)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 22:01:11 -03:00
GustavoandClaude Sonnet 5 e21b1646a7 feat: redesign da tela de login com identidade GTI
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:57:37 -03:00
GustavoandClaude Sonnet 5 310f5e1f71 feat: sistema de design (tokens claro/escuro) + sidebar responsiva em base.html
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:53:30 -03:00
GustavoandClaude Sonnet 5 7656f2261e docs: corrige README contraditorio sobre HTTP Basic e SESSION_SECRET
1. Frase de abertura ainda dizia 'login HTTP Basic em toda rota',
   contradizendo a secao 'Login e permissoes' mais abaixo no mesmo
   arquivo (ja correta, fala de login por sessao). Atualizada para
   'login por sessao em toda rota'.
2. A lista de variaveis de ambiente para rodar localmente (secao
   'Rodando localmente') nao mencionava SESSION_SECRET, mesmo a secao
   'Login e permissoes' ja instruindo a defini-la. Adicionada a lista,
   com nota de que e opcional em dev (chave aleatoria em memoria a cada
   start, sessoes caem a cada restart) mas deve ser fixa em producao.
3. A mencao ao ALTER TABLE FUNCIONARIO_PAINEL ADD COLUMN PERFIL estava
   em tom de fato passado. Ajustada para o mesmo padrao de aviso ja
   usado para LOG_PAINEL/FUNCIONARIO_PAINEL nesse README ('precisa ser
   rodado manualmente em producao tambem'), deixando explicito que sem
   rodar isso todo login falha com 500 (a query de autenticacao
   seleciona uma coluna PERFIL que nao existe na tabela antiga).

Sem teste automatizado - mudanca só de documentacao.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:19:09 -03:00
GustavoandClaude Sonnet 5 e5f5d4211c fix: desativar/rebaixar funcionario nao revogava sessao ativa (ate 12h)
Perfil e status ativo vinham so do cookie assinado (session.py), entao um
funcionario desativado (ATIVO=0) ou rebaixado (ADMIN->PADRAO) continuava
com acesso total ate o cookie expirar (ate 12h) - inclusive a
/funcionarios se era admin.

exigir_login (app/auth.py) agora reconfirma contra o banco a cada
requisicao quando o login nao e o da conta de bootstrap (ADMIN_USER do
.env, que nao tem linha em FUNCIONARIO_PAINEL e continua sempre admin via
variavel de ambiente): busca db_funcionarios.obter_por_login(login); se
nao existir mais ou ATIVO != 1, levanta NaoAutenticado (forca novo
login); caso contrario, monta o UsuarioLogado com o PERFIL atual do
banco, nao o do cookie - rebaixamento tambem passa a ter efeito imediato.

Testes de regressao em tests/test_auth.py:
- test_exigir_login_autentica_funcionario_ativo
- test_exigir_login_revoga_sessao_ao_desativar_funcionario
- test_exigir_admin_usa_perfil_atual_do_banco_nao_do_cookie
- test_exigir_login_conta_bootstrap_nao_consulta_banco

Ajustou tambem test_exigir_admin_rejeita_padrao (usava um login fake sem
linha em FUNCIONARIO_PAINEL, que agora conflita com a checagem nova -
passou a usar a fixture funcionario_teste). Suite inteira roda
147 passed apos a mudanca.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:18:58 -03:00