iobroker.eos-admin 7.9.98 → 7.9.99
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/INSTALL_TEST_V7.9.99.md +11 -0
- package/MERGE_UPDATE_README_V7.9.99.md +5 -0
- package/NEXOWATT_EOS_BUILD_INFO.json +21 -20
- package/PUBLISH_STABLE_V7.9.99.md +15 -0
- package/README.md +6 -16
- package/README_STABILITY_V7.9.99.md +18 -0
- package/RELEASE_ACCEPTANCE_V7.9.99.md +11 -0
- package/RELEASE_NOTES_V7.9.99.md +9 -0
- package/adminWww/assets/Intro-DkwRiz1n-v84.js +2 -2
- package/adminWww/assets/bootstrap-COulQZax-v84.js +2 -2
- package/adminWww/css/eos-ems-overview.css +3 -5
- package/adminWww/css/nexowatt-native-shell.css +26 -112
- package/adminWww/index.html +19 -18
- package/adminWww/js/eos-account-management.js +19 -28
- package/adminWww/js/eos-assistant.js +1 -1
- package/adminWww/js/eos-ems-overview.js +2 -3
- package/adminWww/js/eos-release-watch.js +77 -0
- package/adminWww/js/eos-role-bootstrap.js +11 -8
- package/adminWww/js/eos-role-ui.js +2 -2
- package/adminWww/js/nexowatt-native-shell.js +1 -1
- package/adminWww/static/js/nexowatt-stable-login.js +3 -3
- package/adminWww/static/js/{nexowatt-stable-v98.js → nexowatt-stable-v99.js} +2 -2
- package/build/lib/eosPassword.js +62 -0
- package/build/lib/eosRequestSecurity.js +31 -0
- package/build/lib/web.js +149 -154
- package/build/main.js +2 -1
- package/io-package.json +5 -10
- package/package.json +17 -13
- package/tools/nexowatt-account-management-selftest.cjs +19 -15
- package/tools/nexowatt-apply-ems-overview.cjs +2 -4
- package/tools/nexowatt-cache-update-selftest.cjs +11 -0
- package/tools/nexowatt-clean-legacy-runtime.cjs +1 -1
- package/tools/nexowatt-ems-overview-selftest.cjs +1 -1
- package/tools/nexowatt-login-layout-selftest.cjs +11 -11
- package/tools/nexowatt-merge-update-selftest.cjs +1 -1
- package/tools/nexowatt-modern-ui-selftest.cjs +2 -2
- package/tools/nexowatt-password-write-selftest.cjs +42 -44
- package/tools/nexowatt-request-security-selftest.cjs +11 -0
- package/tools/nexowatt-runtime-cleanup-selftest.cjs +6 -6
- package/tools/nexowatt-stable-v99-selftest.cjs +25 -0
- package/tools/nexowatt-sync-release-version.cjs +1 -42
- package/tools/nexowatt-ui-overview-runtime-selftest.cjs +3 -1
- package/tools/nexowatt-validate-package.cjs +23 -23
- package/tools/nexowatt-version-sync-selftest.cjs +1 -18
- package/INSTALL_TEST_V7.9.98.md +0 -39
- package/MERGE_UPDATE_README_V7.9.98.md +0 -29
- package/PUBLISH_STABLE_V7.9.98.md +0 -33
- package/README_STABILITY_V7.9.98.md +0 -22
- package/RELEASE_ACCEPTANCE_V7.9.98.md +0 -48
- package/RELEASE_NOTES_V7.9.98.md +0 -32
- package/tools/nexowatt-account-password-flow-selftest.cjs +0 -186
- package/tools/nexowatt-stable-v98-selftest.cjs +0 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
|
-
const VERSION = '
|
|
3
|
+
const VERSION = 'v99-compact-login-ems-connection';
|
|
4
4
|
const MANAGED_USERS = new Set(['installer', 'guest', 'user']);
|
|
5
5
|
const normalizeUser = value => String(value || '').trim().toLowerCase().replace(/^system\.user\./, '');
|
|
6
6
|
const apply = () => {
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
document.addEventListener('change', schedule, true);
|
|
30
30
|
};
|
|
31
31
|
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', start, { once: true }) : start();
|
|
32
|
-
window.
|
|
32
|
+
window.NEXOWATT_EOS_STABLE_V99 = Object.freeze({ version: VERSION, refresh: apply });
|
|
33
33
|
})();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeEosPasswordTarget = normalizeEosPasswordTarget;
|
|
4
|
+
exports.setEosUserPasswordWithVerification = setEosUserPasswordWithVerification;
|
|
5
|
+
function normalizeEosPasswordTarget(value) {
|
|
6
|
+
const raw = String(value || '').trim();
|
|
7
|
+
const userId = raw.startsWith('system.user.') ? raw : `system.user.${raw}`;
|
|
8
|
+
const userName = userId.replace(/^system\.user\./, '').trim();
|
|
9
|
+
if (!userName || userName.includes('.') || !/^system\.user\.[^.]+$/.test(userId)) {
|
|
10
|
+
throw new Error('invalidPasswordTarget');
|
|
11
|
+
}
|
|
12
|
+
return { userId, userName };
|
|
13
|
+
}
|
|
14
|
+
async function callPasswordApi(adapter, userName, password, serviceUser) {
|
|
15
|
+
const options = { user: serviceUser };
|
|
16
|
+
if (typeof adapter.setPasswordAsync === 'function') {
|
|
17
|
+
await adapter.setPasswordAsync(userName, password, options);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (typeof adapter.setPassword === 'function') {
|
|
21
|
+
await new Promise((resolve, reject) => {
|
|
22
|
+
adapter.setPassword?.(userName, password, options, error => (error ? reject(error) : resolve()));
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw new Error('passwordApiUnavailable');
|
|
27
|
+
}
|
|
28
|
+
async function waitForPasswordHash(adapter, userId) {
|
|
29
|
+
for (let attempt = 0; attempt < 6; attempt++) {
|
|
30
|
+
const user = await adapter.getForeignObjectAsync(userId);
|
|
31
|
+
if (typeof user?.common?.password === 'string' && user.common.password.length > 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (attempt < 5) {
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve, 40 * (attempt + 1)));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw new Error('passwordNotPersisted');
|
|
39
|
+
}
|
|
40
|
+
async function verifyCredential(adapter, userName, password, serviceUser) {
|
|
41
|
+
if (typeof adapter.checkPasswordAsync !== 'function') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
45
|
+
const result = await adapter.checkPasswordAsync(userName, password, { user: serviceUser });
|
|
46
|
+
const accepted = result === true || (Array.isArray(result) && result[0] === true);
|
|
47
|
+
if (accepted) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (attempt < 4) {
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, 50 * (attempt + 1)));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error('passwordVerificationFailed');
|
|
55
|
+
}
|
|
56
|
+
async function setEosUserPasswordWithVerification(adapter, target, password, serviceUser = 'system.user.admin') {
|
|
57
|
+
const normalized = normalizeEosPasswordTarget(target);
|
|
58
|
+
await callPasswordApi(adapter, normalized.userName, password, serviceUser);
|
|
59
|
+
await waitForPasswordHash(adapter, normalized.userId);
|
|
60
|
+
await verifyCredential(adapter, normalized.userName, password, serviceUser);
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isEosSameOriginRequest = isEosSameOriginRequest;
|
|
4
|
+
function isEosSameOriginRequest(headers) {
|
|
5
|
+
const origin = String(headers.origin || '').trim();
|
|
6
|
+
if (!origin) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const parsed = new URL(origin);
|
|
11
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const allowedHosts = new Set();
|
|
15
|
+
const addHost = value => {
|
|
16
|
+
const host = String(value || '').split(',')[0].trim().toLowerCase();
|
|
17
|
+
if (host)
|
|
18
|
+
allowedHosts.add(host);
|
|
19
|
+
};
|
|
20
|
+
addHost(headers.host);
|
|
21
|
+
addHost(headers['x-forwarded-host']);
|
|
22
|
+
if (!allowedHosts.size || !allowedHosts.has(parsed.host.toLowerCase())) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const fetchSite = String(headers['sec-fetch-site'] || '').trim().toLowerCase();
|
|
26
|
+
return !fetchSite || fetchSite === 'same-origin' || fetchSite === 'same-site' || fetchSite === 'none';
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/build/lib/web.js
CHANGED
|
@@ -21,6 +21,8 @@ const session = require("express-session");
|
|
|
21
21
|
const bodyParser = require("body-parser");
|
|
22
22
|
const cookieParser = require("cookie-parser");
|
|
23
23
|
const iobroker_mcp_1 = require("iobroker.mcp");
|
|
24
|
+
const eosPassword_1 = require("./eosPassword");
|
|
25
|
+
const eosRequestSecurity_1 = require("./eosRequestSecurity");
|
|
24
26
|
let AdapterStore;
|
|
25
27
|
/** Content of a socket-io file */
|
|
26
28
|
let socketIoFile;
|
|
@@ -713,54 +715,10 @@ class Web {
|
|
|
713
715
|
}
|
|
714
716
|
return { valid: true, password: value };
|
|
715
717
|
}
|
|
716
|
-
getEosPasswordUserName(userId) {
|
|
717
|
-
const userName = String(userId || '').trim().replace(/^system\.user\./, '');
|
|
718
|
-
if (!/^[A-Za-z0-9_.@-]+$/.test(userName)) {
|
|
719
|
-
throw new Error('invalidPasswordTarget');
|
|
720
|
-
}
|
|
721
|
-
return userName;
|
|
722
|
-
}
|
|
723
718
|
async setEosUserPassword(userId, password) {
|
|
724
|
-
|
|
725
|
-
const userName = this.getEosPasswordUserName(userId);
|
|
726
|
-
// The server-side Admin/Service context performs the controller password write, so managed
|
|
727
|
-
// Installer and End User accounts do not need global users.write rights in the browser.
|
|
728
|
-
const options = { user: EOS_PASSWORD_SERVICE_USER };
|
|
729
|
-
if (typeof this.adapter.setPasswordAsync === 'function') {
|
|
730
|
-
await this.adapter.setPasswordAsync(userName, password, options);
|
|
731
|
-
}
|
|
732
|
-
else if (typeof this.adapter.setPassword === 'function') {
|
|
733
|
-
await new Promise((resolve, reject) => {
|
|
734
|
-
this.adapter.setPassword?.(userName, password, options, error => (error ? reject(error) : resolve()));
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
else {
|
|
738
|
-
throw new Error('passwordApiUnavailable');
|
|
739
|
-
}
|
|
740
|
-
if (typeof this.adapter.checkPasswordAsync === 'function') {
|
|
741
|
-
const result = await this.adapter.checkPasswordAsync(userName, password, options);
|
|
742
|
-
const valid = Array.isArray(result) ? result[0] === true : result === true;
|
|
743
|
-
if (!valid) {
|
|
744
|
-
throw new Error('passwordVerificationFailed');
|
|
745
|
-
}
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
const userObject = (await this.adapter.getForeignObjectAsync(userId));
|
|
749
|
-
if (!String(userObject?.common?.password || '').trim()) {
|
|
750
|
-
throw new Error('passwordWriteFailed');
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
async updateEosAccountMetadata(userId, updater) {
|
|
754
|
-
const userObject = (await this.adapter.getForeignObjectAsync(userId));
|
|
755
|
-
if (!userObject) {
|
|
756
|
-
throw new Error('userObjectUnavailableAfterPasswordChange');
|
|
757
|
-
}
|
|
758
|
-
const native = { ...(userObject.native || {}) };
|
|
759
|
-
const account = { ...(native.nexowattEosAccount || {}) };
|
|
760
|
-
native.nexowattEosAccount = account;
|
|
761
|
-
updater(native, account);
|
|
762
|
-
await this.adapter.extendForeignObjectAsync(userId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
|
|
719
|
+
await (0, eosPassword_1.setEosUserPasswordWithVerification)(this.adapter, userId, password, EOS_PASSWORD_SERVICE_USER);
|
|
763
720
|
}
|
|
721
|
+
|
|
764
722
|
async destroyEosRequestSessions(req) {
|
|
765
723
|
const token = this.readAccessTokenFromRequest(req);
|
|
766
724
|
if (!token || typeof this.adapter.destroySession !== 'function') {
|
|
@@ -789,7 +747,11 @@ class Web {
|
|
|
789
747
|
res.status(403).json({ error: 'passwordSetupNotAllowed' });
|
|
790
748
|
return;
|
|
791
749
|
}
|
|
792
|
-
if (
|
|
750
|
+
if (req.headers['x-nexowatt-eos-first-login'] !== '1') {
|
|
751
|
+
res.status(403).json({ error: 'invalidRequest' });
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (!this.isEosSameOriginWrite(req)) {
|
|
793
755
|
res.status(403).json({ error: 'invalidRequestOrigin' });
|
|
794
756
|
return;
|
|
795
757
|
}
|
|
@@ -808,24 +770,34 @@ class Web {
|
|
|
808
770
|
return;
|
|
809
771
|
}
|
|
810
772
|
await this.setEosUserPassword(access.userId, validation.password);
|
|
773
|
+
const userObject = (await this.adapter.getForeignObjectAsync(access.userId));
|
|
774
|
+
if (!userObject) {
|
|
775
|
+
throw new Error('userObjectUnavailableAfterPasswordChange');
|
|
776
|
+
}
|
|
777
|
+
const native = { ...(userObject.native || {}) };
|
|
778
|
+
const account = { ...(native.nexowattEosAccount || {}) };
|
|
811
779
|
const now = new Date().toISOString();
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
});
|
|
780
|
+
account.passwordInitialized = true;
|
|
781
|
+
account.passwordInitializedAt = Date.now();
|
|
782
|
+
account.passwordInitializationVersion = 1;
|
|
783
|
+
account.passwordInitializedBy = 'self';
|
|
784
|
+
account.passwordSetupVersion = 1;
|
|
785
|
+
account.passwordSetAt = now;
|
|
786
|
+
account.firstLoginCompletedAt = now;
|
|
787
|
+
account.forcePasswordChange = false;
|
|
788
|
+
account.passwordlessFirstLoginAllowed = false;
|
|
789
|
+
native.nexowattEosAccount = account;
|
|
790
|
+
native.nexowattPasswordChangeRequired = false;
|
|
791
|
+
native.eosPasswordChangeRequired = false;
|
|
792
|
+
native.nexowattFirstLoginPending = false;
|
|
793
|
+
native.eosFirstLoginRequired = false;
|
|
794
|
+
native.nexowattInitialPasswordApplied = false;
|
|
795
|
+
native.nexowattPasswordChangedAt = now;
|
|
796
|
+
await this.adapter.extendForeignObjectAsync(access.userId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
|
|
797
|
+
const persistedState = await this.getEosFirstLoginPasswordState(access.userId, access.role);
|
|
798
|
+
if (persistedState.required || !persistedState.initialized) {
|
|
799
|
+
throw new Error('passwordMetadataNotPersisted');
|
|
800
|
+
}
|
|
829
801
|
await this.destroyEosRequestSessions(req);
|
|
830
802
|
for (const cookie of ['access_token', 'refresh_token', 'connect.sid']) {
|
|
831
803
|
res.clearCookie(cookie);
|
|
@@ -1049,25 +1021,29 @@ class Web {
|
|
|
1049
1021
|
return;
|
|
1050
1022
|
}
|
|
1051
1023
|
await this.setEosUserPassword(claim.userId, validation.password);
|
|
1024
|
+
const user = (await this.adapter.getForeignObjectAsync(claim.userId));
|
|
1025
|
+
if (!user) {
|
|
1026
|
+
throw new Error('userObjectUnavailableAfterPasswordChange');
|
|
1027
|
+
}
|
|
1028
|
+
const native = { ...(user.native || {}) };
|
|
1029
|
+
const account = { ...(native.nexowattEosAccount || {}) };
|
|
1052
1030
|
const now = new Date().toISOString();
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
native.nexowattPasswordChangedAt = now;
|
|
1070
|
-
});
|
|
1031
|
+
account.passwordInitialized = true;
|
|
1032
|
+
account.passwordInitializedAt = Date.now();
|
|
1033
|
+
account.passwordInitializationVersion = 1;
|
|
1034
|
+
account.passwordInitializedBy = 'passwordless-first-activation';
|
|
1035
|
+
account.passwordSetupVersion = 1;
|
|
1036
|
+
account.passwordSetAt = now;
|
|
1037
|
+
account.firstLoginCompletedAt = now;
|
|
1038
|
+
account.forcePasswordChange = false;
|
|
1039
|
+
account.passwordlessFirstLoginAllowed = false;
|
|
1040
|
+
account.passwordlessClaimCompletedAt = now;
|
|
1041
|
+
native.nexowattEosAccount = account;
|
|
1042
|
+
native.nexowattPasswordChangeRequired = false;
|
|
1043
|
+
native.eosPasswordChangeRequired = false;
|
|
1044
|
+
native.nexowattFirstLoginPending = false;
|
|
1045
|
+
native.eosFirstLoginRequired = false;
|
|
1046
|
+
await this.adapter.extendForeignObjectAsync(claim.userId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
|
|
1071
1047
|
this.invalidateEosPasswordClaimsForUser(claim.userId);
|
|
1072
1048
|
res.clearCookie('nexowatt_eos_first_login', { path: '/nexowatt/account' });
|
|
1073
1049
|
this.adapter.log.info(`EOS passwordless first activation completed for ${claim.userId}`);
|
|
@@ -1134,10 +1110,14 @@ class Web {
|
|
|
1134
1110
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
1135
1111
|
const access = await this.getEosRequestAccess(req);
|
|
1136
1112
|
if (!access.userId || (access.role !== 'admin' && access.role !== 'installer')) {
|
|
1137
|
-
res.status(403).json({ error: 'permissionError' });
|
|
1113
|
+
res.status(403).json({ error: 'permissionError', role: access.role });
|
|
1138
1114
|
return;
|
|
1139
1115
|
}
|
|
1140
|
-
if (
|
|
1116
|
+
if (req.headers['x-nexowatt-eos-account-reset'] !== '1') {
|
|
1117
|
+
res.status(403).json({ error: 'invalidRequest' });
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (!this.isEosSameOriginWrite(req)) {
|
|
1141
1121
|
res.status(403).json({ error: 'invalidRequestOrigin' });
|
|
1142
1122
|
return;
|
|
1143
1123
|
}
|
|
@@ -1146,19 +1126,12 @@ class Web {
|
|
|
1146
1126
|
res.status(400).json({ error: 'invalidTarget' });
|
|
1147
1127
|
return;
|
|
1148
1128
|
}
|
|
1149
|
-
const
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
? 'enduser'
|
|
1156
|
-
: null;
|
|
1157
|
-
if (targetAccess.role === 'admin'
|
|
1158
|
-
|| !explicitlyManagedRole
|
|
1159
|
-
|| (access.role === 'installer' && explicitlyManagedRole !== 'enduser')) {
|
|
1160
|
-
// Never rely on the secure-default "enduser" classification for a password reset. The
|
|
1161
|
-
// target must be an explicit member of a managed EOS installer/end-user group.
|
|
1129
|
+
const managedAccounts = await this.getEosManagedAccounts(access.role);
|
|
1130
|
+
const managedTarget = managedAccounts.find(account => account.id === targetUserId);
|
|
1131
|
+
const explicitlyManagedRole = managedTarget?.role === 'installer' || managedTarget?.role === 'enduser'
|
|
1132
|
+
? managedTarget.role
|
|
1133
|
+
: null;
|
|
1134
|
+
if (!managedTarget || !explicitlyManagedRole) {
|
|
1162
1135
|
res.status(403).json({ error: 'permissionError' });
|
|
1163
1136
|
return;
|
|
1164
1137
|
}
|
|
@@ -1172,26 +1145,36 @@ class Web {
|
|
|
1172
1145
|
return;
|
|
1173
1146
|
}
|
|
1174
1147
|
await this.setEosUserPassword(targetUserId, 'nexowatt');
|
|
1148
|
+
const user = (await this.adapter.getForeignObjectAsync(targetUserId));
|
|
1149
|
+
if (!user) {
|
|
1150
|
+
throw new Error('userObjectUnavailableAfterPasswordReset');
|
|
1151
|
+
}
|
|
1152
|
+
const native = { ...(user.native || {}) };
|
|
1153
|
+
const account = { ...(native.nexowattEosAccount || {}) };
|
|
1175
1154
|
const now = new Date().toISOString();
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
});
|
|
1155
|
+
account.passwordInitialized = false;
|
|
1156
|
+
account.passwordSetupVersion = 0;
|
|
1157
|
+
account.passwordInitializationVersion = 0;
|
|
1158
|
+
account.forcePasswordChange = true;
|
|
1159
|
+
account.passwordlessFirstLoginAllowed = false;
|
|
1160
|
+
account.passwordResetAt = now;
|
|
1161
|
+
account.passwordResetBy = access.userId;
|
|
1162
|
+
account.passwordResetMode = 'initial-password';
|
|
1163
|
+
native.nexowattEosAccount = account;
|
|
1164
|
+
native.nexowattPasswordChangeRequired = true;
|
|
1165
|
+
native.eosPasswordChangeRequired = true;
|
|
1166
|
+
native.nexowattFirstLoginPending = true;
|
|
1167
|
+
native.eosFirstLoginRequired = true;
|
|
1168
|
+
native.nexowattInitialPasswordApplied = true;
|
|
1169
|
+
native.nexowattInitialPasswordVersion = 1;
|
|
1170
|
+
native.nexowattStableInitialCredentialVersion = 1;
|
|
1171
|
+
delete account.passwordSetAt;
|
|
1172
|
+
delete account.firstLoginCompletedAt;
|
|
1173
|
+
await this.adapter.extendForeignObjectAsync(targetUserId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
|
|
1174
|
+
const resetState = await this.getEosFirstLoginPasswordState(targetUserId, explicitlyManagedRole);
|
|
1175
|
+
if (!resetState.required || resetState.initialized) {
|
|
1176
|
+
throw new Error('passwordMetadataNotPersisted');
|
|
1177
|
+
}
|
|
1195
1178
|
this.invalidateEosPasswordClaimsForUser(targetUserId);
|
|
1196
1179
|
this.adapter.log.warn(`EOS account ${targetUserId} was reset to the mandatory initial-password flow by ${access.userId}`);
|
|
1197
1180
|
res.status(200).json({
|
|
@@ -1268,25 +1251,9 @@ class Web {
|
|
|
1268
1251
|
});
|
|
1269
1252
|
}
|
|
1270
1253
|
isEosSameOriginWrite(req) {
|
|
1271
|
-
|
|
1272
|
-
if (fetchSite === 'cross-site') {
|
|
1273
|
-
return false;
|
|
1274
|
-
}
|
|
1275
|
-
const origin = String(req.headers.origin || '').trim();
|
|
1276
|
-
if (!origin) {
|
|
1277
|
-
return true;
|
|
1278
|
-
}
|
|
1279
|
-
try {
|
|
1280
|
-
const originUrl = new URL(origin);
|
|
1281
|
-
const directHost = String(req.headers.host || '').trim().toLowerCase();
|
|
1282
|
-
const forwardedHost = String(req.headers['x-forwarded-host'] || '').split(',')[0].trim().toLowerCase();
|
|
1283
|
-
const allowedHosts = new Set([directHost, forwardedHost].filter(Boolean));
|
|
1284
|
-
return allowedHosts.has(originUrl.host.toLowerCase());
|
|
1285
|
-
}
|
|
1286
|
-
catch {
|
|
1287
|
-
return false;
|
|
1288
|
-
}
|
|
1254
|
+
return (0, eosRequestSecurity_1.isEosSameOriginRequest)(req.headers);
|
|
1289
1255
|
}
|
|
1256
|
+
|
|
1290
1257
|
async saveEosBasicSettings(req, res) {
|
|
1291
1258
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
1292
1259
|
const access = await this.getEosRequestAccess(req);
|
|
@@ -1517,6 +1484,9 @@ class Web {
|
|
|
1517
1484
|
next();
|
|
1518
1485
|
});*/
|
|
1519
1486
|
this.server.app.get('/version', (_req, res) => {
|
|
1487
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
1488
|
+
res.setHeader('Pragma', 'no-cache');
|
|
1489
|
+
res.setHeader('Expires', '0');
|
|
1520
1490
|
res.status(200).send(this.adapter.version);
|
|
1521
1491
|
});
|
|
1522
1492
|
// v36: Repair stale/broken URLs generated by older EOS hard-logout builds.
|
|
@@ -1795,8 +1765,13 @@ class Web {
|
|
|
1795
1765
|
});
|
|
1796
1766
|
this.server.app.post('/nexowatt/account/first-password', (req, res) => {
|
|
1797
1767
|
void this.saveEosFirstLoginPassword(req, res).catch(e => {
|
|
1798
|
-
|
|
1799
|
-
|
|
1768
|
+
const detail = e instanceof Error ? e.message : String(e || '');
|
|
1769
|
+
this.adapter.log.warn(`Cannot initialize EOS first-login password: ${detail}`);
|
|
1770
|
+
const known = new Set([
|
|
1771
|
+
'passwordApiUnavailable', 'passwordNotPersisted', 'passwordVerificationFailed',
|
|
1772
|
+
'passwordMetadataNotPersisted', 'userObjectUnavailableAfterPasswordChange',
|
|
1773
|
+
]);
|
|
1774
|
+
res.status(500).json({ error: known.has(detail) ? detail : 'passwordSetupFailed' });
|
|
1800
1775
|
});
|
|
1801
1776
|
});
|
|
1802
1777
|
this.server.app.get('/nexowatt/account/manage', (req, res) => {
|
|
@@ -1807,8 +1782,13 @@ class Web {
|
|
|
1807
1782
|
});
|
|
1808
1783
|
this.server.app.post('/nexowatt/account/reset', (req, res) => {
|
|
1809
1784
|
void this.resetEosAccountPassword(req, res).catch(e => {
|
|
1810
|
-
|
|
1811
|
-
|
|
1785
|
+
const detail = e instanceof Error ? e.message : String(e || '');
|
|
1786
|
+
this.adapter.log.warn(`Cannot reset EOS account: ${detail}`);
|
|
1787
|
+
const known = new Set([
|
|
1788
|
+
'passwordApiUnavailable', 'passwordNotPersisted', 'passwordVerificationFailed',
|
|
1789
|
+
'passwordMetadataNotPersisted', 'userObjectUnavailableAfterPasswordReset',
|
|
1790
|
+
]);
|
|
1791
|
+
res.status(500).json({ error: known.has(detail) ? detail : 'accountResetFailed' });
|
|
1812
1792
|
});
|
|
1813
1793
|
});
|
|
1814
1794
|
this.server.app.get('/iobroker_check.html', (_req, res) => {
|
|
@@ -1935,7 +1915,29 @@ class Web {
|
|
|
1935
1915
|
res.status(404).send(get404Page(`File ${escapeHtml(fileName)} not found`));
|
|
1936
1916
|
}
|
|
1937
1917
|
});
|
|
1938
|
-
const
|
|
1918
|
+
const isEosCurrentAsset = (pathName) => pathName === '/'
|
|
1919
|
+
|| pathName === '/index.html'
|
|
1920
|
+
|| pathName === '/manifest.json'
|
|
1921
|
+
|| pathName === '/mf-manifest.json'
|
|
1922
|
+
|| pathName === '/version'
|
|
1923
|
+
|| /^\/(?:js|css)\/(?:eos-|nexowatt-)/.test(pathName)
|
|
1924
|
+
|| /^\/static\/js\/nexowatt-stable-v\d+\.js$/.test(pathName)
|
|
1925
|
+
|| /^\/remoteEntry(?:-v\d+)?\.js$/.test(pathName)
|
|
1926
|
+
|| /^\/assets\/(?:bootstrap|hostInit|Intro|index-CQZugZ1z|index-D2ymscJA|iobroker_admin__mf_v__runtimeInit__mf_v__)-[^/]+\.js$/.test(pathName);
|
|
1927
|
+
const applyNoStoreHeaders = (res) => {
|
|
1928
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
1929
|
+
res.setHeader('Pragma', 'no-cache');
|
|
1930
|
+
res.setHeader('Expires', '0');
|
|
1931
|
+
res.setHeader('Surrogate-Control', 'no-store');
|
|
1932
|
+
};
|
|
1933
|
+
const appOptions = {
|
|
1934
|
+
setHeaders: (res, filePath) => {
|
|
1935
|
+
const raw = filePath.startsWith(this.wwwDir) ? filePath.slice(this.wwwDir.length) : filePath;
|
|
1936
|
+
const relativePath = `/${raw.replace(/\\/g, '/').replace(/^\/+/, '')}`;
|
|
1937
|
+
if (isEosCurrentAsset(relativePath))
|
|
1938
|
+
applyNoStoreHeaders(res);
|
|
1939
|
+
},
|
|
1940
|
+
};
|
|
1939
1941
|
if (this.settings.cache) {
|
|
1940
1942
|
appOptions.maxAge = 30_758_400_000;
|
|
1941
1943
|
}
|
|
@@ -2033,31 +2035,24 @@ class Web {
|
|
|
2033
2035
|
this.server.app.get('/empty.html', (_req, res) => {
|
|
2034
2036
|
res.status(200).send('');
|
|
2035
2037
|
});
|
|
2036
|
-
//
|
|
2037
|
-
//
|
|
2038
|
+
// EOS entrypoints, overlays and patched module bundles must never be reused
|
|
2039
|
+
// from a previous release. Hashed untouched leaf assets may remain cacheable.
|
|
2038
2040
|
this.server.app.use((req, res, next) => {
|
|
2039
2041
|
const pathName = req.path || req.url.split('?')[0];
|
|
2040
|
-
if (pathName
|
|
2041
|
-
|
|
2042
|
-
|| pathName === '/mf-manifest.json'
|
|
2043
|
-
|| /^\/remoteEntry(?:-v\d+)?\.js$/.test(pathName)
|
|
2044
|
-
|| /^\/assets\/(?:hostInit|index-CQZugZ1z)-[^/]+\.js$/.test(pathName)) {
|
|
2045
|
-
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
2046
|
-
res.setHeader('Pragma', 'no-cache');
|
|
2047
|
-
res.setHeader('Expires', '0');
|
|
2048
|
-
}
|
|
2042
|
+
if (isEosCurrentAsset(pathName))
|
|
2043
|
+
applyNoStoreHeaders(res);
|
|
2049
2044
|
next();
|
|
2050
2045
|
});
|
|
2051
2046
|
this.server.app.get('/index.html', async (_req, res) => {
|
|
2052
|
-
this.indexHTML
|
|
2047
|
+
this.indexHTML = await this.prepareIndex('/index.html');
|
|
2053
2048
|
res.header('Content-Type', 'text/html');
|
|
2054
|
-
res
|
|
2049
|
+
applyNoStoreHeaders(res);
|
|
2055
2050
|
res.status(200).send(this.indexHTML);
|
|
2056
2051
|
});
|
|
2057
2052
|
this.server.app.get('/', async (_req, res) => {
|
|
2058
|
-
this.indexHTML
|
|
2053
|
+
this.indexHTML = await this.prepareIndex('/index.html');
|
|
2059
2054
|
res.header('Content-Type', 'text/html');
|
|
2060
|
-
res
|
|
2055
|
+
applyNoStoreHeaders(res);
|
|
2061
2056
|
res.status(200).send(this.indexHTML);
|
|
2062
2057
|
});
|
|
2063
2058
|
this.server.app.use('/', express.static(this.wwwDir, appOptions));
|
package/build/main.js
CHANGED
|
@@ -20,6 +20,7 @@ const socket_classes_1 = require("@iobroker/socket-classes");
|
|
|
20
20
|
const ws_server_1 = require("@iobroker/ws-server");
|
|
21
21
|
const translations_1 = require("./lib/translations");
|
|
22
22
|
const web_1 = require("./lib/web");
|
|
23
|
+
const eosPassword_1 = require("./lib/eosPassword");
|
|
23
24
|
const checkLinuxPass_1 = require("./lib/checkLinuxPass");
|
|
24
25
|
const plugin_docker_1 = require("@iobroker/plugin-docker");
|
|
25
26
|
const objectFixes_1 = require("./lib/objectFixes");
|
|
@@ -1152,7 +1153,7 @@ class Admin extends adapter_core_1.Adapter {
|
|
|
1152
1153
|
}
|
|
1153
1154
|
if (String(user.common.password || '') === '') {
|
|
1154
1155
|
const bootstrapSecret = `${(0, node_crypto_1.randomBytes)(48).toString('base64url')}!aA1`;
|
|
1155
|
-
await
|
|
1156
|
+
await (0, eosPassword_1.setEosUserPasswordWithVerification)(this, userId, bootstrapSecret, ADMIN_OWNER_USER);
|
|
1156
1157
|
user = (await this.getForeignObjectAsync(userId));
|
|
1157
1158
|
if (!user) {
|
|
1158
1159
|
return;
|
package/io-package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "eos-admin",
|
|
4
|
-
"version": "7.9.
|
|
4
|
+
"version": "7.9.99",
|
|
5
5
|
"titleLang": {
|
|
6
6
|
"en": "NexoWatt EOS Admin",
|
|
7
7
|
"de": "NexoWatt EOS Admin",
|
|
@@ -18,11 +18,6 @@
|
|
|
18
18
|
"connectionType": "local",
|
|
19
19
|
"dataSource": "push",
|
|
20
20
|
"news": {
|
|
21
|
-
"7.9.98": {
|
|
22
|
-
"de": "Stable-Kandidat 7.9.98 ergänzt den vertikalen Cockpit-Scrollbereich, bereinigt die Login-Felder und korrigiert Passwort-Reset sowie persönliche Erstpasswortvergabe über die echte ioBroker-Passwort-API.",
|
|
23
|
-
"en": "Stable candidate 7.9.98 adds vertical cockpit scrolling, cleans up login fields and fixes password reset plus first personal password setup through the real ioBroker password API.",
|
|
24
|
-
"nl": "Stable-kandidaat 7.9.98 voegt verticaal scrollen in het cockpit toe, herstelt de loginvelden en corrigeert wachtwoordreset en het eerste persoonlijke wachtwoord via de echte ioBroker-wachtwoord-API."
|
|
25
|
-
},
|
|
26
21
|
"7.9.95": {
|
|
27
22
|
"de": "Stable 7.9.95 macht den Release-Ordner mergesicher: Die flache Update-ZIP kann direkt über einen bestehenden Arbeitsordner entpackt werden. Vor Paketprüfung und npm-Veröffentlichung werden package.json, io-package.json, beide Lockdateien, die Frontend-Version, Build-Informationen und der aktuelle Repository-Eintrag automatisch auf dieselbe Version gebracht. Dadurch wird insbesondere ein stehen gebliebenes io-package.json aus 7.9.92/7.9.94 automatisch repariert. Die Funktionen aus 7.9.94 bleiben unverändert.",
|
|
28
23
|
"en": "Stable 7.9.95 makes the release folder merge-safe: the flat update ZIP can be extracted directly over an existing working directory. Before package validation and npm publishing, package.json, io-package.json, both lockfiles, the frontend version, build information and the current repository entry are synchronized automatically. In particular, a stale io-package.json from 7.9.92/7.9.94 is repaired automatically. Functional behavior from 7.9.94 remains unchanged.",
|
|
@@ -334,7 +329,7 @@
|
|
|
334
329
|
"icon": "admin.svg",
|
|
335
330
|
"messagebox": true,
|
|
336
331
|
"enabled": true,
|
|
337
|
-
"extIcon": "https://unpkg.com/iobroker.eos-admin@7.9.
|
|
332
|
+
"extIcon": "https://unpkg.com/iobroker.eos-admin@7.9.99/admin/admin.svg",
|
|
338
333
|
"keywords": [
|
|
339
334
|
"NexoWatt",
|
|
340
335
|
"EOS",
|
|
@@ -345,7 +340,7 @@
|
|
|
345
340
|
"licensed"
|
|
346
341
|
],
|
|
347
342
|
"compact": true,
|
|
348
|
-
"readme": "https://unpkg.com/iobroker.eos-admin@7.9.
|
|
343
|
+
"readme": "https://unpkg.com/iobroker.eos-admin@7.9.99/README.md",
|
|
349
344
|
"authors": [
|
|
350
345
|
"NexoWatt"
|
|
351
346
|
],
|
|
@@ -413,7 +408,7 @@
|
|
|
413
408
|
"nondeletable": false,
|
|
414
409
|
"allowAdapterUpdate": true,
|
|
415
410
|
"allowAdapterDelete": false,
|
|
416
|
-
"meta": "https://unpkg.com/iobroker.eos-admin@7.9.
|
|
411
|
+
"meta": "https://unpkg.com/iobroker.eos-admin@7.9.99/io-package.json",
|
|
417
412
|
"npmPackage": "iobroker.eos-admin",
|
|
418
413
|
"packetName": "iobroker.eos-admin",
|
|
419
414
|
"publisher": "NexoWatt"
|
|
@@ -1122,5 +1117,5 @@
|
|
|
1122
1117
|
}
|
|
1123
1118
|
}
|
|
1124
1119
|
},
|
|
1125
|
-
"version": "7.9.
|
|
1120
|
+
"version": "7.9.99"
|
|
1126
1121
|
}
|