iobroker.eos-admin 7.10.0 → 7.10.1

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 (53) hide show
  1. package/INSTALL_TEST_V7.10.1.md +10 -0
  2. package/MERGE_UPDATE_README_V7.10.1.md +10 -0
  3. package/NEXOWATT_EOS_BUILD_INFO.json +20 -21
  4. package/PUBLISH_STABLE_V7.10.1.md +10 -0
  5. package/README.md +16 -6
  6. package/README_STABILITY_V7.10.1.md +10 -0
  7. package/RELEASE_ACCEPTANCE_V7.10.1.md +10 -0
  8. package/RELEASE_NOTES_V7.10.1.md +10 -0
  9. package/adminWww/assets/Intro-DkwRiz1n-v84.js +1 -1
  10. package/adminWww/assets/bootstrap-COulQZax-v84.js +2 -2
  11. package/adminWww/css/eos-ems-overview.css +5 -3
  12. package/adminWww/css/nexowatt-native-shell.css +85 -413
  13. package/adminWww/index.html +20 -21
  14. package/adminWww/js/eos-account-management.js +28 -19
  15. package/adminWww/js/eos-assistant.js +1 -1
  16. package/adminWww/js/eos-ems-overview.js +1 -1
  17. package/adminWww/js/eos-role-bootstrap.js +17 -80
  18. package/adminWww/js/eos-role-ui.js +28 -4
  19. package/adminWww/js/nexowatt-native-shell.js +1 -1
  20. package/adminWww/static/js/nexowatt-stable-login.js +43 -70
  21. package/adminWww/static/js/nexowatt-stable-v7101.js +27 -0
  22. package/build/lib/web.js +159 -283
  23. package/build/main.js +39 -67
  24. package/io-package.json +17 -7
  25. package/package.json +15 -22
  26. package/tools/nexowatt-account-management-selftest.cjs +15 -19
  27. package/tools/nexowatt-account-password-flow-selftest.cjs +15 -0
  28. package/tools/nexowatt-apply-ems-overview.cjs +4 -2
  29. package/tools/nexowatt-clean-legacy-runtime.cjs +1 -1
  30. package/tools/nexowatt-ems-overview-selftest.cjs +1 -1
  31. package/tools/nexowatt-ensure-release-defaults.cjs +1 -1
  32. package/tools/nexowatt-login-layout-selftest.cjs +9 -13
  33. package/tools/nexowatt-merge-update-selftest.cjs +1 -1
  34. package/tools/nexowatt-modern-ui-selftest.cjs +10 -46
  35. package/tools/nexowatt-password-write-selftest.cjs +46 -16
  36. package/tools/nexowatt-release-defaults-selftest.cjs +1 -1
  37. package/tools/nexowatt-runtime-cleanup-selftest.cjs +6 -8
  38. package/tools/nexowatt-stable-v7101-selftest.cjs +15 -0
  39. package/tools/nexowatt-sync-release-version.cjs +43 -5
  40. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +2 -3
  41. package/tools/nexowatt-validate-package.cjs +27 -26
  42. package/tools/nexowatt-version-sync-selftest.cjs +18 -1
  43. package/INSTALL_TEST_V7.10.0.md +0 -11
  44. package/MERGE_UPDATE_README_V7.10.0.md +0 -5
  45. package/PUBLISH_STABLE_V7.10.0.md +0 -15
  46. package/README_STABILITY_V7.10.0.md +0 -3
  47. package/RELEASE_ACCEPTANCE_V7.10.0.md +0 -11
  48. package/RELEASE_NOTES_V7.10.0.md +0 -17
  49. package/adminWww/static/js/nexowatt-stable-v7100.js +0 -111
  50. package/src/lib/eosAutoUpdate.ts +0 -320
  51. package/tools/nexowatt-cache-update-selftest.cjs +0 -12
  52. package/tools/nexowatt-request-security-selftest.cjs +0 -11
  53. 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
  /**
@@ -718,10 +716,54 @@ class Web {
718
716
  }
719
717
  return { valid: true, password: value };
720
718
  }
719
+ getEosPasswordUserName(userId) {
720
+ const userName = String(userId || '').trim().replace(/^system\.user\./, '');
721
+ if (!/^[A-Za-z0-9_.@-]+$/.test(userName)) {
722
+ throw new Error('invalidPasswordTarget');
723
+ }
724
+ return userName;
725
+ }
721
726
  async setEosUserPassword(userId, password) {
722
- await (0, eosPassword_1.setEosUserPasswordWithVerification)(this.adapter, userId, password, EOS_PASSWORD_SERVICE_USER);
727
+ // ioBroker's password API expects the account name, not the system.user.* object id.
728
+ const userName = this.getEosPasswordUserName(userId);
729
+ // The server-side Admin/Service context performs the controller password write, so managed
730
+ // Installer and End User accounts do not need global users.write rights in the browser.
731
+ const options = { user: EOS_PASSWORD_SERVICE_USER };
732
+ if (typeof this.adapter.setPasswordAsync === 'function') {
733
+ await this.adapter.setPasswordAsync(userName, password, options);
734
+ }
735
+ else if (typeof this.adapter.setPassword === 'function') {
736
+ await new Promise((resolve, reject) => {
737
+ this.adapter.setPassword?.(userName, password, options, error => (error ? reject(error) : resolve()));
738
+ });
739
+ }
740
+ else {
741
+ throw new Error('passwordApiUnavailable');
742
+ }
743
+ if (typeof this.adapter.checkPasswordAsync === 'function') {
744
+ const result = await this.adapter.checkPasswordAsync(userName, password, options);
745
+ const valid = Array.isArray(result) ? result[0] === true : result === true;
746
+ if (!valid) {
747
+ throw new Error('passwordVerificationFailed');
748
+ }
749
+ return;
750
+ }
751
+ const userObject = (await this.adapter.getForeignObjectAsync(userId));
752
+ if (!String(userObject?.common?.password || '').trim()) {
753
+ throw new Error('passwordWriteFailed');
754
+ }
755
+ }
756
+ async updateEosAccountMetadata(userId, updater) {
757
+ const userObject = (await this.adapter.getForeignObjectAsync(userId));
758
+ if (!userObject) {
759
+ throw new Error('userObjectUnavailableAfterPasswordChange');
760
+ }
761
+ const native = { ...(userObject.native || {}) };
762
+ const account = { ...(native.nexowattEosAccount || {}) };
763
+ native.nexowattEosAccount = account;
764
+ updater(native, account);
765
+ await this.adapter.extendForeignObjectAsync(userId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
723
766
  }
724
-
725
767
  async destroyEosRequestSessions(req) {
726
768
  const token = this.readAccessTokenFromRequest(req);
727
769
  if (!token || typeof this.adapter.destroySession !== 'function') {
@@ -745,87 +787,55 @@ class Web {
745
787
  }
746
788
  async saveEosFirstLoginPassword(req, res) {
747
789
  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' });
790
+ const access = await this.getEosRequestAccess(req);
791
+ if (!access.userId || access.role === 'admin') {
792
+ res.status(403).json({ error: 'passwordSetupNotAllowed' });
750
793
  return;
751
794
  }
752
- if (!this.isEosSameOriginWrite(req)) {
795
+ if (!this.isEosSameOriginWrite(req) || req.headers['x-nexowatt-eos-first-login'] !== '1') {
753
796
  res.status(403).json({ error: 'invalidRequestOrigin' });
754
797
  return;
755
798
  }
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
799
  const passwordState = await this.getEosFirstLoginPasswordState(access.userId, access.role);
774
800
  if (!passwordState.required) {
775
- this.invalidateEosPasswordSetupTicketsForUser(access.userId);
776
801
  res.status(200).json({ success: true, alreadyInitialized: true });
777
802
  return;
778
803
  }
804
+ const body = (req.body || {});
779
805
  const validation = this.validateEosFirstLoginPassword(body.password, body.passwordRepeat, access.userId);
780
806
  if (!validation.valid) {
781
- res.status(400).json({ error: validation.error, minLength: passwordState.minLength });
807
+ res.status(400).json({
808
+ error: validation.error,
809
+ minLength: passwordState.minLength,
810
+ });
782
811
  return;
783
812
  }
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 || {}) };
813
+ await this.setEosUserPassword(access.userId, validation.password);
796
814
  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);
815
+ await this.updateEosAccountMetadata(access.userId, (native, account) => {
816
+ account.passwordInitialized = true;
817
+ account.passwordInitializedAt = Date.now();
818
+ account.passwordInitializationVersion = 1;
819
+ account.passwordInitializedBy = 'self';
820
+ account.passwordSetupVersion = 1;
821
+ account.passwordSetAt = now;
822
+ account.firstLoginCompletedAt = now;
823
+ account.forcePasswordChange = false;
824
+ account.passwordlessFirstLoginAllowed = false;
825
+ native.nexowattPasswordChangeRequired = false;
826
+ native.eosPasswordChangeRequired = false;
827
+ native.nexowattFirstLoginPending = false;
828
+ native.eosFirstLoginRequired = false;
829
+ native.nexowattInitialPasswordApplied = false;
830
+ native.nexowattPasswordChangedAt = now;
831
+ });
832
+ await this.destroyEosRequestSessions(req);
819
833
  for (const cookie of ['access_token', 'refresh_token', 'connect.sid']) {
820
834
  res.clearCookie(cookie);
821
835
  }
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
836
  this.adapter.log.info(`EOS first-login password initialized for ${access.userId}`);
837
+ res.status(200).json({ success: true, role: access.role, logoutRequired: true, sessionInvalidated: true });
827
838
  }
828
-
829
839
  isEosPasswordlessFirstLoginEnabled() {
830
840
  return this.settings.eosPasswordlessFirstLogin !== false;
831
841
  }
@@ -864,53 +874,6 @@ class Web {
864
874
  hashEosPasswordClaim(token) {
865
875
  return (0, node_crypto_1.createHash)('sha256').update(token).digest('hex');
866
876
  }
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
877
  readEosCookie(req, name) {
915
878
  const row = String(req.headers.cookie || '')
916
879
  .split(';')
@@ -1089,29 +1052,25 @@ class Web {
1089
1052
  return;
1090
1053
  }
1091
1054
  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
1055
  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 });
1056
+ await this.updateEosAccountMetadata(claim.userId, (native, account) => {
1057
+ account.passwordInitialized = true;
1058
+ account.passwordInitializedAt = Date.now();
1059
+ account.passwordInitializationVersion = 1;
1060
+ account.passwordInitializedBy = 'passwordless-first-activation';
1061
+ account.passwordSetupVersion = 1;
1062
+ account.passwordSetAt = now;
1063
+ account.firstLoginCompletedAt = now;
1064
+ account.forcePasswordChange = false;
1065
+ account.passwordlessFirstLoginAllowed = false;
1066
+ account.passwordlessClaimCompletedAt = now;
1067
+ native.nexowattPasswordChangeRequired = false;
1068
+ native.eosPasswordChangeRequired = false;
1069
+ native.nexowattFirstLoginPending = false;
1070
+ native.eosFirstLoginRequired = false;
1071
+ native.nexowattInitialPasswordApplied = false;
1072
+ native.nexowattPasswordChangedAt = now;
1073
+ });
1115
1074
  this.invalidateEosPasswordClaimsForUser(claim.userId);
1116
1075
  res.clearCookie('nexowatt_eos_first_login', { path: '/nexowatt/account' });
1117
1076
  this.adapter.log.info(`EOS passwordless first activation completed for ${claim.userId}`);
@@ -1178,14 +1137,10 @@ class Web {
1178
1137
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1179
1138
  const access = await this.getEosRequestAccess(req);
1180
1139
  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' });
1140
+ res.status(403).json({ error: 'permissionError' });
1186
1141
  return;
1187
1142
  }
1188
- if (!this.isEosSameOriginWrite(req)) {
1143
+ if (!this.isEosSameOriginWrite(req) || req.headers['x-nexowatt-eos-account-reset'] !== '1') {
1189
1144
  res.status(403).json({ error: 'invalidRequestOrigin' });
1190
1145
  return;
1191
1146
  }
@@ -1194,12 +1149,19 @@ class Web {
1194
1149
  res.status(400).json({ error: 'invalidTarget' });
1195
1150
  return;
1196
1151
  }
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) {
1152
+ const targetAccess = await this.getEosAccessForUserId(targetUserId);
1153
+ const explicitInstaller = targetAccess.groups.some(group => this.getEosInstallerGroups().includes(group));
1154
+ const explicitEndUser = targetAccess.groups.some(group => this.getEosEndUserGroups().includes(group));
1155
+ const explicitlyManagedRole = explicitInstaller
1156
+ ? 'installer'
1157
+ : explicitEndUser
1158
+ ? 'enduser'
1159
+ : null;
1160
+ if (targetAccess.role === 'admin'
1161
+ || !explicitlyManagedRole
1162
+ || (access.role === 'installer' && explicitlyManagedRole !== 'enduser')) {
1163
+ // Never rely on the secure-default "enduser" classification for a password reset. The
1164
+ // target must be an explicit member of a managed EOS installer/end-user group.
1203
1165
  res.status(403).json({ error: 'permissionError' });
1204
1166
  return;
1205
1167
  }
@@ -1213,36 +1175,26 @@ class Web {
1213
1175
  return;
1214
1176
  }
1215
1177
  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
1178
  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
- }
1179
+ await this.updateEosAccountMetadata(targetUserId, (native, account) => {
1180
+ account.passwordInitialized = false;
1181
+ account.passwordSetupVersion = 0;
1182
+ account.passwordInitializationVersion = 0;
1183
+ account.forcePasswordChange = true;
1184
+ account.passwordlessFirstLoginAllowed = false;
1185
+ account.passwordResetAt = now;
1186
+ account.passwordResetBy = access.userId;
1187
+ account.passwordResetMode = 'initial-password';
1188
+ account.passwordSetAt = null;
1189
+ account.firstLoginCompletedAt = null;
1190
+ native.nexowattPasswordChangeRequired = true;
1191
+ native.eosPasswordChangeRequired = true;
1192
+ native.nexowattFirstLoginPending = true;
1193
+ native.eosFirstLoginRequired = true;
1194
+ native.nexowattInitialPasswordApplied = true;
1195
+ native.nexowattInitialPasswordVersion = 1;
1196
+ native.nexowattStableInitialCredentialVersion = 1;
1197
+ });
1246
1198
  this.invalidateEosPasswordClaimsForUser(targetUserId);
1247
1199
  this.adapter.log.warn(`EOS account ${targetUserId} was reset to the mandatory initial-password flow by ${access.userId}`);
1248
1200
  res.status(200).json({
@@ -1318,42 +1270,12 @@ class Web {
1318
1270
  ],
1319
1271
  });
1320
1272
  }
1321
- getNexoWattStableUpdateManager() {
1322
- this.nexowattStableUpdateManager ||= new eosAutoUpdate_1.NexoWattStableUpdateManager(this.adapter);
1323
- return this.nexowattStableUpdateManager;
1324
- }
1325
- async sendNexoWattStableUpdateStatus(req, res) {
1326
- const access = await this.getEosRequestAccess(req);
1327
- if (access.role !== 'admin') {
1328
- res.status(403).json({ error: 'permissionError' });
1329
- return;
1330
- }
1331
- res.status(200).json(await this.getNexoWattStableUpdateManager().getStatus(false));
1332
- }
1333
- async saveNexoWattStableUpdateSettings(req, res) {
1334
- const access = await this.getEosRequestAccess(req);
1335
- if (access.role !== 'admin') {
1336
- res.status(403).json({ error: 'permissionError' });
1337
- return;
1338
- }
1339
- if (req.headers['x-nexowatt-eos-auto-update'] !== '1') {
1340
- res.status(403).json({ error: 'invalidRequest' });
1341
- return;
1342
- }
1343
- if (!this.isEosSameOriginWrite(req)) {
1344
- res.status(403).json({ error: 'invalidRequestOrigin' });
1345
- return;
1346
- }
1347
- if (typeof req.body?.enabled !== 'boolean') {
1348
- res.status(400).json({ error: 'invalidEnabledValue' });
1349
- return;
1350
- }
1351
- res.status(200).json(await this.getNexoWattStableUpdateManager().setEnabled(req.body.enabled));
1352
- }
1353
- isEosSameOriginWrite(req) {
1354
- return (0, eosRequestSecurity_1.isEosSameOriginRequest)(req.headers);
1355
- }
1356
1273
 
1274
+ getNexoWattStableUpdateManager() { this.nexowattStableUpdateManager ||= new eosAutoUpdate_1.NexoWattStableUpdateManager(this.adapter); return this.nexowattStableUpdateManager; }
1275
+ async sendNexoWattStableUpdateStatus(req, res) { const access = await this.getEosRequestAccess(req); if (access.role !== 'admin') { res.status(403).json({ error: 'permissionError' }); return; } res.status(200).json(await this.getNexoWattStableUpdateManager().getStatus(false)); }
1276
+ async saveNexoWattStableUpdateSettings(req, res) { const access = await this.getEosRequestAccess(req); if (access.role !== 'admin') { res.status(403).json({ error: 'permissionError' }); return; } if (req.headers['x-nexowatt-eos-auto-update'] !== '1' || !this.isEosSameOriginWrite(req)) { res.status(403).json({ error: 'invalidRequest' }); return; } if (typeof req.body?.enabled !== 'boolean') { res.status(400).json({ error: 'invalidEnabledValue' }); return; } res.status(200).json(await this.getNexoWattStableUpdateManager().setEnabled(req.body.enabled)); }
1277
+
1278
+ isEosSameOriginWrite(req) { return (0, eosRequestSecurity_1.isEosSameOriginRequest)(req.headers); }
1357
1279
  async saveEosBasicSettings(req, res) {
1358
1280
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1359
1281
  const access = await this.getEosRequestAccess(req);
@@ -1421,7 +1343,6 @@ class Web {
1421
1343
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
1422
1344
  const { userId, groups, groupNames, adminGroups, role } = await this.getEosRequestAccess(req);
1423
1345
  const passwordSetup = await this.getEosFirstLoginPasswordState(userId, role);
1424
- const setupTicket = passwordSetup.required && userId ? this.issueEosPasswordSetupTicket(req, userId, role) : null;
1425
1346
  const isAdministrator = role === 'admin' && (userId === 'system.user.admin' || groups.includes('system.group.administrator'));
1426
1347
  const isEosAdminGroup = role === 'admin';
1427
1348
  const isInstaller = role === 'installer';
@@ -1442,12 +1363,7 @@ class Web {
1442
1363
  isEndUser,
1443
1364
  isAdmin: isEosAdminGroup,
1444
1365
  mustChangePassword: passwordSetup.required,
1445
- passwordSetup: {
1446
- ...passwordSetup,
1447
- userName: userId?.replace(/^system\.user\./, '') || '',
1448
- setupToken: setupTicket?.token || '',
1449
- setupTokenExpiresAt: setupTicket?.expiresAt || 0,
1450
- },
1366
+ passwordSetup: { ...passwordSetup, userName: userId?.replace(/^system\.user\./, '') || '' },
1451
1367
  capabilities: this.getEosRoleCapabilities(role),
1452
1368
  hideLegacyAdminForNonAdmins: this.settings.eosHideLegacyAdminForNonAdmins !== false && this.settings.eosHideLegacyAdminFromNonAdmins !== false,
1453
1369
  hideLegacyAdminFromNonAdmins: this.settings.eosHideLegacyAdminForNonAdmins !== false && this.settings.eosHideLegacyAdminFromNonAdmins !== false,
@@ -1589,25 +1505,12 @@ class Web {
1589
1505
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
1590
1506
  next();
1591
1507
  });*/
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
- });
1508
+
1509
+ void this.getNexoWattStableUpdateManager().start().catch(error => this.adapter.log.warn(`[NexoWatt stable updates] Startup reconciliation failed: ${error instanceof Error ? error.message : error}`));
1510
+ this.server.app.get('/nexowatt/updates/status', (req, res) => { void this.sendNexoWattStableUpdateStatus(req, res).catch(error => res.status(500).json({ error: String(error) })); });
1511
+ this.server.app.post('/nexowatt/updates/settings', (req, res) => { void this.saveNexoWattStableUpdateSettings(req, res).catch(error => res.status(500).json({ error: String(error) })); });
1512
+
1607
1513
  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
1514
  res.status(200).send(this.adapter.version);
1612
1515
  });
1613
1516
  // v36: Repair stale/broken URLs generated by older EOS hard-logout builds.
@@ -1886,14 +1789,8 @@ class Web {
1886
1789
  });
1887
1790
  this.server.app.post('/nexowatt/account/first-password', (req, res) => {
1888
1791
  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 });
1792
+ this.adapter.log.warn(`Cannot initialize EOS first-login password: ${e instanceof Error ? e.message : e}`);
1793
+ res.status(500).json({ error: 'passwordSetupFailed' });
1897
1794
  });
1898
1795
  });
1899
1796
  this.server.app.get('/nexowatt/account/manage', (req, res) => {
@@ -1904,14 +1801,8 @@ class Web {
1904
1801
  });
1905
1802
  this.server.app.post('/nexowatt/account/reset', (req, res) => {
1906
1803
  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 });
1804
+ this.adapter.log.warn(`Cannot reset EOS account: ${e instanceof Error ? e.message : e}`);
1805
+ res.status(500).json({ error: 'accountResetFailed' });
1915
1806
  });
1916
1807
  });
1917
1808
  this.server.app.get('/iobroker_check.html', (_req, res) => {
@@ -2038,29 +1929,7 @@ class Web {
2038
1929
  res.status(404).send(get404Page(`File ${escapeHtml(fileName)} not found`));
2039
1930
  }
2040
1931
  });
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
- };
1932
+ const appOptions = {};
2064
1933
  if (this.settings.cache) {
2065
1934
  appOptions.maxAge = 30_758_400_000;
2066
1935
  }
@@ -2158,24 +2027,31 @@ class Web {
2158
2027
  this.server.app.get('/empty.html', (_req, res) => {
2159
2028
  res.status(200).send('');
2160
2029
  });
2161
- // EOS entrypoints, overlays and patched module bundles must never be reused
2162
- // from a previous release. Hashed untouched leaf assets may remain cacheable.
2030
+ // Stability release: entrypoints and module-federation manifests must always
2031
+ // revalidate. Hashed leaf assets remain cacheable through express.static.
2163
2032
  this.server.app.use((req, res, next) => {
2164
2033
  const pathName = req.path || req.url.split('?')[0];
2165
- if (isEosCurrentAsset(pathName))
2166
- applyNoStoreHeaders(res);
2034
+ if (pathName === '/'
2035
+ || pathName === '/index.html'
2036
+ || pathName === '/mf-manifest.json'
2037
+ || /^\/remoteEntry(?:-v\d+)?\.js$/.test(pathName)
2038
+ || /^\/assets\/(?:hostInit|index-CQZugZ1z)-[^/]+\.js$/.test(pathName)) {
2039
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
2040
+ res.setHeader('Pragma', 'no-cache');
2041
+ res.setHeader('Expires', '0');
2042
+ }
2167
2043
  next();
2168
2044
  });
2169
2045
  this.server.app.get('/index.html', async (_req, res) => {
2170
- this.indexHTML = await this.prepareIndex('/index.html');
2046
+ this.indexHTML ||= await this.prepareIndex('/index.html');
2171
2047
  res.header('Content-Type', 'text/html');
2172
- applyNoStoreHeaders(res);
2048
+ res.header('Cache-Control', 'no-cache');
2173
2049
  res.status(200).send(this.indexHTML);
2174
2050
  });
2175
2051
  this.server.app.get('/', async (_req, res) => {
2176
- this.indexHTML = await this.prepareIndex('/index.html');
2052
+ this.indexHTML ||= await this.prepareIndex('/index.html');
2177
2053
  res.header('Content-Type', 'text/html');
2178
- applyNoStoreHeaders(res);
2054
+ res.header('Cache-Control', 'no-cache');
2179
2055
  res.status(200).send(this.indexHTML);
2180
2056
  });
2181
2057
  this.server.app.use('/', express.static(this.wwwDir, appOptions));