2026-01-17 00:55:52 +01:00

228 lines
6.1 KiB
JavaScript

// 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,
};
})();