iobroker.eos-admin 7.10.4 → 7.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL_TEST_V7.10.5.md +8 -0
- package/MERGE_UPDATE_README_V7.10.5.md +7 -0
- package/NEXOWATT_EOS_BUILD_INFO.json +22 -18
- package/NEXOWATT_EOS_PREBUILT_MANIFEST.json +37 -39
- package/PUBLISH_STABLE_V7.10.5.md +8 -0
- package/README_STABILITY_V7.10.5.md +5 -0
- package/RELEASE_ACCEPTANCE_V7.10.5.md +11 -0
- package/RELEASE_NOTES_V7.10.5.md +11 -0
- package/SECURITY_ROLE_MATRIX_V7.10.5.md +12 -0
- package/adminWww/assets/Intro-DkwRiz1n-v84.js +2 -2
- package/adminWww/assets/Objects-DPan0bzw-v84.js +1 -1
- package/adminWww/assets/bootstrap-COulQZax-v84.js +2 -2
- package/adminWww/css/nexowatt-native-shell.css +12 -1
- package/adminWww/index.html +14 -15
- package/adminWww/js/eos-role-bootstrap.js +5 -5
- package/adminWww/js/eos-role-ui.js +130 -17
- package/adminWww/js/nexowatt-native-shell.js +1 -1
- package/adminWww/static/js/{nexowatt-stable-v7101.js → nexowatt-stable-v7105.js} +1 -1
- package/build/main.js +13 -15
- package/io-package.json +27 -5
- package/package.json +9 -9
- package/tools/nexowatt-account-password-flow-selftest.cjs +3 -3
- package/tools/nexowatt-clean-legacy-runtime.cjs +6 -0
- package/tools/nexowatt-internal-reserve-selftest.cjs +1 -1
- package/tools/nexowatt-login-layout-selftest.cjs +3 -3
- package/tools/nexowatt-merge-update-selftest.cjs +1 -1
- package/tools/nexowatt-modern-ui-selftest.cjs +2 -2
- package/tools/nexowatt-role-security-selftest.cjs +129 -41
- package/tools/nexowatt-runtime-cleanup-selftest.cjs +4 -0
- package/tools/nexowatt-stable-v7103-selftest.cjs +4 -4
- package/tools/nexowatt-ui-overview-runtime-selftest.cjs +2 -2
- package/tools/nexowatt-validate-package.cjs +9 -8
- package/INSTALL_TEST_V7.10.4.md +0 -9
- package/MERGE_UPDATE_README_V7.10.4.md +0 -3
- package/PUBLISH_STABLE_V7.10.4.md +0 -10
- package/README_STABILITY_V7.10.4.md +0 -12
- package/RELEASE_ACCEPTANCE_V7.10.4.md +0 -3
- package/RELEASE_NOTES_V7.10.4.md +0 -11
- package/adminWww/nexowatt-role-security.js +0 -336
- package/build/lib/eosRoleSecurity.js +0 -310
- package/src-admin/README.md +0 -69
- package/src-admin/nexowatt-role-security.js +0 -336
package/src-admin/README.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# NexoWatt EOS Admin Frontend Source
|
|
2
|
-
|
|
3
|
-
Dieser Bereich enthält den Frontend-Quellcode der Administrationsoberfläche. Das sichtbare EOS-Branding wird über die fertigen Assets in `adminWww/` und die NexoWatt-spezifischen Branding-Dateien ergänzt.
|
|
4
|
-
|
|
5
|
-
Die folgenden Komponentenhinweise stammen aus der technischen Frontend-Basis und bleiben für Entwicklung und Wartung erhalten.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Components
|
|
10
|
-
|
|
11
|
-
## DrawerItem
|
|
12
|
-
|
|
13
|
-

|
|
14
|
-

|
|
15
|
-

|
|
16
|
-
|
|
17
|
-
### Import
|
|
18
|
-
|
|
19
|
-
```js
|
|
20
|
-
import DrawerItem from './components/DrawerItem';
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### Props
|
|
24
|
-
|
|
25
|
-
| Name | Default | Description |
|
|
26
|
-
| ------------ | ------- | -------------------------------------- |
|
|
27
|
-
| icon | | The icon on the left side |
|
|
28
|
-
| onClick | | Function that is called when clicking |
|
|
29
|
-
| selected | | Indicates whether the item is selected |
|
|
30
|
-
| text | | Text to display |
|
|
31
|
-
| badgeContent | 0 | Content of the badge |
|
|
32
|
-
| badgeColor | primary | Color of the badge |
|
|
33
|
-
|
|
34
|
-
### Example
|
|
35
|
-
|
|
36
|
-
```js
|
|
37
|
-
<DrawerItem
|
|
38
|
-
text="Logs"
|
|
39
|
-
selected={false}
|
|
40
|
-
badgeContent={11}
|
|
41
|
-
badgeColor={'error'}
|
|
42
|
-
/>
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## IntroCard
|
|
46
|
-
|
|
47
|
-

|
|
48
|
-

|
|
49
|
-

|
|
50
|
-
|
|
51
|
-
### Import
|
|
52
|
-
|
|
53
|
-
```js
|
|
54
|
-
import IntroCard from './components/IntroCard';
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### Props
|
|
58
|
-
|
|
59
|
-
| Name | Default | Description |
|
|
60
|
-
|------------------| ------- |-------------------------------------------------------------------------------------|
|
|
61
|
-
| title | | The title of the card |
|
|
62
|
-
| children | | The child elements |
|
|
63
|
-
| image | | The image to display on the left |
|
|
64
|
-
| color | #e2e2e2 | The color to display as the background of the image |
|
|
65
|
-
| action | | Link and text {link: 'https://example.com', text: 'example.com'} |
|
|
66
|
-
| reveal | | Element or text that should be displayed in the reveal element using an info button |
|
|
67
|
-
| enabled | | State of the card |
|
|
68
|
-
| edit | | Editing mode active |
|
|
69
|
-
| toggleActivation | | Function for switching the status |
|
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
(() => {
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const VERSION = "7.10.4";
|
|
5
|
-
const HIDDEN = "data-eos-role-hidden";
|
|
6
|
-
const READONLY = "data-eos-readonly";
|
|
7
|
-
const state = { role: "unknown", backupActive: null, user: "", appliedAt: 0 };
|
|
8
|
-
|
|
9
|
-
const adminOnlyTokens = [
|
|
10
|
-
"app-center", "app center", "appcenter", "lizenz", "license", "simulation", "simulator",
|
|
11
|
-
"expertenmodus", "expert mode", "xterm", "shell-konsole", "shell console", "zugänge & rechte",
|
|
12
|
-
"zugaenge & rechte", "access & rights", "user management", "benutzerverwaltung"
|
|
13
|
-
];
|
|
14
|
-
const backupTokens = ["sicherung", "backup", "backitup"];
|
|
15
|
-
const dataPointTokens = ["datenpunkte", "datapoints", "objects", "objekte"];
|
|
16
|
-
const dangerousWriteTokens = [
|
|
17
|
-
"wert schreiben", "wert setzen", "schreiben", "write value", "set value", "setstate", "speichern",
|
|
18
|
-
"save", "anwenden", "apply", "löschen", "delete", "bearbeiten", "edit", "stift"
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
const normalize = value => String(value == null ? "" : value).trim().toLowerCase();
|
|
22
|
-
const textOf = el => normalize([
|
|
23
|
-
el && el.textContent,
|
|
24
|
-
el && el.getAttribute && el.getAttribute("title"),
|
|
25
|
-
el && el.getAttribute && el.getAttribute("aria-label"),
|
|
26
|
-
el && el.getAttribute && el.getAttribute("data-testid"),
|
|
27
|
-
el && el.getAttribute && el.getAttribute("href"),
|
|
28
|
-
el && el.getAttribute && el.getAttribute("to"),
|
|
29
|
-
el && el.id,
|
|
30
|
-
el && el.className && typeof el.className === "string" ? el.className : ""
|
|
31
|
-
].filter(Boolean).join(" "));
|
|
32
|
-
|
|
33
|
-
function classify(value) {
|
|
34
|
-
const text = normalize(value);
|
|
35
|
-
if (!text) return "unknown";
|
|
36
|
-
if (/^(?:system\.user\.)?admin(?:istrator)?$/.test(text) || /(?:^|[.:/_ -])admin(?:istrator)?(?:$|[.:/_ -])/.test(text)) return "admin";
|
|
37
|
-
if (/installer|installateur|inbetriebnahme/.test(text)) return "installer";
|
|
38
|
-
if (/endkunde|customer|smarthome|guest|gast|(?:^|\s)user(?:$|\s)/.test(text)) return "customer";
|
|
39
|
-
return "unknown";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function roleFromGlobals() {
|
|
43
|
-
const candidates = [
|
|
44
|
-
window.__NEXOWATT_ROLE__, window.__EOS_ROLE__, window.eosRole,
|
|
45
|
-
window.currentUser && window.currentUser.role,
|
|
46
|
-
window.user && window.user.role,
|
|
47
|
-
document.documentElement.getAttribute("data-role"),
|
|
48
|
-
document.body && document.body.getAttribute("data-role")
|
|
49
|
-
];
|
|
50
|
-
for (const value of candidates) {
|
|
51
|
-
const role = classify(value);
|
|
52
|
-
if (role !== "unknown") return role;
|
|
53
|
-
}
|
|
54
|
-
return "unknown";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function roleFromStorage() {
|
|
58
|
-
const stores = [window.sessionStorage, window.localStorage];
|
|
59
|
-
const keys = ["eos.role", "eosRole", "role", "userRole", "nexowatt.role", "auth.role", "currentRole"];
|
|
60
|
-
for (const store of stores) {
|
|
61
|
-
try {
|
|
62
|
-
for (const key of keys) {
|
|
63
|
-
const role = classify(store.getItem(key));
|
|
64
|
-
if (role !== "unknown") return role;
|
|
65
|
-
}
|
|
66
|
-
} catch { /* storage can be disabled */ }
|
|
67
|
-
}
|
|
68
|
-
return "unknown";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function roleFromDom() {
|
|
72
|
-
const selectors = [
|
|
73
|
-
"[data-role]", "[data-user-role]", "[data-current-role]", "[aria-label*='Rolle' i]",
|
|
74
|
-
"[aria-label*='role' i]", "header", "nav", ".MuiAppBar-root"
|
|
75
|
-
];
|
|
76
|
-
for (const selector of selectors) {
|
|
77
|
-
for (const el of document.querySelectorAll(selector)) {
|
|
78
|
-
const value = [
|
|
79
|
-
el.getAttribute && el.getAttribute("data-role"),
|
|
80
|
-
el.getAttribute && el.getAttribute("data-user-role"),
|
|
81
|
-
el.getAttribute && el.getAttribute("data-current-role"),
|
|
82
|
-
el.getAttribute && el.getAttribute("aria-label"),
|
|
83
|
-
el.textContent
|
|
84
|
-
].filter(Boolean).join(" ");
|
|
85
|
-
const role = classify(value);
|
|
86
|
-
if (role !== "unknown") return role;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return "unknown";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function findDisplayedUser() {
|
|
93
|
-
const candidates = document.querySelectorAll("header *, nav *, .MuiAppBar-root *");
|
|
94
|
-
for (const el of candidates) {
|
|
95
|
-
const text = normalize(el.textContent);
|
|
96
|
-
if (!text || text.length > 80) continue;
|
|
97
|
-
if (/\b(admin|installer|user|guest|gast|endkunde)\b/.test(text)) return text;
|
|
98
|
-
}
|
|
99
|
-
return "";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function fetchSecurityStatus() {
|
|
103
|
-
try {
|
|
104
|
-
const user = encodeURIComponent(findDisplayedUser());
|
|
105
|
-
const response = await fetch(`/api/nexowatt-eos/security/status?user=${user}&v=${VERSION}`, {
|
|
106
|
-
credentials: "same-origin", cache: "no-store", headers: { "Accept": "application/json" }
|
|
107
|
-
});
|
|
108
|
-
if (!response.ok) return null;
|
|
109
|
-
return await response.json();
|
|
110
|
-
} catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function isAdmin() { return state.role === "admin"; }
|
|
116
|
-
function isCustomer() { return state.role === "customer"; }
|
|
117
|
-
|
|
118
|
-
function hide(el, reason) {
|
|
119
|
-
if (!el || el === document.body || el === document.documentElement) return;
|
|
120
|
-
if (!el.hasAttribute(HIDDEN)) {
|
|
121
|
-
el.setAttribute(HIDDEN, reason || "restricted");
|
|
122
|
-
el.setAttribute("aria-hidden", "true");
|
|
123
|
-
el.style.setProperty("display", "none", "important");
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function unhide(el) {
|
|
128
|
-
if (!el || !el.hasAttribute(HIDDEN)) return;
|
|
129
|
-
el.removeAttribute(HIDDEN);
|
|
130
|
-
el.removeAttribute("aria-hidden");
|
|
131
|
-
el.style.removeProperty("display");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function closestAction(el) {
|
|
135
|
-
return el && el.closest && el.closest("a,button,[role='button'],[role='tab'],li,.MuiButtonBase-root") || el;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function includesAny(text, tokens) {
|
|
139
|
-
return tokens.some(token => text.includes(token));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function applyNavigationPolicy() {
|
|
143
|
-
const actions = document.querySelectorAll("a,button,[role='button'],[role='tab'],li,.MuiButtonBase-root");
|
|
144
|
-
for (const raw of actions) {
|
|
145
|
-
const el = closestAction(raw);
|
|
146
|
-
const text = textOf(el);
|
|
147
|
-
if (!text) continue;
|
|
148
|
-
|
|
149
|
-
const adminOnly = includesAny(text, adminOnlyTokens) ||
|
|
150
|
-
(/\badmin\b/.test(text) && /(?:8081|adapter=admin|io.?broker)/.test(text));
|
|
151
|
-
const backup = includesAny(text, backupTokens);
|
|
152
|
-
|
|
153
|
-
if (!isAdmin() && adminOnly) hide(el, "admin-only");
|
|
154
|
-
else if (adminOnly) unhide(el);
|
|
155
|
-
|
|
156
|
-
if (backup && state.backupActive === false) hide(el, "backup-inactive");
|
|
157
|
-
else if (backup && state.backupActive === true && (isAdmin() || !adminOnly)) unhide(el);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (!isAdmin()) {
|
|
161
|
-
for (const el of document.querySelectorAll("[data-testid*='expert' i],[class*='expert' i],[id*='expert' i],[title*='Experten' i],[aria-label*='Experten' i],[title*='Expert' i],[aria-label*='Expert' i]")) {
|
|
162
|
-
hide(closestAction(el), "admin-only-expert");
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function isDataPointPage() {
|
|
168
|
-
const route = normalize(location.pathname + " " + location.hash + " " + document.title);
|
|
169
|
-
if (includesAny(route, dataPointTokens)) return true;
|
|
170
|
-
const selected = document.querySelector("[aria-selected='true'],.Mui-selected,.active");
|
|
171
|
-
return selected ? includesAny(textOf(selected), dataPointTokens) : false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function makeDatapointsReadOnly() {
|
|
175
|
-
const root = document.querySelector("main") || document.body;
|
|
176
|
-
if (!isCustomer() || !isDataPointPage()) {
|
|
177
|
-
root && root.removeAttribute(READONLY);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
root.setAttribute(READONLY, "true");
|
|
181
|
-
for (const el of root.querySelectorAll("input,textarea,select,[contenteditable='true'],button,[role='button']")) {
|
|
182
|
-
const text = textOf(el);
|
|
183
|
-
const isSearch = /filter|suche|search|refresh|aktualisieren|reload|sort|spalte|column|zurück|back/.test(text) ||
|
|
184
|
-
/search/i.test(el.getAttribute && el.getAttribute("type") || "") ||
|
|
185
|
-
/filter|search/i.test(el.getAttribute && el.getAttribute("placeholder") || "");
|
|
186
|
-
const likelyWriter = includesAny(text, dangerousWriteTokens) ||
|
|
187
|
-
/value|wert|write|edit|save|delete|setstate|state-value/i.test(text) ||
|
|
188
|
-
(el.matches("input,textarea,select,[contenteditable='true']") && !isSearch);
|
|
189
|
-
if (likelyWriter && !isSearch) {
|
|
190
|
-
el.setAttribute("aria-disabled", "true");
|
|
191
|
-
el.setAttribute("tabindex", "-1");
|
|
192
|
-
if ("disabled" in el) el.disabled = true;
|
|
193
|
-
if (el.getAttribute("contenteditable") === "true") el.setAttribute("contenteditable", "false");
|
|
194
|
-
el.style.setProperty("pointer-events", "none", "important");
|
|
195
|
-
el.style.setProperty("opacity", "0.62", "important");
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function currentRouteRestricted() {
|
|
201
|
-
const route = normalize(location.pathname + " " + location.search + " " + location.hash);
|
|
202
|
-
return includesAny(route, adminOnlyTokens);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function renderForbidden() {
|
|
206
|
-
if (document.getElementById("eos-role-forbidden")) return;
|
|
207
|
-
const overlay = document.createElement("div");
|
|
208
|
-
overlay.id = "eos-role-forbidden";
|
|
209
|
-
overlay.innerHTML = `<div class="eos-role-forbidden-card"><strong>Zugriff gesperrt</strong><span>App-Center, Lizenz, Simulation und Expertenmodus sind ausschließlich für Administratoren freigegeben.</span><button type="button">Zurück zum Cockpit</button></div>`;
|
|
210
|
-
overlay.querySelector("button").addEventListener("click", () => {
|
|
211
|
-
location.hash = "#/";
|
|
212
|
-
try { history.replaceState(null, "", location.pathname); } catch { /* ignore */ }
|
|
213
|
-
overlay.remove();
|
|
214
|
-
});
|
|
215
|
-
document.body.appendChild(overlay);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function guardCurrentRoute() {
|
|
219
|
-
if (!isAdmin() && currentRouteRestricted()) renderForbidden();
|
|
220
|
-
else document.getElementById("eos-role-forbidden")?.remove();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function clearExpertMode() {
|
|
224
|
-
if (isAdmin()) return;
|
|
225
|
-
const keys = ["expertMode", "expert-mode", "eos.expert", "eosExpertMode", "adminExpertMode"];
|
|
226
|
-
for (const store of [window.localStorage, window.sessionStorage]) {
|
|
227
|
-
try { keys.forEach(key => store.removeItem(key)); } catch { /* ignore */ }
|
|
228
|
-
}
|
|
229
|
-
window.__NEXOWATT_EXPERT_MODE__ = false;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function scrubPrivilegedUiSessions() {
|
|
233
|
-
if (isAdmin()) return;
|
|
234
|
-
const marker = "eos.nonAdminUiLogout.v7104";
|
|
235
|
-
try {
|
|
236
|
-
if (sessionStorage.getItem(marker)) return;
|
|
237
|
-
sessionStorage.setItem(marker, "1");
|
|
238
|
-
} catch { /* continue */ }
|
|
239
|
-
const protocol = location.protocol === "https:" ? "https:" : "http:";
|
|
240
|
-
const host = location.hostname;
|
|
241
|
-
for (const port of [8082, 8188]) {
|
|
242
|
-
const iframe = document.createElement("iframe");
|
|
243
|
-
iframe.hidden = true;
|
|
244
|
-
iframe.setAttribute("aria-hidden", "true");
|
|
245
|
-
iframe.src = `${protocol}//${host}:${port}/logout?source=eos-role-security&v=${VERSION}`;
|
|
246
|
-
document.body.appendChild(iframe);
|
|
247
|
-
setTimeout(() => iframe.remove(), 12000);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function apply() {
|
|
252
|
-
state.appliedAt = Date.now();
|
|
253
|
-
document.documentElement.setAttribute("data-eos-effective-role", state.role);
|
|
254
|
-
applyNavigationPolicy();
|
|
255
|
-
makeDatapointsReadOnly();
|
|
256
|
-
clearExpertMode();
|
|
257
|
-
guardCurrentRoute();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function installEventGuards() {
|
|
261
|
-
document.addEventListener("click", event => {
|
|
262
|
-
if (isAdmin()) return;
|
|
263
|
-
const action = closestAction(event.target);
|
|
264
|
-
const text = textOf(action);
|
|
265
|
-
if (includesAny(text, adminOnlyTokens) || (/\badmin\b/.test(text) && /(?:8081|adapter=admin|io.?broker)/.test(text))) {
|
|
266
|
-
event.preventDefault(); event.stopImmediatePropagation(); renderForbidden(); return;
|
|
267
|
-
}
|
|
268
|
-
if (isCustomer() && isDataPointPage() && includesAny(text, dangerousWriteTokens)) {
|
|
269
|
-
event.preventDefault(); event.stopImmediatePropagation(); return;
|
|
270
|
-
}
|
|
271
|
-
}, true);
|
|
272
|
-
document.addEventListener("submit", event => {
|
|
273
|
-
if (isCustomer() && isDataPointPage()) { event.preventDefault(); event.stopImmediatePropagation(); }
|
|
274
|
-
}, true);
|
|
275
|
-
for (const name of ["pushState", "replaceState"]) {
|
|
276
|
-
const original = history[name];
|
|
277
|
-
history[name] = function(...args) {
|
|
278
|
-
const result = original.apply(this, args);
|
|
279
|
-
queueMicrotask(apply);
|
|
280
|
-
return result;
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
addEventListener("hashchange", apply);
|
|
284
|
-
addEventListener("popstate", apply);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function injectStyles() {
|
|
288
|
-
if (document.getElementById("eos-role-security-style")) return;
|
|
289
|
-
const style = document.createElement("style");
|
|
290
|
-
style.id = "eos-role-security-style";
|
|
291
|
-
style.textContent = `
|
|
292
|
-
[${HIDDEN}] { display:none !important; }
|
|
293
|
-
#eos-role-forbidden { position:fixed; inset:0; z-index:2147483647; display:grid; place-items:center; padding:24px; background:rgba(1,17,28,.96); color:#eefaff; font-family:Arial,sans-serif; }
|
|
294
|
-
.eos-role-forbidden-card { width:min(620px,100%); display:grid; gap:18px; padding:30px; border:1px solid #16d49d; border-radius:18px; background:#072333; box-shadow:0 20px 80px rgba(0,0,0,.45); }
|
|
295
|
-
.eos-role-forbidden-card strong { color:#24e6ad; font-size:28px; }
|
|
296
|
-
.eos-role-forbidden-card span { line-height:1.55; }
|
|
297
|
-
.eos-role-forbidden-card button { justify-self:start; padding:10px 18px; border:1px solid #16d49d; border-radius:10px; color:#021911; background:#24e6ad; font-weight:700; cursor:pointer; }
|
|
298
|
-
[${READONLY}="true"]::before { content:"Nur-Lesezugriff für Endkunden"; position:sticky; top:0; z-index:10; display:block; margin:8px; padding:8px 12px; border:1px solid #1aa57d; border-radius:9px; background:#07342b; color:#cffff0; font:600 13px Arial,sans-serif; }
|
|
299
|
-
`;
|
|
300
|
-
document.head.appendChild(style);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async function init() {
|
|
304
|
-
injectStyles();
|
|
305
|
-
let role = roleFromGlobals();
|
|
306
|
-
if (role === "unknown") role = roleFromStorage();
|
|
307
|
-
if (role === "unknown") role = roleFromDom();
|
|
308
|
-
const status = await fetchSecurityStatus();
|
|
309
|
-
if (status) {
|
|
310
|
-
const serverRole = classify(status.role);
|
|
311
|
-
if (serverRole !== "unknown") role = serverRole;
|
|
312
|
-
if (typeof status.backupActive === "boolean") state.backupActive = status.backupActive;
|
|
313
|
-
state.user = status.user || "";
|
|
314
|
-
}
|
|
315
|
-
if (role === "unknown") role = "customer"; // least privilege until authenticated role is known
|
|
316
|
-
state.role = role;
|
|
317
|
-
try { sessionStorage.setItem("eos.role.effective", role); } catch { /* ignore */ }
|
|
318
|
-
installEventGuards();
|
|
319
|
-
apply();
|
|
320
|
-
scrubPrivilegedUiSessions();
|
|
321
|
-
const observer = new MutationObserver(() => {
|
|
322
|
-
clearTimeout(observer._timer);
|
|
323
|
-
observer._timer = setTimeout(apply, 40);
|
|
324
|
-
});
|
|
325
|
-
observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "aria-selected", "href"] });
|
|
326
|
-
setInterval(async () => {
|
|
327
|
-
const refreshed = await fetchSecurityStatus();
|
|
328
|
-
if (refreshed && typeof refreshed.backupActive === "boolean" && refreshed.backupActive !== state.backupActive) {
|
|
329
|
-
state.backupActive = refreshed.backupActive; apply();
|
|
330
|
-
}
|
|
331
|
-
}, 10000);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init, { once: true });
|
|
335
|
-
else void init();
|
|
336
|
-
})();
|