46 lines
2.8 KiB
JavaScript
46 lines
2.8 KiB
JavaScript
/**
|
|||
|
|
* lib/audit.mjs — 감사 기록(§24.2).
|
||
|
|
*
|
||
|
|
* 누가(actor)·언제·어떤 그룹과 대상(subject)에서·무엇을(action)·자동/사용자 작업인지·결과(outcome)를 남긴다.
|
||
|
|
* 비밀값은 redact() 로 거른다. 권한·소유권·배포·해지·복원·이전·결제 승인은 immutable=1 로 두어 삭제 API 가 거부한다.
|
||
|
|
*/
|
||
|
|
import { newId } from './crypto.mjs';
|
||
|
|
import { redact } from './redact.mjs';
|
||
|
|
import { nowIso, toJson, parseJson } from './db.mjs';
|
||
|
|
|
||
|
|
export const MIGRATIONS = [{ id: 'lib_audit_0001', sql: `
|
||
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||
|
|
id TEXT PRIMARY KEY, at TEXT NOT NULL, actor_type TEXT NOT NULL, actor_id TEXT, actor_label TEXT, group_id TEXT,
|
||
|
|
subject_type TEXT, subject_id TEXT, action TEXT NOT NULL, outcome TEXT NOT NULL, automatic INTEGER NOT NULL DEFAULT 0,
|
||
|
|
immutable INTEGER NOT NULL DEFAULT 0, detail_json TEXT, ip TEXT
|
||
|
|
);
|
||
|
|
CREATE INDEX IF NOT EXISTS audit_group ON audit_log(group_id, at);
|
||
|
|
CREATE INDEX IF NOT EXISTS audit_subject ON audit_log(subject_type, subject_id, at);` }];
|
||
|
|
|
||
|
|
/** 삭제할 수 없는 기록으로 남기는 행동 접두어 */
|
||
|
|
const IMMUTABLE_PREFIX = /^(permission|ownership|deploy|cancel|terminate|restore|relocate|order\.approve|order\.reject|payment|refund|member\.role|site_count|emergency|recovery)/;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 기록 남기기.
|
||
|
|
* @param {object} store lib/db
|
||
|
|
* @param {object} e { actorType: 'user'|'operator'|'system'|'agent', actorId, actorLabel, groupId, subjectType, subjectId, action, outcome: 'ok'|'failed'|'cancelled'|'denied', automatic, detail, ip }
|
||
|
|
*/
|
||
|
|
export function audit(store, e) {
|
||
|
|
const id = newId('a_');
|
||
|
|
store.run('INSERT INTO audit_log(id, at, actor_type, actor_id, actor_label, group_id, subject_type, subject_id, action, outcome, automatic, immutable, detail_json, ip) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
|
||
|
|
id, nowIso(), e.actorType || 'system', e.actorId || null, e.actorLabel || null, e.groupId || null, e.subjectType || null, e.subjectId || null, e.action, e.outcome || 'ok',
|
||
|
|
e.automatic ? 1 : 0, e.immutable || IMMUTABLE_PREFIX.test(e.action) ? 1 : 0, toJson(redact(e.detail || null)), e.ip || null);
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
/** 조회(그룹·대상·행동·기간 필터) */
|
||
|
|
export function listAudit(store, { groupId, subjectType, subjectId, action, since, until, limit = 200 } = {}) {
|
||
|
|
const w = []; const p = [];
|
||
|
|
if (groupId) { w.push('group_id=?'); p.push(groupId); }
|
||
|
|
if (subjectType) { w.push('subject_type=?'); p.push(subjectType); }
|
||
|
|
if (subjectId) { w.push('subject_id=?'); p.push(subjectId); }
|
||
|
|
if (action) { w.push('action LIKE ?'); p.push(action + '%'); }
|
||
|
|
if (since) { w.push('at>=?'); p.push(since); }
|
||
|
|
if (until) { w.push('at<=?'); p.push(until); }
|
||
|
|
return store.all(`SELECT * FROM audit_log ${w.length ? 'WHERE ' + w.join(' AND ') : ''} ORDER BY at DESC LIMIT ?`, ...p, Math.min(limit, 1000)).map((r) => ({ ...r, detail: parseJson(r.detail_json, null) }));
|
||
|
|
}
|