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:
parent
f6268440d9
commit
3f69cd4248
9 changed files with 518 additions and 36 deletions
|
|
@ -99,6 +99,13 @@ function createServer({
|
|||
return { user: null, authed: false, via: null };
|
||||
}
|
||||
|
||||
// Admin = lagret admin-rolle, ELLER proxy-bypass uten konto (bootstrap).
|
||||
function isAdminReq(req) {
|
||||
if (!req.authed) return false;
|
||||
if (auth.isAdmin(req.user)) return true;
|
||||
return req.authVia === 'proxy' && !auth.exists(req.user);
|
||||
}
|
||||
|
||||
function sessionCookie(req, token, maxAgeSec) {
|
||||
const secure = (req.headers['x-forwarded-proto'] === 'https' || req.socket.encrypted) ? '; Secure' : '';
|
||||
const age = maxAgeSec != null ? `; Max-Age=${maxAgeSec}` : '';
|
||||
|
|
@ -216,6 +223,8 @@ function createServer({
|
|||
user: req.user,
|
||||
via: req.authVia,
|
||||
hasPassword: req.authed ? auth.hasPassword(req.user) : false,
|
||||
role: req.authed ? (auth.role(req.user) || (isAdminReq(req) ? 'admin' : 'standard')) : null,
|
||||
isAdmin: isAdminReq(req),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -234,8 +243,18 @@ function createServer({
|
|||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Logg ut alle enheter: bump epoke (tilbakekaller alle tokens), behold denne.
|
||||
app.post('/api/auth/logout-all', (req, res) => {
|
||||
if (authMode !== 'app') return res.status(400).json({ error: 'App-innlogging er ikke aktivert' });
|
||||
if (!req.authed || !auth.exists(req.user)) return res.status(401).json({ error: 'Ikke innlogget' });
|
||||
auth.bumpEpoch(req.user);
|
||||
res.setHeader('Set-Cookie', sessionCookie(req, auth.makeToken(req.user), auth.ttlSeconds()));
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Sett/endre eget passord. Krever innlogging (sesjon eller proxy-bypass).
|
||||
// Har du allerede passord og er innlogget via sesjon, kreves nåværende passord.
|
||||
// Ny bruker via proxy-bypass blir admin (bootstrap av første konto).
|
||||
app.post('/api/auth/password', (req, res) => {
|
||||
if (authMode !== 'app') return res.status(400).json({ error: 'App-innlogging er ikke aktivert' });
|
||||
if (!req.authed) return res.status(401).json({ error: 'Ikke innlogget' });
|
||||
|
|
@ -245,11 +264,12 @@ function createServer({
|
|||
return res.status(403).json({ error: 'Feil nåværende passord' });
|
||||
}
|
||||
}
|
||||
try { auth.setPassword(user, req.body && req.body.password); }
|
||||
const bootstrapRole = (!auth.exists(user) && req.authVia === 'proxy') ? 'admin' : undefined;
|
||||
try { auth.setPassword(user, req.body && req.body.password, bootstrapRole); }
|
||||
catch (e) { return res.status(400).json({ error: e.message }); }
|
||||
// Logg inn via sesjon umiddelbart (så proxy-auth kan skrus av etterpå).
|
||||
res.setHeader('Set-Cookie', sessionCookie(req, auth.makeToken(user), auth.ttlSeconds()));
|
||||
res.json({ ok: true, user });
|
||||
res.json({ ok: true, user, role: auth.role(user) });
|
||||
});
|
||||
|
||||
// ----- Guard: alt under her krever innlogging (kun i app-modus) -----
|
||||
|
|
@ -270,7 +290,10 @@ function createServer({
|
|||
res.json({
|
||||
user: req.user,
|
||||
canSetName: COOKIE_AUTH_ENABLED && authMode !== 'app',
|
||||
auth: { mode: authMode, via: req.authVia, hasPassword: auth.hasPassword(req.user) },
|
||||
auth: {
|
||||
mode: authMode, via: req.authVia, hasPassword: auth.hasPassword(req.user),
|
||||
role: auth.role(req.user) || (isAdminReq(req) ? 'admin' : 'standard'), isAdmin: isAdminReq(req),
|
||||
},
|
||||
tasks: store.list(),
|
||||
activity: store.activity,
|
||||
known_users: [...store.knownUsers].sort(),
|
||||
|
|
@ -281,6 +304,49 @@ function createServer({
|
|||
});
|
||||
});
|
||||
|
||||
// ----- Kontoadministrasjon (kun admin, kun app-modus) -----
|
||||
function requireAdmin(req, res, next) {
|
||||
if (authMode !== 'app') return res.status(400).json({ error: 'App-innlogging er ikke aktivert' });
|
||||
if (!isAdminReq(req)) return res.status(403).json({ error: 'Krever administrator' });
|
||||
next();
|
||||
}
|
||||
|
||||
app.get('/api/auth/users', requireAdmin, (req, res) => res.json({ users: auth.listUsers() }));
|
||||
|
||||
app.post('/api/auth/users', requireAdmin, (req, res) => {
|
||||
const user = sanitizeUser(req.body && req.body.user);
|
||||
if (!user) return res.status(400).json({ error: 'Ugyldig brukernavn (a-z, 0-9, . _ @ -)' });
|
||||
const role = req.body && req.body.role === 'admin' ? 'admin' : 'standard';
|
||||
try { auth.createUser(user, req.body && req.body.password, role); }
|
||||
catch (e) { return res.status(400).json({ error: e.message }); }
|
||||
store.rememberUser(user);
|
||||
res.status(201).json({ user, role });
|
||||
});
|
||||
|
||||
app.post('/api/auth/users/:user/password', requireAdmin, (req, res) => {
|
||||
const user = sanitizeUser(req.params.user);
|
||||
if (!auth.exists(user)) return res.status(404).json({ error: 'Ukjent bruker' });
|
||||
// Admin-reset bumper epoke implisitt (setPassword) → logger ut den brukeren.
|
||||
try { auth.setPassword(user, req.body && req.body.password); }
|
||||
catch (e) { return res.status(400).json({ error: e.message }); }
|
||||
res.json({ ok: true, user });
|
||||
});
|
||||
|
||||
app.put('/api/auth/users/:user/role', requireAdmin, (req, res) => {
|
||||
const user = sanitizeUser(req.params.user);
|
||||
try { auth.setRole(user, req.body && req.body.role); }
|
||||
catch (e) { return res.status(400).json({ error: e.message }); }
|
||||
res.json({ ok: true, user, role: auth.role(user) });
|
||||
});
|
||||
|
||||
app.delete('/api/auth/users/:user', requireAdmin, (req, res) => {
|
||||
const user = sanitizeUser(req.params.user);
|
||||
if (user === req.user) return res.status(400).json({ error: 'Du kan ikke slette din egen konto' });
|
||||
try { auth.deleteUser(user); }
|
||||
catch (e) { return res.status(400).json({ error: e.message }); }
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.get('/api/plan', (req, res) => {
|
||||
const date = validDate(req.query.date) ? req.query.date : todayStr();
|
||||
const owner = typeof req.query.user === 'string' && req.query.user ? sanitizeUser(req.query.user) : req.user;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue