directus

Форк
0
/
app.ts 
323 строки · 10.2 Кб
1
import { useEnv } from '@directus/env';
2
import { InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
3
import { handlePressure } from '@directus/pressure';
4
import cookieParser from 'cookie-parser';
5
import type { Request, RequestHandler, Response } from 'express';
6
import express from 'express';
7
import type { ServerResponse } from 'http';
8
import { merge } from 'lodash-es';
9
import { readFile } from 'node:fs/promises';
10
import { createRequire } from 'node:module';
11
import path from 'path';
12
import qs from 'qs';
13
import { registerAuthProviders } from './auth.js';
14
import activityRouter from './controllers/activity.js';
15
import assetsRouter from './controllers/assets.js';
16
import authRouter from './controllers/auth.js';
17
import collectionsRouter from './controllers/collections.js';
18
import dashboardsRouter from './controllers/dashboards.js';
19
import extensionsRouter from './controllers/extensions.js';
20
import fieldsRouter from './controllers/fields.js';
21
import filesRouter from './controllers/files.js';
22
import flowsRouter from './controllers/flows.js';
23
import foldersRouter from './controllers/folders.js';
24
import graphqlRouter from './controllers/graphql.js';
25
import itemsRouter from './controllers/items.js';
26
import notFoundHandler from './controllers/not-found.js';
27
import notificationsRouter from './controllers/notifications.js';
28
import operationsRouter from './controllers/operations.js';
29
import panelsRouter from './controllers/panels.js';
30
import permissionsRouter from './controllers/permissions.js';
31
import presetsRouter from './controllers/presets.js';
32
import relationsRouter from './controllers/relations.js';
33
import revisionsRouter from './controllers/revisions.js';
34
import rolesRouter from './controllers/roles.js';
35
import schemaRouter from './controllers/schema.js';
36
import serverRouter from './controllers/server.js';
37
import settingsRouter from './controllers/settings.js';
38
import sharesRouter from './controllers/shares.js';
39
import translationsRouter from './controllers/translations.js';
40
import usersRouter from './controllers/users.js';
41
import utilsRouter from './controllers/utils.js';
42
import versionsRouter from './controllers/versions.js';
43
import webhooksRouter from './controllers/webhooks.js';
44
import {
45
	isInstalled,
46
	validateDatabaseConnection,
47
	validateDatabaseExtensions,
48
	validateMigrations,
49
} from './database/index.js';
50
import emitter from './emitter.js';
51
import { getExtensionManager } from './extensions/index.js';
52
import { getFlowManager } from './flows.js';
53
import { createExpressLogger, useLogger } from './logger.js';
54
import authenticate from './middleware/authenticate.js';
55
import cache from './middleware/cache.js';
56
import { checkIP } from './middleware/check-ip.js';
57
import cors from './middleware/cors.js';
58
import errorHandler from './middleware/error-handler.js';
59
import extractToken from './middleware/extract-token.js';
60
import getPermissions from './middleware/get-permissions.js';
61
import rateLimiterGlobal from './middleware/rate-limiter-global.js';
62
import rateLimiter from './middleware/rate-limiter-ip.js';
63
import sanitizeQuery from './middleware/sanitize-query.js';
64
import schema from './middleware/schema.js';
65
import { initTelemetry } from './telemetry/index.js';
66
import { getConfigFromEnv } from './utils/get-config-from-env.js';
67
import { Url } from './utils/url.js';
68
import { validateStorage } from './utils/validate-storage.js';
69

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

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

77
	await validateDatabaseConnection();
78

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

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

88
	if (!env['SECRET']) {
89
		logger.warn(
90
			`"SECRET" env variable is missing. Using a random value instead. Tokens will not persist between restarts. This is not appropriate for production usage.`,
91
		);
92
	}
93

94
	if (!new Url(env['PUBLIC_URL'] as string).isAbsolute()) {
95
		logger.warn('"PUBLIC_URL" should be a full URL');
96
	}
97

98
	await validateDatabaseExtensions();
99
	await validateStorage();
100

101
	await registerAuthProviders();
102

103
	const extensionManager = getExtensionManager();
104
	const flowManager = getFlowManager();
105

106
	await extensionManager.initialize();
107
	await flowManager.initialize();
108

109
	const app = express();
110

111
	app.disable('x-powered-by');
112
	app.set('trust proxy', env['IP_TRUST_PROXY']);
113
	app.set('query parser', (str: string) => qs.parse(str, { depth: 10 }));
114

115
	if (env['PRESSURE_LIMITER_ENABLED']) {
116
		const sampleInterval = Number(env['PRESSURE_LIMITER_SAMPLE_INTERVAL']);
117

118
		if (Number.isNaN(sampleInterval) === true || Number.isFinite(sampleInterval) === false) {
119
			throw new Error(`Invalid value for PRESSURE_LIMITER_SAMPLE_INTERVAL environment variable`);
120
		}
121

122
		app.use(
123
			handlePressure({
124
				sampleInterval,
125
				maxEventLoopUtilization: env['PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION'] as number,
126
				maxEventLoopDelay: env['PRESSURE_LIMITER_MAX_EVENT_LOOP_DELAY'] as number,
127
				maxMemoryRss: env['PRESSURE_LIMITER_MAX_MEMORY_RSS'] as number,
128
				maxMemoryHeapUsed: env['PRESSURE_LIMITER_MAX_MEMORY_HEAP_USED'] as number,
129
				error: new ServiceUnavailableError({ service: 'api', reason: 'Under pressure' }),
130
				retryAfter: env['PRESSURE_LIMITER_RETRY_AFTER'] as string,
131
			}),
132
		);
133
	}
134

135
	app.use(
136
		helmet.contentSecurityPolicy(
137
			merge(
138
				{
139
					useDefaults: true,
140
					directives: {
141
						// Unsafe-eval is required for app extensions
142
						scriptSrc: ["'self'", "'unsafe-eval'"],
143

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

149
						// These are required for MapLibre
150
						workerSrc: ["'self'", 'blob:'],
151
						childSrc: ["'self'", 'blob:'],
152
						imgSrc: [
153
							"'self'",
154
							'data:',
155
							'blob:',
156
							'https://raw.githubusercontent.com',
157
							'https://avatars.githubusercontent.com',
158
						],
159
						mediaSrc: ["'self'"],
160
						connectSrc: ["'self'", 'https://*'],
161
					},
162
				},
163
				getConfigFromEnv('CONTENT_SECURITY_POLICY_'),
164
			),
165
		),
166
	);
167

168
	if (env['HSTS_ENABLED']) {
169
		app.use(helmet.hsts(getConfigFromEnv('HSTS_', ['HSTS_ENABLED'])));
170
	}
171

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

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

176
	app.use(createExpressLogger());
177

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

183
	if (env['CORS_ENABLED'] === true) {
184
		app.use(cors);
185
	}
186

187
	app.use((req, res, next) => {
188
		(
189
			express.json({
190
				limit: env['MAX_PAYLOAD_SIZE'] as string,
191
			}) as RequestHandler
192
		)(req, res, (err: any) => {
193
			if (err) {
194
				return next(new InvalidPayloadError({ reason: err.message }));
195
			}
196

197
			return next();
198
		});
199
	});
200

201
	app.use(cookieParser());
202

203
	app.use(extractToken);
204

205
	app.get('/', (_req, res, next) => {
206
		if (env['ROOT_REDIRECT']) {
207
			res.redirect(env['ROOT_REDIRECT'] as string);
208
		} else {
209
			next();
210
		}
211
	});
212

213
	app.get('/robots.txt', (_, res) => {
214
		res.set('Content-Type', 'text/plain');
215
		res.status(200);
216
		res.send(env['ROBOTS_TXT']);
217
	});
218

219
	if (env['SERVE_APP']) {
220
		const adminPath = require.resolve('@directus/app');
221
		const adminUrl = new Url(env['PUBLIC_URL'] as string).addPath('admin');
222

223
		const embeds = extensionManager.getEmbeds();
224

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

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

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

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

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

249
	// use the rate limiter - all routes for now
250
	if (env['RATE_LIMITER_GLOBAL_ENABLED'] === true) {
251
		app.use(rateLimiterGlobal);
252
	}
253

254
	if (env['RATE_LIMITER_ENABLED'] === true) {
255
		app.use(rateLimiter);
256
	}
257

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

260
	app.use(authenticate);
261

262
	app.use(checkIP);
263

264
	app.use(sanitizeQuery);
265

266
	app.use(cache);
267

268
	app.use(schema);
269

270
	app.use(getPermissions);
271

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

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

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

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

280
	app.use('/activity', activityRouter);
281
	app.use('/assets', assetsRouter);
282
	app.use('/collections', collectionsRouter);
283
	app.use('/dashboards', dashboardsRouter);
284
	app.use('/extensions', extensionsRouter);
285
	app.use('/fields', fieldsRouter);
286
	app.use('/files', filesRouter);
287
	app.use('/flows', flowsRouter);
288
	app.use('/folders', foldersRouter);
289
	app.use('/items', itemsRouter);
290
	app.use('/notifications', notificationsRouter);
291
	app.use('/operations', operationsRouter);
292
	app.use('/panels', panelsRouter);
293
	app.use('/permissions', permissionsRouter);
294
	app.use('/presets', presetsRouter);
295
	app.use('/translations', translationsRouter);
296
	app.use('/relations', relationsRouter);
297
	app.use('/revisions', revisionsRouter);
298
	app.use('/roles', rolesRouter);
299
	app.use('/schema', schemaRouter);
300
	app.use('/server', serverRouter);
301
	app.use('/settings', settingsRouter);
302
	app.use('/shares', sharesRouter);
303
	app.use('/users', usersRouter);
304
	app.use('/utils', utilsRouter);
305
	app.use('/versions', versionsRouter);
306
	app.use('/webhooks', webhooksRouter);
307

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

313
	app.use(notFoundHandler);
314
	app.use(errorHandler);
315

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

318
	initTelemetry();
319

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

322
	return app;
323
}
324

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.