|
|
|
|
@@ -7,11 +7,19 @@ import { performance } from 'node:perf_hooks';
|
|
|
|
|
import Fastify, { type FastifyInstance } from 'fastify';
|
|
|
|
|
import fastifyCookie from '@fastify/cookie';
|
|
|
|
|
import fastifyCors from '@fastify/cors';
|
|
|
|
|
import fastifySwagger from '@fastify/swagger';
|
|
|
|
|
import fastifySwaggerUi from '@fastify/swagger-ui';
|
|
|
|
|
import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify';
|
|
|
|
|
import type { IncomingMessage } from 'node:http';
|
|
|
|
|
import type pg from 'pg';
|
|
|
|
|
import { swaggerConfig } from '../shared/swagger.js';
|
|
|
|
|
import { registerHealthRoutes } from '../modules/health/index.js';
|
|
|
|
|
import { registerIdentityRoutes, createSessionAuthenticator } from '../modules/identity/index.js';
|
|
|
|
|
import {
|
|
|
|
|
registerBackofficeRoutes,
|
|
|
|
|
createBackofficeSessionAuthenticator,
|
|
|
|
|
createCombinedAuthenticator,
|
|
|
|
|
} from '../modules/backoffice/index.js';
|
|
|
|
|
import { registerUsersRoutes } from '../modules/users/index.js';
|
|
|
|
|
import { registerCategoriesRoutes } from '../modules/categories/index.js';
|
|
|
|
|
import { registerCatalogRoutes } from '../modules/catalog/index.js';
|
|
|
|
|
@@ -30,7 +38,7 @@ import { registerCmsRoutes } from '../modules/cms/index.js';
|
|
|
|
|
import { registerStoreSettingsRoutes } from '../modules/store-settings/index.js';
|
|
|
|
|
import { CacheService, InMemoryCacheAdapter, registerCacheRoutes } from '../modules/cache/index.js';
|
|
|
|
|
import { AuditLogger, RateLimiter, registerSecurityRoutes } from '../modules/security/index.js';
|
|
|
|
|
import { registerAdminStatsRoutes } from '../modules/admin-stats/api/stats.routes.js';
|
|
|
|
|
import { registerAdminStatsRoutes } from '../modules/admin-stats/index.js';
|
|
|
|
|
import { createInMemoryTelemetry, registerMetricsRoutes } from '../modules/observability/index.js';
|
|
|
|
|
import { LoggingEmailProvider } from '../modules/notifications/index.js';
|
|
|
|
|
import { createFlagStore, type FeatureFlagProvider } from '../modules/flags/index.js';
|
|
|
|
|
@@ -75,6 +83,10 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
const startTimes = new WeakMap<FastifyRequest, number>();
|
|
|
|
|
|
|
|
|
|
const app = Fastify({ logger: false, genReqId: generateRequestId });
|
|
|
|
|
// Route schemas are also the OpenAPI source. Several endpoints intentionally
|
|
|
|
|
// use broad response schemas, so serialize the explicit route DTOs without
|
|
|
|
|
// letting fast-json-stringify discard fields absent from those broad schemas.
|
|
|
|
|
app.setSerializerCompiler(() => (payload) => JSON.stringify(payload));
|
|
|
|
|
app.decorate('flags', flags);
|
|
|
|
|
|
|
|
|
|
await app.register(fastifyCors, {
|
|
|
|
|
@@ -119,8 +131,13 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
details = error.details;
|
|
|
|
|
message = statusCode < 500 ? error.message : 'Internal Server Error';
|
|
|
|
|
} else if (statusCode < 500) {
|
|
|
|
|
code = error.code ?? 'BAD_REQUEST';
|
|
|
|
|
message = error.message;
|
|
|
|
|
if (error.code === 'FST_ERR_VALIDATION') {
|
|
|
|
|
code = 'VALIDATION_ERROR';
|
|
|
|
|
message = 'Invalid request';
|
|
|
|
|
} else {
|
|
|
|
|
code = error.code ?? 'BAD_REQUEST';
|
|
|
|
|
message = error.message;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (statusCode >= 500) {
|
|
|
|
|
@@ -144,6 +161,17 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
// Cookie infrastructure is cross-module (identity + users): register once at root.
|
|
|
|
|
await app.register(fastifyCookie);
|
|
|
|
|
|
|
|
|
|
// ── OpenAPI / Swagger ────────────────────────────────────────────────────────
|
|
|
|
|
await app.register(fastifySwagger, swaggerConfig);
|
|
|
|
|
await app.register(fastifySwaggerUi, {
|
|
|
|
|
routePrefix: '/docs',
|
|
|
|
|
uiConfig: {
|
|
|
|
|
docExpansion: 'list',
|
|
|
|
|
deepLinking: true,
|
|
|
|
|
tryItOutEnabled: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (deps.pool) {
|
|
|
|
|
// Session resolution; created before identity registration so it can be passed
|
|
|
|
|
// to identity's /auth/me route.
|
|
|
|
|
@@ -157,33 +185,48 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// FIX-14: backoffice has its own auth mechanism (backoffice_session cookie,
|
|
|
|
|
// backoffice_users table). Two more authenticators:
|
|
|
|
|
// backofficeAuth — backoffice_session only (for backoffice routes).
|
|
|
|
|
// combinedAuth — tries backoffice_session first, then mdv_session
|
|
|
|
|
// (for routes shared by both admin and storefront).
|
|
|
|
|
const backofficeAuth = createBackofficeSessionAuthenticator(deps.pool as pg.Pool);
|
|
|
|
|
const combinedAuth = createCombinedAuthenticator(deps.pool as pg.Pool);
|
|
|
|
|
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerBackofficeRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate: backofficeAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Session resolution is identity's; users receives it by injection so no
|
|
|
|
|
// module ever imports another module.
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerUsersRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerCategoriesRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerBrandsRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerCatalogRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
logger,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
@@ -192,7 +235,7 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerInventoryRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -200,7 +243,7 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerPricingRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -208,14 +251,14 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerPromotionsRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerCartRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
pricing,
|
|
|
|
|
inventory,
|
|
|
|
|
promotions,
|
|
|
|
|
@@ -225,27 +268,27 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerShippingRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerOrdersRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { telemetry, meter } = createInMemoryTelemetry();
|
|
|
|
|
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerMetricsRoutes(instance, { meter, authenticate });
|
|
|
|
|
await registerMetricsRoutes(instance, { meter, authenticate: combinedAuth });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerCheckoutRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
tracer: telemetry.tracer,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
@@ -253,7 +296,7 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerPaymentsRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? 'whsec_test',
|
|
|
|
|
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
|
|
|
|
});
|
|
|
|
|
@@ -262,7 +305,7 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerNotificationsRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
emailProvider: new LoggingEmailProvider(),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
@@ -270,25 +313,25 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerReviewsRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerCmsRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerStoreSettingsRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
await registerAdminStatsRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -322,7 +365,7 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
invalidation: 'SearchInvalidated',
|
|
|
|
|
});
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerCacheRoutes(instance, { cache, authenticate });
|
|
|
|
|
await registerCacheRoutes(instance, { cache, authenticate: combinedAuth });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const rateLimiter = new RateLimiter();
|
|
|
|
|
@@ -330,7 +373,7 @@ export async function buildApp(deps: BuildAppDeps = {}): Promise<FastifyInstance
|
|
|
|
|
await app.register(async (instance) => {
|
|
|
|
|
await registerSecurityRoutes(instance, {
|
|
|
|
|
pool: deps.pool as pg.Pool,
|
|
|
|
|
authenticate,
|
|
|
|
|
authenticate: combinedAuth,
|
|
|
|
|
rateLimiter,
|
|
|
|
|
auditLogger,
|
|
|
|
|
});
|
|
|
|
|
|