Ctrl K
ComunidadGuías…

api/src/app.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { ServerResponse } from 'http';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'path';
import { useEnv } from '@directus/env';
import { InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
import { handlePressure } from '@directus/pressure';
import cookieParser from 'cookie-parser';
import type { Request, RequestHandler, Response } from 'express';
import express from 'express';
import { merge } from 'lodash-es';
import qs from 'qs';
import { aiRouter } from './ai/chat/router.js';
import { initAIDevTools } from './ai/devtools/index.js';
import { aiFilesRouter } from './ai/files/router.js';
import { initAITelemetry } from './ai/telemetry/index.js';
import { registerAuthProviders } from './auth.js';
import accessRouter from './controllers/access.js';
import activityRouter from './controllers/activity.js';
import assetsRouter from './controllers/assets.js';
import authRouter from './controllers/auth.js';
import collectionsRouter from './controllers/collections.js';
import commentsRouter from './controllers/comments.js';
import dashboardsRouter from './controllers/dashboards.js';
import deploymentWebhookRouter from './controllers/deployment-webhooks.js';
import deploymentRouter from './controllers/deployment.js';
import extensionsRouter from './controllers/extensions.js';
import fieldsRouter from './controllers/fields.js';
import filesRouter from './controllers/files.js';
import flowsRouter from './controllers/flows.js';
import foldersRouter from './controllers/folders.js';
import graphqlRouter from './controllers/graphql.js';
import itemsRouter from './controllers/items.js';
import licenseRouter from './controllers/license.js';
import mcpRouter from './controllers/mcp/index.js';
import mcpOAuthClientsRouter from './controllers/mcp/oauth-clients.js';
import { mcpOAuthProtectedRouter, mcpOAuthPublicRouter } from './controllers/mcp/oauth.js';
import metricsRouter from './controllers/metrics.js';
import notFoundHandler from './controllers/not-found.js';
import notificationsRouter from './controllers/notifications.js';
import operationsRouter from './controllers/operations.js';
import panelsRouter from './controllers/panels.js';
import permissionsRouter from './controllers/permissions.js';
import policiesRouter from './controllers/policies.js';
import presetsRouter from './controllers/presets.js';
import relationsRouter from './controllers/relations.js';
import revisionsRouter from './controllers/revisions.js';
import rolesRouter from './controllers/roles.js';
import schemaRouter from './controllers/schema.js';
import serverRouter from './controllers/server.js';
import settingsRouter from './controllers/settings.js';
import sharesRouter from './controllers/shares.js';
import translationsRouter from './controllers/translations.js';
import tusRouter from './controllers/tus.js';
import usersRouter from './controllers/users.js';
import utilsRouter from './controllers/utils.js';
import versionsRouter from './controllers/versions.js';
import {
	isInstalled,
	validateDatabaseConnection,
	validateDatabaseExtensions,
	validateMigrations,
} from './database/index.js';
import { ensureDeploymentWebhooks, registerDeploymentDrivers } from './deployment.js';
import emitter from './emitter.js';
import { getExtensionManager } from './extensions/index.js';
import { getFlowManager } from './flows.js';
import { getEntitlementManager, getLicenseManager } from './license/index.js';
import { createExpressLogger, useLogger } from './logger/index.js';
import authenticate from './middleware/authenticate.js';
import cache from './middleware/cache.js';
import cors from './middleware/cors.js';
import { errorHandler } from './middleware/error-handler.js';
import extractToken from './middleware/extract-token.js';
import mcpOAuthGuard from './middleware/mcp-oauth-guard.js';
import rateLimiterGlobal from './middleware/rate-limiter-global.js';
import rateLimiter from './middleware/rate-limiter-ip.js';
import requestCounter from './middleware/request-counter.js';
import sanitizeQuery from './middleware/sanitize-query.js';
import schema from './middleware/schema.js';
import metricsSchedule from './schedules/metrics.js';
import scheduleOAuthCleanup from './schedules/oauth-cleanup.js';
import projectSchedule from './schedules/project.js';
import retentionSchedule from './schedules/retention.js';
import telemetrySchedule from './schedules/telemetry.js';
import tusSchedule from './schedules/tus.js';
import { getConfigFromEnv } from './utils/get-config-from-env.js';
import { Url } from './utils/url.js';
import { validateStorage } from './utils/validate-storage.js';

const require = createRequire(import.meta.url);

export default async function createApp(): Promise<express.Application> {
	const env = useEnv();
	const logger = useLogger();
	const helmet = await import('helmet');

	await validateDatabaseConnection();

	if ((await isInstalled()) === false) {
		logger.error(`Database doesn't have Directus tables installed.`);
		process.exit(1);
	}

	if ((await validateMigrations()) === false) {
		logger.warn(`Database migrations have not all been run`);
	}

	if (!env.SECRET) {
		logger.warn(
			`"SECRET" env variable is missing. Using a random value instead. Tokens will not persist between restarts. This is not appropriate for production usage.`,
		);
	}

	if (typeof env.SECRET === 'string' && Buffer.byteLength(env.SECRET) < 32) {
		logger.warn(
			'"SECRET" env variable is shorter than 32 bytes which is insecure. This is not appropriate for production usage.',
		);
	}

	if (!new Url(env.PUBLIC_URL).isAbsolute()) {
		logger.warn('"PUBLIC_URL" should be a full URL');
	}

	if (env.MCP_OAUTH_ENABLED === true && env.MCP_ENABLED === false) {
		logger.warn('MCP_OAUTH_ENABLED requires MCP_ENABLED=true. OAuth disabled.');
		env.MCP_OAUTH_ENABLED = false;
	}

	await validateDatabaseExtensions();
	await validateStorage();

	const licenseManager = getLicenseManager();

	await licenseManager.initialize();
	getEntitlementManager().initialize();

	await registerAuthProviders();
	registerDeploymentDrivers();
	await ensureDeploymentWebhooks();

	const extensionManager = getExtensionManager();
	const flowManager = getFlowManager();

	await extensionManager.initialize();
	await flowManager.initialize();

	const app = express();

	app.disable('x-powered-by');
	app.set('trust proxy', env.IP_TRUST_PROXY);

	app.set('query parser', (str: string) =>
		qs.parse(str, {
			depth: env.QUERYSTRING_MAX_PARSE_DEPTH,
			arrayLimit: env.QUERYSTRING_ARRAY_LIMIT,
		}),
	);

	if (env.PRESSURE_LIMITER_ENABLED) {
		const sampleInterval = env.PRESSURE_LIMITER_SAMPLE_INTERVAL;

		if (Number.isFinite(sampleInterval) === false) {
			throw new Error(`Invalid value for PRESSURE_LIMITER_SAMPLE_INTERVAL environment variable`);
		}

		app.use(
			handlePressure({
				sampleInterval,
				maxEventLoopUtilization: env.PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION,
				maxEventLoopDelay: env.PRESSURE_LIMITER_MAX_EVENT_LOOP_DELAY,
				maxMemoryRss: env.PRESSURE_LIMITER_MAX_MEMORY_RSS || undefined,
				maxMemoryHeapUsed: env.PRESSURE_LIMITER_MAX_MEMORY_HEAP_USED || undefined,
				error: new ServiceUnavailableError({ service: 'api', reason: 'Under pressure' }),
				retryAfter: env.PRESSURE_LIMITER_RETRY_AFTER || undefined,
			}),
		);
	}

	app.use(
		helmet.contentSecurityPolicy(
			merge(
				{
					useDefaults: true,
					directives: {
						// Unsafe-eval is required for app extensions
						scriptSrc: ["'self'", "'unsafe-eval'"],

						// Even though this is recommended to have enabled, it breaks most local
						// installations. Making this opt-in rather than opt-out is a little more
						// friendly. Ref #10806
						upgradeInsecureRequests: null,

						// These are required for MapLibre
						workerSrc: ["'self'", 'blob:'],
						childSrc: ["'self'", 'blob:'],
						imgSrc: [
							"'self'",
							'data:',
							'blob:',
							'https://raw.githubusercontent.com',
							'https://avatars.githubusercontent.com',
						],
						mediaSrc: ["'self'"],
						connectSrc: ["'self'", 'https://*', 'wss://*'],
					},
				},
				getConfigFromEnv('CONTENT_SECURITY_POLICY_'),
			),
		),
	);

	if (env.CROSS_ORIGIN_OPENER_POLICY_ENABLED) {
		app.use(
			helmet.crossOriginOpenerPolicy({
				policy: env.CROSS_ORIGIN_OPENER_POLICY as 'same-origin' | 'same-origin-allow-popups' | 'unsafe-none',
			}),
		);
	}

	if (env['HSTS_ENABLED']) {
		app.use(helmet.hsts(getConfigFromEnv('HSTS_', { omitPrefix: 'HSTS_ENABLED' })));
	}

	await emitter.emitInit('app.before', { app });

	await emitter.emitInit('middlewares.before', { app });

	app.use(createExpressLogger());

	app.use((_req, res, next) => {
		res.setHeader('X-Powered-By', 'Directus');
		next();
	});

	if (env.CORS_ENABLED) {
		app.use(cors);
	}

	app.use((req, res, next) => {
		(
			express.json({
				limit: env.MAX_PAYLOAD_SIZE,
				verify: (req, _res, buf) => {
					(req as any).rawBody = buf;
				},
			}) as RequestHandler
		)(req, res, (err: any) => {
			if (err) {
				return next(new InvalidPayloadError({ reason: err.message }));
			}

			return next();
		});
	});

	app.use(cookieParser());

	app.use(extractToken);

	app.get('/', (_req, res, next) => {
		if (env.ROOT_REDIRECT) {
			res.redirect(env.ROOT_REDIRECT);
		} else {
			next();
		}
	});

	app.get('/robots.txt', (_, res) => {
		res.set('Content-Type', 'text/plain');
		res.status(200);
		res.send(env.ROBOTS_TXT);
	});

	if (env.SERVE_APP) {
		const adminPath = require.resolve('@directus/app');
		const adminUrl = new Url(env.PUBLIC_URL).addPath('admin');

		const embeds = extensionManager.getEmbeds();

		// Set the App's base path according to the APIs public URL
		const html = await readFile(adminPath, 'utf8');

		const htmlWithVars = html
			.replace(/<base \/>/, `<base href="${adminUrl.toString({ rootRelative: true })}/" />`)
			.replace('<!-- directus-embed-head -->', embeds.head)
			.replace('<!-- directus-embed-body -->', embeds.body);

		const sendHtml = (_req: Request, res: Response) => {
			res.setHeader('Cache-Control', 'no-cache');
			res.setHeader('Vary', 'Origin, Cache-Control');
			res.send(htmlWithVars);
		};

		const setStaticHeaders = (res: ServerResponse) => {
			res.setHeader('Cache-Control', 'max-age=31536000, immutable');
			res.setHeader('Vary', 'Origin, Cache-Control');
		};

		app.get('/admin', sendHtml);
		app.use('/admin', express.static(path.join(adminPath, '..'), { setHeaders: setStaticHeaders }));
		app.use('/admin/*', sendHtml);
	}

	// use the rate limiter - all routes for now
	if (env.RATE_LIMITER_GLOBAL_ENABLED) {
		app.use(rateLimiterGlobal);
	}

	if (env.RATE_LIMITER_ENABLED) {
		app.use(rateLimiter);
	}

	app.get('/server/ping', (_req, res) => res.send('pong'));

	// Public webhook endpoint (signature-verified by the provider)
	app.use('/deployments/webhooks', deploymentWebhookRouter);

	if (env.MCP_OAUTH_ENABLED) {
		app.use(mcpOAuthPublicRouter);
	}

	app.use(authenticate);
	app.use(mcpOAuthGuard);

	if (env.MCP_OAUTH_ENABLED) {
		app.use(mcpOAuthProtectedRouter);
	}

	app.use(schema);

	app.use(sanitizeQuery);

	app.use(requestCounter);

	app.use(cache);

	await emitter.emitInit('middlewares.after', { app });

	await emitter.emitInit('routes.before', { app });

	app.use('/auth', authRouter);

	app.use('/graphql', graphqlRouter);

	app.use('/activity', activityRouter);
	app.use('/access', accessRouter);
	app.use('/assets', assetsRouter);
	app.use('/collections', collectionsRouter);
	app.use('/comments', commentsRouter);
	app.use('/dashboards', dashboardsRouter);
	app.use('/deployments', deploymentRouter);
	app.use('/extensions', extensionsRouter);
	app.use('/fields', fieldsRouter);

	if (env.TUS_ENABLED) {
		app.use('/files/tus', tusRouter);
	}

	app.use('/files', filesRouter);
	app.use('/flows', flowsRouter);
	app.use('/folders', foldersRouter);
	app.use('/items', itemsRouter);
	app.use('/license', licenseRouter);

	if (env.MCP_ENABLED) {
		app.use('/mcp', mcpRouter);
	}

	if (env.AI_ENABLED) {
		await initAIDevTools();
		await initAITelemetry();
		app.use('/ai', aiRouter);
		app.use('/ai/files', aiFilesRouter);
	}

	if (env.METRICS_ENABLED) {
		app.use('/metrics', metricsRouter);
	}

	app.use('/notifications', notificationsRouter);
	app.use('/operations', operationsRouter);
	app.use('/panels', panelsRouter);
	app.use('/permissions', permissionsRouter);
	app.use('/policies', policiesRouter);
	app.use('/presets', presetsRouter);
	app.use('/translations', translationsRouter);
	app.use('/relations', relationsRouter);
	app.use('/revisions', revisionsRouter);
	app.use('/roles', rolesRouter);

	if (env.MCP_OAUTH_ENABLED) {
		app.use('/mcp-oauth/clients', mcpOAuthClientsRouter);
	}

	app.use('/schema', schemaRouter);
	app.use('/server', serverRouter);
	app.use('/settings', settingsRouter);
	app.use('/shares', sharesRouter);
	app.use('/users', usersRouter);
	app.use('/utils', utilsRouter);
	app.use('/versions', versionsRouter);

	// Register custom endpoints
	await emitter.emitInit('routes.custom.before', { app });
	app.use(extensionManager.getEndpointRouter());
	await emitter.emitInit('routes.custom.after', { app });

	app.use(notFoundHandler);
	app.use(errorHandler);

	await emitter.emitInit('routes.after', { app });

	await retentionSchedule();
	await telemetrySchedule();
	await tusSchedule();
	await metricsSchedule();
	await projectSchedule();
	await licenseManager.scheduleCheck();

	if (env.MCP_OAUTH_ENABLED) {
		await scheduleOAuthCleanup();
	}

	await emitter.emitInit('app.after', { app });

	return app;
}