// public/assets/auth.js /* English comments: auth token helpers and login UI */ (function () { const LS_REFRESH = 'scmedia_refresh_token'; const LS_CLIENT = 'scmedia_client_type'; const ACCESS_COOKIE = 'scmedia_access_token'; let refreshPromise = null; let refreshTimer = null; function getAccessToken() { const parts = document.cookie.split(';').map((p) => p.trim()); for (const part of parts) { if (!part) continue; const idx = part.indexOf('='); if (idx === -1) continue; const key = part.slice(0, idx); if (key === ACCESS_COOKIE) { return decodeURIComponent(part.slice(idx + 1)); } } return ''; } function getRefreshToken() { return localStorage.getItem(LS_REFRESH) || sessionStorage.getItem(LS_REFRESH) || ''; } function getClientType() { return localStorage.getItem(LS_CLIENT) || sessionStorage.getItem(LS_CLIENT) || 'web'; } function setTokens(tokens, clientType = 'web', remember = true) { const storage = remember ? localStorage : sessionStorage; if (tokens?.refresh_token) storage.setItem(LS_REFRESH, tokens.refresh_token); storage.setItem(LS_CLIENT, clientType); const other = remember ? sessionStorage : localStorage; other.removeItem(LS_REFRESH); other.removeItem(LS_CLIENT); localStorage.removeItem('scmedia_access_token'); sessionStorage.removeItem('scmedia_access_token'); } function clearTokens() { localStorage.removeItem(LS_REFRESH); localStorage.removeItem(LS_CLIENT); sessionStorage.removeItem(LS_REFRESH); sessionStorage.removeItem(LS_CLIENT); localStorage.removeItem('scmedia_access_token'); sessionStorage.removeItem('scmedia_access_token'); } function parseJwt(token) { if (!token) return null; const parts = token.split('.'); if (parts.length !== 3) return null; try { const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); return payload || null; } catch (e) { return null; } } function getRoles() { const payload = parseJwt(getAccessToken()); return Array.isArray(payload?.roles) ? payload.roles : []; } function isAccessExpired(leewaySeconds = 30) { const payload = parseJwt(getAccessToken()); const exp = Number(payload?.exp || 0); if (!exp) return true; const now = Math.floor(Date.now() / 1000); return exp <= (now + leewaySeconds); } function isAdmin() { return getRoles().includes('admin'); } function redirectToLogin() { if (window.location.pathname !== '/login') { window.location.href = '/login'; } } function requireAuth() { const access = getAccessToken(); const refresh = getRefreshToken(); if (!access && !refresh) { redirectToLogin(); return; } if (!access && refresh) { refreshTokens().then((ok) => { if (!ok) redirectToLogin(); }); } } async function refreshTokens() { if (refreshPromise) return refreshPromise; const refresh = getRefreshToken(); if (!refresh) return false; const clientType = getClientType(); refreshPromise = (async () => { const tryRefresh = async (token) => { const res = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: token, client_type: clientType }), }); if (!res.ok) return null; const data = await res.json(); if (!data?.ok) return null; return data?.data || {}; }; let data = await tryRefresh(refresh); if (!data) { const current = getRefreshToken(); if (current && current !== refresh) { data = await tryRefresh(current); } } if (!data) { clearTokens(); return false; } setTokens(data, clientType); return true; })(); try { return await refreshPromise; } finally { refreshPromise = null; } } async function logout() { const refresh = getRefreshToken(); if (window.UI?.stopSseClient) { window.UI.stopSseClient(); } clearTokens(); try { await fetch('/api/auth/logout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(refresh ? { refresh_token: refresh } : {}), }); } catch (e) { // Ignore logout network errors. } redirectToLogin(); } function initSessionRefresh() { if (refreshTimer) { clearTimeout(refreshTimer); refreshTimer = null; } const token = getAccessToken(); const payload = parseJwt(token); const exp = Number(payload?.exp || 0); if (!exp) return; const now = Math.floor(Date.now() / 1000); const lead = 45; const delayMs = Math.max(1000, (exp - now - lead) * 1000); refreshTimer = setTimeout(async () => { const ok = await refreshTokens(); if (ok) { initSessionRefresh(); return; } clearTokens(); redirectToLogin(); }, delayMs); } function initHeaderControls() { const logoutBtn = document.getElementById('btnLogout'); if (logoutBtn) { logoutBtn.addEventListener('click', () => { logout(); }); } const adminBtn = document.getElementById('btnAdmin'); if (adminBtn) { adminBtn.classList.toggle('is-hidden', !isAdmin()); } loadHeaderAvatar().catch(() => {}); } async function loadHeaderAvatar() { const img = document.getElementById('headerAvatar'); if (!img) return; const token = getAccessToken(); if (!token) return; const res = await fetch('/api/account/avatar', { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return; const blob = await res.blob(); const url = URL.createObjectURL(blob); img.onload = () => URL.revokeObjectURL(url); img.src = url; } window.Auth = { getAccessToken, getRefreshToken, setTokens, clearTokens, parseJwt, getRoles, isAdmin, isAccessExpired, requireAuth, refreshTokens, logout, redirectToLogin, initHeaderControls, initSessionRefresh, }; })();