From 2e1d7bf2a908b8d99455cfb2bbf66581809483d3 Mon Sep 17 00:00:00 2001 From: "Prit V. Vasani" Date: Sat, 10 Oct 2026 19:10:07 +0000 Subject: [PATCH] fix(invoices): authenticate print/PDF opened in a new tab window.open cannot carry an Authorization header, so the invoice print/PDF endpoints returned 401. Now: - server: attachTokenFromQuery middleware lifts a ?token= query param into the Bearer header (stripped before the request is processed/logged); mounted on the /invoices router before auth. - frontend: AdminInvoices appends getAuthToken() to the print/pdf URLs; getAuthToken exported from services/api. Verified: print -> 200, pdf -> 200 application/pdf with ?token=; access logs are written to server/logs/info-*.log. Lint clean; no test impact. --- .../src/pages/admin/AdminInvoices.jsx | 12 +++++++++--- cafe-management-sys/src/services/api.js | 2 +- server/middleware/auth.js | 15 +++++++++++++++ server/routes/invoices.js | 3 ++- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/cafe-management-sys/src/pages/admin/AdminInvoices.jsx b/cafe-management-sys/src/pages/admin/AdminInvoices.jsx index c1f5ccf..3916eb1 100644 --- a/cafe-management-sys/src/pages/admin/AdminInvoices.jsx +++ b/cafe-management-sys/src/pages/admin/AdminInvoices.jsx @@ -23,7 +23,7 @@ import Refresh from '@mui/icons-material/Refresh'; import ReceiptLong from '@mui/icons-material/ReceiptLong'; import Print from '@mui/icons-material/Print'; import Download from '@mui/icons-material/Download'; -import { invoicesAPI, activityAPI, API_BASE_URL, unwrap } from '../../services/api'; +import { invoicesAPI, activityAPI, API_BASE_URL, getAuthToken, unwrap } from '../../services/api'; import ErrorState from '../../components/common/ErrorState'; import useOpsEvents from '../../hooks/useOpsEvents'; import { formatMoney } from '../../utils/formatMoney'; @@ -95,12 +95,18 @@ const AdminInvoices = () => { const printInvoice = (inv, mode = paper) => { const id = inv._id || inv.id; - window.open(`${API_BASE_URL}/invoices/${id}/print?mode=${mode}`, '_blank'); + const qp = new URLSearchParams({ mode }); + const token = getAuthToken(); + if (token) qp.set('token', token); + window.open(`${API_BASE_URL}/invoices/${id}/print?${qp.toString()}`, '_blank'); }; const downloadPdf = (inv, format = 'a4') => { const id = inv._id || inv.id; - window.open(`${API_BASE_URL}/invoices/${id}/pdf?format=${format}&download=1`, '_blank'); + const qp = new URLSearchParams({ format, download: '1' }); + const token = getAuthToken(); + if (token) qp.set('token', token); + window.open(`${API_BASE_URL}/invoices/${id}/pdf?${qp.toString()}`, '_blank'); }; return ( diff --git a/cafe-management-sys/src/services/api.js b/cafe-management-sys/src/services/api.js index 938cf0d..0f64831 100644 --- a/cafe-management-sys/src/services/api.js +++ b/cafe-management-sys/src/services/api.js @@ -58,7 +58,7 @@ export const resolveApiBaseUrl = ( export const API_BASE_URL = resolveApiBaseUrl(RAW_API_URL); // Helper function to get auth token (sessionStorage preferred, localStorage fallback) -const getAuthToken = () => { +export const getAuthToken = () => { return sessionStorage.getItem('token') || localStorage.getItem('token'); }; diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 765c151..aaeb463 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -215,12 +215,27 @@ const attachUserIfPresent = (req, _res, next) => { } }; +/** + * Print/PDF links are opened in a new tab (window.open), which cannot carry an + * Authorization header. Allow a `?token=` query param as a fallback so those + * endpoints still authenticate; it is stripped before the request proceeds (and + * thus never logged). + */ +const attachTokenFromQuery = (req, res, next) => { + if (!req.headers.authorization && req.query && typeof req.query.token === 'string') { + req.headers.authorization = `Bearer ${req.query.token}`; + delete req.query.token; + } + next(); +}; + module.exports = { ensureAuthenticated, ensureAdmin, ensureAdminOrStaff, ensurePlatformAdmin, attachUserIfPresent, + attachTokenFromQuery, authRateLimiter, generalRateLimiter, strictRateLimiter, diff --git a/server/routes/invoices.js b/server/routes/invoices.js index 3c3e4de..307a6d2 100644 --- a/server/routes/invoices.js +++ b/server/routes/invoices.js @@ -8,8 +8,9 @@ const { printEscPos, voidInvoice, } = require('../controllers/invoiceController'); -const { ensureAuthenticated, ensureAdmin, ensureAdminOrStaff } = require('../middleware/auth'); +const { ensureAuthenticated, ensureAdmin, ensureAdminOrStaff, attachTokenFromQuery } = require('../middleware/auth'); +router.use(attachTokenFromQuery); router.use(ensureAuthenticated); router.get('/', ensureAdminOrStaff, listInvoices); router.get('/:id', ensureAdminOrStaff, getInvoice);