Innebygd innlogging (AUTH_MODE=app) med neon-login og proxy-bypass
Valgfri app-auth ved siden av proxy-modus (default uendret). Signert sesjons-cookie (scrypt-passord, HMAC, ingen avhengigheter), gating av API + WebSocket, og en selvstendig neon-login (public/login.html) for uautentiserte. Trusted bypass: en proxy-identitet (X-Remote-User / Basic Auth) regnes som innlogget uten app-passord (TRUST_PROXY_HEADER, default på), så man kan bootstrappe: logg inn via NPM Basic Auth → sett app-passord i konto-modalen → skru av Basic Auth. Konto-UI for sett/endre passord + logg ut. AUTH_MODE leses per createServer-instans (testbart). 34 integrasjonstester (8 nye for app-auth), alle grønne. README/CLAUDE.md/setup-skript oppdatert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7287f33e47
commit
f6268440d9
9 changed files with 711 additions and 42 deletions
|
|
@ -48,6 +48,7 @@ function catName(id) { return (catById(id) || {}).name || id; }
|
|||
const state = {
|
||||
user: 'anon',
|
||||
canSetName: false,
|
||||
auth: { mode: 'proxy', via: null, hasPassword: false },
|
||||
tasks: [],
|
||||
activity: [],
|
||||
knownUsers: [],
|
||||
|
|
@ -111,6 +112,8 @@ async function api(method, path, body = null) {
|
|||
}
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
// Sesjon utløpt / ikke innlogget i app-modus → til login-siden.
|
||||
if (res.status === 401 && data && data.login) { location.href = '/login'; }
|
||||
const err = new Error(data.error || `HTTP ${res.status}`);
|
||||
err.status = res.status;
|
||||
err.data = data;
|
||||
|
|
@ -301,6 +304,7 @@ async function refreshState() {
|
|||
const data = await api('GET', '/api/state');
|
||||
state.user = data.user;
|
||||
state.canSetName = !!data.canSetName;
|
||||
if (data.auth) state.auth = data.auth;
|
||||
state.tasks = data.tasks || [];
|
||||
state.activity = data.activity || [];
|
||||
state.knownUsers = data.known_users || [];
|
||||
|
|
@ -439,7 +443,9 @@ function renderAll() {
|
|||
|
||||
function renderHeader() {
|
||||
$('user-chip').textContent = state.user;
|
||||
$('user-chip').className = 'user-chip' + (state.canSetName ? ' clickable' : '');
|
||||
const chipClickable = state.canSetName || state.auth.mode === 'app';
|
||||
$('user-chip').className = 'user-chip' + (chipClickable ? ' clickable' : '');
|
||||
$('user-chip').title = state.auth.mode === 'app' ? 'Konto: passord / logg ut' : 'Pålogget bruker';
|
||||
$('anon-banner').className = 'anon-banner' + (state.user === 'anon' && state.canSetName ? ' show' : '');
|
||||
const others = state.online.filter(u => u !== state.user);
|
||||
$('online-row').innerHTML = others.length
|
||||
|
|
@ -1388,6 +1394,52 @@ async function setName() {
|
|||
} catch (e) { toast(e.message, { kind: 'err' }); }
|
||||
}
|
||||
|
||||
// ---------- Konto (app-innlogging) ----------
|
||||
function openAccount() {
|
||||
const viaProxy = state.auth.via === 'proxy';
|
||||
const has = state.auth.hasPassword;
|
||||
const status = $('account-status');
|
||||
if (viaProxy && !has) {
|
||||
status.textContent = `Du er innlogget via proxy (${state.user}). Sett et app-passord her, så kan du logge inn uten proxy og skru av Basic Auth.`;
|
||||
} else if (viaProxy && has) {
|
||||
status.textContent = `Innlogget via proxy som ${state.user}. Du har allerede et app-passord — du kan endre det her.`;
|
||||
} else {
|
||||
status.textContent = `Innlogget som ${state.user} (app-sesjon).`;
|
||||
}
|
||||
status.className = 'modal-warning show';
|
||||
// Nåværende passord kreves kun når du endrer via en app-sesjon
|
||||
$('account-current-field').style.display = (has && state.auth.via === 'session') ? '' : 'none';
|
||||
$('account-pw-label').textContent = has ? 'Nytt passord' : 'Velg passord';
|
||||
$('account-current').value = '';
|
||||
$('account-newpw').value = '';
|
||||
$('account-newpw2').value = '';
|
||||
$('account-modal').classList.add('open');
|
||||
setTimeout(() => $('account-newpw').focus(), 50);
|
||||
}
|
||||
function closeAccount() { $('account-modal').classList.remove('open'); }
|
||||
|
||||
async function saveAccountPassword() {
|
||||
const pw = $('account-newpw').value;
|
||||
const pw2 = $('account-newpw2').value;
|
||||
if (pw.length < 6) { toast('Passord må være minst 6 tegn', { kind: 'warn' }); return; }
|
||||
if (pw !== pw2) { toast('Passordene er ikke like', { kind: 'warn' }); return; }
|
||||
const body = { password: pw };
|
||||
if ($('account-current-field').style.display !== 'none') body.current = $('account-current').value;
|
||||
try {
|
||||
await api('POST', '/api/auth/password', body);
|
||||
state.auth.hasPassword = true;
|
||||
state.auth.via = 'session';
|
||||
closeAccount();
|
||||
renderHeader();
|
||||
toast('Passord lagret — du er nå innlogget med app-sesjon');
|
||||
} catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try { await api('POST', '/api/auth/logout'); } catch { /* uansett til login */ }
|
||||
location.href = '/login';
|
||||
}
|
||||
|
||||
// ---------- Import / eksport / seed ----------
|
||||
function exportJSON() {
|
||||
const blob = new Blob([JSON.stringify(state.tasks, null, 2)], { type: 'application/json' });
|
||||
|
|
@ -1488,7 +1540,18 @@ function bindUI() {
|
|||
});
|
||||
$('seedBtn').addEventListener('click', seedSamples);
|
||||
$('setNameBtn').addEventListener('click', setName);
|
||||
$('user-chip').addEventListener('click', () => { if (state.canSetName) setName(); });
|
||||
$('user-chip').addEventListener('click', () => {
|
||||
if (state.auth.mode === 'app') openAccount();
|
||||
else if (state.canSetName) setName();
|
||||
});
|
||||
|
||||
// Konto-modal
|
||||
$('account-close').addEventListener('click', closeAccount);
|
||||
$('account-cancel').addEventListener('click', closeAccount);
|
||||
$('account-save').addEventListener('click', saveAccountPassword);
|
||||
$('account-logout').addEventListener('click', logout);
|
||||
$('account-modal').addEventListener('click', e => { if (e.target.id === 'account-modal') closeAccount(); });
|
||||
$('account-newpw2').addEventListener('keydown', e => { if (e.key === 'Enter') saveAccountPassword(); });
|
||||
|
||||
// Sortering (tabell)
|
||||
document.querySelectorAll('thead th[data-sort]').forEach(th => {
|
||||
|
|
@ -1510,6 +1573,7 @@ function bindUI() {
|
|||
const typing = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||
if (e.key === 'Escape') {
|
||||
if ($('status-modal').classList.contains('open')) { resolveStatus(null); return; }
|
||||
if ($('account-modal').classList.contains('open')) { closeAccount(); return; }
|
||||
closeModal(); $('drawer').classList.remove('open'); return;
|
||||
}
|
||||
if (typing) return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue