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.
- package/INSTALL_TEST_V7.10.1.md +10 -0
- package/MERGE_UPDATE_README_V7.10.1.md +10 -0
- package/NEXOWATT_EOS_BUILD_INFO.json +20 -21
- package/PUBLISH_STABLE_V7.10.1.md +10 -0
- package/README.md +16 -6
- package/README_STABILITY_V7.10.1.md +10 -0
- package/RELEASE_ACCEPTANCE_V7.10.1.md +10 -0
- package/RELEASE_NOTES_V7.10.1.md +10 -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/web.js +159 -283
- package/build/main.js +39 -67
- package/io-package.json +17 -7
- package/package.json +15 -22
- 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-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 +1 -1
- package/tools/nexowatt-modern-ui-selftest.cjs +10 -46
- package/tools/nexowatt-password-write-selftest.cjs +46 -16
- package/tools/nexowatt-release-defaults-selftest.cjs +1 -1
- package/tools/nexowatt-runtime-cleanup-selftest.cjs +6 -8
- package/tools/nexowatt-stable-v7101-selftest.cjs +15 -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 +27 -26
- 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/static/js/nexowatt-stable-v7100.js +0 -111
- 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
|
/**
|
|
@@ -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
|
-
|
|
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
|
-
|
|
749
|
-
|
|
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({
|
|
807
|
+
res.status(400).json({
|
|
808
|
+
error: validation.error,
|
|
809
|
+
minLength: passwordState.minLength,
|
|
810
|
+
});
|
|
782
811
|
return;
|
|
783
812
|
}
|
|
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 || {}) };
|
|
813
|
+
await this.setEosUserPassword(access.userId, validation.password);
|
|
796
814
|
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);
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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'
|
|
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
|
|
1198
|
-
const
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
1593
|
-
|
|
1594
|
-
});
|
|
1595
|
-
this.server.app.
|
|
1596
|
-
|
|
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
|
-
|
|
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 });
|
|
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
|
-
|
|
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 });
|
|
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
|
|
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
|
-
//
|
|
2162
|
-
//
|
|
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 (
|
|
2166
|
-
|
|
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
|
|
2046
|
+
this.indexHTML ||= await this.prepareIndex('/index.html');
|
|
2171
2047
|
res.header('Content-Type', 'text/html');
|
|
2172
|
-
|
|
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
|
|
2052
|
+
this.indexHTML ||= await this.prepareIndex('/index.html');
|
|
2177
2053
|
res.header('Content-Type', 'text/html');
|
|
2178
|
-
|
|
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));
|