import express from 'express' import cors from 'cors' import helmet from 'helmet' import morgan from 'morgan' import { getLegacyStorageRoots, getStorageRoot } from './lib/storage' import { authLimiter, apiLimiter, publicLimiter, adminLimiter } from './middleware/rateLimiter' // ─── Module routes ──────────────────────────────────────────── import webhookRouter from './modules/webhooks/webhook.routes' import companyAuthRouter from './modules/auth/auth.company.routes' import employeeAuthRouter from './modules/auth/auth.employee.routes' import renterAuthRouter from './modules/auth/auth.renter.routes' import teamRouter from './modules/team/team.routes' import offersRouter from './modules/offers/offer.routes' import analyticsRouter from './modules/analytics/analytics.routes' import notificationsRouter from './modules/notifications/notification.routes' import adminRouter from './modules/admin/admin.routes' import subscriptionsRouter from './modules/subscriptions/subscription.routes' import paymentsRouter from './modules/payments/payment.routes' import customersRouter from './modules/customers/customer.routes' import vehiclesRouter from './modules/vehicles/vehicle.routes' import companiesRouter from './modules/companies/company.routes' import reservationsRouter from './modules/reservations/reservation.routes' import marketplaceRouter from './modules/marketplace/marketplace.routes' import siteRouter from './modules/site/site.routes' // ─── Centralized error handling ─────────────────────────────── import { errorMiddleware } from './http/errors/errorMiddleware' const v1 = '/api/v1' const defaultCorsOrigins = [ 'http://localhost:3000', 'http://localhost:4000', 'http://127.0.0.1:3000', 'http://127.0.0.1:4000', ] export const corsOrigins = process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(',').map((o) => o.trim()).filter(Boolean) : defaultCorsOrigins const routeDocs = [ { method: 'GET', path: '/health', description: 'Health check' }, { method: 'GET', path: `${v1}/docs`, description: 'Machine-readable API index' }, { method: 'GET', path: `${v1}/auth/renter/me`, description: 'Current renter profile' }, { method: 'GET', path: `${v1}/vehicles`, description: 'List company vehicles' }, { method: 'POST', path: `${v1}/vehicles`, description: 'Create vehicle' }, { method: 'GET', path: `${v1}/reservations`, description: 'List reservations' }, { method: 'POST', path: `${v1}/reservations`, description: 'Create reservation' }, { method: 'GET', path: `${v1}/customers`, description: 'List customers' }, { method: 'POST', path: `${v1}/customers`, description: 'Create customer' }, { method: 'GET', path: `${v1}/offers`, description: 'List offers' }, { method: 'GET', path: `${v1}/analytics/dashboard`, description: 'Dashboard analytics' }, { method: 'GET', path: `${v1}/analytics/report`, description: 'Analytics report' }, { method: 'GET', path: `${v1}/notifications/company`, description: 'Company notifications' }, { method: 'GET', path: `${v1}/marketplace/cities`, description: 'Marketplace cities' }, { method: 'GET', path: `${v1}/marketplace/search`, description: 'Marketplace search' }, { method: 'GET', path: `${v1}/site/:slug/brand`, description: 'Public site brand config' }, { method: 'GET', path: `${v1}/companies/me`, description: 'Current company profile' }, { method: 'GET', path: `${v1}/subscriptions/plans`, description: 'Subscription plans' }, { method: 'POST', path: `${v1}/admin/auth/login`, description: 'Admin login' }, ] export function createApp() { const app = express() const corsMiddleware = cors({ origin: corsOrigins, credentials: true }) if (process.env.NODE_ENV === 'production') { app.set('trust proxy', 1) } app.use(corsMiddleware) // Customer identity documents must never be anonymously retrievable from the // public storage mount, even if an older raw storage URL leaks. app.use('/storage/companies/:companyId/customers/:customerId', (_req, res) => { res.status(404).end() }) // Public storage assets (logos, vehicle photos, etc.) must be loadable cross-origin // by the browser. Without this header, helmet's default CORP: same-origin reaches // 404 responses (when express.static calls next() on a miss) and the browser blocks // the response even for broken-image cases, producing ERR_BLOCKED_BY_RESPONSE. app.use('/storage', (_req, res, next) => { res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin') next() }) // Serve uploaded assets from the configured storage root, with a legacy fallback // for older files that were written under the previous API-local path. app.use('/storage', express.static(getStorageRoot())) for (const legacyRoot of getLegacyStorageRoots()) { app.use('/storage', express.static(legacyRoot)) } // Webhook must use raw body BEFORE express.json() app.use('/api/v1/webhooks', express.raw({ type: 'application/json' }), webhookRouter) // Let /storage responses manage CORP explicitly so missing files still return // a normal cross-origin 404 instead of being blocked by Helmet's default // same-origin policy. app.use(helmet({ crossOriginResourcePolicy: false })) if (process.env.NODE_ENV !== 'test') app.use(morgan('combined')) app.use(express.json({ limit: '10mb' })) // ─── API Routes ───────────────────────────────────────────── app.use(`${v1}/auth/renter`, authLimiter, renterAuthRouter) app.use(`${v1}/auth/company`, authLimiter, companyAuthRouter) app.use(`${v1}/auth/employee`, authLimiter, employeeAuthRouter) app.use(`${v1}/admin`, adminLimiter, adminRouter) app.use(`${v1}/marketplace`, publicLimiter, marketplaceRouter) app.use(`${v1}/site`, publicLimiter, siteRouter) app.use(`${v1}/vehicles`, apiLimiter, vehiclesRouter) app.use(`${v1}/reservations`, apiLimiter, reservationsRouter) app.use(`${v1}/team`, apiLimiter, teamRouter) app.use(`${v1}/customers`, apiLimiter, customersRouter) app.use(`${v1}/offers`, apiLimiter, offersRouter) app.use(`${v1}/analytics`, apiLimiter, analyticsRouter) app.use(`${v1}/notifications`, apiLimiter, notificationsRouter) app.use(`${v1}/companies`, apiLimiter, companiesRouter) app.use(`${v1}/subscriptions`, apiLimiter, subscriptionsRouter) app.use(`${v1}/payments`, apiLimiter, paymentsRouter) // ─── Health / Docs ────────────────────────────────────────── app.get('/health', (_req, res) => { res.json({ status: 'ok', version: '1.0.0', timestamp: new Date().toISOString() }) }) app.get(`${v1}/docs`, (_req, res) => { res.json({ name: 'rentaldrivego-api', version: '1.0.0', baseUrl: v1, docsUrl: '/docs', routes: routeDocs }) }) app.get('/docs', (_req, res) => { const rows = routeDocs .map((r) => `${r.method}${r.path}${r.description}`) .join('') res.type('html').send(`RentalDriveGo API Docs

RentalDriveGo API

${rows}
MethodPathDescription
`) }) // ─── Error handler ────────────────────────────────────────── app.use(errorMiddleware) return app }