diff --git a/backend/src/controllers/adminController.js b/backend/src/controllers/adminController.js index 6b330e2..75439f9 100644 --- a/backend/src/controllers/adminController.js +++ b/backend/src/controllers/adminController.js @@ -206,7 +206,7 @@ function getUsers(req, res) { const cursorWhere = where + ' AND u.id < ?'; const cursorArgs = [...args, cursor]; const users = db.prepare(` - SELECT u.id, u.name, u.email, u.role, u.created_at, u.last_login, u.is_banned, + SELECT u.id, u.name, u.email, u.role, u.custom_role, u.created_at, u.last_login, u.is_banned, COUNT(ts.id) AS tests_count, ROUND(AVG(CAST(ts.score AS REAL) / ts.total * 100), 1) AS avg_pct FROM users u diff --git a/frontend/admin.html b/frontend/admin.html index 31af389..6378e6f 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -1305,6 +1305,8 @@ oninput="filterPermissions(this.value)"> +
+
Учитель diff --git a/frontend/js/admin/sections/permissions.js b/frontend/js/admin/sections/permissions.js index ce0bcb0..2bb820f 100644 --- a/frontend/js/admin/sections/permissions.js +++ b/frontend/js/admin/sections/permissions.js @@ -8,11 +8,15 @@ let _bulkClassId = null; let _presets = null; // { student:[{id,label,desc,perms}] } + let _roles = null; // [{name,label,baseRoles,isBuiltin,users}] + let _roleEdit = null; // { name, base, permissions, definitions } — открытый редактор прав роли + async function load() { try { _permData = await LS.getPermissions(); renderPermissions(); loadBulk(); + loadRoles(); } catch(e) { document.getElementById('perm-teacher').innerHTML = `

Ошибка загрузки: ${esc(e.message)}

`; @@ -220,6 +224,109 @@ } } + /* ════════ Конструктор ролей (C-4) ════════ */ + const BASE_OPTS = [['teacher', 'Учитель'], ['student', 'Ученик'], ['free_student', 'Свободный ученик'], ['admin', 'Администратор']]; + + async function loadRoles() { + const root = document.getElementById('perm-roles'); + if (!root) return; + try { _roles = await LS.listRoles(); } catch { _roles = []; } + renderRoles(); + } + + function renderRoles() { + const root = document.getElementById('perm-roles'); + if (!root) return; + const customs = (_roles || []).filter(r => !r.isBuiltin); + const baseChecks = BASE_OPTS.map(([v, lbl]) => + ``).join(''); + const rolesList = customs.length ? customs.map(r => ` +
+
${esc(r.label)} (${esc(r.name)} · база: ${(r.baseRoles || []).join(', ')} · ${r.users} польз.)
+ + + + +
`).join('') : '

Кастомных ролей пока нет.

'; + + root.innerHTML = ` +
Конструктор ролей
+
Кастомная роль проходит гейты своих «базовых ролей» и имеет свой набор прав (изначально — копия базы). Назначается пользователю в его карточке.
+
+ + + +
+
${baseChecks}
+ ${rolesList} +
${_roleEdit ? roleEditorHtml() : ''}
`; + } + + function roleEditorHtml() { + const defs = _roleEdit.definitions || []; + const map = _roleEdit.permissions || {}; + const order = [], byGroup = {}; + defs.forEach(d => { const g = d.group || 'Прочее'; if (!byGroup[g]) { byGroup[g] = []; order.push(g); } byGroup[g].push(d); }); + const card = (d) => ` +
+
${esc(d.label)}
${esc(d.desc)}
+ +
`; + const groups = order.map(g => `
${esc(g)}
${byGroup[g].map(card).join('')}
`).join(''); + return `
+
+ Права роли «${esc(_roleEdit.name)}» (база: ${esc(_roleEdit.base)}) + +
+ ${defs.length ? groups : '

У базы нет настраиваемых прав.

'}
`; + } + + async function createRoleUI() { + const name = (document.getElementById('nr-name').value || '').trim(); + const label = (document.getElementById('nr-label').value || '').trim(); + const bases = [...document.querySelectorAll('#perm-roles .nr-base:checked')].map(c => c.value); + if (!name) { LS.toast('Укажите имя роли', 'error'); return; } + if (!bases.length) { LS.toast('Отметьте хотя бы одну базовую роль', 'error'); return; } + try { + await LS.createRole(name, label || name, bases); + await loadRoles(); + LS.toast('Роль создана', 'success'); + } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } + } + + async function deleteRoleUI(name) { + if (!await LS.confirm(`Удалить роль «${name}»? Пользователи вернутся на её базовую роль.`, { title: 'Удалить роль', confirmText: 'Удалить' })) return; + try { + const r = await LS.deleteRole(name); + if (_roleEdit && _roleEdit.name === name) _roleEdit = null; + await loadRoles(); + LS.toast(`Роль удалена (вернулось ${r.reassigned || 0} польз.)`, 'success'); + } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } + } + + async function configureRole(name) { + try { _roleEdit = await LS.rolePermissions(name); renderRoles(); } + catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } + } + function closeRoleEditor() { _roleEdit = null; renderRoles(); } + + async function toggleRolePerm(roleName, key, enabled, checkbox) { + checkbox.disabled = true; + try { + await LS.setPermission(roleName, key, enabled); + if (_roleEdit && _roleEdit.name === roleName) _roleEdit.permissions[key] = enabled; + const editor = document.getElementById('role-editor'); + if (editor) editor.innerHTML = _roleEdit ? roleEditorHtml() : ''; + LS.toast(enabled ? 'Право включено' : 'Право отключено', 'success'); + } catch (e) { checkbox.checked = !enabled; LS.toast('Ошибка: ' + e.message, 'error'); } + finally { checkbox.disabled = false; } + } + + window.createRoleUI = createRoleUI; + window.deleteRoleUI = deleteRoleUI; + window.configureRole = configureRole; + window.closeRoleEditor = closeRoleEditor; + window.toggleRolePerm = toggleRolePerm; window.togglePermission = togglePermission; window.togglePermGroup = togglePermGroup; window.filterPermissions = filterPermissions; diff --git a/frontend/js/admin/sections/users.js b/frontend/js/admin/sections/users.js index c18a300..14bec6a 100644 --- a/frontend/js/admin/sections/users.js +++ b/frontend/js/admin/sections/users.js @@ -6,6 +6,7 @@ let _usersPage = 1; const _USERS_PER_PAGE = 50; + let _customRoles = []; // кастомные роли для выпадающего списка назначения /* ── one-time CSS injection for hover row-actions (shared with sessions) ── */ function ensureRowActionsStyles() { @@ -65,6 +66,7 @@ try { const r = await LS.adminGetUsers({ page: _usersPage, limit: _USERS_PER_PAGE }); const users = r.users || []; + try { _customRoles = (await LS.listRoles()).filter(x => !x.isBuiltin); } catch { _customRoles = []; } const tbody = document.getElementById('users-body'); if (!users.length) { tbody.innerHTML = '
Пользователей нет
'; @@ -77,10 +79,11 @@ const avatarBg = u.role==='admin' ? 'linear-gradient(135deg,#9B5DE5,#c084fc)' : u.role==='teacher' ? 'linear-gradient(135deg,#06D6E0,#9B5DE5)' : u.role==='free_student' ? 'linear-gradient(135deg,#10B981,#059669)' : 'linear-gradient(135deg,#8898AA,#3D4F6B)'; const roleCell = isAdmin && u.id !== user.id ? `` : `${{student:'Ученик',free_student:'Своб. ученик',teacher:'Учитель',admin:'Админ'}[u.role]||u.role}`; return ` diff --git a/js/api.js b/js/api.js index a9c5b7c..88c6d34 100644 --- a/js/api.js +++ b/js/api.js @@ -1034,6 +1034,7 @@ window.LS = { getFolderAccess, clearFolderAccess, assignFolder, unassignFolder, getStudentsList, submitWork, resubmitWork, getMySubmissions, getClassSubmissions, reviewSubmission, deleteSubmission, submissionDownloadUrl, getPermissions, permissionsLog, setClassPermission, permissionsPresets, applyClassPreset, setPermission, getUserPermissions, setUserPermission, resetUserPermissions, + listRoles, createRole, updateRoleDef, deleteRole, rolePermissions, accessCatalog, accessTargets, accessSummary, accessClassOpen, accessMatrix, accessLog, accessRules, accessSetRule, getCourseTemplates, saveCourseTemplate, createFromCourseTemplate, deleteCourseTemplate, getLessonTemplates, saveLessonTemplate, createFromLessonTemplate, deleteLessonTemplate, @@ -1298,6 +1299,11 @@ async function getPermissions() { return req('GET', async function permissionsLog(userId) { return req('GET', userId ? `/permissions/log?user_id=${userId}` : '/permissions/log'); } async function setClassPermission(classId, permission, enabled) { return req('POST', `/permissions/class/${classId}/bulk`, { permission, enabled }); } async function permissionsPresets() { return req('GET', '/permissions/presets'); } +async function listRoles() { return req('GET', '/roles'); } +async function createRole(name, label, baseRoles) { return req('POST', '/roles', { name, label, baseRoles }); } +async function updateRoleDef(name, body) { return req('PUT', `/roles/${encodeURIComponent(name)}`, body); } +async function deleteRole(name) { return req('DELETE', `/roles/${encodeURIComponent(name)}`); } +async function rolePermissions(name) { return req('GET', `/roles/${encodeURIComponent(name)}/permissions`); } async function applyClassPreset(classId, preset) { return req('POST', `/permissions/class/${classId}/preset`, { preset }); } async function setPermission(role, permission, enabled) { return req('POST', '/permissions', { role, permission, enabled }); } async function getUserPermissions(uid) { return req('GET', `/permissions/users/${uid}`); }