This commit is contained in:
+25
-8
@@ -1,11 +1,13 @@
|
||||
import express from 'express'
|
||||
import express, { type Request, type Response } from 'express'
|
||||
import cors, { type CorsOptions } from 'cors'
|
||||
import helmet from 'helmet'
|
||||
import morgan from 'morgan'
|
||||
import swaggerUi from 'swagger-ui-express'
|
||||
import { openApiDocument } from './swagger/openapi'
|
||||
import { getPublicStorageRoot } from './lib/storage'
|
||||
import { authLimiter, apiLimiter, publicLimiter, adminLimiter } from './middleware/rateLimiter'
|
||||
import { authLimiter, apiLimiter, publicLimiter, adminLimiter, webhookLimiter } from './middleware/rateLimiter'
|
||||
import { requireTrustedOriginForCookieMutations } from './middleware/csrf'
|
||||
import { sanitizeForwardedHeaders } from './middleware/forwardedHeaders'
|
||||
import { requestIdMiddleware } from './middleware/requestId'
|
||||
|
||||
// ─── Module routes ────────────────────────────────────────────
|
||||
@@ -115,6 +117,7 @@ export function createApp() {
|
||||
app.set('trust proxy', 1)
|
||||
}
|
||||
|
||||
app.use(sanitizeForwardedHeaders)
|
||||
app.use(requestIdMiddleware)
|
||||
|
||||
app.use((req, res, next) => {
|
||||
@@ -125,12 +128,16 @@ export function createApp() {
|
||||
})
|
||||
|
||||
app.use(corsMiddleware)
|
||||
app.use(requireTrustedOriginForCookieMutations)
|
||||
|
||||
// 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()
|
||||
})
|
||||
app.use('/storage/companies/:companyId/reservations/:reservationId', (_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
|
||||
@@ -145,15 +152,24 @@ export function createApp() {
|
||||
// through authenticated API routes such as /customers/:id/license-image.
|
||||
app.use('/storage', express.static(getPublicStorageRoot()))
|
||||
|
||||
// Swagger UI — mounted before helmet so its assets are not blocked by CSP
|
||||
app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument, { customSiteTitle: 'RentalDriveGo API Docs' }))
|
||||
app.get('/api/v1/openapi.json', (_req, res) => res.json(openApiDocument))
|
||||
const publicDocsEnabled = process.env.NODE_ENV !== 'production' || process.env.ENABLE_PUBLIC_API_DOCS === 'true'
|
||||
const docsDisabled = (_req: Request, res: Response) => res.status(404).json({ error: 'not_found', message: 'API docs are not available in this environment', statusCode: 404 })
|
||||
|
||||
// Swagger UI — never expose API reconnaissance material publicly in production
|
||||
// unless explicitly enabled for a protected/internal deployment.
|
||||
if (publicDocsEnabled) {
|
||||
app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument, { customSiteTitle: 'RentalDriveGo API Docs' }))
|
||||
app.get('/api/v1/openapi.json', (_req, res) => res.json(openApiDocument))
|
||||
} else {
|
||||
app.use('/docs', docsDisabled)
|
||||
app.get('/api/v1/openapi.json', docsDisabled)
|
||||
}
|
||||
|
||||
// Webhooks must use raw body BEFORE express.json(); signature verification
|
||||
// must never reconstruct the payload with JSON.stringify(req.body).
|
||||
app.use(`${v1}/webhooks`, express.raw({ type: 'application/json' }), webhookRouter)
|
||||
app.use(`${v1}/payments/webhooks`, express.raw({ type: 'application/json' }))
|
||||
app.use(`${v1}/subscriptions/webhooks`, express.raw({ type: 'application/json' }))
|
||||
app.use(`${v1}/webhooks`, webhookLimiter, express.raw({ type: 'application/json', limit: '1mb' }), webhookRouter)
|
||||
app.use(`${v1}/payments/webhooks`, webhookLimiter, express.raw({ type: 'application/json', limit: '1mb' }))
|
||||
app.use(`${v1}/subscriptions/webhooks`, webhookLimiter, express.raw({ type: 'application/json', limit: '1mb' }))
|
||||
|
||||
// Let /storage responses manage CORP explicitly so missing files still return
|
||||
// a normal cross-origin 404 instead of being blocked by Helmet's default
|
||||
@@ -227,6 +243,7 @@ export function createApp() {
|
||||
})
|
||||
|
||||
app.get(`${v1}/docs`, (_req, res) => {
|
||||
if (!publicDocsEnabled) return docsDisabled(_req, res)
|
||||
res.json({
|
||||
name: 'rentaldrivego-api',
|
||||
version: '1.0.0',
|
||||
|
||||
Reference in New Issue
Block a user