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 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 { attachCustomer } from './middleware/customerAuth'; 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()); app.use(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); app.use('/api/admin/customers', adminCustomersRouter); app.use('/api/admin/settings', adminSettingsRouter); app.use('/api/admin/categories', adminCategoriesRouter); app.use('/api/admin/tags', adminTagsRouter); app.use('/api/admin', adminRouter); app.use('/api/customers/me/addresses', shippingAddressesRouter); app.use('/api/customers', customersRouter); 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;