Auth-herding: roller, kontoadmin, token-revokering + søkbar planliste + neon

Auth (app-modus):
- Roller admin/standard. Admin oppretter/sletter kontoer, resetter passord,
  endrer roller (konto-modal). Eksisterende brukere → admin ved migrering;
  bootstrap-bruker via proxy-bypass → admin. Siste admin vernet.
- Tilbakekalling via per-bruker epoke i tokenet: passordbytte, admin-reset,
  «Logg ut alle enheter» og sletting dreper alle aktive sesjoner. Token-format
  endret (3- → 4-delt) — alle logges ut én gang ved oppgradering.

Dagsplan: søkbar/velgbar planliste («📋 Planer») avledet fra planIndex —
finn plan på navn/dato/person uten å gjette dato.

GUI: login-neon trukket inn i hovedappen (header/knapper/KPI-kanter), og
login-siden fikk mer tech (HUD-hjørner, glitch-tittel, scanlines).

40 integrasjonstester (6 nye), alle grønne. README + CLAUDE.md oppdatert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jon Vanvik 2026-06-15 14:56:39 +02:00
parent f6268440d9
commit 3f69cd4248
9 changed files with 518 additions and 36 deletions

View file

@ -508,3 +508,72 @@ test('proxy-modus (default): ingen guard, alt åpent', async () => {
assert.equal(res.status, 200);
assert.equal((await res.json()).auth.mode, 'proxy');
});
// ---------- Roller, admin og tilbakekalling ----------
// jon fikk admin via bootstrap (proxy-bypass) i en tidligere test; passord = 'hemmelig123'.
test('app-auth: bootstrap-bruker ble admin', async () => {
const res = await aReq('GET', '/api/auth/users', undefined, { 'X-Remote-User': 'jon' });
assert.equal(res.status, 200);
const { users } = await res.json();
const jon = users.find(u => u.user === 'jon');
assert.equal(jon.role, 'admin');
});
test('app-auth: admin oppretter standard-bruker; standard er ikke admin', async () => {
let res = await aReq('POST', '/api/auth/users', { user: 'kari', password: 'karipass1', role: 'standard' }, { 'X-Remote-User': 'jon' });
assert.equal(res.status, 201);
// kari (via proxy-bypass, men finnes som standard) får ikke admin-endepunkt
res = await aReq('GET', '/api/auth/users', undefined, { 'X-Remote-User': 'kari' });
assert.equal(res.status, 403);
});
test('app-auth: passordbytte tilbakekaller gamle tokens', async () => {
let res = await aReq('POST', '/api/auth/login', { user: 'jon', password: 'hemmelig123' });
const oldCookie = cookieFrom(res);
assert.ok(oldCookie);
res = await aReq('POST', '/api/auth/password', { current: 'hemmelig123', password: 'jonpass2' }, { Cookie: oldCookie });
assert.equal(res.status, 200);
const newCookie = cookieFrom(res);
// Gammel cookie er nå ugyldig (epoke bumpet), ny virker
res = await aReq('GET', '/api/state', undefined, { Cookie: oldCookie });
assert.equal(res.status, 401);
res = await aReq('GET', '/api/state', undefined, { Cookie: newCookie });
assert.equal(res.status, 200);
});
test('app-auth: logg ut alle enheter dreper andre økter, beholder denne', async () => {
const a = cookieFrom(await aReq('POST', '/api/auth/login', { user: 'jon', password: 'jonpass2' }));
const b = cookieFrom(await aReq('POST', '/api/auth/login', { user: 'jon', password: 'jonpass2' }));
const res = await aReq('POST', '/api/auth/logout-all', undefined, { Cookie: a });
const aNew = cookieFrom(res);
// b (eldre epoke) er død, a fornyet virker
assert.equal((await aReq('GET', '/api/state', undefined, { Cookie: b })).status, 401);
assert.equal((await aReq('GET', '/api/state', undefined, { Cookie: aNew })).status, 200);
});
test('app-auth: kan ikke slette egen konto eller demote siste admin', async () => {
let res = await aReq('DELETE', '/api/auth/users/jon', undefined, { 'X-Remote-User': 'jon' });
assert.equal(res.status, 400);
res = await aReq('PUT', '/api/auth/users/jon/role', { role: 'standard' }, { 'X-Remote-User': 'jon' });
assert.equal(res.status, 400); // siste admin
});
test('app-auth: admin endrer rolle og resetter passord (logger ut brukeren)', async () => {
// kari logger inn
const kariCookie = cookieFrom(await aReq('POST', '/api/auth/login', { user: 'kari', password: 'karipass1' }));
assert.ok(kariCookie);
// jon (admin) resetter karis passord → karis token dør
let res = await aReq('POST', '/api/auth/users/kari/password', { password: 'nyttkari1' }, { 'X-Remote-User': 'jon' });
assert.equal(res.status, 200);
assert.equal((await aReq('GET', '/api/state', undefined, { Cookie: kariCookie })).status, 401);
// jon gjør kari til admin
res = await aReq('PUT', '/api/auth/users/kari/role', { role: 'admin' }, { 'X-Remote-User': 'jon' });
assert.equal(res.status, 200);
assert.equal((await res.json()).role, 'admin');
});