iobroker.eos-admin 7.9.23 → 7.9.26

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.
@@ -0,0 +1,212 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ const VERSION = 'v26-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,wEAAwE;AACxE,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"}
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"}
package/build/lib/web.js CHANGED
@@ -341,6 +341,159 @@ class Web {
341
341
  resetIndexHtml() {
342
342
  this.indexHTML = '';
343
343
  }
344
+
345
+ normalizeEosGroupId(value) {
346
+ if (typeof value !== 'string') return null;
347
+ let group = value.trim();
348
+ if (!group) return null;
349
+ if (!group.startsWith('system.group.')) group = `system.group.${group.replace(/^group\./, '')}`;
350
+ return /^system\.group\.[a-z0-9_-]+$/i.test(group) ? group : null;
351
+ }
352
+ normalizeEosUserId(value) {
353
+ if (!value) return null;
354
+ let user;
355
+ if (typeof value === 'string') user = value;
356
+ else if (typeof value === 'object') {
357
+ const raw = value;
358
+ user = String(raw.id || raw._id || raw.user || raw.name || '');
359
+ }
360
+ else user = String(value);
361
+ user = user.trim();
362
+ if (!user) return null;
363
+ if (!user.startsWith('system.user.')) user = `system.user.${user.replace(/^user\./, '')}`;
364
+ return /^system\.user\.[a-z0-9_-]+$/i.test(user) ? user : null;
365
+ }
366
+ normalizeEosAdapterName(value) {
367
+ if (typeof value !== 'string') return null;
368
+ let adapter = value.trim().toLowerCase();
369
+ if (!adapter) return null;
370
+ adapter = adapter
371
+ .replace(/^system\.adapter\./, '')
372
+ .replace(/^iobroker\./, '')
373
+ .replace(/^@nexowatt\/iobroker\./, '')
374
+ .replace(/^@nexowatt\//, '')
375
+ .replace(/\.\d+$/, '');
376
+ return /^[a-z0-9_-]+$/.test(adapter) ? adapter : null;
377
+ }
378
+ getEosSecurityAdminGroups() {
379
+ const configured = this.settings.eosAdminOnlyGroups || this.settings.eosSecurityAdminGroups || this.settings.eosAdminOnlyGroup;
380
+ const groups = new Set();
381
+ const add = value => {
382
+ if (!value) return;
383
+ if (Array.isArray(value)) {
384
+ value.forEach(add);
385
+ return;
386
+ }
387
+ if (typeof value === 'object') {
388
+ if (value.enabled === false) return;
389
+ add(value.group || value.id || value.name);
390
+ return;
391
+ }
392
+ const group = this.normalizeEosGroupId(String(value));
393
+ if (group) groups.add(group);
394
+ };
395
+ add(configured);
396
+ groups.add('system.group.administrator');
397
+ return [...groups].sort();
398
+ }
399
+ getEosProtectedAdapterNames() {
400
+ const names = new Set(['eos-admin']);
401
+ const add = value => {
402
+ if (!value) return;
403
+ if (Array.isArray(value)) {
404
+ value.forEach(add);
405
+ return;
406
+ }
407
+ if (typeof value === 'object') {
408
+ if (value.enabled === false) return;
409
+ add(value.adapter || value.name);
410
+ return;
411
+ }
412
+ const adapter = this.normalizeEosAdapterName(String(value));
413
+ if (adapter) names.add(adapter);
414
+ };
415
+ if (this.settings.eosProtectAdapterDeletion !== false) add(this.settings.eosProtectedAdapters);
416
+ return [...names].sort();
417
+ }
418
+ async getEosGroupsForUser(userId) {
419
+ const groups = new Set();
420
+ try {
421
+ const result = await this.adapter.getObjectViewAsync('system', 'group', {
422
+ startkey: 'system.group.',
423
+ endkey: 'system.group.香',
424
+ });
425
+ for (const row of result.rows) {
426
+ const group = row.value || row.doc;
427
+ const members = Array.isArray(group?.common?.members) ? group.common.members : [];
428
+ if (members.includes(userId)) groups.add(row.id);
429
+ }
430
+ }
431
+ catch (e) {
432
+ this.adapter.log.debug(`Cannot resolve EOS security groups for ${userId}: ${e instanceof Error ? e.message : e}`);
433
+ }
434
+ return [...groups].sort();
435
+ }
436
+ readAccessTokenFromRequest(req) {
437
+ const cookieHeader = req.headers.cookie || '';
438
+ const accessCookie = cookieHeader
439
+ .split(';')
440
+ .map(cookie => cookie.trim())
441
+ .find(cookie => cookie.startsWith('access_token='));
442
+ let token = accessCookie ? accessCookie.split('=').slice(1).join('=') : '';
443
+ if (!token && req.headers.authorization?.startsWith('Bearer ')) token = req.headers.authorization.substring('Bearer '.length);
444
+ if (!token && req.query?.token) token = req.query.token;
445
+ if (!token) return null;
446
+ try {
447
+ return decodeURIComponent(token);
448
+ }
449
+ catch {
450
+ return token;
451
+ }
452
+ }
453
+ readSession(id) {
454
+ return new Promise(resolve => this.adapter.getSession(id, token => resolve(token)));
455
+ }
456
+ async readEosCurrentUser(req) {
457
+ const fromRequest = this.normalizeEosUserId(req.user);
458
+ if (fromRequest) return fromRequest;
459
+ if (!this.settings.auth) return this.normalizeEosUserId(this.settings.defaultUser) || 'system.user.admin';
460
+ const token = this.readAccessTokenFromRequest(req);
461
+ if (!token) return null;
462
+ const candidates = new Set();
463
+ candidates.add(token);
464
+ candidates.add(token.startsWith('a:') ? token : `a:${token}`);
465
+ if (token.length > 1) candidates.add(`a:${token[1]}`);
466
+ for (const id of candidates) {
467
+ const session = await this.readSession(id).catch(() => null);
468
+ const user = this.normalizeEosUserId(session?.user);
469
+ if (user) return user;
470
+ }
471
+ return null;
472
+ }
473
+ async sendEosSecuritySession(req, res) {
474
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
475
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
476
+ const userId = await this.readEosCurrentUser(req);
477
+ const groups = userId ? await this.getEosGroupsForUser(userId) : [];
478
+ const adminGroups = this.getEosSecurityAdminGroups();
479
+ const isAdministrator = userId === 'system.user.admin' || groups.includes('system.group.administrator');
480
+ const isEosAdminGroup = isAdministrator || groups.some(group => adminGroups.includes(group));
481
+ res.status(200).json({
482
+ user: userId,
483
+ groups,
484
+ adminGroups,
485
+ isAdministrator,
486
+ isEosAdminGroup,
487
+ isAdmin: isEosAdminGroup,
488
+ hideLegacyAdminForNonAdmins: this.settings.eosHideLegacyAdminForNonAdmins !== false && this.settings.eosHideLegacyAdminFromNonAdmins !== false,
489
+ hideLegacyAdminFromNonAdmins: this.settings.eosHideLegacyAdminForNonAdmins !== false && this.settings.eosHideLegacyAdminFromNonAdmins !== false,
490
+ restrictProtectedAdapterControls: this.settings.eosRestrictProtectedAdapterControls !== false && this.settings.eosApplyAdminOnlyAclToProtectedAdapters !== false,
491
+ legacyAdminAdapter: 'admin',
492
+ legacyAdminInstance: 'admin.0',
493
+ protectedAdapters: this.getEosProtectedAdapterNames(),
494
+ });
495
+ }
496
+
344
497
  /**
345
498
  * Initialize the server
346
499
  */
@@ -462,6 +615,24 @@ class Web {
462
615
  }
463
616
  res.json({ error: 'Cannot find session' });
464
617
  });
618
+ this.server.app.get(/.*\/nexowatt\/security\/(?:context|session)$/, (req, res) => {
619
+ void this.sendEosSecuritySession(req, res).catch(e => {
620
+ this.adapter.log.warn(`Cannot create NexoWatt security context: ${e instanceof Error ? e.message : e}`);
621
+ res.status(200).json({
622
+ user: null,
623
+ groups: [],
624
+ adminGroups: ['system.group.administrator'],
625
+ isAdministrator: false,
626
+ isEosAdminGroup: false,
627
+ isAdmin: false,
628
+ hideLegacyAdminForNonAdmins: true,
629
+ hideLegacyAdminFromNonAdmins: true,
630
+ restrictProtectedAdapterControls: true,
631
+ protectedAdapters: ['eos-admin'],
632
+ });
633
+ });
634
+ });
635
+
465
636
  this.server.app.get('/logout', (req, res) => {
466
637
  const isDev = req.url.includes('?dev');
467
638
  let origin = req.url.split('origin=')[1];
@@ -536,6 +707,27 @@ class Web {
536
707
  else {
537
708
  this.server.app.get('/logout', (_req, res) => res.redirect('/'));
538
709
  }
710
+ const sendSecuritySession = (req, res) => {
711
+ void this.sendEosSecuritySession(req, res).catch(e => {
712
+ this.adapter.log.warn(`Cannot read EOS security session: ${e instanceof Error ? e.message : e}`);
713
+ res.status(200).json({
714
+ user: null,
715
+ groups: [],
716
+ adminGroups: ['system.group.administrator'],
717
+ isAdministrator: false,
718
+ isEosAdminGroup: false,
719
+ isAdmin: false,
720
+ hideLegacyAdminForNonAdmins: true,
721
+ hideLegacyAdminFromNonAdmins: true,
722
+ restrictProtectedAdapterControls: true,
723
+ protectedAdapters: ['eos-admin'],
724
+ });
725
+ });
726
+ };
727
+ this.server.app.get('/eos/security/status', sendSecuritySession);
728
+ this.server.app.get('/nexowatt/security/session', sendSecuritySession);
729
+ this.server.app.get('/nexowatt/security/context', sendSecuritySession);
730
+
539
731
  this.server.app.get('/iobroker_check.html', (_req, res) => {
540
732
  res.status(200).send('ioBroker');
541
733
  });
@@ -1020,34 +1212,20 @@ class Web {
1020
1212
  }
1021
1213
  catch (err) {
1022
1214
  this.adapter.log.error(`Cannot create web-server: ${err}`);
1023
- if (this.adapter.terminate) {
1024
- this.adapter.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
1025
- }
1026
- else {
1027
- process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
1028
- }
1215
+ this.adapter.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
1029
1216
  return;
1030
1217
  }
1031
1218
  if (!this.server.server) {
1032
1219
  this.adapter.log.error(`Cannot create web-server`);
1033
- if (this.adapter.terminate) {
1034
- this.adapter.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
1035
- }
1036
- else {
1037
- process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
1038
- }
1220
+ this.adapter.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
1039
1221
  return;
1040
1222
  }
1041
1223
  this.server.server.__server = this.server;
1042
1224
  }
1043
1225
  else {
1044
1226
  this.adapter.log.error('port missing');
1045
- if (this.adapter.terminate) {
1046
- this.adapter.terminate('port missing', adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
1047
- }
1048
- else {
1049
- process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
1050
- }
1227
+ this.adapter.terminate('port missing', adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
1228
+ return;
1051
1229
  }
1052
1230
  const systemConfig = await this.adapter.getForeignObjectAsync('system.config');
1053
1231
  this.systemConfig = systemConfig || {};