feat(access): Фаза 2c — история правил + пресет «копировать доступ из класса»

История: GET /api/access/log (admin-only) — кто/когда открыл/закрыл/сбросил
правило для контента (из admin_audit_log, имена классов/учеников резолвятся).
Клиент LS.accessLog; в режиме «По контенту» — кнопка «История изменений».
Пресет: в режиме «По классу» — «Скопировать доступ из класса [выбор]» (дополняет
текущие правила открытыми правилами класса-источника). Тест: история (admin
видит запись, учителю 403). content-access 13/13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 13:55:02 +03:00
parent 11ec350dfa
commit b702b04ed2
4 changed files with 102 additions and 2 deletions
+13
View File
@@ -116,6 +116,19 @@ describe('contentAccess', () => {
assert.ok(Array.isArray(r.body.sims) && r.body.sims.length >= 1, 'каталог содержит симуляции');
});
it('GET /api/access/log — история (admin видит запись; учителю 403)', async () => {
const admin = await getToken('admin');
const p = await inject('POST', '/api/access/rules',
{ content_type: 'textbook', content_ref: HUB, scope: 'class', target_id: classId, allow: 1 }, admin.token);
assert.ok(p.status < 300, JSON.stringify(p.body));
const log = await inject('GET', `/api/access/log?content_type=textbook&content_ref=${HUB}`, null, admin.token);
assert.equal(log.status, 200);
assert.ok(Array.isArray(log.body) && log.body.length >= 1, 'есть запись истории');
assert.equal(log.body[0].action, 'grant');
const t = await inject('GET', `/api/access/log?content_type=textbook&content_ref=${HUB}`, null, teacher.token);
assert.equal(t.status, 403, 'учителю недоступно');
});
it('DELETE /api/classes/:id чистит правила класса (через purgeAccessFor)', async () => {
setRule('class', classId, 1);
const del = await inject('DELETE', `/api/classes/${classId}`, null, teacher.token);