Files

46 lines
2.8 KiB
JavaScript
Raw Permalink Normal View History

/**
* 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) }));
}