iobroker.eos-admin 7.9.45 → 7.9.48
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/jsonConfig.json5 +70 -0
- package/adminWww/css/eos-branding.css +77 -0
- package/adminWww/index.html +6 -5
- package/adminWww/js/eos-branding.js +12 -11
- package/adminWww/js/eos-role-ui.js +304 -0
- package/adminWww/js/eos-security-ui.js +5 -5
- package/build/lib/web.js +124 -9
- package/build/main.js +16 -5
- package/docs/NEXOWATT_EOS_BRANDING_DE.md +29 -29
- package/docs/NEXOWATT_EOS_UI_LOGO_LOGIN_V6_DE.md +9 -9
- package/docs/NEXOWATT_EOS_UI_PERFORMANCE_V5_DE.md +25 -25
- package/docs/NEXOWATT_EOS_UI_V12_HEADER_LOGO_NAV_CLEANUP_DE.md +16 -16
- package/docs/NEXOWATT_EOS_UI_V12_HEADER_NAV_LOGO_FIX_DE.md +16 -16
- package/docs/NEXOWATT_EOS_UI_V15_DE.md +8 -8
- package/docs/NEXOWATT_EOS_UI_V16_DE.md +9 -9
- package/docs/NEXOWATT_EOS_UI_V17_DE.md +24 -24
- package/docs/NEXOWATT_EOS_UI_V6_FIXES_DE.md +13 -13
- package/docs/NEXOWATT_EOS_UI_V6_LOGIN_LOGO_404_FIX_DE.md +9 -9
- package/docs/NEXOWATT_EOS_UI_V6_LOGO_LOGIN_FIX_DE.md +10 -10
- package/docs/NEXOWATT_EOS_UI_V7_RESTORE_LOGO_SCOPE_FIX_DE.md +18 -18
- package/io-package.json +32 -5
- package/package.json +1 -1
package/admin/jsonConfig.json5
CHANGED
|
@@ -253,6 +253,76 @@
|
|
|
253
253
|
},
|
|
254
254
|
],
|
|
255
255
|
},
|
|
256
|
+
eosInstallerGroups: {
|
|
257
|
+
newLine: true,
|
|
258
|
+
type: 'table',
|
|
259
|
+
label: 'EOS Installateurgruppen',
|
|
260
|
+
help: 'Mitglieder dieser Gruppen erhalten die Installateur-Rolle: Einrichtung, Service und Konfiguration ohne Admin-Systemverwaltung.',
|
|
261
|
+
xs: 12,
|
|
262
|
+
sm: 12,
|
|
263
|
+
items: [
|
|
264
|
+
{
|
|
265
|
+
type: 'text',
|
|
266
|
+
title: 'Gruppe',
|
|
267
|
+
attr: 'group',
|
|
268
|
+
width: '30%',
|
|
269
|
+
filter: false,
|
|
270
|
+
sort: false,
|
|
271
|
+
default: 'system.group.installateur',
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
type: 'text',
|
|
275
|
+
title: 'Beschreibung',
|
|
276
|
+
attr: 'note',
|
|
277
|
+
width: '55%',
|
|
278
|
+
filter: false,
|
|
279
|
+
sort: false,
|
|
280
|
+
default: 'Installateur / Service',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
type: 'checkbox',
|
|
284
|
+
title: 'Aktiv',
|
|
285
|
+
attr: 'enabled',
|
|
286
|
+
width: '15%',
|
|
287
|
+
default: true,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
eosEndUserGroups: {
|
|
292
|
+
newLine: true,
|
|
293
|
+
type: 'table',
|
|
294
|
+
label: 'EOS Endkundengruppen',
|
|
295
|
+
help: 'Mitglieder dieser Gruppen werden auf das Endkunden-Cockpit geführt und sehen keine Admin-/Systembereiche.',
|
|
296
|
+
xs: 12,
|
|
297
|
+
sm: 12,
|
|
298
|
+
items: [
|
|
299
|
+
{
|
|
300
|
+
type: 'text',
|
|
301
|
+
title: 'Gruppe',
|
|
302
|
+
attr: 'group',
|
|
303
|
+
width: '30%',
|
|
304
|
+
filter: false,
|
|
305
|
+
sort: false,
|
|
306
|
+
default: 'system.group.endkunde',
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
type: 'text',
|
|
310
|
+
title: 'Beschreibung',
|
|
311
|
+
attr: 'note',
|
|
312
|
+
width: '55%',
|
|
313
|
+
filter: false,
|
|
314
|
+
sort: false,
|
|
315
|
+
default: 'Endkunde / Bediener',
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
type: 'checkbox',
|
|
319
|
+
title: 'Aktiv',
|
|
320
|
+
attr: 'enabled',
|
|
321
|
+
width: '15%',
|
|
322
|
+
default: true,
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
},
|
|
256
326
|
eosProtectAdapterDeletion: {
|
|
257
327
|
newLine: true,
|
|
258
328
|
type: 'checkbox',
|
|
@@ -3528,3 +3528,80 @@ html.eos-objects-surface #eos-assist-root {
|
|
|
3528
3528
|
}
|
|
3529
3529
|
|
|
3530
3530
|
/* === NexoWatt EOS v40: keep command dialogs technically accurate ========= */
|
|
3531
|
+
|
|
3532
|
+
/* === NexoWatt EOS v48: role-aware navigation and end-customer landing === */
|
|
3533
|
+
html.eos-role-loaded .eos-role-hidden,
|
|
3534
|
+
html.eos-role-loaded .eos-role-hidden * {
|
|
3535
|
+
display: none !important;
|
|
3536
|
+
visibility: hidden !important;
|
|
3537
|
+
pointer-events: none !important;
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
.eos-role-landing {
|
|
3541
|
+
min-height: calc(100vh - 120px);
|
|
3542
|
+
width: 100%;
|
|
3543
|
+
display: flex;
|
|
3544
|
+
align-items: center;
|
|
3545
|
+
justify-content: center;
|
|
3546
|
+
padding: 32px;
|
|
3547
|
+
box-sizing: border-box;
|
|
3548
|
+
background:
|
|
3549
|
+
radial-gradient(circle at 20% 20%, rgba(0, 255, 170, .12), transparent 34%),
|
|
3550
|
+
radial-gradient(circle at 80% 70%, rgba(0, 140, 255, .13), transparent 36%),
|
|
3551
|
+
linear-gradient(135deg, rgba(0, 18, 28, .96), rgba(1, 8, 20, .98));
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
.eos-role-landing-card {
|
|
3555
|
+
width: min(560px, calc(100vw - 48px));
|
|
3556
|
+
border: 1px solid rgba(0, 255, 170, .38);
|
|
3557
|
+
border-radius: 28px;
|
|
3558
|
+
padding: 34px;
|
|
3559
|
+
color: #f4fff9;
|
|
3560
|
+
background: rgba(4, 28, 39, .88);
|
|
3561
|
+
box-shadow: 0 24px 70px rgba(0, 0, 0, .45), 0 0 34px rgba(0, 255, 170, .12);
|
|
3562
|
+
text-align: left;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
.eos-role-landing-eyebrow {
|
|
3566
|
+
display: inline-flex;
|
|
3567
|
+
align-items: center;
|
|
3568
|
+
gap: 8px;
|
|
3569
|
+
color: #00ffc0;
|
|
3570
|
+
font-size: 12px;
|
|
3571
|
+
font-weight: 800;
|
|
3572
|
+
letter-spacing: .12em;
|
|
3573
|
+
text-transform: uppercase;
|
|
3574
|
+
margin-bottom: 12px;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
.eos-role-landing-card h1 {
|
|
3578
|
+
margin: 0 0 12px;
|
|
3579
|
+
font-size: clamp(28px, 4vw, 44px);
|
|
3580
|
+
line-height: 1.08;
|
|
3581
|
+
color: #ffffff;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
.eos-role-landing-card p {
|
|
3585
|
+
margin: 0 0 24px;
|
|
3586
|
+
color: rgba(244, 255, 249, .82);
|
|
3587
|
+
font-size: 16px;
|
|
3588
|
+
line-height: 1.55;
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
.eos-role-open-cockpit {
|
|
3592
|
+
border: 0;
|
|
3593
|
+
border-radius: 999px;
|
|
3594
|
+
padding: 13px 24px;
|
|
3595
|
+
color: #001612;
|
|
3596
|
+
background: linear-gradient(135deg, #00ffc0, #00a2ff);
|
|
3597
|
+
font-weight: 900;
|
|
3598
|
+
cursor: pointer;
|
|
3599
|
+
box-shadow: 0 12px 30px rgba(0, 255, 170, .24);
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
.eos-role-open-cockpit:hover,
|
|
3603
|
+
.eos-role-open-cockpit:focus-visible {
|
|
3604
|
+
transform: translateY(-1px);
|
|
3605
|
+
filter: brightness(1.08);
|
|
3606
|
+
outline: none;
|
|
3607
|
+
}
|
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=48" />
|
|
35
35
|
<link
|
|
36
36
|
rel="manifest"
|
|
37
37
|
href="manifest.json"
|
|
@@ -154,10 +154,11 @@
|
|
|
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-
|
|
160
|
-
<script defer src="./js/eos-
|
|
157
|
+
<script defer src="./js/eos-branding.js?v=48"></script>
|
|
158
|
+
<script defer src="./js/eos-security-ui.js?v=48"></script>
|
|
159
|
+
<script defer src="./js/eos-role-ui.js?v=48"></script>
|
|
160
|
+
<script defer src="./js/eos-assistant.js?v=48"></script>
|
|
161
|
+
<script defer src="./js/eos-objects-state-tools.js?v=48"></script>
|
|
161
162
|
</head>
|
|
162
163
|
<body>
|
|
163
164
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
window.NEXOWATT_EOS_UI_VERSION = '
|
|
4
|
+
window.NEXOWATT_EOS_UI_VERSION = 'v48-role-guard-fix';
|
|
5
5
|
|
|
6
6
|
const BRAND = 'NexoWatt EOS';
|
|
7
7
|
const EOS_MEANING = 'Energy Operation System';
|
|
@@ -308,7 +308,7 @@
|
|
|
308
308
|
];
|
|
309
309
|
|
|
310
310
|
const normalizeSecurityPolicy = policy => {
|
|
311
|
-
//
|
|
311
|
+
// v47: delete protection is fixed to EOS core adapters. Dynamic policy entries
|
|
312
312
|
// from older settings are ignored, otherwise normal Dienste can be blocked.
|
|
313
313
|
const protectedAdapters = new Set(CORE_PROTECTED_ADAPTERS);
|
|
314
314
|
const isAdmin = !!(policy?.isAdmin || policy?.isAdminGroup || policy?.isEosAdminGroup || policy?.isAdministrator);
|
|
@@ -410,7 +410,7 @@
|
|
|
410
410
|
};
|
|
411
411
|
|
|
412
412
|
const unlockDeleteControls = () => safe(() => {
|
|
413
|
-
//
|
|
413
|
+
// v47: clean up classes/attributes created by older EOS delete guards. We do not
|
|
414
414
|
// install capture listeners anymore; React handlers enforce the core protection list.
|
|
415
415
|
document.querySelectorAll('#app-paper .eos-protected-delete-control, #app-paper .eos-security-hidden-delete, #app-paper [data-eos-delete-guard-bound="true"]').forEach(control => {
|
|
416
416
|
control.classList.remove('eos-protected-delete-control', 'eos-security-hidden-delete');
|
|
@@ -426,7 +426,7 @@
|
|
|
426
426
|
});
|
|
427
427
|
|
|
428
428
|
const lockDeleteControls = container => {
|
|
429
|
-
//
|
|
429
|
+
// v47: Intentionally no DOM click blocking. Stale capture listeners and disabled
|
|
430
430
|
// attributes were the reason normal services could not be deleted via trash icon.
|
|
431
431
|
unlockDeleteControls();
|
|
432
432
|
};
|
|
@@ -478,7 +478,7 @@
|
|
|
478
478
|
});
|
|
479
479
|
|
|
480
480
|
const protectDeleteDialogs = () => {
|
|
481
|
-
//
|
|
481
|
+
// v47: Do not disable delete confirmation dialogs in the DOM. The delete command
|
|
482
482
|
// guard is handled in React and keeps the fixed EOS core list protected.
|
|
483
483
|
unlockDeleteControls();
|
|
484
484
|
};
|
|
@@ -945,6 +945,8 @@
|
|
|
945
945
|
|
|
946
946
|
const desiredPermission = (profile, block, perm) => {
|
|
947
947
|
if (!perm) return null;
|
|
948
|
+
if (profile === 'enduser') profile = 'operator';
|
|
949
|
+
if (profile === 'installer') profile = 'service';
|
|
948
950
|
if (profile === 'admin') return true;
|
|
949
951
|
if (profile === 'viewer') return ['object', 'state', 'file'].includes(block) && ['list', 'read'].includes(perm);
|
|
950
952
|
if (profile === 'operator') {
|
|
@@ -981,13 +983,12 @@
|
|
|
981
983
|
const panel = document.createElement('section');
|
|
982
984
|
panel.className = 'eos-permission-presets';
|
|
983
985
|
panel.innerHTML = `
|
|
984
|
-
<div class="eos-permission-presets-title">
|
|
985
|
-
<div class="eos-permission-presets-text">Wähle ein Profil und passe danach einzelne Rechte an
|
|
986
|
+
<div class="eos-permission-presets-title">EOS Rollen-Schnellprofile</div>
|
|
987
|
+
<div class="eos-permission-presets-text">NexoWatt EOS arbeitet mit drei Rollen: Admin, Installateur und Endkunde. Wähle ein Profil und passe danach bei Bedarf einzelne Rechte an.</div>
|
|
986
988
|
<div class="eos-permission-presets-actions">
|
|
987
|
-
<button type="button" data-profile="
|
|
988
|
-
<button type="button" data-profile="
|
|
989
|
-
<button type="button" data-profile="
|
|
990
|
-
<button type="button" data-profile="admin">Vollzugriff</button>
|
|
989
|
+
<button type="button" data-profile="enduser">Endkunde</button>
|
|
990
|
+
<button type="button" data-profile="installer">Installateur</button>
|
|
991
|
+
<button type="button" data-profile="admin">Admin</button>
|
|
991
992
|
</div>
|
|
992
993
|
`;
|
|
993
994
|
panel.addEventListener('click', event => {
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
window.NEXOWATT_EOS_ROLE_UI_VERSION = 'v48-role-guard-fix';
|
|
5
|
+
|
|
6
|
+
const ASSET_BASE = (() => {
|
|
7
|
+
const script = document.currentScript?.src || document.querySelector('script[src*="eos-role-ui.js"]')?.src || window.location.href;
|
|
8
|
+
return new URL('../', script).href;
|
|
9
|
+
})();
|
|
10
|
+
|
|
11
|
+
const state = {
|
|
12
|
+
policy: null,
|
|
13
|
+
role: 'unknown',
|
|
14
|
+
scheduled: false,
|
|
15
|
+
observer: null,
|
|
16
|
+
fallbackTimer: null,
|
|
17
|
+
redirects: 0,
|
|
18
|
+
lastTarget: '',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const securityEndpointUrls = () => [
|
|
22
|
+
new URL('nexowatt/security/context', ASSET_BASE).href,
|
|
23
|
+
new URL('nexowatt/security/session', ASSET_BASE).href,
|
|
24
|
+
new URL('eos/security/status', ASSET_BASE).href,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const safe = fn => {
|
|
28
|
+
try { return fn(); } catch (_) { return undefined; }
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const normalize = value => String(value || '')
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.normalize('NFD')
|
|
34
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
35
|
+
.replace(/ß/g, 'ss')
|
|
36
|
+
.replace(/[^a-z0-9#/_:.-]+/g, ' ')
|
|
37
|
+
.trim();
|
|
38
|
+
|
|
39
|
+
const stripTab = value => String(value || '').replace(/^#?\/?/, '').replace(/^tab-/, '');
|
|
40
|
+
|
|
41
|
+
const hashTab = () => {
|
|
42
|
+
const hash = decodeURIComponent(window.location.hash || '');
|
|
43
|
+
const match = hash.match(/#\/?(tab-[a-z0-9_-]+(?:-\d+)?)\b/i);
|
|
44
|
+
if (match) return match[1];
|
|
45
|
+
if (!hash || hash === '#' || hash === '#/' || hash === '#/tab-intro') return 'tab-intro';
|
|
46
|
+
return '';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const tabFromHref = href => {
|
|
50
|
+
const value = String(href || '');
|
|
51
|
+
const match = value.match(/#\/?(tab-[a-z0-9_-]+(?:-\d+)?)\b/i);
|
|
52
|
+
return match ? match[1] : '';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const textOf = el => {
|
|
56
|
+
if (!el) return '';
|
|
57
|
+
const attrs = ['href', 'title', 'aria-label', 'data-name', 'data-tab', 'data-id', 'id'];
|
|
58
|
+
const values = [el.textContent || ''];
|
|
59
|
+
attrs.forEach(attr => {
|
|
60
|
+
if (el.getAttribute && el.hasAttribute(attr)) values.push(el.getAttribute(attr) || '');
|
|
61
|
+
});
|
|
62
|
+
return values.join(' ');
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isLoginView = () => /(?:^|[?&])login(?:[=&]|$)/i.test(window.location.search || '') ||
|
|
66
|
+
!!document.querySelector('.login-form, #login, [data-testid="login"]');
|
|
67
|
+
|
|
68
|
+
const roleFromPolicy = policy => {
|
|
69
|
+
const rawRole = normalize(policy?.role || policy?.eosRole || policy?.accessRole || '');
|
|
70
|
+
if (policy?.isAdmin || policy?.isEosAdminGroup || policy?.isAdministrator || rawRole === 'admin' || rawRole === 'administrator') return 'admin';
|
|
71
|
+
if (policy?.isInstaller || /installateur|installer|service|techniker|technician|integrator|partner|wartung|maintenance/.test(rawRole)) return 'installer';
|
|
72
|
+
if (policy?.isEndUser || /endkunde|endkunden|kunde|kunden|customer|bediener|operator|viewer|user|nutzer/.test(rawRole)) return 'enduser';
|
|
73
|
+
|
|
74
|
+
const groupText = normalize([
|
|
75
|
+
...(Array.isArray(policy?.groups) ? policy.groups : []),
|
|
76
|
+
...(Array.isArray(policy?.groupNames) ? policy.groupNames : []),
|
|
77
|
+
].join(' '));
|
|
78
|
+
if (/system\.group\.administrator|\badministrator\b/.test(groupText)) return 'admin';
|
|
79
|
+
if (/installateur|installer|service|techniker|technician|integrator|partner|wartung|maintenance/.test(groupText)) return 'installer';
|
|
80
|
+
if (/endkunde|endkunden|kunde|kunden|customer|bediener|operator|viewer|nutzer/.test(groupText)) return 'enduser';
|
|
81
|
+
|
|
82
|
+
// Secure default: a normal, non-admin ioBroker user is an EOS end customer.
|
|
83
|
+
return 'enduser';
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const setRoleClasses = role => safe(() => {
|
|
87
|
+
const root = document.documentElement;
|
|
88
|
+
root.classList.add('eos-role-loaded');
|
|
89
|
+
['admin', 'installer', 'installateur', 'enduser', 'unknown'].forEach(name => root.classList.remove(`eos-role-${name}`));
|
|
90
|
+
root.classList.add(`eos-role-${role}`);
|
|
91
|
+
if (role === 'installer') root.classList.add('eos-role-installateur');
|
|
92
|
+
if (role === 'admin') {
|
|
93
|
+
root.classList.add('eos-security-admin');
|
|
94
|
+
root.classList.remove('eos-security-nonadmin');
|
|
95
|
+
} else {
|
|
96
|
+
root.classList.add('eos-security-nonadmin');
|
|
97
|
+
root.classList.remove('eos-security-admin');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const endUserAllowedByText = value => {
|
|
102
|
+
const text = normalize(value);
|
|
103
|
+
if (!text) return false;
|
|
104
|
+
if (/(eos cockpit|nexowatt ui|nexowatt cockpit|kunden cockpit|kundenbereich|endkundenbereich|visualisierung|visualisation|visu|dashboard|energie cockpit|energy cockpit|bedienung|smart home|smart-home)/.test(text)) return true;
|
|
105
|
+
if (/\b(lovelace|jarvis|vis|iqontrol|material)\b/.test(text)) return true;
|
|
106
|
+
return false;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const endUserDeniedByText = value => {
|
|
110
|
+
const text = normalize(value);
|
|
111
|
+
return /(module|dienste|datenpunkte|objekte|struktur|systemlogs|logs|zugange|rechte|benutzer|users|gruppen|groups|system hosts|system-hosts|hosts|dateien|files|sicherung|backup|konsole|console|terminal|xterm|admin|adapter|instances?|instanzen|geraetesuche|discovery|systemschutz|security)/.test(text);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const isEndUserTab = (tab, label = '') => {
|
|
115
|
+
const clean = normalize(tab);
|
|
116
|
+
const text = `${clean} ${normalize(label)}`;
|
|
117
|
+
if (!clean || clean === 'tab-intro') return false;
|
|
118
|
+
if (/^tab-(nexowatt-ui|nexowatt-cockpit|eos-cockpit|eos-dashboard|kunden-cockpit|endkunden-cockpit)(?:-|$)/.test(clean)) return true;
|
|
119
|
+
if (/^tab-(lovelace|jarvis|vis|iqontrol|material)(?:-|$)/.test(clean)) return true;
|
|
120
|
+
if (endUserDeniedByText(text)) return false;
|
|
121
|
+
return endUserAllowedByText(text);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const isInstallerDenied = (tab, label = '') => {
|
|
125
|
+
const text = `${normalize(tab)} ${normalize(label)}`;
|
|
126
|
+
return /(?:tab-users|tab-hosts|tab-xterm|tab-xtrem|tab-admin|tab-system|zugange\s*&?\s*rechte|zugange|rechte|benutzer|users|gruppen|system-hosts|system hosts|hosts|konsole|console|terminal|xterm|sicherung|backup|systemschutz|security|legacy admin)/.test(text);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const isRouteAllowed = (role, tab, label = '') => {
|
|
130
|
+
if (role === 'admin') return true;
|
|
131
|
+
if (role === 'installer') return !isInstallerDenied(tab, label);
|
|
132
|
+
if (role === 'enduser') return isEndUserTab(tab, label);
|
|
133
|
+
return false;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const navEntries = () => {
|
|
137
|
+
const anchors = Array.from(document.querySelectorAll('a[href*="#tab-"], a[href*="#/tab-"]'));
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
return anchors.map(anchor => {
|
|
140
|
+
const tab = tabFromHref(anchor.getAttribute('href') || anchor.href || '');
|
|
141
|
+
if (!tab || seen.has(anchor)) return null;
|
|
142
|
+
seen.add(anchor);
|
|
143
|
+
const root = anchor.closest('.MuiListItem-root, .MuiButtonBase-root, li, [role="button"], [class*="DrawerItem"], [class*="dragWrapper"]') || anchor;
|
|
144
|
+
return { anchor, root, tab, label: textOf(root) || textOf(anchor) };
|
|
145
|
+
}).filter(Boolean);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const firstAllowedTab = role => {
|
|
149
|
+
const entries = navEntries();
|
|
150
|
+
const hit = entries.find(entry => isRouteAllowed(role, entry.tab, entry.label));
|
|
151
|
+
if (hit) return hit.tab;
|
|
152
|
+
if (role === 'enduser') return 'tab-nexowatt-ui';
|
|
153
|
+
if (role === 'installer') return 'tab-instances';
|
|
154
|
+
return 'tab-intro';
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const applyMenuPolicy = () => safe(() => {
|
|
158
|
+
if (!state.policy || !state.role || state.role === 'unknown') return;
|
|
159
|
+
navEntries().forEach(entry => {
|
|
160
|
+
const hide = !isRouteAllowed(state.role, entry.tab, entry.label);
|
|
161
|
+
[entry.anchor, entry.root].forEach(el => {
|
|
162
|
+
if (!el) return;
|
|
163
|
+
el.classList.toggle('eos-role-hidden', hide);
|
|
164
|
+
if (hide) el.setAttribute('aria-hidden', 'true');
|
|
165
|
+
else el.removeAttribute('aria-hidden');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const setHashTab = tab => {
|
|
171
|
+
if (!tab) return;
|
|
172
|
+
if (state.lastTarget === tab && Date.now() - Number(sessionStorage.getItem('eosRoleLastRedirectAt') || 0) < 1200) return;
|
|
173
|
+
state.lastTarget = tab;
|
|
174
|
+
state.redirects += 1;
|
|
175
|
+
sessionStorage.setItem('eosRoleLastRedirectAt', String(Date.now()));
|
|
176
|
+
const next = `#${tab}`;
|
|
177
|
+
if (window.location.hash !== next) window.location.hash = next;
|
|
178
|
+
window.dispatchEvent(new CustomEvent('eos-role-navigate', { detail: { tab } }));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const redirectIfNeeded = () => safe(() => {
|
|
182
|
+
if (isLoginView()) return;
|
|
183
|
+
const role = state.role;
|
|
184
|
+
if (!role || role === 'unknown' || role === 'admin') return;
|
|
185
|
+
const tab = hashTab() || 'tab-intro';
|
|
186
|
+
if (isRouteAllowed(role, tab, '')) return;
|
|
187
|
+
const target = firstAllowedTab(role);
|
|
188
|
+
if (target && target !== tab && state.redirects < 6) setHashTab(target);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const appPaper = () => document.getElementById('app-paper') || document.querySelector('main, #root');
|
|
192
|
+
|
|
193
|
+
const appLooksBlank = () => {
|
|
194
|
+
const paper = appPaper();
|
|
195
|
+
if (!paper) return false;
|
|
196
|
+
const tab = hashTab();
|
|
197
|
+
const text = normalize(paper.textContent || '');
|
|
198
|
+
const interactive = paper.querySelectorAll('iframe, a, button, input, select, textarea, table, [role="grid"], .MuiDataGrid-root, .MuiTable-root').length;
|
|
199
|
+
if (state.role === 'enduser' && (!isEndUserTab(tab, text) || tab === 'tab-intro')) return true;
|
|
200
|
+
return text.length < 12 && interactive === 0;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const showLandingFallback = () => safe(() => {
|
|
204
|
+
if (isLoginView() || state.role !== 'enduser') return;
|
|
205
|
+
if (!appLooksBlank()) {
|
|
206
|
+
document.getElementById('eos-role-landing')?.remove();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const paper = appPaper();
|
|
210
|
+
if (!paper) return;
|
|
211
|
+
let landing = document.getElementById('eos-role-landing');
|
|
212
|
+
if (!landing) {
|
|
213
|
+
landing = document.createElement('section');
|
|
214
|
+
landing.id = 'eos-role-landing';
|
|
215
|
+
landing.className = 'eos-role-landing';
|
|
216
|
+
landing.innerHTML = `
|
|
217
|
+
<div class="eos-role-landing-card">
|
|
218
|
+
<div class="eos-role-landing-eyebrow">NexoWatt EOS</div>
|
|
219
|
+
<h1>Endkundenbereich</h1>
|
|
220
|
+
<p>Dieser Benutzer ist als Endkunde angemeldet. Die Administrations-Startseite ist für diese Rolle gesperrt; EOS öffnet stattdessen das Kunden-Cockpit.</p>
|
|
221
|
+
<button type="button" class="eos-role-open-cockpit">EOS Cockpit öffnen</button>
|
|
222
|
+
</div>
|
|
223
|
+
`;
|
|
224
|
+
landing.querySelector('.eos-role-open-cockpit')?.addEventListener('click', event => {
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
setHashTab(firstAllowedTab('enduser') || 'tab-nexowatt-ui');
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (!paper.contains(landing)) {
|
|
230
|
+
paper.appendChild(landing);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const scheduleFallbackCheck = () => {
|
|
235
|
+
clearTimeout(state.fallbackTimer);
|
|
236
|
+
state.fallbackTimer = setTimeout(() => {
|
|
237
|
+
showLandingFallback();
|
|
238
|
+
}, 1000);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const apply = () => {
|
|
242
|
+
if (!state.policy) return;
|
|
243
|
+
setRoleClasses(state.role);
|
|
244
|
+
applyMenuPolicy();
|
|
245
|
+
redirectIfNeeded();
|
|
246
|
+
scheduleFallbackCheck();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const scheduleApply = () => {
|
|
250
|
+
if (state.scheduled) return;
|
|
251
|
+
state.scheduled = true;
|
|
252
|
+
requestAnimationFrame(() => {
|
|
253
|
+
state.scheduled = false;
|
|
254
|
+
apply();
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const fetchPolicy = async () => {
|
|
259
|
+
for (const url of securityEndpointUrls()) {
|
|
260
|
+
try {
|
|
261
|
+
const response = await fetch(url, { credentials: 'same-origin', cache: 'no-store' });
|
|
262
|
+
if (!response.ok) continue;
|
|
263
|
+
const json = await response.json();
|
|
264
|
+
const role = roleFromPolicy(json);
|
|
265
|
+
state.policy = { ...json, role };
|
|
266
|
+
state.role = role;
|
|
267
|
+
window.NEXOWATT_EOS_ROLE_POLICY = state.policy;
|
|
268
|
+
scheduleApply();
|
|
269
|
+
return;
|
|
270
|
+
} catch (_) {
|
|
271
|
+
// try next endpoint
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
state.policy = { role: 'enduser', isEndUser: true, groups: [] };
|
|
275
|
+
state.role = 'enduser';
|
|
276
|
+
window.NEXOWATT_EOS_ROLE_POLICY = state.policy;
|
|
277
|
+
scheduleApply();
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const startObserver = () => safe(() => {
|
|
281
|
+
if (state.observer || !document.documentElement) return;
|
|
282
|
+
state.observer = new MutationObserver(scheduleApply);
|
|
283
|
+
state.observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['href', 'class', 'aria-label', 'title'] });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const start = () => {
|
|
287
|
+
fetchPolicy();
|
|
288
|
+
startObserver();
|
|
289
|
+
window.addEventListener('hashchange', () => {
|
|
290
|
+
state.redirects = 0;
|
|
291
|
+
scheduleApply();
|
|
292
|
+
});
|
|
293
|
+
window.addEventListener('storage', scheduleApply);
|
|
294
|
+
setTimeout(scheduleApply, 400);
|
|
295
|
+
setTimeout(scheduleApply, 1500);
|
|
296
|
+
setTimeout(scheduleApply, 3500);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (document.readyState === 'loading') {
|
|
300
|
+
document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
301
|
+
} else {
|
|
302
|
+
start();
|
|
303
|
+
}
|
|
304
|
+
})();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const VERSION = '
|
|
4
|
+
const VERSION = 'v48-role-guard-fix';
|
|
5
5
|
const LEGACY_ADMIN = 'admin';
|
|
6
6
|
const LEGACY_ADMIN_INSTANCE = 'admin.0';
|
|
7
7
|
const CORE_PROTECTED_ADAPTERS = ['admin', 'eos-admin', 'backitup', 'nexowatt-devices', 'nexowatt-device', 'nexowatt-dev', 'nexowatt-ui'];
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
|
|
66
66
|
const isAdminUser = () => !!(state.policy?.isAdmin || state.policy?.isEosAdminGroup || state.policy?.isAdministrator);
|
|
67
67
|
const protectedAdapters = () => {
|
|
68
|
-
//
|
|
68
|
+
// v47: hard delete protection only for EOS core adapters. Dynamic/stale
|
|
69
69
|
// policy entries must never block ordinary installed adapters.
|
|
70
70
|
return new Set([LEGACY_ADMIN, ...CORE_PROTECTED_ADAPTERS].map(normalizeAdapter).filter(Boolean));
|
|
71
71
|
};
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
};
|
|
149
149
|
|
|
150
150
|
const releaseDeleteControls = () => {
|
|
151
|
-
//
|
|
151
|
+
// v47: undo markers from older EOS DOM guards. Only touch controls that were
|
|
152
152
|
// marked by EOS scripts, not native Admin disabled states.
|
|
153
153
|
document.querySelectorAll('.eos-security-hidden-delete, .eos-protected-delete-control, [data-eos-security-locked-delete="true"]').forEach(control => {
|
|
154
154
|
control.classList?.remove('eos-security-hidden-delete', 'eos-protected-delete-control');
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
};
|
|
164
164
|
|
|
165
165
|
const hideProtectedDeleteControls = () => {
|
|
166
|
-
//
|
|
166
|
+
// v47: Do not hide or capture trash buttons in the DOM. The shipped React
|
|
167
167
|
// source/bundles already block protected EOS core adapters before executing
|
|
168
168
|
// the ioBroker del command. DOM blocking caused stale/virtualized rows to make
|
|
169
169
|
// normal services undeletable.
|
|
@@ -302,7 +302,7 @@
|
|
|
302
302
|
state.observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
303
303
|
};
|
|
304
304
|
|
|
305
|
-
//
|
|
305
|
+
// v47: No global capture listener for delete buttons. Protected adapter deletion is
|
|
306
306
|
// handled in the React delete handlers; global DOM interception broke normal Dienste.
|
|
307
307
|
|
|
308
308
|
const start = () => {
|