All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m37s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 29s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
171 lines
6.4 KiB
TypeScript
171 lines
6.4 KiB
TypeScript
/**
|
|
* @ai-summary Fastify app configuration with feature registration
|
|
* @ai-context Each feature capsule registers its routes independently
|
|
*/
|
|
import Fastify, { FastifyInstance } from 'fastify';
|
|
import cors from '@fastify/cors';
|
|
import helmet from '@fastify/helmet';
|
|
import fastifyMultipart from '@fastify/multipart';
|
|
|
|
// Core plugins
|
|
import authPlugin from './core/plugins/auth.plugin';
|
|
import adminGuardPlugin, { setAdminGuardPool } from './core/plugins/admin-guard.plugin';
|
|
import tierGuardPlugin from './core/plugins/tier-guard.plugin';
|
|
import loggingPlugin from './core/plugins/logging.plugin';
|
|
import errorPlugin from './core/plugins/error.plugin';
|
|
import { appConfig } from './core/config/config-loader';
|
|
|
|
// Fastify feature routes
|
|
import { authRoutes } from './features/auth';
|
|
import { vehiclesRoutes } from './features/vehicles/api/vehicles.routes';
|
|
import { fuelLogsRoutes } from './features/fuel-logs/api/fuel-logs.routes';
|
|
import { stationsRoutes } from './features/stations/api/stations.routes';
|
|
import { communityStationsRoutes } from './features/stations/api/community-stations.routes';
|
|
import { documentsRoutes } from './features/documents/api/documents.routes';
|
|
import { maintenanceRoutes } from './features/maintenance';
|
|
import { platformRoutes } from './features/platform';
|
|
import { adminRoutes } from './features/admin/api/admin.routes';
|
|
import { auditLogRoutes } from './features/audit-log/api/audit-log.routes';
|
|
import { notificationsRoutes } from './features/notifications';
|
|
import { userProfileRoutes } from './features/user-profile';
|
|
import { onboardingRoutes } from './features/onboarding';
|
|
import { userPreferencesRoutes } from './features/user-preferences';
|
|
import { userExportRoutes } from './features/user-export';
|
|
import { userImportRoutes } from './features/user-import';
|
|
import { pool } from './core/config/database';
|
|
import { configRoutes } from './core/config/config.routes';
|
|
|
|
async function buildApp(): Promise<FastifyInstance> {
|
|
const app = Fastify({
|
|
logger: false, // Use custom logging plugin instead
|
|
maxParamLength: 1000, // Required for long Google Maps photo references
|
|
});
|
|
|
|
// Core middleware plugins
|
|
await app.register(helmet);
|
|
await app.register(cors);
|
|
await app.register(loggingPlugin);
|
|
await app.register(errorPlugin);
|
|
|
|
// Multipart upload support with config-driven size limits
|
|
const parseSizeToBytes = (val: string): number => {
|
|
// Accept forms like "10MB", "5M", "1048576", "20kb", case-insensitive
|
|
const s = String(val).trim().toLowerCase();
|
|
const match = s.match(/^(\d+)(b|kb|k|mb|m|gb|g)?$/i);
|
|
if (!match) {
|
|
// Fallback: try to parse integer bytes
|
|
const n = parseInt(s, 10);
|
|
return Number.isFinite(n) && n > 0 ? n : 10 * 1024 * 1024; // default 10MB
|
|
}
|
|
const num = parseInt(match[1], 10);
|
|
const unit = match[2] || 'b';
|
|
switch (unit) {
|
|
case 'b': return num;
|
|
case 'k':
|
|
case 'kb': return num * 1024;
|
|
case 'm':
|
|
case 'mb': return num * 1024 * 1024;
|
|
case 'g':
|
|
case 'gb': return num * 1024 * 1024 * 1024;
|
|
default: return num;
|
|
}
|
|
};
|
|
const fileSizeLimit = parseSizeToBytes(appConfig.config.performance.max_request_size);
|
|
await app.register(fastifyMultipart, {
|
|
limits: {
|
|
fileSize: fileSizeLimit,
|
|
},
|
|
});
|
|
|
|
// Authentication plugin
|
|
await app.register(authPlugin);
|
|
|
|
// Admin guard plugin - initializes after auth plugin
|
|
await app.register(adminGuardPlugin);
|
|
setAdminGuardPool(pool);
|
|
|
|
// Tier guard plugin - for subscription tier enforcement
|
|
await app.register(tierGuardPlugin);
|
|
|
|
// Health check
|
|
app.get('/health', async (_request, reply) => {
|
|
return reply.code(200).send({
|
|
status: 'healthy',
|
|
timestamp: new Date().toISOString(),
|
|
environment: process.env['NODE_ENV'],
|
|
features: ['admin', 'auth', 'config', 'onboarding', 'vehicles', 'documents', 'fuel-logs', 'stations', 'maintenance', 'platform', 'notifications', 'user-profile', 'user-preferences', 'user-export', 'user-import']
|
|
});
|
|
});
|
|
|
|
// API-prefixed health for Traefik route validation and diagnostics
|
|
app.get('/api/health', async (_request, reply) => {
|
|
return reply.code(200).send({
|
|
status: 'healthy',
|
|
scope: 'api',
|
|
timestamp: new Date().toISOString(),
|
|
features: ['admin', 'auth', 'config', 'onboarding', 'vehicles', 'documents', 'fuel-logs', 'stations', 'maintenance', 'platform', 'notifications', 'user-profile', 'user-preferences', 'user-export', 'user-import']
|
|
});
|
|
});
|
|
|
|
// Traefik forward-auth integration endpoint
|
|
app.get('/auth/verify', {
|
|
preHandler: [app.authenticate]
|
|
}, async (request, reply) => {
|
|
const user = request.user;
|
|
const userId = user?.sub || 'unknown';
|
|
const rolesClaim = user?.['https://motovaultpro.com/roles'];
|
|
const roles = Array.isArray(rolesClaim) ? rolesClaim : [];
|
|
|
|
reply
|
|
.header('X-Auth-User', userId)
|
|
.header('X-Auth-Roles', roles.join(','))
|
|
.code(200)
|
|
.send({
|
|
status: 'verified',
|
|
userId,
|
|
roles,
|
|
verifiedAt: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
// Register Fastify feature routes
|
|
await app.register(authRoutes, { prefix: '/api' });
|
|
await app.register(onboardingRoutes, { prefix: '/api' });
|
|
await app.register(platformRoutes, { prefix: '/api' });
|
|
await app.register(vehiclesRoutes, { prefix: '/api' });
|
|
await app.register(documentsRoutes, { prefix: '/api' });
|
|
await app.register(fuelLogsRoutes, { prefix: '/api' });
|
|
await app.register(stationsRoutes, { prefix: '/api' });
|
|
await app.register(communityStationsRoutes, { prefix: '/api' });
|
|
await app.register(maintenanceRoutes, { prefix: '/api' });
|
|
await app.register(adminRoutes, { prefix: '/api' });
|
|
await app.register(auditLogRoutes, { prefix: '/api' });
|
|
await app.register(notificationsRoutes, { prefix: '/api' });
|
|
await app.register(userProfileRoutes, { prefix: '/api' });
|
|
await app.register(userPreferencesRoutes, { prefix: '/api' });
|
|
await app.register(userExportRoutes, { prefix: '/api' });
|
|
await app.register(userImportRoutes, { prefix: '/api' });
|
|
await app.register(configRoutes, { prefix: '/api' });
|
|
|
|
// 404 handler
|
|
app.setNotFoundHandler(async (_request, reply) => {
|
|
return reply.code(404).send({ error: 'Route not found' });
|
|
});
|
|
|
|
return app;
|
|
}
|
|
|
|
export { buildApp };
|
|
|
|
// For compatibility with existing server.ts
|
|
let appInstance: FastifyInstance | null = null;
|
|
|
|
export async function getApp(): Promise<FastifyInstance> {
|
|
if (!appInstance) {
|
|
appInstance = await buildApp();
|
|
}
|
|
return appInstance;
|
|
}
|
|
|
|
export default buildApp;
|