import Fastify from 'fastify' import rateLimit from '@fastify/rate-limit' import { databasePlugin } from './plugins/database.js' import { redisPlugin } from './plugins/redis.js' import { corsPlugin } from './plugins/cors.js' import { errorHandlerPlugin } from './plugins/error-handler.js' import { authPlugin } from './plugins/auth.js' import { devAuthPlugin } from './plugins/dev-auth.js' import { storagePlugin } from './plugins/storage.js' import { healthRoutes } from './routes/v1/health.js' import { authRoutes } from './routes/v1/auth.js' import { accountRoutes } from './routes/v1/accounts.js' import { inventoryRoutes } from './routes/v1/inventory.js' import { productRoutes } from './routes/v1/products.js' import { lookupRoutes } from './routes/v1/lookups.js' import { fileRoutes } from './routes/v1/files.js' import { rbacRoutes } from './routes/v1/rbac.js' import { repairRoutes } from './routes/v1/repairs.js' import { storageRoutes } from './routes/v1/storage.js' import { RbacService } from './services/rbac.service.js' export async function buildApp() { const app = Fastify({ logger: { level: process.env.LOG_LEVEL ?? 'info', ...(process.env.NODE_ENV === 'development' ? { transport: { target: 'pino-pretty' } } : process.env.LOG_FILE ? { transport: { targets: [ { target: 'pino/file', options: { destination: 1 } }, // stdout { target: 'pino/file', options: { destination: process.env.LOG_FILE, mkdir: true } }, ], }, } : {}), }, genReqId: () => crypto.randomUUID(), requestTimeout: 30000, // 30 seconds }) // Plugins await app.register(corsPlugin) await app.register(errorHandlerPlugin) await app.register(databasePlugin) await app.register(redisPlugin) await app.register(rateLimit, { global: false }) await app.register(storagePlugin) // Auth — JWT in production/test, dev bypass only in development without JWT_SECRET if (process.env.JWT_SECRET) { await app.register(authPlugin) } else if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { app.log.warn('JWT_SECRET not set — using dev auth bypass. DO NOT USE IN PRODUCTION.') await app.register(devAuthPlugin) } else { throw new Error('JWT_SECRET is required in production') } // Routes await app.register(healthRoutes, { prefix: '/v1' }) await app.register(authRoutes, { prefix: '/v1' }) await app.register(accountRoutes, { prefix: '/v1' }) await app.register(inventoryRoutes, { prefix: '/v1' }) await app.register(productRoutes, { prefix: '/v1' }) await app.register(lookupRoutes, { prefix: '/v1' }) await app.register(fileRoutes, { prefix: '/v1' }) await app.register(rbacRoutes, { prefix: '/v1' }) await app.register(repairRoutes, { prefix: '/v1' }) await app.register(storageRoutes, { prefix: '/v1' }) // Auto-seed system permissions on startup app.addHook('onReady', async () => { try { await RbacService.seedPermissions(app.db) app.log.info('System permissions seeded') } catch (err) { app.log.error({ err }, 'Failed to seed permissions') } }) return app } async function start() { const app = await buildApp() const port = parseInt(process.env.PORT ?? '8000', 10) const host = process.env.HOST ?? '0.0.0.0' try { await app.listen({ port, host }) } catch (err) { app.log.error(err) process.exit(1) } } // Only auto-start when not imported by tests if (process.env.NODE_ENV !== 'test') { start() }