Ariweb Suite 0.1.1 기반: 공통 라이브러리(db·http·세션·작업 엔진·할당/예약·알림·감사·보관소), 포털 뼈대, Webmaker/AriMail 도입 소스와 manifest
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
9ca4cee341
commit
c7aea8d419
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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) }));
|
||||
}
|
||||
Reference in New Issue
Block a user