ligbox-ops-platform/projects/ops-desk/frontend/assets/auth.js
Ligbox Spec Hub f344931925 feat(desk): Spec 043 Fase 1 — activation preview API e UI
Adiciona mapper de campos, endpoint GET activation-preview, modal
FOSS/OpenPanel/Odoo e canManageBilling para staff activar conta.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 15:06:31 +00:00

262 lines
7.5 KiB
JavaScript

const AUTH_TOKEN_KEY = 'ligbox_ops_token';
const AUTH_USER_KEY = 'ligbox_ops_user';
const SSO_COOKIE = 'ligbox_sso_token';
const SSO_MAX_AGE = 8 * 3600;
function readCookie(name) {
const re = new RegExp(`(?:^|; )${name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}=([^;]*)`);
const m = document.cookie.match(re);
return m ? decodeURIComponent(m[1]) : null;
}
function writeSsoCookie(token, maxAgeSec = SSO_MAX_AGE) {
if (!token) return;
document.cookie = `${SSO_COOKIE}=${encodeURIComponent(token)}; domain=.ligbox.com.br; path=/; max-age=${maxAgeSec}; secure; samesite=lax`;
}
function clearSsoCookie() {
document.cookie = `${SSO_COOKIE}=; domain=.ligbox.com.br; path=/; max-age=0; secure; samesite=lax`;
}
function getToken() {
return sessionStorage.getItem(AUTH_TOKEN_KEY);
}
function getUser() {
try {
return JSON.parse(sessionStorage.getItem(AUTH_USER_KEY) || 'null');
} catch {
return null;
}
}
function setSession(token, user, opts = {}) {
sessionStorage.setItem(AUTH_TOKEN_KEY, token);
sessionStorage.setItem(AUTH_USER_KEY, JSON.stringify(user));
writeSsoCookie(token, opts.maxAgeSec ?? SSO_MAX_AGE);
}
function clearSession() {
sessionStorage.removeItem(AUTH_TOKEN_KEY);
sessionStorage.removeItem(AUTH_USER_KEY);
clearSsoCookie();
}
function isLoggedIn() {
return Boolean(getToken());
}
function authHeaders(extra = {}) {
const token = getToken();
const headers = { ...extra };
if (token) headers.Authorization = `Bearer ${token}`;
return headers;
}
const FETCH_TIMEOUT_MS = 12000;
function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT_MS) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timer));
}
function requireAuth() {
if (!isLoggedIn()) {
window.location.href = '/login.html';
return false;
}
return true;
}
/** Valida JWT no servidor; limpa sessão se expirado/inválido (ex.: após rotação JWT). */
async function ensureValidSession() {
const token = getToken();
if (!token) return false;
try {
const res = await fetchWithTimeout('/api/v1/auth/me', { headers: authHeaders() });
if (!res.ok) {
clearSession();
return false;
}
const me = await res.json();
setSession(token, {
username: me.username,
role: me.role,
display_name: me.display_name,
});
return true;
} catch (err) {
// API reiniciando / rede — não expulsar sessão (evita flash login em Serviços IaaS)
console.warn('ensureValidSession (rede, sessão mantida):', err?.name || err);
return true;
}
}
function logout() {
clearSession();
window.location.replace('/login.html?logout=1');
}
function hasRole(...roles) {
const user = getUser();
return user && roles.includes(user.role);
}
function canPatchTickets() {
return hasRole('super_admin', 'ops_lead', 'technician');
}
function canRunAudit() {
return hasRole('super_admin', 'ops_lead');
}
function canManageUsers() {
return hasRole('super_admin');
}
function canManageVm112Domains() {
return hasRole('super_admin', 'ops_lead');
}
function canManageBilling() {
return hasRole('super_admin', 'ops_lead', 'finance', 'sales_admin');
}
function canAssist() {
return hasRole('super_admin', 'ops_lead', 'technician');
}
function canReadLeads() {
return hasRole('super_admin', 'ops_lead', 'technician');
}
function canReadTickets() {
return hasRole('super_admin', 'ops_lead', 'technician', 'noc');
}
async function login(username, password) {
const res = await fetchWithTimeout('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.detail || `Login falhou (${res.status})`);
}
if (data.mfa_required) {
return { mfaRequired: true, mfaToken: data.mfa_token, username: data.username };
}
setSession(
data.access_token,
{
username: data.username,
role: data.role,
display_name: data.display_name,
},
{ maxAgeSec: data.expires_in || SSO_MAX_AGE },
);
return data;
}
async function loginMfa(mfaToken, totpCode, backupCode) {
const payload = { mfa_token: mfaToken };
if (backupCode) payload.backup_code = backupCode;
else payload.totp_code = totpCode;
const res = await fetchWithTimeout('/api/v1/auth/login/mfa', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.detail || `Código 2FA inválido (${res.status})`);
}
setSession(
data.access_token,
{
username: data.username,
role: data.role,
display_name: data.display_name,
},
{ maxAgeSec: data.expires_in || SSO_MAX_AGE },
);
return data;
}
async function mfaRecoverySendEmail(mfaToken) {
const res = await fetchWithTimeout('/api/v1/auth/mfa-recovery/send-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mfa_token: mfaToken }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || `Falha ao enviar (${res.status})`);
return data;
}
async function mfaRecoveryVerifyEmail(mfaToken, emailOtp) {
const res = await fetchWithTimeout('/api/v1/auth/mfa-recovery/verify-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mfa_token: mfaToken, email_otp: emailOtp }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || `Verificação falhou (${res.status})`);
return data;
}
async function mfaRecoveryComplete(recoveryToken, totpCode) {
const res = await fetchWithTimeout('/api/v1/auth/mfa-recovery/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ recovery_token: recoveryToken, totp_code: totpCode }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || `Recuperação falhou (${res.status})`);
if (data.access_token) {
setSession(
data.access_token,
{
username: data.username,
role: data.role,
display_name: data.display_name,
},
{ maxAgeSec: data.expires_in || SSO_MAX_AGE },
);
}
return data;
}
function canReadDnsViewer() {
return hasRole('super_admin', 'ops_lead', 'technician', 'noc', 'seo', 'devops', 'developer');
}
/** Desk → Console (handoff + cookie SSO .ligbox.com.br). */
async function openConsolePath(path, { domain } = {}) {
const url = new URL(path, 'https://console.ligbox.com.br');
const dom = (domain || '').trim().toLowerCase();
if (dom) url.searchParams.set('domain', dom);
if (!isLoggedIn()) {
window.open(url.toString(), '_blank', 'noopener');
return;
}
try {
const res = await fetchWithTimeout('/api/v1/auth/console-handoff', {
method: 'POST',
headers: authHeaders(),
});
const data = await res.json().catch(() => ({}));
if (res.ok && data.handoff_token) {
url.searchParams.set('desk_handoff', data.handoff_token);
}
} catch (err) {
console.warn('openConsolePath handoff:', err?.name || err);
}
window.open(url.toString(), '_blank', 'noopener');
}
async function openConsoleDnsViewer(domain) {
return openConsolePath('/admin/dominio', { domain });
}