Frontend refactored to ES6 modules (no bundler): js/main.js — entry point, wires all modules js/auth.js — OIDC login, token management js/ws.js — /ws, /ws/test, /ws/trace connections + HUD handler js/chat.js — messages, send, streaming js/graph.js — Cytoscape visualization + animation js/trace.js — trace panel js/dashboard.js — workspace controls rendering js/awareness.js — state panel, sensors, meters js/tests.js — test status display js/util.js — shared utilities New 2-row layout: Top: test status | connection status Middle: Workspace | Node Details | Graph Bottom: Chat | Awareness | Trace PA routing: routes ALL tool requests to expert (DB, UI, buttons, machines) Dashboard integration test: 15/15 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
89 lines
2.7 KiB
JavaScript
89 lines
2.7 KiB
JavaScript
/** Authentication: OIDC login, token management. */
|
|
|
|
export let authToken = localStorage.getItem('cog_token');
|
|
export let authConfig = null;
|
|
let _authFailed = false;
|
|
|
|
export function isAuthFailed() { return _authFailed; }
|
|
export function setAuthFailed(v) { _authFailed = v; }
|
|
|
|
export async function initAuth(onReady) {
|
|
try {
|
|
const r = await fetch('/auth/config');
|
|
authConfig = await r.json();
|
|
} catch (e) {
|
|
authConfig = { enabled: false };
|
|
}
|
|
|
|
if (!authConfig.enabled) { onReady(); return; }
|
|
|
|
// Check for OIDC callback
|
|
const params = new URLSearchParams(location.search);
|
|
if (params.has('code')) {
|
|
// Exchange code for token (PKCE)
|
|
const code = params.get('code');
|
|
const verifier = sessionStorage.getItem('cog_pkce_verifier');
|
|
try {
|
|
const tokenResp = await fetch(authConfig.issuer + '/oauth/v2/token', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
grant_type: 'authorization_code',
|
|
code,
|
|
redirect_uri: location.origin + '/callback',
|
|
client_id: authConfig.clientId,
|
|
code_verifier: verifier || '',
|
|
}),
|
|
});
|
|
const tokenData = await tokenResp.json();
|
|
if (tokenData.id_token) {
|
|
authToken = tokenData.id_token;
|
|
localStorage.setItem('cog_token', authToken);
|
|
if (tokenData.access_token) {
|
|
localStorage.setItem('cog_access_token', tokenData.access_token);
|
|
}
|
|
}
|
|
} catch (e) { console.error('token exchange failed', e); }
|
|
history.replaceState(null, '', '/');
|
|
}
|
|
|
|
if (authToken) {
|
|
onReady();
|
|
} else {
|
|
showLogin();
|
|
}
|
|
}
|
|
|
|
export function showLogin() {
|
|
const el = document.getElementById('login-overlay');
|
|
if (el) el.style.display = 'flex';
|
|
}
|
|
|
|
export async function startLogin() {
|
|
if (!authConfig) return;
|
|
const verifier = randomString(64);
|
|
sessionStorage.setItem('cog_pkce_verifier', verifier);
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(verifier);
|
|
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
|
|
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
|
|
const params = new URLSearchParams({
|
|
response_type: 'code',
|
|
client_id: authConfig.clientId,
|
|
redirect_uri: location.origin + '/callback',
|
|
scope: 'openid profile email',
|
|
code_challenge: challenge,
|
|
code_challenge_method: 'S256',
|
|
prompt: 'login',
|
|
});
|
|
location.href = authConfig.issuer + '/oauth/v2/authorize?' + params;
|
|
}
|
|
|
|
function randomString(len) {
|
|
const arr = new Uint8Array(len);
|
|
crypto.getRandomValues(arr);
|
|
return Array.from(arr, b => b.toString(36).slice(-1)).join('').slice(0, len);
|
|
}
|