Skip to content

Commit 3c7037c

Browse files
committed
ci: Add GitHub Actions workflow for build and release
1 parent 7642b6e commit 3c7037c

File tree

9 files changed

+280
-48
lines changed

9 files changed

+280
-48
lines changed

index.html

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,28 @@
196196
body.role-admin .admin-only {
197197
display: flex !important;
198198
}
199+
200+
/* VVV СТИЛИ ДЛЯ ГИБКИХ ПРАВ VVV */
201+
/* По умолчанию скрываем элементы, требующие прав, для всех операторов */
202+
body.role-operator .requires-permission {
203+
display: none !important;
204+
}
205+
/* А для админа всегда показываем */
206+
body.role-admin .requires-permission {
207+
display: flex !important;
208+
}
209+
/* Теперь выборочно показываем для операторов с нужными правами */
210+
body.can-edit-cameras #discover-btn,
211+
body.can-edit-cameras #add-group-btn,
212+
body.can-edit-cameras #add-camera-sidebar-btn {
213+
display: flex !important;
214+
}
215+
body.can-manage-layout #layout-controls,
216+
body.can-manage-layout #presentation-mode-btn {
217+
display: flex !important;
218+
}
219+
/* ^^^ КОНЕЦ СТИЛЕЙ ДЛЯ ГИБКИХ ПРАВ ^^^ */
220+
199221
body.role-operator .camera-item,
200222
body.role-operator .grid-cell.active {
201223
cursor: default;
@@ -206,16 +228,23 @@
206228
display: none !important;
207229
}
208230

209-
#user-list .delete-user-btn,
210-
#user-list .change-pass-btn {
211-
background: none;
212-
border: none;
231+
#user-list button {
232+
background: none;
233+
border: 1px solid #ccc;
234+
border-radius: 4px;
235+
padding: 4px 8px;
213236
cursor: pointer;
214-
text-decoration: underline;
215237
font-size: 13px;
216238
}
217-
#user-list .delete-user-btn { color: #dc3545; }
218-
#user-list .change-pass-btn { color: #007bff; }
239+
#user-list button:hover {
240+
background-color: #f0f0f0;
241+
}
242+
#user-list .delete-user-btn { color: #dc3545; border-color: #dc3545; }
243+
#user-list .delete-user-btn:hover { background-color: #dc3545; color: white; }
244+
#user-list .change-pass-btn { color: #007bff; border-color: #007bff; }
245+
#user-list .change-pass-btn:hover { background-color: #007bff; color: white; }
246+
#user-list button:disabled { opacity: 0.5; cursor: not-allowed; }
247+
219248
</style>
220249
</head>
221250
<body>
@@ -252,7 +281,7 @@ <h2 data-i18n-key="app_title"></h2>
252281
<div class="sidebar">
253282
<div class="sidebar-header">
254283
<div class="header-buttons">
255-
<span class="admin-only" style="display: contents;">
284+
<span class="requires-permission" style="display: contents;">
256285
<button id="discover-btn" class="icon-button" data-i18n-tooltip="discover_cameras_tooltip"><i class="material-icons">travel_explore</i></button>
257286
<button id="add-group-btn" class="icon-button" data-i18n-tooltip="create_group_tooltip"><i class="material-icons">create_new_folder</i></button>
258287
<button id="add-camera-sidebar-btn" class="icon-button" data-i18n-tooltip="add_camera_tooltip"><i class="material-icons">add_circle_outline</i></button>
@@ -272,9 +301,9 @@ <h3 data-i18n-key="devices"></h3>
272301
<button id="open-recordings-btn" class="icon-button admin-only" data-i18n-tooltip="open_recordings_tooltip"><i class="material-icons" style="font-size: 20px;">folder</i></button>
273302
<div class="status-info" id="status-info"></div>
274303
</div>
275-
<div class="toolbar-group admin-only" id="layout-controls"></div>
304+
<div class="toolbar-group requires-permission" id="layout-controls"></div>
276305
<div class="toolbar-group">
277-
<button id="presentation-mode-btn" class="icon-button admin-only" data-i18n-tooltip="presentation_mode_tooltip"><i class="material-icons" style="font-size: 20px;">tv</i></button>
306+
<button id="presentation-mode-btn" class="icon-button requires-permission" data-i18n-tooltip="presentation_mode_tooltip"><i class="material-icons" style="font-size: 20px;">tv</i></button>
278307
</div>
279308
</div>
280309
</div>
@@ -652,6 +681,22 @@ <h2 data-i18n-key="add_user_title"></h2>
652681
</div>
653682
</div>
654683
</div>
684+
685+
<div id="permissions-modal" class="modal-backdrop hidden">
686+
<div class="modal-content" style="width: 500px;">
687+
<span id="permissions-modal-close-btn" class="modal-close-btn">×</span>
688+
<h2 id="permissions-modal-title"></h2>
689+
<div class="modal-body" style="padding-top: 15px;">
690+
<div id="permissions-list" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
691+
<!-- Checkboxes will be inserted here dynamically -->
692+
</div>
693+
</div>
694+
<div class="modal-footer" style="justify-content: flex-end;">
695+
<button id="save-permissions-btn" data-i18n-key="save"></button>
696+
<button id="cancel-permissions-btn" style="background-color: #6c757d;" data-i18n-key="cancel"></button>
697+
</div>
698+
</div>
699+
</div>
655700

656701
<script src="jsmpeg.min.js"></script>
657702
<script src="./js/i18n.js"></script>

js/camera-list.js

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@
108108
openRecordingsBtn.addEventListener('click', () => window.api.openRecordingsFolder());
109109

110110
cameraListContainer.addEventListener('contextmenu', (e) => {
111-
// VVV ИЗМЕНЕНИЕ: Блокируем контекстное меню для всех, кроме админа VVV
112-
if (App.stateManager.state.currentUser?.role !== 'admin') {
111+
const currentUser = App.stateManager.state.currentUser;
112+
// VVV ИЗМЕНЕНИЕ: Блокируем контекстное меню для операторов без прав VVV
113+
if (currentUser?.role !== 'admin' && !(currentUser.permissions?.edit_cameras || currentUser.permissions?.delete_cameras || currentUser.permissions?.access_settings || currentUser.permissions?.view_archive)) {
113114
e.preventDefault();
114115
return;
115116
}
@@ -119,16 +120,27 @@
119120
if (cameraItem) {
120121
e.preventDefault();
121122
const cameraId = parseInt(cameraItem.dataset.cameraId, 10);
122-
const labels = {
123-
open_in_browser: `🌐 ${App.i18n.t('context_open_in_browser')}`,
124-
files: `🗂️ ${App.i18n.t('context_file_manager')}`,
125-
ssh: `💻 ${App.i18n.t('context_ssh')}`,
126-
archive: `🗄️ ${App.i18n.t('archive_title')}`,
127-
settings: `⚙️ ${App.i18n.t('context_settings')}`,
128-
edit: `✏️ ${App.i18n.t('context_edit')}`,
129-
delete: `🗑️ ${App.i18n.t('context_delete')}`
130-
};
131-
window.api.showCameraContextMenu({ cameraId, labels });
123+
// Динамически строим меню на основе прав
124+
const menuItems = {};
125+
126+
menuItems.open_in_browser = `🌐 ${App.i18n.t('context_open_in_browser')}`;
127+
menuItems.files = `🗂️ ${App.i18n.t('context_file_manager')}`;
128+
menuItems.ssh = `💻 ${App.i18n.t('context_ssh')}`;
129+
130+
if (currentUser.role === 'admin' || currentUser.permissions?.view_archive) {
131+
menuItems.archive = `🗄️ ${App.i18n.t('archive_title')}`;
132+
}
133+
if (currentUser.role === 'admin' || currentUser.permissions?.access_settings) {
134+
menuItems.settings = `⚙️ ${App.i18n.t('context_settings')}`;
135+
}
136+
if (currentUser.role === 'admin' || currentUser.permissions?.edit_cameras) {
137+
menuItems.edit = `✏️ ${App.i18n.t('context_edit')}`;
138+
}
139+
if (currentUser.role === 'admin' || currentUser.permissions?.delete_cameras) {
140+
menuItems.delete = `🗑️ ${App.i18n.t('context_delete')}`;
141+
}
142+
143+
window.api.showCameraContextMenu({ cameraId, labels: menuItems });
132144
}
133145
});
134146

js/modal-handler.js

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
const addDiscoveredBtn = document.getElementById('add-discovered-btn');
3939
const rediscoverBtn = document.getElementById('rediscover-btn');
4040

41-
// VVV НОВОЕ: Элементы для модального окна управления пользователями VVV
41+
// VVV Элементы для модального окна управления пользователями VVV
4242
const userManagementModal = document.getElementById('user-management-modal');
4343
const userManagementCloseBtn = document.getElementById('user-management-close-btn');
4444
const userListEl = document.getElementById('user-list');
@@ -49,12 +49,30 @@
4949
const saveUserBtn = document.getElementById('save-user-btn');
5050
const cancelUserBtn = document.getElementById('cancel-user-btn');
5151

52+
// VVV НОВОЕ: Элементы для модального окна управления правами VVV
53+
const permissionsModal = document.getElementById('permissions-modal');
54+
const permissionsModalCloseBtn = document.getElementById('permissions-modal-close-btn');
55+
const permissionsModalTitle = document.getElementById('permissions-modal-title');
56+
const permissionsListEl = document.getElementById('permissions-list');
57+
const savePermissionsBtn = document.getElementById('save-permissions-btn');
58+
const cancelPermissionsBtn = document.getElementById('cancel-permissions-btn');
59+
5260
let toastTimeout;
5361
let editingCameraId = null;
5462
let settingsCameraId = null;
5563
let rangeSyncFunctions = {};
5664
let selectedDiscoveredDevice = null;
5765
let isDiscovering = false;
66+
let editingPermissionsForUser = null;
67+
68+
const availablePermissions = [
69+
{ key: 'view_archive', labelKey: 'view_archive', defaultLabel: 'Просмотр архива' },
70+
{ key: 'export_archive', labelKey: 'export_archive', defaultLabel: 'Экспорт из архива' },
71+
{ key: 'edit_cameras', labelKey: 'edit_cameras', defaultLabel: 'Управление камерами' },
72+
{ key: 'delete_cameras', labelKey: 'delete_cameras', defaultLabel: 'Удаление камер' },
73+
{ key: 'access_settings', labelKey: 'access_settings', defaultLabel: 'Доступ к настройкам' },
74+
{ key: 'manage_layout', labelKey: 'manage_layout', defaultLabel: 'Управление сеткой' },
75+
];
5876

5977
const openModal = (modalElement) => modalElement.classList.remove('hidden');
6078
const closeModal = (modalElement) => {
@@ -129,7 +147,6 @@
129147
allTabs.forEach(btn => {
130148
const tabName = btn.dataset.tab;
131149
const isGeneralTab = tabName === 'tab-general';
132-
// --- ИЗМЕНЕНИЕ: Логика для вкладки пользователей удалена отсюда ---
133150
const isCameraTab = !isGeneralTab;
134151

135152
if (isGeneralSettings) {
@@ -192,7 +209,6 @@
192209
}
193210
}
194211

195-
// VVV НОВАЯ ФУНКЦИЯ VVV
196212
async function openUserManagementModal() {
197213
openModal(userManagementModal);
198214
await renderUserList();
@@ -349,18 +365,26 @@
349365
if (result.success) {
350366
result.users.forEach(user => {
351367
const li = document.createElement('li');
368+
const isCurrentUser = user.username === App.stateManager.state.currentUser?.username;
369+
352370
li.style.cssText = "display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #eee;";
353371
li.innerHTML = `
354372
<div>
355373
<strong>${user.username}</strong>
356374
<small style="color: #666; margin-left: 10px;">(${App.t('role_' + user.role)})</small>
357375
</div>
358-
<div>
359-
<button class="change-pass-btn" style="margin-right: 10px;">${App.t('change_password')}</button>
360-
<button class="delete-user-btn" style="color: var(--danger-color);">${App.t('context_delete')}</button>
376+
<div style="display: flex; align-items: center; gap: 10px;">
377+
${user.role === 'operator' ? `<button class="permissions-btn" data-username="${user.username}">${App.t('permissions_btn', 'Права')}</button>` : ''}
378+
<button class="change-pass-btn">${App.t('change_password')}</button>
379+
<button class="delete-user-btn" style="color: var(--danger-color);" ${isCurrentUser ? 'disabled' : ''}>${App.t('context_delete')}</button>
361380
</div>
362381
`;
363382

383+
const permissionsBtn = li.querySelector('.permissions-btn');
384+
if (permissionsBtn) {
385+
permissionsBtn.addEventListener('click', () => openPermissionsModal(user));
386+
}
387+
364388
li.querySelector('.change-pass-btn').addEventListener('click', async () => {
365389
const newPassword = prompt(App.t('enter_new_password_for', { username: user.username }));
366390
if (newPassword && newPassword.trim()) {
@@ -418,6 +442,50 @@
418442
}
419443
}
420444

445+
// VVV НОВЫЕ ФУНКЦИИ ДЛЯ МОДАЛЬНОГО ОКНА ПРАВ VVV
446+
function openPermissionsModal(user) {
447+
editingPermissionsForUser = user;
448+
permissionsModalTitle.textContent = App.t('permissions_for_user', { username: user.username });
449+
permissionsListEl.innerHTML = '';
450+
451+
availablePermissions.forEach(perm => {
452+
const isChecked = user.permissions && user.permissions[perm.key];
453+
const item = document.createElement('div');
454+
item.className = 'form-check-inline';
455+
item.innerHTML = `
456+
<input type="checkbox" id="perm-${perm.key}" data-key="${perm.key}" class="form-check-input" ${isChecked ? 'checked' : ''}>
457+
<label for="perm-${perm.key}">${App.t(perm.labelKey, perm.defaultLabel)}</label>
458+
`;
459+
permissionsListEl.appendChild(item);
460+
});
461+
462+
openModal(permissionsModal);
463+
}
464+
465+
async function savePermissions() {
466+
if (!editingPermissionsForUser) return;
467+
468+
const newPermissions = {};
469+
permissionsListEl.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
470+
if (checkbox.checked) {
471+
newPermissions[checkbox.dataset.key] = true;
472+
}
473+
});
474+
475+
const result = await window.api.updateUserPermissions({
476+
username: editingPermissionsForUser.username,
477+
permissions: newPermissions
478+
});
479+
480+
if (result.success) {
481+
showToast(App.t('permissions_saved_success', 'Права успешно сохранены.'));
482+
closeModal(permissionsModal);
483+
await renderUserList(); // Обновляем список, чтобы подтянуть новые данные
484+
} else {
485+
alert(`${App.t('error')}: ${result.error}`);
486+
}
487+
}
488+
421489
function init() {
422490
window.api.onOnvifDeviceFound((device) => {
423491
if (discoverList.children.length > 0 && discoverList.children[0].textContent.includes(App.i18n.t('searching_for_cameras'))) {
@@ -447,7 +515,6 @@
447515
const addCameraSidebarBtn = document.getElementById('add-camera-sidebar-btn');
448516
const addGroupBtn = document.getElementById('add-group-btn');
449517
const generalSettingsBtn = document.getElementById('general-settings-btn');
450-
// VVV НОВОЕ VVV
451518
const userManagementBtn = document.getElementById('user-management-btn');
452519

453520
addCameraSidebarBtn.addEventListener('click', () => openAddModal());
@@ -474,7 +541,7 @@
474541
discoverModal.addEventListener('click', (e) => { if (e.target === discoverModal) closeModal(discoverModal); });
475542
addDiscoveredBtn.addEventListener('click', addDiscoveredCamera);
476543

477-
// VVV НОВОЕ: Обработчики для модального окна управления пользователями VVV
544+
// VVV Обработчики для модального окна управления пользователями VVV
478545
userManagementBtn.addEventListener('click', openUserManagementModal);
479546
userManagementCloseBtn.addEventListener('click', () => closeModal(userManagementModal));
480547
userManagementModal.addEventListener('click', (e) => { if (e.target === userManagementModal) closeModal(userManagementModal); });
@@ -485,6 +552,12 @@
485552
saveUserBtn.addEventListener('click', saveNewUser);
486553
cancelUserBtn.addEventListener('click', () => closeModal(addUserModal));
487554

555+
// VVV НОВОЕ: Обработчики для модального окна прав VVV
556+
savePermissionsBtn.addEventListener('click', savePermissions);
557+
cancelPermissionsBtn.addEventListener('click', () => closeModal(permissionsModal));
558+
permissionsModalCloseBtn.addEventListener('click', () => closeModal(permissionsModal));
559+
permissionsModal.addEventListener('click', (e) => { if (e.target === permissionsModal) closeModal(permissionsModal); });
560+
488561
languageSelect.addEventListener('change', async (e) => {
489562
const newLang = e.target.value;
490563
stateManager.setAppSettings({ language: newLang });
@@ -513,7 +586,6 @@
513586
}
514587
});
515588

516-
// --- ИЗМЕНЕНИЕ: Упрощенная логика для вкладок настроек ---
517589
settingsModal.querySelectorAll('.tab-button').forEach(button => {
518590
button.addEventListener('click', () => {
519591
settingsModal.querySelectorAll('.tab-button, .tab-content').forEach(el => el.classList.remove('active'));
@@ -535,7 +607,8 @@
535607
closeModal(addGroupModal);
536608
closeModal(discoverModal);
537609
closeModal(addUserModal);
538-
closeModal(userManagementModal); // VVV НОВОЕ VVV
610+
closeModal(permissionsModal);
611+
closeModal(userManagementModal);
539612
}
540613
});
541614
}

js/renderer.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,19 @@
203203
}, 20);
204204

205205
const user = App.stateManager.state.currentUser;
206-
document.body.classList.remove('role-admin', 'role-operator');
206+
// Сначала удаляем все классы ролей и прав
207+
document.body.className = document.body.className.replace(/role-\w+|can-\w+/g, '').trim();
208+
207209
if (user) {
208210
document.body.classList.add(`role-${user.role}`);
211+
// Если это оператор, добавляем классы для его прав
212+
if (user.role === 'operator' && user.permissions) {
213+
for (const permission in user.permissions) {
214+
if (user.permissions[permission]) {
215+
document.body.classList.add(`can-${permission.replace(/_/g, '-')}`);
216+
}
217+
}
218+
}
209219
}
210220
});
211221

0 commit comments

Comments
 (0)