Files
AriWeb-Suite/portal/auditui/api.mjs

13 lines
3.0 KiB
JavaScript

/**
* portal/auditui/api.mjs — 활동·감사 기록 화면(§24.2). 현재 그룹의 기록만, 비밀값 없음(lib/audit 가 기록 시 redact).
* 필터: 행동(action 접두어)·대상 종류·기간. 삭제 불가 기록(immutable)과 자동 작업을 표시한다.
*/
import { requireGroup } from '../auth/guard.mjs';
import { listAudit } from '../../lib/audit.mjs';
import { t } from '../../lib/i18n.mjs';
export async function register(app, deps) {
app.get('/activity', (ctx) => { requireGroup(ctx); return ctx.html(deps.shell(ctx, { title: t('activity.title'), nav: null, crumbs: [{ label: t('activity.title') }], body: `<div class="card"><form id="af" class="actions" onsubmit="return false"><input name="action" placeholder="행동(예: deploy, member)" style="max-width:200px"><input name="subjectType" placeholder="대상 종류(site, user…)" style="max-width:180px"><input name="since" type="date"><input name="until" type="date"><button class="btn">조회</button></form></div><div id="alist">${deps.statePanel('loading')}</div>
<script>(()=>{const f=document.getElementById('af'),el=document.getElementById('alist');async function load(){const q=new URLSearchParams(Object.fromEntries([...new FormData(f)].filter(([k,v])=>v)));try{const d=await aw.api('GET','/api/audit?'+q);if(!d.items.length)return aw.state(el,'empty');el.innerHTML='<table class="r"><thead><tr><th>언제</th><th>누가</th><th>무엇을</th><th>대상</th><th>결과</th></tr></thead><tbody>'+d.items.map(a=>'<tr><td data-l="언제">'+aw.esc(aw.fmt(a.at))+'</td><td data-l="누가">'+(a.automatic?'<span class="badge">⚙ 자동</span>':'<span class="badge">👤 '+aw.esc(a.actor)+'</span>')+'</td><td data-l="무엇을">'+aw.esc(a.action)+(a.immutable?' <span class="badge" title="삭제 불가 기록">🔒</span>':'')+(a.detail?'<div class="sub mono">'+aw.esc(JSON.stringify(a.detail)).slice(0,300)+'</div>':'')+'</td><td data-l="대상">'+aw.esc((a.subjectType||'')+' '+(a.subjectId||''))+'</td><td data-l="결과">'+(a.outcome==='ok'?'<span class="badge ok">✔ 성공</span>':'<span class="badge failed">✘ '+aw.esc(a.outcome)+'</span>')+'</td></tr>').join('')+'</tbody></table>'}catch(e){aw.state(el,e.status===403?'forbidden':'failed',e.message,{retry:load})}}f.onsubmit=()=>{load();return false};load()})();</script>` })); });
app.get('/api/audit', (ctx) => { requireGroup(ctx); const items = listAudit(deps.store, { groupId: ctx.group.id, action: ctx.query.action || undefined, subjectType: ctx.query.subjectType || undefined, since: ctx.query.since ? ctx.query.since + 'T00:00:00.000Z' : undefined, until: ctx.query.until ? ctx.query.until + 'T23:59:59.999Z' : undefined, limit: Number(ctx.query.limit || 200) }).map((a) => ({ id: a.id, at: a.at, actor: a.actor_label || a.actor_type, actorType: a.actor_type, automatic: !!a.automatic || a.actor_type === 'system', action: a.action, outcome: a.outcome, subjectType: a.subject_type, subjectId: a.subject_id, immutable: !!a.immutable, detail: a.detail })); return { ok: true, items }; });
}