@underpostnet/underpost 2.97.1 → 2.97.5

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.
Files changed (59) hide show
  1. package/README.md +2 -2
  2. package/cli.md +3 -1
  3. package/conf.js +2 -0
  4. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  5. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  6. package/package.json +1 -1
  7. package/src/api/core/core.service.js +0 -5
  8. package/src/api/default/default.service.js +7 -5
  9. package/src/api/document/document.model.js +1 -1
  10. package/src/api/document/document.router.js +5 -0
  11. package/src/api/document/document.service.js +105 -47
  12. package/src/api/file/file.model.js +112 -4
  13. package/src/api/file/file.ref.json +42 -0
  14. package/src/api/file/file.service.js +380 -32
  15. package/src/api/user/user.model.js +38 -1
  16. package/src/api/user/user.router.js +96 -63
  17. package/src/api/user/user.service.js +81 -48
  18. package/src/cli/db.js +424 -166
  19. package/src/cli/index.js +8 -0
  20. package/src/cli/repository.js +1 -1
  21. package/src/cli/run.js +1 -0
  22. package/src/cli/ssh.js +10 -10
  23. package/src/client/components/core/Account.js +327 -36
  24. package/src/client/components/core/AgGrid.js +3 -0
  25. package/src/client/components/core/Auth.js +9 -3
  26. package/src/client/components/core/Chat.js +2 -2
  27. package/src/client/components/core/Content.js +159 -78
  28. package/src/client/components/core/CssCore.js +16 -12
  29. package/src/client/components/core/FileExplorer.js +115 -8
  30. package/src/client/components/core/Input.js +204 -11
  31. package/src/client/components/core/LogIn.js +42 -20
  32. package/src/client/components/core/Modal.js +138 -24
  33. package/src/client/components/core/Panel.js +69 -31
  34. package/src/client/components/core/PanelForm.js +262 -77
  35. package/src/client/components/core/PublicProfile.js +888 -0
  36. package/src/client/components/core/Router.js +117 -15
  37. package/src/client/components/core/SearchBox.js +329 -13
  38. package/src/client/components/core/SignUp.js +26 -7
  39. package/src/client/components/core/SocketIo.js +6 -3
  40. package/src/client/components/core/Translate.js +98 -0
  41. package/src/client/components/core/Validator.js +15 -0
  42. package/src/client/components/core/windowGetDimensions.js +6 -6
  43. package/src/client/components/default/MenuDefault.js +59 -12
  44. package/src/client/components/default/RoutesDefault.js +1 -0
  45. package/src/client/services/core/core.service.js +163 -1
  46. package/src/client/services/default/default.management.js +451 -64
  47. package/src/client/services/default/default.service.js +13 -6
  48. package/src/client/services/file/file.service.js +43 -16
  49. package/src/client/services/user/user.service.js +13 -9
  50. package/src/db/DataBaseProvider.js +1 -1
  51. package/src/db/mongo/MongooseDB.js +1 -1
  52. package/src/index.js +1 -1
  53. package/src/mailer/MailerProvider.js +4 -4
  54. package/src/runtime/express/Express.js +2 -1
  55. package/src/runtime/lampp/Lampp.js +2 -2
  56. package/src/server/auth.js +3 -6
  57. package/src/server/data-query.js +449 -0
  58. package/src/server/object-layer.js +0 -3
  59. package/src/ws/IoInterface.js +2 -2
@@ -1,10 +1,110 @@
1
+ /**
2
+ * File service module for handling file uploads, retrieval, and deletion.
3
+ * Provides REST API handlers for file operations with MongoDB storage.
4
+ *
5
+ * @module src/api/file/file.service.js
6
+ * @namespace FileServiceServer
7
+ */
8
+
1
9
  import { DataBaseProvider } from '../../db/DataBaseProvider.js';
10
+ import { getBearerToken, jwtVerify } from '../../server/auth.js';
2
11
  import { loggerFactory } from '../../server/logger.js';
3
12
  import crypto from 'crypto';
13
+ import { Types } from 'mongoose';
4
14
 
15
+ /**
16
+ * Logger instance for this module.
17
+ * @type {Function}
18
+ * @memberof FileServiceServer
19
+ * @private
20
+ */
5
21
  const logger = loggerFactory(import.meta);
6
22
 
23
+ /**
24
+ * File Data Transfer Object (DTO) for the service layer.
25
+ * Provides API-specific transformation methods for file documents including
26
+ * metadata selection queries and response formatting for REST endpoints.
27
+ * @namespace FileServiceServer.FileServiceDto
28
+ * @memberof FileServiceServer
29
+ */
30
+ const FileServiceDto = {
31
+ /**
32
+ * Returns file metadata only (no buffer data).
33
+ * Used for list responses and API integration.
34
+ * @function toMetadata
35
+ * @memberof FileServiceServer.FileServiceDto
36
+ * @param {Object} file - File document from database.
37
+ * @returns {Object|null} File metadata object, or null if file is falsy.
38
+ */
39
+ toMetadata: (file) => {
40
+ if (!file) return null;
41
+ return {
42
+ _id: file._id,
43
+ name: file.name || '',
44
+ mimetype: file.mimetype || 'application/octet-stream',
45
+ size: file.size || 0,
46
+ md5: file.md5 || '',
47
+ cid: file.cid || '',
48
+ createdAt: file.createdAt,
49
+ updatedAt: file.updatedAt,
50
+ };
51
+ },
52
+
53
+ /**
54
+ * Transforms array of files to metadata only.
55
+ * @function toMetadataArray
56
+ * @memberof FileServiceServer.FileServiceDto
57
+ * @param {Array} files - Array of file documents.
58
+ * @returns {Array} Array of file metadata objects.
59
+ */
60
+ toMetadataArray: (files) => {
61
+ if (!Array.isArray(files)) return [];
62
+ return files.map((file) => FileServiceDto.toMetadata(file));
63
+ },
64
+
65
+ /**
66
+ * Ensures UTF-8 encoding for filenames.
67
+ * Fixes issues with special characters (e.g., ñ, é, ü).
68
+ * @function normalizeFilename
69
+ * @memberof FileServiceServer.FileServiceDto
70
+ * @param {string} filename - Raw filename from upload.
71
+ * @returns {string} UTF-8 encoded filename.
72
+ */
73
+ normalizeFilename: (filename) => {
74
+ if (!filename) return '';
75
+ // Ensure string and normalize to UTF-8
76
+ let normalized = String(filename);
77
+ // Replace any incorrectly encoded sequences
78
+ normalized = Buffer.from(normalized, 'utf8').toString('utf8');
79
+ return normalized;
80
+ },
81
+
82
+ /**
83
+ * Get select fields for metadata-only queries.
84
+ * Excludes the 'data' buffer field.
85
+ * @function metadataSelect
86
+ * @memberof FileServiceServer.FileServiceDto
87
+ * @returns {string} Space-separated list of field names for metadata selection.
88
+ */
89
+ metadataSelect: () => {
90
+ return '_id name mimetype size encoding md5 cid createdAt updatedAt';
91
+ },
92
+ };
93
+
94
+ /**
95
+ * File Factory for file extraction, upload, and creation utilities.
96
+ * @namespace FileServiceServer.FileFactory
97
+ * @memberof FileServiceServer
98
+ */
7
99
  const FileFactory = {
100
+ /**
101
+ * Extract files from request.
102
+ * Handles both standard 'file' field and custom fields.
103
+ * @function filesExtract
104
+ * @memberof FileServiceServer.FileFactory
105
+ * @param {Object} req - Express request object with files.
106
+ * @returns {Array} Array of extracted file objects.
107
+ */
8
108
  filesExtract: (req) => {
9
109
  const files = [];
10
110
  if (!req.files || Object.keys(req.files).length === 0) {
@@ -13,7 +113,9 @@ const FileFactory = {
13
113
 
14
114
  // Handle standard 'file' field
15
115
  if (Array.isArray(req.files.file)) {
16
- for (const file of req.files.file) files.push(file);
116
+ for (const file of req.files.file) {
117
+ files.push(file);
118
+ }
17
119
  } else if (req.files.file) {
18
120
  files.push(req.files.file);
19
121
  }
@@ -38,89 +140,335 @@ const FileFactory = {
38
140
 
39
141
  return files;
40
142
  },
143
+
144
+ /**
145
+ * Upload files to database with UTF-8 encoding.
146
+ * @async
147
+ * @function upload
148
+ * @memberof FileServiceServer.FileFactory
149
+ * @param {Object} req - Express request object with files.
150
+ * @param {import('mongoose').Model} File - Mongoose File model.
151
+ * @returns {Promise<Array>} Array of uploaded file metadata objects.
152
+ */
41
153
  upload: async function (req, File) {
42
154
  const results = FileFactory.filesExtract(req);
43
155
  let index = -1;
156
+
44
157
  for (let file of results) {
158
+ // Normalize filename to ensure UTF-8 encoding
159
+ file.name = FileServiceDto.normalizeFilename(file.name);
160
+ file.encoding = 'utf-8';
161
+
162
+ // Save file to database
45
163
  file = await new File(file).save();
46
164
  index++;
165
+
166
+ // Retrieve metadata-only response
47
167
  const [result] = await File.find({
48
168
  _id: file._id,
49
- }).select({ _id: 1, name: 1, mimetype: 1 });
169
+ }).select(FileServiceDto.metadataSelect());
170
+
50
171
  results[index] = result;
51
172
  }
173
+
52
174
  return results;
53
175
  },
176
+
177
+ /**
178
+ * Convert string to hexadecimal.
179
+ * @function hex
180
+ * @memberof FileServiceServer.FileFactory
181
+ * @param {string} [raw=''] - Raw string to convert.
182
+ * @returns {string} Hexadecimal representation of the string.
183
+ */
54
184
  hex: (raw = '') => {
55
185
  return Buffer.from(raw, 'utf8').toString('hex');
56
- // reverse hexValue.toString()
57
186
  },
187
+
188
+ /**
189
+ * Get MIME type from file path based on extension.
190
+ * @function getMymeTypeFromPath
191
+ * @memberof FileServiceServer.FileFactory
192
+ * @param {string} path - File path or filename with extension.
193
+ * @returns {string} MIME type string.
194
+ */
58
195
  getMymeTypeFromPath: (path) => {
59
- switch (path.split('.').pop()) {
60
- case 'png':
61
- return 'image/png';
62
- case 'jpg':
63
- return 'image/jpeg';
64
- case 'jpeg':
65
- return 'image/jpeg';
66
- case 'gif':
67
- return 'image/gif';
68
- case 'svg':
69
- return 'image/svg+xml';
70
- default:
71
- return 'application/octet-stream';
72
- }
196
+ const ext = String(path || '')
197
+ .toLowerCase()
198
+ .split('.')
199
+ .pop();
200
+ const mimeTypes = {
201
+ png: 'image/png',
202
+ jpg: 'image/jpeg',
203
+ jpeg: 'image/jpeg',
204
+ gif: 'image/gif',
205
+ svg: 'image/svg+xml',
206
+ webp: 'image/webp',
207
+ pdf: 'application/pdf',
208
+ md: 'text/markdown',
209
+ txt: 'text/plain',
210
+ json: 'application/json',
211
+ };
212
+ return mimeTypes[ext] || 'application/octet-stream';
73
213
  },
214
+
215
+ /**
216
+ * Create file object with proper encoding.
217
+ * @function create
218
+ * @memberof FileServiceServer.FileFactory
219
+ * @param {Buffer} [data=Buffer.from([])] - File data buffer.
220
+ * @param {string} [name=''] - File name.
221
+ * @returns {Object} File object with name, data, size, encoding, mimetype, and md5.
222
+ */
74
223
  create: (data = Buffer.from([]), name = '') => {
224
+ const normalizedName = FileServiceDto.normalizeFilename(name);
225
+
75
226
  return {
76
- name: name,
227
+ name: normalizedName,
77
228
  data: data,
78
229
  size: data.length,
79
- encoding: '7bit',
230
+ encoding: 'utf-8',
80
231
  tempFilePath: '',
81
232
  truncated: false,
82
- mimetype: FileFactory.getMymeTypeFromPath(name),
233
+ mimetype: FileFactory.getMymeTypeFromPath(normalizedName),
83
234
  md5: crypto.createHash('md5').update(data).digest('hex'),
84
235
  cid: undefined,
85
236
  };
86
237
  },
87
238
  };
88
239
 
240
+ /**
241
+ * File cleanup utilities for preventing orphaned files.
242
+ * These utilities help maintain database integrity by automatically removing
243
+ * orphaned file records when references are changed or deleted in documents.
244
+ * @namespace FileServiceServer.FileCleanup
245
+ * @memberof FileServiceServer
246
+ */
247
+ const FileCleanup = {
248
+ /**
249
+ * Clean up old file references when document fields are updated.
250
+ * Deletes old files that are being replaced by new file IDs.
251
+ * @async
252
+ * @function cleanupReplacedFiles
253
+ * @memberof FileServiceServer.FileCleanup
254
+ * @param {Object} options - Cleanup options.
255
+ * @param {Object} options.oldDoc - Original document with old file references.
256
+ * @param {Object} options.newData - New data containing updated file references.
257
+ * @param {Array<string>} options.fileFields - Array of field names that contain file IDs (e.g., ['fileId', 'mdFileId']).
258
+ * @param {import('mongoose').Model} options.File - Mongoose File model.
259
+ * @returns {Promise<Array>} Array of deleted file IDs.
260
+ */
261
+ cleanupReplacedFiles: async ({ oldDoc, newData, fileFields, File }) => {
262
+ const deletedFileIds = [];
263
+
264
+ for (const field of fileFields) {
265
+ const oldFileId = oldDoc[field];
266
+ const newFileId = newData[field];
267
+
268
+ // If field has old file and new data changes or removes it
269
+ if (oldFileId && newFileId !== undefined && String(oldFileId) !== String(newFileId)) {
270
+ try {
271
+ const file = await File.findOne({ _id: oldFileId });
272
+ if (file) {
273
+ await File.findByIdAndDelete(oldFileId);
274
+ deletedFileIds.push(oldFileId);
275
+ logger.info(`Cleaned up orphaned file: ${oldFileId} from field: ${field}`);
276
+ }
277
+ } catch (err) {
278
+ logger.error(`Failed to cleanup file ${oldFileId} from field ${field}:`, err);
279
+ }
280
+ }
281
+ }
282
+
283
+ return deletedFileIds;
284
+ },
285
+
286
+ /**
287
+ * Delete all files referenced in a document.
288
+ * Used when deleting an entire document.
289
+ * This method removes all files associated with the document before
290
+ * the document itself is deleted, preventing orphaned file records.
291
+ * @async
292
+ * @function deleteDocumentFiles
293
+ * @memberof FileServiceServer.FileCleanup
294
+ * @param {Object} options - Deletion options.
295
+ * @param {Object} options.doc - Document containing file references.
296
+ * @param {Array<string>} options.fileFields - Array of field names that contain file IDs.
297
+ * @param {import('mongoose').Model} options.File - Mongoose File model.
298
+ * @returns {Promise<Array>} Array of deleted file IDs.
299
+ */
300
+ deleteDocumentFiles: async ({ doc, fileFields, File }) => {
301
+ const deletedFileIds = [];
302
+
303
+ for (const field of fileFields) {
304
+ const fileId = doc[field];
305
+
306
+ if (fileId) {
307
+ try {
308
+ const file = await File.findOne({ _id: fileId });
309
+ if (file) {
310
+ await File.findByIdAndDelete(fileId);
311
+ deletedFileIds.push(fileId);
312
+ logger.info(`Deleted file: ${fileId} from field: ${field}`);
313
+ }
314
+ } catch (err) {
315
+ logger.error(`Failed to delete file ${fileId} from field ${field}:`, err);
316
+ }
317
+ }
318
+ }
319
+
320
+ return deletedFileIds;
321
+ },
322
+ };
323
+
324
+ /**
325
+ * File Service for handling REST API file operations.
326
+ * @namespace FileServiceServer.FileService
327
+ * @memberof FileServiceServer
328
+ */
89
329
  const FileService = {
330
+ /**
331
+ * POST - Upload files.
332
+ * Returns metadata-only response (no buffer data).
333
+ * @async
334
+ * @function post
335
+ * @memberof FileServiceServer.FileService
336
+ * @param {Object} req - Express request object.
337
+ * @param {Object} res - Express response object.
338
+ * @param {Object} options - Request options containing host and path.
339
+ * @returns {Promise<Array>} Array of uploaded file metadata objects.
340
+ */
90
341
  post: async (req, res, options) => {
342
+ // Check that user is authenticated and not a guest
343
+ if (!req.auth || !req.auth.user || req.auth.user.role === 'guest') {
344
+ throw new Error('Authentication required. Guest users cannot upload files.');
345
+ }
346
+
91
347
  /** @type {import('./file.model.js').FileModel} */
92
348
  const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
93
- return await FileFactory.upload(req, File);
349
+
350
+ const uploadedFiles = await FileFactory.upload(req, File);
351
+ return FileServiceDto.toMetadataArray(uploadedFiles);
94
352
  },
353
+
354
+ /**
355
+ * GET - Retrieve files.
356
+ * Returns metadata-only for regular GET.
357
+ * Returns buffer data for /blob endpoint.
358
+ * Authorization: Check if file belongs to a public document or user owns the document.
359
+ * @async
360
+ * @function get
361
+ * @memberof FileServiceServer.FileService
362
+ * @param {Object} req - Express request object.
363
+ * @param {Object} res - Express response object.
364
+ * @param {Object} options - Request options containing host and path.
365
+ * @returns {Promise<Array|Buffer>} Array of file metadata objects or Buffer for blob endpoint.
366
+ * @throws {Error} If file not found or user not authorized.
367
+ */
95
368
  get: async (req, res, options) => {
96
369
  /** @type {import('./file.model.js').FileModel} */
97
370
  const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
371
+ /** @type {import('../document/document.model.js').DocumentModel} */
372
+ const Document = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Document;
373
+ /** @type {import('../user/user.model.js').User} */
374
+ const User = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.User;
375
+
376
+ const isFileAuthorized = async (fileId) => {
377
+ try {
378
+ // Find document that references this file
379
+ const doc = await Document.findOne({
380
+ $or: [{ fileId: new Types.ObjectId(fileId) }, { mdFileId: new Types.ObjectId(fileId) }],
381
+ });
98
382
 
383
+ // If file not in any document, allow access
384
+ if (!doc) return true;
385
+
386
+ // If document has 'public' tag, allow all access
387
+ if (doc.isPublic) return true;
388
+
389
+ // Otherwise, user must be authenticated and own the document
390
+ const token = getBearerToken(req);
391
+ if (!token) false;
392
+ const payload = jwtVerify(token, options);
393
+ const user = await User.findOne({ _id: payload._id }).lean();
394
+ if (!user) return false;
395
+ return String(doc.userId) === String(user._id);
396
+ } catch (err) {
397
+ console.log(err);
398
+ logger.error('Authorization check failed:', err);
399
+ return false;
400
+ }
401
+ };
402
+
403
+ // Handle blob endpoint - return raw buffer data
99
404
  if (req.path.startsWith('/blob') && req.params.id) {
405
+ // Check authorization
406
+ const authorized = await isFileAuthorized(req.params.id);
407
+ if (!authorized) {
408
+ throw new Error('Unauthorized');
409
+ }
410
+
100
411
  const file = await File.findOne({ _id: req.params.id });
101
- res.set('Content-Type', file.mimetype);
102
- return Buffer.from(file.data, 'base64');
412
+
413
+ if (!file) {
414
+ throw new Error('File not found');
415
+ }
416
+
417
+ // Set proper content type and return buffer
418
+ res.set('Content-Type', file.mimetype || 'application/octet-stream');
419
+ res.set('Content-Length', file.size || 0);
420
+ res.set('Content-Disposition', `inline; filename="${encodeURIComponent(file.name)}"`);
421
+
422
+ // Return Buffer directly (not JSON)
423
+ return Buffer.from(file.data);
103
424
  }
104
425
 
426
+ // Handle regular GET - return metadata only
105
427
  switch (req.params.id) {
106
- case 'all':
107
- return await File.find();
428
+ case 'all': {
429
+ const files = await File.find().select(FileServiceDto.metadataSelect());
430
+ return FileServiceDto.toMetadataArray(files);
431
+ }
108
432
 
109
- default:
110
- return await File.find({
433
+ default: {
434
+ // Check authorization
435
+ const authorized = await isFileAuthorized(req.params.id);
436
+ if (!authorized) {
437
+ throw new Error('Unauthorized');
438
+ }
439
+
440
+ const files = await File.find({
111
441
  _id: req.params.id,
112
- });
442
+ }).select(FileServiceDto.metadataSelect());
443
+
444
+ return FileServiceDto.toMetadataArray(files);
445
+ }
113
446
  }
114
447
  },
448
+
449
+ /**
450
+ * DELETE - Remove files.
451
+ * @async
452
+ * @function delete
453
+ * @memberof FileServiceServer.FileService
454
+ * @param {Object} req - Express request object.
455
+ * @param {Object} res - Express response object.
456
+ * @param {Object} options - Request options containing host and path.
457
+ * @returns {Promise<Object>} Deleted file metadata object.
458
+ * @throws {Error} If file not found.
459
+ */
115
460
  delete: async (req, res, options) => {
116
461
  /** @type {import('./file.model.js').FileModel} */
117
462
  const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
118
463
 
119
- switch (req.params.id) {
120
- default:
121
- return await File.findByIdAndDelete(req.params.id);
464
+ const result = await File.findByIdAndDelete(req.params.id);
465
+
466
+ if (!result) {
467
+ throw new Error('File not found');
122
468
  }
469
+
470
+ return FileServiceDto.toMetadata(result);
123
471
  },
124
472
  };
125
473
 
126
- export { FileService, FileFactory };
474
+ export { FileService, FileFactory, FileServiceDto, FileCleanup };
@@ -20,7 +20,28 @@ const UserSchema = new Schema(
20
20
  lastLoginDate: { type: Date },
21
21
  failedLoginAttempts: { type: Number, default: 0 },
22
22
  password: { type: String, trim: true, required: 'Password is required' },
23
- username: { type: String, trim: true, unique: true, required: 'Username is required' },
23
+ username: {
24
+ type: String,
25
+ trim: true,
26
+ unique: true,
27
+ required: 'Username is required',
28
+ validate: [
29
+ {
30
+ validator: function (username) {
31
+ // Allow only alphanumeric characters, hyphens, and underscores (URI-safe)
32
+ return /^[a-zA-Z0-9_-]+$/.test(username);
33
+ },
34
+ message: 'Username can only contain letters, numbers, hyphens, and underscores',
35
+ },
36
+ {
37
+ validator: function (username) {
38
+ // Length validation
39
+ return username && username.length >= 2 && username.length <= 20;
40
+ },
41
+ message: 'Username must be between 2 and 20 characters',
42
+ },
43
+ ],
44
+ },
24
45
  role: { type: String, enum: userRoleEnum, default: 'guest' },
25
46
  activeSessions: {
26
47
  type: [
@@ -60,6 +81,8 @@ const UserSchema = new Schema(
60
81
  context: [{ type: String, enum: ['client', 'supplier', 'employee', 'owner'] }],
61
82
  },
62
83
  ],
84
+ publicProfile: { type: Boolean, default: false },
85
+ briefDescription: { type: String, default: 'Uploader' },
63
86
  },
64
87
  {
65
88
  timestamps: true,
@@ -80,6 +103,8 @@ const UserDto = {
80
103
  role: 1,
81
104
  emailConfirmed: 1,
82
105
  profileImageId: 1,
106
+ publicProfile: 1,
107
+ briefDescription: 1,
83
108
  createdAt: 1,
84
109
  updatedAt: 1,
85
110
  };
@@ -88,6 +113,18 @@ const UserDto = {
88
113
  return { _id: 1 };
89
114
  },
90
115
  },
116
+ public: {
117
+ get: () => {
118
+ return {
119
+ _id: 1,
120
+ username: 1,
121
+ profileImageId: 1,
122
+ publicProfile: 1,
123
+ briefDescription: 1,
124
+ createdAt: 1,
125
+ };
126
+ },
127
+ },
91
128
  auth: {
92
129
  payload: (user, jwtid, ip, userAgent, host, path) => {
93
130
  const tokenPayload = {