import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import cookieParser from 'cookie-parser'; import compression from 'compression'; import { pinoHttp } from 'pino-http'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import yaml from 'js-yaml'; import fs from 'node:fs'; import swaggerUi from 'swagger-ui-express'; import { env } from './config/env.js'; import { errorHandler, notFoundHandler } from './middleware/errorHandler.js'; import { sessionMiddleware } from './middleware/session.js'; import { authRouter } from './routes/auth.js'; import { dashboardRouter } from './routes/dashboard.js'; import { adminRouter } from './routes/admin.js'; import { filesRouter } from './routes/files.js'; import { linksRouter } from './routes/links.js'; import { analyticsRouter } from './routes/analytics.js'; import { store } from './services/store.js'; import { csrfProtection } from './middleware/csrf.js'; import { checkGdriveHealth } from './utils/gdrive.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); // yaml.load uses DEFAULT_SCHEMA in js-yaml v4 — no arbitrary code execution const openapiSpec = yaml.load( fs.readFileSync(join(__dirname, 'openapi.yaml'), 'utf8'), { schema: yaml.DEFAULT_SCHEMA }, ) as object; export async function createApp() { await store.init(); const app = express(); app.disable('x-powered-by'); app.set('trust proxy', env.nodeEnv === 'production' ? 1 : false); app.use(helmet({ crossOriginEmbedderPolicy: false, contentSecurityPolicy: env.nodeEnv === 'production' ? { directives: { defaultSrc: ["'none'"], baseUri: ["'none'"], frameAncestors: ["'none'"], formAction: ["'none'"], }, } : false, })); app.use(compression()); app.use(cors({ origin(origin, callback) { if (env.nodeEnv !== 'production' || !origin || origin === env.frontendUrl) { callback(null, true); return; } callback(new Error('Not allowed by CORS')); }, credentials: true, })); app.use(pinoHttp({ enabled: env.nodeEnv !== 'test', redact: ['req.headers.authorization', 'req.headers.cookie'], })); app.use(express.json({ limit: '1mb' })); app.use(cookieParser()); app.use(csrfProtection); app.use(sessionMiddleware); app.use('/api', (_req, res, next) => { res.setHeader('Cache-Control', 'no-store'); next(); }); app.get('/health', async (_req, res) => { const [dbOk, gdriveOk] = await Promise.all([ store.read().then(() => true).catch(() => false), env.nodeEnv === 'test' ? Promise.resolve(true) : checkGdriveHealth(), ]); const allOk = dbOk && gdriveOk; res.status(allOk ? 200 : 503).json({ status: allOk ? 'ok' : 'degraded', db: dbOk ? 'ok' : 'error', storage: gdriveOk ? 'ok' : 'error', uptime: process.uptime(), version: '0.1.0', }); }); // Swagger UI — only mounted outside production or behind admin routes if (env.nodeEnv !== 'production') { app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(openapiSpec)); } // Always expose the raw spec for tooling app.get('/api/openapi.json', (_req, res) => res.json(openapiSpec)); app.use('/api/auth', authRouter); app.use('/api/admin', adminRouter); app.use('/api/v1', analyticsRouter); app.use('/api/v1/files', filesRouter); app.use('/api/v1/links', linksRouter); app.use('/api/v1/dashboard', dashboardRouter); app.use(notFoundHandler); app.use(errorHandler); return app; }