iobroker.mywebui 1.37.34 → 1.37.35
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/io-package.json
CHANGED
package/package.json
CHANGED
|
@@ -424,6 +424,59 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
|
|
|
424
424
|
const bindRow = document.createElement('div');
|
|
425
425
|
bindRow.style.cssText = 'display:flex;align-items:center;gap:6px;padding:4px 0;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid #ddd;';
|
|
426
426
|
|
|
427
|
+
// □ button on the LEFT (like Properties panel style)
|
|
428
|
+
const bindBtn = document.createElement('button');
|
|
429
|
+
bindBtn.textContent = '□';
|
|
430
|
+
bindBtn.title = 'Right-click for binding options';
|
|
431
|
+
bindBtn.style.cssText = existingBinding
|
|
432
|
+
? 'width:16px;height:16px;padding:0;font-size:11px;line-height:1;border:1px solid #888;background:#ffd700;cursor:pointer;flex-shrink:0;'
|
|
433
|
+
: 'width:16px;height:16px;padding:0;font-size:11px;line-height:1;border:1px solid #888;background:#f0f0f0;cursor:pointer;flex-shrink:0;';
|
|
434
|
+
|
|
435
|
+
// Left-click: open binding editor
|
|
436
|
+
bindBtn.onclick = () => {
|
|
437
|
+
const property = { name: 'hidden', propertyType: 'propertyAndAttribute' };
|
|
438
|
+
serviceContainer.config.openBindingsEditor(property, [designItem], existingBinding, 'property');
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// Right-click: context menu (like Properties panel)
|
|
442
|
+
bindBtn.oncontextmenu = (e) => {
|
|
443
|
+
e.preventDefault();
|
|
444
|
+
const existing = document.getElementById('__vis-ctx-menu');
|
|
445
|
+
if (existing) existing.remove();
|
|
446
|
+
|
|
447
|
+
const menu = document.createElement('div');
|
|
448
|
+
menu.id = '__vis-ctx-menu';
|
|
449
|
+
menu.style.cssText = `position:fixed;left:${e.clientX}px;top:${e.clientY}px;background:#2d2d2d;color:#ddd;
|
|
450
|
+
font-size:12px;border:1px solid #555;border-radius:3px;z-index:99999;min-width:130px;box-shadow:2px 2px 6px rgba(0,0,0,0.5);`;
|
|
451
|
+
|
|
452
|
+
const addItem = (text, cb, disabled = false) => {
|
|
453
|
+
const item = document.createElement('div');
|
|
454
|
+
item.textContent = text;
|
|
455
|
+
item.style.cssText = `padding:6px 12px;cursor:${disabled ? 'default' : 'pointer'};color:${disabled ? '#666' : '#ddd'};`;
|
|
456
|
+
if (!disabled) {
|
|
457
|
+
item.onmouseenter = () => item.style.background = '#3e6db4';
|
|
458
|
+
item.onmouseleave = () => item.style.background = '';
|
|
459
|
+
item.onclick = () => { menu.remove(); cb(); };
|
|
460
|
+
}
|
|
461
|
+
menu.appendChild(item);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
addItem('edit binding', () => {
|
|
465
|
+
const property = { name: 'hidden', propertyType: 'propertyAndAttribute' };
|
|
466
|
+
serviceContainer.config.openBindingsEditor(property, [designItem], existingBinding, 'property');
|
|
467
|
+
});
|
|
468
|
+
addItem('clear binding', () => {
|
|
469
|
+
designItem.removeAttribute('bind-prop:hidden');
|
|
470
|
+
this._updateVisibilityPanel();
|
|
471
|
+
}, !existingBinding);
|
|
472
|
+
|
|
473
|
+
document.body.appendChild(menu);
|
|
474
|
+
const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('mousedown', close); } };
|
|
475
|
+
setTimeout(() => document.addEventListener('mousedown', close), 0);
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
bindRow.appendChild(bindBtn);
|
|
479
|
+
|
|
427
480
|
const bindLabel = document.createElement('span');
|
|
428
481
|
bindLabel.textContent = 'Visibility';
|
|
429
482
|
bindLabel.style.cssText = 'font-size:12px;font-weight:600;flex:1;';
|
|
@@ -434,34 +487,10 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
|
|
|
434
487
|
const statusSpan = document.createElement('span');
|
|
435
488
|
statusSpan.textContent = sig;
|
|
436
489
|
statusSpan.title = JSON.stringify(existingBinding);
|
|
437
|
-
statusSpan.style.cssText = 'font-size:10px;color:#
|
|
490
|
+
statusSpan.style.cssText = 'font-size:10px;color:#aaa;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right;';
|
|
438
491
|
bindRow.appendChild(statusSpan);
|
|
439
492
|
}
|
|
440
493
|
|
|
441
|
-
const bindBtn = document.createElement('button');
|
|
442
|
-
bindBtn.textContent = '□';
|
|
443
|
-
bindBtn.title = 'Open binding editor';
|
|
444
|
-
bindBtn.style.cssText = existingBinding
|
|
445
|
-
? 'width:20px;height:20px;padding:0;font-size:13px;line-height:1;border:1px solid #888;background:#ff0;cursor:pointer;flex-shrink:0;'
|
|
446
|
-
: 'width:20px;height:20px;padding:0;font-size:13px;line-height:1;border:1px solid #888;background:#f0f0f0;cursor:pointer;flex-shrink:0;';
|
|
447
|
-
bindBtn.onclick = () => {
|
|
448
|
-
const property = { name: 'hidden', propertyType: 'propertyAndAttribute' };
|
|
449
|
-
serviceContainer.config.openBindingsEditor(property, [designItem], existingBinding, 'property');
|
|
450
|
-
};
|
|
451
|
-
bindRow.appendChild(bindBtn);
|
|
452
|
-
|
|
453
|
-
if (existingBinding) {
|
|
454
|
-
const clearBtn = document.createElement('button');
|
|
455
|
-
clearBtn.textContent = '✕';
|
|
456
|
-
clearBtn.title = 'Remove binding';
|
|
457
|
-
clearBtn.style.cssText = 'width:20px;height:20px;padding:0;font-size:11px;line-height:1;border:1px solid #c66;background:#fee;cursor:pointer;color:#c00;flex-shrink:0;';
|
|
458
|
-
clearBtn.onclick = () => {
|
|
459
|
-
designItem.removeAttribute('bind-prop:hidden');
|
|
460
|
-
this._updateVisibilityPanel();
|
|
461
|
-
};
|
|
462
|
-
bindRow.appendChild(clearBtn);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
494
|
content.appendChild(bindRow);
|
|
466
495
|
|
|
467
496
|
// --- GROUP ACCESS CONTROL ---
|
|
@@ -1,271 +1,65 @@
|
|
|
1
1
|
import { iobrokerHandler } from '../common/IobrokerHandler.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Visibility Service - Handles element visibility based on user groups and conditions
|
|
5
|
-
* Provides protection against browser manipulation
|
|
6
|
-
*/
|
|
7
3
|
class VisibilityService {
|
|
8
4
|
static instance = new VisibilityService();
|
|
9
|
-
|
|
10
|
-
#intervalChecks = new Map();
|
|
11
|
-
#originalDisplayStyles = new WeakMap();
|
|
12
|
-
#configs = new WeakMap();
|
|
13
|
-
#tampering = false;
|
|
14
|
-
|
|
15
|
-
constructor() {
|
|
16
|
-
this.#setupAntiTampering();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Setup anti-tampering detection
|
|
21
|
-
*/
|
|
22
|
-
#setupAntiTampering() {
|
|
23
|
-
// Detect DevTools opening
|
|
24
|
-
const detectDevTools = () => {
|
|
25
|
-
const threshold = 160;
|
|
26
|
-
const widthThreshold = window.outerWidth - window.innerWidth > threshold;
|
|
27
|
-
const heightThreshold = window.outerHeight - window.innerHeight > threshold;
|
|
28
|
-
|
|
29
|
-
if (widthThreshold || heightThreshold) {
|
|
30
|
-
if (!this.#tampering) {
|
|
31
|
-
this.#tampering = true;
|
|
32
|
-
console.warn('⚠️ [Visibility] Developer tools detected - visibility checks will be re-validated');
|
|
33
|
-
this.#revalidateAll();
|
|
34
|
-
}
|
|
35
|
-
} else {
|
|
36
|
-
this.#tampering = false;
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
setInterval(detectDevTools, 1000);
|
|
41
|
-
|
|
42
|
-
// Detect element.style modifications
|
|
43
|
-
this.#setupMutationObserver();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Setup mutation observer to detect tampering
|
|
48
|
-
*/
|
|
49
|
-
#setupMutationObserver() {
|
|
50
|
-
const observer = new MutationObserver((mutations) => {
|
|
51
|
-
for (const mutation of mutations) {
|
|
52
|
-
const element = mutation.target;
|
|
53
|
-
|
|
54
|
-
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
|
|
55
|
-
if (element.hasAttribute('data-visibility-controlled')) {
|
|
56
|
-
console.warn('🚨 [Visibility] Style tampered:', element);
|
|
57
|
-
this.#revalidateElement(element);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
5
|
|
|
61
|
-
|
|
62
|
-
if (mutation.type === 'attributes' && mutation.attributeName === 'data-visibility-controlled') {
|
|
63
|
-
if (!element.hasAttribute('data-visibility-controlled') && this.#configs.has(element)) {
|
|
64
|
-
console.warn('🚨 [Visibility] Controlled attribute removed — restoring');
|
|
65
|
-
element.setAttribute('data-visibility-controlled', 'true');
|
|
66
|
-
this.#revalidateElement(element);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
observer.observe(document.body, {
|
|
73
|
-
attributes: true,
|
|
74
|
-
subtree: true,
|
|
75
|
-
attributeFilter: ['style', 'data-visibility-controlled']
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Apply visibility rules to an element
|
|
81
|
-
* @param {HTMLElement} element
|
|
82
|
-
* @param {object} visibilityConfig
|
|
83
|
-
*/
|
|
84
|
-
async applyVisibility(element, visibilityConfig) {
|
|
85
|
-
if (!visibilityConfig || !visibilityConfig.enabled) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
console.log('🔍 [Visibility] Applying visibility rules to:', element.tagName, visibilityConfig);
|
|
90
|
-
|
|
91
|
-
// Store original display style
|
|
92
|
-
if (!this.#originalDisplayStyles.has(element)) {
|
|
93
|
-
this.#originalDisplayStyles.set(element, element.style.display || '');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Config-i memory-də saxla — DOM-da sensitiv məlumat saxlama
|
|
97
|
-
this.#configs.set(element, visibilityConfig);
|
|
98
|
-
|
|
99
|
-
// Yalnız "izlənilir" işarəsi
|
|
100
|
-
element.setAttribute('data-visibility-controlled', 'true');
|
|
6
|
+
#originalDisplayStyles = new WeakMap();
|
|
101
7
|
|
|
102
|
-
// Initial check
|
|
103
|
-
await this.#checkAndApply(element, visibilityConfig);
|
|
104
|
-
|
|
105
|
-
// Subscribe to datapoint changes if objectId specified
|
|
106
|
-
if (visibilityConfig.objectId) {
|
|
107
|
-
console.log('📡 [Visibility] Subscribing to state changes:', visibilityConfig.objectId);
|
|
108
|
-
await iobrokerHandler.subscribeState(visibilityConfig.objectId, (id, state) => {
|
|
109
|
-
console.log('🔔 [Visibility] State changed:', id, 'new value:', state?.val);
|
|
110
|
-
this.#checkAndApply(element, visibilityConfig);
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Periodic re-validation (protection against tampering)
|
|
115
|
-
const intervalId = setInterval(() => {
|
|
116
|
-
this.#checkAndApply(element, visibilityConfig);
|
|
117
|
-
}, 30000); // Check every 30 seconds (subscription handles real-time updates)
|
|
118
|
-
|
|
119
|
-
this.#intervalChecks.set(element, intervalId);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Check visibility and apply
|
|
124
|
-
*/
|
|
125
8
|
async #checkAndApply(element, visibilityConfig) {
|
|
126
9
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (visibilityConfig.groups && visibilityConfig.groups.length > 0) {
|
|
130
|
-
// Qrup yoxlaması → backend-ə sendTo: qrupları backend DB-dən oxuyur, frontend-ə inanmır
|
|
131
|
-
const username = await iobrokerHandler.connection.getCurrentUser();
|
|
132
|
-
if (username) {
|
|
133
|
-
result = await iobrokerHandler.connection.sendTo(
|
|
134
|
-
'mywebui.0',
|
|
135
|
-
'checkVisibility',
|
|
136
|
-
{ username, allowedGroups: visibilityConfig.groups }
|
|
137
|
-
);
|
|
138
|
-
result = result ?? { visible: false };
|
|
139
|
-
} else {
|
|
140
|
-
// User alınmadı → fallback
|
|
141
|
-
result = await iobrokerHandler.checkVisibility(visibilityConfig);
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
// Qrup yoxlaması yoxdur → datapoint condition yoxlaması (frontend)
|
|
145
|
-
result = await iobrokerHandler.checkVisibility(visibilityConfig);
|
|
146
|
-
}
|
|
10
|
+
const result = await iobrokerHandler.checkVisibility(visibilityConfig);
|
|
147
11
|
|
|
148
|
-
console.log('✅ [Visibility] Check result for', element.tagName, ':', { visible: result.visible, enabled: result.enabled });
|
|
149
|
-
|
|
150
|
-
// Apply visibility
|
|
151
12
|
if (!result.visible) {
|
|
152
13
|
element.style.display = 'none';
|
|
153
14
|
element.style.visibility = 'hidden';
|
|
154
|
-
element.setAttribute('aria-hidden', 'true');
|
|
155
15
|
} else {
|
|
156
|
-
const
|
|
157
|
-
element.style.display =
|
|
158
|
-
element.style.visibility = '
|
|
159
|
-
element.removeAttribute('aria-hidden');
|
|
16
|
+
const orig = this.#originalDisplayStyles.get(element) || '';
|
|
17
|
+
element.style.display = orig;
|
|
18
|
+
element.style.visibility = '';
|
|
160
19
|
}
|
|
161
|
-
|
|
162
|
-
// Apply enabled/disabled
|
|
20
|
+
|
|
163
21
|
if (!result.enabled) {
|
|
164
22
|
element.style.pointerEvents = 'none';
|
|
165
23
|
element.style.opacity = '0.5';
|
|
166
|
-
element.setAttribute('disabled', 'true');
|
|
167
|
-
if (element.setAttribute) {
|
|
168
|
-
element.setAttribute('data-disabled-by-visibility', 'true');
|
|
169
|
-
}
|
|
170
24
|
} else {
|
|
171
25
|
element.style.pointerEvents = '';
|
|
172
26
|
element.style.opacity = '';
|
|
173
|
-
element.removeAttribute('disabled');
|
|
174
|
-
element.removeAttribute('data-disabled-by-visibility');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Store encrypted hash to detect tampering
|
|
178
|
-
this.#storeElementHash(element, result);
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
console.error('❌ [Visibility] Failed to check visibility:', err);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Store element hash for tampering detection
|
|
187
|
-
*/
|
|
188
|
-
#storeElementHash(element, result) {
|
|
189
|
-
const hash = btoa(JSON.stringify({
|
|
190
|
-
visible: result.visible,
|
|
191
|
-
enabled: result.enabled,
|
|
192
|
-
timestamp: Date.now()
|
|
193
|
-
}));
|
|
194
|
-
element.setAttribute('data-visibility-hash', hash);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Validate element hasn't been tampered
|
|
199
|
-
*/
|
|
200
|
-
async #revalidateElement(element) {
|
|
201
|
-
try {
|
|
202
|
-
// DOM-dan deyil, WeakMap-dan oxu — attribute dəyişdirilsə belə orijinal config qalır
|
|
203
|
-
const config = this.#configs.get(element);
|
|
204
|
-
if (config?.enabled) {
|
|
205
|
-
await this.#checkAndApply(element, config);
|
|
206
27
|
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
console.error('[Visibility] Revalidation error:', err);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('[Visibility] Check failed:', err);
|
|
210
30
|
}
|
|
211
31
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
controlled.forEach(element => {
|
|
219
|
-
this.#revalidateElement(element);
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Remove visibility control from element
|
|
225
|
-
*/
|
|
226
|
-
removeVisibility(element) {
|
|
227
|
-
const intervalId = this.#intervalChecks.get(element);
|
|
228
|
-
if (intervalId) {
|
|
229
|
-
clearInterval(intervalId);
|
|
230
|
-
this.#intervalChecks.delete(element);
|
|
32
|
+
|
|
33
|
+
async applyVisibility(element, visibilityConfig) {
|
|
34
|
+
if (!visibilityConfig || !visibilityConfig.enabled) return;
|
|
35
|
+
|
|
36
|
+
if (!this.#originalDisplayStyles.has(element)) {
|
|
37
|
+
this.#originalDisplayStyles.set(element, element.style.display || '');
|
|
231
38
|
}
|
|
232
|
-
|
|
233
|
-
this.#
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (originalDisplay !== undefined) {
|
|
240
|
-
element.style.display = originalDisplay;
|
|
39
|
+
|
|
40
|
+
await this.#checkAndApply(element, visibilityConfig);
|
|
41
|
+
|
|
42
|
+
if (visibilityConfig.objectId) {
|
|
43
|
+
await iobrokerHandler.subscribeState(visibilityConfig.objectId, () => {
|
|
44
|
+
this.#checkAndApply(element, visibilityConfig);
|
|
45
|
+
});
|
|
241
46
|
}
|
|
242
|
-
element.style.visibility = '';
|
|
243
|
-
element.style.pointerEvents = '';
|
|
244
|
-
element.style.opacity = '';
|
|
245
47
|
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Scan and apply visibility to all elements with visibility config
|
|
249
|
-
*/
|
|
48
|
+
|
|
250
49
|
async scanAndApply(container = document.body) {
|
|
251
50
|
const elements = container.querySelectorAll('[data-visibility-enabled="true"]');
|
|
252
|
-
console.log(`🔍 [Visibility] Scanning ${elements.length} elements with visibility config`);
|
|
253
|
-
|
|
254
51
|
for (const element of elements) {
|
|
255
52
|
try {
|
|
256
|
-
// Read config from separate attributes
|
|
257
53
|
const visibilityConfig = {
|
|
258
|
-
enabled:
|
|
54
|
+
enabled: true,
|
|
259
55
|
objectId: element.getAttribute('data-visibility-signal'),
|
|
260
56
|
condition: element.getAttribute('data-visibility-condition') || '==',
|
|
261
57
|
conditionValue: element.getAttribute('data-visibility-value') || '',
|
|
262
58
|
action: element.getAttribute('data-visibility-action') || 'hide',
|
|
263
59
|
groups: element.getAttribute('data-visibility-groups')?.split(',').filter(g => g) || []
|
|
264
60
|
};
|
|
265
|
-
|
|
266
61
|
await this.applyVisibility(element, visibilityConfig);
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
62
|
+
} catch (err) {
|
|
269
63
|
console.error('[Visibility] Error parsing visibility config:', err);
|
|
270
64
|
}
|
|
271
65
|
}
|