iobroker.eos-admin 7.9.35 → 7.9.37
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/admin/i18n/de.json +7 -3
- package/admin/i18n/en.json +5 -3
- package/admin/i18n/es.json +5 -3
- package/admin/i18n/fr.json +5 -3
- package/admin/i18n/it.json +5 -3
- package/admin/i18n/nl.json +5 -3
- package/admin/i18n/pl.json +5 -3
- package/admin/i18n/pt.json +5 -3
- package/admin/i18n/ru.json +5 -3
- package/admin/i18n/uk.json +5 -3
- package/admin/i18n/zh-cn.json +5 -3
- package/admin/jsonConfig.json5 +8 -7
- package/adminWww/assets/bootstrap-COulQZax.js +1 -1
- package/adminWww/css/eos-branding.css +150 -0
- package/adminWww/index.html +4 -5
- package/adminWww/js/eos-branding.js +216 -18
- package/adminWww/js/eos-hard-logout.js +15 -162
- package/adminWww/js/eos-runtime-fixes.js +144 -0
- package/adminWww/js/eos-security-ui.js +40 -7
- package/build/i18n/de.json +2 -2
- package/build/i18n/en.json +2 -2
- package/build/lib/web.js +48 -64
- package/build/main.js +13 -3
- package/docs/NEXOWATT_EOS_UI_V37_STABILITY_DE.md +13 -0
- package/io-package.json +13 -12
- package/package.json +2 -2
- package/public/404.html +12 -0
- package/tools/nexowatt-validate-package.cjs +9 -3
|
@@ -1,165 +1,18 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 logoutUrl = new URL('logout', window.location.origin + '/');
|
|
83
|
-
logoutUrl.searchParams.set('hard', '1');
|
|
84
|
-
logoutUrl.searchParams.set('ts', String(Date.now()));
|
|
85
|
-
window.location.replace(logoutUrl.href);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function scheduleHardLogout(deadline) {
|
|
89
|
-
const ms = Math.min(Math.max(deadline - Date.now() + 250, 250), MAX_TIMER_MS);
|
|
90
|
-
if (logoutTimer) clearTimeout(logoutTimer);
|
|
91
|
-
logoutTimer = setTimeout(() => hardLogout('configured login timeout reached'), ms);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function applyServerExpiration(expireInSec) {
|
|
95
|
-
const seconds = Number(expireInSec);
|
|
96
|
-
if (!Number.isFinite(seconds)) return;
|
|
97
|
-
if (seconds <= 0) {
|
|
98
|
-
hardLogout('server reported expired session');
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (seconds < MIN_TTL_SEC) return;
|
|
102
|
-
|
|
103
|
-
const now = Date.now();
|
|
104
|
-
const candidateDeadline = now + seconds * 1000;
|
|
105
|
-
const storedDeadline = readDeadline();
|
|
106
|
-
|
|
107
|
-
// Set the hard deadline once. If the server reports an earlier expiration later,
|
|
108
|
-
// tighten the deadline. Never extend it through refresh-token based renewal.
|
|
109
|
-
const deadline = !storedDeadline || candidateDeadline < storedDeadline - 5000 ? candidateDeadline : storedDeadline;
|
|
110
|
-
if (deadline !== storedDeadline) writeDeadline(deadline);
|
|
111
|
-
|
|
112
|
-
if (now >= deadline) {
|
|
113
|
-
hardLogout('stored hard deadline reached');
|
|
114
|
-
} else {
|
|
115
|
-
scheduleHardLogout(deadline);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function checkSession() {
|
|
120
|
-
if (logoutStarted) return;
|
|
121
|
-
if (isLoginPage()) {
|
|
122
|
-
clearStoredTokens();
|
|
123
|
-
clearAuthCookies();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const storedDeadline = readDeadline();
|
|
128
|
-
if (storedDeadline && Date.now() >= storedDeadline) {
|
|
129
|
-
hardLogout('stored hard deadline reached');
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
const response = await fetch('./session?hard=1&ts=' + Date.now(), {
|
|
135
|
-
credentials: 'include',
|
|
136
|
-
cache: 'no-store',
|
|
137
|
-
headers: { Accept: 'application/json' },
|
|
138
|
-
});
|
|
139
|
-
if (!response.ok) return;
|
|
140
|
-
const session = await response.json();
|
|
141
|
-
if (typeof session.expireInSec === 'number') {
|
|
142
|
-
applyServerExpiration(session.expireInSec);
|
|
143
|
-
}
|
|
144
|
-
} catch (e) {
|
|
145
|
-
// During update/restart the endpoint can be unavailable. Do not logout just because of a network error.
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function start() {
|
|
150
|
-
checkSession();
|
|
151
|
-
pollTimer = setInterval(checkSession, MIN_POLL_MS);
|
|
152
|
-
window.addEventListener('focus', checkSession, { passive: true });
|
|
153
|
-
document.addEventListener('visibilitychange', () => {
|
|
154
|
-
if (!document.hidden) checkSession();
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
window.NEXOWATT_EOS_HARD_LOGOUT = { version: VERSION, checkSession, hardLogout, clearStoredTokens, clearAuthCookies };
|
|
159
|
-
|
|
160
|
-
if (document.readyState === 'loading') {
|
|
161
|
-
document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
162
|
-
} else {
|
|
163
|
-
start();
|
|
164
|
-
}
|
|
3
|
+
// NexoWatt EOS v36: compatibility stub.
|
|
4
|
+
// The custom hard-logout timer was removed because it could expire sessions
|
|
5
|
+
// earlier than the configured admin TTL and broke native adapter dialogs.
|
|
6
|
+
// Session handling is now delegated to the same OAuth/session flow as the
|
|
7
|
+
// upstream ioBroker Admin.
|
|
8
|
+
const VERSION = '36';
|
|
9
|
+
function noop() {}
|
|
10
|
+
window.NEXOWATT_EOS_HARD_LOGOUT = {
|
|
11
|
+
version: VERSION,
|
|
12
|
+
checkSession: noop,
|
|
13
|
+
hardLogout: noop,
|
|
14
|
+
clearStoredTokens: noop,
|
|
15
|
+
clearAuthCookies: noop,
|
|
16
|
+
disabled: true,
|
|
17
|
+
};
|
|
165
18
|
})();
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
window.NEXOWATT_EOS_RUNTIME_FIXES_VERSION = 'v37-notification-backitup-security-text-fix';
|
|
5
|
+
|
|
6
|
+
const MOJIBAKE_MAP = new Map(Object.entries({
|
|
7
|
+
'dürfen': 'dürfen', 'Dürfen': 'Dürfen',
|
|
8
|
+
'für': 'für', 'Für': 'Für',
|
|
9
|
+
'müssen': 'müssen', 'Müssen': 'Müssen',
|
|
10
|
+
'können': 'können', 'Können': 'Können',
|
|
11
|
+
'möglich': 'möglich', 'Möglich': 'Möglich',
|
|
12
|
+
'Löschen': 'Löschen', 'löschen': 'löschen',
|
|
13
|
+
'schützen': 'schützen', 'Schützen': 'Schützen',
|
|
14
|
+
'Schützt': 'Schützt', 'schützt': 'schützt',
|
|
15
|
+
'Geschützte': 'Geschützte', 'geschützte': 'geschützte',
|
|
16
|
+
'Geschützter': 'Geschützter', 'geschützter': 'geschützter',
|
|
17
|
+
'geschützten': 'geschützten', 'Geschützten': 'Geschützten',
|
|
18
|
+
'ausgewählte': 'ausgewählte', 'Ausgewählte': 'Ausgewählte',
|
|
19
|
+
'ändern': 'ändern', 'Ändern': 'Ändern',
|
|
20
|
+
'über': 'über', 'Über': 'Über',
|
|
21
|
+
'Wähle': 'Wähle', 'wähle': 'wähle',
|
|
22
|
+
'öffnen': 'öffnen', 'Öffnen': 'Öffnen',
|
|
23
|
+
'schließen': 'schließen', 'Schließen': 'Schließen',
|
|
24
|
+
'Gerät': 'Gerät', 'gerät': 'gerät',
|
|
25
|
+
'Geräte': 'Geräte', 'geräte': 'geräte',
|
|
26
|
+
'Zugänge': 'Zugänge', 'zugänge': 'zugänge',
|
|
27
|
+
'Sicherheitsgründen': 'Sicherheitsgründen',
|
|
28
|
+
'Kompatibilitätsgründen': 'Kompatibilitätsgründen',
|
|
29
|
+
'Benachrichtigungen': 'Benachrichtigungen',
|
|
30
|
+
'ß': 'ß', 'Ä': 'Ä', 'Ö': 'Ö', 'Ü': 'Ü', 'ä': 'ä', 'ö': 'ö', 'ü': 'ü',
|
|
31
|
+
'–': '–', '—': '—', '„': '„', '“': '“', 'â€': '”', ' ': ' ', 'Â': ''
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const safe = fn => { try { return fn(); } catch { return undefined; } };
|
|
35
|
+
|
|
36
|
+
const repairText = value => {
|
|
37
|
+
let text = String(value || '');
|
|
38
|
+
if (!/[ÃÂâ]/.test(text)) return text;
|
|
39
|
+
for (const [from, to] of MOJIBAKE_MAP) {
|
|
40
|
+
if (text.includes(from)) text = text.split(from).join(to);
|
|
41
|
+
}
|
|
42
|
+
return text;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const skip = el => {
|
|
46
|
+
const tag = el?.tagName;
|
|
47
|
+
return tag === 'SCRIPT' || tag === 'STYLE' || tag === 'TEXTAREA' || tag === 'CODE' || tag === 'PRE';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const repairSecurityText = root => safe(() => {
|
|
51
|
+
const base = root && root.nodeType ? root : document.body || document.documentElement;
|
|
52
|
+
if (!base) return;
|
|
53
|
+
const walker = document.createTreeWalker(base, NodeFilter.SHOW_TEXT, {
|
|
54
|
+
acceptNode(node) {
|
|
55
|
+
return skip(node.parentElement) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
let node;
|
|
59
|
+
while ((node = walker.nextNode())) {
|
|
60
|
+
const before = node.nodeValue || '';
|
|
61
|
+
const after = repairText(before);
|
|
62
|
+
if (after !== before) node.nodeValue = after;
|
|
63
|
+
}
|
|
64
|
+
const attrSelector = '[title],[aria-label],[placeholder],[value]';
|
|
65
|
+
const elements = base.nodeType === Node.ELEMENT_NODE && base.matches?.(attrSelector)
|
|
66
|
+
? [base]
|
|
67
|
+
: Array.from(base.querySelectorAll?.(attrSelector) || []);
|
|
68
|
+
elements.forEach(el => {
|
|
69
|
+
if (skip(el)) return;
|
|
70
|
+
['title', 'aria-label', 'placeholder', 'value'].forEach(attr => {
|
|
71
|
+
if (!el.hasAttribute?.(attr)) return;
|
|
72
|
+
const before = el.getAttribute(attr) || '';
|
|
73
|
+
const after = repairText(before);
|
|
74
|
+
if (after !== before) el.setAttribute(attr, after);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const isNotificationSurface = el => !!el?.closest?.([
|
|
80
|
+
'.MuiSnackbar-root',
|
|
81
|
+
'.MuiAlert-root',
|
|
82
|
+
'[role="alert"]',
|
|
83
|
+
'[role="status"]',
|
|
84
|
+
'.Toastify__toast',
|
|
85
|
+
'.eos-notification-safe'
|
|
86
|
+
].join(','));
|
|
87
|
+
|
|
88
|
+
const isCloseControl = el => {
|
|
89
|
+
const label = `${el?.textContent || ''} ${el?.getAttribute?.('aria-label') || ''} ${el?.getAttribute?.('title') || ''}`.toLowerCase();
|
|
90
|
+
return /close|schlie(?:ß|ss)en|dismiss|ausblenden|ok|verstanden|x$/.test(label)
|
|
91
|
+
|| !!el?.querySelector?.('svg[data-testid*="Close"], svg[data-testid*="Clear"], .material-icons');
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const restoreCloseButton = button => safe(() => {
|
|
95
|
+
if (!button || !isCloseControl(button)) return;
|
|
96
|
+
button.classList.remove('eos-protected-delete-control', 'eos-security-hidden-delete', 'eos-hidden-logout', 'eos-native-logout-hidden');
|
|
97
|
+
button.removeAttribute('aria-disabled');
|
|
98
|
+
button.removeAttribute('data-eos-security-blocked');
|
|
99
|
+
if ('disabled' in button && button.disabled && !button.dataset.eosOriginalDisabled) button.disabled = false;
|
|
100
|
+
button.style.pointerEvents = 'auto';
|
|
101
|
+
button.style.visibility = 'visible';
|
|
102
|
+
button.style.opacity = '1';
|
|
103
|
+
if (button.style.display === 'none') button.style.display = '';
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const repairNotifications = root => safe(() => {
|
|
107
|
+
const base = root && root.nodeType ? root : document;
|
|
108
|
+
const surfaces = Array.from(base.querySelectorAll?.([
|
|
109
|
+
'.MuiSnackbar-root',
|
|
110
|
+
'.MuiAlert-root',
|
|
111
|
+
'[role="alert"]',
|
|
112
|
+
'[role="status"]',
|
|
113
|
+
'.Toastify__toast'
|
|
114
|
+
].join(',')) || []);
|
|
115
|
+
if (base.nodeType === Node.ELEMENT_NODE && isNotificationSurface(base)) surfaces.push(base);
|
|
116
|
+
surfaces.forEach(surface => {
|
|
117
|
+
surface.classList.add('eos-notification-safe');
|
|
118
|
+
surface.style.pointerEvents = 'auto';
|
|
119
|
+
surface.querySelectorAll('button,[role="button"],a,.MuiIconButton-root').forEach(restoreCloseButton);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const run = root => {
|
|
124
|
+
repairSecurityText(root);
|
|
125
|
+
repairNotifications(root);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const install = () => {
|
|
129
|
+
run(document.body || document.documentElement);
|
|
130
|
+
const observer = new MutationObserver(mutations => {
|
|
131
|
+
const roots = new Set();
|
|
132
|
+
for (const mutation of mutations) {
|
|
133
|
+
if (mutation.type === 'characterData') roots.add(mutation.target.parentElement || document.body);
|
|
134
|
+
mutation.addedNodes?.forEach(node => roots.add(node));
|
|
135
|
+
}
|
|
136
|
+
roots.forEach(root => run(root));
|
|
137
|
+
});
|
|
138
|
+
observer.observe(document.documentElement, { subtree: true, childList: true, characterData: true });
|
|
139
|
+
[250, 1000, 3000, 8000].forEach(ms => window.setTimeout(() => run(document.body || document.documentElement), ms));
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', install, { once: true });
|
|
143
|
+
else install();
|
|
144
|
+
})();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const VERSION = '
|
|
4
|
+
const VERSION = 'v37-security-text-polish';
|
|
5
5
|
const LEGACY_ADMIN = 'admin';
|
|
6
6
|
const LEGACY_ADMIN_INSTANCE = 'admin.0';
|
|
7
7
|
const ASSET_BASE = (() => {
|
|
@@ -34,14 +34,24 @@
|
|
|
34
34
|
let text = String(value || '');
|
|
35
35
|
const map = new Map(Object.entries({
|
|
36
36
|
'dürfen': 'dürfen', 'Dürfen': 'Dürfen', 'für': 'für', 'Für': 'Für',
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
37
|
+
'müssen': 'müssen', 'Müssen': 'Müssen', 'können': 'können', 'Können': 'Können',
|
|
38
|
+
'möglich': 'möglich', 'Möglich': 'Möglich', 'Löschen': 'Löschen', 'löschen': 'löschen',
|
|
39
|
+
'schützen': 'schützen', 'Schützen': 'Schützen', 'Schützt': 'Schützt', 'schützt': 'schützt',
|
|
40
|
+
'Geschützte': 'Geschützte', 'geschützte': 'geschützte', 'Geschützter': 'Geschützter',
|
|
40
41
|
'ausgewählte': 'ausgewählte', 'Ausgewählte': 'Ausgewählte', 'ändern': 'ändern', 'Ändern': 'Ändern',
|
|
41
42
|
'über': 'über', 'Über': 'Über', 'Wähle': 'Wähle', 'wähle': 'wähle',
|
|
42
|
-
'
|
|
43
|
+
'öffnen': 'öffnen', 'Öffnen': 'Öffnen', 'schließen': 'schließen', 'Schließen': 'Schließen',
|
|
44
|
+
'Gerät': 'Gerät', 'Geräte': 'Geräte', 'Geräteliste': 'Geräteliste',
|
|
45
|
+
'ß': 'ß', 'Ä': 'Ä', 'Ö': 'Ö', 'Ü': 'Ü', 'ä': 'ä', 'ö': 'ö', 'ü': 'ü', 'Â': ''
|
|
43
46
|
}));
|
|
44
47
|
for (const [from, to] of map) if (text.includes(from)) text = text.split(from).join(to);
|
|
48
|
+
// Generic Latin1-as-UTF8 repair for labels injected by older bundles. Guard against false positives.
|
|
49
|
+
if (/[ÃÂ]/.test(text)) {
|
|
50
|
+
try {
|
|
51
|
+
const repaired = decodeURIComponent(escape(text));
|
|
52
|
+
if (/[äöüÄÖÜß]/.test(repaired) && !/[ÃÂ]/.test(repaired)) text = repaired;
|
|
53
|
+
} catch { /* keep mapped text */ }
|
|
54
|
+
}
|
|
45
55
|
return text;
|
|
46
56
|
};
|
|
47
57
|
const normalizeFlat = value => normalize(value).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
@@ -220,16 +230,39 @@
|
|
|
220
230
|
};
|
|
221
231
|
|
|
222
232
|
const applyPolicyToDom = () => {
|
|
233
|
+
// Text repair is safe and must also run on adapter config pages.
|
|
234
|
+
replaceTextNodes();
|
|
223
235
|
const admin = isAdminUser();
|
|
224
236
|
document.documentElement.classList.toggle('eos-security-admin-user', admin);
|
|
225
237
|
document.documentElement.classList.toggle('eos-security-non-admin-user', !admin);
|
|
226
238
|
document.documentElement.classList.toggle('eos-security-nonadmin', !admin);
|
|
227
|
-
|
|
239
|
+
releaseNotificationControls();
|
|
240
|
+
if (isAdapterConfigSurface()) return;
|
|
228
241
|
hideLegacyAdminPanels();
|
|
229
242
|
hideProtectedDeleteControls();
|
|
230
243
|
hideEosSecuritySettingsForNonAdmins();
|
|
231
244
|
};
|
|
232
245
|
|
|
246
|
+
const isAdapterConfigSurface = () => document.documentElement.classList.contains('eos-adapter-config-surface') || /Instanzeinstellungen:|Instance settings:|Geräteliste|Gerät hinzufügen|Gerät bearbeiten/i.test(document.body?.textContent || '');
|
|
247
|
+
|
|
248
|
+
const releaseNotificationControls = () => {
|
|
249
|
+
// Never block notification/toast close actions. These controls are owned by
|
|
250
|
+
// the native Admin UI and must remain clickable regardless of EOS security rules.
|
|
251
|
+
document.querySelectorAll('.MuiSnackbar-root, .MuiAlert-root, .MuiSnackbarContent-root, [role="alert"], .Toastify__toast, .notistack-Snackbar').forEach(box => {
|
|
252
|
+
box.classList.add('eos-notification-safe');
|
|
253
|
+
box.style.pointerEvents = 'auto';
|
|
254
|
+
box.querySelectorAll('button, [role="button"], a, .MuiIconButton-root').forEach(control => {
|
|
255
|
+
control.classList.remove('eos-protected-delete-control', 'eos-security-hidden-delete');
|
|
256
|
+
control.removeAttribute('disabled');
|
|
257
|
+
control.removeAttribute('aria-disabled');
|
|
258
|
+
if ('disabled' in control) control.disabled = false;
|
|
259
|
+
control.style.pointerEvents = 'auto';
|
|
260
|
+
control.style.display = '';
|
|
261
|
+
control.style.visibility = '';
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
|
|
233
266
|
const scheduleApply = () => {
|
|
234
267
|
if (state.scheduled) return;
|
|
235
268
|
state.scheduled = true;
|
|
@@ -267,7 +300,7 @@
|
|
|
267
300
|
|
|
268
301
|
document.addEventListener('click', event => {
|
|
269
302
|
const target = event.target?.closest?.('button,[role="button"],a,[role="menuitem"],.MuiMenuItem-root');
|
|
270
|
-
if (!target || isAdminUser()) return;
|
|
303
|
+
if (!target || isAdminUser() || isAdapterConfigSurface()) return;
|
|
271
304
|
const label = normalizeFlat(`${target.textContent || ''} ${target.getAttribute?.('title') || ''} ${target.getAttribute?.('aria-label') || ''}`);
|
|
272
305
|
if (/loschen|delete|remove|deinstall|uninstall/.test(label)) {
|
|
273
306
|
target.classList.add('eos-security-hidden-delete');
|
package/build/i18n/de.json
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"Password is too short (min 6 chars)": "Das Passwort ist zu kurz (mindestens 6 Zeichen)",
|
|
26
26
|
"Password repeat": "Passwort wiederholen",
|
|
27
27
|
"Password successfully changed for \"%s\"": "Passwort für „%s“ erfolgreich geändert",
|
|
28
|
-
"Schützt ausgewählte Adapter
|
|
28
|
+
"Schützt ausgewählte Adapter in der EOS Oberfläche vor kritischen Aktionen durch Installateur- und Endkundenrollen. Adaptereigene Funktionen und Updates bleiben möglich.": "Schützt ausgewählte Adapter für Installateur- und Endkundenbenutzer über Administratorrechte und EOS-Regeln. dontDelete und nondeletable bleiben false, damit Updates weiter funktionieren.",
|
|
29
29
|
"Set password": "Passwort festlegen",
|
|
30
|
-
"
|
|
30
|
+
"Blendet kritische Aktionen für Installateur- und Endkundenrollen aus. Harte ACL-Sperren werden nicht auf Runtime-Adapter gesetzt, damit BackItUp und adaptereigene Konfigurationen stabil bleiben.": "Setzt Administratorrechte auf die geschützte Adapterliste. Installateur- und Endkundenbenutzer können diese Adapter nicht stoppen, aktivieren oder löschen. Administratoren können sie weiterhin updaten und warten.",
|
|
31
31
|
"Sperr-Adresse des alten Admins": "Sperr-Adresse des alten Admins",
|
|
32
32
|
"Sperr-Port des alten Admins": "Sperr-Port des alten Admins",
|
|
33
33
|
"The password for user \"%s\" was successfully changed": "Das Passwort für den Benutzer „%s“ wurde erfolgreich geändert",
|
package/build/i18n/en.json
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"Password is too short (min 6 chars)": "Password is too short (min 6 chars)",
|
|
26
26
|
"Password repeat": "Password repeat",
|
|
27
27
|
"Password successfully changed for \"%s\"": "Password successfully changed for \"%s\"",
|
|
28
|
-
"Schützt ausgewählte Adapter
|
|
28
|
+
"Schützt ausgewählte Adapter in der EOS Oberfläche vor kritischen Aktionen durch Installateur- und Endkundenrollen. Adaptereigene Funktionen und Updates bleiben möglich.": "Protects selected adapters from installer/end-customer users via administrator ACLs and EOS UI rules. dontDelete and nondeletable remain false so updates keep working.",
|
|
29
29
|
"Set password": "Set password",
|
|
30
|
-
"
|
|
30
|
+
"Blendet kritische Aktionen für Installateur- und Endkundenrollen aus. Harte ACL-Sperren werden nicht auf Runtime-Adapter gesetzt, damit BackItUp und adaptereigene Konfigurationen stabil bleiben.": "Applies administrator ACLs to the protected adapter list. Installer/end-customer users cannot stop, enable or delete these adapters; administrators can still update and maintain them.",
|
|
31
31
|
"Sperr-Adresse des alten Admins": "Legacy admin lock bind address",
|
|
32
32
|
"Sperr-Port des alten Admins": "Legacy admin lock port",
|
|
33
33
|
"The password for user \"%s\" was successfully changed": "The password for user \"%s\" was successfully changed",
|
package/build/lib/web.js
CHANGED
|
@@ -583,6 +583,16 @@ class Web {
|
|
|
583
583
|
this.server.app.get('/version', (_req, res) => {
|
|
584
584
|
res.status(200).send(this.adapter.version);
|
|
585
585
|
});
|
|
586
|
+
// v36: Repair stale/broken URLs generated by older EOS hard-logout builds.
|
|
587
|
+
this.server.app.use((req, res, next) => {
|
|
588
|
+
const requested = String(req.originalUrl || req.url || '');
|
|
589
|
+
if (/(?:%2f|%252f)(?:%23|%2523)|\/login\/|\/logout\/|hard=1/i.test(requested)
|
|
590
|
+
&& /index\.html|login|hard=1|origin=|%23|%2523/i.test(requested)) {
|
|
591
|
+
res.redirect(this.LOGIN_PAGE);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
next();
|
|
595
|
+
});
|
|
586
596
|
// replace socket.io
|
|
587
597
|
this.server.app.use((req, res, next) => {
|
|
588
598
|
const url = req.url.split('?')[0];
|
|
@@ -633,38 +643,12 @@ class Web {
|
|
|
633
643
|
this.server.app.use(cookieParser());
|
|
634
644
|
this.server.app.use(bodyParser.urlencoded({ extended: true }));
|
|
635
645
|
this.server.app.use(bodyParser.json());
|
|
636
|
-
//
|
|
637
|
-
// We remove refresh tokens from OAuth responses so the admin client cannot silently extend sessions.
|
|
638
|
-
const eosHardLogout = false; // v35: keep OAuth refresh flow alive; hard timeout is enforced client-side by an absolute deadline.
|
|
639
|
-
if (eosHardLogout) {
|
|
640
|
-
this.server.app.use('/oauth/token', (req, res, next) => {
|
|
641
|
-
const grantType = String(req.body?.grant_type || '');
|
|
642
|
-
if (req.method === 'POST' && grantType === 'refresh_token') {
|
|
643
|
-
res.status(401).json({
|
|
644
|
-
error: 'invalid_grant',
|
|
645
|
-
error_description: 'EOS session expired. Please log in again.',
|
|
646
|
-
eosHardLogout: true,
|
|
647
|
-
});
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
const originalJson = res.json.bind(res);
|
|
651
|
-
res.json = body => {
|
|
652
|
-
if (body && typeof body === 'object' && body.access_token && body.expires_in) {
|
|
653
|
-
const expiresIn = Math.max(1, Math.round(Number(body.expires_in) || Number(this.settings.ttl) || 3600));
|
|
654
|
-
body.refresh_token = '';
|
|
655
|
-
body.refresh_token_expires_in = expiresIn;
|
|
656
|
-
body.eosHardLogout = true;
|
|
657
|
-
}
|
|
658
|
-
return originalJson(body);
|
|
659
|
-
};
|
|
660
|
-
next();
|
|
661
|
-
});
|
|
662
|
-
}
|
|
646
|
+
// v36: OAuth/session handling intentionally follows upstream ioBroker Admin.
|
|
663
647
|
this.oauth2Model = (0, webserver_1.createOAuth2Server)(this.adapter, {
|
|
664
648
|
app: this.server.app,
|
|
665
649
|
secure: this.settings.secure,
|
|
666
650
|
accessLifetime: this.settings.ttl,
|
|
667
|
-
refreshLifetime:
|
|
651
|
+
refreshLifetime: 60 * 60 * 24 * 7, // 1 week, same as upstream admin
|
|
668
652
|
noBasicAuth: this.settings.noBasicAuth,
|
|
669
653
|
loginPage: (req) => {
|
|
670
654
|
const isDev = req.url.includes('?dev');
|
|
@@ -676,44 +660,44 @@ class Web {
|
|
|
676
660
|
},
|
|
677
661
|
});
|
|
678
662
|
this.server.app.get('/session', (req, res) => {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
663
|
+
// v36: Follow upstream admin semantics again. Do not run a second
|
|
664
|
+
// EOS hard-logout timer here; the official OAuth/session handling
|
|
665
|
+
// already uses the configured access lifetime.
|
|
666
|
+
if (req.headers.cookie) {
|
|
667
|
+
const cookies = req.headers.cookie.split(';').find(c => c.trim().startsWith('access_token='));
|
|
668
|
+
let tokenCookie = cookies?.split('=')[1];
|
|
669
|
+
if (!tokenCookie && req.headers.authorization?.startsWith('Bearer ')) {
|
|
670
|
+
tokenCookie = req.headers.authorization.split(' ')[1];
|
|
671
|
+
}
|
|
672
|
+
else if (!tokenCookie && req.query?.token) {
|
|
673
|
+
tokenCookie = req.query.token;
|
|
674
|
+
}
|
|
675
|
+
if (tokenCookie) {
|
|
676
|
+
const candidates = new Set();
|
|
677
|
+
candidates.add(tokenCookie.startsWith('a:') ? tokenCookie : `a:${tokenCookie}`);
|
|
678
|
+
if (tokenCookie.length > 1)
|
|
679
|
+
candidates.add(`a:${tokenCookie[1]}`);
|
|
680
|
+
const ids = Array.from(candidates);
|
|
681
|
+
const readNext = (index) => {
|
|
682
|
+
const id = ids[index];
|
|
683
|
+
if (!id) {
|
|
684
|
+
res.json({ expireInSec: 0 });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
void this.adapter.getSession(id, (token) => {
|
|
688
|
+
if (!token?.user) {
|
|
689
|
+
readNext(index + 1);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
res.json({ expireInSec: Math.round((token.aExp - Date.now()) / 1000) });
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
};
|
|
696
|
+
readNext(0);
|
|
699
697
|
return;
|
|
700
698
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
readNext(index + 1);
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
const now = Date.now();
|
|
707
|
-
const expirations = [Number(token.aExp), Number(token.rExp), Number(token.expire), Number(token.expires), Number(token.expiresAt)].filter(value => Number.isFinite(value) && value > 0);
|
|
708
|
-
const expiresAt = expirations.length ? Math.min(...expirations) : now + (this.settings.ttl || 3600) * 1000;
|
|
709
|
-
res.json({
|
|
710
|
-
expireInSec: Math.max(0, Math.floor((expiresAt - now) / 1000)),
|
|
711
|
-
hardLogout: true,
|
|
712
|
-
user: token.user,
|
|
713
|
-
});
|
|
714
|
-
});
|
|
715
|
-
};
|
|
716
|
-
readNext(0);
|
|
699
|
+
}
|
|
700
|
+
res.json({ error: 'Cannot find session' });
|
|
717
701
|
});
|
|
718
702
|
this.server.app.get(/.*\/nexowatt\/security\/(?:context|session)$/, (req, res) => {
|
|
719
703
|
void this.sendEosSecuritySession(req, res).catch(e => {
|
package/build/main.js
CHANGED
|
@@ -737,7 +737,8 @@ class Admin extends adapter_core_1.Adapter {
|
|
|
737
737
|
return this.config.eosHideLegacyAdminFromNonAdmins !== false && this.config.eosHideLegacyAdminForNonAdmins !== false && this.config.nexowattHideLegacyAdminForNonAdmins !== false;
|
|
738
738
|
}
|
|
739
739
|
shouldApplyAdminOnlyAclToProtectedAdapters() {
|
|
740
|
-
|
|
740
|
+
const config = this.config;
|
|
741
|
+
return config.eosStrictProtectedAdapterAcl === true && this.config.eosApplyAdminOnlyAclToProtectedAdapters === true;
|
|
741
742
|
}
|
|
742
743
|
getAdminOnlyGroup() {
|
|
743
744
|
const configuredGroups = normalizeAdminOnlyGroups(this.config.eosAdminOnlyGroups || this.config.eosSecurityAdminGroups);
|
|
@@ -837,8 +838,8 @@ class Admin extends adapter_core_1.Adapter {
|
|
|
837
838
|
obj.common.nondeletable = false;
|
|
838
839
|
changed = true;
|
|
839
840
|
}
|
|
841
|
+
const targetAcl = this.getAdminOnlyAcl();
|
|
840
842
|
if (options.adminOnlyAcl) {
|
|
841
|
-
const targetAcl = this.getAdminOnlyAcl();
|
|
842
843
|
const acl = (obj.acl ||= {});
|
|
843
844
|
if (acl.owner !== targetAcl.owner) {
|
|
844
845
|
acl.owner = targetAcl.owner;
|
|
@@ -853,6 +854,10 @@ class Admin extends adapter_core_1.Adapter {
|
|
|
853
854
|
changed = true;
|
|
854
855
|
}
|
|
855
856
|
}
|
|
857
|
+
else if (obj.acl && obj.acl.owner === targetAcl.owner && obj.acl.ownerGroup === targetAcl.ownerGroup && obj.acl.object === targetAcl.object) {
|
|
858
|
+
delete obj.acl;
|
|
859
|
+
changed = true;
|
|
860
|
+
}
|
|
856
861
|
if (changed) {
|
|
857
862
|
await this.setForeignObjectAsync(id, obj);
|
|
858
863
|
}
|
|
@@ -863,7 +868,12 @@ class Admin extends adapter_core_1.Adapter {
|
|
|
863
868
|
return;
|
|
864
869
|
}
|
|
865
870
|
const isEosAdmin = adapter === EOS_ADMIN_ADAPTER_NAME;
|
|
866
|
-
const adminOnlyAcl = isEosAdmin || this.shouldApplyAdminOnlyAclToProtectedAdapters();
|
|
871
|
+
const adminOnlyAcl = isEosAdmin || (this.shouldApplyAdminOnlyAclToProtectedAdapters() && adapter !== 'backitup');
|
|
872
|
+
// v37 BackItUp/runtime-adapter compatibility: keep default protection UI/role based.
|
|
873
|
+
// Do not rewrite common/ACL of runtime adapters unless explicitly configured.
|
|
874
|
+
if (!isEosAdmin && !adminOnlyAcl) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
867
877
|
let changed = await this.ensureObjectProtectionPolicy(`system.adapter.${adapter}`, {
|
|
868
878
|
keepDontDelete: false,
|
|
869
879
|
adminOnlyAcl,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# NexoWatt EOS Admin v37
|
|
2
|
+
|
|
3
|
+
## Fokus
|
|
4
|
+
|
|
5
|
+
Diese Version stabilisiert drei produktive Bereiche:
|
|
6
|
+
|
|
7
|
+
- Benachrichtigungsdialoge bleiben vollständig klickbar und können sauber geschlossen bzw. bestätigt werden.
|
|
8
|
+
- BackItUp und andere Runtime-Adapter werden vom EOS-Sicherheitswächter standardmäßig nicht mehr über Objekt-/ACL-Umschreibungen verändert. Der Schutz läuft über EOS Rollen/UI.
|
|
9
|
+
- EOS-Sicherheitstexte werden auch auf Adapter-Konfigurationsseiten gegen UTF-8/Latin1-Mojibake repariert.
|
|
10
|
+
|
|
11
|
+
## Hinweise
|
|
12
|
+
|
|
13
|
+
BackItUp-Fehler wie `cannot found source "undefined" for compress` kommen typischerweise aus der BackItUp-Quell-/Zielkonfiguration. v37 sorgt dafür, dass EOS Admin BackItUp nicht durch ACL/common-Änderungen beeinflusst. Die BackItUp-Konfiguration selbst muss weiterhin im BackItUp-Adapter geprüft werden.
|