directus
321 строка · 7.1 Кб
1import { useEnv } from '@directus/env';
2import { ErrorCode, ForbiddenError, isDirectusError, RouteNotFoundError } from '@directus/errors';
3import { EXTENSION_TYPES } from '@directus/extensions';
4import {
5account,
6describe,
7list,
8type AccountOptions,
9type DescribeOptions,
10type ListOptions,
11type ListQuery,
12} from '@directus/extensions-registry';
13import type { FieldFilter } from '@directus/types';
14import { isIn } from '@directus/utils';
15import express from 'express';
16import { isNil } from 'lodash-es';
17import { UUID_REGEX } from '../constants.js';
18import { getExtensionManager } from '../extensions/index.js';
19import { respond } from '../middleware/respond.js';
20import useCollection from '../middleware/use-collection.js';
21import { ExtensionReadError, ExtensionsService } from '../services/extensions.js';
22import asyncHandler from '../utils/async-handler.js';
23import { getCacheControlHeader } from '../utils/get-cache-headers.js';
24import { getMilliseconds } from '../utils/get-milliseconds.js';
25
26const router = express.Router();
27const env = useEnv();
28
29router.use(useCollection('directus_extensions'));
30
31router.get(
32'/',
33asyncHandler(async (req, res, next) => {
34const service = new ExtensionsService({
35accountability: req.accountability,
36schema: req.schema,
37});
38
39const extensions = await service.readAll();
40res.locals['payload'] = { data: extensions || null };
41return next();
42}),
43respond,
44);
45
46router.get(
47'/registry',
48asyncHandler(async (req, res, next) => {
49if (req.accountability && req.accountability.admin !== true) {
50throw new ForbiddenError();
51}
52
53const { search, limit, offset, sort, filter } = req.sanitizedQuery;
54
55const query: ListQuery = {};
56
57if (!isNil(search)) {
58query.search = search;
59}
60
61if (!isNil(limit)) {
62query.limit = limit;
63}
64
65if (!isNil(offset)) {
66query.offset = offset;
67}
68
69if (filter) {
70const getFilterValue = (key: string) => {
71const field = (filter as FieldFilter)[key];
72if (!field || !('_eq' in field) || typeof field._eq !== 'string') return;
73return field._eq;
74};
75
76const by = getFilterValue('by');
77const type = getFilterValue('type');
78
79if (by) {
80query.by = by;
81}
82
83if (type) {
84if (isIn(type, EXTENSION_TYPES) === false) {
85throw new ForbiddenError();
86}
87
88query.type = type;
89}
90}
91
92if (!isNil(sort) && sort[0] && isIn(sort[0], ['popular', 'recent', 'downloads'] as const)) {
93query.sort = sort[0];
94}
95
96if (env['MARKETPLACE_TRUST'] === 'sandbox') {
97query.sandbox = true;
98}
99
100const options: ListOptions = {};
101
102if (env['MARKETPLACE_REGISTRY'] && typeof env['MARKETPLACE_REGISTRY'] === 'string') {
103options.registry = env['MARKETPLACE_REGISTRY'];
104}
105
106const payload = await list(query, options);
107
108res.locals['payload'] = payload;
109return next();
110}),
111respond,
112);
113
114router.get(
115`/registry/account/:pk(${UUID_REGEX})`,
116asyncHandler(async (req, res, next) => {
117if (typeof req.params['pk'] !== 'string') {
118throw new ForbiddenError();
119}
120
121const options: AccountOptions = {};
122
123if (env['MARKETPLACE_REGISTRY'] && typeof env['MARKETPLACE_REGISTRY'] === 'string') {
124options.registry = env['MARKETPLACE_REGISTRY'];
125}
126
127const payload = await account(req.params['pk'], options);
128
129res.locals['payload'] = payload;
130return next();
131}),
132respond,
133);
134
135router.get(
136`/registry/extension/:pk(${UUID_REGEX})`,
137asyncHandler(async (req, res, next) => {
138if (typeof req.params['pk'] !== 'string') {
139throw new ForbiddenError();
140}
141
142const options: DescribeOptions = {};
143
144if (env['MARKETPLACE_REGISTRY'] && typeof env['MARKETPLACE_REGISTRY'] === 'string') {
145options.registry = env['MARKETPLACE_REGISTRY'];
146}
147
148const payload = await describe(req.params['pk'], options);
149
150res.locals['payload'] = payload;
151return next();
152}),
153respond,
154);
155
156router.post(
157'/registry/install',
158asyncHandler(async (req, _res, next) => {
159if (req.accountability && req.accountability.admin !== true) {
160throw new ForbiddenError();
161}
162
163const { version, extension } = req.body;
164
165if (!version || !extension) {
166throw new ForbiddenError();
167}
168
169const service = new ExtensionsService({
170accountability: req.accountability,
171schema: req.schema,
172});
173
174await service.install(extension, version);
175return next();
176}),
177respond,
178);
179
180router.post(
181'/registry/reinstall',
182asyncHandler(async (req, _res, next) => {
183if (req.accountability && req.accountability.admin !== true) {
184throw new ForbiddenError();
185}
186
187const { extension } = req.body;
188
189if (!extension) {
190throw new ForbiddenError();
191}
192
193const service = new ExtensionsService({
194accountability: req.accountability,
195schema: req.schema,
196});
197
198await service.reinstall(extension);
199return next();
200}),
201respond,
202);
203
204router.delete(
205`/registry/uninstall/:pk(${UUID_REGEX})`,
206asyncHandler(async (req, _res, next) => {
207if (req.accountability && req.accountability.admin !== true) {
208throw new ForbiddenError();
209}
210
211const pk = req.params['pk'];
212
213if (typeof pk !== 'string') {
214throw new ForbiddenError();
215}
216
217const service = new ExtensionsService({
218accountability: req.accountability,
219schema: req.schema,
220});
221
222await service.uninstall(pk);
223return next();
224}),
225respond,
226);
227
228router.patch(
229`/:pk(${UUID_REGEX})`,
230asyncHandler(async (req, res, next) => {
231if (req.accountability && req.accountability.admin !== true) {
232throw new ForbiddenError();
233}
234
235if (typeof req.params['pk'] !== 'string') {
236throw new ForbiddenError();
237}
238
239const service = new ExtensionsService({
240accountability: req.accountability,
241schema: req.schema,
242});
243
244try {
245const result = await service.updateOne(req.params['pk'], req.body);
246res.locals['payload'] = { data: result || null };
247} catch (error) {
248let finalError = error;
249
250if (error instanceof ExtensionReadError) {
251finalError = error.originalError;
252
253if (isDirectusError(finalError, ErrorCode.Forbidden)) {
254return next();
255}
256}
257
258throw finalError;
259}
260
261return next();
262}),
263respond,
264);
265
266router.delete(
267`/:pk(${UUID_REGEX})`,
268asyncHandler(async (req, _res, next) => {
269if (req.accountability && req.accountability.admin !== true) {
270throw new ForbiddenError();
271}
272
273const service = new ExtensionsService({
274accountability: req.accountability,
275schema: req.schema,
276});
277
278const pk = req.params['pk'];
279
280if (typeof pk !== 'string') {
281throw new ForbiddenError();
282}
283
284await service.deleteOne(pk);
285
286return next();
287}),
288respond,
289);
290
291router.get(
292'/sources/:chunk',
293asyncHandler(async (req, res) => {
294const chunk = req.params['chunk'] as string;
295const extensionManager = getExtensionManager();
296
297let source: string | null;
298
299if (chunk === 'index.js') {
300source = extensionManager.getAppExtensionsBundle();
301} else {
302source = extensionManager.getAppExtensionChunk(chunk);
303}
304
305if (source === null) {
306throw new RouteNotFoundError({ path: req.path });
307}
308
309res.setHeader('Content-Type', 'application/javascript; charset=UTF-8');
310
311res.setHeader(
312'Cache-Control',
313getCacheControlHeader(req, getMilliseconds(env['EXTENSIONS_CACHE_TTL']), false, false),
314);
315
316res.setHeader('Vary', 'Origin, Cache-Control');
317res.end(source);
318}),
319);
320
321export default router;
322