Auth fixes, show correct cost each value on pos

This commit is contained in:
2026-05-28 14:15:13 -04:00
parent 421b3d5922
commit 8c707e28ea
21 changed files with 564 additions and 82 deletions
+28 -1
View File
@@ -2,6 +2,20 @@ import { extractBearerToken, verifyToken, TokenError } from './verify.js';
const USER_CACHE_TTL_MS = 60_000;
// Source IPs that bypass token auth — used so the office kiosk can render
// /small without anyone having to log in daily on the device. Synthetic user
// has no permissions, so only endpoints that don't gate on requirePermission()
// are reachable. Requires server.js `trust proxy` setting so req.ip is the
// real client behind Caddy, not 127.0.0.1.
function parseKioskIps(raw) {
return new Set(
(raw || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
);
}
function createUserCache() {
const entries = new Map();
return {
@@ -47,10 +61,23 @@ async function loadUser(pool, userId) {
return user;
}
export function authenticate({ pool, secret = process.env.JWT_SECRET }) {
export function authenticate({ pool, secret = process.env.JWT_SECRET, kioskIps = process.env.KIOSK_IPS }) {
const cache = createUserCache();
const kioskIpSet = parseKioskIps(kioskIps);
return async function authenticateMiddleware(req, res, next) {
if (kioskIpSet.size > 0 && kioskIpSet.has(req.ip)) {
req.user = {
id: 'kiosk',
username: 'kiosk',
is_admin: false,
is_active: true,
permissions: [],
is_kiosk: true,
};
return next();
}
let decoded;
try {
const token = extractBearerToken(req.headers.authorization);
@@ -126,6 +126,62 @@ describe('authenticate middleware', () => {
expect(pool.calls.count).toBe(4);
vi.useRealTimers();
});
describe('KIOSK_IPS bypass', () => {
it('bypasses token check and mints a synthetic kiosk user when req.ip matches', async () => {
const pool = makeFakePool({});
const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7' });
const req = { headers: {}, ip: '203.0.113.7' };
const res = makeRes();
const next = vi.fn();
await mw(req, res, next);
expect(next).toHaveBeenCalledOnce();
expect(req.user).toEqual({
id: 'kiosk',
username: 'kiosk',
is_admin: false,
is_active: true,
permissions: [],
is_kiosk: true,
});
expect(pool.calls.count).toBe(0);
});
it('falls through to normal Bearer auth when req.ip is not in KIOSK_IPS', async () => {
const pool = makeFakePool({ 1: activeUser }, { 1: [] });
const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7' });
const req = { headers: { authorization: `Bearer ${validToken}` }, ip: '198.51.100.1' };
const res = makeRes();
const next = vi.fn();
await mw(req, res, next);
expect(next).toHaveBeenCalledOnce();
expect(req.user.id).toBe(1);
expect(req.user.is_kiosk).toBeUndefined();
});
it('does not bypass when KIOSK_IPS is empty, even if req.ip is undefined', async () => {
const pool = makeFakePool({ 1: activeUser });
const mw = authenticate({ pool, secret: SECRET, kioskIps: '' });
const req = { headers: {} };
const res = makeRes();
const next = vi.fn();
await mw(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(next).not.toHaveBeenCalled();
});
it('supports multiple comma-separated IPs', async () => {
const pool = makeFakePool({});
const mw = authenticate({ pool, secret: SECRET, kioskIps: '203.0.113.7, 203.0.113.8 ,203.0.113.9' });
const next = vi.fn();
for (const ip of ['203.0.113.7', '203.0.113.8', '203.0.113.9']) {
const req = { headers: {}, ip };
await mw(req, makeRes(), next);
expect(req.user?.is_kiosk).toBe(true);
}
expect(next).toHaveBeenCalledTimes(3);
});
});
});
describe('requirePermission middleware', () => {
@@ -20,7 +20,8 @@ export function requestLog(options = {}) {
return {
method: req.method,
url: req.url,
userId: req.raw?.user?.id,
userId: req.raw?.user?.id ?? req.user?.id,
ip: req.raw?.ip ?? req.ip,
};
},
res(res) {