@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.
- package/README.md +2 -2
- package/cli.md +3 -1
- package/conf.js +2 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/src/api/core/core.service.js +0 -5
- package/src/api/default/default.service.js +7 -5
- package/src/api/document/document.model.js +1 -1
- package/src/api/document/document.router.js +5 -0
- package/src/api/document/document.service.js +105 -47
- package/src/api/file/file.model.js +112 -4
- package/src/api/file/file.ref.json +42 -0
- package/src/api/file/file.service.js +380 -32
- package/src/api/user/user.model.js +38 -1
- package/src/api/user/user.router.js +96 -63
- package/src/api/user/user.service.js +81 -48
- package/src/cli/db.js +424 -166
- package/src/cli/index.js +8 -0
- package/src/cli/repository.js +1 -1
- package/src/cli/run.js +1 -0
- package/src/cli/ssh.js +10 -10
- package/src/client/components/core/Account.js +327 -36
- package/src/client/components/core/AgGrid.js +3 -0
- package/src/client/components/core/Auth.js +9 -3
- package/src/client/components/core/Chat.js +2 -2
- package/src/client/components/core/Content.js +159 -78
- package/src/client/components/core/CssCore.js +16 -12
- package/src/client/components/core/FileExplorer.js +115 -8
- package/src/client/components/core/Input.js +204 -11
- package/src/client/components/core/LogIn.js +42 -20
- package/src/client/components/core/Modal.js +138 -24
- package/src/client/components/core/Panel.js +69 -31
- package/src/client/components/core/PanelForm.js +262 -77
- package/src/client/components/core/PublicProfile.js +888 -0
- package/src/client/components/core/Router.js +117 -15
- package/src/client/components/core/SearchBox.js +329 -13
- package/src/client/components/core/SignUp.js +26 -7
- package/src/client/components/core/SocketIo.js +6 -3
- package/src/client/components/core/Translate.js +98 -0
- package/src/client/components/core/Validator.js +15 -0
- package/src/client/components/core/windowGetDimensions.js +6 -6
- package/src/client/components/default/MenuDefault.js +59 -12
- package/src/client/components/default/RoutesDefault.js +1 -0
- package/src/client/services/core/core.service.js +163 -1
- package/src/client/services/default/default.management.js +451 -64
- package/src/client/services/default/default.service.js +13 -6
- package/src/client/services/file/file.service.js +43 -16
- package/src/client/services/user/user.service.js +13 -9
- package/src/db/DataBaseProvider.js +1 -1
- package/src/db/mongo/MongooseDB.js +1 -1
- package/src/index.js +1 -1
- package/src/mailer/MailerProvider.js +4 -4
- package/src/runtime/express/Express.js +2 -1
- package/src/runtime/lampp/Lampp.js +2 -2
- package/src/server/auth.js +3 -6
- package/src/server/data-query.js +449 -0
- package/src/server/object-layer.js +0 -3
- 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)
|
|
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(
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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:
|
|
227
|
+
name: normalizedName,
|
|
77
228
|
data: data,
|
|
78
229
|
size: data.length,
|
|
79
|
-
encoding: '
|
|
230
|
+
encoding: 'utf-8',
|
|
80
231
|
tempFilePath: '',
|
|
81
232
|
truncated: false,
|
|
82
|
-
mimetype: FileFactory.getMymeTypeFromPath(
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
428
|
+
case 'all': {
|
|
429
|
+
const files = await File.find().select(FileServiceDto.metadataSelect());
|
|
430
|
+
return FileServiceDto.toMetadataArray(files);
|
|
431
|
+
}
|
|
108
432
|
|
|
109
|
-
default:
|
|
110
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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: {
|
|
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 = {
|