This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/sdk/auth.js
gobha 74f3cb84e9 Changeset 0.37.5 (#217)
Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
2026-03-21 13:41:25 +00:00

247 lines
8.4 KiB
JavaScript

// ==========================================
// Chat Switchboard — SDK: Auth Module
// ==========================================
// Owns token storage, login/logout/refresh lifecycle,
// and the permissions/teams/policies cache.
//
// Factory: createAuth()
// Wire after creation: _setRestClient(client), _setEmit(fn)
// ==========================================
const BASE = window.__BASE__ || '';
const _storageKey = BASE ? `sb_auth_${BASE.replace(/\//g, '')}` : 'sb_auth';
/**
* Create the auth module.
* Must call _setRestClient() and _setEmit() before use.
*
* @returns {object} auth
*/
export function createAuth() {
// ── Private state ───────────────────────────
let _accessToken = null;
let _refreshToken = null;
let _user = null;
let _permissions = new Set();
let _teams = [];
let _groups = [];
let _policies = {};
let _refreshTimer = null;
let _refreshPromise = null;
let _restClient = null;
let _emit = () => {};
// ── Token persistence ───────────────────────
function _saveTokens() {
localStorage.setItem(_storageKey, JSON.stringify({
accessToken: _accessToken,
refreshToken: _refreshToken,
user: _user,
}));
// Cookie sync for Go template page auth
if (_accessToken) {
document.cookie = `sb_token=${_accessToken}; path=/; max-age=900; SameSite=Strict`;
} else {
document.cookie = 'sb_token=; path=/; max-age=0';
}
}
function _loadTokens() {
try {
const saved = JSON.parse(localStorage.getItem(_storageKey) || 'null');
if (saved) {
_accessToken = saved.accessToken || null;
_refreshToken = saved.refreshToken || null;
_user = saved.user || null;
}
} catch (_) { /* corrupt storage */ }
}
function _clearTokens() {
_accessToken = null;
_refreshToken = null;
_user = null;
_permissions = new Set();
_teams = [];
_groups = [];
_policies = {};
localStorage.removeItem(_storageKey);
document.cookie = 'sb_token=; path=/; max-age=0';
if (_refreshTimer) { clearTimeout(_refreshTimer); _refreshTimer = null; }
}
// ── Token lifecycle ─────────────────────────
function _setAuth(data) {
_accessToken = data.access_token;
_refreshToken = data.refresh_token;
_user = data.user;
_saveTokens();
_scheduleRefresh(data.expires_in || 900);
}
function _scheduleRefresh(expiresIn) {
if (_refreshTimer) clearTimeout(_refreshTimer);
// Refresh at 80% of expiry (e.g., 12min for 15min token)
const ms = Math.max((expiresIn * 0.8) * 1000, 30000);
_refreshTimer = setTimeout(async () => {
if (!_refreshToken) return;
console.debug('[sw.auth] Proactive token refresh');
try {
const data = await _restClient.post('/api/v1/auth/refresh', {
refresh_token: _refreshToken,
});
_setAuth(data);
_emit('auth.refresh', {}, { localOnly: true });
} catch (e) {
console.warn('[sw.auth] Proactive refresh failed, token still valid');
}
}, ms);
}
// ── Permissions fetch ───────────────────────
async function _fetchPermissions() {
try {
const data = await _restClient.get('/api/v1/profile/permissions');
_permissions = new Set(data.permissions || []);
_teams = Object.freeze(data.teams || []);
_groups = Object.freeze(data.groups || []);
_policies = Object.freeze(data.policies || {});
_emit('auth.permissions.changed', {}, { localOnly: true });
} catch (e) {
console.warn('[sw.auth] Failed to fetch permissions:', e.message);
throw e;
}
}
// ── Public API ──────────────────────────────
const auth = {
// ── Getters ─────────────────────────────
get isAuthenticated() { return !!_accessToken; },
get user() {
if (!_user) return null;
return Object.freeze({
id: _user.id,
username: _user.username,
display_name: _user.display_name || _user.username,
email: _user.email || null,
role: _user.role || 'user',
avatar: _user.avatar_url || _user.avatar || null,
});
},
get permissions() { return _permissions; },
get teams() { return _teams; },
get groups() { return _groups; },
get policies() { return _policies; },
// ── Lifecycle ───────────────────────────
/**
* Login with credentials. Stores tokens, fetches permissions, emits event.
*/
async login(login, password) {
const data = await _restClient.post('/api/v1/auth/login', { login, password });
_setAuth(data);
await _fetchPermissions();
_emit('auth.login', { user: auth.user }, { localOnly: true });
return auth.user;
},
/**
* Logout. Clears tokens, emits event. Server call is best-effort.
*/
async logout() {
try {
await _restClient.post('/api/v1/auth/logout', { refresh_token: _refreshToken });
} catch (_) { /* best effort */ }
_clearTokens();
_emit('auth.logout', {}, { localOnly: true });
},
/**
* Manual token refresh. Returns true on success, false on failure.
*/
async refresh() {
if (_refreshPromise) return _refreshPromise;
_refreshPromise = (async () => {
try {
const data = await _restClient.post('/api/v1/auth/refresh', {
refresh_token: _refreshToken,
});
_setAuth(data);
return true;
} catch (_) {
_clearTokens();
return false;
} finally {
_refreshPromise = null;
}
})();
return _refreshPromise;
},
/**
* Boot sequence. Called by index.js on startup.
* Loads tokens from localStorage, validates, fetches permissions.
*/
async boot() {
_loadTokens();
if (!_refreshToken) return;
// Unknown token age — schedule refresh soon
_scheduleRefresh(60);
// Fetch permissions (validates the access token)
try {
await _fetchPermissions();
} catch (e) {
if (e.status === 401) {
// Token expired — try refresh
const ok = await auth.refresh();
if (ok) {
try { await _fetchPermissions(); }
catch (_) { _clearTokens(); }
}
// If refresh failed, _clearTokens already called
} else {
// Network error or other — keep tokens, user may be offline
console.warn('[sw.auth] Boot: permissions fetch failed (keeping tokens):', e.message);
}
}
if (_accessToken) {
_emit('auth.boot', { user: auth.user }, { localOnly: true });
}
},
// ── Internal (called by rest-client / index.js) ──
_getToken() { return _accessToken; },
async _onRefresh() {
if (_refreshToken) return auth.refresh();
return false;
},
_on401Failure() {
_clearTokens();
const base = window.__BASE__ || '';
window.location.href = base + '/login';
},
_setAuth(data) { _setAuth(data); },
_setRestClient(client) { _restClient = client; },
_setEmit(fn) { _emit = fn; },
};
return auth;
}