@modular-rest/server 1.19.0 → 1.20.0
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/.nvmrc +1 -1
- package/dist/application.js +5 -4
- package/dist/class/combinator.js +7 -3
- package/dist/class/directory.d.ts +2 -4
- package/dist/class/directory.js +42 -64
- package/dist/helper/data_insertion.js +93 -26
- package/dist/services/data_provider/model_registry.d.ts +5 -0
- package/dist/services/data_provider/model_registry.js +25 -0
- package/dist/services/data_provider/service.js +8 -0
- package/dist/services/file/service.d.ts +47 -78
- package/dist/services/file/service.js +114 -155
- package/dist/services/functions/service.js +4 -4
- package/dist/services/jwt/router.js +2 -1
- package/dist/services/user_manager/router.js +1 -1
- package/dist/services/user_manager/service.js +48 -17
- package/jest.config.ts +18 -0
- package/package.json +11 -2
- package/src/application.ts +5 -4
- package/src/class/combinator.ts +10 -3
- package/src/class/directory.ts +40 -58
- package/src/helper/data_insertion.ts +101 -27
- package/src/services/data_provider/model_registry.ts +28 -0
- package/src/services/data_provider/service.ts +6 -0
- package/src/services/file/service.ts +136 -178
- package/src/services/functions/service.ts +4 -4
- package/src/services/jwt/router.ts +2 -1
- package/src/services/user_manager/router.ts +1 -1
- package/src/services/user_manager/service.ts +49 -20
- package/tests/helpers/test-app.ts +182 -0
- package/tests/router/data-provider.router.int.test.ts +192 -0
- package/tests/router/file.router.int.test.ts +104 -0
- package/tests/router/functions.router.int.test.ts +91 -0
- package/tests/router/jwt.router.int.test.ts +69 -0
- package/tests/router/user-manager.router.int.test.ts +85 -0
- package/tests/setup/jest.setup.ts +5 -0
|
@@ -28,9 +28,12 @@ interface StoredFileDetail {
|
|
|
28
28
|
* File upload options interface
|
|
29
29
|
* @interface StoreFileOptions
|
|
30
30
|
* @property {Object} file - File details
|
|
31
|
-
* @property {string} file.path - Temporary file path
|
|
32
|
-
* @property {string} file.
|
|
33
|
-
* @property {string} file.
|
|
31
|
+
* @property {string} [file.path] - Temporary file path (legacy)
|
|
32
|
+
* @property {string} [file.filepath] - Temporary file path (koa-body v6+)
|
|
33
|
+
* @property {string} [file.type] - MIME type of the file (legacy)
|
|
34
|
+
* @property {string} [file.mimetype] - MIME type of the file (koa-body v6+)
|
|
35
|
+
* @property {string} [file.name] - Original filename (legacy)
|
|
36
|
+
* @property {string} [file.originalFilename] - Original filename (koa-body v6+)
|
|
34
37
|
* @property {number} file.size - File size in bytes
|
|
35
38
|
* @property {string} ownerId - ID of the file owner
|
|
36
39
|
* @property {string} tag - Tag for file organization
|
|
@@ -38,9 +41,12 @@ interface StoredFileDetail {
|
|
|
38
41
|
*/
|
|
39
42
|
interface StoreFileOptions {
|
|
40
43
|
file: {
|
|
41
|
-
path
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
path?: string;
|
|
45
|
+
filepath?: string;
|
|
46
|
+
type?: string;
|
|
47
|
+
mimetype?: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
originalFilename?: string;
|
|
44
50
|
size: number;
|
|
45
51
|
};
|
|
46
52
|
ownerId: string;
|
|
@@ -49,11 +55,11 @@ interface StoreFileOptions {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
/**
|
|
52
|
-
* File service for handling file
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
58
|
+
* File service class for handling file operations
|
|
59
|
+
* @class FileService
|
|
60
|
+
* @description
|
|
61
|
+
* This class provides methods for managing file uploads, retrieval, and deletion.
|
|
62
|
+
* It handles physical file storage and database metadata management.
|
|
57
63
|
*/
|
|
58
64
|
class FileService {
|
|
59
65
|
/**
|
|
@@ -107,43 +113,23 @@ class FileService {
|
|
|
107
113
|
fs.mkdirSync(directoryOrConfig, { recursive: true });
|
|
108
114
|
}
|
|
109
115
|
this.directory = directoryOrConfig;
|
|
110
|
-
this.urlPath =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!directory) {
|
|
119
|
-
throw new Error('directory is required in uploadDirectoryConfig');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!fs.existsSync(directory)) {
|
|
123
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
116
|
+
this.urlPath = '/assets'; // Default urlPath for legacy
|
|
117
|
+
} else {
|
|
118
|
+
const { directory, urlPath } = directoryOrConfig;
|
|
119
|
+
if (!fs.existsSync(directory)) {
|
|
120
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
this.directory = directory;
|
|
123
|
+
this.urlPath = urlPath || '/assets';
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
// Store only the necessary properties (ignore koa-static options)
|
|
127
|
-
this.directory = directory;
|
|
128
|
-
this.urlPath = urlPath;
|
|
129
125
|
}
|
|
130
126
|
|
|
131
127
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* Creates stored file details with unique filename
|
|
128
|
+
* Creates a unique filename and storage details
|
|
135
129
|
* @param {string} fileType - MIME type of the file
|
|
136
|
-
* @param {string} tag -
|
|
137
|
-
* @returns {StoredFileDetail} Storage details including filename and path
|
|
138
|
-
* @
|
|
139
|
-
*
|
|
140
|
-
* @example
|
|
141
|
-
* ```typescript
|
|
142
|
-
* import { fileService } from '@modular-rest/server';
|
|
143
|
-
*
|
|
144
|
-
* const details = fileService.createStoredDetail('image/jpeg', 'profile');
|
|
145
|
-
* // Returns: { fileName: '1234567890.jpeg', fullPath: '/uploads/jpeg/profile/1234567890.jpeg', fileFormat: 'jpeg' }
|
|
146
|
-
* ```
|
|
130
|
+
* @param {string} tag - File tag
|
|
131
|
+
* @returns {StoredFileDetail} Storage details including unique filename and path
|
|
132
|
+
* @hidden
|
|
147
133
|
*/
|
|
148
134
|
createStoredDetail(fileType: string, tag: string): StoredFileDetail {
|
|
149
135
|
const typeParts = fileType.split('/');
|
|
@@ -152,21 +138,23 @@ class FileService {
|
|
|
152
138
|
const time = new Date().getTime();
|
|
153
139
|
const fileName = `${time}.${fileFormat}`;
|
|
154
140
|
|
|
155
|
-
if (!
|
|
156
|
-
throw new Error('Upload directory
|
|
141
|
+
if (!this.directory) {
|
|
142
|
+
throw new Error('Upload directory not set');
|
|
157
143
|
}
|
|
158
144
|
|
|
159
|
-
const fullPath = pathModule.join(
|
|
145
|
+
const fullPath = pathModule.join(this.directory, fileFormat, tag, fileName);
|
|
160
146
|
|
|
161
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
fileName,
|
|
149
|
+
fullPath,
|
|
150
|
+
fileFormat,
|
|
151
|
+
};
|
|
162
152
|
}
|
|
163
153
|
|
|
164
154
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* Stores a file, removes the temporary file, and saves metadata to database
|
|
155
|
+
* Stores a file on disc and creates metadata in database
|
|
168
156
|
* @param {StoreFileOptions} options - File storage options
|
|
169
|
-
* @returns {Promise<IFile>}
|
|
157
|
+
* @returns {Promise<IFile>} The created file document
|
|
170
158
|
* @throws {Error} If upload directory is not set or storage fails
|
|
171
159
|
* @example
|
|
172
160
|
* ```typescript
|
|
@@ -190,12 +178,20 @@ class FileService {
|
|
|
190
178
|
throw new Error('Upload directory has not been set');
|
|
191
179
|
}
|
|
192
180
|
|
|
181
|
+
const fileType = file.mimetype || file.type || 'unknown/unknown';
|
|
182
|
+
const filePath = file.filepath || file.path;
|
|
183
|
+
const fileName = file.originalFilename || file.name || 'unknown';
|
|
184
|
+
|
|
185
|
+
if (!filePath) {
|
|
186
|
+
throw new Error('File path is missing');
|
|
187
|
+
}
|
|
188
|
+
|
|
193
189
|
let storedFile: StoredFileDetail;
|
|
194
190
|
|
|
195
191
|
return new Promise(async (done, reject) => {
|
|
196
|
-
storedFile = FileService.instance.createStoredDetail(
|
|
192
|
+
storedFile = FileService.instance.createStoredDetail(fileType, tag);
|
|
197
193
|
|
|
198
|
-
fs.copyFile(
|
|
194
|
+
fs.copyFile(filePath, storedFile.fullPath, (err: Error | null) => {
|
|
199
195
|
if (err) {
|
|
200
196
|
reject(err);
|
|
201
197
|
} else {
|
|
@@ -204,7 +200,13 @@ class FileService {
|
|
|
204
200
|
|
|
205
201
|
// remove temp file
|
|
206
202
|
if (removeFileAfterStore) {
|
|
207
|
-
|
|
203
|
+
try {
|
|
204
|
+
if (fs.existsSync(filePath)) {
|
|
205
|
+
fs.unlinkSync(filePath);
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.warn('Failed to remove temp file:', e);
|
|
209
|
+
}
|
|
208
210
|
}
|
|
209
211
|
});
|
|
210
212
|
})
|
|
@@ -218,174 +220,131 @@ class FileService {
|
|
|
218
220
|
|
|
219
221
|
const data = {
|
|
220
222
|
owner: ownerId,
|
|
223
|
+
tag,
|
|
224
|
+
originalName: fileName,
|
|
221
225
|
fileName: storedFile.fileName,
|
|
222
|
-
originalName: file.name,
|
|
223
226
|
format: storedFile.fileFormat,
|
|
224
|
-
tag,
|
|
225
227
|
size: file.size,
|
|
226
228
|
};
|
|
227
229
|
|
|
228
|
-
|
|
229
|
-
const doc = new CollectionModel(data);
|
|
230
|
-
|
|
231
|
-
return doc.save().then(savedDoc => {
|
|
232
|
-
triggerService.call('insert-one', 'cms', 'file', {
|
|
233
|
-
query: null,
|
|
234
|
-
queryResult: savedDoc,
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
return savedDoc;
|
|
238
|
-
});
|
|
230
|
+
return CollectionModel.create(data);
|
|
239
231
|
})
|
|
240
|
-
.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
throw err;
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* @hidden
|
|
250
|
-
*
|
|
251
|
-
* Removes a file from the disk
|
|
252
|
-
* @param {string} path - File path to remove
|
|
253
|
-
* @returns {Promise<void>} Promise resolving when file is removed
|
|
254
|
-
* @throws {Error} If file removal fails
|
|
255
|
-
* @example
|
|
256
|
-
* ```typescript
|
|
257
|
-
* import { fileService } from '@modular-rest/server';
|
|
258
|
-
*
|
|
259
|
-
* await fileService.removeFromDisc('/uploads/jpeg/profile/1234567890.jpeg');
|
|
260
|
-
* ```
|
|
261
|
-
*/
|
|
262
|
-
removeFromDisc(path: string): Promise<void> {
|
|
263
|
-
return new Promise((done, reject) => {
|
|
264
|
-
fs.unlink(path, (err: Error | null) => {
|
|
265
|
-
if (err) reject(err);
|
|
266
|
-
else done();
|
|
232
|
+
.then(async (doc: IFile) => {
|
|
233
|
+
triggerService.call('insert-one', 'cms', 'file', { queryResult: doc });
|
|
234
|
+
return doc;
|
|
267
235
|
});
|
|
268
|
-
});
|
|
269
236
|
}
|
|
270
237
|
|
|
271
238
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* @
|
|
275
|
-
* @
|
|
276
|
-
* @throws {Error} If file is not found or removal fails
|
|
239
|
+
* Deletes a file from disc and database
|
|
240
|
+
* @param {string} fileId - ID of the file to delete
|
|
241
|
+
* @returns {Promise<boolean>} True if deletion was successful
|
|
242
|
+
* @throws {Error} If file is not found or deletion fails
|
|
277
243
|
* @example
|
|
278
244
|
* ```typescript
|
|
279
245
|
* import { fileService } from '@modular-rest/server';
|
|
280
246
|
*
|
|
281
|
-
*
|
|
282
|
-
* await fileService.removeFile('file123');
|
|
283
|
-
* console.log('File removed successfully');
|
|
284
|
-
* } catch (error) {
|
|
285
|
-
* console.error('Failed to remove file:', error);
|
|
286
|
-
* }
|
|
247
|
+
* await fileService.removeFile('file123');
|
|
287
248
|
* ```
|
|
288
249
|
*/
|
|
289
|
-
removeFile(fileId: string): Promise<
|
|
250
|
+
async removeFile(fileId: string): Promise<boolean> {
|
|
290
251
|
if (!FileService.instance.directory) {
|
|
291
252
|
throw new Error('Upload directory has not been set');
|
|
292
253
|
}
|
|
293
254
|
|
|
294
|
-
|
|
295
|
-
const CollectionModel = DataProvider.getCollection<IFile>('cms', 'file');
|
|
255
|
+
const CollectionModel = DataProvider.getCollection<IFile>('cms', 'file');
|
|
296
256
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
257
|
+
if (!CollectionModel) {
|
|
258
|
+
throw new Error('Collection model not found');
|
|
259
|
+
}
|
|
300
260
|
|
|
301
|
-
|
|
261
|
+
const doc = await CollectionModel.findById(fileId);
|
|
262
|
+
|
|
263
|
+
if (!doc) {
|
|
264
|
+
throw new Error('File not found');
|
|
265
|
+
}
|
|
302
266
|
|
|
303
|
-
|
|
304
|
-
|
|
267
|
+
const filePath = pathModule.join(
|
|
268
|
+
FileService.instance.directory as string,
|
|
269
|
+
doc.format,
|
|
270
|
+
doc.tag,
|
|
271
|
+
doc.fileName
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (fs.existsSync(filePath)) {
|
|
275
|
+
try {
|
|
276
|
+
await FileService.instance.removeFromDisc(filePath);
|
|
277
|
+
} catch (err: any) {
|
|
278
|
+
// If the file is not found on disc, we can still proceed with deleting metadata
|
|
279
|
+
if (err.code !== 'ENOENT') {
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
305
282
|
}
|
|
283
|
+
}
|
|
306
284
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
triggerService.call('remove-one', 'cms', 'file', {
|
|
328
|
-
query: { _id: fileId },
|
|
329
|
-
queryResult: null,
|
|
330
|
-
});
|
|
331
|
-
})
|
|
332
|
-
.then(done)
|
|
333
|
-
.catch(reject);
|
|
285
|
+
await CollectionModel.findByIdAndDelete(fileId);
|
|
286
|
+
triggerService.call('remove-one', 'cms', 'file', { queryResult: doc });
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Deletes a file from physical storage
|
|
292
|
+
* @param {string} path - Physical path to the file
|
|
293
|
+
* @returns {Promise<boolean>} True if deletion was successful
|
|
294
|
+
* @hidden
|
|
295
|
+
*/
|
|
296
|
+
removeFromDisc(path: string): Promise<boolean> {
|
|
297
|
+
return new Promise((done, reject) => {
|
|
298
|
+
fs.unlink(path, err => {
|
|
299
|
+
if (err) {
|
|
300
|
+
reject(err);
|
|
301
|
+
} else {
|
|
302
|
+
done(true);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
334
305
|
});
|
|
335
306
|
}
|
|
336
307
|
|
|
337
308
|
/**
|
|
338
|
-
* Retrieves a file document from
|
|
339
|
-
*
|
|
340
|
-
* @
|
|
341
|
-
* @
|
|
342
|
-
* @
|
|
343
|
-
* @example
|
|
344
|
-
* ```typescript
|
|
345
|
-
* import { fileService } from '@modular-rest/server';
|
|
346
|
-
*
|
|
347
|
-
* const fileDoc = await fileService.getFile('file123');
|
|
348
|
-
* console.log('File details:', fileDoc);
|
|
349
|
-
* ```
|
|
309
|
+
* Retrieves a file document from database
|
|
310
|
+
* @param {string} fileId - ID of the file
|
|
311
|
+
* @returns {Promise<IFile>} The file document
|
|
312
|
+
* @throws {Error} If file is not found
|
|
313
|
+
* @hidden
|
|
350
314
|
*/
|
|
351
|
-
getFile(fileId: string): Promise<IFile> {
|
|
315
|
+
async getFile(fileId: string): Promise<IFile> {
|
|
352
316
|
const CollectionModel = DataProvider.getCollection<IFile>('cms', 'file');
|
|
353
317
|
|
|
354
318
|
if (!CollectionModel) {
|
|
355
319
|
throw new Error('Collection model not found');
|
|
356
320
|
}
|
|
357
321
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
});
|
|
322
|
+
const doc = await CollectionModel.findById(fileId);
|
|
323
|
+
|
|
324
|
+
if (!doc) {
|
|
325
|
+
throw new Error('File not found');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return doc;
|
|
366
329
|
}
|
|
367
330
|
|
|
368
331
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* @
|
|
372
|
-
* @returns {Promise<string>} Promise resolving to file URL
|
|
373
|
-
* @throws {Error} If URL path is not defined or file is not found
|
|
332
|
+
* Gets the public URL for a file
|
|
333
|
+
* @param {string} fileId - ID of the file
|
|
334
|
+
* @returns {Promise<string>} The public URL
|
|
374
335
|
* @example
|
|
375
336
|
* ```typescript
|
|
376
337
|
* import { fileService } from '@modular-rest/server';
|
|
377
338
|
*
|
|
378
|
-
* const
|
|
379
|
-
* // Returns: '/
|
|
339
|
+
* const url = await fileService.getFileLink('file123');
|
|
340
|
+
* // Returns: '/assets/jpeg/profile/1234567890.jpeg'
|
|
380
341
|
* ```
|
|
381
342
|
*/
|
|
382
343
|
async getFileLink(fileId: string): Promise<string> {
|
|
383
344
|
const fileDoc = await FileService.instance.getFile(fileId);
|
|
384
345
|
|
|
385
346
|
if (!FileService.instance.urlPath) {
|
|
386
|
-
throw new Error(
|
|
387
|
-
'Upload directory URL path is not defined. Please configure uploadDirectoryConfig with a urlPath property.'
|
|
388
|
-
);
|
|
347
|
+
throw new Error('Upload directory config has not been set');
|
|
389
348
|
}
|
|
390
349
|
|
|
391
350
|
const link = `${FileService.instance.urlPath}/${fileDoc.format}/${fileDoc.tag}/${fileDoc.fileName}`;
|
|
@@ -394,11 +353,9 @@ class FileService {
|
|
|
394
353
|
}
|
|
395
354
|
|
|
396
355
|
/**
|
|
397
|
-
* Gets the
|
|
398
|
-
*
|
|
399
|
-
* @
|
|
400
|
-
* @returns {Promise<string>} Promise resolving to full file path
|
|
401
|
-
* @throws {Error} If upload directory is not set or file is not found
|
|
356
|
+
* Gets the physical path for a file
|
|
357
|
+
* @param {string} fileId - ID of the file
|
|
358
|
+
* @returns {Promise<string>} The physical path
|
|
402
359
|
* @example
|
|
403
360
|
* ```typescript
|
|
404
361
|
* import { fileService } from '@modular-rest/server';
|
|
@@ -423,3 +380,4 @@ class FileService {
|
|
|
423
380
|
* @constant {FileService}
|
|
424
381
|
*/
|
|
425
382
|
export const main = new FileService();
|
|
383
|
+
FileService.instance = main;
|
|
@@ -93,19 +93,19 @@ export interface DefinedFunction {
|
|
|
93
93
|
*/
|
|
94
94
|
export function defineFunction(options: DefinedFunction): DefinedFunction {
|
|
95
95
|
// Check if the function already exists
|
|
96
|
-
const existingFunction = functions.find(f => f.name === name);
|
|
96
|
+
const existingFunction = functions.find(f => f.name === options.name);
|
|
97
97
|
if (existingFunction) {
|
|
98
|
-
throw new Error(`Function with name ${name} already exists`);
|
|
98
|
+
throw new Error(`Function with name ${options.name} already exists`);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Check if the permission types provided
|
|
102
102
|
if (!options.permissionTypes || !options.permissionTypes.length) {
|
|
103
|
-
throw new Error(`Permission types not provided for function ${name}`);
|
|
103
|
+
throw new Error(`Permission types not provided for function ${options.name}`);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Check if the callback is a function
|
|
107
107
|
if (typeof options.callback !== 'function') {
|
|
108
|
-
throw new Error(`Callback is not a function for function ${name}`);
|
|
108
|
+
throw new Error(`Callback is not a function for function ${options.name}`);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Add the function to the list of functions
|
|
@@ -3,6 +3,7 @@ import { validateObject } from '../../class/validator';
|
|
|
3
3
|
import { create as reply } from '../../class/reply';
|
|
4
4
|
import { Context } from 'koa';
|
|
5
5
|
import * as service from './service';
|
|
6
|
+
import * as userManager from '../user_manager/service';
|
|
6
7
|
|
|
7
8
|
const name = 'verify';
|
|
8
9
|
const verify = new Router();
|
|
@@ -53,7 +54,7 @@ verify.post('/checkAccess', async (ctx: Context) => {
|
|
|
53
54
|
|
|
54
55
|
const userid = payload.id;
|
|
55
56
|
|
|
56
|
-
await
|
|
57
|
+
await userManager.main
|
|
57
58
|
.getUserById(userid)
|
|
58
59
|
.then((user: any) => {
|
|
59
60
|
const key = user.hasPermission(body.permissionField);
|
|
@@ -3,6 +3,7 @@ import { validateObject } from '../../class/validator';
|
|
|
3
3
|
import { create as reply } from '../../class/reply';
|
|
4
4
|
import { Context } from 'koa';
|
|
5
5
|
import * as service from './service';
|
|
6
|
+
import * as dataProvider from '../data_provider/service';
|
|
6
7
|
|
|
7
8
|
const name = 'user';
|
|
8
9
|
const userManager = new Router();
|
|
@@ -178,7 +179,6 @@ userManager.post('/getPermission', async (ctx: Context) => {
|
|
|
178
179
|
|
|
179
180
|
const query = { _id: body.id };
|
|
180
181
|
|
|
181
|
-
const dataProvider = (global as any).services.dataProvider;
|
|
182
182
|
const permission = await dataProvider
|
|
183
183
|
.getCollection('cms', 'permission')
|
|
184
184
|
.findOne(query)
|
|
@@ -470,13 +470,21 @@ class UserManager {
|
|
|
470
470
|
throw new Error('Invalid verification code');
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
const tempId = this.tempIds[id];
|
|
474
|
+
const idType = tempId ? tempId.type : 'email';
|
|
475
|
+
|
|
473
476
|
const userModel = DataProvider.getCollection('cms', 'auth');
|
|
474
477
|
|
|
475
478
|
if (!userModel) {
|
|
476
479
|
throw new Error('User model not found');
|
|
477
480
|
}
|
|
478
481
|
|
|
479
|
-
const query = {
|
|
482
|
+
const query: Record<string, any> = {};
|
|
483
|
+
if (idType === 'phone') {
|
|
484
|
+
query.phone = id;
|
|
485
|
+
} else {
|
|
486
|
+
query.email = id;
|
|
487
|
+
}
|
|
480
488
|
|
|
481
489
|
// Get from database
|
|
482
490
|
let gottenFromDB;
|
|
@@ -486,25 +494,30 @@ class UserManager {
|
|
|
486
494
|
throw error;
|
|
487
495
|
}
|
|
488
496
|
|
|
489
|
-
if (!gottenFromDB) {
|
|
490
|
-
throw new Error('User not found');
|
|
491
|
-
}
|
|
492
|
-
|
|
493
497
|
try {
|
|
494
|
-
|
|
495
|
-
const user = await User.loadFromModel(gottenFromDB);
|
|
496
|
-
|
|
497
|
-
// Update password
|
|
498
|
-
user.password = password;
|
|
499
|
-
|
|
500
|
-
// Save to database
|
|
501
|
-
await user.save();
|
|
498
|
+
let token: string;
|
|
502
499
|
|
|
503
|
-
|
|
504
|
-
|
|
500
|
+
if (!gottenFromDB) {
|
|
501
|
+
// Registration flow: create new user
|
|
502
|
+
const registrationData: any = {
|
|
503
|
+
password,
|
|
504
|
+
type: 'user',
|
|
505
|
+
};
|
|
506
|
+
if (idType === 'phone') {
|
|
507
|
+
registrationData.phone = id;
|
|
508
|
+
} else {
|
|
509
|
+
registrationData.email = id;
|
|
510
|
+
}
|
|
511
|
+
token = await this.registerUser(registrationData);
|
|
512
|
+
} else {
|
|
513
|
+
// Password reset flow: update existing user
|
|
514
|
+
const user = await User.loadFromModel(gottenFromDB);
|
|
515
|
+
user.password = password;
|
|
516
|
+
await user.save();
|
|
505
517
|
|
|
506
|
-
|
|
507
|
-
|
|
518
|
+
const payload = user.getBrief();
|
|
519
|
+
token = await JWT.main.sign(payload);
|
|
520
|
+
}
|
|
508
521
|
|
|
509
522
|
// Remove temporary ID
|
|
510
523
|
delete this.tempIds[id];
|
|
@@ -543,13 +556,21 @@ class UserManager {
|
|
|
543
556
|
throw new Error('Invalid verification code');
|
|
544
557
|
}
|
|
545
558
|
|
|
559
|
+
const tempId = this.tempIds[id];
|
|
560
|
+
const idType = tempId ? tempId.type : 'email';
|
|
561
|
+
|
|
546
562
|
const userModel = DataProvider.getCollection('cms', 'auth');
|
|
547
563
|
|
|
548
564
|
if (!userModel) {
|
|
549
565
|
throw new Error('User model not found');
|
|
550
566
|
}
|
|
551
567
|
|
|
552
|
-
const query = {
|
|
568
|
+
const query: Record<string, any> = {};
|
|
569
|
+
if (idType === 'phone') {
|
|
570
|
+
query.phone = id;
|
|
571
|
+
} else {
|
|
572
|
+
query.email = id;
|
|
573
|
+
}
|
|
553
574
|
|
|
554
575
|
// Get from database
|
|
555
576
|
let gottenFromDB;
|
|
@@ -619,8 +640,8 @@ class UserManager {
|
|
|
619
640
|
}
|
|
620
641
|
|
|
621
642
|
try {
|
|
622
|
-
// Create user document
|
|
623
|
-
const
|
|
643
|
+
// Create user document with timeout
|
|
644
|
+
const createPromise = userModel.create({
|
|
624
645
|
...detail,
|
|
625
646
|
type: detail.type || 'user',
|
|
626
647
|
permissionGroup: detail.permissionGroup || getDefaultPermissionGroups().title,
|
|
@@ -629,6 +650,14 @@ class UserManager {
|
|
|
629
650
|
password: detail.password || undefined,
|
|
630
651
|
});
|
|
631
652
|
|
|
653
|
+
// Add timeout wrapper to prevent hanging
|
|
654
|
+
const userDoc = await Promise.race([
|
|
655
|
+
createPromise,
|
|
656
|
+
new Promise<never>((_, reject) =>
|
|
657
|
+
setTimeout(() => reject(new Error('User creation timeout after 10s')), 10000)
|
|
658
|
+
),
|
|
659
|
+
]);
|
|
660
|
+
|
|
632
661
|
// Load user from document
|
|
633
662
|
const user = await User.loadFromModel(userDoc);
|
|
634
663
|
|