import { config as loadEnv } from 'dotenv'; import express from 'express'; import path from 'node:path'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { corsMiddleware, corsErrorHandler } from './middleware/cors.js'; import { initPool } from './utils/db.js'; import { authenticate } from '../shared/auth/middleware.js'; import { requestLog } from '../shared/logging/request-log.js'; import { logger } from '../shared/logging/logger.js'; import { errorHandler } from '../shared/errors/handler.js'; import productsRouter from './routes/products.js'; import dashboardRouter from './routes/dashboard.js'; import ordersRouter from './routes/orders.js'; import csvRouter from './routes/data-management.js'; import analyticsRouter from './routes/analytics.js'; import purchaseOrdersRouter from './routes/purchase-orders.js'; import configRouter from './routes/config.js'; import metricsRouter from './routes/metrics.js'; import importRouter from './routes/import.js'; import aiValidationRouter from './routes/ai-validation.js'; import aiRouter, { initInBackground as initAiInBackground } from './routes/ai.js'; import templatesRouter from './routes/templates.js'; import aiPromptsRouter from './routes/ai-prompts.js'; import reusableImagesRouter from './routes/reusable-images.js'; import categoriesAggregateRouter from './routes/categoriesAggregate.js'; import vendorsAggregateRouter from './routes/vendorsAggregate.js'; import brandsAggregateRouter from './routes/brandsAggregate.js'; import htsLookupRouter from './routes/hts-lookup.js'; import specLookupRouter from './routes/spec-lookup.js'; import importSessionsRouter from './routes/import-sessions.js'; import importAuditLogRouter from './routes/import-audit-log.js'; import productEditorAuditLogRouter from './routes/product-editor-audit-log.js'; import newsletterRouter from './routes/newsletter.js'; import linesAggregateRouter from './routes/linesAggregate.js'; import repeatOrdersRouter from './routes/repeat-orders.js'; import apiv2BridgeRouter from './routes/apiv2-bridge.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const envPath = '/var/www/inventory/.env'; loadEnv({ path: envPath }); logger.info({ envPath, envExists: fs.existsSync(envPath), NODE_ENV: process.env.NODE_ENV || 'not set', PORT: process.env.PORT || 'not set', DB_HOST: process.env.DB_HOST || 'not set', DB_NAME: process.env.DB_NAME || 'not set', DB_PASSWORD: process.env.DB_PASSWORD ? '[set]' : 'not set', DB_SSL: process.env.DB_SSL || 'not set', }, 'inventory-server starting'); if (!process.env.JWT_SECRET) { logger.error('JWT_SECRET is not set; refusing to start (per Phase 6.4)'); process.exit(1); } const serverRoot = path.resolve(__dirname, '..'); const configuredUploadsDir = process.env.UPLOADS_DIR; const uploadsDir = configuredUploadsDir ? (path.isAbsolute(configuredUploadsDir) ? configuredUploadsDir : path.resolve(serverRoot, configuredUploadsDir)) : path.resolve(serverRoot, 'uploads'); process.env.UPLOADS_DIR = uploadsDir; const requiredDirs = [path.resolve(serverRoot, 'logs'), uploadsDir]; requiredDirs.forEach((dir) => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); const app = express(); // Trust X-Forwarded-* only when the immediate hop is loopback (Caddy on the same // host). Anything stricter would leave req.ip as 127.0.0.1; anything looser would // let arbitrary clients spoof their source IP via X-Forwarded-For. Required for // the KIOSK_IPS bypass in shared/auth/middleware.js to match real client IPs. app.set('trust proxy', 'loopback'); // Phase 6.5/6.9: structured access log (replaces the previous header-dumping debug // middleware that wrote raw Authorization values to stdout). Pino redaction strips // `authorization` and `cookie` automatically — see shared/logging/logger.js. app.use(requestLog()); app.use(corsMiddleware); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); async function startServer() { try { const pool = await initPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, port: process.env.DB_PORT || 5432, max: process.env.NODE_ENV === 'production' ? 20 : 10, idleTimeoutMillis: 30_000, connectionTimeoutMillis: 2_000, ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false, }); app.locals.pool = pool; // Phase 6.1/6.2: every /api request requires a valid JWT. Defense in depth — Caddy // forward_auth (when enabled) is the first reject; this is the second. Frontend // service files MUST include `Authorization: Bearer ` on every fetch. app.use('/api', authenticate({ pool, secret: process.env.JWT_SECRET })); app.use('/api/products', productsRouter); app.use('/api/dashboard', dashboardRouter); app.use('/api/orders', ordersRouter); app.use('/api/csv', csvRouter); app.use('/api/analytics', analyticsRouter); app.use('/api/purchase-orders', purchaseOrdersRouter); app.use('/api/config', configRouter); app.use('/api/metrics', metricsRouter); app.use('/api/vendors', vendorsAggregateRouter); app.use('/api/categories', categoriesAggregateRouter); app.use('/api/categories-aggregate', categoriesAggregateRouter); app.use('/api/vendors-aggregate', vendorsAggregateRouter); app.use('/api/brands-aggregate', brandsAggregateRouter); app.use('/api/import', importRouter); app.use('/api/ai-validation', aiValidationRouter); app.use('/api/ai', aiRouter); app.use('/api/templates', templatesRouter); app.use('/api/ai-prompts', aiPromptsRouter); app.use('/api/reusable-images', reusableImagesRouter); app.use('/api/hts-lookup', htsLookupRouter); app.use('/api/spec-lookup', specLookupRouter); app.use('/api/import-sessions', importSessionsRouter); app.use('/api/import-audit-log', importAuditLogRouter); app.use('/api/product-editor-audit-log', productEditorAuditLogRouter); app.use('/api/newsletter', newsletterRouter); app.use('/api/lines-aggregate', linesAggregateRouter); app.use('/api/repeat-orders', repeatOrdersRouter); // Side-service: lets external apps (import / product-edit skills) post to the // legacy PHP /apiv2 API without a browser cookie. Isolated, additive router; // inherits the same user-JWT auth applied to /api above. See apiv2-bridge.js. app.use('/api/apiv2-bridge', apiv2BridgeRouter); app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), environment: process.env.NODE_ENV, }); }); app.use(corsErrorHandler); app.use(errorHandler); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { logger.info({ port: PORT, mode: process.env.NODE_ENV || 'development' }, 'inventory-server listening'); initAiInBackground(); }); } catch (error) { logger.error({ err: error }, 'Failed to start server'); process.exit(1); } } process.on('uncaughtException', (err) => { logger.error({ err }, 'Uncaught Exception'); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { logger.error({ reason, promise }, 'Unhandled Rejection'); }); startServer();