feat: Added AdminAuditLog page with paginated impersonation log table,…
- "frontend/src/pages/AdminAuditLog.tsx" - "frontend/src/pages/AdminAuditLog.module.css" - "frontend/src/App.tsx" - "frontend/src/components/AdminDropdown.tsx" GSD-Task: S07/T03
This commit is contained in:
parent
944e152c6f
commit
2abeb391bd
5 changed files with 248 additions and 1 deletions
|
|
@ -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() {
|
|||
<Route path="/admin/pipeline" element={<Suspense fallback={<LoadingFallback />}><AdminPipeline /></Suspense>} />
|
||||
<Route path="/admin/techniques" element={<Suspense fallback={<LoadingFallback />}><AdminTechniquePages /></Suspense>} />
|
||||
<Route path="/admin/users" element={<Suspense fallback={<LoadingFallback />}><AdminUsers /></Suspense>} />
|
||||
<Route path="/admin/audit-log" element={<Suspense fallback={<LoadingFallback />}><AdminAuditLog /></Suspense>} />
|
||||
|
||||
{/* Info routes */}
|
||||
<Route path="/about" element={<Suspense fallback={<LoadingFallback />}><About /></Suspense>} />
|
||||
|
|
|
|||
|
|
@ -118,6 +118,14 @@ export default function AdminDropdown() {
|
|||
>
|
||||
Users
|
||||
</Link>
|
||||
<Link
|
||||
to="/admin/audit-log"
|
||||
className="admin-dropdown__item"
|
||||
role="menuitem"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Audit Log
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
133
frontend/src/pages/AdminAuditLog.module.css
Normal file
133
frontend/src/pages/AdminAuditLog.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
104
frontend/src/pages/AdminAuditLog.tsx
Normal file
104
frontend/src/pages/AdminAuditLog.tsx
Normal file
|
|
@ -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<ImpersonationLogEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className={styles.page}>
|
||||
<h1 className={styles.title}>Audit Log</h1>
|
||||
<p className={styles.loading}>Loading audit log…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<h1 className={styles.title}>Audit Log</h1>
|
||||
<p className={styles.error}>{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<h1 className={styles.title}>Audit Log</h1>
|
||||
{entries.length === 0 ? (
|
||||
<p className={styles.empty}>No impersonation log entries found.</p>
|
||||
) : (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date/Time</th>
|
||||
<th>Admin</th>
|
||||
<th>Target User</th>
|
||||
<th>Action</th>
|
||||
<th>Write Mode</th>
|
||||
<th>IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry) => (
|
||||
<tr key={entry.id}>
|
||||
<td>{new Date(entry.created_at).toLocaleString()}</td>
|
||||
<td>{entry.admin_name}</td>
|
||||
<td>{entry.target_name}</td>
|
||||
<td>
|
||||
<span className={styles.actionBadge} data-action={entry.action}>
|
||||
{entry.action}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={styles.writeBadge}
|
||||
data-write={entry.write_mode ? "yes" : "no"}
|
||||
>
|
||||
{entry.write_mode ? "Yes" : "No"}
|
||||
</span>
|
||||
</td>
|
||||
<td className={styles.ip}>{entry.ip_address ?? "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<div className={styles.pagination}>
|
||||
<button
|
||||
className={styles.pageBtn}
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
<span className={styles.pageNum}>Page {page}</span>
|
||||
<button
|
||||
className={styles.pageBtn}
|
||||
disabled={entries.length === 0}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"}
|
||||
{"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"}
|
||||
Loading…
Add table
Reference in a new issue