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>
95 lines
5.1 KiB
HTML
95 lines
5.1 KiB
HTML
<!-- app/templates/base.html -->
|
|
<!DOCTYPE html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{% block titulo %}Painel de Clientes{% endblock %}</title>
|
|
<link rel="icon" type="image/png" href="/static/logo-gti-256x256.png">
|
|
<link rel="stylesheet" href="/static/estilo.css?v={{ request.state.asset_versao }}">
|
|
<script>
|
|
(function () {
|
|
var tema = localStorage.getItem("tema");
|
|
if (tema === "claro" || tema === "escuro") {
|
|
document.documentElement.setAttribute("data-tema", tema);
|
|
}
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="layout">
|
|
<div class="sidebar-backdrop" id="sidebar-backdrop"></div>
|
|
<aside class="sidebar" id="sidebar">
|
|
<div class="sidebar-topo">
|
|
<img src="/static/logo-gti-256x256.png" alt="GTI Grupo" class="sidebar-logo">
|
|
<span class="sidebar-titulo">Painel de Clientes</span>
|
|
</div>
|
|
<nav class="sidebar-nav">
|
|
<a href="/clientes" class="{{ 'ativo' if request.url.path.startswith('/clientes') else '' }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
|
<span>Clientes</span>
|
|
</a>
|
|
<a href="/gerador-chave" class="{{ 'ativo' if request.url.path.startswith('/gerador-chave') else '' }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="8" cy="15" r="4"/><path d="M11 12l9-9M17 6l3 3M14 9l2 2"/></svg>
|
|
<span>Gerador de chave</span>
|
|
</a>
|
|
{% if request.state.usuario and request.state.usuario.eh_admin %}
|
|
<a href="/funcionarios" class="{{ 'ativo' if request.url.path.startswith('/funcionarios') else '' }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="7" r="4"/><path d="M2 21v-2a5 5 0 0 1 5-5h4a5 5 0 0 1 5 5v2M17 11l2 2 4-4"/></svg>
|
|
<span>Funcionários</span>
|
|
</a>
|
|
{% endif %}
|
|
<a href="/log" class="{{ 'ativo' if request.url.path.startswith('/log') else '' }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-4"/><path d="M9 15h6M9 11h10M9 7h4"/></svg>
|
|
<span>Log</span>
|
|
</a>
|
|
<a href="/sso/fiscal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
|
|
<span>Fiscal</span>
|
|
</a>
|
|
{% if request.state.usuario %}
|
|
<a href="/minha-conta" class="{{ 'ativo' if request.url.path.startswith('/minha-conta') else '' }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>
|
|
<span>Minha conta</span>
|
|
</a>
|
|
{% endif %}
|
|
</nav>
|
|
<div class="sidebar-rodape">
|
|
{% if request.state.usuario %}
|
|
<div class="sidebar-usuario">{{ request.state.usuario.nome }}</div>
|
|
<button type="button" class="botao-tema" id="botao-tema" aria-label="Alternar tema claro/escuro">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
|
|
<span>Tema claro/escuro</span>
|
|
</button>
|
|
<form method="post" action="/logout" class="form-logout">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token or request.state.csrf_token }}">
|
|
<button type="submit" class="botao-sair">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5M21 12H9"/></svg>
|
|
<span>Sair</span>
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
<button type="button" class="botao-recolher" id="botao-recolher" aria-label="Recolher menu">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 19l-7-7 7-7M18 19l-7-7 7-7"/></svg>
|
|
<span>Recolher</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
<div class="conteudo-area">
|
|
<header class="topbar-mobile">
|
|
<button type="button" class="topbar-menu-btn" id="btn-menu-mobile" aria-label="Abrir menu">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:22px;height:22px"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
|
|
</button>
|
|
<span class="topbar-titulo">{{ self.titulo() }}</span>
|
|
</header>
|
|
<main class="conteudo">
|
|
{% if mensagem %}<div class="mensagem {{ mensagem_tipo or 'success' }}">{{ mensagem }}</div>{% endif %}
|
|
{% block conteudo %}{% endblock %}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<script src="/static/sidebar.js?v={{ request.state.asset_versao }}" defer></script>
|
|
<script src="/static/tema.js?v={{ request.state.asset_versao }}" defer></script>
|
|
</body>
|
|
</html>
|