+ {t(
+ "msg.dev.audit.forbidden",
+ "감사 로그를 조회할 권한이 없습니다. 관리자에게 권한을 요청해주세요.",
+ )}
+
+ );
+ }
+
+ const errMsg =
+ axiosError.response?.data?.error ?? (query.error as Error).message;
+ return (
+
+ {t("msg.dev.audit.load_error", "Error loading logs: {{error}}", {
+ error: errMsg,
+ })}
+
+ );
+ }
+
return (
-
-
-
-
- Audit stream
-
-
- Observe admin actions per tenant
-
-
- ClickHouse-backed feed. Filter by tenant, actor, action, and
- rate-limit status. Enforce admin-only access under /admin.
-
-
-
-
-
-
-
+
+
+
+
+
+ {t("ui.dev.audit.registry.title", "Audit registry")}
+
+
+ {t("ui.dev.audit.title", "Audit Logs")}
+
+
+ {t(
+ "msg.dev.audit.subtitle",
+ "Shows DevFront activity history within current tenant/app scope.",
+ )}
+
+
+
+
+ {t("msg.dev.audit.loaded_count", "Loaded {{count}} rows", {
+ count: logs.length,
+ })}
+
+
+
+
+
+
+
-
-
-
-
-
- Try: tenant:TENANT-12 action:client.*
-
-
-
- {auditFilters.map((filter) => (
-
-
- {filter}
-
- ))}
-
-
- {auditRows.map((row) => (
-
-
{row.action}
-
{row.tenant}
-
{row.actor}
-
-
+
+
+
+ {t("ui.dev.audit.table.time", "Time")}
+
+
+ {t("ui.dev.audit.table.actor", "Actor")}
+
+
+ {t("ui.dev.audit.table.action", "Action")}
+
+
+ {t("ui.dev.audit.table.target", "Target")}
+
+
+ {t("ui.dev.audit.table.status", "Status")}
+
+
+
+
+
+ {logs.length === 0 && (
+
+
- {row.result}
-
-
{row.ts}
-
-
- ))}
-
-
+ {t("msg.dev.audit.empty", "No audit logs found.")}
+
+
+ )}
+ {logs.map((row, index) => {
+ const details = parseDetails(row.details);
+ const actionLabel = details.action || row.event_type;
+ const targetValue = details.target_id || "-";
+ const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
+ const expanded = Boolean(expandedRows[rowKey]);
+ return (
+
+
+
+ {formatDateTime(row.timestamp)}
+
+
+
+ {row.user_id || "-"}
+ {row.user_id ? (
+
+ ) : null}
+
+
+ {actionLabel}
+
+
+ {targetValue}
+ {targetValue !== "-" ? (
+
+ ) : null}
+
+
+
+
+ {row.status}
+
+
+
+
+
+
+ {expanded ? (
+
+
+
+
+
+ Request ID: {formatValue(details.request_id)}
+
+
Method: {formatValue(details.method)}
+
Path: {formatValue(details.path)}
+
+ Tenant: {formatValue(details.tenant_id)}
+
+
+
+
Before: {formatValue(details.before)}
+
After: {formatValue(details.after)}
+
Error: {formatValue(details.error)}
+
+
+
+
+ ) : null}
+
+ );
+ })}
+
+
-
-
-
- Guard rails
-
-
Tenant admin only
-
- Enforce Tenant Admin middleware and admin session TTL before
- surfacing any audit feed. Super Admin role can bypass tenant
- filter when needed.
-
-
-
-
- Export rules
-
-
- Rate-limit sensitive exports
-
-
- Keep export endpoints behind admin-only routes with ClickHouse
- query limits. Log download attempts with IP, role, and tenant
- scope.
-
-
-
-
+ {query.hasNextPage ? (
+
+
+
+ ) : null}
+
+
);
}
diff --git a/devfront/src/features/auth/AuthCallbackPage.tsx b/devfront/src/features/auth/AuthCallbackPage.tsx
index 339dade5..929bc7b4 100644
--- a/devfront/src/features/auth/AuthCallbackPage.tsx
+++ b/devfront/src/features/auth/AuthCallbackPage.tsx
@@ -10,7 +10,9 @@ export default function AuthCallbackPage() {
useEffect(() => {
// 팝업으로 열린 경우 signinPopupCallback 처리
if (window.opener) {
- userManager.signinPopupCallback();
+ userManager.signinPopupCallback().catch((error) => {
+ console.error("Popup callback failed:", error);
+ });
return;
}
diff --git a/devfront/src/features/auth/AuthGuard.tsx b/devfront/src/features/auth/AuthGuard.tsx
index 50abe838..26069583 100644
--- a/devfront/src/features/auth/AuthGuard.tsx
+++ b/devfront/src/features/auth/AuthGuard.tsx
@@ -4,7 +4,7 @@ import { Navigate, Outlet } from "react-router-dom";
export default function AuthGuard() {
const auth = useAuth();
- if (auth.isLoading) {
+ if (auth.isLoading || auth.activeNavigator) {
return
Loading...
;
}
diff --git a/devfront/src/features/auth/LoginPage.tsx b/devfront/src/features/auth/LoginPage.tsx
index 4d592b68..3c87a65e 100644
--- a/devfront/src/features/auth/LoginPage.tsx
+++ b/devfront/src/features/auth/LoginPage.tsx
@@ -1,4 +1,5 @@
import { ExternalLink, LogIn, ShieldHalf } from "lucide-react";
+import { useEffect } from "react";
import { useAuth } from "react-oidc-context";
import { useNavigate } from "react-router-dom";
import { Button } from "../../components/ui/button";
@@ -14,10 +15,15 @@ function LoginPage() {
const auth = useAuth();
const navigate = useNavigate();
+ useEffect(() => {
+ if (auth.isAuthenticated) {
+ navigate("/clients", { replace: true });
+ }
+ }, [auth.isAuthenticated, navigate]);
+
const handleSSOLogin = async () => {
try {
await auth.signinPopup();
- navigate("/clients", { replace: true });
} catch (error) {
console.error("Popup login failed", error);
}
diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx
index c6f3d108..56cb764b 100644
--- a/devfront/src/features/clients/ClientConsentsPage.tsx
+++ b/devfront/src/features/clients/ClientConsentsPage.tsx
@@ -3,6 +3,7 @@ import {
ArrowLeft,
ChevronLeft,
ChevronRight,
+ Download,
Filter,
Search,
} from "lucide-react";
@@ -275,6 +276,7 @@ function ClientConsentsPage() {
onClick={handleExportCSV}
disabled={filteredRows.length === 0}
>
+
{t("ui.dev.clients.consents.export_csv", "Export CSV")}
diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx
index 91bcd6cc..964d4d55 100644
--- a/devfront/src/features/clients/ClientGeneralPage.tsx
+++ b/devfront/src/features/clients/ClientGeneralPage.tsx
@@ -30,6 +30,7 @@ import {
deleteClient,
fetchClient,
updateClient,
+ updateClientStatus,
} from "../../lib/devApi";
import type {
ClientStatus,
@@ -63,6 +64,7 @@ function ClientGeneralPage() {
const [logoUrl, setLogoUrl] = useState("");
const [clientType, setClientType] = useState