@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.
- package/README.md +2 -2
- package/baremetal/commission-workflows.json +33 -3
- package/bin/deploy.js +1 -1
- package/cli.md +7 -2
- package/conf.js +3 -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/packer/scripts/fuse-tar-root +3 -3
- package/scripts/disk-clean.sh +23 -23
- package/scripts/gpu-diag.sh +2 -2
- package/scripts/ip-info.sh +11 -11
- package/scripts/maas-upload-boot-resource.sh +1 -1
- package/scripts/nvim.sh +1 -1
- package/scripts/packer-setup.sh +13 -13
- package/scripts/rocky-setup.sh +2 -2
- package/scripts/rpmfusion-ffmpeg-setup.sh +4 -4
- package/scripts/ssl.sh +7 -7
- 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 +30 -1
- package/src/api/document/document.router.js +6 -0
- package/src/api/document/document.service.js +423 -51
- 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/baremetal.js +689 -329
- package/src/cli/cluster.js +50 -52
- package/src/cli/db.js +424 -166
- package/src/cli/deploy.js +1 -1
- package/src/cli/index.js +12 -1
- package/src/cli/lxd.js +3 -3
- package/src/cli/repository.js +1 -1
- package/src/cli/run.js +2 -1
- 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/Css.js +16 -2
- 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 +257 -177
- package/src/client/components/core/Panel.js +324 -27
- package/src/client/components/core/PanelForm.js +280 -73
- 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 +1117 -0
- 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/document/document.service.js +23 -0
- 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/dns.js +4 -4
- package/src/server/object-layer.js +0 -3
- 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}
|
|
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>', '
|
|
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
|
|
78
|
-
shellExec(`sudo snap remove lxd --purge
|
|
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}
|
|
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
|
);
|
package/src/cli/repository.js
CHANGED
|
@@ -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
|
-
* @
|
|
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}
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
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
|
-
|
|
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] =
|
|
73
|
-
|
|
111
|
+
body[inputData.model] = value;
|
|
112
|
+
currentUser[inputData.model] = value;
|
|
74
113
|
}
|
|
75
114
|
}
|
|
76
|
-
const result = await UserService.put({ id:
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/${
|
|
216
|
+
id: `profile-image/${currentUser._id}`,
|
|
134
217
|
body: formFile,
|
|
135
218
|
headerId: 'file',
|
|
136
219
|
});
|
|
137
220
|
|
|
138
221
|
if (status === 'success') {
|
|
139
|
-
|
|
222
|
+
currentUser.profileImageId = data.profileImageId;
|
|
223
|
+
LogIn.Scope.user.main.model.user = { ...currentUser };
|
|
140
224
|
delete LogIn.Scope.user.main.model.user.profileImage;
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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 () =>
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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 =
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
};
|