iobroker.eos-admin 7.9.23 → 7.9.25
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/README.md +20 -1
- package/admin/jsonConfig.json5 +143 -0
- package/adminWww/assets/Adapters-B5_jQ7DE.js +1 -1
- package/adminWww/assets/Instances-YdaGnS5a.js +1 -1
- package/adminWww/css/eos-branding.css +81 -0
- package/adminWww/index.html +3 -2
- package/adminWww/js/eos-branding.js +221 -1
- package/adminWww/js/eos-security-ui.js +212 -0
- package/build/lib/testPassword.js.map +1 -1
- package/build/lib/web.js +196 -18
- package/build/lib/web.js.map +1 -1
- package/build/main.js +332 -1
- package/build/main.js.map +1 -1
- package/io-package.json +46 -10
- package/package.json +1 -1
- package/tools/nexowatt-generate-eos-admin-repo-entry.cjs +1 -1
- package/tools/nexowatt-generate-repo-entry.cjs +12 -23
- package/tools/nexowatt-patch-repo.cjs +14 -22
|
@@ -2203,3 +2203,84 @@ html.eos-app .eos-hidden-logout * {
|
|
|
2203
2203
|
.eos-hidden-nexowatt-repo-warning {
|
|
2204
2204
|
display: none !important;
|
|
2205
2205
|
}
|
|
2206
|
+
|
|
2207
|
+
|
|
2208
|
+
/* NexoWatt EOS security visibility */
|
|
2209
|
+
.eos-hidden-legacy-admin {
|
|
2210
|
+
display: none !important;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
.eos-hide-legacy-admin-active [data-id="admin"],
|
|
2214
|
+
.eos-hide-legacy-admin-active [data-id="admin.0"],
|
|
2215
|
+
.eos-hide-legacy-admin-active [data-name="admin"] {
|
|
2216
|
+
display: none !important;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
/* v25: role-aware EOS security UI guard */
|
|
2220
|
+
html.eos-security-nonadmin .eos-hidden-legacy-admin {
|
|
2221
|
+
display: none !important;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
html.eos-security-nonadmin .eos-protected-adapter-row .eos-protected-delete-control,
|
|
2225
|
+
html.eos-security-nonadmin .eos-protected-delete-control {
|
|
2226
|
+
opacity: 0.28 !important;
|
|
2227
|
+
pointer-events: none !important;
|
|
2228
|
+
filter: grayscale(1) !important;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
html.eos-security-nonadmin .eos-protected-delete-dialog .eos-protected-delete-control {
|
|
2232
|
+
display: none !important;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
html.eos-security-nonadmin .eos-security-admin-only-field {
|
|
2236
|
+
display: none !important;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
.eos-security-restricted-note {
|
|
2240
|
+
margin: 10px 0 18px !important;
|
|
2241
|
+
padding: 14px 16px !important;
|
|
2242
|
+
border-radius: 16px !important;
|
|
2243
|
+
border: 1px solid rgba(45, 255, 182, 0.26) !important;
|
|
2244
|
+
background: rgba(4, 18, 28, 0.78) !important;
|
|
2245
|
+
color: rgba(232, 255, 248, 0.95) !important;
|
|
2246
|
+
box-shadow: 0 0 22px rgba(0, 229, 168, 0.13) !important;
|
|
2247
|
+
display: flex !important;
|
|
2248
|
+
flex-direction: column !important;
|
|
2249
|
+
gap: 4px !important;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
.eos-security-restricted-note strong {
|
|
2253
|
+
color: #53ffd0 !important;
|
|
2254
|
+
font-weight: 800 !important;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
.eos-security-restricted-note span {
|
|
2258
|
+
color: rgba(219, 246, 240, 0.78) !important;
|
|
2259
|
+
font-size: 0.9rem !important;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
|
|
2263
|
+
/* NexoWatt EOS v25 security visibility guard */
|
|
2264
|
+
.eos-security-hidden,
|
|
2265
|
+
.eos-security-hidden-delete {
|
|
2266
|
+
display: none !important;
|
|
2267
|
+
}
|
|
2268
|
+
.eos-security-protected-adapter {
|
|
2269
|
+
position: relative;
|
|
2270
|
+
}
|
|
2271
|
+
.eos-security-protected-adapter::after {
|
|
2272
|
+
content: 'EOS geschützt';
|
|
2273
|
+
position: absolute;
|
|
2274
|
+
right: 10px;
|
|
2275
|
+
top: 10px;
|
|
2276
|
+
z-index: 2;
|
|
2277
|
+
font-size: 10px;
|
|
2278
|
+
letter-spacing: .06em;
|
|
2279
|
+
text-transform: uppercase;
|
|
2280
|
+
padding: 3px 8px;
|
|
2281
|
+
border-radius: 999px;
|
|
2282
|
+
color: rgba(225, 255, 246, .88);
|
|
2283
|
+
background: rgba(5, 22, 28, .72);
|
|
2284
|
+
border: 1px solid rgba(55, 230, 180, .22);
|
|
2285
|
+
pointer-events: none;
|
|
2286
|
+
}
|
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=25" />
|
|
35
35
|
<link
|
|
36
36
|
rel="manifest"
|
|
37
37
|
href="manifest.json"
|
|
@@ -154,7 +154,8 @@
|
|
|
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=
|
|
157
|
+
<script defer src="./js/eos-branding.js?v=25"></script>
|
|
158
|
+
<script defer src="./js/eos-security-ui.js?v=25"></script>
|
|
158
159
|
</head>
|
|
159
160
|
<body>
|
|
160
161
|
<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 = 'v25-admin-visibility-acl-guard';
|
|
5
5
|
|
|
6
6
|
const BRAND = 'NexoWatt EOS';
|
|
7
7
|
const EOS_MEANING = 'Energy Operation System';
|
|
@@ -64,6 +64,14 @@
|
|
|
64
64
|
scopePatchScheduled: false,
|
|
65
65
|
pendingScopes: new Set(),
|
|
66
66
|
lastFullPatch: 0,
|
|
67
|
+
securityPolicy: {
|
|
68
|
+
loaded: false,
|
|
69
|
+
isAdmin: false,
|
|
70
|
+
hideLegacyAdminForNonAdmins: true,
|
|
71
|
+
restrictProtectedAdapterControls: true,
|
|
72
|
+
protectedAdapters: ['eos-admin', 'backitup'],
|
|
73
|
+
},
|
|
74
|
+
securityFetchStarted: false,
|
|
67
75
|
};
|
|
68
76
|
|
|
69
77
|
const safe = fn => {
|
|
@@ -163,6 +171,214 @@
|
|
|
163
171
|
};
|
|
164
172
|
};
|
|
165
173
|
|
|
174
|
+
const normalizeIdentifier = value => String(value || '')
|
|
175
|
+
.toLowerCase()
|
|
176
|
+
.normalize('NFD')
|
|
177
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
178
|
+
.replace(/^iobroker\./, '')
|
|
179
|
+
.replace(/^system\.adapter\./, '')
|
|
180
|
+
.replace(/\.\d+$/, '')
|
|
181
|
+
.replace(/[^a-z0-9_.-]+/g, ' ')
|
|
182
|
+
.trim();
|
|
183
|
+
|
|
184
|
+
const adapterPattern = adapter => {
|
|
185
|
+
const escaped = String(adapter || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
186
|
+
return new RegExp(`(?:^|[^a-z0-9_-])${escaped}(?:$|[^a-z0-9_-])`, 'i');
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const textOfElement = el => {
|
|
190
|
+
if (!el) return '';
|
|
191
|
+
const attrs = ['data-adapter-name', 'data-adapter', 'data-id', 'id', 'title', 'aria-label', 'alt', 'href', 'src'];
|
|
192
|
+
const values = [el.textContent || ''];
|
|
193
|
+
attrs.forEach(attr => {
|
|
194
|
+
if (el.getAttribute && el.hasAttribute(attr)) values.push(el.getAttribute(attr) || '');
|
|
195
|
+
});
|
|
196
|
+
return values.join(' ');
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const securityEndpointUrls = () => [
|
|
200
|
+
new URL('nexowatt/security/context', ASSET_BASE).href,
|
|
201
|
+
new URL('nexowatt/security/session', ASSET_BASE).href,
|
|
202
|
+
new URL('eos/security/status', ASSET_BASE).href,
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
const normalizeSecurityPolicy = policy => {
|
|
206
|
+
const protectedAdapters = new Set(['eos-admin', 'backitup']);
|
|
207
|
+
(Array.isArray(policy?.protectedAdapters) ? policy.protectedAdapters : []).forEach(item => {
|
|
208
|
+
const adapter = typeof item === 'string' ? normalizeIdentifier(item) : normalizeIdentifier(item?.adapter || item?.name);
|
|
209
|
+
if (adapter) protectedAdapters.add(adapter);
|
|
210
|
+
});
|
|
211
|
+
const isAdmin = !!(policy?.isAdmin || policy?.isAdminGroup || policy?.isEosAdminGroup || policy?.isAdministrator);
|
|
212
|
+
return {
|
|
213
|
+
loaded: true,
|
|
214
|
+
user: policy?.user || null,
|
|
215
|
+
groups: Array.isArray(policy?.groups) ? policy.groups : [],
|
|
216
|
+
isAdmin,
|
|
217
|
+
hideLegacyAdminForNonAdmins: policy?.hideLegacyAdminForNonAdmins !== false && policy?.hideLegacyAdminFromNonAdmins !== false,
|
|
218
|
+
restrictProtectedAdapterControls: policy?.restrictProtectedAdapterControls !== false,
|
|
219
|
+
protectedAdapters: [...protectedAdapters].sort(),
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const applySecurityClasses = () => {
|
|
224
|
+
const policy = state.securityPolicy;
|
|
225
|
+
document.documentElement.classList.toggle('eos-security-loaded', !!policy.loaded);
|
|
226
|
+
document.documentElement.classList.toggle('eos-security-admin', !!policy.isAdmin);
|
|
227
|
+
document.documentElement.classList.toggle('eos-security-nonadmin', policy.loaded && !policy.isAdmin);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const fetchSecurityPolicy = async () => {
|
|
231
|
+
if (state.securityFetchStarted) return;
|
|
232
|
+
state.securityFetchStarted = true;
|
|
233
|
+
for (const url of securityEndpointUrls()) {
|
|
234
|
+
try {
|
|
235
|
+
const response = await fetch(url, { credentials: 'same-origin', cache: 'no-store' });
|
|
236
|
+
if (!response.ok) continue;
|
|
237
|
+
const json = await response.json();
|
|
238
|
+
if (!json || json.error) continue;
|
|
239
|
+
state.securityPolicy = normalizeSecurityPolicy(json);
|
|
240
|
+
applySecurityClasses();
|
|
241
|
+
scheduleFullPatch(0);
|
|
242
|
+
return;
|
|
243
|
+
} catch (e) {
|
|
244
|
+
// Security endpoint may be unavailable during login or old cache. Fallback below.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
state.securityPolicy = normalizeSecurityPolicy({ isAdmin: false, protectedAdapters: ['eos-admin', 'backitup'] });
|
|
248
|
+
applySecurityClasses();
|
|
249
|
+
scheduleFullPatch(0);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const isAdminUser = () => !!state.securityPolicy?.isAdmin;
|
|
253
|
+
|
|
254
|
+
const isLegacyAdminContainer = el => {
|
|
255
|
+
const text = textOfElement(el);
|
|
256
|
+
if (/\badmin\.0\b/i.test(text) || /\bsystem\.adapter\.admin\.0\b/i.test(text)) return true;
|
|
257
|
+
const candidates = Array.from(el.querySelectorAll ? el.querySelectorAll('*') : []);
|
|
258
|
+
candidates.push(el);
|
|
259
|
+
return candidates.some(node => {
|
|
260
|
+
const value = textOfElement(node).trim();
|
|
261
|
+
return /^admin$/i.test(value) || /^iobroker\.admin$/i.test(value) || /^system\.adapter\.admin$/i.test(value);
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const containerMentionsAdapter = (container, adapter) => {
|
|
266
|
+
if (!container || !adapter) return false;
|
|
267
|
+
const normalized = normalizeIdentifier(adapter);
|
|
268
|
+
if (!normalized) return false;
|
|
269
|
+
if (normalized === 'admin') return isLegacyAdminContainer(container);
|
|
270
|
+
const pattern = adapterPattern(normalized);
|
|
271
|
+
if (pattern.test(textOfElement(container))) return true;
|
|
272
|
+
return Array.from(container.querySelectorAll ? container.querySelectorAll('[title],[aria-label],[data-adapter-name],[data-adapter],[data-id]') : [])
|
|
273
|
+
.some(el => pattern.test(textOfElement(el)));
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const getSecurityContainers = () => Array.from(document.querySelectorAll([
|
|
277
|
+
'#app-paper .MuiCard-root',
|
|
278
|
+
'#app-paper tr.MuiTableRow-root',
|
|
279
|
+
'#app-paper tr',
|
|
280
|
+
'#app-paper .MuiAccordion-root',
|
|
281
|
+
'#app-paper [role="row"]',
|
|
282
|
+
'#app-paper [role="treeitem"]',
|
|
283
|
+
'#app-paper .MuiDataGrid-row',
|
|
284
|
+
'#app-paper .MuiTreeItem-root',
|
|
285
|
+
'#app-paper .MuiFormControlLabel-root',
|
|
286
|
+
'#app-paper .MuiListItem-root',
|
|
287
|
+
'#app-paper .MuiListItemButton-root',
|
|
288
|
+
].join(','))).filter(el => !el.closest('.MuiDialog-paper'));
|
|
289
|
+
|
|
290
|
+
const isDeleteControl = el => {
|
|
291
|
+
const text = normalize(el.textContent || el.getAttribute?.('title') || el.getAttribute?.('aria-label') || '');
|
|
292
|
+
const title = normalize(el.getAttribute?.('title') || el.closest?.('[title]')?.getAttribute('title') || '');
|
|
293
|
+
const svg = el.querySelector?.('svg[data-testid*="Delete"], svg[data-testid*="Remove"], svg[data-testid*="Clear"]');
|
|
294
|
+
return !!svg || /\b(delete|remove|uninstall|del|loschen|entfernen|deinstallieren)\b/.test(`${text} ${title}`);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const lockDeleteControls = container => {
|
|
298
|
+
Array.from(container.querySelectorAll('button, [role="button"], a')).forEach(control => {
|
|
299
|
+
if (!isDeleteControl(control)) return;
|
|
300
|
+
control.classList.add('eos-protected-delete-control');
|
|
301
|
+
control.setAttribute('aria-disabled', 'true');
|
|
302
|
+
control.setAttribute('title', 'Nur Administratoren dürfen geschützte EOS-Systemmodule löschen');
|
|
303
|
+
if ('disabled' in control) control.disabled = true;
|
|
304
|
+
control.addEventListener('click', event => {
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
event.stopImmediatePropagation();
|
|
307
|
+
}, true);
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const protectDeleteDialogs = () => {
|
|
312
|
+
if (isAdminUser() || state.securityPolicy.restrictProtectedAdapterControls === false) return;
|
|
313
|
+
const protectedAdapters = state.securityPolicy.protectedAdapters || [];
|
|
314
|
+
Array.from(document.querySelectorAll('.MuiDialog-paper, [role="dialog"]')).forEach(dialog => {
|
|
315
|
+
const text = textOfElement(dialog);
|
|
316
|
+
if (!/(delete|remove|loschen|entfernen|deinstallieren|del\s+)/i.test(text)) return;
|
|
317
|
+
const protectedHit = protectedAdapters.some(adapter => containerMentionsAdapter(dialog, adapter));
|
|
318
|
+
if (!protectedHit) return;
|
|
319
|
+
dialog.classList.add('eos-protected-delete-dialog');
|
|
320
|
+
Array.from(dialog.querySelectorAll('button')).forEach(button => {
|
|
321
|
+
const label = normalize(button.textContent || button.getAttribute('aria-label') || '');
|
|
322
|
+
if (/^(ok|ja|yes|delete|remove|loschen|entfernen|deinstallieren)$/.test(label)) {
|
|
323
|
+
button.disabled = true;
|
|
324
|
+
button.classList.add('eos-protected-delete-control');
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const hideSecuritySettingsForNonAdmin = () => {
|
|
331
|
+
if (isAdminUser()) return;
|
|
332
|
+
Array.from(document.querySelectorAll('.MuiDialog-paper, [role="dialog"]')).forEach(dialog => {
|
|
333
|
+
const dialogText = textOfElement(dialog);
|
|
334
|
+
if (!/(eos security|nexowatt security|legacy admin|alter admin|protected adapters|geschutzte adapter|eos admin groups)/i.test(dialogText)) return;
|
|
335
|
+
dialog.classList.add('eos-security-settings-restricted');
|
|
336
|
+
const needles = /(eos security|nexowatt security|legacy admin|alter admin|protected adapters|geschutzte adapter|eos admin groups|lock legacy admin|hide legacy admin|restrict protected adapter)/i;
|
|
337
|
+
Array.from(dialog.querySelectorAll('label, legend, h2, h3, h4, .MuiTypography-root, .MuiFormLabel-root')).forEach(label => {
|
|
338
|
+
if (!needles.test(label.textContent || '')) return;
|
|
339
|
+
const row = label.closest('.MuiGrid2-root, .MuiGrid-root, .MuiFormControl-root, .MuiBox-root, .MuiPaper-root') || label.parentElement;
|
|
340
|
+
if (row && row !== dialog) row.classList.add('eos-security-admin-only-field');
|
|
341
|
+
});
|
|
342
|
+
if (!dialog.querySelector('.eos-security-restricted-note')) {
|
|
343
|
+
const note = document.createElement('div');
|
|
344
|
+
note.className = 'eos-security-restricted-note';
|
|
345
|
+
note.innerHTML = '<strong>EOS Systemschutz aktiv</strong><span>Diese Sicherheitseinstellungen sind nur für Administratoren sichtbar und änderbar.</span>';
|
|
346
|
+
const content = dialog.querySelector('.MuiDialogContent-root') || dialog;
|
|
347
|
+
content.insertBefore(note, content.firstElementChild || null);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const applySecurityUiGuard = () => safe(() => {
|
|
353
|
+
const policy = state.securityPolicy;
|
|
354
|
+
applySecurityClasses();
|
|
355
|
+
if (!policy.loaded) return;
|
|
356
|
+
if (isAdminUser()) {
|
|
357
|
+
document.querySelectorAll('.eos-hidden-legacy-admin, .eos-protected-adapter-row').forEach(el => {
|
|
358
|
+
el.classList.remove('eos-hidden-legacy-admin', 'eos-protected-adapter-row');
|
|
359
|
+
el.removeAttribute('aria-hidden');
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const containers = getSecurityContainers();
|
|
365
|
+
containers.forEach(container => {
|
|
366
|
+
if (policy.hideLegacyAdminForNonAdmins !== false && isLegacyAdminContainer(container)) {
|
|
367
|
+
container.classList.add('eos-hidden-legacy-admin');
|
|
368
|
+
container.setAttribute('aria-hidden', 'true');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (policy.restrictProtectedAdapterControls !== false) {
|
|
372
|
+
const protectedHit = (policy.protectedAdapters || []).some(adapter => adapter !== 'admin' && containerMentionsAdapter(container, adapter));
|
|
373
|
+
if (protectedHit) {
|
|
374
|
+
container.classList.add('eos-protected-adapter-row');
|
|
375
|
+
lockDeleteControls(container);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
protectDeleteDialogs();
|
|
380
|
+
hideSecuritySettingsForNonAdmin();
|
|
381
|
+
});
|
|
166
382
|
|
|
167
383
|
const isLoginView = () => safe(() => {
|
|
168
384
|
const hasApp = !!document.getElementById('app-paper');
|
|
@@ -534,6 +750,7 @@
|
|
|
534
750
|
ensureSettingsDialogClasses();
|
|
535
751
|
hideNativeLogoutNav();
|
|
536
752
|
hideOfficialNexoWattRepoWarning();
|
|
753
|
+
applySecurityUiGuard();
|
|
537
754
|
patchTextNodes(document.body || document.documentElement);
|
|
538
755
|
patchAttributes(document.body || document.documentElement);
|
|
539
756
|
};
|
|
@@ -550,6 +767,7 @@
|
|
|
550
767
|
ensureSettingsDialogClasses();
|
|
551
768
|
hideNativeLogoutNav();
|
|
552
769
|
hideOfficialNexoWattRepoWarning();
|
|
770
|
+
applySecurityUiGuard();
|
|
553
771
|
for (const scope of scopes.slice(0, 80)) {
|
|
554
772
|
if (!scope || !scope.isConnected) continue;
|
|
555
773
|
patchTextNodes(scope);
|
|
@@ -605,11 +823,13 @@
|
|
|
605
823
|
if (document.readyState === 'loading') {
|
|
606
824
|
document.addEventListener('DOMContentLoaded', () => {
|
|
607
825
|
fullPatch();
|
|
826
|
+
fetchSecurityPolicy();
|
|
608
827
|
installObserver();
|
|
609
828
|
[250, 1000, 2500, 5000].forEach(scheduleFullPatch);
|
|
610
829
|
}, { once: true });
|
|
611
830
|
} else {
|
|
612
831
|
fullPatch();
|
|
832
|
+
fetchSecurityPolicy();
|
|
613
833
|
installObserver();
|
|
614
834
|
[250, 1000, 2500, 5000].forEach(scheduleFullPatch);
|
|
615
835
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const VERSION = 'v25-security-visibility-guard';
|
|
5
|
+
const LEGACY_ADMIN = 'admin';
|
|
6
|
+
const LEGACY_ADMIN_INSTANCE = 'admin.0';
|
|
7
|
+
const SECURITY_URL = '/nexowatt/security/session';
|
|
8
|
+
|
|
9
|
+
const state = {
|
|
10
|
+
loaded: false,
|
|
11
|
+
policy: {
|
|
12
|
+
isAdmin: false,
|
|
13
|
+
isEosAdminGroup: false,
|
|
14
|
+
isAdministrator: false,
|
|
15
|
+
hideLegacyAdminFromNonAdmins: true,
|
|
16
|
+
restrictProtectedAdapterControls: true,
|
|
17
|
+
protectedAdapters: ['eos-admin'],
|
|
18
|
+
},
|
|
19
|
+
scheduled: false,
|
|
20
|
+
observer: null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const normalize = value => String(value || '').replace(/\s+/g, ' ').trim();
|
|
24
|
+
const normalizeAdapter = value => {
|
|
25
|
+
let adapter = String(value || '').trim().toLowerCase();
|
|
26
|
+
adapter = adapter.replace(/^system\.adapter\./, '').replace(/^iobroker\./, '').replace(/^@nexowatt\/iobroker\./, '').replace(/^@nexowatt\//, '');
|
|
27
|
+
adapter = adapter.replace(/\.\d+$/, '');
|
|
28
|
+
return /^[a-z0-9_-]+$/.test(adapter) ? adapter : '';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isAdminUser = () => !!(state.policy?.isAdmin || state.policy?.isEosAdminGroup || state.policy?.isAdministrator);
|
|
32
|
+
const protectedAdapters = () => new Set((state.policy?.protectedAdapters || []).map(normalizeAdapter).filter(Boolean));
|
|
33
|
+
|
|
34
|
+
const isLegacyAdminId = value => {
|
|
35
|
+
const text = String(value || '').toLowerCase();
|
|
36
|
+
return text === LEGACY_ADMIN || text === LEGACY_ADMIN_INSTANCE || text === 'system.adapter.admin' || text === 'system.adapter.admin.0';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const isProtectedAdapter = value => {
|
|
40
|
+
const adapter = normalizeAdapter(value);
|
|
41
|
+
return !!adapter && protectedAdapters().has(adapter);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
window.NEXOWATT_EOS_SECURITY = {
|
|
45
|
+
version: VERSION,
|
|
46
|
+
getPolicy: () => state.policy,
|
|
47
|
+
isAdminUser,
|
|
48
|
+
isProtectedAdapter,
|
|
49
|
+
shouldBlockAdapterDelete(adapterName) {
|
|
50
|
+
if (isAdminUser()) return false;
|
|
51
|
+
const adapter = normalizeAdapter(adapterName);
|
|
52
|
+
return adapter === LEGACY_ADMIN || isProtectedAdapter(adapter);
|
|
53
|
+
},
|
|
54
|
+
shouldBlockInstanceDelete(instanceIdOrAdapter) {
|
|
55
|
+
if (isAdminUser()) return false;
|
|
56
|
+
const raw = String(instanceIdOrAdapter || '').replace(/^system\.adapter\./, '');
|
|
57
|
+
const adapter = normalizeAdapter(raw);
|
|
58
|
+
return adapter === LEGACY_ADMIN || isProtectedAdapter(adapter);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const closestPanel = el => {
|
|
63
|
+
if (!el || !el.closest) return null;
|
|
64
|
+
return el.closest('.MuiCard-root, .MuiPaper-root, [role="row"], tr, .MuiListItem-root, .MuiBox-root');
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const markHidden = el => {
|
|
68
|
+
if (!el || el.classList?.contains('eos-security-keep-visible')) return;
|
|
69
|
+
el.classList.add('eos-security-hidden');
|
|
70
|
+
el.setAttribute('aria-hidden', 'true');
|
|
71
|
+
el.style.display = 'none';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const elementHasLegacyAdminIcon = el => {
|
|
75
|
+
if (!el?.querySelectorAll) return false;
|
|
76
|
+
return Array.from(el.querySelectorAll('img,[src]')).some(img => {
|
|
77
|
+
const src = String(img.getAttribute('src') || '').toLowerCase();
|
|
78
|
+
return /(?:^|\/)adapter\/admin\//.test(src) || /(?:^|\/)admin\/(?:admin\.(?:png|svg)|admin\.svg)/.test(src);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const elementTextMatchesLegacyAdmin = el => {
|
|
83
|
+
const text = normalize(el?.textContent || '').toLowerCase();
|
|
84
|
+
if (!text) return false;
|
|
85
|
+
if (text.includes('eos-admin') || text.includes('eos admin')) return false;
|
|
86
|
+
return /\badmin\.0\b/.test(text) || /system\.adapter\.admin(?:\.0)?\b/.test(text) || /\biobroker\.admin\b/.test(text);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const hideLegacyAdminPanels = () => {
|
|
90
|
+
if (!state.policy?.hideLegacyAdminFromNonAdmins || isAdminUser()) return;
|
|
91
|
+
const candidates = new Set();
|
|
92
|
+
document.querySelectorAll('img,[src]').forEach(img => {
|
|
93
|
+
if (elementHasLegacyAdminIcon(img.parentElement || img)) candidates.add(closestPanel(img) || img);
|
|
94
|
+
});
|
|
95
|
+
document.querySelectorAll('.MuiCard-root, .MuiPaper-root, [role="row"], tr, .MuiListItem-root').forEach(el => {
|
|
96
|
+
if (elementHasLegacyAdminIcon(el) || elementTextMatchesLegacyAdmin(el)) candidates.add(el);
|
|
97
|
+
});
|
|
98
|
+
candidates.forEach(el => {
|
|
99
|
+
const text = normalize(el.textContent || '').toLowerCase();
|
|
100
|
+
if (text.includes('eos-admin') || text.includes('eos admin')) return;
|
|
101
|
+
markHidden(el);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const hideProtectedDeleteControls = () => {
|
|
106
|
+
if (!state.policy?.restrictProtectedAdapterControls || isAdminUser()) return;
|
|
107
|
+
const protectedSet = protectedAdapters();
|
|
108
|
+
if (!protectedSet.size) return;
|
|
109
|
+
|
|
110
|
+
document.querySelectorAll('.MuiCard-root, .MuiPaper-root, [role="row"], tr, .MuiListItem-root').forEach(panel => {
|
|
111
|
+
let adapter = '';
|
|
112
|
+
const icon = panel.querySelector('img[src*="/adapter/"], img[src*="adapter/"]');
|
|
113
|
+
const src = icon ? String(icon.getAttribute('src') || '') : '';
|
|
114
|
+
const match = src.match(/adapter\/([^\/]+)\//i);
|
|
115
|
+
if (match) adapter = normalizeAdapter(match[1]);
|
|
116
|
+
const text = normalize(panel.textContent || '').toLowerCase();
|
|
117
|
+
if (!adapter) {
|
|
118
|
+
for (const protectedName of protectedSet) {
|
|
119
|
+
if (new RegExp(`\\b${protectedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\.\\d+)?\\b`, 'i').test(text)) {
|
|
120
|
+
adapter = protectedName;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!adapter || !protectedSet.has(adapter)) return;
|
|
126
|
+
panel.classList.add('eos-security-protected-adapter');
|
|
127
|
+
panel.querySelectorAll('button,[role="button"],a').forEach(button => {
|
|
128
|
+
const label = normalize(`${button.textContent || ''} ${button.getAttribute('title') || ''} ${button.getAttribute('aria-label') || ''}`).toLowerCase();
|
|
129
|
+
if (/löschen|delete|remove|deinstall|uninstall/.test(label)) {
|
|
130
|
+
button.classList.add('eos-security-hidden-delete');
|
|
131
|
+
button.setAttribute('disabled', 'disabled');
|
|
132
|
+
button.setAttribute('aria-disabled', 'true');
|
|
133
|
+
button.style.display = 'none';
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
document.querySelectorAll('[role="menuitem"], .MuiMenuItem-root').forEach(item => {
|
|
139
|
+
const text = normalize(item.textContent || '').toLowerCase();
|
|
140
|
+
if (/löschen|delete|remove|deinstall|uninstall/.test(text)) {
|
|
141
|
+
// Context menus cannot always be mapped back to the originating card. For protected NexoWatt systems,
|
|
142
|
+
// deletion through pop-up menus is blocked by the command guard and visually hidden for non-admin users.
|
|
143
|
+
item.classList.add('eos-security-hidden-delete');
|
|
144
|
+
item.style.display = 'none';
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const applyPolicyToDom = () => {
|
|
150
|
+
document.documentElement.classList.toggle('eos-security-admin-user', isAdminUser());
|
|
151
|
+
document.documentElement.classList.toggle('eos-security-non-admin-user', !isAdminUser());
|
|
152
|
+
hideLegacyAdminPanels();
|
|
153
|
+
hideProtectedDeleteControls();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const scheduleApply = () => {
|
|
157
|
+
if (state.scheduled) return;
|
|
158
|
+
state.scheduled = true;
|
|
159
|
+
const run = () => {
|
|
160
|
+
state.scheduled = false;
|
|
161
|
+
applyPolicyToDom();
|
|
162
|
+
};
|
|
163
|
+
if ('requestIdleCallback' in window) window.requestIdleCallback(run, { timeout: 600 });
|
|
164
|
+
else window.requestAnimationFrame(run);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const loadPolicy = async () => {
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetch(SECURITY_URL, { credentials: 'same-origin', cache: 'no-store' });
|
|
170
|
+
const policy = await response.json();
|
|
171
|
+
state.policy = { ...state.policy, ...policy };
|
|
172
|
+
state.loaded = true;
|
|
173
|
+
} catch (e) {
|
|
174
|
+
state.loaded = false;
|
|
175
|
+
state.policy = { ...state.policy, isAdmin: false, isEosAdminGroup: false, isAdministrator: false };
|
|
176
|
+
console.warn('[NexoWatt EOS] Cannot read security policy, using safe non-admin UI mode:', e);
|
|
177
|
+
}
|
|
178
|
+
scheduleApply();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const installObserver = () => {
|
|
182
|
+
if (state.observer) return;
|
|
183
|
+
state.observer = new MutationObserver(() => scheduleApply());
|
|
184
|
+
state.observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
document.addEventListener('click', event => {
|
|
188
|
+
const target = event.target?.closest?.('button,[role="button"],a,[role="menuitem"],.MuiMenuItem-root');
|
|
189
|
+
if (!target || isAdminUser()) return;
|
|
190
|
+
const label = normalize(`${target.textContent || ''} ${target.getAttribute?.('title') || ''} ${target.getAttribute?.('aria-label') || ''}`).toLowerCase();
|
|
191
|
+
if (/löschen|delete|remove|deinstall|uninstall/.test(label)) {
|
|
192
|
+
target.classList.add('eos-security-hidden-delete');
|
|
193
|
+
target.style.display = 'none';
|
|
194
|
+
}
|
|
195
|
+
}, true);
|
|
196
|
+
|
|
197
|
+
if (document.readyState === 'loading') {
|
|
198
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
199
|
+
loadPolicy();
|
|
200
|
+
installObserver();
|
|
201
|
+
[300, 1000, 2500, 5000].forEach(ms => window.setTimeout(scheduleApply, ms));
|
|
202
|
+
}, { once: true });
|
|
203
|
+
} else {
|
|
204
|
+
loadPolicy();
|
|
205
|
+
installObserver();
|
|
206
|
+
[300, 1000, 2500, 5000].forEach(ms => window.setTimeout(scheduleApply, ms));
|
|
207
|
+
}
|
|
208
|
+
window.addEventListener('hashchange', () => {
|
|
209
|
+
loadPolicy();
|
|
210
|
+
scheduleApply();
|
|
211
|
+
});
|
|
212
|
+
})();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testPassword.js","sourceRoot":"./src/","sources":["lib/testPassword.ts"],"names":[],"mappings":";;AAAA,iDAAgD;AAChD,qDAA6E;AAC7E,6CAAuC;AAEvC,IAAI,UAAU,GAAG,KAAK,CAAC;AACvB,kDAAkD;AAClD,
|
|
1
|
+
{"version":3,"file":"testPassword.js","sourceRoot":"./src/","sources":["lib/testPassword.ts"],"names":[],"mappings":";;AAAA,iDAAgD;AAChD,qDAA6E;AAC7E,6CAAuC;AAEvC,IAAI,UAAU,GAAG,KAAK,CAAC;AACvB,kDAAkD;AAClD,4EAA4E;AAC5E,MAAM,aAAa,GAAG,IAAI,sBAAQ,CAAC;IAC/B,KAAK,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;QACnC,IAAI,UAAU,EAAE,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;aAAM,CAAC;YACJ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC5C,CAAC;QACD,QAAQ,EAAE,CAAC;IACf,CAAC;CACJ,CAAC,CAAC;AAEH,mEAAmE;AACnE,IAAA,wCAAuB,GAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;IACnC,IAAI,KAAK,EAAE,CAAC;QACR,OAAO,CAAC,GAAG,CAAC,8BAA8B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACnE,qBAAqB;QACrB,MAAM,EAAE,GAAG,IAAA,+BAAe,EAAC;YACvB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,aAAa;YACrB,QAAQ,EAAE,IAAI;SACjB,CAAC,CAAC;QACH,EAAE,CAAC,QAAQ,CAAC,2BAA2B,KAAK,CAAC,KAAK,kBAAkB,EAAE,CAAC,QAAgB,EAAE,EAAE;YACvF,UAAU,GAAG,KAAK,CAAC;YACnB,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC1D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;gBACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACpB,CAAC;YACD,EAAE,CAAC,QAAQ,CAAC,4BAA4B,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,cAAsB,EAAE,EAAE;gBAChF,UAAU,GAAG,KAAK,CAAC;gBACnB,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACtE,IAAI,QAAQ,KAAK,cAAc,EAAE,CAAC;oBAC9B,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;oBACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACpB,CAAC;gBACD,EAAE,CAAC,KAAK,EAAE,CAAC;gBACX,KAAK,IAAA,iCAAgB,EAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;oBACvE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACrD,CAAC,CAAC,CAAC;YACP,CAAC,CAAC,CAAC;YACH,UAAU,GAAG,IAAI,CAAC;QACtB,CAAC,CAAC,CAAC;QACH,UAAU,GAAG,IAAI,CAAC;IACtB,CAAC;SAAM,CAAC;QACJ,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACL,CAAC,CAAC,CAAC"}
|