iobroker.mywebui 1.37.34 → 1.37.36
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 (pass null so openBindingsEditor doesn't reject on service check)
|
|
436
|
+
bindBtn.onclick = () => {
|
|
437
|
+
const property = { name: 'hidden', propertyType: 'propertyAndAttribute' };
|
|
438
|
+
serviceContainer.config.openBindingsEditor(property, [designItem], null, '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], null, '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,76 @@
|
|
|
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
|
-
|
|
10
|
+
const result = await iobrokerHandler.checkVisibility(visibilityConfig);
|
|
128
11
|
|
|
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
|
-
}
|
|
147
|
-
|
|
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
27
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.#storeElementHash(element, result);
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
console.error('❌ [Visibility] Failed to check visibility:', err);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('[Visibility] Check failed:', err);
|
|
182
30
|
}
|
|
183
31
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
}
|
|
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 || '');
|
|
207
38
|
}
|
|
208
|
-
|
|
209
|
-
|
|
39
|
+
|
|
40
|
+
await this.#checkAndApply(element, visibilityConfig);
|
|
41
|
+
|
|
42
|
+
if (visibilityConfig.objectId) {
|
|
43
|
+
await iobrokerHandler.subscribeState(visibilityConfig.objectId, () => {
|
|
44
|
+
this.#checkAndApply(element, visibilityConfig);
|
|
45
|
+
});
|
|
210
46
|
}
|
|
211
47
|
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Revalidate all controlled elements
|
|
215
|
-
*/
|
|
216
|
-
#revalidateAll() {
|
|
217
|
-
const controlled = document.querySelectorAll('[data-visibility-controlled="true"]');
|
|
218
|
-
controlled.forEach(element => {
|
|
219
|
-
this.#revalidateElement(element);
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Remove visibility control from element
|
|
225
|
-
*/
|
|
48
|
+
|
|
226
49
|
removeVisibility(element) {
|
|
227
|
-
const intervalId = this.#intervalChecks.get(element);
|
|
228
|
-
if (intervalId) {
|
|
229
|
-
clearInterval(intervalId);
|
|
230
|
-
this.#intervalChecks.delete(element);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
this.#configs.delete(element);
|
|
234
|
-
element.removeAttribute('data-visibility-controlled');
|
|
235
|
-
element.removeAttribute('data-visibility-hash');
|
|
236
|
-
|
|
237
|
-
// Restore original style
|
|
238
50
|
const originalDisplay = this.#originalDisplayStyles.get(element);
|
|
239
51
|
if (originalDisplay !== undefined) {
|
|
240
52
|
element.style.display = originalDisplay;
|
|
53
|
+
this.#originalDisplayStyles.delete(element);
|
|
241
54
|
}
|
|
242
55
|
element.style.visibility = '';
|
|
243
56
|
element.style.pointerEvents = '';
|
|
244
57
|
element.style.opacity = '';
|
|
245
58
|
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Scan and apply visibility to all elements with visibility config
|
|
249
|
-
*/
|
|
59
|
+
|
|
250
60
|
async scanAndApply(container = document.body) {
|
|
251
61
|
const elements = container.querySelectorAll('[data-visibility-enabled="true"]');
|
|
252
|
-
console.log(`🔍 [Visibility] Scanning ${elements.length} elements with visibility config`);
|
|
253
|
-
|
|
254
62
|
for (const element of elements) {
|
|
255
63
|
try {
|
|
256
|
-
// Read config from separate attributes
|
|
257
64
|
const visibilityConfig = {
|
|
258
|
-
enabled:
|
|
65
|
+
enabled: true,
|
|
259
66
|
objectId: element.getAttribute('data-visibility-signal'),
|
|
260
67
|
condition: element.getAttribute('data-visibility-condition') || '==',
|
|
261
68
|
conditionValue: element.getAttribute('data-visibility-value') || '',
|
|
262
69
|
action: element.getAttribute('data-visibility-action') || 'hide',
|
|
263
70
|
groups: element.getAttribute('data-visibility-groups')?.split(',').filter(g => g) || []
|
|
264
71
|
};
|
|
265
|
-
|
|
266
72
|
await this.applyVisibility(element, visibilityConfig);
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
73
|
+
} catch (err) {
|
|
269
74
|
console.error('[Visibility] Error parsing visibility config:', err);
|
|
270
75
|
}
|
|
271
76
|
}
|