ligbox-ops-platform/projects/ops-desk/assets/auth.js
Ligbox Spec Hub 821675ab4a Reorganize monorepo into projects/wizard, ops-desk, finance
Specs stay at repo root (cross-VM). Move deploy and code into logical
projects with README per domain, updated manifest.yaml, and symlinks at
legacy paths for VM122 backward compatibility.
2026-06-19 18:55:03 +00:00

211 lines
5.6 KiB
JavaScript

const AUTH_TOKEN_KEY = 'ligbox_ops_token';
const AUTH_USER_KEY = 'ligbox_ops_user';
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) {
sessionStorage.setItem(AUTH_TOKEN_KEY, token);
sessionStorage.setItem(AUTH_USER_KEY, JSON.stringify(user));
}
function clearSession() {
sessionStorage.removeItem(AUTH_TOKEN_KEY);
sessionStorage.removeItem(AUTH_USER_KEY);
}
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) {
console.warn('ensureValidSession:', err?.name || err);
clearSession();
return false;
}
}
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 canAssist() {
return hasRole('super_admin', 'ops_lead', 'technician');
}
function canReadLeads() {
return hasRole(
'super_admin',
'ops_lead',
'technician',
'sales_admin',
'sales_support',
'marketing',
'seo',
);
}
function canReadBilling() {
return hasRole('super_admin', 'ops_lead', 'noc', 'finance', 'sales_admin', 'sales_support');
}
function canManageBilling() {
return hasRole('super_admin', 'ops_lead', 'finance', 'sales_admin');
}
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,
});
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,
});
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,
});
}
return data;
}