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

@ -1,9 +1,69 @@
#!/usr/bin/env node
/** Direct customer web access. Not Zapier. NATS only after authz. */
/** Direct customer web access. Not Zapier. Serves the portal; APIs after authz. */
import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { PLANE, statement, reload } from './gate.js';
const PORT = Number(process.env.PORT || 3021);
const EDGE = (process.env.ZAPPIER_EDGE_URL || 'http://127.0.0.1:3000').replace(/\/$/, '');
const HERE = path.dirname(fileURLToPath(import.meta.url));
const PORTAL = process.env.PORTAL_STATIC || path.join(HERE, '..', '..', 'zappier', 'portal');
function mime(p) {
if (p.endsWith('.js')) return 'application/javascript; charset=utf-8';
if (p.endsWith('.css')) return 'text/css; charset=utf-8';
if (p.endsWith('.html')) return 'text/html; charset=utf-8';
if (p.endsWith('.svg')) return 'image/svg+xml';
if (p.endsWith('.json')) return 'application/json';
return 'application/octet-stream';
}
async function proxyPortalApi(req, res, url) {
const dest = `${EDGE}${url.pathname}${url.search}`;
const chunks = [];
for await (const c of req) chunks.push(c);
const r = await fetch(dest, {
method: req.method,
headers: {
'content-type': req.headers['content-type'] || 'application/json',
authorization: req.headers.authorization || '',
cookie: req.headers.cookie || '',
},
body: req.method === 'GET' || req.method === 'HEAD' ? undefined : Buffer.concat(chunks),
});
const buf = Buffer.from(await r.arrayBuffer());
const headers = { 'content-type': r.headers.get('content-type') || 'application/json' };
const setc = r.headers.get('set-cookie');
if (setc) headers['set-cookie'] = setc;
res.writeHead(r.status, headers);
res.end(buf);
}
function servePortal(req, res, url) {
let rel = url.pathname.replace(/^\/portal\/?/, '') || 'index.html';
if (rel.endsWith('/')) rel += 'index.html';
const file = path.normalize(path.join(PORTAL, rel));
if (!file.startsWith(path.normalize(PORTAL))) {
res.writeHead(403);
res.end('forbidden');
return;
}
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
const index = path.join(PORTAL, 'index.html');
if (fs.existsSync(index) && !path.extname(rel)) {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end(fs.readFileSync(index));
return;
}
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'not found' }));
return;
}
res.writeHead(200, { 'content-type': mime(file) });
res.end(fs.readFileSync(file));
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url || '/', `http://127.0.0.1:${PORT}`);
@ -13,7 +73,17 @@ const server = http.createServer(async (req, res) => {
};
try {
if (req.method === 'GET' && url.pathname === '/health') {
return json(200, { ok: true, role: 'verae-access-web', plane: PLANE });
return json(200, { ok: true, role: 'verae-access-web', plane: PLANE, portal: fs.existsSync(PORTAL) });
}
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/portal')) {
res.writeHead(302, { location: '/portal/' });
return res.end();
}
if (url.pathname.startsWith('/portal/api')) {
return proxyPortalApi(req, res, url);
}
if (url.pathname.startsWith('/portal')) {
return servePortal(req, res, url);
}
const st = url.pathname.match(/^\/statement\/([^/]+)$/);
if (req.method === 'GET' && st) {
@ -34,5 +104,5 @@ const server = http.createServer(async (req, res) => {
});
server.listen(PORT, '0.0.0.0', () => {
process.stdout.write(`verae-access-web http://0.0.0.0:${PORT}/ plane=${PLANE}\n`);
process.stdout.write(`verae-access-web http://0.0.0.0:${PORT}/ plane=${PLANE} portal=${PORTAL}\n`);
});