feat: SSO from GTI Gerenciador and the Gerenciador visual identity
Collaborators logged into GTI Gerenciador open GTI Fiscal without a second password. The panel signs a 60 s JWT (Ed25519; the Hub holds only the public key); the Hub checks signature, iss/aud/exp/nbf, maximum lifetime, nonce against a one-time state cookie set when the flow starts at GET /login (login-CSRF protection) and a single-use jti (sso_used_assertions). Collaborators are provisioned in admin_users with auth_source=GTI_GERENCIADOR and no password; a CHECK constraint makes a passwordless local account or a federated account with a password impossible (migration 0021). Sessions last FEDERATED_SESSION_SECONDS (1 h) and are renewed silently through the panel. The local admin stays as break-glass at /login/local, audited. Logout ends only the Hub session. Every SSO success/rejection (stable reason code), logout and local login is audited with the real client IP, never the token. verify_password now fails closed on a missing hash instead of raising. Visual identity: gti-theme.css is a verbatim copy of the panel's design system (tokens, sidebar, light/dark theme) plus its tema.js, sidebar.js and logo; control-panel.css keeps only Hub components, all colours from the panel tokens. Page structure and features unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,16 +3,89 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{% block title %}GTI Fiscal Control Panel{% endblock %}</title>
|
||||
<title>{% block title %}GTI Fiscal{% endblock %}</title>
|
||||
<link rel="icon" type="image/png" href="/static/logo-gti-256x256.png">
|
||||
<!-- Mesma identidade visual do GTI Gerenciador: tokens/layout em gti-theme.css (cópia fiel), ajustes do Fiscal em control-panel.css -->
|
||||
<link rel="stylesheet" href="/static/gti-theme.css">
|
||||
<link rel="stylesheet" href="/static/control-panel.css">
|
||||
<script>
|
||||
(function () {
|
||||
var tema = localStorage.getItem("tema");
|
||||
if (tema === "claro" || tema === "escuro") {
|
||||
document.documentElement.setAttribute("data-tema", tema);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/">GTI <span>FISCAL</span></a>
|
||||
<nav aria-label="Navegação principal"><a href="/">Resumo</a><a href="/agents">Agents</a><a href="/blockers">Blockers</a><a href="/jobs">Jobs</a></nav>
|
||||
{% if auth %}<form method="post" action="/logout"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost" type="submit">Sair</button></form>{% endif %}
|
||||
</header>
|
||||
<main class="shell">{% block content %}{% endblock %}</main>
|
||||
{% set path = request.url.path %}
|
||||
<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">GTI Fiscal</span>
|
||||
</div>
|
||||
<nav class="sidebar-nav" aria-label="Navegação principal">
|
||||
{% if panel_home %}
|
||||
<a href="{{ panel_home }}" class="sidebar-voltar">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
|
||||
<span>Painel de Clientes</span>
|
||||
</a>
|
||||
<div class="sidebar-secao"><span>Fiscal</span></div>
|
||||
{% endif %}
|
||||
<a href="/" class="{{ 'ativo' if path == '/' else '' }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></svg>
|
||||
<span>Resumo</span>
|
||||
</a>
|
||||
<a href="/agents" class="{{ 'ativo' if path.startswith('/agents') else '' }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><path d="M7 7.5h.01M7 16.5h.01"/></svg>
|
||||
<span>Agents / Servidores</span>
|
||||
</a>
|
||||
<a href="/blockers" class="{{ 'ativo' if path.startswith('/blockers') else '' }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4Z"/><path d="M12 8v5M12 16h.01"/></svg>
|
||||
<span>Blockers</span>
|
||||
</a>
|
||||
<a href="/jobs" class="{{ 'ativo' if path.startswith('/jobs') else '' }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>
|
||||
<span>Jobs</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-rodape">
|
||||
{% if auth %}
|
||||
<div class="sidebar-usuario">{{ auth[1].display_name or auth[1].username }}</div>
|
||||
{% endif %}
|
||||
<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>
|
||||
{% if auth %}
|
||||
<form method="post" action="/logout" class="form-logout">
|
||||
<input type="hidden" name="csrf_token" value="{{ 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 do Fiscal</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">GTI Fiscal</span>
|
||||
</header>
|
||||
<main class="conteudo conteudo-fiscal">{% block content %}{% endblock %}</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/sidebar.js" defer></script>
|
||||
<script src="/static/tema.js" defer></script>
|
||||
<script src="/static/control-panel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="pt-BR" data-tema="escuro">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Você saiu | GTI Fiscal</title>
|
||||
<link rel="icon" type="image/png" href="/static/logo-gti-256x256.png">
|
||||
<link rel="stylesheet" href="/static/gti-theme.css">
|
||||
<link rel="stylesheet" href="/static/control-panel.css">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="cartao login-card">
|
||||
<img src="/static/logo-gti-256x256.png" alt="GTI Grupo" class="login-logo">
|
||||
<h1>Você saiu do GTI Fiscal</h1>
|
||||
<p class="login-subtitle">Sua sessão no Painel de Clientes continua ativa.</p>
|
||||
<div class="login-actions">
|
||||
{% if panel_home %}<a class="botao" href="{{ panel_home }}">Voltar ao Painel de Clientes</a>{% endif %}
|
||||
<a class="botao" href="/login">Entrar novamente no Fiscal</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +1,30 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Login | GTI Fiscal</title><link rel="stylesheet" href="/static/control-panel.css"></head><body class="login-page"><form class="login-card" method="post" action="/login"><p class="eyebrow">GTI FISCAL / CONTROL PANEL</p><h1>Entrar no painel</h1>{% if error %}<div class="alert error">{{ error }}</div>{% endif %}<label for="login-username">Usuário</label><input id="login-username" name="username" autocomplete="username" required><label for="login-password">Senha</label><input id="login-password" name="password" type="password" autocomplete="current-password" required><button class="primary" type="submit">Acessar</button></form></body></html>
|
||||
<!doctype html>
|
||||
<html lang="pt-BR" data-tema="escuro">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{% if emergency %}Acesso de emergência{% else %}Entrar{% endif %} | GTI Fiscal</title>
|
||||
<link rel="icon" type="image/png" href="/static/logo-gti-256x256.png">
|
||||
<link rel="stylesheet" href="/static/gti-theme.css">
|
||||
<link rel="stylesheet" href="/static/control-panel.css">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="cartao login-card">
|
||||
<img src="/static/logo-gti-256x256.png" alt="GTI Grupo" class="login-logo">
|
||||
<h1>GTI Fiscal</h1>
|
||||
{% if emergency %}
|
||||
<p class="login-subtitle">Acesso de emergência (conta local). Colaboradores entram pelo Painel de Clientes.</p>
|
||||
{% else %}
|
||||
<p class="login-subtitle">Entre com suas credenciais para acessar o sistema</p>
|
||||
{% endif %}
|
||||
{% if error %}<div class="mensagem danger">{{ error }}</div>{% endif %}
|
||||
<form method="post" action="/login">
|
||||
<label for="login-username">Usuário</label>
|
||||
<input id="login-username" name="username" autocomplete="username" required autofocus>
|
||||
<label for="login-password">Senha</label>
|
||||
<input id="login-password" name="password" type="password" autocomplete="current-password" required>
|
||||
<button type="submit">Entrar</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="pt-BR" data-tema="escuro">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Não foi possível entrar | GTI Fiscal</title>
|
||||
<link rel="icon" type="image/png" href="/static/logo-gti-256x256.png">
|
||||
<link rel="stylesheet" href="/static/gti-theme.css">
|
||||
<link rel="stylesheet" href="/static/control-panel.css">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="cartao login-card">
|
||||
<img src="/static/logo-gti-256x256.png" alt="GTI Grupo" class="login-logo">
|
||||
<h1>Não foi possível entrar no GTI Fiscal</h1>
|
||||
{% set messages = {
|
||||
"EXPIRED": "O acesso demorou demais para ser concluído.",
|
||||
"STATE_MISMATCH": "O acesso foi iniciado em outra aba ou já foi usado.",
|
||||
"REPLAY": "Este acesso já foi utilizado.",
|
||||
"DISABLED_IN_FISCAL": "Seu usuário está desativado no GTI Fiscal.",
|
||||
"SSO_DISABLED": "O acesso pelo Painel de Clientes não está configurado."
|
||||
} %}
|
||||
<p class="login-subtitle">{{ messages.get(reason, "A identificação enviada pelo Painel de Clientes não foi aceita.") }}</p>
|
||||
<div class="login-actions">
|
||||
<a class="botao" href="/login">Tentar novamente</a>
|
||||
{% if panel_home %}<a class="botao" href="{{ panel_home }}">Voltar ao Painel de Clientes</a>{% endif %}
|
||||
</div>
|
||||
<p class="muted" style="margin-top:16px;font-size:.78rem">Código: {{ reason }}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user