1
0
forked from baron/baron-sso

최근 변경된 앱 대시보드 추가

This commit is contained in:
2026-06-01 15:16:21 +09:00
parent c4487b9334
commit d40e443d48
7 changed files with 1034 additions and 649 deletions

View File

@@ -0,0 +1,183 @@
import {
formatAuditValue,
parseAuditDetails,
resolveAuditActor,
type AuditDetails,
type CommonAuditLog,
} from "../../../../common/core/audit";
import type { ClientSummary, DevAuditLog } from "../../lib/devApi";
export type RecentClientChange = {
eventId: string;
clientId: string;
clientName: string;
actorId: string;
action: string;
actionLabel: string;
timestamp: string;
detailLabels: Array<{ label: string; value: string }>;
};
const recentClientActions = new Set([
"CREATE_CLIENT",
"UPDATE_CLIENT",
"UPDATE_CLIENT_STATUS",
"ROTATE_SECRET",
"ADD_RELATION",
"REMOVE_RELATION",
"DELETE_CLIENT",
]);
const recentClientFieldLabels: Record<string, string> = {
name: "이름",
type: "유형",
status: "상태",
scopes: "스코프",
tenant_access_restricted: "테넌트 접근 제한",
allowed_tenants: "허용 테넌트",
id_token_claims: "커스텀 클레임",
token_endpoint_auth_method: "인증 방식",
jwks_uri: "JWKS URI",
backchannel_logout_uri: "Backchannel Logout URI",
backchannel_logout_session_required: "세션 필수",
headless_login_enabled: "헤드리스 로그인",
headless_token_endpoint_auth_method: "헤드리스 인증 방식",
headless_jwks_uri: "헤드리스 JWKS URI",
redirect_uri_count: "Redirect URI 수",
scope_count: "Scope 수",
relation: "관계",
subject: "대상",
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
export function getRecentClientActionLabel(action: string) {
switch (action) {
case "CREATE_CLIENT":
return "클라이언트 생성";
case "UPDATE_CLIENT":
return "설정 변경";
case "UPDATE_CLIENT_STATUS":
return "상태 변경";
case "ROTATE_SECRET":
return "클라이언트 시크릿 재발급";
case "ADD_RELATION":
return "관계 추가";
case "REMOVE_RELATION":
return "관계 삭제";
case "DELETE_CLIENT":
return "클라이언트 삭제";
default:
return action;
}
}
export function buildRecentClientChangeDetails(
action: string,
details: AuditDetails,
) {
const before = isRecord(details.before) ? details.before : {};
const after = isRecord(details.after) ? details.after : {};
if (action === "ROTATE_SECRET") {
return [{ label: "클라이언트 시크릿", value: "재발급" }];
}
if (action === "ADD_RELATION" || action === "REMOVE_RELATION") {
const source = action === "ADD_RELATION" ? after : before;
return [
...(source.relation
? [{ label: "관계", value: formatAuditValue(source.relation) }]
: []),
...(source.subject
? [{ label: "대상", value: formatAuditValue(source.subject) }]
: []),
];
}
const keys = Array.from(
new Set([...Object.keys(before), ...Object.keys(after)]),
);
const changes = keys
.map((key) => {
const beforeValue = before[key];
const afterValue = after[key];
if (action !== "CREATE_CLIENT" && action !== "DELETE_CLIENT") {
if (JSON.stringify(beforeValue) === JSON.stringify(afterValue)) {
return null;
}
}
const label = recentClientFieldLabels[key] ?? key;
if (action === "CREATE_CLIENT") {
if (afterValue === undefined) {
return null;
}
return { label, value: formatAuditValue(afterValue) };
}
if (action === "DELETE_CLIENT") {
if (beforeValue === undefined) {
return null;
}
return { label, value: formatAuditValue(beforeValue) };
}
if (beforeValue === undefined && afterValue === undefined) {
return null;
}
if (beforeValue === undefined) {
return { label, value: formatAuditValue(afterValue) };
}
if (afterValue === undefined) {
return { label, value: formatAuditValue(beforeValue) };
}
return {
label,
value: `${formatAuditValue(beforeValue)}${formatAuditValue(afterValue)}`,
};
})
.filter((item): item is { label: string; value: string } => Boolean(item));
return changes.slice(0, 3);
}
export function buildRecentClientChanges(
auditLogs: DevAuditLog[],
clients: ClientSummary[],
) {
const clientNameById = new Map(
clients.map((client) => [client.id, client.name || client.id]),
);
return auditLogs
.map((item) => {
const details = parseAuditDetails(item.details);
const action = details.action || "";
const clientId = String(details.target_id || "");
if (!recentClientActions.has(action) || !clientId) {
return null;
}
return {
eventId: item.event_id,
clientId,
clientName: clientNameById.get(clientId) || clientId,
actorId: resolveAuditActor(
item as Pick<CommonAuditLog, "user_id">,
details,
),
action,
actionLabel: getRecentClientActionLabel(action),
timestamp: item.timestamp,
detailLabels: buildRecentClientChangeDetails(action, details),
} satisfies RecentClientChange;
})
.filter((item): item is RecentClientChange => Boolean(item))
.sort(
(left, right) =>
new Date(right.timestamp).getTime() -
new Date(left.timestamp).getTime(),
);
}