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
Jeffrey Smith c470037fb5
Some checks failed
CI/CD / detect-changes (push) Successful in 23s
CI/CD / test-sqlite (push) Failing after 20s
CI/CD / test-frontend (push) Failing after 21s
CI/CD / test-go-pg (push) Failing after 38s
CI/CD / build-and-deploy (push) Has been skipped
rebrand + purge orphaned frontend: Chat Switchboard → Switchboard Core
Branding: bulk rename across 50+ files (Go, JS, CSS, HTML, YAML, JSON,
shell scripts). Fix Helm chart description and git repo URLs.
Fix test helper taglines.

Admin surface: strip 14 gutted tabs (providers, models, personas,
roles, knowledge, memory, tasks, health, routing, capabilities,
channels, dashboard, usage, stats). Collapse to 4 categories:
People, Workflows, System, Monitoring.

Settings surface: strip 11 gutted tabs (models, personas, providers,
roles, knowledge, memory, usage, workflows, tasks, data, gitkeys).
Keep: general, appearance, profile, teams, connections, notifications.

Team-admin surface: strip 5 gutted tabs (personas, providers,
knowledge, tasks, usage). Keep: members, groups, connections,
workflows, settings, activity.

Components: delete chat-pane/ (13 files, 1938 lines) and
notes-pane/ (10 files, 2381 lines).

main.go: remove stale Health.Prune maintenance goroutine.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:28:09 +00:00

266 lines
9.4 KiB
JavaScript

// ==========================================
// Switchboard Core — 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 _booting = false;
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);
sessionStorage.removeItem('sw-chat-active');
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) {
// No tokens in localStorage — clear any stale sb_token cookie
// so Go SSR middleware doesn't trust a leftover cookie.
document.cookie = 'sb_token=; path=/; max-age=0';
return;
}
// Unknown token age — schedule refresh soon
_scheduleRefresh(60);
// Suppress _on401Failure redirect during boot — boot handles 401 itself.
_booting = true;
// 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);
}
} finally {
_booting = false;
}
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();
// During boot(), auth.boot() handles 401 gracefully — don't redirect.
if (_booting) return;
// Already on login page — don't redirect (prevents blank-page reload loop
// when stale localStorage tokens trigger 401 during SDK boot on /login).
const base = window.__BASE__ || '';
const loginPath = base + '/login';
const here = window.location.pathname.replace(/\/+$/, '');
if (here === loginPath) return;
window.location.href = loginPath;
},
_setAuth(data) { _setAuth(data); },
_setRestClient(client) { _restClient = client; },
_setEmit(fn) { _emit = fn; },
};
return auth;
}