Merge feature/permissions-hardening: RBAC hardening + B-lite + P0 UX
Phase A (security): permission registry, audit log on perm/feature changes, token_version bump on permission changes. B-lite: requireFeature middleware blocks API on disabled global flags. P0 UX: search, modified-dot, confirm on critical perms, badge wording. Conflict resolution: admin.js monolith was restructured into frontend/js/admin/sections/* by feature/admin-redesign merge. P0 UX edits (originally in monolith) were manually ported to: - sections/permissions.js — modDot, confirm gate, filterPermissions - sections/users.js — 'Инд.' → 'Индивидуально' badge in user-perms modal admin.html search input + dot CSS auto-merged cleanly.
This commit is contained in:
@@ -23,10 +23,14 @@
|
||||
const defs = definitions.filter(d => d.role === role);
|
||||
container.innerHTML = defs.map(def => {
|
||||
const enabled = permissions[role]?.[def.key] ?? def.default;
|
||||
const isModified = (enabled ? 1 : 0) !== def.default;
|
||||
const modDot = isModified
|
||||
? `<span class="perm-modified-dot" title="Отличается от значения по умолчанию"></span>`
|
||||
: '';
|
||||
return `
|
||||
<div class="perm-card${enabled ? ' enabled' : ''}" id="perm-card-${role}-${def.key.replace('.','_')}">
|
||||
<div class="perm-info">
|
||||
<div class="perm-label">${esc(def.label)}</div>
|
||||
<div class="perm-label">${esc(def.label)}${modDot}</div>
|
||||
<div class="perm-desc">${esc(def.desc)}</div>
|
||||
</div>
|
||||
<label class="perm-toggle" title="${enabled ? 'Выключить' : 'Включить'}">
|
||||
@@ -41,6 +45,17 @@
|
||||
}
|
||||
|
||||
async function togglePermission(role, key, enabled, checkbox) {
|
||||
if (!enabled) {
|
||||
const def = (_permData.definitions || []).find(d => d.role === role && d.key === key);
|
||||
if (def && def.requireConfirmOff) {
|
||||
const roleLabel = role === 'teacher' ? 'Учитель' : 'Ученик';
|
||||
const ok = await LS.confirm(
|
||||
`Выключение «${def.label}» затронет всех пользователей роли «${roleLabel}». Они потеряют доступ. Продолжить?`,
|
||||
{ title: 'Подтвердите выключение права', confirmText: 'Выключить' }
|
||||
);
|
||||
if (!ok) { checkbox.checked = true; return; }
|
||||
}
|
||||
}
|
||||
checkbox.disabled = true;
|
||||
try {
|
||||
await LS.setPermission(role, key, enabled);
|
||||
@@ -49,6 +64,8 @@
|
||||
const safeKey = key.replace('.', '_');
|
||||
const card = document.getElementById(`perm-card-${role}-${safeKey}`);
|
||||
if (card) card.classList.toggle('enabled', enabled);
|
||||
// Re-render to refresh the modified-dot indicator across all cards.
|
||||
renderPermissions();
|
||||
LS.toast(enabled ? 'Право включено' : 'Право отключено', 'success');
|
||||
} catch(e) {
|
||||
checkbox.checked = !enabled;
|
||||
@@ -58,7 +75,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
function filterPermissions(query) {
|
||||
const q = (query || '').trim().toLowerCase();
|
||||
['teacher', 'student'].forEach(role => {
|
||||
const block = document.querySelector(`#perm-${role}`)?.closest('.perm-role-block');
|
||||
const cards = document.querySelectorAll(`#perm-${role} .perm-card`);
|
||||
let visibleCount = 0;
|
||||
cards.forEach(card => {
|
||||
const label = (card.querySelector('.perm-label')?.textContent || '').toLowerCase();
|
||||
const desc = (card.querySelector('.perm-desc')?.textContent || '').toLowerCase();
|
||||
const show = !q || label.includes(q) || desc.includes(q);
|
||||
card.style.display = show ? '' : 'none';
|
||||
if (show) visibleCount++;
|
||||
});
|
||||
if (block) block.style.display = visibleCount === 0 ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
window.togglePermission = togglePermission;
|
||||
window.filterPermissions = filterPermissions;
|
||||
|
||||
window.AdminSections = window.AdminSections || {};
|
||||
window.AdminSections.permissions = {
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
const hasOverride = p.userVal !== undefined;
|
||||
const checked = p.effective;
|
||||
const badge = hasOverride
|
||||
? `<span style="font-size:10px;padding:2px 7px;border-radius:var(--r-pill);background:rgba(155,93,229,0.12);color:var(--violet);font-weight:700">Инд.</span>`
|
||||
? `<span style="font-size:11px;padding:2px 5px;border-radius:var(--r-pill);background:rgba(155,93,229,0.12);color:var(--violet);font-weight:700">Индивидуально</span>`
|
||||
: `<span style="font-size:10px;padding:2px 7px;border-radius:var(--r-pill);background:rgba(136,152,170,0.12);color:var(--text-3);font-weight:700">По роли</span>`;
|
||||
const resetBtn = hasOverride
|
||||
? `<button style="background:none;border:none;cursor:pointer;color:var(--text-3);padding:3px 6px;border-radius:6px;font-size:11px;font-weight:700;transition:color .2s"
|
||||
|
||||
Reference in New Issue
Block a user