adminforth 1.1.107 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -4,12 +4,11 @@ import MongoConnector from './dataConnectors/mongo.js';
4
4
  import PostgresConnector from './dataConnectors/postgres.js';
5
5
  import SQLiteConnector from './dataConnectors/sqlite.js';
6
6
  import CodeInjector from './modules/codeInjector.js';
7
- import { guessLabelFromName } from './modules/utils.js';
8
7
  import ExpressServer from './servers/express.js';
9
8
  import {v1 as uuid} from 'uuid';
10
9
  import fs from 'fs';
11
10
  import { ADMINFORTH_VERSION, listify } from './modules/utils.js';
12
- import { AdminForthConfig, IAdminForth, AdminForthComponentDeclaration, AdminForthComponentDeclarationFull,
11
+ import { AdminForthConfig, IAdminForth, IConfigValidator, AdminForthComponentDeclaration, AdminForthComponentDeclarationFull,
13
12
  AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages, IHttpServer,
14
13
  BeforeSaveFunction,
15
14
  AfterSaveFunction,
@@ -24,6 +23,8 @@ import { AdminForthConfig, IAdminForth, AdminForthComponentDeclaration, AdminFor
24
23
  } from './types/AdminForthConfig.js';
25
24
  import path from 'path';
26
25
  import AdminForthPlugin from './basePlugin.js';
26
+ import ConfigValidator from './modules/configValidator.js';
27
+ import AdminForthRestAPI from './modules/restApi.js';
27
28
 
28
29
 
29
30
  //get array from enum AdminForthResourcePages
@@ -52,26 +53,32 @@ class AdminForth implements IAdminForth {
52
53
  connectorClasses: any;
53
54
  runningHotReload: boolean;
54
55
  activatedPlugins: Array<AdminForthPlugin>;
56
+ configValidator: IConfigValidator;
57
+ restApi: AdminForthRestAPI;
55
58
 
56
59
  baseUrlSlashed: string;
57
60
 
58
61
  statuses: {
59
- dbDiscover?: 'running' | 'done',
62
+ dbDiscover: 'running' | 'done',
60
63
  }
61
64
 
62
65
  constructor(config: AdminForthConfig) {
63
66
  this.config = {...this.#defaultConfig,...config};
64
67
  this.codeInjector = new CodeInjector(this);
68
+ this.configValidator = new ConfigValidator(this, this.config);
69
+ this.restApi = new AdminForthRestAPI(this);
65
70
  this.activatedPlugins = [];
66
71
 
67
- this.validateConfig();
72
+ this.configValidator.validateConfig();
68
73
  this.activatePlugins();
69
- this.validateConfig(); // revalidate after plugins
74
+ this.configValidator.validateConfig(); // revalidate after plugins
70
75
 
71
76
  this.express = new ExpressServer(this);
72
77
  this.auth = new AdminForthAuth(this);
73
78
  this.connectors = {};
74
- this.statuses = {};
79
+ this.statuses = {
80
+ dbDiscover: 'running',
81
+ };
75
82
 
76
83
  console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`)
77
84
  }
@@ -85,442 +92,6 @@ class AdminForth implements IAdminForth {
85
92
  };
86
93
  }
87
94
 
88
- checkCustomFileExists(filePath: string): Array<string> {
89
- if (filePath.startsWith('@@/')) {
90
- const checkPath = path.join(this.config.customization.customComponentsDir, filePath.replace('@@/', ''));
91
- if (!fs.existsSync(checkPath)) {
92
- return [`File file ${filePath} does not exist in ${this.config.customization.customComponentsDir}`];
93
- }
94
- }
95
- return [];
96
- }
97
-
98
- validateComponent(component: AdminForthComponentDeclaration, errors: Array<string>, ignoreExistsCheck: boolean = false): AdminForthComponentDeclaration {
99
- if (!component) {
100
- return component;
101
- }
102
- let obj: AdminForthComponentDeclarationFull;
103
- if (typeof component === 'string') {
104
- obj = { file: component, meta: {} };
105
- } else {
106
- obj = component;
107
- }
108
- if (!ignoreExistsCheck) {
109
- errors.push(...this.checkCustomFileExists(obj.file));
110
- }
111
-
112
- return obj;
113
- }
114
-
115
-
116
- validateConfig() {
117
- const errors = [];
118
-
119
- if (this.config.rootUser) {
120
- if (!this.config.rootUser.username) {
121
- throw new Error('rootUser.username is required');
122
- }
123
- if (!this.config.rootUser.password) {
124
- throw new Error('rootUser.password is required');
125
- }
126
-
127
- console.log('\n ⚠️⚠️⚠️ [INSECURE ALERT] config.rootUser is set, please create a new user and remove config.rootUser from config ASAP when you are in production\n');
128
- }
129
-
130
- if (!this.config.customization.customComponentsDir) {
131
- this.config.customization.customComponentsDir = './custom';
132
- }
133
-
134
- try {
135
- // check customComponentsDir exists
136
- fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
137
- } catch (e) {
138
- this.config.customization.customComponentsDir = undefined;
139
- }
140
-
141
- if (!this.config.auth) {
142
- if (!this.config.auth.resourceId) {
143
- throw new Error('No config.auth.resourceId defined');
144
- }
145
- if (!this.config.auth.passwordHashField) {
146
- throw new Error('No config.auth.passwordHashField defined');
147
- }
148
- if (!this.config.auth.usernameField) {
149
- throw new Error('No config.auth.usernameField defined');
150
- }
151
- if (this.config.auth.loginBackgroundImage) {
152
- errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
153
- }
154
- const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
155
- if (!userResource) {
156
- throw new Error(`Resource with id "${this.config.auth.resourceId}" not found`);
157
- }
158
-
159
- if (!this.config.auth.beforeLoginConfirmation) {
160
- this.config.auth.beforeLoginConfirmation = [];
161
- }
162
- }
163
-
164
- if (!this.config.customization) {
165
- this.config.customization = {};
166
- }
167
-
168
- if (!this.config.customization.customComponentsDir) {
169
- this.config.customization.customComponentsDir = './custom';
170
- }
171
-
172
- try {
173
- // check customComponentsDir exists
174
- fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
175
- } catch (e) {
176
- this.config.customization.customComponentsDir = undefined;
177
- }
178
-
179
- if (this.config.customization.customPages) {
180
- this.config.customization.customPages.forEach((page, i) => {
181
- // validate component if it's not plugin injection
182
- if (this.codeInjector.allComponentNames.hasOwnProperty(page.component as PropertyKey)) {
183
- const validatedPage = this.validateComponent(page.component, errors, true);
184
- }
185
- });
186
- } else {
187
- this.config.customization.customPages = [];
188
- }
189
- if (!this.config.baseUrl) {
190
- this.config.baseUrl = '';
191
- }
192
- if (!this.config.baseUrl.endsWith('/')) {
193
- this.baseUrlSlashed = this.config.baseUrl + '/';
194
- } else {
195
- this.baseUrlSlashed = this.config.baseUrl;
196
- }
197
- if (this.config?.customization.brandName === undefined) {
198
- this.config.customization.brandName = 'AdminForth';
199
- }
200
- if (this.config.customization.brandLogo) {
201
- errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
202
- }
203
-
204
- if (this.config.customization.favicon) {
205
- errors.push(...this.checkCustomFileExists(this.config.customization.favicon));
206
- }
207
-
208
- if (!this.config.customization.datesFormat) {
209
- this.config.customization.datesFormat = 'MMM D, YYYY HH:mm:ss';
210
- }
211
-
212
- if (this.config.resources) {
213
- this.config.resources.forEach((res) => {
214
- if (!res.table) {
215
- errors.push(`Resource "${res.dataSource}" is missing table`);
216
- }
217
- // if recordLabel is not callable, throw error
218
- if (res.recordLabel && typeof res.recordLabel !== 'function') {
219
- errors.push(`Resource "${res.dataSource}" recordLabel is not a function`);
220
- }
221
- if (!res.recordLabel) {
222
- res.recordLabel = (item) => {
223
- const pkVal = item[res.columns.find((col) => col.primaryKey).name];
224
- return `${res.label} ${pkVal}`;
225
- }
226
- }
227
-
228
-
229
- res.resourceId = res.resourceId || res.table;
230
- res.label = res.label || res.table.charAt(0).toUpperCase() + res.table.slice(1);
231
- if (!res.dataSource) {
232
- errors.push(`Resource "${res.resourceId}" is missing dataSource`);
233
- }
234
- if (!res.columns) {
235
- res.columns = [];
236
- }
237
- res.columns.forEach((col) => {
238
- col.label = col.label || guessLabelFromName(col.name);
239
- //define default sortable
240
- if (!Object.keys(col).includes('sortable')) {col.sortable = true;}
241
- if (col.showIn && !Array.isArray(col.showIn)) {
242
- errors.push(`Resource "${res.resourceId}" column "${col.name}" showIn must be an array`);
243
- }
244
-
245
- // check col.required is string or object
246
- if (col.required && !((typeof col.required === 'boolean') || (typeof col.required === 'object'))) {
247
- errors.push(`Resource "${res.resourceId}" column "${col.name}" required must be a string or object`);
248
- }
249
-
250
- // if it is object check the keys are one of ['create', 'edit']
251
- if (typeof col.required === 'object') {
252
- const wrongRequiredOn = Object.keys(col.required).find((c) => !['create', 'edit'].includes(c));
253
- if (wrongRequiredOn) {
254
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid required value "${wrongRequiredOn}", allowed keys are 'create', 'edit']`);
255
- }
256
- }
257
-
258
- // same for editingNote
259
- if (col.editingNote && !((typeof col.editingNote === 'string') || (typeof col.editingNote === 'object'))) {
260
- errors.push(`Resource "${res.resourceId}" column "${col.name}" editingNote must be a string or object`);
261
- }
262
- if (typeof col.editingNote === 'object') {
263
- const wrongEditingNoteOn = Object.keys(col.editingNote).find((c) => !['create', 'edit'].includes(c));
264
- if (wrongEditingNoteOn) {
265
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid editingNote value "${wrongEditingNoteOn}", allowed keys are 'create', 'edit']`);
266
- }
267
- }
268
-
269
- const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
270
- if (wrongShowIn) {
271
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
272
- }
273
- col.showIn = col.showIn || Object.values(AdminForthResourcePages);
274
-
275
- if (col.foreignResource) {
276
- const befHook = col.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest;
277
- if (befHook) {
278
- if (!Array.isArray(befHook)) {
279
- col.foreignResource.hooks.dropdownList.beforeDatasourceRequest = [befHook];
280
- }
281
- }
282
- const aftHook = col.foreignResource.hooks?.dropdownList?.afterDatasourceResponse;
283
- if (aftHook) {
284
- if (!Array.isArray(aftHook)) {
285
- col.foreignResource.hooks.dropdownList.afterDatasourceResponse = [aftHook];
286
- }
287
- }
288
- }
289
- })
290
-
291
- if (!res.options) {
292
- res.options = {bulkActions: [], allowedActions: {}};
293
- }
294
-
295
- if (!res.options.allowedActions) {
296
- res.options.allowedActions = {
297
- all: true,
298
- };
299
- }
300
-
301
-
302
- if (Object.keys(res.options.allowedActions).includes('all')) {
303
- if (Object.keys(res.options.allowedActions).length > 1) {
304
- errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
305
- }
306
- for (const key of Object.keys(AllowedActionsEnum)) {
307
- if (key !== 'all') {
308
- res.options.allowedActions[key] = res.options.allowedActions.all;
309
- }
310
- }
311
- delete res.options.allowedActions.all;
312
- } else {
313
- // by default allow all actions
314
- for (const key of Object.keys(AllowedActionsEnum)) {
315
- if (!Object.keys(res.options.allowedActions).includes(key)) {
316
- res.options.allowedActions[key] = true;
317
- }
318
- }
319
- }
320
-
321
-
322
- //check if resource has bulkActions
323
- let bulkActions = res?.options?.bulkActions || [];
324
-
325
- if (!Array.isArray(bulkActions)) {
326
- errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
327
- bulkActions = [];
328
- }
329
-
330
- if(res.options?.allowedActions?.delete && !bulkActions.find((action) => action.label === 'Delete checked')){
331
- bulkActions.push({
332
- label: `Delete checked`,
333
- state: 'danger',
334
- icon: 'flowbite:trash-bin-outline',
335
- action: async ({selectedIds}) => {
336
- const connector = this.connectors[res.dataSource];
337
- await Promise.all(selectedIds.map(async (recordId) => {
338
- await connector.deleteRecord({ resource: res, recordId });
339
- }));
340
- }
341
- });
342
- }
343
-
344
- const newBulkActions = bulkActions.map((action) => {
345
- return Object.assign(action, {id: uuid()});
346
- });
347
- res.options.bulkActions = newBulkActions;
348
-
349
- // if pageInjection is a string, make array with one element. Also check file exists
350
- const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
351
- if(res.options.pageInjections) {
352
- Object.entries(res.options.pageInjections).map(([key, value]) => {
353
- Object.entries(value).map(([injection, target]) => {
354
- if (possibleInjections.includes(injection)) {
355
- if (!Array.isArray(res.options.pageInjections[key][injection])) {
356
- // not array
357
- res.options.pageInjections[key][injection] = [target];
358
- }
359
- res.options.pageInjections[key][injection].forEach((target, i) => {
360
- res.options.pageInjections[key][injection][i] = this.validateComponent(target, errors);
361
- });
362
- } else {
363
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')}`);
364
- }
365
- });
366
-
367
- })
368
- }
369
-
370
- // transform all hooks Functions to array of functions
371
- if (!res.hooks) {
372
- res.hooks = {};
373
- }
374
- for (const hookName of ['show', 'list']) {
375
- if (!res.hooks[hookName]) {
376
- res.hooks[hookName] = {};
377
- }
378
- if (!res.hooks[hookName].beforeDatasourceRequest) {
379
- res.hooks[hookName].beforeDatasourceRequest = [];
380
- }
381
-
382
- if (!Array.isArray(res.hooks[hookName].beforeDatasourceRequest)) {
383
- res.hooks[hookName].beforeDatasourceRequest = [res.hooks[hookName].beforeDatasourceRequest];
384
- }
385
-
386
- if (!res.hooks[hookName].afterDatasourceResponse) {
387
- res.hooks[hookName].afterDatasourceResponse = [];
388
- }
389
-
390
- if (!Array.isArray(res.hooks[hookName].afterDatasourceResponse)) {
391
- res.hooks[hookName].afterDatasourceResponse = [res.hooks[hookName].afterDatasourceResponse];
392
- }
393
- }
394
- for (const hookName of ['create', 'edit', 'delete']) {
395
- if (!res.hooks[hookName]) {
396
- res.hooks[hookName] = {};
397
- }
398
- if (!res.hooks[hookName].beforeSave) {
399
- res.hooks[hookName].beforeSave = [];
400
- }
401
- if (!Array.isArray(res.hooks[hookName].beforeSave)) {
402
- res.hooks[hookName].beforeSave = [res.hooks[hookName].beforeSave];
403
- }
404
- if (!res.hooks[hookName].afterSave) {
405
- res.hooks[hookName].afterSave = [];
406
- }
407
- if (!Array.isArray(res.hooks[hookName].afterSave)) {
408
- res.hooks[hookName].afterSave = [res.hooks[hookName].afterSave];
409
- }
410
- }
411
- });
412
-
413
-
414
-
415
- if (!this.config.menu) {
416
- errors.push('No config.menu defined');
417
- }
418
-
419
- // check if there is only one homepage: true in menu, recursivly
420
- let homepages = 0;
421
- const browseMenu = (menu) => {
422
- menu.forEach((item) => {
423
- if (item.component && item.resourceId) {
424
- errors.push(`Menu item cannot have both component and resourceId: ${JSON.stringify(item)}`);
425
- }
426
- if (item.component && !item.path) {
427
- errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
428
- }
429
-
430
- if (item.type === 'resource' && !item.resourceId) {
431
- errors.push(`Menu item with type 'resource' must have resourceId : ${JSON.stringify(item)}`);
432
- }
433
-
434
- if (item.resourceId && !this.config.resources.find((res) => res.resourceId === item.resourceId)) {
435
- errors.push(`Menu item with type 'resourceId' has resourceId which is not in resources: ${JSON.stringify(item)}`);
436
- }
437
-
438
- if (item.type === 'component' && !item.component) {
439
- errors.push(`Menu item with type 'component' must have component : ${JSON.stringify(item)}`);
440
- }
441
-
442
- // make sure component starts with @@
443
- if (item.component) {
444
- if (!item.component.startsWith('@@')) {
445
- errors.push(`Menu item component must start with @@ : ${JSON.stringify(item)}`);
446
- }
447
-
448
- const path = item.component.replace('@@', this.config.customization.customComponentsDir);
449
- if ( !fs.existsSync(path) ) {
450
- errors.push(`Menu item component "${item.component.replace('@@', '')}" does not exist in "${this.config.customization.customComponentsDir}"`);
451
- }
452
- }
453
-
454
- if (item.homepage) {
455
- homepages++;
456
- if (homepages > 1) {
457
- errors.push('There must be only one homepage: true in menu, found second one in ' + JSON.stringify(item) );
458
- }
459
- }
460
- if (item.children) {
461
- browseMenu(item.children);
462
- }
463
- });
464
- };
465
- browseMenu(this.config.menu);
466
-
467
- }
468
-
469
- // check for duplicate resourceIds and show which ones are duplicated
470
- const resourceIds = this.config.resources.map((res) => res.resourceId);
471
- const uniqueResourceIds = new Set(resourceIds);
472
- if (uniqueResourceIds.size != resourceIds.length) {
473
- const duplicates = resourceIds.filter((item, index) => resourceIds.indexOf(item) != index);
474
- errors.push(`Duplicate fields "resourceId" or "table": ${duplicates.join(', ')}`);
475
- }
476
-
477
- //add ids for onSelectedAllActions for each resource
478
-
479
-
480
-
481
-
482
- if (errors.length > 0) {
483
- throw new Error(`Invalid AdminForth config: ${errors.join(', ')}`);
484
- }
485
-
486
- // check is all custom components files exists
487
- for (const resource of this.config.resources) {
488
- for (const column of resource.columns) {
489
- if (column.components) {
490
-
491
- for (const [key, comp] of Object.entries(column.components as Record<string, AdminForthComponentDeclarationFull>)) {
492
- let ignoreExistsCheck = false;
493
- if (this.codeInjector.allComponentNames[comp.file]) {
494
- // not obvious, but if we are in this if, it means that this is plugin component
495
- // and there is no sense to check if it exists in users folder
496
- ignoreExistsCheck = true;
497
- }
498
- column.components[key] = this.validateComponent(comp, errors, ignoreExistsCheck);
499
- }
500
- }
501
- }
502
- }
503
- }
504
-
505
- postProcessAfterDiscover(resource) {
506
- resource.columns.forEach((column) => {
507
- // if db/user says column is required in boolean, exapd
508
- if (typeof column.required === 'boolean') {
509
- column.required = { create: column.required, edit: column.required };
510
- }
511
-
512
- if (!column.required) {
513
- column.required = { create: false, edit: false };
514
- }
515
-
516
- // same for editingNote
517
- if (typeof column.editingNote === 'string') {
518
- column.editingNote = { create: column.editingNote, edit: column.editingNote };
519
- }
520
- })
521
- resource.dataSourceColumns = resource.columns.filter((col) => !col.virtual);
522
- }
523
-
524
95
  async discoverDatabases() {
525
96
  this.statuses.dbDiscover = 'running';
526
97
  this.connectorClasses = {
@@ -560,7 +131,7 @@ class AdminForth implements IAdminForth {
560
131
  res.columns[i] = { ...fieldTypes[col.name], ...col };
561
132
  });
562
133
 
563
- this.postProcessAfterDiscover(res);
134
+ this.configValidator.postProcessAfterDiscover(res);
564
135
 
565
136
  // check if primaryKey column is present
566
137
  if (!res.columns.some((col) => col.primaryKey)) {
@@ -575,7 +146,6 @@ class AdminForth implements IAdminForth {
575
146
  }
576
147
 
577
148
  async bundleNow({ hotReload=false, verbose=false }) {
578
-
579
149
  await this.codeInjector.bundleNow({ hotReload, verbose });
580
150
  }
581
151
 
@@ -605,7 +175,11 @@ class AdminForth implements IAdminForth {
605
175
  });
606
176
  }
607
177
  }
608
- if ((column.required as {create?: boolean, edit?: boolean}) ?.create && record[column.name] === undefined) {
178
+ if (
179
+ (column.required as {create?: boolean, edit?: boolean}) ?.create &&
180
+ record[column.name] === undefined &&
181
+ column.showIn.includes(AdminForthResourcePages.create)
182
+ ) {
609
183
  return { error: `Column '${column.name}' is required` };
610
184
  }
611
185
 
@@ -625,15 +199,15 @@ class AdminForth implements IAdminForth {
625
199
 
626
200
  // execute hook if needed
627
201
  for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
628
- const resp = await hook({ resource, record, adminUser });
629
- if (!resp || (!resp.ok && !resp.error)) {
630
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
631
- }
202
+ const resp = await hook({ resource, record, adminUser });
203
+ if (!resp || (!resp.ok && !resp.error)) {
204
+ throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
205
+ }
632
206
 
633
- if (resp.error) {
634
- return { error: resp.error };
635
- }
207
+ if (resp.error) {
208
+ return { error: resp.error };
636
209
  }
210
+ }
637
211
 
638
212
  // remove virtual columns from record
639
213
  for (const column of resource.columns.filter((col) => col.virtual)) {
@@ -642,6 +216,7 @@ class AdminForth implements IAdminForth {
642
216
  }
643
217
  }
644
218
  const connector = this.connectors[resource.dataSource];
219
+ process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record createResourceRecord', record);
645
220
  await connector.createRecord({ resource, record });
646
221
  // execute hook if needed
647
222
  for (const hook of listify(resource.hooks?.create?.afterSave as AfterSaveFunction[])) {
@@ -655,683 +230,12 @@ class AdminForth implements IAdminForth {
655
230
  return { error: resp.error };
656
231
  }
657
232
  }
658
- }
659
-
660
- setupEndpoints(server: IHttpServer) {
661
- server.endpoint({
662
- noAuth: true,
663
- method: 'POST',
664
- path: '/login',
665
- handler: async ({ body, response }) => {
666
-
667
- const INVALID_MESSAGE = 'Invalid username or password';
668
- const { username, password } = body;
669
- let adminUser: AdminUser;
670
- let toReturn: { ok: boolean, redirectTo?: string, allowedLogin:boolean } = { ok: true, allowedLogin:true};
671
233
 
672
- let token;
673
- if (username === this.config.rootUser.username && password === this.config.rootUser.password) {
674
- this.auth.setAuthCookie({ response, username, pk: null });
675
- adminUser = { isRoot: true, dbUser: null, pk: null, username: this.config.rootUser.username};
676
- } else {
677
- // get resource from db
678
- if (!this.config.auth) {
679
- throw new Error('No config.auth defined');
680
- }
681
- const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
682
- // if there is no passwordHashField, in columns, add it, with backendOnly and showIn: []
683
- if (!userResource.dataSourceColumns.find((col) => col.name === this.config.auth.passwordHashField)) {
684
- userResource.dataSourceColumns.push({
685
- name: this.config.auth.passwordHashField,
686
- backendOnly: true,
687
- showIn: [],
688
- type: AdminForth.Types.STRING,
689
- });
690
- console.log('Adding passwordHashField to userResource', userResource)
691
- }
692
-
693
- const userRecord = (
694
- await this.connectors[userResource.dataSource].getData({
695
- resource: userResource,
696
- filters: [
697
- { field: this.config.auth.usernameField, operator: AdminForthFilterOperators.EQ, value: username },
698
- ],
699
- limit: 1,
700
- offset: 0,
701
- sort: [],
702
- })
703
- ).data?.[0];
704
-
705
- if (!userRecord) {
706
- return { error: 'User not found' };
707
- }
708
-
709
- const passwordHash = userRecord[this.config.auth.passwordHashField];
710
- const valid = await AdminForthAuth.verifyPassword(password, passwordHash);
711
- if (valid) {
712
- adminUser = {
713
- isRoot: false, dbUser: userRecord,
714
- pk: userRecord[userResource.columns.find((col) => col.primaryKey).name],
715
- username,
716
- };
717
- const beforeLoginConfirmation = this.config.auth.beforeLoginConfirmation as (BeforeLoginConfirmationFunction[] | undefined);
718
- if (beforeLoginConfirmation?.length){
719
- for (const hook of beforeLoginConfirmation) {
720
- const resp = await hook({ adminUser, response });
721
-
722
- if (resp?.body?.redirectTo) {
723
- toReturn = {ok:resp.ok, redirectTo:resp?.body?.redirectTo, allowedLogin:resp?.body?.allowedLogin};
724
- break;
725
- }
726
- }
727
- }
728
- if (toReturn.allowedLogin){
729
- this.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
730
- }
731
- } else {
732
- return { error: INVALID_MESSAGE };
733
- }
734
-
735
- }
736
-
737
- return toReturn;
738
- }
739
- });
740
-
741
- server.endpoint({
742
- method: 'POST',
743
- path: '/check_auth',
744
- handler: async ({ adminUser }) => {
745
- return { ok: true };
746
- },
747
- });
748
-
749
- server.endpoint({
750
- noAuth: true,
751
- method: 'POST',
752
- path: '/logout',
753
- handler: async ({ response }) => {
754
- this.auth.removeAuthCookie( response );
755
- return { ok: true };
756
- },
757
- })
758
-
759
- server.endpoint({
760
- noAuth: true,
761
- method: 'GET',
762
- path: '/get_public_config',
763
- handler: async ({ body }) => {
764
-
765
- // find resource
766
- if (!this.config.auth) {
767
- throw new Error('No config.auth defined');
768
- }
769
- const usernameField = this.config.auth.usernameField;
770
- const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
771
- const usernameColumn = resource.columns.find((col) => col.name === usernameField);
772
-
773
- return {
774
- brandName: this.config.customization.brandName,
775
- usernameFieldName: usernameColumn.label,
776
- loginBackgroundImage: this.config.auth.loginBackgroundImage,
777
- title: this.config.customization?.title,
778
- };
779
- },
780
- });
781
-
782
- server.endpoint({
783
- method: 'GET',
784
- path: '/get_base_config',
785
- handler: async ({input, adminUser, cookies}) => {
786
- let username = ''
787
- let userFullName = ''
788
- if (adminUser.isRoot) {
789
- username = this.config.rootUser.username;
790
- } else {
791
- const dbUser = adminUser.dbUser;
792
- username = dbUser[this.config.auth.usernameField];
793
- userFullName =dbUser[this.config.auth.userFullNameField];
794
- }
795
-
796
- const userData = {
797
- [this.config.auth.usernameField]: username,
798
- [this.config.auth.userFullNameField]: userFullName
799
- };
800
- const checkIsMenuItemVisible = (menuItem) => {
801
- if (typeof menuItem.visible === 'function') {
802
- const toReturn = menuItem.visible( adminUser );
803
- if (typeof toReturn !== 'boolean') {
804
- throw new Error(`'visible' function of ${menuItem.label || menuItem.type } must return boolean value`);
805
- }
806
- return toReturn;
807
- }}
808
- let newMenu = []
809
- for (let menuItem of this.config.menu) {
810
- let newMenuItem = {...menuItem,}
811
- if (menuItem.visible){
812
- if (!checkIsMenuItemVisible(menuItem)){
813
- continue
814
- }
815
- }
816
- if (menuItem.children){
817
- let newChildren = []
818
- for (let child of menuItem.children){
819
- let newChild = {...child,}
820
- if (child.visible){
821
- if (!checkIsMenuItemVisible(child)){
822
- continue
823
- }
824
- }
825
- newChildren.push(newChild)
826
- }
827
- newMenuItem = {...newMenuItem, children: newChildren}
828
- }
829
- newMenu.push(newMenuItem)
830
- }
831
-
832
- return {
833
- user: userData,
834
- resources: this.config.resources.map((res) => ({
835
- resourceId: res.resourceId,
836
- label: res.label,
837
- })),
838
- menu: newMenu,
839
- config: {
840
- brandName: this.config.customization.brandName,
841
- brandLogo: this.config.customization.brandLogo,
842
- datesFormat: this.config.customization.datesFormat,
843
- deleteConfirmation: this.config.deleteConfirmation,
844
- auth: this.config.auth,
845
- usernameField: this.config.auth.usernameField,
846
- title: this.config.customization?.title,
847
- emptyFieldPlaceholder: this.config.customization?.emptyFieldPlaceholder,
848
- },
849
- adminUser,
850
- version: ADMINFORTH_VERSION,
851
- };
852
- },
853
- });
854
-
855
- async function interpretResource(adminUser: AdminUser, resource: AdminForthResource, meta: any, source: ActionCheckSource): Promise<{allowedActions: AllowedActions}> {
856
- if (process.env.HEAVY_DEBUG) {
857
- console.log('🪲Interpreting resource', resource.resourceId, source);
858
- }
859
- const allowedActions = {};
860
-
861
- await Promise.all(
862
- Object.entries(resource.options?.allowedActions || {}).map(
863
- async ([key, value]: [string, AllowedActionValue]) => {
864
- if (process.env.HEAVY_DEBUG) {
865
- console.log('🪲checking for allowed call', key, 'value:', value, 'typeof', typeof value);
866
- }
867
-
868
- // if callable then call
869
- if (typeof value === 'function') {
870
- allowedActions[key] = await value({ adminUser, resource, meta, source });
871
- } else {
872
- allowedActions[key] = value;
873
- }
874
- })
875
- );
876
-
877
- return { allowedActions };
878
- }
879
-
880
- function checkAccess(action: AllowedActionsEnum, allowedActions: AllowedActions): { allowed: boolean, error?: string } {
881
- const allowed = (allowedActions[action] as boolean | string | undefined);
882
- if (allowed !== true) {
883
- return { error: typeof allowed === 'string' ? allowed : 'Action is not allowed', allowed: false };
884
- }
885
- return { allowed: true };
886
- }
887
-
888
-
889
- server.endpoint({
890
- method: 'POST',
891
- path: '/get_resource',
892
- handler: async ({ body, adminUser }) => {
893
- const { resourceId } = body;
894
- if (!this.statuses.dbDiscover) {
895
- return { error: 'Database discovery not started' };
896
- }
897
- if (this.statuses.dbDiscover !== 'done') {
898
- return { error : 'Database discovery is still in progress, please try later' };
899
- }
900
- const resource = this.config.resources.find((res) => res.resourceId == resourceId);
901
- if (!resource) {
902
- return { error: `Resource ${resourceId} not found` };
903
- }
904
-
905
- const { allowedActions } = await interpretResource(adminUser, resource, {}, ActionCheckSource.DisplayButtons);
906
-
907
- // exclude "plugins" key
908
- return {
909
- resource: {
910
- ...resource,
911
- plugins: undefined,
912
- options: {
913
- ...resource.options,
914
- allowedActions,
915
- }
916
- }
917
- };
918
- },
919
- });
920
- server.endpoint({
921
- method: 'POST',
922
- path: '/get_resource_data',
923
- handler: async ({ body, adminUser }) => {
924
- const { resourceId, source } = body;
925
- if (['show', 'list'].includes(source) === false) {
926
- return { error: 'Invalid source, should be list or show' };
927
- }
928
- if (!this.statuses.dbDiscover) {
929
- return { error: 'Database discovery not started' };
930
- }
931
- if (this.statuses.dbDiscover !== 'done') {
932
- return { error : 'Database discovery is still in progress, please try later' };
933
- }
934
- const resource = this.config.resources.find((res) => res.resourceId == resourceId);
935
- if (!resource) {
936
- return { error: `Resource ${resourceId} not found` };
937
- }
938
-
939
- const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.DisplayButtons);
940
-
941
- const { allowed, error } = checkAccess(source as AllowedActionsEnum, allowedActions);
942
- if (!allowed) {
943
- return { error };
944
- }
945
-
946
- for (const hook of listify(resource.hooks?.[source]?.beforeDatasourceRequest)) {
947
- const resp = await hook({ resource, query: body, adminUser });
948
- if (!resp || (!resp.ok && !resp.error)) {
949
- throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
950
- }
951
-
952
- if (resp.error) {
953
- return { error: resp.error };
954
- }
955
- }
956
- const { limit, offset, filters, sort } = body;
957
-
958
-
959
-
960
- for (const filter of (filters || [])) {
961
- if (!Object.values(AdminForthFilterOperators).includes(filter.operator)) {
962
- throw new Error(`Operator '${filter.operator}' is not allowed`);
963
- }
964
-
965
- if (!resource.columns.some((col) => col.name === filter.field)) {
966
- throw new Error(`Field '${filter.field}' is not in resource '${resource.resourceId}'. Available fields: ${resource.columns.map((col) => col.name).join(', ')}`);
967
- }
968
-
969
- if (filter.operator === AdminForthFilterOperators.IN || filter.operator === AdminForthFilterOperators.NIN) {
970
- if (!Array.isArray(filter.value)) {
971
- throw new Error(`Value for operator '${filter.operator}' should be an array`);
972
- }
973
- }
974
-
975
- if (filter.operator === AdminForthFilterOperators.IN && filter.value.length === 0) {
976
- // nonsense
977
- return { data: [], total: 0 };
978
- }
979
- }
980
-
981
- const data = await this.connectors[resource.dataSource].getData({
982
- resource,
983
- limit,
984
- offset,
985
- filters,
986
- sort,
987
- });
988
- // for foreign keys, add references
989
- await Promise.all(
990
- resource.columns.filter((col) => col.foreignResource).map(async (col) => {
991
- const targetResource = this.config.resources.find((res) => res.resourceId == col.foreignResource.resourceId);
992
- const targetConnector = this.connectors[targetResource.dataSource];
993
- const targetResourcePkField = targetResource.columns.find((col) => col.primaryKey).name;
994
- const pksUnique = [...new Set(data.data.map((item) => item[col.name]))];
995
- if (pksUnique.length === 0) {
996
- return;
997
- }
998
- const targetData = await targetConnector.getData({
999
- resource: targetResource,
1000
- limit: limit,
1001
- offset: 0,
1002
- filters: [
1003
- {
1004
- field: targetResourcePkField,
1005
- operator: AdminForthFilterOperators.IN,
1006
- value: pksUnique,
1007
- }
1008
- ],
1009
- sort: [],
1010
- });
1011
- const targetDataMap = targetData.data.reduce((acc, item) => {
1012
- acc[item[targetResourcePkField]] = {
1013
- label: targetResource.recordLabel(item),
1014
- pk: item[targetResourcePkField],
1015
- }
1016
- return acc;
1017
- }, {});
1018
- data.data.forEach((item) => {
1019
- item[col.name] = targetDataMap[item[col.name]];
1020
- });
1021
- })
1022
- );
1023
-
1024
- for (const hook of listify(resource.hooks?.[source]?.afterDatasourceResponse)) {
1025
- const resp = await hook({ resource, response: data.data, adminUser });
1026
- if (!resp || (!resp.ok && !resp.error)) {
1027
- throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
1028
- }
1029
-
1030
- if (resp.error) {
1031
- return { error: resp.error };
1032
- }
1033
- }
1034
-
1035
- // remove all columns which are not defined in resources, or defined but backendOnly
1036
- data.data.forEach((item) => {
1037
- Object.keys(item).forEach((key) => {
1038
- if (!resource.columns.find((col) => col.name === key) || resource.columns.find((col) => col.name === key && col.backendOnly)) {
1039
- delete item[key];
1040
- }
1041
- })
1042
- });
1043
-
1044
- data.data.forEach((item) => {
1045
- item._label = resource.recordLabel(item);
1046
- });
1047
-
1048
- return {
1049
- ...data,
1050
- options: resource?.options,
1051
- };
1052
- },
1053
- });
1054
- server.endpoint({
1055
- method: 'POST',
1056
- path: '/get_resource_foreign_data',
1057
- handler: async ({ body, adminUser }) => {
1058
- const { resourceId, column } = body;
1059
- if (!this.statuses.dbDiscover) {
1060
- return { error: 'Database discovery not started' };
1061
- }
1062
- if (this.statuses.dbDiscover !== 'done') {
1063
- return { error : 'Database discovery is still in progress, please try later' };
1064
- }
1065
- const resource = this.config.resources.find((res) => res.resourceId == resourceId);
1066
- if (!resource) {
1067
- return { error: `Resource '${resourceId}' not found` };
1068
- }
1069
- const columnConfig = resource.columns.find((col) => col.name == column);
1070
- if (!columnConfig) {
1071
- return { error: `Column "${column}' not found in resource with resourceId '${resourceId}'` };
1072
- }
1073
- if (!columnConfig.foreignResource) {
1074
- return { error: `Column '${column}' in resource '${resourceId}' is not a foreign key` };
1075
- }
1076
- const targetResourceId = columnConfig.foreignResource.resourceId;
1077
- const targetResource = this.config.resources.find((res) => res.resourceId == targetResourceId);
1078
-
1079
- for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest as BeforeDataSourceRequestFunction[])) {
1080
- const resp = await hook({ query: body, adminUser, resource: targetResource });
1081
- if (!resp || (!resp.ok && !resp.error)) {
1082
- throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
1083
- }
1084
-
1085
- if (resp.error) {
1086
- return { error: resp.error };
1087
- }
1088
- }
1089
- const { limit, offset, filters, sort } = body;
1090
- const dbDataItems = await this.connectors[targetResource.dataSource].getData({
1091
- resource: targetResource,
1092
- limit,
1093
- offset,
1094
- filters: filters || [],
1095
- sort: sort || [],
1096
- });
1097
- const items = dbDataItems.data.map((item) => {
1098
- const pk = item[targetResource.columns.find((col) => col.primaryKey).name];
1099
- const labler = targetResource.recordLabel;
1100
- return {
1101
- value: pk,
1102
- label: labler(item),
1103
- _item: item, // user might need it in hook to form new label
1104
- }
1105
- });
1106
- const response = {
1107
- items
1108
- };
1109
-
1110
- for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.afterDatasourceResponse as AfterDataSourceResponseFunction[])) {
1111
- const resp = await hook({ response, adminUser, resource: targetResource });
1112
- if (!resp || (!resp.ok && !resp.error)) {
1113
- throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
1114
- }
1115
-
1116
- if (resp.error) {
1117
- return { error: resp.error };
1118
- }
1119
- }
1120
-
1121
- return response;
1122
- },
1123
- });
1124
-
1125
- server.endpoint({
1126
- method: 'POST',
1127
- path: '/get_min_max_for_columns',
1128
- handler: async ({ body }) => {
1129
- const { resourceId } = body;
1130
- if (!this.statuses.dbDiscover) {
1131
- return { error: 'Database discovery not started' };
1132
- }
1133
- if (this.statuses.dbDiscover !== 'done') {
1134
- return { error : 'Database discovery is still in progress, please try later' };
1135
- }
1136
- const resource = this.config.resources.find((res) => res.resourceId == resourceId);
1137
- if (!resource) {
1138
- return { error: `Resource '${resourceId}' not found` };
1139
- }
1140
- const item = await this.connectors[resource.dataSource].getMinMaxForColumns({
1141
- resource,
1142
- columns: resource.columns.filter((col) => [
1143
- AdminForthDataTypes.INTEGER,
1144
- AdminForthDataTypes.FLOAT,
1145
- AdminForthDataTypes.DATE,
1146
- AdminForthDataTypes.DATETIME,
1147
- AdminForthDataTypes.TIME,
1148
- AdminForthDataTypes.DECIMAL,
1149
- ].includes(col.type) && col.allowMinMaxQuery === true),
1150
- });
1151
- return item;
1152
- },
1153
- });
1154
- server.endpoint({
1155
- method: 'POST',
1156
- path: '/create_record',
1157
- handler: async ({ body, adminUser }) => {
1158
- const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
1159
- if (!resource) {
1160
- return { error: `Resource '${body['resourceId']}' not found` };
1161
- }
1162
- const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body}, ActionCheckSource.CreateRequest);
1163
-
1164
- const { allowed, error } = checkAccess(AllowedActionsEnum.create, allowedActions);
1165
- if (!allowed) {
1166
- return { error };
1167
- }
1168
-
1169
- const { record } = body;
1170
-
1171
- await this.createResourceRecord({ resource, record, adminUser });
1172
- const connector = this.connectors[resource.dataSource];
1173
-
1174
- return {
1175
- newRecordId: body['record'][connector.getPrimaryKey(resource)]
1176
- }
1177
- }
1178
- });
1179
- server.endpoint({
1180
- method: 'POST',
1181
- path: '/update_record',
1182
- handler: async ({ body, adminUser }) => {
1183
- const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
1184
- if (!resource) {
1185
- return { error: `Resource '${body['resourceId']}' not found` };
1186
- }
1187
-
1188
- const recordId = body['recordId'];
1189
- const connector = this.connectors[resource.dataSource];
1190
- const oldRecord = await connector.getRecordByPrimaryKey(resource, recordId)
1191
- if (!oldRecord) {
1192
- const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
1193
- return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
1194
- }
1195
- const record = body['record'];
1196
-
1197
- const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body, newRecord: record, oldRecord}, ActionCheckSource.EditRequest);
1198
-
1199
- const { allowed, error } = checkAccess(AllowedActionsEnum.edit, allowedActions);
1200
- if (!allowed) {
1201
- return { error };
1202
- }
1203
-
1204
- // execute hook if needed
1205
- for (const hook of listify(resource.hooks?.edit?.beforeSave as BeforeSaveFunction[])) {
1206
- const resp = await hook({ resource, record, adminUser });
1207
- if (!resp || (!resp.ok && !resp.error)) {
1208
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
1209
- }
1210
-
1211
- if (resp.error) {
1212
- return { error: resp.error };
1213
- }
1214
- }
1215
- const newValues = {};
1216
-
1217
- for (const recordField in record) {
1218
- if (record[recordField] !== oldRecord[recordField]) {
1219
- const column = resource.columns.find((col) => col.name === recordField);
1220
- if (column) {
1221
- if (!column.virtual) {
1222
- newValues[recordField] = connector.setFieldValue(column, record[recordField]);
1223
- }
1224
- } else {
1225
- newValues[recordField] = record[recordField];
1226
- }
1227
- }
1228
- }
1229
-
1230
- if (Object.keys(newValues).length > 0) {
1231
- await connector.updateRecord({ resource, recordId, newValues});
1232
- }
1233
-
1234
- // execute hook if needed
1235
- for (const hook of listify(resource.hooks?.edit?.afterSave as AfterSaveFunction[])) {
1236
- const resp = await hook({ resource, record, adminUser, oldRecord });
1237
- if (!resp || (!resp.ok && !resp.error)) {
1238
- throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
1239
- }
1240
-
1241
- if (resp.error) {
1242
- return { error: resp.error };
1243
- }
1244
- }
1245
-
1246
- return {
1247
- newRecordId: recordId
1248
- }
1249
- }
1250
- });
1251
- server.endpoint({
1252
- method: 'POST',
1253
- path: '/delete_record',
1254
- handler: async ({ body, adminUser }) => {
1255
- const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
1256
- const record = await this.connectors[resource.dataSource].getRecordByPrimaryKey(resource, body['primaryKey']);
1257
- if (!resource) {
1258
- return { error: `Resource '${body['resourceId']}' not found` };
1259
- }
1260
- if (!record){
1261
- return { error: `Record with ${body['primaryKey']} not found` };
1262
- }
1263
- if (resource.options.allowedActions.delete === false) {
1264
- return { error: `Resource '${resource.resourceId}' does not allow delete action` };
1265
- }
1266
-
1267
- const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.DeleteRequest);
1268
-
1269
- const { allowed, error } = checkAccess(AllowedActionsEnum.delete, allowedActions);
1270
- if (!allowed) {
1271
- return { error };
1272
- }
1273
-
1274
- // execute hook if needed
1275
- for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
1276
- const resp = await hook({ resource, record, adminUser });
1277
- if (!resp || (!resp.ok && !resp.error)) {
1278
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
1279
- }
1280
-
1281
- if (resp.error) {
1282
- return { error: resp.error };
1283
- }
1284
- }
1285
-
1286
- const connector = this.connectors[resource.dataSource];
1287
- await connector.deleteRecord({ resource, recordId: body['primaryKey']});
1288
-
1289
- // execute hook if needed
1290
- for (const hook of listify(resource.hooks?.delete?.afterSave as BeforeSaveFunction[])) {
1291
- const resp = await hook({ resource, record, adminUser });
1292
- if (!resp || (!resp.ok && !resp.error)) {
1293
- throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
1294
- }
1295
-
1296
- if (resp.error) {
1297
- return { error: resp.error };
1298
- }
1299
- }
1300
- return {
1301
- recordId: body['primaryKey']
1302
- }
1303
- }
1304
- });
1305
- server.endpoint({
1306
- method: 'POST',
1307
- path: '/start_bulk_action',
1308
- handler: async ({ body }) => {
1309
- const { resourceId, actionId, recordIds } = body;
1310
- const resource = this.config.resources.find((res) => res.resourceId == resourceId);
1311
- if (!resource) {
1312
- return { error: `Resource '${resourceId}' not found` };
1313
- }
1314
- const action = resource.options.bulkActions.find((act) => act.id == actionId);
1315
- if (!action) {
1316
- return { error: `Action '${actionId}' not found` };
1317
- } else{
1318
- await action.action({selectedIds:recordIds})
1319
-
1320
- }
1321
- return {
1322
- actionId,
1323
- recordIds,
1324
- resourceId,
1325
- status:'success'
1326
-
1327
- }
1328
- }
1329
- })
234
+ return { ok: true };
235
+ }
1330
236
 
1331
- // setup endpoints for all plugins
1332
- this.activatedPlugins.forEach((plugin) => {
1333
- plugin.setupEndpoints(server);
1334
- });
237
+ setupEndpoints(server: IHttpServer) {
238
+ this.restApi.registerEndpoints(server);
1335
239
  }
1336
240
  }
1337
241