mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
* Typecheck across packages that are built with esbuild * Boilerplate new Errors package * No need, tsup checks with --dts * Switch to tsup * Setup dev script * Add readme * More boilerplaty things * Finish createError function * Install @directus/random * Downgrade node types * Add utility function to check if an error is a DirectusError * Use new is-error check * Install errors package * Add failed validation common error * Export common errors * Move joi convertion to utils * Export failed validation * Use new failed validation error in validate-batch * Enhance typing output of createError * Remove outdir (handled by tsup now) * Replace Exception with Error * Replace exception in test * Remove exceptions from app * Remove exceptions from app * Remove failed validation exception from users service * Remove old failed validation exception from shared * Remove exceptions package in favor of errors * Uninstall exceptions * Replace baseexception check * Migrate content too large error * Critical detail * Replace ForbiddenException * WIP remove exceptions * Add ForbiddenError to errors * HitRateLimitError * Move validation related error/helper to new validation package * Add index * Add docs * Install random * Convert TokenExpired * Convert user-suspended * Convert invalid-credentials * Move UnsupportedMediaType * Replace wrong imports for forbidden * Convert invalid-ip * Move invalid provider * Move InvalidOtp * Convert InvalidToken * Move MethodNotAllowed * Convert range not satisfiable * Move unexpect response * Move UnprocessableContent * Move IllegalAssetTransformation * Move RouteNotFound * Finalize not found * Various db errors * Move value too long * Move not null * Move record-not-unique * Move value out of range * Finish db errors * Service unavailable * GQL errors * Update packages/validation/src/errors/failed-validation.ts Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> * Update packages/validation/src/errors/failed-validation.ts Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> * InvalidQuery * Add test for invalid query message constructor * Invalid Payload * Finalize exceptions move * Improve type of isDirectusError * Various fixes * Fix build in api * Update websocket exceptions use * Allow optional reason for invalid config * Update errors usage in utils * Remove unused package from errors * Update lockfile * Update api/src/auth/drivers/ldap.ts Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> * Update packages/validation/src/utils/joi-to-error-extensions.ts Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> * Put error codes in shared enum * Replace instanceof checks in api * Fix tests I think * Tweak override names * Fix linter warnings * Set snapshots * Start fixing BB tests * Fix blackbox tests * Add changeset * Update changeset * Update extension docs to use new createError abstraction * 🙄 * Fix graphql validation error name * 🥳 * use ErrorCode.Forbidden * fix blackbox auth login test * Add license files * Rename preMutationException to preMutationError * Remove unused ms dep & sort package.json * Remove periods from error messages for consistency Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> * Add optional code check * Use updated error code checker * Rename InvalidConfigError to InvalidProviderConfigError --------- Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch> Co-authored-by: ian <licitdev@gmail.com>
315 lines
9.8 KiB
TypeScript
315 lines
9.8 KiB
TypeScript
import { handlePressure } from '@directus/pressure';
|
|
import cookieParser from 'cookie-parser';
|
|
import type { Request, RequestHandler, Response } from 'express';
|
|
import express from 'express';
|
|
import type { ServerResponse } from 'http';
|
|
import { merge } from 'lodash-es';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { createRequire } from 'node:module';
|
|
import path from 'path';
|
|
import qs from 'qs';
|
|
import { registerAuthProviders } from './auth.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 dashboardsRouter from './controllers/dashboards.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 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 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 usersRouter from './controllers/users.js';
|
|
import utilsRouter from './controllers/utils.js';
|
|
import webhooksRouter from './controllers/webhooks.js';
|
|
import {
|
|
isInstalled,
|
|
validateDatabaseConnection,
|
|
validateDatabaseExtensions,
|
|
validateMigrations,
|
|
} from './database/index.js';
|
|
import emitter from './emitter.js';
|
|
import env from './env.js';
|
|
import { InvalidPayloadError, ServiceUnavailableError } from './errors/index.js';
|
|
import { getExtensionManager } from './extensions.js';
|
|
import { getFlowManager } from './flows.js';
|
|
import logger, { expressLogger } from './logger.js';
|
|
import authenticate from './middleware/authenticate.js';
|
|
import cache from './middleware/cache.js';
|
|
import { checkIP } from './middleware/check-ip.js';
|
|
import cors from './middleware/cors.js';
|
|
import errorHandler from './middleware/error-handler.js';
|
|
import extractToken from './middleware/extract-token.js';
|
|
import getPermissions from './middleware/get-permissions.js';
|
|
import rateLimiterGlobal from './middleware/rate-limiter-global.js';
|
|
import rateLimiter from './middleware/rate-limiter-ip.js';
|
|
import sanitizeQuery from './middleware/sanitize-query.js';
|
|
import schema from './middleware/schema.js';
|
|
import { getConfigFromEnv } from './utils/get-config-from-env.js';
|
|
import { collectTelemetry } from './utils/telemetry.js';
|
|
import { Url } from './utils/url.js';
|
|
import { validateEnv } from './utils/validate-env.js';
|
|
import { validateStorage } from './utils/validate-storage.js';
|
|
import { init as initWebhooks } from './webhooks.js';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
export default async function createApp(): Promise<express.Application> {
|
|
const helmet = await import('helmet');
|
|
|
|
validateEnv(['KEY', 'SECRET']);
|
|
|
|
if (!new Url(env['PUBLIC_URL']).isAbsolute()) {
|
|
logger.warn('PUBLIC_URL should be a full URL');
|
|
}
|
|
|
|
await validateStorage();
|
|
|
|
await validateDatabaseConnection();
|
|
await validateDatabaseExtensions();
|
|
|
|
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`);
|
|
}
|
|
|
|
await registerAuthProviders();
|
|
|
|
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: 10 }));
|
|
|
|
if (env['PRESSURE_LIMITER_ENABLED']) {
|
|
const sampleInterval = Number(env['PRESSURE_LIMITER_SAMPLE_INTERVAL']);
|
|
|
|
if (Number.isNaN(sampleInterval) === true || 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'],
|
|
maxMemoryHeapUsed: env['PRESSURE_LIMITER_MAX_MEMORY_HEAP_USED'],
|
|
error: new ServiceUnavailableError({ service: 'api', reason: 'Under pressure' }),
|
|
retryAfter: env['PRESSURE_LIMITER_RETRY_AFTER'],
|
|
})
|
|
);
|
|
}
|
|
|
|
app.use(
|
|
helmet.contentSecurityPolicy(
|
|
merge(
|
|
{
|
|
useDefaults: true,
|
|
directives: {
|
|
// Unsafe-eval is required for vue3 / vue-i18n / 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:'],
|
|
mediaSrc: ["'self'"],
|
|
connectSrc: ["'self'", 'https://*'],
|
|
},
|
|
},
|
|
getConfigFromEnv('CONTENT_SECURITY_POLICY_')
|
|
)
|
|
)
|
|
);
|
|
|
|
if (env['HSTS_ENABLED']) {
|
|
app.use(helmet.hsts(getConfigFromEnv('HSTS_', ['HSTS_ENABLED'])));
|
|
}
|
|
|
|
await emitter.emitInit('app.before', { app });
|
|
|
|
await emitter.emitInit('middlewares.before', { app });
|
|
|
|
app.use(expressLogger);
|
|
|
|
app.use((_req, res, next) => {
|
|
res.setHeader('X-Powered-By', 'Directus');
|
|
next();
|
|
});
|
|
|
|
if (env['CORS_ENABLED'] === true) {
|
|
app.use(cors);
|
|
}
|
|
|
|
app.use((req, res, next) => {
|
|
(
|
|
express.json({
|
|
limit: env['MAX_PAYLOAD_SIZE'],
|
|
}) 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(/<embed-head \/>/, embeds.head)
|
|
.replace(/<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'] === true) {
|
|
app.use(rateLimiterGlobal);
|
|
}
|
|
|
|
if (env['RATE_LIMITER_ENABLED'] === true) {
|
|
app.use(rateLimiter);
|
|
}
|
|
|
|
app.get('/server/ping', (_req, res) => res.send('pong'));
|
|
|
|
app.use(authenticate);
|
|
|
|
app.use(checkIP);
|
|
|
|
app.use(sanitizeQuery);
|
|
|
|
app.use(cache);
|
|
|
|
app.use(schema);
|
|
|
|
app.use(getPermissions);
|
|
|
|
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('/assets', assetsRouter);
|
|
app.use('/collections', collectionsRouter);
|
|
app.use('/dashboards', dashboardsRouter);
|
|
app.use('/extensions', extensionsRouter);
|
|
app.use('/fields', fieldsRouter);
|
|
app.use('/files', filesRouter);
|
|
app.use('/flows', flowsRouter);
|
|
app.use('/folders', foldersRouter);
|
|
app.use('/items', itemsRouter);
|
|
app.use('/notifications', notificationsRouter);
|
|
app.use('/operations', operationsRouter);
|
|
app.use('/panels', panelsRouter);
|
|
app.use('/permissions', permissionsRouter);
|
|
app.use('/presets', presetsRouter);
|
|
app.use('/translations', translationsRouter);
|
|
app.use('/relations', relationsRouter);
|
|
app.use('/revisions', revisionsRouter);
|
|
app.use('/roles', rolesRouter);
|
|
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('/webhooks', webhooksRouter);
|
|
|
|
// 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 });
|
|
|
|
// Register all webhooks
|
|
await initWebhooks();
|
|
|
|
collectTelemetry();
|
|
|
|
await emitter.emitInit('app.after', { app });
|
|
|
|
return app;
|
|
}
|