@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
package/src/cli/deploy.js CHANGED
@@ -761,7 +761,7 @@ EOF`);
761
761
  // shellExec(`docker cp ${volume.volumeMountPath} kind-worker:${rootVolumeHostPath}`);
762
762
  shellExec(`tar -C ${volume.volumeMountPath} -c . | docker cp - kind-worker:${rootVolumeHostPath}`);
763
763
  shellExec(
764
- `docker exec -i kind-worker bash -c "chown -R 1000:1000 ${rootVolumeHostPath} || true; chmod -R 755 ${rootVolumeHostPath}"`,
764
+ `docker exec -i kind-worker bash -c "chown -R 1000:1000 ${rootVolumeHostPath}; chmod -R 755 ${rootVolumeHostPath}"`,
765
765
  );
766
766
  }
767
767
  shellExec(`kubectl delete pvc ${pvcId} -n ${namespace} --ignore-not-found`);
package/src/cli/index.js CHANGED
@@ -22,7 +22,7 @@ program.name('underpost').description(`underpost ci/cd cli ${Underpost.version}`
22
22
  program
23
23
  .command('new')
24
24
  .argument('[app-name]', 'The name of the new project.')
25
- .option('--deploy-id <deploy-id>', 'Crete deploy ID conf env files')
25
+ .option('--deploy-id <deploy-id>', 'Create deploy ID conf env files')
26
26
  .option('--sub-conf <sub-conf>', 'Create sub conf env files')
27
27
  .option('--cluster', 'Create deploy ID cluster files and sync to current cluster')
28
28
  .option('--build-repos', 'Create deploy ID repositories')
@@ -362,6 +362,14 @@ program
362
362
  '--macro-rollback-export <n-commits-reset>',
363
363
  'Exports a macro rollback script that reverts the last n commits (Git integration required).',
364
364
  )
365
+ .option(
366
+ '--clean-fs-collection',
367
+ 'Cleans orphaned File documents from collections that are not referenced by any models.',
368
+ )
369
+ .option(
370
+ '--clean-fs-dry-run',
371
+ 'Dry run mode - shows what would be deleted without actually deleting (use with --clean-fs-collection).',
372
+ )
365
373
  .option('--dev', 'Sets the development cli context')
366
374
  .option('--kubeadm', 'Enables the kubeadm context for database operations.')
367
375
  .option('--kind', 'Enables the kind context for database operations.')
@@ -628,11 +636,14 @@ program
628
636
  .option('--nfs-build', 'Builds an NFS root filesystem for a workflow id config architecture using QEMU emulation.')
629
637
  .option('--nfs-mount', 'Mounts the NFS root filesystem for a workflow id config architecture.')
630
638
  .option('--nfs-unmount', 'Unmounts the NFS root filesystem for a workflow id config architecture.')
639
+ .option('--nfs-build-server', 'Builds the NFS server for a workflow id config architecture.')
631
640
  .option('--nfs-sh', 'Copies QEMU emulation root entrypoint shell command to the clipboard.')
632
641
  .option('--cloud-init', 'Sets the kernel parameters and sets the necessary seed users on the HTTP server.')
633
642
  .option('--cloud-init-update', 'Updates cloud init for a workflow id config architecture.')
634
643
  .option('--ubuntu-tools-build', 'Builds ubuntu tools for chroot environment.')
635
644
  .option('--ubuntu-tools-test', 'Tests ubuntu tools in chroot environment.')
645
+ .option('--rocky-tools-build', 'Builds rocky linux tools for chroot environment.')
646
+ .option('--rocky-tools-test', 'Tests rocky linux tools in chroot environment.')
636
647
  .option('--bootcmd <bootcmd-list>', 'Comma-separated list of boot commands to execute.')
637
648
  .option('--runcmd <runcmd-list>', 'Comma-separated list of run commands to execute.')
638
649
  .option('--logs <log-id>', 'Displays logs for log id: dhcp, cloud, machine, cloud-config.')
package/src/cli/lxd.js CHANGED
@@ -74,8 +74,8 @@ class UnderpostLxd {
74
74
  const npmRoot = getNpmRootPath();
75
75
  const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
76
76
  if (options.reset === true) {
77
- shellExec(`sudo systemctl stop snap.lxd.daemon || true`);
78
- shellExec(`sudo snap remove lxd --purge || true`);
77
+ shellExec(`sudo systemctl stop snap.lxd.daemon`);
78
+ shellExec(`sudo snap remove lxd --purge`);
79
79
  }
80
80
  if (options.install === true) shellExec(`sudo snap install lxd`);
81
81
  if (options.init === true) {
@@ -213,7 +213,7 @@ ipv6.address=none`);
213
213
  for (const port of ports.split(',')) {
214
214
  for (const protocol of protocols) {
215
215
  const deviceName = `${vmName}-${protocol}-port-${port}`;
216
- shellExec(`lxc config device remove ${vmName} ${deviceName} || true`); // Use || true to prevent error if device doesn't exist
216
+ shellExec(`lxc config device remove ${vmName} ${deviceName}`); // Use to prevent error if device doesn't exist
217
217
  shellExec(
218
218
  `lxc config device add ${vmName} ${deviceName} proxy listen=${protocol}:${hostIp}:${port} connect=${protocol}:${vmIp}:${port} nat=true`,
219
219
  );
@@ -679,7 +679,7 @@ Prevent build private config repo.`,
679
679
 
680
680
  /**
681
681
  * Internal method to recursively fetch and copy files from GitHub API.
682
- * @private
682
+ * @method
683
683
  * @param {object} options - Fetch options.
684
684
  * @param {string} options.apiUrl - The GitHub API URL.
685
685
  * @param {string} options.targetPath - The local target path.
package/src/cli/run.js CHANGED
@@ -313,6 +313,7 @@ class UnderpostRun {
313
313
  console.log('Loading fordward services...');
314
314
  await timer(5000);
315
315
  shellExec(`node bin metadata --generate ${path}`);
316
+ shellExec(`node bin db --dev --clean-fs-collection dd`);
316
317
  shellExec(`node bin run kill '${ports}'`);
317
318
  },
318
319
 
@@ -925,7 +926,7 @@ EOF
925
926
  shellExec(`docker exec -i kind-worker bash -c "mkdir -p ${volumeHostPath}"`);
926
927
  shellExec(`docker cp ${volumeHostPath}/engine kind-worker:${volumeHostPath}/engine`);
927
928
  shellExec(
928
- `docker exec -i kind-worker bash -c "chown -R 1000:1000 ${volumeHostPath} || true; chmod -R 755 ${volumeHostPath}"`,
929
+ `docker exec -i kind-worker bash -c "chown -R 1000:1000 ${volumeHostPath}; chmod -R 755 ${volumeHostPath}"`,
929
930
  );
930
931
  } else {
931
932
  shellExec(`kubectl apply -f ${options.underpostRoot}/manifests/pv-pvc-dd.yaml -n ${options.namespace}`);
package/src/cli/ssh.js CHANGED
@@ -22,7 +22,7 @@ class UnderpostSSH {
22
22
  static API = {
23
23
  /**
24
24
  * Loads configuration node from disk or returns default empty config.
25
- * @private
25
+ * @method
26
26
  * @function loadConfigNode
27
27
  * @memberof UnderpostSSH
28
28
  * @param {string} deployId - Deployment ID for the config path
@@ -37,7 +37,7 @@ class UnderpostSSH {
37
37
 
38
38
  /**
39
39
  * Saves configuration node to disk.
40
- * @private
40
+ * @method
41
41
  * @function saveConfigNode
42
42
  * @memberof UnderpostSSH
43
43
  * @param {string} confNodePath - Path to the configuration file
@@ -50,7 +50,7 @@ class UnderpostSSH {
50
50
 
51
51
  /**
52
52
  * Checks if a system user exists.
53
- * @private
53
+ * @method
54
54
  * @function checkUserExists
55
55
  * @memberof UnderpostSSH
56
56
  * @param {string} username - Username to check
@@ -66,7 +66,7 @@ class UnderpostSSH {
66
66
 
67
67
  /**
68
68
  * Gets the home directory for a given user.
69
- * @private
69
+ * @method
70
70
  * @function getUserHome
71
71
  * @memberof UnderpostSSH
72
72
  * @param {string} username - Username to get home directory for
@@ -81,7 +81,7 @@ class UnderpostSSH {
81
81
 
82
82
  /**
83
83
  * Creates a system user with password and groups.
84
- * @private
84
+ * @method
85
85
  * @function createSystemUser
86
86
  * @memberof UnderpostSSH
87
87
  * @param {string} username - Username to create
@@ -101,7 +101,7 @@ class UnderpostSSH {
101
101
 
102
102
  /**
103
103
  * Ensures SSH directory exists with proper permissions.
104
- * @private
104
+ * @method
105
105
  * @function ensureSSHDirectory
106
106
  * @memberof UnderpostSSH
107
107
  * @param {string} sshDir - Path to SSH directory
@@ -116,7 +116,7 @@ class UnderpostSSH {
116
116
 
117
117
  /**
118
118
  * Sets proper permissions on SSH files.
119
- * @private
119
+ * @method
120
120
  * @function setSSHFilePermissions
121
121
  * @memberof UnderpostSSH
122
122
  * @param {string} sshDir - SSH directory path
@@ -135,7 +135,7 @@ class UnderpostSSH {
135
135
 
136
136
  /**
137
137
  * Configures authorized_keys for a user.
138
- * @private
138
+ * @method
139
139
  * @function configureAuthorizedKeys
140
140
  * @memberof UnderpostSSH
141
141
  * @param {string} sshDir - SSH directory path
@@ -155,7 +155,7 @@ EOF`);
155
155
 
156
156
  /**
157
157
  * Configures known_hosts with SSH server keys.
158
- * @private
158
+ * @method
159
159
  * @function configureKnownHosts
160
160
  * @memberof UnderpostSSH
161
161
  * @param {string} sshDir - SSH directory path
@@ -171,7 +171,7 @@ EOF`);
171
171
 
172
172
  /**
173
173
  * Configures sudoers for passwordless sudo or sets user password.
174
- * @private
174
+ * @method
175
175
  * @function configureSudoAccess
176
176
  * @memberof UnderpostSSH
177
177
  * @param {string} username - Username to configure
@@ -1,4 +1,5 @@
1
1
  import { UserService } from '../../services/user/user.service.js';
2
+ import { FileService } from '../../services/file/file.service.js';
2
3
  import { Auth } from './Auth.js';
3
4
  import { BtnIcon } from './BtnIcon.js';
4
5
  import { newInstance, s4 } from './CommonJs.js';
@@ -8,10 +9,15 @@ import { fileFormDataFactory, Input } from './Input.js';
8
9
  import { LogIn } from './LogIn.js';
9
10
  import { Modal } from './Modal.js';
10
11
  import { NotificationManager } from './NotificationManager.js';
12
+ import { ToggleSwitch } from './ToggleSwitch.js';
11
13
  import { Translate } from './Translate.js';
12
14
  import { Validator } from './Validator.js';
13
15
  import { append, htmls, s } from './VanillaJs.js';
14
16
  import { getProxyPath } from './Router.js';
17
+ import { getApiBaseUrl } from '../../services/core/core.service.js';
18
+ import { loggerFactory } from './Logger.js';
19
+
20
+ const logger = loggerFactory(import.meta);
15
21
 
16
22
  const Account = {
17
23
  UpdateEvent: {},
@@ -30,7 +36,10 @@ const Account = {
30
36
  style="opacity: 1"
31
37
  ${LogIn.Scope.user.main.model.user.profileImage
32
38
  ? `src="${LogIn.Scope.user.main.model.user.profileImage.imageSrc}"`
33
- : ''}
39
+ : `src="${getApiBaseUrl({
40
+ id: 'assets/avatar',
41
+ endpoint: 'user',
42
+ })}"`}
34
43
  />
35
44
  </div>
36
45
  <div class="abs center account-profile-image-loading" style="color: white"></div>`,
@@ -40,7 +49,7 @@ const Account = {
40
49
  {
41
50
  model: 'username',
42
51
  id: `account-username`,
43
- rules: [{ type: 'isEmpty' }, { type: 'isLength', options: { min: 2, max: 20 } }],
52
+ rules: [{ type: 'isEmpty' }, { type: 'isLength', options: { min: 2, max: 20 } }, { type: 'isValidUsername' }],
44
53
  },
45
54
  { model: 'email', id: `account-email`, rules: [{ type: 'isEmpty' }, { type: 'isEmail' }] },
46
55
  {
@@ -49,11 +58,18 @@ const Account = {
49
58
  id: `account-password`,
50
59
  rules: [{ type: 'isStrongPassword' }],
51
60
  },
61
+ {
62
+ model: 'briefDescription',
63
+ id: `account-brief-description`,
64
+ defaultValue: 'Uploader',
65
+ rules: [{ type: 'isLength', options: { min: 0, max: 200 } }],
66
+ },
52
67
  ];
53
68
 
54
69
  this.formData = formData;
55
70
 
56
71
  this.instanceModalUiEvents = async ({ user }) => {
72
+ const accountInstance = this;
57
73
  const validators = await Validator.instance(formData);
58
74
 
59
75
  for (const inputData of formData) {
@@ -62,18 +78,41 @@ const Account = {
62
78
  }
63
79
  let lastUser;
64
80
  const submit = async () => {
65
- lastUser = newInstance(user);
66
- const { successKeys } = await validators();
67
- if (successKeys.length === 0) return;
81
+ // Always get the current user from LogIn.Scope to avoid stale closure references
82
+ const currentUser = LogIn.Scope.user.main.model.user;
83
+ if (!currentUser || !currentUser._id) {
84
+ NotificationManager.Push({
85
+ html: Translate.Render('error-user-not-authenticated'),
86
+ status: 'error',
87
+ });
88
+ return;
89
+ }
90
+ // Guest users cannot submit form
91
+ if (currentUser.role === 'guest') {
92
+ NotificationManager.Push({
93
+ html: Translate.Render('error-user-not-authenticated'),
94
+ status: 'error',
95
+ });
96
+ return;
97
+ }
98
+ lastUser = newInstance(currentUser);
99
+ const { successKeys, errorKeys, errorMessage } = await validators();
100
+ if (errorMessage) {
101
+ NotificationManager.Push({
102
+ html: `${errorKeys.map((e) => Translate.Render(e.replace('account-', '')))} ${errorMessage}`,
103
+ });
104
+ return;
105
+ }
68
106
  const body = {};
69
107
  for (const inputData of formData) {
70
- if (!s(`.${inputData.id}`).value || s(`.${inputData.id}`).value === 'undefined') continue;
108
+ const value = s(`.${inputData.id}`).value;
109
+ if (!value || value === 'undefined') continue;
71
110
  if ('model' in inputData && successKeys.includes(inputData.id)) {
72
- body[inputData.model] = s(`.${inputData.id}`).value;
73
- user[inputData.model] = s(`.${inputData.id}`).value;
111
+ body[inputData.model] = value;
112
+ currentUser[inputData.model] = value;
74
113
  }
75
114
  }
76
- const result = await UserService.put({ id: user._id, body });
115
+ const result = await UserService.put({ id: currentUser._id, body });
77
116
  NotificationManager.Push({
78
117
  html:
79
118
  result.status === 'error' && result.message
@@ -82,12 +121,18 @@ const Account = {
82
121
  status: result.status,
83
122
  });
84
123
  if (result.status === 'success') {
85
- user = result.data;
86
- this.triggerUpdateEvent({ user });
87
- if (lastUser.emailConfirmed !== user.emailConfirmed) {
88
- this.renderVerifyEmailStatus(user);
124
+ const updatedUser = result.data;
125
+ // Preserve profileImage from scope if it exists
126
+ const existingProfileImage = LogIn.Scope.user.main.model.user.profileImage;
127
+ LogIn.Scope.user.main.model.user = { ...updatedUser };
128
+ if (existingProfileImage && !updatedUser.profileImage) {
129
+ LogIn.Scope.user.main.model.user.profileImage = existingProfileImage;
89
130
  }
90
- lastUser = newInstance(user);
131
+ accountInstance.triggerUpdateEvent({ user: updatedUser });
132
+ if (lastUser.emailConfirmed !== updatedUser.emailConfirmed) {
133
+ accountInstance.renderVerifyEmailStatus(updatedUser);
134
+ }
135
+ lastUser = newInstance(updatedUser);
91
136
  }
92
137
  };
93
138
  EventsUI.onClick(`.btn-account`, async (e) => {
@@ -102,6 +147,23 @@ const Account = {
102
147
  if (s(`.btn-confirm-email`))
103
148
  EventsUI.onClick(`.btn-confirm-email`, async (e) => {
104
149
  e.preventDefault();
150
+ // Check if user is authenticated
151
+ const currentUser = LogIn.Scope.user.main.model.user;
152
+ if (!currentUser || !currentUser._id) {
153
+ NotificationManager.Push({
154
+ html: Translate.Render('error-user-not-authenticated'),
155
+ status: 'error',
156
+ });
157
+ return;
158
+ }
159
+ // Guest users cannot verify email
160
+ if (currentUser.role === 'guest') {
161
+ NotificationManager.Push({
162
+ html: Translate.Render('error-user-not-authenticated'),
163
+ status: 'error',
164
+ });
165
+ return;
166
+ }
105
167
  const result = await UserService.post({
106
168
  id: 'mailer/verify-email',
107
169
  body: {
@@ -115,7 +177,8 @@ const Account = {
115
177
  status: result.status,
116
178
  });
117
179
  });
118
- this.renderVerifyEmailStatus(user);
180
+ const currentUser = LogIn.Scope.user.main.model.user;
181
+ accountInstance.renderVerifyEmailStatus(currentUser || user);
119
182
 
120
183
  s(`.${waveAnimationId}`).style.cursor = 'pointer';
121
184
  s(`.${waveAnimationId}`).onclick = async (e) => {
@@ -129,20 +192,79 @@ const Account = {
129
192
  s(`.account-profile-image`).style.opacity = 0;
130
193
  const formFile = fileFormDataFactory(e, profileFileAccept);
131
194
 
195
+ // Always get the current user from LogIn.Scope
196
+ const currentUser = LogIn.Scope.user.main.model.user;
197
+ if (!currentUser || !currentUser._id) {
198
+ NotificationManager.Push({
199
+ html: Translate.Render('error-user-not-authenticated'),
200
+ status: 'error',
201
+ });
202
+ s(`.account-profile-image`).style.opacity = 1;
203
+ return;
204
+ }
205
+ // Guest users cannot upload profile images
206
+ if (currentUser.role === 'guest') {
207
+ NotificationManager.Push({
208
+ html: Translate.Render('error-user-not-authenticated'),
209
+ status: 'error',
210
+ });
211
+ s(`.account-profile-image`).style.opacity = 1;
212
+ return;
213
+ }
214
+
132
215
  const { status, data } = await UserService.put({
133
- id: `profile-image/${user._id}`,
216
+ id: `profile-image/${currentUser._id}`,
134
217
  body: formFile,
135
218
  headerId: 'file',
136
219
  });
137
220
 
138
221
  if (status === 'success') {
139
- user.profileImageId = data.profileImageId;
222
+ currentUser.profileImageId = data.profileImageId;
223
+ LogIn.Scope.user.main.model.user = { ...currentUser };
140
224
  delete LogIn.Scope.user.main.model.user.profileImage;
141
- await LogIn.Trigger({ user });
142
- s(`.account-profile-image`).src = LogIn.Scope.user.main.model.user.profileImage.imageSrc;
225
+
226
+ const defaultAvatarUrl = getApiBaseUrl({
227
+ id: 'assets/avatar',
228
+ endpoint: 'user',
229
+ });
230
+
231
+ // Fetch the new image immediately
232
+ let newImageSrc = defaultAvatarUrl;
233
+ try {
234
+ const resultFile = await FileService.get({ id: data.profileImageId });
235
+ if (resultFile && resultFile.status === 'success' && resultFile.data[0]) {
236
+ const imageData = resultFile.data[0];
237
+
238
+ if (!imageData.data?.data && imageData._id) {
239
+ newImageSrc = getApiBaseUrl({ id: imageData._id, endpoint: 'file/blob' });
240
+ } else if (imageData.data?.data) {
241
+ const imageBlob = new Blob([new Uint8Array(imageData.data.data)], { type: imageData.mimetype });
242
+ const imageFile = new File([imageBlob], imageData.name, { type: imageData.mimetype });
243
+ newImageSrc = URL.createObjectURL(imageFile);
244
+ }
245
+
246
+ LogIn.Scope.user.main.model.user.profileImage = {
247
+ resultFile,
248
+ imageData,
249
+ imageSrc: newImageSrc,
250
+ };
251
+ }
252
+ } catch (error) {
253
+ logger.warn('Error fetching new profile image:', error);
254
+ }
255
+
256
+ // Update both images immediately
257
+ s(`.account-profile-image`).src = newImageSrc;
258
+ const topbarImg = s(`.top-box-profile-img`);
259
+ if (topbarImg) topbarImg.src = newImageSrc;
260
+
261
+ NotificationManager.Push({
262
+ html: Translate.Render('success-update-user'),
263
+ status: 'success',
264
+ });
143
265
  } else {
144
266
  NotificationManager.Push({
145
- html: Translate.Render('file-upload-failed'),
267
+ html: data?.message || Translate.Render('file-upload-failed'),
146
268
  status: 'error',
147
269
  });
148
270
  }
@@ -177,9 +299,116 @@ const Account = {
177
299
  },
178
300
  { context: 'modal' },
179
301
  );
302
+ EventsUI.onClick(`.btn-brief-description-update`, async (e) => {
303
+ e.preventDefault();
304
+ const descriptionValue = s(`.account-brief-description`).value;
305
+ if (!descriptionValue || descriptionValue === 'undefined' || descriptionValue.trim() === '') {
306
+ NotificationManager.Push({
307
+ html: Translate.Render('brief-description-cannot-be-empty'),
308
+ status: 'error',
309
+ });
310
+ return;
311
+ }
312
+ // Always get the current user from LogIn.Scope
313
+ const currentUser = LogIn.Scope.user.main.model.user;
314
+ if (!currentUser || !currentUser._id) {
315
+ NotificationManager.Push({
316
+ html: Translate.Render('error-user-not-authenticated'),
317
+ status: 'error',
318
+ });
319
+ return;
320
+ }
321
+ // Guest users cannot update brief description
322
+ if (currentUser.role === 'guest') {
323
+ NotificationManager.Push({
324
+ html: Translate.Render('error-user-not-authenticated'),
325
+ status: 'error',
326
+ });
327
+ return;
328
+ }
329
+ const result = await UserService.put({ id: currentUser._id, body: { briefDescription: descriptionValue } });
330
+ NotificationManager.Push({
331
+ html:
332
+ result.status === 'error' && result.message
333
+ ? result.message
334
+ : Translate.Render(`${result.status}-update-user`),
335
+ status: result.status,
336
+ });
337
+ if (result.status === 'success') {
338
+ currentUser.briefDescription = descriptionValue;
339
+ // Preserve profileImage from scope
340
+ const existingProfileImage = LogIn.Scope.user.main.model.user.profileImage;
341
+ LogIn.Scope.user.main.model.user.briefDescription = descriptionValue;
342
+ if (existingProfileImage) {
343
+ LogIn.Scope.user.main.model.user.profileImage = existingProfileImage;
344
+ }
345
+ accountInstance.triggerUpdateEvent({ user: currentUser });
346
+ }
347
+ });
348
+
349
+ // Setup public profile toggle handler
350
+ setTimeout(() => {
351
+ if (ToggleSwitch.Tokens['account-public-profile']) {
352
+ const originalClick = ToggleSwitch.Tokens['account-public-profile'].click;
353
+ ToggleSwitch.Tokens['account-public-profile'].click = async function () {
354
+ originalClick.call(this);
355
+ const isChecked = s(`.account-public-profile-checkbox`).checked;
356
+ // Always get the current user from LogIn.Scope
357
+ const currentUser = LogIn.Scope.user.main.model.user;
358
+ if (!currentUser || !currentUser._id) {
359
+ NotificationManager.Push({
360
+ html: Translate.Render('error-user-not-authenticated'),
361
+ status: 'error',
362
+ });
363
+ return;
364
+ }
365
+ // Guest users cannot toggle public profile
366
+ if (currentUser.role === 'guest') {
367
+ NotificationManager.Push({
368
+ html: Translate.Render('error-user-not-authenticated'),
369
+ status: 'error',
370
+ });
371
+ return;
372
+ }
373
+ const result = await UserService.put({ id: currentUser._id, body: { publicProfile: isChecked } });
374
+ NotificationManager.Push({
375
+ html:
376
+ result.status === 'error' && result.message
377
+ ? result.message
378
+ : Translate.Render(`${result.status}-update-user`),
379
+ status: result.status,
380
+ });
381
+ if (result.status === 'success') {
382
+ currentUser.publicProfile = isChecked;
383
+ // Preserve profileImage from scope
384
+ const existingProfileImage = LogIn.Scope.user.main.model.user.profileImage;
385
+ LogIn.Scope.user.main.model.user.publicProfile = isChecked;
386
+ if (existingProfileImage) {
387
+ LogIn.Scope.user.main.model.user.profileImage = existingProfileImage;
388
+ }
389
+ accountInstance.triggerUpdateEvent({ user: currentUser });
390
+ }
391
+ };
392
+
393
+ // Override wrapper click handler to use our custom handler
394
+ const wrapperElement = s(`.toggle-form-container-account-public-profile`);
395
+ if (wrapperElement) {
396
+ wrapperElement.onclick = () => ToggleSwitch.Tokens['account-public-profile'].click();
397
+ }
398
+ }
399
+ });
180
400
  EventsUI.onClick(`.btn-account-delete`, async (e) => {
181
401
  e.preventDefault();
182
- const result = await UserService.delete({ id: user._id });
402
+ // Always get the current user from LogIn.Scope
403
+ const currentUser = LogIn.Scope.user.main.model.user;
404
+ if (!currentUser || !currentUser._id) {
405
+ NotificationManager.Push({
406
+ html: Translate.Render('error-user-not-authenticated'),
407
+ status: 'error',
408
+ });
409
+ return;
410
+ }
411
+ const result = await UserService.delete({ id: currentUser._id });
183
412
  NotificationManager.Push({
184
413
  html: result.status === 'error' ? result.message : Translate.Render(`success-delete-account`),
185
414
  status: result.status,
@@ -233,16 +462,17 @@ const Account = {
233
462
  autocomplete: 'email',
234
463
  disabled: user.emailConfirmed,
235
464
  extension: !(options && options.disabled && options.disabled.includes('emailConfirm'))
236
- ? async () => html`<div class="in verify-email-status"></div>
237
- ${await BtnIcon.Render({
238
- class: `in wfa btn-input-extension btn-confirm-email`,
239
- type: 'button',
240
- style: 'text-align: left',
241
- label: html`<div class="in">
242
- <i class="fa-solid fa-paper-plane"></i> ${Translate.Render('send')}
243
- ${Translate.Render('verify-email')}
244
- </div> `,
245
- })}`
465
+ ? async () =>
466
+ html`<div class="in verify-email-status"></div>
467
+ ${await BtnIcon.Render({
468
+ class: `in wfa btn-input-extension btn-confirm-email`,
469
+ type: 'button',
470
+ style: 'text-align: left',
471
+ label: html`<div class="in">
472
+ <i class="fa-solid fa-paper-plane"></i> ${Translate.Render('send')}
473
+ ${Translate.Render('verify-email')}
474
+ </div> `,
475
+ })}`
246
476
  : undefined,
247
477
  })}
248
478
  </div>
@@ -265,6 +495,31 @@ const Account = {
265
495
  })}`,
266
496
  })}
267
497
  </div>
498
+ <div class="in">
499
+ ${await Input.Render({
500
+ id: `account-brief-description`,
501
+ label: html`<i class="fa-solid fa-pen-fancy"></i> ${Translate.Render('brief-description')}`,
502
+ containerClass: 'inl section-mp width-mini-box input-container',
503
+ placeholder: true,
504
+ rows: 4,
505
+ extension: async () =>
506
+ html`${await BtnIcon.Render({
507
+ class: `in wfa btn-input-extension btn-brief-description-update`,
508
+ type: 'button',
509
+ style: 'text-align: left',
510
+ label: html`${Translate.Render(`update`)}`,
511
+ })}`,
512
+ })}
513
+ </div>
514
+ <div class="in section-mp">
515
+ ${await ToggleSwitch.Render({
516
+ wrapper: true,
517
+ wrapperLabel: html`<i class="fa-solid fa-globe"></i> ${Translate.Render('public-profile')}`,
518
+ id: 'account-public-profile',
519
+ disabledOnClick: true,
520
+ checked: user.publicProfile ? true : false,
521
+ })}
522
+ </div>
268
523
  ${options?.bottomRender ? await options.bottomRender() : ``}
269
524
  <div class="in hide">
270
525
  ${await BtnIcon.Render({
@@ -312,13 +567,49 @@ const Account = {
312
567
  instanceModalUiEvents: async (user) => null,
313
568
  updateForm: async function (user) {
314
569
  if (!s(`.modal-account`)) return;
315
- await this.instanceModalUiEvents({ user });
570
+
571
+ // Always sync the current user data into LogIn.Scope, preserving profileImage if it exists
572
+ if (user && user._id) {
573
+ const existingProfileImage = LogIn.Scope.user.main.model.user.profileImage;
574
+ LogIn.Scope.user.main.model.user = { ...user };
575
+ // Preserve existing profileImage if new user doesn't have one
576
+ if (existingProfileImage && !user.profileImage) {
577
+ LogIn.Scope.user.main.model.user.profileImage = existingProfileImage;
578
+ }
579
+ }
580
+
581
+ // Use the current user from scope to ensure we have the latest data
582
+ const currentUser = LogIn.Scope.user.main.model.user;
583
+ if (!currentUser || !currentUser._id) {
584
+ return;
585
+ }
586
+
587
+ await this.instanceModalUiEvents({ user: currentUser });
316
588
  s(`.account-profile-image`).style.opacity = 0;
317
589
  for (const inputData of this.formData)
318
- if (s(`.${inputData.id}`)) s(`.${inputData.id}`).value = user[inputData.model];
319
- if (LogIn.Scope.user.main.model.user.profileImage) {
320
- s(`.account-profile-image`).src = LogIn.Scope.user.main.model.user.profileImage.imageSrc;
321
- s(`.account-profile-image`).style.opacity = 1;
590
+ if (s(`.${inputData.id}`)) s(`.${inputData.id}`).value = currentUser[inputData.model];
591
+
592
+ // Update profile image - always show default avatar as fallback (skip for guest users)
593
+ const profileImageElement = s(`.account-profile-image`);
594
+ const defaultAvatarUrl = getApiBaseUrl({
595
+ id: 'assets/avatar',
596
+ endpoint: 'user',
597
+ });
598
+
599
+ if (currentUser.role !== 'guest' && profileImageElement) {
600
+ // Show custom image if available, otherwise default avatar
601
+ const customImageSrc = LogIn.Scope.user.main.model.user.profileImage?.imageSrc;
602
+ profileImageElement.src = customImageSrc || defaultAvatarUrl;
603
+ profileImageElement.style.opacity = 1;
604
+ } else if (profileImageElement) {
605
+ profileImageElement.style.opacity = 0;
606
+ }
607
+
608
+ // update public profile toggle
609
+ if (ToggleSwitch.Tokens['account-public-profile']) {
610
+ if (currentUser.publicProfile && !s(`.account-public-profile-checkbox`).checked) {
611
+ ToggleSwitch.Tokens['account-public-profile'].click();
612
+ }
322
613
  }
323
614
  },
324
615
  };