feat(permissions): B8 — временные права (expires_at) с авто-снятием

Миграция 053: user_permissions.expires_at (NULL = бессрочно). Резолвер isEnabled
+ /me + /users/:id игнорируют просроченные оверрайды (наследуют роль); seedDefaults
чистит просроченные строки. setUserPermission принимает days → выдаёт право на
срок (datetime('now','+N days')). API отдаёт expiresAt. Клиент: setUserPermission(...,days).
В модалке прав пользователя — бейдж «до ДАТА» + кнопка «врем.» (выдать на N дней).
Тест: срок хранится/отдаётся, просроченное игнорируется и вычищается. Backend pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 14:43:06 +03:00
parent 8b495f1508
commit a250d15f9a
6 changed files with 83 additions and 14 deletions
+25 -1
View File
@@ -364,6 +364,12 @@
const badge = hasOverride
? `<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 expBadge = (hasOverride && p.expiresAt)
? `<span title="Временный оверрайд истекает (UTC)" style="font-size:11px;padding:2px 6px;border-radius:var(--r-pill);background:rgba(245,158,11,0.14);color:#b45309;font-weight:700">до ${esc(p.expiresAt.slice(0, 10))}</span>`
: '';
const tempBtn = `<button style="background:none;border:none;cursor:pointer;color:var(--text-3);padding:3px 6px;border-radius:6px;font-size:11px;font-weight:600"
onmouseover="this.style.color='var(--violet)'" onmouseout="this.style.color='var(--text-3)'"
onclick="doSetUserPermTemp('${esc(p.key)}')" title="Выдать право на срок">врем.</button>`;
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"
onmouseover="this.style.color='var(--danger)'" onmouseout="this.style.color='var(--text-3)'"
@@ -372,9 +378,11 @@
return `
<div class="perm-card${checked ? ' enabled' : ''}" id="up-perm-card-${p.key.replace('.','_')}">
<div class="perm-info">
<div style="display:flex;align-items:center;gap:7px">
<div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap">
<span class="perm-label">${esc(p.label)}</span>
${badge}
${expBadge}
${tempBtn}
${resetBtn}
</div>
<div class="perm-desc">${esc(p.desc)}</div>
@@ -408,6 +416,21 @@
}
}
async function doSetUserPermTemp(key) {
const uid = getActiveUid();
if (!uid) return;
const raw = window.prompt('Выдать право временно. На сколько дней?', '7');
if (raw === null) return;
const days = parseInt(raw, 10);
if (!Number.isInteger(days) || days <= 0) { LS.toast('Введите число дней > 0', 'error'); return; }
try {
await LS.setUserPermission(uid, key, true, days);
_upPermsData = await LS.getUserPermissions(uid);
renderUserPerms();
LS.toast(`Право выдано на ${days} дн.`, 'success');
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
}
async function doResetOneUserPerm(key) {
const uid = getActiveUid();
if (!uid) return;
@@ -445,6 +468,7 @@
window.closeUserPermsModal = closeUserPermsModal;
window.openUserPermsModal = openUserPermsModal;
window.doSetUserPerm = doSetUserPerm;
window.doSetUserPermTemp = doSetUserPermTemp;
window.doResetOneUserPerm = doResetOneUserPerm;
window.doResetAllUserPerms = doResetAllUserPerms;
// Phase 5 quick actions