Auth fixes, show correct cost each value on pos
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user