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.
Files changed (52) hide show
  1. package/INSTALL_TEST_V7.9.99.md +11 -0
  2. package/MERGE_UPDATE_README_V7.9.99.md +5 -0
  3. package/NEXOWATT_EOS_BUILD_INFO.json +21 -20
  4. package/PUBLISH_STABLE_V7.9.99.md +15 -0
  5. package/README.md +6 -16
  6. package/README_STABILITY_V7.9.99.md +18 -0
  7. package/RELEASE_ACCEPTANCE_V7.9.99.md +11 -0
  8. package/RELEASE_NOTES_V7.9.99.md +9 -0
  9. package/adminWww/assets/Intro-DkwRiz1n-v84.js +2 -2
  10. package/adminWww/assets/bootstrap-COulQZax-v84.js +2 -2
  11. package/adminWww/css/eos-ems-overview.css +3 -5
  12. package/adminWww/css/nexowatt-native-shell.css +26 -112
  13. package/adminWww/index.html +19 -18
  14. package/adminWww/js/eos-account-management.js +19 -28
  15. package/adminWww/js/eos-assistant.js +1 -1
  16. package/adminWww/js/eos-ems-overview.js +2 -3
  17. package/adminWww/js/eos-release-watch.js +77 -0
  18. package/adminWww/js/eos-role-bootstrap.js +11 -8
  19. package/adminWww/js/eos-role-ui.js +2 -2
  20. package/adminWww/js/nexowatt-native-shell.js +1 -1
  21. package/adminWww/static/js/nexowatt-stable-login.js +3 -3
  22. package/adminWww/static/js/{nexowatt-stable-v98.js → nexowatt-stable-v99.js} +2 -2
  23. package/build/lib/eosPassword.js +62 -0
  24. package/build/lib/eosRequestSecurity.js +31 -0
  25. package/build/lib/web.js +149 -154
  26. package/build/main.js +2 -1
  27. package/io-package.json +5 -10
  28. package/package.json +17 -13
  29. package/tools/nexowatt-account-management-selftest.cjs +19 -15
  30. package/tools/nexowatt-apply-ems-overview.cjs +2 -4
  31. package/tools/nexowatt-cache-update-selftest.cjs +11 -0
  32. package/tools/nexowatt-clean-legacy-runtime.cjs +1 -1
  33. package/tools/nexowatt-ems-overview-selftest.cjs +1 -1
  34. package/tools/nexowatt-login-layout-selftest.cjs +11 -11
  35. package/tools/nexowatt-merge-update-selftest.cjs +1 -1
  36. package/tools/nexowatt-modern-ui-selftest.cjs +2 -2
  37. package/tools/nexowatt-password-write-selftest.cjs +42 -44
  38. package/tools/nexowatt-request-security-selftest.cjs +11 -0
  39. package/tools/nexowatt-runtime-cleanup-selftest.cjs +6 -6
  40. package/tools/nexowatt-stable-v99-selftest.cjs +25 -0
  41. package/tools/nexowatt-sync-release-version.cjs +1 -42
  42. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +3 -1
  43. package/tools/nexowatt-validate-package.cjs +23 -23
  44. package/tools/nexowatt-version-sync-selftest.cjs +1 -18
  45. package/INSTALL_TEST_V7.9.98.md +0 -39
  46. package/MERGE_UPDATE_README_V7.9.98.md +0 -29
  47. package/PUBLISH_STABLE_V7.9.98.md +0 -33
  48. package/README_STABILITY_V7.9.98.md +0 -22
  49. package/RELEASE_ACCEPTANCE_V7.9.98.md +0 -48
  50. package/RELEASE_NOTES_V7.9.98.md +0 -32
  51. package/tools/nexowatt-account-password-flow-selftest.cjs +0 -186
  52. package/tools/nexowatt-stable-v98-selftest.cjs +0 -14
@@ -1,6 +1,6 @@
1
1
  (() => {
2
2
  'use strict';
3
- const VERSION = 'v98-scroll-login-password-write';
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.NEXOWATT_EOS_STABLE_V98 = Object.freeze({ version: VERSION, refresh: apply });
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
- // ioBroker's password API expects the account name, not the system.user.* object id.
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 (!this.isEosSameOriginWrite(req) || req.headers['x-nexowatt-eos-first-login'] !== '1') {
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
- await this.updateEosAccountMetadata(access.userId, (native, account) => {
813
- account.passwordInitialized = true;
814
- account.passwordInitializedAt = Date.now();
815
- account.passwordInitializationVersion = 1;
816
- account.passwordInitializedBy = 'self';
817
- account.passwordSetupVersion = 1;
818
- account.passwordSetAt = now;
819
- account.firstLoginCompletedAt = now;
820
- account.forcePasswordChange = false;
821
- account.passwordlessFirstLoginAllowed = false;
822
- native.nexowattPasswordChangeRequired = false;
823
- native.eosPasswordChangeRequired = false;
824
- native.nexowattFirstLoginPending = false;
825
- native.eosFirstLoginRequired = false;
826
- native.nexowattInitialPasswordApplied = false;
827
- native.nexowattPasswordChangedAt = now;
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
- await this.updateEosAccountMetadata(claim.userId, (native, account) => {
1054
- account.passwordInitialized = true;
1055
- account.passwordInitializedAt = Date.now();
1056
- account.passwordInitializationVersion = 1;
1057
- account.passwordInitializedBy = 'passwordless-first-activation';
1058
- account.passwordSetupVersion = 1;
1059
- account.passwordSetAt = now;
1060
- account.firstLoginCompletedAt = now;
1061
- account.forcePasswordChange = false;
1062
- account.passwordlessFirstLoginAllowed = false;
1063
- account.passwordlessClaimCompletedAt = now;
1064
- native.nexowattPasswordChangeRequired = false;
1065
- native.eosPasswordChangeRequired = false;
1066
- native.nexowattFirstLoginPending = false;
1067
- native.eosFirstLoginRequired = false;
1068
- native.nexowattInitialPasswordApplied = false;
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 (!this.isEosSameOriginWrite(req) || req.headers['x-nexowatt-eos-account-reset'] !== '1') {
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 targetAccess = await this.getEosAccessForUserId(targetUserId);
1150
- const explicitInstaller = targetAccess.groups.some(group => this.getEosInstallerGroups().includes(group));
1151
- const explicitEndUser = targetAccess.groups.some(group => this.getEosEndUserGroups().includes(group));
1152
- const explicitlyManagedRole = explicitInstaller
1153
- ? 'installer'
1154
- : explicitEndUser
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
- await this.updateEosAccountMetadata(targetUserId, (native, account) => {
1177
- account.passwordInitialized = false;
1178
- account.passwordSetupVersion = 0;
1179
- account.passwordInitializationVersion = 0;
1180
- account.forcePasswordChange = true;
1181
- account.passwordlessFirstLoginAllowed = false;
1182
- account.passwordResetAt = now;
1183
- account.passwordResetBy = access.userId;
1184
- account.passwordResetMode = 'initial-password';
1185
- account.passwordSetAt = null;
1186
- account.firstLoginCompletedAt = null;
1187
- native.nexowattPasswordChangeRequired = true;
1188
- native.eosPasswordChangeRequired = true;
1189
- native.nexowattFirstLoginPending = true;
1190
- native.eosFirstLoginRequired = true;
1191
- native.nexowattInitialPasswordApplied = true;
1192
- native.nexowattInitialPasswordVersion = 1;
1193
- native.nexowattStableInitialCredentialVersion = 1;
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
- const fetchSite = String(req.headers['sec-fetch-site'] || '').trim().toLowerCase();
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
- this.adapter.log.warn(`Cannot initialize EOS first-login password: ${e instanceof Error ? e.message : e}`);
1799
- res.status(500).json({ error: 'passwordSetupFailed' });
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
- this.adapter.log.warn(`Cannot reset EOS account: ${e instanceof Error ? e.message : e}`);
1811
- res.status(500).json({ error: 'accountResetFailed' });
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 appOptions = {};
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
- // Stability release: entrypoints and module-federation manifests must always
2037
- // revalidate. Hashed leaf assets remain cacheable through express.static.
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
- || pathName === '/index.html'
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 ||= await this.prepareIndex('/index.html');
2047
+ this.indexHTML = await this.prepareIndex('/index.html');
2053
2048
  res.header('Content-Type', 'text/html');
2054
- res.header('Cache-Control', 'no-cache');
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 ||= await this.prepareIndex('/index.html');
2053
+ this.indexHTML = await this.prepareIndex('/index.html');
2059
2054
  res.header('Content-Type', 'text/html');
2060
- res.header('Cache-Control', 'no-cache');
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 this.setPasswordAsync(userId.replace(/^system\.user\./, ''), bootstrapSecret, { user: ADMIN_OWNER_USER });
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.98",
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.98/admin/admin.svg",
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.98/README.md",
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.98/io-package.json",
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.98"
1120
+ "version": "7.9.99"
1126
1121
  }