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.
- package/INSTALL_TEST_V7.10.2.md +22 -0
- package/MERGE_UPDATE_README_V7.10.2.md +11 -0
- package/NEXOWATT_EOS_BUILD_INFO.json +19 -21
- package/NEXOWATT_EOS_PREBUILT_MANIFEST.json +1410 -0
- package/PUBLISH_STABLE_V7.10.2.md +21 -0
- package/README.md +16 -6
- package/README_STABILITY_V7.10.2.md +20 -0
- package/RELEASE_ACCEPTANCE_V7.10.2.md +13 -0
- package/RELEASE_NOTES_V7.10.2.md +11 -0
- package/adminWww/assets/Intro-DkwRiz1n-v84.js +1 -1
- package/adminWww/assets/bootstrap-COulQZax-v84.js +2 -2
- package/adminWww/css/eos-ems-overview.css +5 -3
- package/adminWww/css/nexowatt-native-shell.css +85 -413
- package/adminWww/index.html +20 -21
- package/adminWww/js/eos-account-management.js +28 -19
- package/adminWww/js/eos-assistant.js +1 -1
- package/adminWww/js/eos-ems-overview.js +1 -1
- package/adminWww/js/eos-role-bootstrap.js +17 -80
- package/adminWww/js/eos-role-ui.js +28 -4
- package/adminWww/js/nexowatt-native-shell.js +1 -1
- package/adminWww/static/js/nexowatt-stable-login.js +43 -70
- package/adminWww/static/js/nexowatt-stable-v7101.js +27 -0
- package/build/lib/eosAutoUpdate.js +418 -20
- package/build/lib/eosAutoUpdate.js.map +1 -0
- package/build/lib/eosRequestSecurity.js +4 -1
- package/build/lib/eosRequestSecurity.js.map +1 -0
- package/build/lib/web.js +161 -254
- package/build/lib/web.js.map +1 -1
- package/build/main.js +39 -67
- package/io-package.json +72 -7
- package/package.json +24 -25
- package/tools/nexowatt-account-management-selftest.cjs +15 -19
- package/tools/nexowatt-account-password-flow-selftest.cjs +15 -0
- package/tools/nexowatt-apply-ems-overview.cjs +4 -2
- package/tools/nexowatt-auto-update-selftest.cjs +144 -32
- package/tools/nexowatt-backend-runtime-selftest.cjs +147 -0
- package/tools/nexowatt-clean-legacy-runtime.cjs +1 -1
- package/tools/nexowatt-ems-overview-selftest.cjs +1 -1
- package/tools/nexowatt-ensure-release-defaults.cjs +1 -1
- package/tools/nexowatt-login-layout-selftest.cjs +9 -13
- package/tools/nexowatt-merge-update-selftest.cjs +7 -1
- package/tools/nexowatt-modern-ui-selftest.cjs +10 -46
- package/tools/nexowatt-password-write-selftest.cjs +46 -16
- package/tools/nexowatt-prebuilt-release-selftest.cjs +292 -0
- package/tools/nexowatt-release-defaults-selftest.cjs +1 -1
- package/tools/nexowatt-runtime-cleanup-selftest.cjs +6 -8
- package/tools/nexowatt-stable-v7102-selftest.cjs +51 -0
- package/tools/nexowatt-sync-release-version.cjs +43 -5
- package/tools/nexowatt-ui-overview-runtime-selftest.cjs +2 -3
- package/tools/nexowatt-validate-package.cjs +54 -29
- package/tools/nexowatt-version-sync-selftest.cjs +18 -1
- package/INSTALL_TEST_V7.10.0.md +0 -11
- package/MERGE_UPDATE_README_V7.10.0.md +0 -5
- package/PUBLISH_STABLE_V7.10.0.md +0 -15
- package/README_STABILITY_V7.10.0.md +0 -3
- package/RELEASE_ACCEPTANCE_V7.10.0.md +0 -11
- package/RELEASE_NOTES_V7.10.0.md +0 -17
- package/adminWww/js/eos-release-watch.js +0 -77
- package/adminWww/static/js/nexowatt-stable-v7100.js +0 -111
- package/build/lib/eosPassword.js +0 -72
- package/build/lib/nexowattStableAuth.js +0 -74
- package/src/lib/eosAutoUpdate.ts +0 -320
- package/tools/nexowatt-cache-update-selftest.cjs +0 -12
- package/tools/nexowatt-request-security-selftest.cjs +0 -11
- 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
|
-
|
|
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
|
-
|
|
749
|
-
|
|
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({
|
|
815
|
+
res.status(400).json({
|
|
816
|
+
error: validation.error,
|
|
817
|
+
minLength: passwordState.minLength,
|
|
818
|
+
});
|
|
782
819
|
return;
|
|
783
820
|
}
|
|
784
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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'
|
|
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
|
|
1198
|
-
const
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1890
|
-
|
|
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
|
-
|
|
1908
|
-
|
|
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
|
|
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
|
-
//
|
|
2162
|
-
//
|
|
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 (
|
|
2166
|
-
|
|
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
|
|
2077
|
+
this.indexHTML ||= await this.prepareIndex('/index.html');
|
|
2171
2078
|
res.header('Content-Type', 'text/html');
|
|
2172
|
-
|
|
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
|
|
2083
|
+
this.indexHTML ||= await this.prepareIndex('/index.html');
|
|
2177
2084
|
res.header('Content-Type', 'text/html');
|
|
2178
|
-
|
|
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));
|