/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>
painel-clientes-web
Painel administrativo (FastAPI + Jinja2) para gestão de CLIENTE, CLIENTE_CONTATO e geração manual de chave de licenciamento no MySQL do sistema legado.
Proteções: login por sessão em toda rota (cookie assinado — ver seção "Login e permissões" abaixo), CSRF via cookie de duplo envio (app/csrf.py — todo formulário que muda estado embute o mesmo token do cookie csrf_token, definido pelo middleware em app/main.py) e a lista de clientes é paginada (100 por página), com busca e ordenação por código/razão/fantasia/CNPJ.
CODIGO de cliente não aceita duplicado (bloqueio duro, sem bypass) e o formulário de novo cliente já sugere o próximo código sequencial (editável). Só CODIGO, CNPJ, RAZAO e FANTASIA são obrigatórios — os campos de contrato (implantação, mensalidade, juros, datas) podem ficar em branco e recebem default (0,00 / data de hoje).
CLIENTE tem uma coluna ATIVO (criada no banco de dev — precisa ser criada manualmente em produção também, ALTER TABLE completo no início de app/db/clientes.py) que controla o filtro de status da listagem (Ativos/Inativos/Todos, padrão Ativos). Sem essa coluna, a listagem quebra com erro 500. O DEFAULT 1 do ALTER TABLE é intencional: todo cliente cadastrado antes da migração nasce como Ativo.
A rota /log mostra o histórico de ações do painel (tabela LOG_PAINEL, criada no banco de dev — precisa ser criada manualmente em produção também, DDL no início de app/db/log.py) e, se N8N_API_URL/N8N_API_KEY estiverem configuradas (.env.example), os últimos envios automáticos de chave via WhatsApp (lidos da Data Table pagamentos_processados do n8n, somente leitura).
Login e permissões
O painel usa login por sessão (cookie assinado, app/session.py) — não é
mais HTTP Basic Auth. Defina SESSION_SECRET no .env (.env.example
tem o exemplo); sem isso, uma chave é gerada em memória a cada start do
processo, o que derruba todas as sessões ativas a cada restart (funciona,
só incomoda).
Cada funcionário (FUNCIONARIO_PAINEL, criada no banco de dev — precisa
ser criada manualmente em produção também, DDL no início de
app/db/funcionarios.py) tem um perfil: admin (gerencia outros
funcionários em /funcionarios: cadastra, edita, redefine senha,
ativa/desativa, promove/rebaixa perfil) ou padrão (sem acesso a
/funcionarios). Se FUNCIONARIO_PAINEL já existir de uma instalação
anterior sem a coluna PERFIL, rode o ALTER TABLE FUNCIONARIO_PAINEL ADD COLUMN PERFIL ... (DDL completo em app/db/funcionarios.py) — precisa
ser rodado manualmente em produção também; sem essa coluna, todo login
falha com erro 500 (a query de autenticação seleciona PERFIL, que não
existe na tabela antiga). O DEFAULT 'ADMIN' desse ALTER TABLE é
intencional: todo funcionário cadastrado antes da migração vira admin
automaticamente. O sistema nunca deixa ficar sem nenhum admin ativo —
rebaixar ou desativar o último admin é bloqueado.
Qualquer usuário logado troca a própria senha em Minha conta
(/minha-conta), pedindo a senha atual antes de trocar.
ADMIN_USER/ADMIN_PASSWORD (.env.example) continuam funcionando como
conta de bootstrap, sempre com perfil admin — é com ela que você entra a
primeira vez para cadastrar o primeiro funcionário, e serve de plano B se
a tabela ficar vazia. Login checa primeiro o admin do .env, depois
FUNCIONARIO_PAINEL (só contas com ATIVO=1).
Rodando localmente (sem Docker)
- Instale as dependências de desenvolvimento:
pip install -r requirements-dev.txt - Defina as variáveis de ambiente necessárias (
ADMIN_USER,ADMIN_PASSWORD,DB_HOST,DB_PORT,DB_NAME,DB_USER,DB_PASSWORD,SESSION_SECRET— veja.env.example) diretamente no shell antes de subir a aplicação.SESSION_SECRETé opcional em dev — sem ela, uma chave aleatória é gerada em memória a cada start (sessões caem a cada restart); defina um valor fixo em produção. Importante: apesar depython-dotenvestar nas dependências, nada no código chamaload_dotenv()hoje — um arquivo.envsozinho NÃO é lido automaticamente fora do Docker. Exporte as variáveis manualmente (ex.:set -a; source .env; set +ano bash, ou$env:...no PowerShell) ou defina-as por outro meio. - Rode a aplicação:
uvicorn app.main:app --reload - Rodando direto na máquina (fora de container), use
DB_HOST=127.0.0.1(porta3307, a mesma do container de devlicenciamento-mysql-dev).
Rodando os testes
pytest -v
Os testes usam defaults definidos em tests/conftest.py (equivalentes ao .env.example) e incluem testes de integração que precisam do container MySQL de dev (licenciamento-mysql-dev, 127.0.0.1:3307) rodando.
Rodando via Docker
- Copie o arquivo de exemplo de variáveis de ambiente e edite
ADMIN_PASSWORD(e demais valores, se necessário):cp .env.example .env - Suba o container:
O
docker compose up -d --builddocker-compose.ymljá usaenv_file: .enve mapeiahost.docker.internalpara o host (viaextra_hosts), então o valor padrãoDB_HOST=host.docker.internaldo.env.examplefunciona também em hosts Docker Linux, não só no Docker Desktop (Windows/Mac). - A aplicação fica disponível em
http://localhost:8010.