iobroker.eos-admin 7.10.0 → 7.10.2

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 (65) hide show
  1. package/INSTALL_TEST_V7.10.2.md +22 -0
  2. package/MERGE_UPDATE_README_V7.10.2.md +11 -0
  3. package/NEXOWATT_EOS_BUILD_INFO.json +19 -21
  4. package/NEXOWATT_EOS_PREBUILT_MANIFEST.json +1410 -0
  5. package/PUBLISH_STABLE_V7.10.2.md +21 -0
  6. package/README.md +16 -6
  7. package/README_STABILITY_V7.10.2.md +20 -0
  8. package/RELEASE_ACCEPTANCE_V7.10.2.md +13 -0
  9. package/RELEASE_NOTES_V7.10.2.md +11 -0
  10. package/adminWww/assets/Intro-DkwRiz1n-v84.js +1 -1
  11. package/adminWww/assets/bootstrap-COulQZax-v84.js +2 -2
  12. package/adminWww/css/eos-ems-overview.css +5 -3
  13. package/adminWww/css/nexowatt-native-shell.css +85 -413
  14. package/adminWww/index.html +20 -21
  15. package/adminWww/js/eos-account-management.js +28 -19
  16. package/adminWww/js/eos-assistant.js +1 -1
  17. package/adminWww/js/eos-ems-overview.js +1 -1
  18. package/adminWww/js/eos-role-bootstrap.js +17 -80
  19. package/adminWww/js/eos-role-ui.js +28 -4
  20. package/adminWww/js/nexowatt-native-shell.js +1 -1
  21. package/adminWww/static/js/nexowatt-stable-login.js +43 -70
  22. package/adminWww/static/js/nexowatt-stable-v7101.js +27 -0
  23. package/build/lib/eosAutoUpdate.js +418 -20
  24. package/build/lib/eosAutoUpdate.js.map +1 -0
  25. package/build/lib/eosRequestSecurity.js +4 -1
  26. package/build/lib/eosRequestSecurity.js.map +1 -0
  27. package/build/lib/web.js +161 -254
  28. package/build/lib/web.js.map +1 -1
  29. package/build/main.js +39 -67
  30. package/io-package.json +72 -7
  31. package/package.json +24 -25
  32. package/tools/nexowatt-account-management-selftest.cjs +15 -19
  33. package/tools/nexowatt-account-password-flow-selftest.cjs +15 -0
  34. package/tools/nexowatt-apply-ems-overview.cjs +4 -2
  35. package/tools/nexowatt-auto-update-selftest.cjs +144 -32
  36. package/tools/nexowatt-backend-runtime-selftest.cjs +147 -0
  37. package/tools/nexowatt-clean-legacy-runtime.cjs +1 -1
  38. package/tools/nexowatt-ems-overview-selftest.cjs +1 -1
  39. package/tools/nexowatt-ensure-release-defaults.cjs +1 -1
  40. package/tools/nexowatt-login-layout-selftest.cjs +9 -13
  41. package/tools/nexowatt-merge-update-selftest.cjs +7 -1
  42. package/tools/nexowatt-modern-ui-selftest.cjs +10 -46
  43. package/tools/nexowatt-password-write-selftest.cjs +46 -16
  44. package/tools/nexowatt-prebuilt-release-selftest.cjs +292 -0
  45. package/tools/nexowatt-release-defaults-selftest.cjs +1 -1
  46. package/tools/nexowatt-runtime-cleanup-selftest.cjs +6 -8
  47. package/tools/nexowatt-stable-v7102-selftest.cjs +51 -0
  48. package/tools/nexowatt-sync-release-version.cjs +43 -5
  49. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +2 -3
  50. package/tools/nexowatt-validate-package.cjs +54 -29
  51. package/tools/nexowatt-version-sync-selftest.cjs +18 -1
  52. package/INSTALL_TEST_V7.10.0.md +0 -11
  53. package/MERGE_UPDATE_README_V7.10.0.md +0 -5
  54. package/PUBLISH_STABLE_V7.10.0.md +0 -15
  55. package/README_STABILITY_V7.10.0.md +0 -3
  56. package/RELEASE_ACCEPTANCE_V7.10.0.md +0 -11
  57. package/RELEASE_NOTES_V7.10.0.md +0 -17
  58. package/adminWww/js/eos-release-watch.js +0 -77
  59. package/adminWww/static/js/nexowatt-stable-v7100.js +0 -111
  60. package/build/lib/eosPassword.js +0 -72
  61. package/build/lib/nexowattStableAuth.js +0 -74
  62. package/src/lib/eosAutoUpdate.ts +0 -320
  63. package/tools/nexowatt-cache-update-selftest.cjs +0 -12
  64. package/tools/nexowatt-request-security-selftest.cjs +0 -11
  65. package/tools/nexowatt-stable-v7100-selftest.cjs +0 -17
package/build/lib/web.js CHANGED
@@ -21,7 +21,6 @@ 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
24
  const eosRequestSecurity_1 = require("./eosRequestSecurity");
26
25
  const eosAutoUpdate_1 = require("./eosAutoUpdate");
27
26
  let AdapterStore;
@@ -137,6 +136,7 @@ function MemoryWriteStream() {
137
136
  (0, node_util_1.inherits)(MemoryWriteStream, node_stream_1.Transform);
138
137
  /** Webserver class */
139
138
  class Web {
139
+ nexowattStableUpdateManager;
140
140
  server = {
141
141
  app: null,
142
142
  server: null,
@@ -164,8 +164,6 @@ class Web {
164
164
  mcpServer = null;
165
165
  /** Short-lived passwordless first-activation claims. Keys are SHA-256 hashes of HttpOnly cookie tokens. */
166
166
  eosPasswordClaims = new Map();
167
- /** One-time authenticated tickets for the mandatory personal-password POST. */
168
- eosPasswordSetupTickets = new Map();
169
167
  /** Small in-memory rate limiter for unauthenticated first-activation requests. */
170
168
  eosPasswordClaimRates = new Map();
171
169
  /**
@@ -197,6 +195,7 @@ class Web {
197
195
  this.checkTimeout = null;
198
196
  }
199
197
  this.mcpServer?.unload();
198
+ this.nexowattStableUpdateManager?.stop();
200
199
  void this.adapter.setState('info.connection', false, true);
201
200
  this.server.server?.close();
202
201
  }
@@ -718,10 +717,61 @@ class Web {
718
717
  }
719
718
  return { valid: true, password: value };
720
719
  }
720
+ getEosPasswordUserName(userId) {
721
+ const userName = String(userId || '').trim().replace(/^system\.user\./, '');
722
+ if (!/^[A-Za-z0-9_.@-]+$/.test(userName)) {
723
+ throw new Error('invalidPasswordTarget');
724
+ }
725
+ return userName;
726
+ }
721
727
  async setEosUserPassword(userId, password) {
722
- await (0, eosPassword_1.setEosUserPasswordWithVerification)(this.adapter, userId, password, EOS_PASSWORD_SERVICE_USER);
728
+ // ioBroker's password API expects the account name (for example "user"), not the
729
+ // object id ("system.user.user"). Supplying the object id can return without changing
730
+ // the password on some controller versions. Always normalize to the real account name.
731
+ const userName = this.getEosPasswordUserName(userId);
732
+ // The server-side Admin/Service context performs the controller password write, so managed
733
+ // Installer and End User accounts do not need global users.write rights in the browser.
734
+ const options = { user: EOS_PASSWORD_SERVICE_USER };
735
+ if (typeof this.adapter.setPasswordAsync === 'function') {
736
+ await this.adapter.setPasswordAsync(userName, password, options);
737
+ }
738
+ else if (typeof this.adapter.setPassword === 'function') {
739
+ await new Promise((resolve, reject) => {
740
+ this.adapter.setPassword?.(userName, password, options, error => (error ? reject(error) : resolve()));
741
+ });
742
+ }
743
+ else {
744
+ throw new Error('passwordApiUnavailable');
745
+ }
746
+ // Verify the write with the controller API whenever it is exposed. This prevents the
747
+ // UI from reporting success while the object database still contains the old hash.
748
+ if (typeof this.adapter.checkPasswordAsync === 'function') {
749
+ const result = await this.adapter.checkPasswordAsync(userName, password, options);
750
+ const valid = Array.isArray(result) ? result[0] === true : result === true;
751
+ if (!valid) {
752
+ throw new Error('passwordVerificationFailed');
753
+ }
754
+ return;
755
+ }
756
+ // Compatibility fallback for older controllers: require a non-empty stored password hash.
757
+ const userObject = (await this.adapter.getForeignObjectAsync(userId));
758
+ if (!String(userObject?.common?.password || '').trim()) {
759
+ throw new Error('passwordWriteFailed');
760
+ }
761
+ }
762
+ async updateEosAccountMetadata(userId, updater) {
763
+ const userObject = (await this.adapter.getForeignObjectAsync(userId));
764
+ if (!userObject) {
765
+ throw new Error('userObjectUnavailableAfterPasswordChange');
766
+ }
767
+ const native = { ...(userObject.native || {}) };
768
+ const account = { ...(native.nexowattEosAccount || {}) };
769
+ native.nexowattEosAccount = account;
770
+ updater(native, account);
771
+ // Extend only the EOS metadata. Replacing the complete user object here could overwrite the
772
+ // password hash which was just written by setPasswordAsync/changePassword.
773
+ await this.adapter.extendForeignObjectAsync(userId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
723
774
  }
724
-
725
775
  async destroyEosRequestSessions(req) {
726
776
  const token = this.readAccessTokenFromRequest(req);
727
777
  if (!token || typeof this.adapter.destroySession !== 'function') {
@@ -745,87 +795,55 @@ class Web {
745
795
  }
746
796
  async saveEosFirstLoginPassword(req, res) {
747
797
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
748
- if (req.headers['x-nexowatt-eos-first-login'] !== '1') {
749
- res.status(403).json({ error: 'invalidRequest' });
798
+ const access = await this.getEosRequestAccess(req);
799
+ if (!access.userId || access.role === 'admin') {
800
+ res.status(403).json({ error: 'passwordSetupNotAllowed' });
750
801
  return;
751
802
  }
752
- if (!this.isEosSameOriginWrite(req)) {
803
+ if (!this.isEosSameOriginWrite(req) || req.headers['x-nexowatt-eos-first-login'] !== '1') {
753
804
  res.status(403).json({ error: 'invalidRequestOrigin' });
754
805
  return;
755
806
  }
756
- const body = req.body || {};
757
- const suppliedToken = String(req.headers['x-nexowatt-eos-password-setup-token'] || body.setupToken || '').trim();
758
- const ticket = this.resolveEosPasswordSetupTicket(req, suppliedToken);
759
- const requestAccess = await this.getEosRequestAccess(req);
760
- if (suppliedToken && !ticket) {
761
- res.status(403).json({ error: 'passwordSetupTokenExpired' });
762
- return;
763
- }
764
- if (ticket && requestAccess.userId && requestAccess.userId !== ticket.userId) {
765
- res.status(403).json({ error: 'passwordSetupTokenMismatch' });
766
- return;
767
- }
768
- const access = ticket ? { ...requestAccess, userId: ticket.userId, role: ticket.role } : requestAccess;
769
- if (!access.userId || access.role === 'admin') {
770
- res.status(403).json({ error: suppliedToken ? 'passwordSetupTokenMismatch' : 'passwordSetupNotAllowed' });
771
- return;
772
- }
773
807
  const passwordState = await this.getEosFirstLoginPasswordState(access.userId, access.role);
774
808
  if (!passwordState.required) {
775
- this.invalidateEosPasswordSetupTicketsForUser(access.userId);
776
809
  res.status(200).json({ success: true, alreadyInitialized: true });
777
810
  return;
778
811
  }
812
+ const body = (req.body || {});
779
813
  const validation = this.validateEosFirstLoginPassword(body.password, body.passwordRepeat, access.userId);
780
814
  if (!validation.valid) {
781
- res.status(400).json({ error: validation.error, minLength: passwordState.minLength });
815
+ res.status(400).json({
816
+ error: validation.error,
817
+ minLength: passwordState.minLength,
818
+ });
782
819
  return;
783
820
  }
784
- if (body.passwordAlreadyWritten === true) {
785
- await (0, eosPassword_1.verifyEosUserPasswordCredential)(this.adapter, access.userId, validation.password, EOS_PASSWORD_SERVICE_USER);
786
- }
787
- else {
788
- await this.setEosUserPassword(access.userId, validation.password);
789
- }
790
- const userObject = await this.adapter.getForeignObjectAsync(access.userId);
791
- if (!userObject) {
792
- throw new Error('userObjectUnavailableAfterPasswordChange');
793
- }
794
- const native = { ...(userObject.native || {}) };
795
- const account = { ...(native.nexowattEosAccount || {}) };
821
+ await this.setEosUserPassword(access.userId, validation.password);
796
822
  const now = new Date().toISOString();
797
- account.passwordInitialized = true;
798
- account.passwordInitializedAt = Date.now();
799
- account.passwordInitializationVersion = 1;
800
- account.passwordInitializedBy = 'self';
801
- account.passwordSetupVersion = 1;
802
- account.passwordSetAt = now;
803
- account.firstLoginCompletedAt = now;
804
- account.forcePasswordChange = false;
805
- account.passwordlessFirstLoginAllowed = false;
806
- native.nexowattEosAccount = account;
807
- native.nexowattPasswordChangeRequired = false;
808
- native.eosPasswordChangeRequired = false;
809
- native.nexowattFirstLoginPending = false;
810
- native.eosFirstLoginRequired = false;
811
- native.nexowattInitialPasswordApplied = false;
812
- native.nexowattPasswordChangedAt = now;
813
- await this.adapter.extendForeignObjectAsync(access.userId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
814
- const persistedState = await this.getEosFirstLoginPasswordState(access.userId, access.role);
815
- if (persistedState.required || !persistedState.initialized) {
816
- throw new Error('passwordMetadataNotPersisted');
817
- }
818
- this.invalidateEosPasswordSetupTicketsForUser(access.userId);
823
+ await this.updateEosAccountMetadata(access.userId, (native, account) => {
824
+ account.passwordInitialized = true;
825
+ account.passwordInitializedAt = Date.now();
826
+ account.passwordInitializationVersion = 1;
827
+ account.passwordInitializedBy = 'self';
828
+ account.passwordSetupVersion = 1;
829
+ account.passwordSetAt = now;
830
+ account.firstLoginCompletedAt = now;
831
+ account.forcePasswordChange = false;
832
+ account.passwordlessFirstLoginAllowed = false;
833
+ native.nexowattPasswordChangeRequired = false;
834
+ native.eosPasswordChangeRequired = false;
835
+ native.nexowattFirstLoginPending = false;
836
+ native.eosFirstLoginRequired = false;
837
+ native.nexowattInitialPasswordApplied = false;
838
+ native.nexowattPasswordChangedAt = now;
839
+ });
840
+ await this.destroyEosRequestSessions(req);
819
841
  for (const cookie of ['access_token', 'refresh_token', 'connect.sid']) {
820
842
  res.clearCookie(cookie);
821
843
  }
822
- res.status(200).json({ success: true, role: access.role, logoutRequired: true, sessionInvalidated: true });
823
- void this.destroyEosRequestSessions(req).catch(error => {
824
- this.adapter.log.debug(`Deferred EOS session cleanup failed: ${error instanceof Error ? error.message : error}`);
825
- });
826
844
  this.adapter.log.info(`EOS first-login password initialized for ${access.userId}`);
845
+ res.status(200).json({ success: true, role: access.role, logoutRequired: true, sessionInvalidated: true });
827
846
  }
828
-
829
847
  isEosPasswordlessFirstLoginEnabled() {
830
848
  return this.settings.eosPasswordlessFirstLogin !== false;
831
849
  }
@@ -864,53 +882,6 @@ class Web {
864
882
  hashEosPasswordClaim(token) {
865
883
  return (0, node_crypto_1.createHash)('sha256').update(token).digest('hex');
866
884
  }
867
- cleanEosPasswordSetupTickets() {
868
- const now = Date.now();
869
- for (const [key, ticket] of this.eosPasswordSetupTickets.entries()) {
870
- if (ticket.expiresAt <= now) {
871
- this.eosPasswordSetupTickets.delete(key);
872
- }
873
- }
874
- }
875
- invalidateEosPasswordSetupTicketsForUser(userId) {
876
- for (const [key, ticket] of this.eosPasswordSetupTickets.entries()) {
877
- if (ticket.userId === userId) {
878
- this.eosPasswordSetupTickets.delete(key);
879
- }
880
- }
881
- }
882
- issueEosPasswordSetupTicket(req, userId, role) {
883
- if (role !== 'installer' && role !== 'enduser') {
884
- return null;
885
- }
886
- this.cleanEosPasswordSetupTickets();
887
- // Policy-client refreshes must not invalidate the token displayed by bootstrap.
888
- // Tokens are short-lived and all are removed after a successful password change.
889
- const token = (0, node_crypto_1.randomBytes)(32).toString('base64url');
890
- const expiresAt = Date.now() + 10 * 60_000;
891
- this.eosPasswordSetupTickets.set(this.hashEosPasswordClaim(token), {
892
- userId,
893
- role,
894
- expiresAt,
895
- remoteAddress: this.getEosRemoteAddress(req),
896
- });
897
- return { token, expiresAt };
898
- }
899
- resolveEosPasswordSetupTicket(req, token) {
900
- this.cleanEosPasswordSetupTickets();
901
- if (!token) {
902
- return null;
903
- }
904
- const ticket = this.eosPasswordSetupTickets.get(this.hashEosPasswordClaim(token));
905
- if (!ticket || ticket.expiresAt <= Date.now()) {
906
- return null;
907
- }
908
- const address = this.getEosRemoteAddress(req);
909
- if (ticket.remoteAddress && address && ticket.remoteAddress !== address) {
910
- return null;
911
- }
912
- return ticket;
913
- }
914
885
  readEosCookie(req, name) {
915
886
  const row = String(req.headers.cookie || '')
916
887
  .split(';')
@@ -1089,29 +1060,25 @@ class Web {
1089
1060
  return;
1090
1061
  }
1091
1062
  await this.setEosUserPassword(claim.userId, validation.password);
1092
- const user = (await this.adapter.getForeignObjectAsync(claim.userId));
1093
- if (!user) {
1094
- throw new Error('userObjectUnavailableAfterPasswordChange');
1095
- }
1096
- const native = { ...(user.native || {}) };
1097
- const account = { ...(native.nexowattEosAccount || {}) };
1098
1063
  const now = new Date().toISOString();
1099
- account.passwordInitialized = true;
1100
- account.passwordInitializedAt = Date.now();
1101
- account.passwordInitializationVersion = 1;
1102
- account.passwordInitializedBy = 'passwordless-first-activation';
1103
- account.passwordSetupVersion = 1;
1104
- account.passwordSetAt = now;
1105
- account.firstLoginCompletedAt = now;
1106
- account.forcePasswordChange = false;
1107
- account.passwordlessFirstLoginAllowed = false;
1108
- account.passwordlessClaimCompletedAt = now;
1109
- native.nexowattEosAccount = account;
1110
- native.nexowattPasswordChangeRequired = false;
1111
- native.eosPasswordChangeRequired = false;
1112
- native.nexowattFirstLoginPending = false;
1113
- native.eosFirstLoginRequired = false;
1114
- await this.adapter.extendForeignObjectAsync(claim.userId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
1064
+ await this.updateEosAccountMetadata(claim.userId, (native, account) => {
1065
+ account.passwordInitialized = true;
1066
+ account.passwordInitializedAt = Date.now();
1067
+ account.passwordInitializationVersion = 1;
1068
+ account.passwordInitializedBy = 'passwordless-first-activation';
1069
+ account.passwordSetupVersion = 1;
1070
+ account.passwordSetAt = now;
1071
+ account.firstLoginCompletedAt = now;
1072
+ account.forcePasswordChange = false;
1073
+ account.passwordlessFirstLoginAllowed = false;
1074
+ account.passwordlessClaimCompletedAt = now;
1075
+ native.nexowattPasswordChangeRequired = false;
1076
+ native.eosPasswordChangeRequired = false;
1077
+ native.nexowattFirstLoginPending = false;
1078
+ native.eosFirstLoginRequired = false;
1079
+ native.nexowattInitialPasswordApplied = false;
1080
+ native.nexowattPasswordChangedAt = now;
1081
+ });
1115
1082
  this.invalidateEosPasswordClaimsForUser(claim.userId);
1116
1083
  res.clearCookie('nexowatt_eos_first_login', { path: '/nexowatt/account' });
1117
1084
  this.adapter.log.info(`EOS passwordless first activation completed for ${claim.userId}`);
@@ -1178,14 +1145,10 @@ class Web {
1178
1145
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1179
1146
  const access = await this.getEosRequestAccess(req);
1180
1147
  if (!access.userId || (access.role !== 'admin' && access.role !== 'installer')) {
1181
- res.status(403).json({ error: 'permissionError', role: access.role });
1182
- return;
1183
- }
1184
- if (req.headers['x-nexowatt-eos-account-reset'] !== '1') {
1185
- res.status(403).json({ error: 'invalidRequest' });
1148
+ res.status(403).json({ error: 'permissionError' });
1186
1149
  return;
1187
1150
  }
1188
- if (!this.isEosSameOriginWrite(req)) {
1151
+ if (!this.isEosSameOriginWrite(req) || req.headers['x-nexowatt-eos-account-reset'] !== '1') {
1189
1152
  res.status(403).json({ error: 'invalidRequestOrigin' });
1190
1153
  return;
1191
1154
  }
@@ -1194,12 +1157,19 @@ class Web {
1194
1157
  res.status(400).json({ error: 'invalidTarget' });
1195
1158
  return;
1196
1159
  }
1197
- const managedAccounts = await this.getEosManagedAccounts(access.role);
1198
- const managedTarget = managedAccounts.find(account => account.id === targetUserId);
1199
- const explicitlyManagedRole = managedTarget?.role === 'installer' || managedTarget?.role === 'enduser'
1200
- ? managedTarget.role
1201
- : null;
1202
- if (!managedTarget || !explicitlyManagedRole) {
1160
+ const targetAccess = await this.getEosAccessForUserId(targetUserId);
1161
+ const explicitInstaller = targetAccess.groups.some(group => this.getEosInstallerGroups().includes(group));
1162
+ const explicitEndUser = targetAccess.groups.some(group => this.getEosEndUserGroups().includes(group));
1163
+ const explicitlyManagedRole = explicitInstaller
1164
+ ? 'installer'
1165
+ : explicitEndUser
1166
+ ? 'enduser'
1167
+ : null;
1168
+ if (targetAccess.role === 'admin'
1169
+ || !explicitlyManagedRole
1170
+ || (access.role === 'installer' && explicitlyManagedRole !== 'enduser')) {
1171
+ // Never rely on the secure-default "enduser" classification for a password reset. The
1172
+ // target must be an explicit member of a managed EOS installer/end-user group.
1203
1173
  res.status(403).json({ error: 'permissionError' });
1204
1174
  return;
1205
1175
  }
@@ -1213,36 +1183,26 @@ class Web {
1213
1183
  return;
1214
1184
  }
1215
1185
  await this.setEosUserPassword(targetUserId, 'nexowatt');
1216
- const user = (await this.adapter.getForeignObjectAsync(targetUserId));
1217
- if (!user) {
1218
- throw new Error('userObjectUnavailableAfterPasswordReset');
1219
- }
1220
- const native = { ...(user.native || {}) };
1221
- const account = { ...(native.nexowattEosAccount || {}) };
1222
1186
  const now = new Date().toISOString();
1223
- account.passwordInitialized = false;
1224
- account.passwordSetupVersion = 0;
1225
- account.passwordInitializationVersion = 0;
1226
- account.forcePasswordChange = true;
1227
- account.passwordlessFirstLoginAllowed = false;
1228
- account.passwordResetAt = now;
1229
- account.passwordResetBy = access.userId;
1230
- account.passwordResetMode = 'initial-password';
1231
- native.nexowattEosAccount = account;
1232
- native.nexowattPasswordChangeRequired = true;
1233
- native.eosPasswordChangeRequired = true;
1234
- native.nexowattFirstLoginPending = true;
1235
- native.eosFirstLoginRequired = true;
1236
- native.nexowattInitialPasswordApplied = true;
1237
- native.nexowattInitialPasswordVersion = 1;
1238
- native.nexowattStableInitialCredentialVersion = 1;
1239
- delete account.passwordSetAt;
1240
- delete account.firstLoginCompletedAt;
1241
- await this.adapter.extendForeignObjectAsync(targetUserId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
1242
- const resetState = await this.getEosFirstLoginPasswordState(targetUserId, explicitlyManagedRole);
1243
- if (!resetState.required || resetState.initialized) {
1244
- throw new Error('passwordMetadataNotPersisted');
1245
- }
1187
+ await this.updateEosAccountMetadata(targetUserId, (native, account) => {
1188
+ account.passwordInitialized = false;
1189
+ account.passwordSetupVersion = 0;
1190
+ account.passwordInitializationVersion = 0;
1191
+ account.forcePasswordChange = true;
1192
+ account.passwordlessFirstLoginAllowed = false;
1193
+ account.passwordResetAt = now;
1194
+ account.passwordResetBy = access.userId;
1195
+ account.passwordResetMode = 'initial-password';
1196
+ account.passwordSetAt = null;
1197
+ account.firstLoginCompletedAt = null;
1198
+ native.nexowattPasswordChangeRequired = true;
1199
+ native.eosPasswordChangeRequired = true;
1200
+ native.nexowattFirstLoginPending = true;
1201
+ native.eosFirstLoginRequired = true;
1202
+ native.nexowattInitialPasswordApplied = true;
1203
+ native.nexowattInitialPasswordVersion = 1;
1204
+ native.nexowattStableInitialCredentialVersion = 1;
1205
+ });
1246
1206
  this.invalidateEosPasswordClaimsForUser(targetUserId);
1247
1207
  this.adapter.log.warn(`EOS account ${targetUserId} was reset to the mandatory initial-password flow by ${access.userId}`);
1248
1208
  res.status(200).json({
@@ -1336,14 +1296,10 @@ class Web {
1336
1296
  res.status(403).json({ error: 'permissionError' });
1337
1297
  return;
1338
1298
  }
1339
- if (req.headers['x-nexowatt-eos-auto-update'] !== '1') {
1299
+ if (req.headers['x-nexowatt-eos-auto-update'] !== '1' || !this.isEosSameOriginWrite(req)) {
1340
1300
  res.status(403).json({ error: 'invalidRequest' });
1341
1301
  return;
1342
1302
  }
1343
- if (!this.isEosSameOriginWrite(req)) {
1344
- res.status(403).json({ error: 'invalidRequestOrigin' });
1345
- return;
1346
- }
1347
1303
  if (typeof req.body?.enabled !== 'boolean') {
1348
1304
  res.status(400).json({ error: 'invalidEnabledValue' });
1349
1305
  return;
@@ -1353,7 +1309,6 @@ class Web {
1353
1309
  isEosSameOriginWrite(req) {
1354
1310
  return (0, eosRequestSecurity_1.isEosSameOriginRequest)(req.headers);
1355
1311
  }
1356
-
1357
1312
  async saveEosBasicSettings(req, res) {
1358
1313
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1359
1314
  const access = await this.getEosRequestAccess(req);
@@ -1421,7 +1376,6 @@ class Web {
1421
1376
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
1422
1377
  const { userId, groups, groupNames, adminGroups, role } = await this.getEosRequestAccess(req);
1423
1378
  const passwordSetup = await this.getEosFirstLoginPasswordState(userId, role);
1424
- const setupTicket = passwordSetup.required && userId ? this.issueEosPasswordSetupTicket(req, userId, role) : null;
1425
1379
  const isAdministrator = role === 'admin' && (userId === 'system.user.admin' || groups.includes('system.group.administrator'));
1426
1380
  const isEosAdminGroup = role === 'admin';
1427
1381
  const isInstaller = role === 'installer';
@@ -1442,12 +1396,7 @@ class Web {
1442
1396
  isEndUser,
1443
1397
  isAdmin: isEosAdminGroup,
1444
1398
  mustChangePassword: passwordSetup.required,
1445
- passwordSetup: {
1446
- ...passwordSetup,
1447
- userName: userId?.replace(/^system\.user\./, '') || '',
1448
- setupToken: setupTicket?.token || '',
1449
- setupTokenExpiresAt: setupTicket?.expiresAt || 0,
1450
- },
1399
+ passwordSetup: { ...passwordSetup, userName: userId?.replace(/^system\.user\./, '') || '' },
1451
1400
  capabilities: this.getEosRoleCapabilities(role),
1452
1401
  hideLegacyAdminForNonAdmins: this.settings.eosHideLegacyAdminForNonAdmins !== false && this.settings.eosHideLegacyAdminFromNonAdmins !== false,
1453
1402
  hideLegacyAdminFromNonAdmins: this.settings.eosHideLegacyAdminForNonAdmins !== false && this.settings.eosHideLegacyAdminFromNonAdmins !== false,
@@ -1589,25 +1538,10 @@ class Web {
1589
1538
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
1590
1539
  next();
1591
1540
  });*/
1592
- void this.getNexoWattStableUpdateManager().start().catch(error => {
1593
- this.adapter.log.warn(`[NexoWatt stable updates] Startup reconciliation failed: ${error instanceof Error ? error.message : error}`);
1594
- });
1595
- this.server.app.get('/nexowatt/updates/status', (req, res) => {
1596
- void this.sendNexoWattStableUpdateStatus(req, res).catch(error => {
1597
- this.adapter.log.warn(`[NexoWatt stable updates] Cannot read status: ${error instanceof Error ? error.message : error}`);
1598
- res.status(500).json({ error: 'autoUpdateStatusFailed' });
1599
- });
1600
- });
1601
- this.server.app.post('/nexowatt/updates/settings', (req, res) => {
1602
- void this.saveNexoWattStableUpdateSettings(req, res).catch(error => {
1603
- this.adapter.log.warn(`[NexoWatt stable updates] Cannot save settings: ${error instanceof Error ? error.message : error}`);
1604
- res.status(500).json({ error: 'autoUpdateSettingsFailed' });
1605
- });
1606
- });
1541
+ void this.getNexoWattStableUpdateManager().start().catch(error => this.adapter.log.warn(`[NexoWatt stable updates] Startup reconciliation failed: ${error instanceof Error ? error.message : error}`));
1542
+ this.server.app.get('/nexowatt/updates/status', (req, res) => { void this.sendNexoWattStableUpdateStatus(req, res).catch(error => res.status(500).json({ error: String(error) })); });
1543
+ this.server.app.post('/nexowatt/updates/settings', (req, res) => { void this.saveNexoWattStableUpdateSettings(req, res).catch(error => res.status(500).json({ error: String(error) })); });
1607
1544
  this.server.app.get('/version', (_req, res) => {
1608
- res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1609
- res.setHeader('Pragma', 'no-cache');
1610
- res.setHeader('Expires', '0');
1611
1545
  res.status(200).send(this.adapter.version);
1612
1546
  });
1613
1547
  // v36: Repair stale/broken URLs generated by older EOS hard-logout builds.
@@ -1886,14 +1820,8 @@ class Web {
1886
1820
  });
1887
1821
  this.server.app.post('/nexowatt/account/first-password', (req, res) => {
1888
1822
  void this.saveEosFirstLoginPassword(req, res).catch(e => {
1889
- const detail = e instanceof Error ? e.message : String(e || '');
1890
- const code = detail.split(':', 1)[0];
1891
- this.adapter.log.warn(`Cannot initialize EOS first-login password [${code}]: ${detail}`);
1892
- const known = new Set([
1893
- 'passwordApiUnavailable', 'passwordNotPersisted', 'passwordVerificationFailed',
1894
- 'passwordMetadataNotPersisted', 'userObjectUnavailableAfterPasswordChange',
1895
- ]);
1896
- res.status(500).json({ error: known.has(code) ? code : 'passwordSetupFailed', detail: code });
1823
+ this.adapter.log.warn(`Cannot initialize EOS first-login password: ${e instanceof Error ? e.message : e}`);
1824
+ res.status(500).json({ error: 'passwordSetupFailed' });
1897
1825
  });
1898
1826
  });
1899
1827
  this.server.app.get('/nexowatt/account/manage', (req, res) => {
@@ -1904,14 +1832,8 @@ class Web {
1904
1832
  });
1905
1833
  this.server.app.post('/nexowatt/account/reset', (req, res) => {
1906
1834
  void this.resetEosAccountPassword(req, res).catch(e => {
1907
- const detail = e instanceof Error ? e.message : String(e || '');
1908
- const code = detail.split(':', 1)[0];
1909
- this.adapter.log.warn(`Cannot reset EOS account [${code}]: ${detail}`);
1910
- const known = new Set([
1911
- 'passwordApiUnavailable', 'passwordNotPersisted', 'passwordVerificationFailed',
1912
- 'passwordMetadataNotPersisted', 'userObjectUnavailableAfterPasswordReset',
1913
- ]);
1914
- res.status(500).json({ error: known.has(code) ? code : 'accountResetFailed', detail: code });
1835
+ this.adapter.log.warn(`Cannot reset EOS account: ${e instanceof Error ? e.message : e}`);
1836
+ res.status(500).json({ error: 'accountResetFailed' });
1915
1837
  });
1916
1838
  });
1917
1839
  this.server.app.get('/iobroker_check.html', (_req, res) => {
@@ -2038,29 +1960,7 @@ class Web {
2038
1960
  res.status(404).send(get404Page(`File ${escapeHtml(fileName)} not found`));
2039
1961
  }
2040
1962
  });
2041
- const isEosCurrentAsset = (pathName) => pathName === '/'
2042
- || pathName === '/index.html'
2043
- || pathName === '/manifest.json'
2044
- || pathName === '/mf-manifest.json'
2045
- || pathName === '/version'
2046
- || /^\/(?:js|css)\/(?:eos-|nexowatt-)/.test(pathName)
2047
- || /^\/static\/js\/nexowatt-stable-v\d+\.js$/.test(pathName)
2048
- || /^\/remoteEntry(?:-v\d+)?\.js$/.test(pathName)
2049
- || /^\/assets\/(?:bootstrap|hostInit|Intro|index-CQZugZ1z|index-D2ymscJA|iobroker_admin__mf_v__runtimeInit__mf_v__)-[^/]+\.js$/.test(pathName);
2050
- const applyNoStoreHeaders = (res) => {
2051
- res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
2052
- res.setHeader('Pragma', 'no-cache');
2053
- res.setHeader('Expires', '0');
2054
- res.setHeader('Surrogate-Control', 'no-store');
2055
- };
2056
- const appOptions = {
2057
- setHeaders: (res, filePath) => {
2058
- const raw = filePath.startsWith(this.wwwDir) ? filePath.slice(this.wwwDir.length) : filePath;
2059
- const relativePath = `/${raw.replace(/\\/g, '/').replace(/^\/+/, '')}`;
2060
- if (isEosCurrentAsset(relativePath))
2061
- applyNoStoreHeaders(res);
2062
- },
2063
- };
1963
+ const appOptions = {};
2064
1964
  if (this.settings.cache) {
2065
1965
  appOptions.maxAge = 30_758_400_000;
2066
1966
  }
@@ -2158,24 +2058,31 @@ class Web {
2158
2058
  this.server.app.get('/empty.html', (_req, res) => {
2159
2059
  res.status(200).send('');
2160
2060
  });
2161
- // EOS entrypoints, overlays and patched module bundles must never be reused
2162
- // from a previous release. Hashed untouched leaf assets may remain cacheable.
2061
+ // Stability release: entrypoints and module-federation manifests must always
2062
+ // revalidate. Hashed leaf assets remain cacheable through express.static.
2163
2063
  this.server.app.use((req, res, next) => {
2164
2064
  const pathName = req.path || req.url.split('?')[0];
2165
- if (isEosCurrentAsset(pathName))
2166
- applyNoStoreHeaders(res);
2065
+ if (pathName === '/'
2066
+ || pathName === '/index.html'
2067
+ || pathName === '/mf-manifest.json'
2068
+ || /^\/remoteEntry(?:-v\d+)?\.js$/.test(pathName)
2069
+ || /^\/assets\/(?:hostInit|index-CQZugZ1z)-[^/]+\.js$/.test(pathName)) {
2070
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
2071
+ res.setHeader('Pragma', 'no-cache');
2072
+ res.setHeader('Expires', '0');
2073
+ }
2167
2074
  next();
2168
2075
  });
2169
2076
  this.server.app.get('/index.html', async (_req, res) => {
2170
- this.indexHTML = await this.prepareIndex('/index.html');
2077
+ this.indexHTML ||= await this.prepareIndex('/index.html');
2171
2078
  res.header('Content-Type', 'text/html');
2172
- applyNoStoreHeaders(res);
2079
+ res.header('Cache-Control', 'no-cache');
2173
2080
  res.status(200).send(this.indexHTML);
2174
2081
  });
2175
2082
  this.server.app.get('/', async (_req, res) => {
2176
- this.indexHTML = await this.prepareIndex('/index.html');
2083
+ this.indexHTML ||= await this.prepareIndex('/index.html');
2177
2084
  res.header('Content-Type', 'text/html');
2178
- applyNoStoreHeaders(res);
2085
+ res.header('Cache-Control', 'no-cache');
2179
2086
  res.status(200).send(this.indexHTML);
2180
2087
  });
2181
2088
  this.server.app.use('/', express.static(this.wwwDir, appOptions));