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
+26
View File
@@ -274,4 +274,30 @@ describe('Permissions', () => {
const badP = await inject('POST', `/api/permissions/class/${cid}/preset`, { preset: 'nope' }, adminToken);
assert.equal(badP.status, 400);
});
// ── B8: временные права (expires_at) ───────────────────────────────────────
it('B8: временный оверрайд хранит срок, отдаётся, просроченный игнорируется и чистится', async () => {
const r = await inject('POST', `/api/permissions/users/${studentUser.userId}`,
{ permission: 'shop.purchase', enabled: false, days: 7 }, adminToken);
assert.equal(r.status, 200);
assert.equal(r.body.expires_in_days, 7);
const row = db.prepare('SELECT enabled, expires_at FROM user_permissions WHERE user_id=? AND permission=?')
.get(studentUser.userId, 'shop.purchase');
assert.ok(row && row.enabled === 0 && row.expires_at, 'оверрайд с expires_at сохранён');
const view = await inject('GET', `/api/permissions/users/${studentUser.userId}`, null, adminToken);
const sp = view.body.permissions.find(p => p.key === 'shop.purchase');
assert.equal(sp.userVal, false, 'активный временный оверрайд виден');
assert.ok(sp.expiresAt, 'expiresAt отдаётся в API');
// имитируем просрочку
db.prepare("UPDATE user_permissions SET expires_at = datetime('now','-1 day') WHERE user_id=? AND permission=?")
.run(studentUser.userId, 'shop.purchase');
const view2 = await inject('GET', `/api/permissions/users/${studentUser.userId}`, null, adminToken);
const sp2 = view2.body.permissions.find(p => p.key === 'shop.purchase');
assert.equal(sp2.userVal, undefined, 'просроченный оверрайд не учитывается (наследует роль)');
const left = db.prepare("SELECT COUNT(*) n FROM user_permissions WHERE user_id=? AND permission=?")
.get(studentUser.userId, 'shop.purchase').n;
assert.equal(left, 0, 'просроченная строка вычищена seedDefaults');
});
});