Turn lab IAM on, move portal to access-web, harden sessions and receipts.
Some checks are pending
offline / test (push) Waiting to run

Fleet and department doors now check STAFF_IAM_URL. Walkthrough: cs can
credit (agent is the IAM user) and cannot export; operator can fleet
POST; admin /me is owner. Portal is the public web door at :3021/portal/.
IAM sessions persist; login is rate-limited per user; receipt PDF is
branded. lan-134 stays disabled.
This commit is contained in:
George Lambert 2026-09-11 18:59:18 -04:00
parent d299d245e8
commit c32b65038a
20 changed files with 277 additions and 73 deletions

View file

@ -3,16 +3,17 @@ import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { allows, publicUser, ROLES, PERMISSIONS, expandRoles } from './roles.js';
import { allows, publicUser, ROLES, PERMISSIONS } from './roles.js';
import { verifyPassword } from './passwords.js';
import { loadIam, saveIam } from './store.js';
import {
issueSession,
getSession,
revokeSession,
cookieHeader,
clearCookieHeader,
tokenFromReq,
loginLocked,
loginFail,
loginOk,
clientKey,
} from './sessions.js';
const PUBLIC = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'public');
@ -41,7 +42,7 @@ function readBody(req) {
function actorOf(req) {
const tok = tokenFromReq(req);
const s = getSession(tok);
const s = iam.getSession(tok);
if (!s) return null;
const u = iam.findById(s.userId);
if (!u || !u.active) return null;
@ -87,15 +88,22 @@ const server = http.createServer(async (req, res) => {
const username = String(body.username || '').trim();
const password = String(body.password || '');
const next = body.next || '/';
if (loginLocked(req, username)) {
iam.log(username || 'unknown', 'login.lock', clientKey(req));
saveIam(iam);
return json(429, { error: 'too many login attempts; try again in 10 minutes' });
}
const user = iam.findByUsername(username);
if (!user || !user.active || !verifyPassword(password, user.passwordHash)) {
loginFail(req, username);
iam.log(username || 'unknown', 'login.fail', username);
saveIam(iam);
if ((req.headers['content-type'] || '').includes('json')) return json(401, { error: 'invalid username or password' });
res.writeHead(302, { location: '/login?error=1' });
return res.end();
}
const token = issueSession(user.id);
loginOk(req, username);
const token = iam.issueSession(user.id);
iam.log(username, 'login.ok', username);
saveIam(iam);
const loc = typeof next === 'string' && (next.startsWith('http') || next.startsWith('/')) ? next : '/';
@ -110,7 +118,8 @@ const server = http.createServer(async (req, res) => {
return res.end();
}
if (req.method === 'POST' && url.pathname === '/logout') {
revokeSession(tokenFromReq(req));
iam.revokeSession(tokenFromReq(req));
saveIam(iam);
if ((req.headers['content-type'] || '').includes('json')) {
res.writeHead(200, { 'content-type': 'application/json', 'set-cookie': clearCookieHeader() });
return res.end(JSON.stringify({ ok: true }));