From 2abeb391bdcfb890b66850017e94f431828d4fba Mon Sep 17 00:00:00 2001 From: jlightner Date: Sat, 4 Apr 2026 06:29:47 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Added=20AdminAuditLog=20page=20with=20p?= =?UTF-8?q?aginated=20impersonation=20log=20table,=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "frontend/src/pages/AdminAuditLog.tsx" - "frontend/src/pages/AdminAuditLog.module.css" - "frontend/src/App.tsx" - "frontend/src/components/AdminDropdown.tsx" GSD-Task: S07/T03 --- frontend/src/App.tsx | 2 + frontend/src/components/AdminDropdown.tsx | 8 ++ frontend/src/pages/AdminAuditLog.module.css | 133 ++++++++++++++++++++ frontend/src/pages/AdminAuditLog.tsx | 104 +++++++++++++++ frontend/tsconfig.app.tsbuildinfo | 2 +- 5 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/AdminAuditLog.module.css create mode 100644 frontend/src/pages/AdminAuditLog.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 09d5681..2170069 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,6 +20,7 @@ const CreatorSettings = React.lazy(() => import("./pages/CreatorSettings")); const ConsentDashboard = React.lazy(() => import("./pages/ConsentDashboard")); const WatchPage = React.lazy(() => import("./pages/WatchPage")); const AdminUsers = React.lazy(() => import("./pages/AdminUsers")); +const AdminAuditLog = React.lazy(() => import("./pages/AdminAuditLog")); const ChatPage = React.lazy(() => import("./pages/ChatPage")); const ChapterReview = React.lazy(() => import("./pages/ChapterReview")); import AdminDropdown from "./components/AdminDropdown"; @@ -187,6 +188,7 @@ function AppShell() { }>} /> }>} /> }>} /> + }>} /> {/* Info routes */} }>} /> diff --git a/frontend/src/components/AdminDropdown.tsx b/frontend/src/components/AdminDropdown.tsx index 16d2cb8..be43a49 100644 --- a/frontend/src/components/AdminDropdown.tsx +++ b/frontend/src/components/AdminDropdown.tsx @@ -118,6 +118,14 @@ export default function AdminDropdown() { > Users + setOpen(false)} + > + Audit Log + )} diff --git a/frontend/src/pages/AdminAuditLog.module.css b/frontend/src/pages/AdminAuditLog.module.css new file mode 100644 index 0000000..25adbc8 --- /dev/null +++ b/frontend/src/pages/AdminAuditLog.module.css @@ -0,0 +1,133 @@ +.page { + max-width: 900px; + margin: 0 auto; + padding: 2rem 1rem; +} + +.title { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + color: var(--text-primary, #e2e8f0); +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.table th { + text-align: left; + padding: 0.6rem 0.75rem; + border-bottom: 2px solid var(--color-border, #2d2d3d); + color: var(--text-secondary, #828291); + font-weight: 600; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.table td { + padding: 0.6rem 0.75rem; + border-bottom: 1px solid var(--color-border, #2d2d3d); + color: var(--text-primary, #e2e8f0); +} + +.actionBadge { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.actionBadge[data-action="start"] { + background: rgba(34, 211, 238, 0.15); + color: #22d3ee; +} + +.actionBadge[data-action="stop"] { + background: rgba(148, 163, 184, 0.15); + color: #94a3b8; +} + +.writeBadge { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.writeBadge[data-write="yes"] { + background: rgba(239, 68, 68, 0.15); + color: #ef4444; +} + +.writeBadge[data-write="no"] { + background: rgba(148, 163, 184, 0.1); + color: #828291; +} + +.ip { + font-family: monospace; + font-size: 0.82rem; +} + +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + margin-top: 1.5rem; +} + +.pageBtn { + padding: 0.35rem 0.75rem; + border: 1px solid var(--color-border, #2d2d3d); + border-radius: 4px; + background: transparent; + color: var(--text-primary, #e2e8f0); + font-size: 0.85rem; + cursor: pointer; + transition: background 150ms, border-color 150ms; +} + +.pageBtn:hover:not(:disabled) { + border-color: var(--color-accent, #22d3ee); + background: rgba(34, 211, 238, 0.08); +} + +.pageBtn:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.pageNum { + font-size: 0.85rem; + color: var(--text-secondary, #828291); +} + +.loading, +.error, +.empty { + text-align: center; + padding: 3rem 1rem; + color: var(--text-secondary, #828291); +} + +.error { + color: #ef4444; +} + +@media (max-width: 600px) { + .table th:nth-child(6), + .table td:nth-child(6) { + display: none; + } +} diff --git a/frontend/src/pages/AdminAuditLog.tsx b/frontend/src/pages/AdminAuditLog.tsx new file mode 100644 index 0000000..26faf9e --- /dev/null +++ b/frontend/src/pages/AdminAuditLog.tsx @@ -0,0 +1,104 @@ +import { useEffect, useState } from "react"; +import { useAuth } from "../context/AuthContext"; +import { fetchImpersonationLog, type ImpersonationLogEntry } from "../api/auth"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; +import styles from "./AdminAuditLog.module.css"; + +export default function AdminAuditLog() { + useDocumentTitle("Audit Log — Admin"); + const { token } = useAuth(); + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + + useEffect(() => { + if (!token) return; + setLoading(true); + setError(null); + fetchImpersonationLog(token, page) + .then(setEntries) + .catch((e) => setError(e.message || "Failed to load audit log")) + .finally(() => setLoading(false)); + }, [token, page]); + + if (loading) { + return ( +
+

Audit Log

+

Loading audit log…

+
+ ); + } + + if (error) { + return ( +
+

Audit Log

+

{error}

+
+ ); + } + + return ( +
+

Audit Log

+ {entries.length === 0 ? ( +

No impersonation log entries found.

+ ) : ( + + + + + + + + + + + + + {entries.map((entry) => ( + + + + + + + + + ))} + +
Date/TimeAdminTarget UserActionWrite ModeIP Address
{new Date(entry.created_at).toLocaleString()}{entry.admin_name}{entry.target_name} + + {entry.action} + + + + {entry.write_mode ? "Yes" : "No"} + + {entry.ip_address ?? "—"}
+ )} +
+ + Page {page} + +
+
+ ); +} diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index c248a2a..6fd5928 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/index.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PlayerControls.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"errors":true,"version":"5.6.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/chat.ts","./src/api/client.ts","./src/api/consent.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/index.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/AudioWaveform.tsx","./src/components/CategoryIcons.tsx","./src/components/ChapterMarkers.tsx","./src/components/ConfirmModal.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ImpersonationBanner.tsx","./src/components/PlayerControls.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/ToggleSwitch.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminAuditLog.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/AdminUsers.tsx","./src/pages/ChapterReview.tsx","./src/pages/ChatPage.tsx","./src/pages/ConsentDashboard.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"errors":true,"version":"5.6.3"} \ No newline at end of file