/** * Auth helpers for the Print-Agent protocol (server/api/print-agent/*) and the * cookie-session admin routes (server/api/settings/print-agents/*). * * Agents authenticate with `Authorization: Bearer lspa_…` (token issued once at enrolment, * only its SHA-256 is stored). The browser side uses the normal LogShip session cookies. */ import getTokenHelper from './getTokenHelper' import { getAgentByToken, type PrintAgentRow } from './printSpoolDb' export const requireAgent = (event: any): PrintAgentRow => { const header = String(getHeader(event, 'authorization') || '') const token = header.startsWith('Bearer ') ? header.slice(7).trim() : '' const agent = token ? getAgentByToken(token) : null if (!agent) throw createError({ statusCode: 401, statusMessage: 'invalid or revoked agent token' }) return agent } export interface AdminSession { userId: number | null; roleId: number | null; clientId: number; orgId: number } /** Browser session (cookies + JWT). Throws 401 without a usable token. */ export const requireAdminSession = async (event: any): Promise => { let token: string = '' try { token = String((await getTokenHelper(event)) || '') } catch { /* handled below */ } if (!token) throw createError({ statusCode: 401, statusMessage: 'not authenticated' }) let payload: any = {} try { payload = JSON.parse(Buffer.from(token.split('.')[1] || '', 'base64').toString()) } catch { /* ignore */ } // Cookies may carry the id as "1000000", a JSON object/string or be absent (role selected in the // browser vs. auto-finalised login) — take the first number we can find, then the JWT claims // (AD_Client_ID, or the `Clients` list on a pre-role token). Never block the page on it. const firstInt = (v: any): number => { const m = /\d+/.exec(String(v ?? '')) return m ? Number(m[0]) : 0 } const clientId = firstInt(getCookie(event, 'logship_client_id')) || firstInt(payload?.AD_Client_ID) || firstInt(payload?.Clients) || 0 return { userId: firstInt(getCookie(event, 'logship_user_id')) || firstInt(payload?.AD_User_ID) || null, roleId: firstInt(getCookie(event, 'logship_role_id')) || firstInt(payload?.AD_Role_ID) || null, clientId, orgId: firstInt(getCookie(event, 'logship_organization_id')) || firstInt(payload?.AD_Org_ID) || 0 } } // Tiny fixed-window limiter for the public enrolment endpoint (per remote IP). const buckets = new Map() export const rateLimit = (key: string, max: number, windowMs: number): boolean => { const now = Date.now() const b = buckets.get(key) if (!b || now - b.ts > windowMs) { buckets.set(key, { n: 1, ts: now }); return true } b.n++ return b.n <= max } const first = (v: any): string => String(v || '').split(',')[0]?.trim() || '' export const clientIp = (event: any): string => { const xf = first(getHeader(event, 'x-forwarded-for')) return xf || event.node?.req?.socket?.remoteAddress || 'unknown' } /** Public origin of this instance as seen by the agent/browser (for enrolment leaflets). */ export const instanceOrigin = (event: any): string => { const proto = first(getHeader(event, 'x-forwarded-proto')) || 'https' const host = first(getHeader(event, 'x-forwarded-host') || getHeader(event, 'host')) return host ? `${proto}://${host}` : '' }