@underpostnet/underpost 2.97.0 → 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 (78) hide show
  1. package/README.md +2 -2
  2. package/baremetal/commission-workflows.json +33 -3
  3. package/bin/deploy.js +1 -1
  4. package/cli.md +7 -2
  5. package/conf.js +3 -0
  6. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  7. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  8. package/package.json +1 -1
  9. package/packer/scripts/fuse-tar-root +3 -3
  10. package/scripts/disk-clean.sh +23 -23
  11. package/scripts/gpu-diag.sh +2 -2
  12. package/scripts/ip-info.sh +11 -11
  13. package/scripts/maas-upload-boot-resource.sh +1 -1
  14. package/scripts/nvim.sh +1 -1
  15. package/scripts/packer-setup.sh +13 -13
  16. package/scripts/rocky-setup.sh +2 -2
  17. package/scripts/rpmfusion-ffmpeg-setup.sh +4 -4
  18. package/scripts/ssl.sh +7 -7
  19. package/src/api/core/core.service.js +0 -5
  20. package/src/api/default/default.service.js +7 -5
  21. package/src/api/document/document.model.js +30 -1
  22. package/src/api/document/document.router.js +6 -0
  23. package/src/api/document/document.service.js +423 -51
  24. package/src/api/file/file.model.js +112 -4
  25. package/src/api/file/file.ref.json +42 -0
  26. package/src/api/file/file.service.js +380 -32
  27. package/src/api/user/user.model.js +38 -1
  28. package/src/api/user/user.router.js +96 -63
  29. package/src/api/user/user.service.js +81 -48
  30. package/src/cli/baremetal.js +689 -329
  31. package/src/cli/cluster.js +50 -52
  32. package/src/cli/db.js +424 -166
  33. package/src/cli/deploy.js +1 -1
  34. package/src/cli/index.js +12 -1
  35. package/src/cli/lxd.js +3 -3
  36. package/src/cli/repository.js +1 -1
  37. package/src/cli/run.js +2 -1
  38. package/src/cli/ssh.js +10 -10
  39. package/src/client/components/core/Account.js +327 -36
  40. package/src/client/components/core/AgGrid.js +3 -0
  41. package/src/client/components/core/Auth.js +9 -3
  42. package/src/client/components/core/Chat.js +2 -2
  43. package/src/client/components/core/Content.js +159 -78
  44. package/src/client/components/core/Css.js +16 -2
  45. package/src/client/components/core/CssCore.js +16 -12
  46. package/src/client/components/core/FileExplorer.js +115 -8
  47. package/src/client/components/core/Input.js +204 -11
  48. package/src/client/components/core/LogIn.js +42 -20
  49. package/src/client/components/core/Modal.js +257 -177
  50. package/src/client/components/core/Panel.js +324 -27
  51. package/src/client/components/core/PanelForm.js +280 -73
  52. package/src/client/components/core/PublicProfile.js +888 -0
  53. package/src/client/components/core/Router.js +117 -15
  54. package/src/client/components/core/SearchBox.js +1117 -0
  55. package/src/client/components/core/SignUp.js +26 -7
  56. package/src/client/components/core/SocketIo.js +6 -3
  57. package/src/client/components/core/Translate.js +98 -0
  58. package/src/client/components/core/Validator.js +15 -0
  59. package/src/client/components/core/windowGetDimensions.js +6 -6
  60. package/src/client/components/default/MenuDefault.js +59 -12
  61. package/src/client/components/default/RoutesDefault.js +1 -0
  62. package/src/client/services/core/core.service.js +163 -1
  63. package/src/client/services/default/default.management.js +451 -64
  64. package/src/client/services/default/default.service.js +13 -6
  65. package/src/client/services/document/document.service.js +23 -0
  66. package/src/client/services/file/file.service.js +43 -16
  67. package/src/client/services/user/user.service.js +13 -9
  68. package/src/db/DataBaseProvider.js +1 -1
  69. package/src/db/mongo/MongooseDB.js +1 -1
  70. package/src/index.js +1 -1
  71. package/src/mailer/MailerProvider.js +4 -4
  72. package/src/runtime/express/Express.js +2 -1
  73. package/src/runtime/lampp/Lampp.js +2 -2
  74. package/src/server/auth.js +3 -6
  75. package/src/server/data-query.js +449 -0
  76. package/src/server/dns.js +4 -4
  77. package/src/server/object-layer.js +0 -3
  78. package/src/ws/IoInterface.js +2 -2
@@ -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 = {
@@ -4,8 +4,6 @@ import { loggerFactory } from '../../server/logger.js';
4
4
  import { UserController } from './user.controller.js';
5
5
  import express from 'express';
6
6
  import { DataBaseProvider } from '../../db/DataBaseProvider.js';
7
- import { FileFactory } from '../file/file.service.js';
8
- import { s4 } from '../../client/components/core/CommonJs.js';
9
7
 
10
8
  const logger = loggerFactory(import.meta);
11
9
 
@@ -40,12 +38,13 @@ const UserRouter = (options) => {
40
38
  console.log(error);
41
39
  }
42
40
 
43
- // default user avatar seed
41
+ // Cache mailer images
44
42
  options.png = {
45
43
  buffer: {
46
44
  'invalid-token': fs.readFileSync(`./src/client/public/default/assets/mailer/api-user-invalid-token.png`),
47
45
  recover: fs.readFileSync(`./src/client/public/default/assets/mailer/api-user-recover.png`),
48
46
  check: fs.readFileSync(`./src/client/public/default/assets/mailer/api-user-check.png`),
47
+ avatar: fs.readFileSync(`./src/client/public/default/assets/mailer/api-user-default-avatar.png`),
49
48
  },
50
49
  header: (res) => {
51
50
  res.set('Cross-Origin-Resource-Policy', 'cross-origin');
@@ -54,52 +53,38 @@ const UserRouter = (options) => {
54
53
  res.set('Content-Type', 'image/png');
55
54
  },
56
55
  };
57
-
58
- try {
59
- const models = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models;
60
- const name = 'api-user-default-avatar.png';
61
- const imageFile = await models.File.findOne({ name });
62
- let _id;
63
- if (imageFile) {
64
- _id = imageFile._id;
65
- } else {
66
- const file = await new models.File(
67
- FileFactory.create(fs.readFileSync(`./src/client/public/default/assets/mailer/${name}`), name),
68
- ).save();
69
- _id = file._id;
70
- }
71
- options.getDefaultProfileImageId = async () => {
72
- return _id;
73
- };
74
- } catch (error) {
75
- logger.error('Error checking/creating default profile image');
76
- console.log(error);
77
- }
78
56
  })();
79
57
 
80
58
  router.post(`/mailer/:id`, authMiddleware, async (req, res) => {
81
- /*
59
+ /*
82
60
  #swagger.ignore = true
83
61
  */
84
62
  return await UserController.post(req, res, options);
85
63
  });
86
64
 
65
+ router.get(`/assets/:id`, async (req, res) => {
66
+ /*
67
+ #swagger.ignore = true
68
+ */
69
+ return await UserController.get(req, res, options);
70
+ });
71
+
87
72
  router.get(`/mailer/:id`, async (req, res) => {
88
- /*
73
+ /*
89
74
  #swagger.ignore = true
90
75
  */
91
76
  return await UserController.get(req, res, options);
92
77
  });
93
78
 
94
79
  router.get(`/email/:email`, authMiddleware, async (req, res) => {
95
- /*
80
+ /*
96
81
  #swagger.ignore = true
97
82
  */
98
83
  return await UserController.get(req, res, options);
99
84
  });
100
85
 
101
86
  router.post(`/:id`, async (req, res) => {
102
- /*
87
+ /*
103
88
  #swagger.ignore = true
104
89
  */
105
90
  return await UserController.post(req, res, options);
@@ -125,11 +110,11 @@ const UserRouter = (options) => {
125
110
  'application/json': {
126
111
  schema: {
127
112
  $ref: '#/components/schemas/userLogInRequest'
128
- }
113
+ }
129
114
  }
130
115
  }
131
116
  }
132
-
117
+
133
118
  #swagger.responses[200] = {
134
119
  description: 'User created successfully',
135
120
  content: {
@@ -137,9 +122,9 @@ const UserRouter = (options) => {
137
122
  schema: {
138
123
  $ref: '#/components/schemas/userResponse'
139
124
  }
140
- }
125
+ }
141
126
  }
142
- }
127
+ }
143
128
 
144
129
  #swagger.responses[400] = {
145
130
  description: 'Bad request. Please check the input data',
@@ -148,9 +133,9 @@ const UserRouter = (options) => {
148
133
  schema: {
149
134
  $ref: '#/components/schemas/userBadRequestResponse'
150
135
  }
151
- }
136
+ }
152
137
  }
153
- }
138
+ }
154
139
  */
155
140
 
156
141
  // #swagger.end
@@ -174,11 +159,11 @@ const UserRouter = (options) => {
174
159
  'application/json': {
175
160
  schema: {
176
161
  $ref: '#/components/schemas/userRequest'
177
- }
162
+ }
178
163
  }
179
164
  }
180
165
  }
181
-
166
+
182
167
  #swagger.responses[200] = {
183
168
  description: 'User created successfully',
184
169
  content: {
@@ -186,9 +171,9 @@ const UserRouter = (options) => {
186
171
  schema: {
187
172
  $ref: '#/components/schemas/userResponse'
188
173
  }
189
- }
174
+ }
190
175
  }
191
- }
176
+ }
192
177
 
193
178
  #swagger.responses[400] = {
194
179
  description: 'Bad request. Please check the input data',
@@ -197,20 +182,63 @@ const UserRouter = (options) => {
197
182
  schema: {
198
183
  $ref: '#/components/schemas/userBadRequestResponse'
199
184
  }
200
- }
185
+ }
201
186
  }
202
- }
187
+ }
203
188
  */
204
189
  return await UserController.post(req, res, options);
205
190
  });
206
191
 
207
192
  router.get(`/recover/:id`, async (req, res) => {
208
- /*
193
+ /*
209
194
  #swagger.ignore = true
210
195
  */
211
196
  return await UserController.get(req, res, options);
212
197
  });
213
198
 
199
+ router.get(`/u/:username`, async (req, res) => {
200
+ /*
201
+ #swagger.auto = false
202
+ #swagger.tags = ['user']
203
+ #swagger.summary = 'Get public user profile'
204
+ #swagger.description = 'This endpoint gets public user profile data by username (no auth required)'
205
+ #swagger.path = '/user/u/{username}'
206
+ #swagger.method = 'get'
207
+ #swagger.produces = ['application/json']
208
+ #swagger.consumes = ['application/json']
209
+
210
+ #swagger.parameters['username'] = {
211
+ in: 'path',
212
+ description: 'User username',
213
+ required: true,
214
+ type: 'string'
215
+ }
216
+
217
+ #swagger.responses[200] = {
218
+ description: 'get public user profile successfully',
219
+ content: {
220
+ 'application/json': {
221
+ schema: {
222
+ $ref: '#/components/schemas/userPublicResponse'
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ #swagger.responses[400] = {
229
+ description: 'Bad request. Please check the input data',
230
+ content: {
231
+ 'application/json': {
232
+ schema: {
233
+ $ref: '#/components/schemas/userBadRequestResponse'
234
+ }
235
+ }
236
+ }
237
+ }
238
+ */
239
+ return await UserController.get(req, res, options);
240
+ });
241
+
214
242
  router.get(`/:id`, authMiddleware, async (req, res) => {
215
243
  /*
216
244
  #swagger.auto = false
@@ -223,7 +251,7 @@ const UserRouter = (options) => {
223
251
  #swagger.consumes = ['application/json']
224
252
  #swagger.security = [{
225
253
  'bearerAuth': []
226
- }]
254
+ }]
227
255
 
228
256
  #swagger.parameters['id'] = {
229
257
  in: 'path',
@@ -239,9 +267,9 @@ const UserRouter = (options) => {
239
267
  schema: {
240
268
  $ref: '#/components/schemas/userGetResponse'
241
269
  }
242
- }
270
+ }
243
271
  }
244
- }
272
+ }
245
273
 
246
274
  #swagger.responses[400] = {
247
275
  description: 'Bad request. Please check the input data',
@@ -250,26 +278,26 @@ const UserRouter = (options) => {
250
278
  schema: {
251
279
  $ref: '#/components/schemas/userBadRequestResponse'
252
280
  }
253
- }
281
+ }
254
282
  }
255
- }
283
+ }
256
284
  */
257
285
  return await UserController.get(req, res, options);
258
286
  });
259
287
  router.get(`/`, authMiddleware, async (req, res) => {
260
- /*
288
+ /*
261
289
  #swagger.ignore = true
262
290
  */
263
291
  return await UserController.get(req, res, options);
264
292
  });
265
293
  router.put(`/recover/:id`, async (req, res) => {
266
- /*
294
+ /*
267
295
  #swagger.ignore = true
268
296
  */
269
297
  return await UserController.put(req, res, options);
270
298
  });
271
299
  router.put(`/profile-image/:id`, authMiddleware, async (req, res) => {
272
- /*
300
+ /*
273
301
  #swagger.ignore = true
274
302
  */
275
303
  return await UserController.put(req, res, options);
@@ -286,8 +314,8 @@ const UserRouter = (options) => {
286
314
  #swagger.consumes = ['application/json']
287
315
  #swagger.security = [{
288
316
  'bearerAuth': []
289
- }]
290
-
317
+ }]
318
+
291
319
  #swagger.parameters['id'] = {
292
320
  in: 'path',
293
321
  description: 'User ID',
@@ -303,7 +331,7 @@ const UserRouter = (options) => {
303
331
  'application/json': {
304
332
  schema: {
305
333
  $ref: '#/components/schemas/userRequest'
306
- }
334
+ }
307
335
  }
308
336
  }
309
337
  }
@@ -315,9 +343,9 @@ const UserRouter = (options) => {
315
343
  schema: {
316
344
  $ref: '#/components/schemas/userUpdateResponse'
317
345
  }
318
- }
346
+ }
319
347
  }
320
- }
348
+ }
321
349
 
322
350
  #swagger.responses[400] = {
323
351
  description: 'Bad request. Please check the input data',
@@ -326,14 +354,14 @@ const UserRouter = (options) => {
326
354
  schema: {
327
355
  $ref: '#/components/schemas/userBadRequestResponse'
328
356
  }
329
- }
357
+ }
330
358
  }
331
- }
359
+ }
332
360
  */
333
361
  return await UserController.put(req, res, options);
334
362
  });
335
363
  router.put(`/`, authMiddleware, async (req, res) => {
336
- /*
364
+ /*
337
365
  #swagger.ignore = true
338
366
  */
339
367
  return await UserController.put(req, res, options);
@@ -351,7 +379,7 @@ const UserRouter = (options) => {
351
379
  #swagger.consumes = ['application/json']
352
380
  #swagger.security = [{
353
381
  'bearerAuth': []
354
- }]
382
+ }]
355
383
 
356
384
  #swagger.parameters['id'] = {
357
385
  in: 'path',
@@ -367,9 +395,9 @@ const UserRouter = (options) => {
367
395
  schema: {
368
396
  $ref: '#/components/schemas/userGetResponse'
369
397
  }
370
- }
398
+ }
371
399
  }
372
- }
400
+ }
373
401
 
374
402
  #swagger.responses[400] = {
375
403
  description: 'Bad request. Please check the input data',
@@ -378,20 +406,25 @@ const UserRouter = (options) => {
378
406
  schema: {
379
407
  $ref: '#/components/schemas/userBadRequestResponse'
380
408
  }
381
- }
409
+ }
382
410
  }
383
- }
411
+ }
384
412
  */
385
413
  return await UserController.delete(req, res, options);
386
414
  });
387
415
 
388
416
  router.delete(`/`, authMiddleware, async (req, res) => {
389
- /*
417
+ /*
390
418
  #swagger.ignore = true
391
419
  */
392
420
  return await UserController.delete(req, res, options);
393
421
  });
394
422
 
423
+ // Username public profile redirect
424
+ options.app.get(`${options.path === '/' ? '' : options.path}/u/:username`, async (req, res, next) =>
425
+ res.redirect(`${options.path === '/' ? '' : options.path}/u?cid=${req.params.username}`),
426
+ );
427
+
395
428
  return router;
396
429
  };
397
430
 
@@ -1,4 +1,5 @@
1
1
  import { loggerFactory } from '../../server/logger.js';
2
+ import { DataQuery } from '../../server/data-query.js';
2
3
  import {
3
4
  hashPassword,
4
5
  verifyPassword,
@@ -17,9 +18,10 @@ import { CoreWsEmit } from '../../ws/core/core.ws.emit.js';
17
18
  import { CoreWsMailerChannel } from '../../ws/core/channels/core.ws.mailer.js';
18
19
  import validator from 'validator';
19
20
  import { DataBaseProvider } from '../../db/DataBaseProvider.js';
20
- import { FileFactory } from '../file/file.service.js';
21
+ import { FileFactory, FileCleanup } from '../file/file.service.js';
21
22
  import { UserDto } from './user.model.js';
22
23
  import { selectDtoFactory, ValkeyAPI } from '../../server/valkey.js';
24
+ import { timer } from '../../client/components/core/CommonJs.js';
23
25
 
24
26
  const logger = loggerFactory(import.meta);
25
27
 
@@ -36,7 +38,11 @@ const UserService = {
36
38
  email: req.body.email,
37
39
  });
38
40
 
39
- if (!user) throw new Error('Email address does not exist');
41
+ // Simulate success even if email doesn't exist to prevent email enumeration attacks
42
+ if (!user) {
43
+ await timer(3000);
44
+ return { message: 'email send successfully' };
45
+ }
40
46
 
41
47
  const token = jwtSign({ email: req.body.email }, options, 15);
42
48
  const payloadToken = jwtSign({ email: req.body.email }, options, 15);
@@ -136,19 +142,23 @@ const UserService = {
136
142
  const { _id } = user;
137
143
  const validPassword = await verifyPassword(req.body.password, user.password);
138
144
  if (validPassword === true) {
139
- if (!user.profileImageId)
140
- await User.findByIdAndUpdate(
141
- user._id,
142
- { profileImageId: await options.getDefaultProfileImageId(File) },
143
- {
144
- runValidators: true,
145
- },
146
- );
147
145
  {
148
146
  if (getMinutesRemaining() <= 0 || user.failedLoginAttempts >= 0) {
149
147
  const user = await User.findOne({
150
148
  _id,
151
149
  }).select(UserDto.select.get());
150
+
151
+ // Check if profileImageId exists, if not set to null explicitly
152
+ if (!user.profileImageId) {
153
+ user.profileImageId = null;
154
+ } else {
155
+ const fileExists = await File.findById(user.profileImageId);
156
+ if (!fileExists) {
157
+ await User.findByIdAndUpdate(_id, { profileImageId: null }, { runValidators: true });
158
+ user.profileImageId = null;
159
+ }
160
+ }
161
+
152
162
  await User.findByIdAndUpdate(
153
163
  _id,
154
164
  { lastLoginDate: new Date(), failedLoginAttempts: 0 },
@@ -176,15 +186,18 @@ const UserService = {
176
186
  runValidators: true,
177
187
  },
178
188
  );
179
- setTimeout(async () => {
180
- await User.findByIdAndUpdate(
181
- _id,
182
- { failedLoginAttempts: 0 },
183
- {
184
- runValidators: true,
185
- },
186
- );
187
- }, 60 * 1000 * 15);
189
+ setTimeout(
190
+ async () => {
191
+ await User.findByIdAndUpdate(
192
+ _id,
193
+ { failedLoginAttempts: 0 },
194
+ {
195
+ runValidators: true,
196
+ },
197
+ );
198
+ },
199
+ 60 * 1000 * 15,
200
+ );
188
201
  throw new Error(`Account locked. Please try again in: 15 min.`);
189
202
  } else if (user.failedLoginAttempts < 0 && getMinutesRemaining() > 0) {
190
203
  throw new Error(accountLocketMessage());
@@ -225,9 +238,8 @@ const UserService = {
225
238
  };
226
239
  }
227
240
 
228
- default: {
229
- return await createUserAndSession(req, res, User, File, options);
230
- }
241
+ default:
242
+ return await createUserAndSession(req, res, User, options);
231
243
  }
232
244
  },
233
245
  get: async (req, res, options) => {
@@ -237,6 +249,26 @@ const UserService = {
237
249
  /** @type {import('../file/file.model.js').FileModel} */
238
250
  const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
239
251
 
252
+ if (req.path.startsWith('/u/')) {
253
+ // First lookup user by username
254
+ const userByUsername = await User.findOne({
255
+ username: req.params.username,
256
+ });
257
+ if (!userByUsername) throw new Error('User not found');
258
+ if (!userByUsername.publicProfile) throw new Error('Public profile is private');
259
+
260
+ // Then fetch complete public data by ID
261
+ const user = await User.findOne({
262
+ _id: userByUsername._id,
263
+ }).select(UserDto.public.get());
264
+ return user;
265
+ }
266
+
267
+ if (req.path.startsWith('/assets')) {
268
+ options.png.header(res);
269
+ return options.png.buffer[req.params.id];
270
+ }
271
+
240
272
  if (req.path.startsWith('/email')) {
241
273
  return await User.findOne({
242
274
  email: req.params.email,
@@ -303,12 +335,16 @@ const UserService = {
303
335
  switch (req.params.id) {
304
336
  case 'all': {
305
337
  if (req.auth.user.role === 'admin') {
306
- const page = parseInt(req.query.page) || 1;
307
- const limit = parseInt(req.query.limit) || 10;
308
- const skip = (page - 1) * limit;
338
+ // Use DataQuery.parse for filtering, sorting, and pagination
339
+ const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
309
340
 
310
- const data = await User.find().select(UserDto.select.get()).skip(skip).limit(limit);
311
- const total = await User.countDocuments();
341
+ // Apply default sort if no sort was specified
342
+ const finalSort = Object.keys(sort).length > 0 ? sort : { updatedAt: -1 };
343
+
344
+ const [data, total] = await Promise.all([
345
+ User.find(query).select(UserDto.select.get()).sort(finalSort).skip(skip).limit(limit),
346
+ User.countDocuments(query),
347
+ ]);
312
348
 
313
349
  return {
314
350
  data,
@@ -332,18 +368,6 @@ const UserService = {
332
368
 
333
369
  if (!user) throw new Error('user not found');
334
370
 
335
- const file = await File.findOne({ _id: user.profileImageId });
336
-
337
- if (!file && !(await ValkeyAPI.getValkeyObject(options, req.auth.user.email))) {
338
- await User.findByIdAndUpdate(
339
- user._id,
340
- { profileImageId: await options.getDefaultProfileImageId(File) },
341
- {
342
- runValidators: true,
343
- },
344
- );
345
- }
346
-
347
371
  const guestUser = await ValkeyAPI.getValkeyObject(options, req.auth.user.email);
348
372
  if (guestUser)
349
373
  return {
@@ -426,8 +450,18 @@ const UserService = {
426
450
  _id,
427
451
  });
428
452
  if (!user) throw new Error(`User not found`);
429
- if (user.profileImageId) await File.findByIdAndDelete(user.profileImageId);
453
+
430
454
  const [imageFile] = await FileFactory.upload(req, File);
455
+
456
+ // Clean up old profile image if being replaced
457
+ if (user.profileImageId && imageFile) {
458
+ await FileCleanup.cleanupReplacedFiles({
459
+ oldDoc: user,
460
+ newData: { profileImageId: imageFile._id.toString() },
461
+ fileFields: ['profileImageId'],
462
+ File,
463
+ });
464
+ }
431
465
  if (!imageFile) throw new Error('invalid file');
432
466
  await User.findByIdAndUpdate(
433
467
  _id,
@@ -479,14 +513,13 @@ const UserService = {
479
513
  const _id = req.auth.user._id;
480
514
  if (_id !== req.params.id) throw new Error(`Invalid token user id`);
481
515
  const user = await User.findOne({ _id });
482
- await User.findByIdAndUpdate(
483
- _id,
484
- {
485
- email: req.body.email && !user.emailConfirmed ? req.body.email : user.email,
486
- username: req.body.username,
487
- },
488
- { runValidators: true },
489
- );
516
+ const updateData = {
517
+ email: req.body.email && !user.emailConfirmed ? req.body.email : user.email,
518
+ username: req.body.username,
519
+ };
520
+ if (req.body.publicProfile !== undefined) updateData.publicProfile = req.body.publicProfile;
521
+ if (req.body.briefDescription !== undefined) updateData.briefDescription = req.body.briefDescription;
522
+ await User.findByIdAndUpdate(_id, updateData, { runValidators: true });
490
523
  return await User.findOne({
491
524
  _id,
492
525
  }).select(UserDto.select.get());