import express, { Request, Response, NextFunction } from 'express'; import cookieParser from 'cookie-parser'; import path from 'path'; import itemsRouter from './routes/items'; import { router as cartCheckoutRouter, webhookRouter as cartCheckoutWebhookRouter } from './routes/cartCheckout'; import adminRouter from './routes/admin'; import adminCustomersRouter from './routes/adminCustomers'; import adminSettingsRouter from './routes/adminSettings'; import adminEmailTemplatesRouter from './routes/adminEmailTemplates'; import adminCategoriesRouter from './routes/adminCategories'; import adminTagsRouter from './routes/adminTags'; import filtersRouter from './routes/filters'; import customersRouter from './routes/customers'; import publicRouter from './routes/public'; import cartRouter from './routes/cart'; import shippingAddressesRouter from './routes/shippingAddresses'; import clientErrorsRouter from './routes/clientErrors'; import { attachCustomer } from './middleware/customerAuth'; import { requireAdminGate } from './middleware/adminGate'; import { asyncRoute } from './asyncRoute'; const app = express(); // Express advertises itself in X-Powered-By by default, which hands an // attacker the server stack for free when picking exploits to try. app.disable('x-powered-by'); app.set('trust proxy', 1); app.use('/webhooks/paypal', express.json(), cartCheckoutWebhookRouter); app.use(express.json()); app.use(cookieParser()); // Wrapped like any route: attachCustomer awaits a session lookup, and mounted // globally an unforwarded rejection here would hang every request in the app — // including the routes that wrap their own handlers correctly. app.use(asyncRoute(attachCustomer)); app.use('/uploads', express.static(process.env.UPLOADS_DIR || '/app/uploads')); app.get('/api/config', (_req, res) => { const clientId = process.env.PAYPAL_CLIENT_ID; const isPlaceholder = !clientId || clientId.length < 10 || clientId === 'REPLACE_WITH_PAYPAL_CLIENT_ID'; res.json({ paypalClientId: isPlaceholder ? null : clientId, demoMode: process.env.DEMO_MODE !== 'false', currency: process.env.SITE_CURRENCY || 'USD' }); }); app.use('/api/items', itemsRouter); app.use('/api/filters', filtersRouter); app.use('/api/cart', cartRouter); app.use('/api/checkout/cart', cartCheckoutRouter); // requireAdminGate is attached to each admin router rather than to a path // prefix. Attached to the router, an admin router added later at some other // path still inherits it — and since the proxy only injects the header on the // paths its regex matches, that router refuses loudly on its first request // instead of being quietly public. See middleware/adminGate.ts and #63. app.use('/api/admin/customers', requireAdminGate, adminCustomersRouter); app.use('/api/admin/settings', requireAdminGate, adminSettingsRouter); app.use('/api/admin/email-templates', requireAdminGate, adminEmailTemplatesRouter); app.use('/api/admin/categories', requireAdminGate, adminCategoriesRouter); app.use('/api/admin/tags', requireAdminGate, adminTagsRouter); app.use('/api/admin', requireAdminGate, adminRouter); app.use('/api/customers/me/addresses', shippingAddressesRouter); app.use('/api/customers', customersRouter); app.use('/api/client-errors', clientErrorsRouter); app.use('/', publicRouter); if (process.env.NODE_ENV !== 'test') { const staticDir = path.join(__dirname, '..', 'public'); app.use(express.static(staticDir)); app.get('*', (_req, res) => { res.sendFile(path.join(staticDir, 'index.html')); }); } // Mounted last, so it sees errors from every route above. Without it, a route // that hands an error to next() falls through to Express's default handler, // and — worse — an async route that rejects never responds at all, leaving the // client hanging. A hung request is indistinguishable from an empty catalogue // in the UI, which is exactly how a schema mismatch once read as "the store // has no items". Always answer. // // The four-argument signature is what marks this as error middleware; `next` // is unused but must stay for Express to recognise it. // eslint-disable-next-line @typescript-eslint/no-unused-vars app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { console.error(err); if (res.headersSent) return; res.status(500).json({ error: 'internal error' }); }); export default app;