iobroker.eos-admin 7.9.32 → 7.9.34
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/adminWww/assets/Adapters-B5_jQ7DE.js +2 -2
- package/adminWww/assets/bootstrap-COulQZax.js +1 -1
- package/adminWww/css/eos-branding.css +55 -0
- package/adminWww/index.html +6 -5
- package/adminWww/js/eos-assistant.js +1 -1
- package/adminWww/js/eos-branding.js +75 -1
- package/adminWww/js/eos-hard-logout.js +163 -0
- package/build/lib/web.js +75 -23
- package/docs/NEXOWATT_EOS_UI_V33_ASSIST_POSITION_DE.md +11 -0
- package/docs/NEXOWATT_EOS_UI_V34_UPDATE_HARD_LOGOUT_DE.md +23 -0
- package/io-package.json +8 -8
- package/package.json +1 -1
- package/tools/nexowatt-validate-package.cjs +14 -0
package/adminWww/index.html
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
rel="stylesheet"
|
|
32
32
|
href="css/leaflet.css"
|
|
33
33
|
/>
|
|
34
|
-
<link rel="stylesheet" href="./css/eos-branding.css?v=
|
|
34
|
+
<link rel="stylesheet" href="./css/eos-branding.css?v=34" />
|
|
35
35
|
<link
|
|
36
36
|
rel="manifest"
|
|
37
37
|
href="manifest.json"
|
|
@@ -154,12 +154,13 @@
|
|
|
154
154
|
<script type="module" crossorigin src="./assets/index-CQZugZ1z.js"></script>
|
|
155
155
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-BDBacUwf.js">
|
|
156
156
|
<link rel="modulepreload" crossorigin href="./assets/iobroker_admin__mf_v__runtimeInit__mf_v__-g2X2zhAf.js">
|
|
157
|
-
<script defer src="./js/eos-branding.js?v=
|
|
158
|
-
<script defer src="./js/eos-security-ui.js?v=
|
|
159
|
-
<script defer src="./js/eos-
|
|
157
|
+
<script defer src="./js/eos-branding.js?v=34"></script>
|
|
158
|
+
<script defer src="./js/eos-security-ui.js?v=34"></script>
|
|
159
|
+
<script defer src="./js/eos-hard-logout.js?v=34"></script>
|
|
160
|
+
<script defer src="./js/eos-assistant.js?v=34"></script>
|
|
160
161
|
</head>
|
|
161
162
|
<body>
|
|
162
163
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
163
164
|
<div id="root"></div>
|
|
164
|
-
|
|
165
|
+
</body>
|
|
165
166
|
</html>
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
// v31: EOS Assist is rendered by eos-branding.js so it can share route/security state.
|
|
4
4
|
// This file stays as a lightweight compatibility hook for cache-safe deployments.
|
|
5
|
-
window.NEXOWATT_EOS_ASSIST_VERSION = '
|
|
5
|
+
window.NEXOWATT_EOS_ASSIST_VERSION = 'v33-assist-position-fix';
|
|
6
6
|
})();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
window.NEXOWATT_EOS_UI_VERSION = '
|
|
4
|
+
window.NEXOWATT_EOS_UI_VERSION = 'v33-assist-position-fix';
|
|
5
5
|
|
|
6
6
|
const BRAND = 'NexoWatt EOS';
|
|
7
7
|
const EOS_MEANING = 'Energy Operation System';
|
|
@@ -510,6 +510,78 @@
|
|
|
510
510
|
window.location.assign(cleanRoot);
|
|
511
511
|
};
|
|
512
512
|
|
|
513
|
+
|
|
514
|
+
let hardLogoutTimer = 0;
|
|
515
|
+
let hardLogoutPollTimer = 0;
|
|
516
|
+
let hardLogoutInstalled = false;
|
|
517
|
+
|
|
518
|
+
const clearAuthStorage = () => safe(() => {
|
|
519
|
+
const storageKeys = [
|
|
520
|
+
'App.refreshToken', 'App.accessToken', 'App.token', 'tokens', 'iobroker.admin.token',
|
|
521
|
+
'access_token', 'refresh_token', 'oidc_id_token', 'oidc_access_token', 'oidc_refresh_token'
|
|
522
|
+
];
|
|
523
|
+
storageKeys.forEach(key => {
|
|
524
|
+
window.localStorage?.removeItem(key);
|
|
525
|
+
window.sessionStorage?.removeItem(key);
|
|
526
|
+
window._localStorage?.removeItem?.(key);
|
|
527
|
+
});
|
|
528
|
+
['access_token', 'refresh_token', 'connect.sid', 'io', 'sid'].forEach(name => {
|
|
529
|
+
document.cookie = `${name}=; Max-Age=0; path=/; SameSite=Lax`;
|
|
530
|
+
document.cookie = `${name}=; Max-Age=0; path=${new URL(ASSET_BASE).pathname || '/'}; SameSite=Lax`;
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const hardLogout = () => {
|
|
535
|
+
clearAuthStorage();
|
|
536
|
+
const loginUrl = new URL('index.html?login', ASSET_BASE);
|
|
537
|
+
loginUrl.searchParams.set('eosLogout', 'timeout');
|
|
538
|
+
window.location.replace(loginUrl.href);
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const scheduleHardLogoutCheck = delay => {
|
|
542
|
+
if (hardLogoutPollTimer) window.clearTimeout(hardLogoutPollTimer);
|
|
543
|
+
hardLogoutPollTimer = window.setTimeout(checkHardLogoutSession, Math.max(1000, delay || 15000));
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
async function checkHardLogoutSession() {
|
|
547
|
+
if (isLoginView() || !document.getElementById('app-paper')) {
|
|
548
|
+
scheduleHardLogoutCheck(10000);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const response = await fetch(new URL('session', ASSET_BASE).href, {
|
|
553
|
+
credentials: 'same-origin',
|
|
554
|
+
cache: 'no-store',
|
|
555
|
+
headers: { accept: 'application/json' },
|
|
556
|
+
});
|
|
557
|
+
const data = await response.json().catch(() => ({}));
|
|
558
|
+
const expireInSec = Number(data?.expireInSec);
|
|
559
|
+
if (Number.isFinite(expireInSec) && expireInSec <= 0) {
|
|
560
|
+
hardLogout();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (Number.isFinite(expireInSec) && expireInSec > 0) {
|
|
564
|
+
if (hardLogoutTimer) window.clearTimeout(hardLogoutTimer);
|
|
565
|
+
hardLogoutTimer = window.setTimeout(hardLogout, Math.max(1200, expireInSec * 1000 + 500));
|
|
566
|
+
scheduleHardLogoutCheck(Math.min(Math.max(5000, expireInSec * 500), 30000));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
} catch (_) {
|
|
570
|
+
// Network reconnects happen during updates/restarts. Do not log out on a transient request failure.
|
|
571
|
+
}
|
|
572
|
+
scheduleHardLogoutCheck(30000);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const installHardLogoutWatchdog = () => {
|
|
576
|
+
if (hardLogoutInstalled) return;
|
|
577
|
+
hardLogoutInstalled = true;
|
|
578
|
+
window.addEventListener('focus', () => checkHardLogoutSession(), { passive: true });
|
|
579
|
+
document.addEventListener('visibilitychange', () => {
|
|
580
|
+
if (!document.hidden) checkHardLogoutSession();
|
|
581
|
+
}, { passive: true });
|
|
582
|
+
scheduleHardLogoutCheck(2500);
|
|
583
|
+
};
|
|
584
|
+
|
|
513
585
|
const ensureBrandBadge = toolbar => {
|
|
514
586
|
if (!toolbar) return;
|
|
515
587
|
document.querySelectorAll('.eos-brand-badge').forEach(existing => {
|
|
@@ -1104,6 +1176,7 @@
|
|
|
1104
1176
|
ensureStandaloneNavToggle();
|
|
1105
1177
|
installAssistDelegatedClick();
|
|
1106
1178
|
ensureEosAssist();
|
|
1179
|
+
installHardLogoutWatchdog();
|
|
1107
1180
|
ensureRightsHelper();
|
|
1108
1181
|
ensurePermissionPresets();
|
|
1109
1182
|
ensureSettingsDialogClasses();
|
|
@@ -1125,6 +1198,7 @@
|
|
|
1125
1198
|
ensureStandaloneNavToggle();
|
|
1126
1199
|
installAssistDelegatedClick();
|
|
1127
1200
|
ensureEosAssist();
|
|
1201
|
+
installHardLogoutWatchdog();
|
|
1128
1202
|
ensureRightsHelper();
|
|
1129
1203
|
ensurePermissionPresets();
|
|
1130
1204
|
ensureSettingsDialogClasses();
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const LOG_PREFIX = '[NexoWatt EOS hard logout]';
|
|
5
|
+
const VERSION = '34';
|
|
6
|
+
const MIN_POLL_MS = 15_000;
|
|
7
|
+
const MAX_TIMER_MS = 2_147_000_000;
|
|
8
|
+
const MIN_TTL_SEC = 5;
|
|
9
|
+
const STORAGE_KEY = `nexowatt:eos:hardLogoutAt:${location.host}:${location.pathname.split('/')[1] || 'root'}`;
|
|
10
|
+
let logoutTimer = null;
|
|
11
|
+
let pollTimer = null;
|
|
12
|
+
let logoutStarted = false;
|
|
13
|
+
|
|
14
|
+
function isLoginPage() {
|
|
15
|
+
const path = String(window.location.pathname || '').toLowerCase();
|
|
16
|
+
const search = String(window.location.search || '').toLowerCase();
|
|
17
|
+
return path.endsWith('/login') || path.includes('/login/') || search.includes('login');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clearStoredTokens() {
|
|
21
|
+
const storages = [window.localStorage, window.sessionStorage, window._localStorage, window._sessionStorage]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.filter((storage, index, array) => array.indexOf(storage) === index);
|
|
24
|
+
const tokenKeyPattern = /(access[_-]?token|refresh[_-]?token|token[_-]?expires|expires[_-]?in|oauth|bearer|auth|connection)/i;
|
|
25
|
+
for (const storage of storages) {
|
|
26
|
+
try {
|
|
27
|
+
const keys = [];
|
|
28
|
+
for (let i = 0; i < storage.length; i++) {
|
|
29
|
+
const key = storage.key(i);
|
|
30
|
+
if (key && (tokenKeyPattern.test(key) || key === STORAGE_KEY)) {
|
|
31
|
+
keys.push(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
keys.forEach(key => storage.removeItem(key));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// ignore blocked storage
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clearAuthCookies() {
|
|
42
|
+
const cookieNames = ['access_token', 'refresh_token', 'connect.sid', 'ioBroker.sid'];
|
|
43
|
+
const paths = ['/', window.location.pathname.replace(/\/[^/]*$/, '/') || '/'];
|
|
44
|
+
for (const name of cookieNames) {
|
|
45
|
+
for (const path of paths) {
|
|
46
|
+
document.cookie = `${name}=; Max-Age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}; SameSite=Lax`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readDeadline() {
|
|
52
|
+
try {
|
|
53
|
+
const value = Number(window.localStorage.getItem(STORAGE_KEY));
|
|
54
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeDeadline(deadline) {
|
|
61
|
+
try {
|
|
62
|
+
window.localStorage.setItem(STORAGE_KEY, String(deadline));
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// ignore blocked storage
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hardLogout(reason) {
|
|
69
|
+
if (logoutStarted || isLoginPage()) return;
|
|
70
|
+
logoutStarted = true;
|
|
71
|
+
try { console.warn(LOG_PREFIX, reason || 'session expired'); } catch (e) {}
|
|
72
|
+
if (logoutTimer) {
|
|
73
|
+
clearTimeout(logoutTimer);
|
|
74
|
+
logoutTimer = null;
|
|
75
|
+
}
|
|
76
|
+
if (pollTimer) {
|
|
77
|
+
clearInterval(pollTimer);
|
|
78
|
+
pollTimer = null;
|
|
79
|
+
}
|
|
80
|
+
clearStoredTokens();
|
|
81
|
+
clearAuthCookies();
|
|
82
|
+
const origin = encodeURIComponent((window.location.pathname || '/') + (window.location.search || '') + (window.location.hash || ''));
|
|
83
|
+
window.location.href = './logout?origin=' + origin + '&hard=1';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function scheduleHardLogout(deadline) {
|
|
87
|
+
const ms = Math.min(Math.max(deadline - Date.now() + 250, 250), MAX_TIMER_MS);
|
|
88
|
+
if (logoutTimer) clearTimeout(logoutTimer);
|
|
89
|
+
logoutTimer = setTimeout(() => hardLogout('configured login timeout reached'), ms);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function applyServerExpiration(expireInSec) {
|
|
93
|
+
const seconds = Number(expireInSec);
|
|
94
|
+
if (!Number.isFinite(seconds)) return;
|
|
95
|
+
if (seconds <= 0) {
|
|
96
|
+
hardLogout('server reported expired session');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (seconds < MIN_TTL_SEC) return;
|
|
100
|
+
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
const candidateDeadline = now + seconds * 1000;
|
|
103
|
+
const storedDeadline = readDeadline();
|
|
104
|
+
|
|
105
|
+
// Set the hard deadline once. If the server reports an earlier expiration later,
|
|
106
|
+
// tighten the deadline. Never extend it through refresh-token based renewal.
|
|
107
|
+
const deadline = !storedDeadline || candidateDeadline < storedDeadline - 5000 ? candidateDeadline : storedDeadline;
|
|
108
|
+
if (deadline !== storedDeadline) writeDeadline(deadline);
|
|
109
|
+
|
|
110
|
+
if (now >= deadline) {
|
|
111
|
+
hardLogout('stored hard deadline reached');
|
|
112
|
+
} else {
|
|
113
|
+
scheduleHardLogout(deadline);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function checkSession() {
|
|
118
|
+
if (logoutStarted) return;
|
|
119
|
+
if (isLoginPage()) {
|
|
120
|
+
clearStoredTokens();
|
|
121
|
+
clearAuthCookies();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const storedDeadline = readDeadline();
|
|
126
|
+
if (storedDeadline && Date.now() >= storedDeadline) {
|
|
127
|
+
hardLogout('stored hard deadline reached');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetch('./session?hard=1&ts=' + Date.now(), {
|
|
133
|
+
credentials: 'include',
|
|
134
|
+
cache: 'no-store',
|
|
135
|
+
headers: { Accept: 'application/json' },
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) return;
|
|
138
|
+
const session = await response.json();
|
|
139
|
+
if (typeof session.expireInSec === 'number') {
|
|
140
|
+
applyServerExpiration(session.expireInSec);
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// During update/restart the endpoint can be unavailable. Do not logout just because of a network error.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function start() {
|
|
148
|
+
checkSession();
|
|
149
|
+
pollTimer = setInterval(checkSession, MIN_POLL_MS);
|
|
150
|
+
window.addEventListener('focus', checkSession, { passive: true });
|
|
151
|
+
document.addEventListener('visibilitychange', () => {
|
|
152
|
+
if (!document.hidden) checkSession();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
window.NEXOWATT_EOS_HARD_LOGOUT = { version: VERSION, checkSession, hardLogout, clearStoredTokens, clearAuthCookies };
|
|
157
|
+
|
|
158
|
+
if (document.readyState === 'loading') {
|
|
159
|
+
document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
160
|
+
} else {
|
|
161
|
+
start();
|
|
162
|
+
}
|
|
163
|
+
})();
|
package/build/lib/web.js
CHANGED
|
@@ -450,12 +450,18 @@ class Web {
|
|
|
450
450
|
if (!token && req.query?.token) token = req.query.token;
|
|
451
451
|
if (!token) return null;
|
|
452
452
|
try {
|
|
453
|
-
|
|
453
|
+
token = decodeURIComponent(token);
|
|
454
454
|
}
|
|
455
455
|
catch {
|
|
456
|
-
|
|
456
|
+
// keep raw token
|
|
457
457
|
}
|
|
458
|
+
token = token.trim();
|
|
459
|
+
if (token.startsWith('Bearer ')) token = token.substring('Bearer '.length).trim();
|
|
460
|
+
if (token.startsWith('s:')) token = token.substring(2);
|
|
461
|
+
if (token.includes('.') && !token.startsWith('eyJ')) token = token.substring(0, token.indexOf('.'));
|
|
462
|
+
return token || null;
|
|
458
463
|
}
|
|
464
|
+
|
|
459
465
|
readSession(id) {
|
|
460
466
|
return new Promise(resolve => this.adapter.getSession(id, token => resolve(token)));
|
|
461
467
|
}
|
|
@@ -576,11 +582,38 @@ class Web {
|
|
|
576
582
|
this.server.app.use(cookieParser());
|
|
577
583
|
this.server.app.use(bodyParser.urlencoded({ extended: true }));
|
|
578
584
|
this.server.app.use(bodyParser.json());
|
|
585
|
+
// NexoWatt EOS: enforce the configured login timeout as a hard logout.
|
|
586
|
+
// We remove refresh tokens from OAuth responses so the admin client cannot silently extend sessions.
|
|
587
|
+
const eosHardLogout = this.adapter.config.nexowattHardLogout !== false;
|
|
588
|
+
if (eosHardLogout) {
|
|
589
|
+
this.server.app.use('/oauth/token', (req, res, next) => {
|
|
590
|
+
const grantType = String(req.body?.grant_type || '');
|
|
591
|
+
if (req.method === 'POST' && grantType === 'refresh_token') {
|
|
592
|
+
res.status(401).json({
|
|
593
|
+
error: 'invalid_grant',
|
|
594
|
+
error_description: 'EOS session expired. Please log in again.',
|
|
595
|
+
eosHardLogout: true,
|
|
596
|
+
});
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const originalJson = res.json.bind(res);
|
|
600
|
+
res.json = body => {
|
|
601
|
+
if (body && typeof body === 'object' && body.access_token && body.expires_in) {
|
|
602
|
+
const expiresIn = Math.max(1, Math.round(Number(body.expires_in) || Number(this.settings.ttl) || 3600));
|
|
603
|
+
body.refresh_token = '';
|
|
604
|
+
body.refresh_token_expires_in = expiresIn;
|
|
605
|
+
body.eosHardLogout = true;
|
|
606
|
+
}
|
|
607
|
+
return originalJson(body);
|
|
608
|
+
};
|
|
609
|
+
next();
|
|
610
|
+
});
|
|
611
|
+
}
|
|
579
612
|
this.oauth2Model = (0, webserver_1.createOAuth2Server)(this.adapter, {
|
|
580
613
|
app: this.server.app,
|
|
581
614
|
secure: this.settings.secure,
|
|
582
615
|
accessLifetime: this.settings.ttl,
|
|
583
|
-
refreshLifetime:
|
|
616
|
+
refreshLifetime: this.settings.ttl,
|
|
584
617
|
noBasicAuth: this.settings.noBasicAuth,
|
|
585
618
|
loginPage: (req) => {
|
|
586
619
|
const isDev = req.url.includes('?dev');
|
|
@@ -598,28 +631,44 @@ class Web {
|
|
|
598
631
|
},
|
|
599
632
|
});
|
|
600
633
|
this.server.app.get('/session', (req, res) => {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
634
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
635
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
636
|
+
if (!this.settings.auth) {
|
|
637
|
+
res.json({ expireInSec: Math.max(120, this.settings.ttl || 3600), hardLogout: true, auth: false });
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const tokenValue = this.readAccessTokenFromRequest(req);
|
|
641
|
+
if (!tokenValue) {
|
|
642
|
+
res.json({ expireInSec: 0, hardLogout: true, error: 'Cannot find session token' });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const candidates = new Set();
|
|
646
|
+
candidates.add(tokenValue);
|
|
647
|
+
candidates.add(tokenValue.startsWith('a:') ? tokenValue : `a:${tokenValue}`);
|
|
648
|
+
if (tokenValue.length > 1) candidates.add(`a:${tokenValue[1]}`);
|
|
649
|
+
const ids = Array.from(candidates);
|
|
650
|
+
const readNext = (index) => {
|
|
651
|
+
const id = ids[index];
|
|
652
|
+
if (!id) {
|
|
653
|
+
res.json({ expireInSec: 0, hardLogout: true, error: 'Cannot find session' });
|
|
619
654
|
return;
|
|
620
655
|
}
|
|
621
|
-
|
|
622
|
-
|
|
656
|
+
void this.adapter.getSession(id, (token) => {
|
|
657
|
+
if (!token?.user) {
|
|
658
|
+
readNext(index + 1);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const now = Date.now();
|
|
662
|
+
const expirations = [Number(token.aExp), Number(token.rExp), Number(token.expire), Number(token.expires), Number(token.expiresAt)].filter(value => Number.isFinite(value) && value > 0);
|
|
663
|
+
const expiresAt = expirations.length ? Math.min(...expirations) : now + (this.settings.ttl || 3600) * 1000;
|
|
664
|
+
res.json({
|
|
665
|
+
expireInSec: Math.max(0, Math.floor((expiresAt - now) / 1000)),
|
|
666
|
+
hardLogout: true,
|
|
667
|
+
user: token.user,
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
};
|
|
671
|
+
readNext(0);
|
|
623
672
|
});
|
|
624
673
|
this.server.app.get(/.*\/nexowatt\/security\/(?:context|session)$/, (req, res) => {
|
|
625
674
|
void this.sendEosSecuritySession(req, res).catch(e => {
|
|
@@ -648,6 +697,9 @@ class Web {
|
|
|
648
697
|
origin = origin.substring(0, pos);
|
|
649
698
|
}
|
|
650
699
|
}
|
|
700
|
+
for (const cookieName of ['access_token', 'refresh_token', 'connect.sid', 'io', 'ioBroker.sid', 'eos-admin.sid']) {
|
|
701
|
+
res.clearCookie(cookieName, { path: '/' });
|
|
702
|
+
}
|
|
651
703
|
if (isDev) {
|
|
652
704
|
res.redirect('http://127.0.0.1:3000/index.html?login');
|
|
653
705
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# NexoWatt EOS Admin v33 – EOS Assist Position
|
|
2
|
+
|
|
3
|
+
Diese Version verschiebt den EOS Assist Launcher weiter nach links, damit der Bereich unten rechts für den Seiten-Einstellbutton, Floating-Action-Buttons und Slider frei bleibt.
|
|
4
|
+
|
|
5
|
+
## Änderungen
|
|
6
|
+
|
|
7
|
+
- Version auf `7.9.33` erhöht.
|
|
8
|
+
- EOS Assist bleibt unten rechts sichtbar, sitzt aber mit Sicherheitsabstand zum rechten Rand.
|
|
9
|
+
- Der native Seiten-Einstellbutton und andere Floating-Action-Buttons erhalten eine höhere Bedienpriorität.
|
|
10
|
+
- Mobile Ansicht berücksichtigt kleinere Abstände.
|
|
11
|
+
- Keine Backend-, MCP- oder Server-Abhängigkeiten geändert.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# NexoWatt EOS Admin v34
|
|
2
|
+
|
|
3
|
+
## Zweck
|
|
4
|
+
|
|
5
|
+
Version 7.9.34 korrigiert zwei produktive Punkte:
|
|
6
|
+
|
|
7
|
+
1. Repository-/Update-Metadaten zeigen vollständig auf die aktuelle npm-Version.
|
|
8
|
+
2. Die konfigurierte Abmeldezeit wird als harte Sitzungslaufzeit erzwungen.
|
|
9
|
+
|
|
10
|
+
## Update-Fix
|
|
11
|
+
|
|
12
|
+
In vorherigen Paketen konnten `common.meta`, `common.extIcon` und `common.readme` noch auf ältere unpkg-Versionen zeigen. Das konnte dazu führen, dass das Repository zwar eine neue Version anzeigt, der Admin beim Aktualisieren aber alte Metadaten nachlädt.
|
|
13
|
+
|
|
14
|
+
v34 setzt diese Felder synchron auf `7.9.34`.
|
|
15
|
+
|
|
16
|
+
## Harte Abmeldung
|
|
17
|
+
|
|
18
|
+
Der Upstream-Admin-Client verlängert OAuth-Tokens normalerweise automatisch. Für EOS wird das deaktiviert:
|
|
19
|
+
|
|
20
|
+
- erfolgreiche OAuth-Responses enthalten keinen nutzbaren Refresh-Token mehr,
|
|
21
|
+
- alte Refresh-Token-Versuche werden mit `401 invalid_grant` beantwortet,
|
|
22
|
+
- der Client muss nach Ablauf der eingestellten Abmeldezeit neu anmelden.
|
|
23
|
+
|
package/io-package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "eos-admin",
|
|
4
|
-
"version": "7.9.
|
|
4
|
+
"version": "7.9.34",
|
|
5
5
|
"titleLang": {
|
|
6
6
|
"en": "NexoWatt EOS Admin",
|
|
7
7
|
"de": "NexoWatt EOS Admin",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"connectionType": "local",
|
|
19
19
|
"dataSource": "push",
|
|
20
20
|
"news": {
|
|
21
|
+
"7.9.34": {
|
|
22
|
+
"en": "Fixed repository metadata for reliable EOS Admin updates and enforced hard logout after the configured login timeout.",
|
|
23
|
+
"de": "Repository-Metadaten für zuverlässige EOS-Admin-Updates korrigiert und harte Abmeldung nach der eingestellten Abmeldezeit erzwungen."
|
|
24
|
+
},
|
|
21
25
|
"7.9.31": {
|
|
22
26
|
"en": "UI cleanup: fixes EOS Security German text encoding, removes the logout tile next to the compact navigation arrow, and expands EOS Assist with AI-ready multi-path setup guidance.",
|
|
23
27
|
"de": "UI-Feinschliff: korrigiert deutsche EOS-Sicherheitsbeschriftungen, entfernt die Abmelden-Kachel neben dem Kompaktpfeil und erweitert EOS Assist um KI-bereite Mehrwege-Einrichtungshilfe."
|
|
@@ -161,10 +165,6 @@
|
|
|
161
165
|
"pl": "Rozszerzony plik konfiguracyjny JSON do adaptera fregata",
|
|
162
166
|
"uk": "Розширений налаштування JSON для адаптера фригату",
|
|
163
167
|
"zh-cn": "JSON 护卫舰适配器扩展配置"
|
|
164
|
-
},
|
|
165
|
-
"7.9.32": {
|
|
166
|
-
"en": "Fixed the EOS navigation compact toggle: the decorative left tile was removed while the arrow button remains visible and usable.",
|
|
167
|
-
"de": "EOS Navigationsumschalter korrigiert: Die dekorative linke Kachel wurde entfernt, der Pfeilbutton bleibt sichtbar und nutzbar."
|
|
168
168
|
}
|
|
169
169
|
},
|
|
170
170
|
"desc": {
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
"icon": "admin.svg",
|
|
203
203
|
"messagebox": true,
|
|
204
204
|
"enabled": true,
|
|
205
|
-
"extIcon": "https://unpkg.com/iobroker.eos-admin@7.9.
|
|
205
|
+
"extIcon": "https://unpkg.com/iobroker.eos-admin@7.9.34/admin/admin.svg",
|
|
206
206
|
"keywords": [
|
|
207
207
|
"NexoWatt",
|
|
208
208
|
"EOS",
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
"licensed"
|
|
214
214
|
],
|
|
215
215
|
"compact": true,
|
|
216
|
-
"readme": "https://unpkg.com/iobroker.eos-admin@7.9.
|
|
216
|
+
"readme": "https://unpkg.com/iobroker.eos-admin@7.9.34/README.md",
|
|
217
217
|
"authors": [
|
|
218
218
|
"bluefox <bluefox@ccu.io>",
|
|
219
219
|
"hobbyquaker <hq@ccu.io>"
|
|
@@ -282,7 +282,7 @@
|
|
|
282
282
|
"nondeletable": false,
|
|
283
283
|
"allowAdapterUpdate": true,
|
|
284
284
|
"allowAdapterDelete": false,
|
|
285
|
-
"meta": "https://unpkg.com/iobroker.eos-admin@7.9.
|
|
285
|
+
"meta": "https://unpkg.com/iobroker.eos-admin@7.9.34/io-package.json",
|
|
286
286
|
"npmPackage": "iobroker.eos-admin"
|
|
287
287
|
},
|
|
288
288
|
"native": {
|
package/package.json
CHANGED
|
@@ -22,6 +22,15 @@ if (io.common.stopBeforeUpdate !== false) fail('io-package common.stopBeforeUpda
|
|
|
22
22
|
if (io.common.dontDelete === true) fail('io-package common.dontDelete must not be true because it blocks clean updates');
|
|
23
23
|
if (io.common.nondeletable === true) fail('io-package common.nondeletable must not be true because it blocks updates');
|
|
24
24
|
|
|
25
|
+
const expectedBase = `https://unpkg.com/iobroker.eos-admin@${pkg.version}`;
|
|
26
|
+
for (const [field, expected] of Object.entries({
|
|
27
|
+
extIcon: `${expectedBase}/admin/admin.svg`,
|
|
28
|
+
readme: `${expectedBase}/README.md`,
|
|
29
|
+
meta: `${expectedBase}/io-package.json`,
|
|
30
|
+
})) {
|
|
31
|
+
if (io.common[field] !== expected) fail(`io-package common.${field} must be ${expected}, got ${io.common[field]}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
for (const file of [
|
|
26
35
|
'adminWww/index.html',
|
|
27
36
|
'adminWww/js/eos-branding.js',
|
|
@@ -46,4 +55,9 @@ const bootstrap = bootstrapFiles.map(f => fs.readFileSync(path.join(root, 'admin
|
|
|
46
55
|
if (!bootstrap.includes('window.adapterName="eos-admin"')) fail('frontend bootstrap does not set window.adapterName="eos-admin"');
|
|
47
56
|
if (bootstrap.includes('window.adapterName="admin"')) fail('frontend bootstrap still contains window.adapterName="admin"');
|
|
48
57
|
|
|
58
|
+
const webBuild = fs.readFileSync(path.join(root, 'build/lib/web.js'), 'utf8');
|
|
59
|
+
if (!webBuild.includes("body.refresh_token = '';")) fail('build/lib/web.js does not remove OAuth refresh tokens for hard logout');
|
|
60
|
+
if (!webBuild.includes("grantType === 'refresh_token'")) fail('build/lib/web.js does not reject refresh_token grants');
|
|
61
|
+
if (!webBuild.includes('refreshLifetime: this.settings.ttl')) fail('build/lib/web.js does not bind refresh lifetime to ttl');
|
|
62
|
+
|
|
49
63
|
console.log('[NexoWatt EOS package validation] OK');
|