[PULSE-13] Org Audit Log UI with advanced filtering #10

Merged
uz1mani merged 9 commits from staging into main 2026-02-05 13:53:56 +00:00
2 changed files with 243 additions and 4 deletions
Showing only changes of commit 54578d00ca - Show all commits

View File

@@ -17,6 +17,7 @@ import {
Organization
} from '@/lib/api/organization'
import { getSubscription, createPortalSession, getInvoices, SubscriptionDetails, Invoice } from '@/lib/api/billing'
import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit'
import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@/lib/utils/authErrors'
import { motion, AnimatePresence } from 'framer-motion'
@@ -30,7 +31,8 @@ import {
Captcha,
BookOpenIcon,
DownloadIcon,
ExternalLinkIcon
ExternalLinkIcon,
LayoutDashboardIcon
} from '@ciphera-net/ui'
// @ts-ignore
import { Button, Input } from '@ciphera-net/ui'
@@ -39,9 +41,9 @@ export default function OrganizationSettings() {
const { user } = useAuth()
const router = useRouter()
const searchParams = useSearchParams()
const [activeTab, setActiveTab] = useState<'general' | 'members' | 'billing'>(() => {
const [activeTab, setActiveTab] = useState<'general' | 'members' | 'billing' | 'audit'>(() => {
const tab = searchParams.get('tab')
return tab === 'billing' || tab === 'members' ? tab : 'general'
return tab === 'billing' || tab === 'members' || tab === 'audit' ? tab : 'general'
})
const [showDeletePrompt, setShowDeletePrompt] = useState(false)
@@ -77,6 +79,16 @@ export default function OrganizationSettings() {
const [orgSlug, setOrgSlug] = useState('')
greptile-apps[bot] commented 2026-02-05 13:36:33 +00:00 (Migrated from github.com)
Review

this useEffect is unnecessary - refs can be updated directly during render without causing issues

  // Update refs when state changes (no useEffect needed)
  filtersRef.current = {
    action: auditActionFilter,
    logId: auditLogIdFilter,
    startDate: auditStartDate,
    endDate: auditEndDate
  }
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 103:110

Comment:
this `useEffect` is unnecessary - refs can be updated directly during render without causing issues

```suggestion
  // Update refs when state changes (no useEffect needed)
  filtersRef.current = {
    action: auditActionFilter,
    logId: auditLogIdFilter,
    startDate: auditStartDate,
    endDate: auditEndDate
  }
```

How can I resolve this? If you propose a fix, please make it concise.
this `useEffect` is unnecessary - refs can be updated directly during render without causing issues ```suggestion // Update refs when state changes (no useEffect needed) filtersRef.current = { action: auditActionFilter, logId: auditLogIdFilter, startDate: auditStartDate, endDate: auditEndDate } ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 103:110 Comment: this `useEffect` is unnecessary - refs can be updated directly during render without causing issues ```suggestion // Update refs when state changes (no useEffect needed) filtersRef.current = { action: auditActionFilter, logId: auditLogIdFilter, startDate: auditStartDate, endDate: auditEndDate } ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-05 13:39:46 +00:00 (Migrated from github.com)
Review

Issue: Unnecessary useEffect for updating filtersRef.
Fix: Removed the useEffect and now updating filtersRef.current synchronously during the render cycle.
Why: Refs can be safely updated during render for this use case, avoiding an extra effect hook and simplifying the component lifecycle.

Issue: Unnecessary useEffect for updating filtersRef. Fix: Removed the useEffect and now updating filtersRef.current synchronously during the render cycle. Why: Refs can be safely updated during render for this use case, avoiding an extra effect hook and simplifying the component lifecycle.
const [isSaving, setIsSaving] = useState(false)
// Audit log State
const [auditEntries, setAuditEntries] = useState<AuditLogEntry[]>([])
const [auditTotal, setAuditTotal] = useState(0)
const [isLoadingAudit, setIsLoadingAudit] = useState(false)
const [auditPage, setAuditPage] = useState(0)
const auditPageSize = 20
const [auditActionFilter, setAuditActionFilter] = useState('')
const [auditStartDate, setAuditStartDate] = useState('')
const [auditEndDate, setAuditEndDate] = useState('')
const getOrgIdFromToken = () => {
return user?.org_id || null
}
@@ -141,7 +153,7 @@ export default function OrganizationSettings() {
useEffect(() => {
const tab = searchParams.get('tab')
if ((tab === 'billing' || tab === 'members') && tab !== activeTab) {
if ((tab === 'billing' || tab === 'members' || tab === 'audit') && tab !== activeTab) {
setActiveTab(tab)
}
}, [searchParams, activeTab])
@@ -153,6 +165,34 @@ export default function OrganizationSettings() {
}
}, [activeTab, currentOrgId, loadSubscription, loadInvoices])
greptile-apps[bot] commented 2026-02-05 13:00:15 +00:00 (Migrated from github.com)
Review

Filters don't trigger reload

In the Audit Log tab, changing auditActionFilter, auditLogIdFilter, auditStartDate, or auditEndDate does not actually refetch data because loadAudit() is only invoked by the useEffect that depends on activeTab/currentOrgId/loadAudit (and loadAudit’s identity doesn’t change when its deps change). As a result, the UI shows updated filter inputs but still displays stale table data until the user changes tabs or pagination. Consider invoking loadAudit() when any filter changes (and resetting auditPage as needed).

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 169:196

Comment:
**Filters don't trigger reload**

In the Audit Log tab, changing `auditActionFilter`, `auditLogIdFilter`, `auditStartDate`, or `auditEndDate` does not actually refetch data because `loadAudit()` is only invoked by the `useEffect` that depends on `activeTab/currentOrgId/loadAudit` (and `loadAudit`’s identity doesn’t change when its deps change). As a result, the UI shows updated filter inputs but still displays stale table data until the user changes tabs or pagination. Consider invoking `loadAudit()` when any filter changes (and resetting `auditPage` as needed).

How can I resolve this? If you propose a fix, please make it concise.
**Filters don't trigger reload** In the Audit Log tab, changing `auditActionFilter`, `auditLogIdFilter`, `auditStartDate`, or `auditEndDate` does not actually refetch data because `loadAudit()` is only invoked by the `useEffect` that depends on `activeTab/currentOrgId/loadAudit` (and `loadAudit`’s identity doesn’t change when its deps change). As a result, the UI shows updated filter inputs but still displays stale table data until the user changes tabs or pagination. Consider invoking `loadAudit()` when any filter changes (and resetting `auditPage` as needed). <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 169:196 Comment: **Filters don't trigger reload** In the Audit Log tab, changing `auditActionFilter`, `auditLogIdFilter`, `auditStartDate`, or `auditEndDate` does not actually refetch data because `loadAudit()` is only invoked by the `useEffect` that depends on `activeTab/currentOrgId/loadAudit` (and `loadAudit`’s identity doesn’t change when its deps change). As a result, the UI shows updated filter inputs but still displays stale table data until the user changes tabs or pagination. Consider invoking `loadAudit()` when any filter changes (and resetting `auditPage` as needed). How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-05 13:02:52 +00:00 (Migrated from github.com)
Review

Introduced a debounced useEffect that listens to changes in auditActionFilter, auditLogIdFilter, auditStartDate, and auditEndDate.
When any filter changes, it now waits 500ms (debounce) and then automatically triggers loadAudit() and resets the page to 0.
Refactored loadAudit to use a useRef for filter values, ensuring the function identity remains stable while always accessing the latest state. This prevents excessive re-renders while ensuring data freshness.

Introduced a debounced useEffect that listens to changes in auditActionFilter, auditLogIdFilter, auditStartDate, and auditEndDate. When any filter changes, it now waits 500ms (debounce) and then automatically triggers loadAudit() and resets the page to 0. Refactored loadAudit to use a useRef for filter values, ensuring the function identity remains stable while always accessing the latest state. This prevents excessive re-renders while ensuring data freshness.
const loadAudit = useCallback(async () => {
if (!currentOrgId) return
setIsLoadingAudit(true)
try {
const params: GetAuditLogParams = {
limit: auditPageSize,
offset: auditPage * auditPageSize,
}
if (auditActionFilter) params.action = auditActionFilter
if (auditStartDate) params.start_date = auditStartDate
if (auditEndDate) params.end_date = auditEndDate
const { entries, total } = await getAuditLog(params)
setAuditEntries(entries)
setAuditTotal(total)
} catch (error) {
console.error('Failed to load audit log', error)
toast.error(getAuthErrorMessage(error as Error) || 'Failed to load audit log')
greptile-apps[bot] commented 2026-02-05 13:28:16 +00:00 (Migrated from github.com)
Review

infinite loop risk: loadAudit in dependency array

since loadAudit depends on auditPage (line 208), changing filters triggers the debounced effect, which calls loadAudit() and resets auditPage to 0 (line 215). this causes loadAudit to be recreated, potentially re-triggering the effect

remove loadAudit from dependencies since you're already using filtersRef to access latest values:

  useEffect(() => {
    if (activeTab !== 'audit') return
    
    const timer = setTimeout(() => {
        setAuditPage(0)
        loadAudit()
    }, 500)
    return () => clearTimeout(timer)
  }, [auditActionFilter, auditLogIdFilter, auditStartDate, auditEndDate, activeTab])
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 211:219

Comment:
infinite loop risk: `loadAudit` in dependency array

since `loadAudit` depends on `auditPage` (line 208), changing filters triggers the debounced effect, which calls `loadAudit()` and resets `auditPage` to 0 (line 215). this causes `loadAudit` to be recreated, potentially re-triggering the effect

remove `loadAudit` from dependencies since you're already using `filtersRef` to access latest values:

```suggestion
  useEffect(() => {
    if (activeTab !== 'audit') return
    
    const timer = setTimeout(() => {
        setAuditPage(0)
        loadAudit()
    }, 500)
    return () => clearTimeout(timer)
  }, [auditActionFilter, auditLogIdFilter, auditStartDate, auditEndDate, activeTab])
```

How can I resolve this? If you propose a fix, please make it concise.
infinite loop risk: `loadAudit` in dependency array since `loadAudit` depends on `auditPage` (line 208), changing filters triggers the debounced effect, which calls `loadAudit()` and resets `auditPage` to 0 (line 215). this causes `loadAudit` to be recreated, potentially re-triggering the effect remove `loadAudit` from dependencies since you're already using `filtersRef` to access latest values: ```suggestion useEffect(() => { if (activeTab !== 'audit') return const timer = setTimeout(() => { setAuditPage(0) loadAudit() }, 500) return () => clearTimeout(timer) }, [auditActionFilter, auditLogIdFilter, auditStartDate, auditEndDate, activeTab]) ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 211:219 Comment: infinite loop risk: `loadAudit` in dependency array since `loadAudit` depends on `auditPage` (line 208), changing filters triggers the debounced effect, which calls `loadAudit()` and resets `auditPage` to 0 (line 215). this causes `loadAudit` to be recreated, potentially re-triggering the effect remove `loadAudit` from dependencies since you're already using `filtersRef` to access latest values: ```suggestion useEffect(() => { if (activeTab !== 'audit') return const timer = setTimeout(() => { setAuditPage(0) loadAudit() }, 500) return () => clearTimeout(timer) }, [auditActionFilter, auditLogIdFilter, auditStartDate, auditEndDate, activeTab]) ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
greptile-apps[bot] commented 2026-02-05 13:28:17 +00:00 (Migrated from github.com)
Review

double API call when page changes

when user clicks pagination, setAuditPage triggers this effect AND the debounced filter effect (211-219) since loadAudit is in both dependency arrays. causes 2 API calls

add auditPage to the debounced effect's dependencies and remove this separate effect

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 221:225

Comment:
double API call when page changes

when user clicks pagination, `setAuditPage` triggers this effect AND the debounced filter effect (211-219) since `loadAudit` is in both dependency arrays. causes 2 API calls

add `auditPage` to the debounced effect's dependencies and remove this separate effect

How can I resolve this? If you propose a fix, please make it concise.
double API call when page changes when user clicks pagination, `setAuditPage` triggers this effect AND the debounced filter effect (211-219) since `loadAudit` is in both dependency arrays. causes 2 API calls add `auditPage` to the debounced effect's dependencies and remove this separate effect <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 221:225 Comment: double API call when page changes when user clicks pagination, `setAuditPage` triggers this effect AND the debounced filter effect (211-219) since `loadAudit` is in both dependency arrays. causes 2 API calls add `auditPage` to the debounced effect's dependencies and remove this separate effect How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-05 13:32:49 +00:00 (Migrated from github.com)
Review

Fix:
Introduced a auditFetchTrigger state to reliably trigger fetches when filters change, even if the page number remains 0.
Debounced Effect: Removed loadAudit from dependencies (fixing the loop). Now it only resets the page and increments the trigger.
Fetch Effect: Merged the pagination logic. It now listens to loadAudit (which changes on page change) AND auditFetchTrigger (which changes on filter change).

Fix: Introduced a auditFetchTrigger state to reliably trigger fetches when filters change, even if the page number remains 0. Debounced Effect: Removed loadAudit from dependencies (fixing the loop). Now it only resets the page and increments the trigger. Fetch Effect: Merged the pagination logic. It now listens to loadAudit (which changes on page change) AND auditFetchTrigger (which changes on filter change).
uz1mani commented 2026-02-05 13:32:58 +00:00 (Migrated from github.com)
Review

Fix:
Introduced a auditFetchTrigger state to reliably trigger fetches when filters change, even if the page number remains 0.
Debounced Effect: Removed loadAudit from dependencies (fixing the loop). Now it only resets the page and increments the trigger.
Fetch Effect: Merged the pagination logic. It now listens to loadAudit (which changes on page change) AND auditFetchTrigger (which changes on filter change).

Fix: Introduced a auditFetchTrigger state to reliably trigger fetches when filters change, even if the page number remains 0. Debounced Effect: Removed loadAudit from dependencies (fixing the loop). Now it only resets the page and increments the trigger. Fetch Effect: Merged the pagination logic. It now listens to loadAudit (which changes on page change) AND auditFetchTrigger (which changes on filter change).
} finally {
setIsLoadingAudit(false)
}
}, [currentOrgId, auditPage, auditActionFilter, auditStartDate, auditEndDate])
useEffect(() => {
if (activeTab === 'audit' && currentOrgId) {
loadAudit()
}
}, [activeTab, currentOrgId, loadAudit])
// If no org ID, we are in personal workspace, so don't show org settings
if (!currentOrgId) {
return (
@@ -309,6 +349,17 @@ export default function OrganizationSettings() {
<BoxIcon className="w-5 h-5" />
Billing
</button>
<button
onClick={() => setActiveTab('audit')}
className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 ${
activeTab === 'audit'
? 'bg-brand-orange/10 text-brand-orange'
: 'text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800'
}`}
>
<LayoutDashboardIcon className="w-5 h-5" />
Audit log
</button>
</nav>
{/* Content Area */}
@@ -734,6 +785,125 @@ export default function OrganizationSettings() {
)}
</div>
)}
{activeTab === 'audit' && (
<div className="space-y-12">
<div>
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-1">Audit log</h2>
<p className="text-sm text-neutral-500 dark:text-neutral-400">Who did what and when for this organization.</p>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-4 items-end">
<div className="space-y-1">
<label className="block text-xs font-medium text-neutral-500 uppercase">Action</label>
<input
type="text"
placeholder="e.g. site_created"
value={auditActionFilter}
onChange={(e) => setAuditActionFilter(e.target.value)}
className="w-40 px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-neutral-900 dark:text-white"
/>
</div>
<div className="space-y-1">
<label className="block text-xs font-medium text-neutral-500 uppercase">From date</label>
<input
type="date"
value={auditStartDate}
onChange={(e) => setAuditStartDate(e.target.value)}
className="px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-neutral-900 dark:text-white"
/>
</div>
<div className="space-y-1">
<label className="block text-xs font-medium text-neutral-500 uppercase">To date</label>
<input
type="date"
value={auditEndDate}
onChange={(e) => setAuditEndDate(e.target.value)}
className="px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-neutral-900 dark:text-white"
/>
</div>
<Button
variant="secondary"
onClick={() => {
setAuditPage(0)
loadAudit()
}}
disabled={isLoadingAudit}
>
Apply
</Button>
</div>
{/* Table */}
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden">
{isLoadingAudit ? (
<div className="p-12 text-center text-neutral-500">
<div className="animate-spin w-6 h-6 border-2 border-neutral-400 border-t-transparent rounded-full mx-auto mb-3"></div>
Loading audit log...
</div>
) : auditEntries.length === 0 ? (
<div className="p-8 text-center text-neutral-500">No audit events found.</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800/50">
<th className="text-left px-4 py-3 font-medium text-neutral-700 dark:text-neutral-300">Time</th>
<th className="text-left px-4 py-3 font-medium text-neutral-700 dark:text-neutral-300">Actor</th>
<th className="text-left px-4 py-3 font-medium text-neutral-700 dark:text-neutral-300">Action</th>
<th className="text-left px-4 py-3 font-medium text-neutral-700 dark:text-neutral-300">Resource</th>
<th className="text-left px-4 py-3 font-medium text-neutral-700 dark:text-neutral-300">ID</th>
</tr>
</thead>
<tbody>
{auditEntries.map((entry) => (
<tr key={entry.id} className="border-b border-neutral-100 dark:border-neutral-800 hover:bg-neutral-50 dark:hover:bg-neutral-800/30">
<td className="px-4 py-3 text-neutral-600 dark:text-neutral-400 whitespace-nowrap">
{new Date(entry.occurred_at).toLocaleString()}
</td>
<td className="px-4 py-3 text-neutral-900 dark:text-white">
{entry.actor_id ? entry.actor_id : 'System'}
</td>
<td className="px-4 py-3 font-medium text-neutral-900 dark:text-white">{entry.action}</td>
<td className="px-4 py-3 text-neutral-600 dark:text-neutral-400">{entry.resource_type}</td>
<td className="px-4 py-3 text-neutral-500 dark:text-neutral-500 font-mono text-xs">{entry.resource_id || ''}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Pagination */}
{auditTotal > auditPageSize && (
<div className="flex items-center justify-between px-4 py-3 border-t border-neutral-200 dark:border-neutral-800">
<span className="text-sm text-neutral-500">
{auditPage * auditPageSize + 1}{Math.min((auditPage + 1) * auditPageSize, auditTotal)} of {auditTotal}
</span>
<div className="flex gap-2">
<Button
variant="ghost"
onClick={() => setAuditPage((p) => Math.max(0, p - 1))}
disabled={auditPage === 0 || isLoadingAudit}
className="text-sm py-2 px-3"
>
Previous
</Button>
<Button
variant="ghost"
onClick={() => setAuditPage((p) => p + 1)}
disabled={(auditPage + 1) * auditPageSize >= auditTotal || isLoadingAudit}
className="text-sm py-2 px-3"
>
Next
</Button>
</div>
</div>
)}
</div>
</div>
)}
</motion.div>
</div>
</div>

69
lib/api/audit.ts Normal file
View File

@@ -0,0 +1,69 @@
/**
greptile-apps[bot] commented 2026-02-05 13:28:18 +00:00 (Migrated from github.com)
Review

auditFetch bypasses the standard client's token refresh

lib/api/client.ts has automatic token refresh on 401 (lines 103-173). this custom fetch duplicates error handling but lacks that retry mechanism. if the access token expires, audit log calls fail instead of auto-refreshing

use apiRequest from client.ts instead (note: may need to adjust endpoint format to match)

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/api/audit.ts
Line: 32:55

Comment:
`auditFetch` bypasses the standard client's token refresh

`lib/api/client.ts` has automatic token refresh on 401 (lines 103-173). this custom fetch duplicates error handling but lacks that retry mechanism. if the access token expires, audit log calls fail instead of auto-refreshing

use `apiRequest` from `client.ts` instead (note: may need to adjust endpoint format to match)

How can I resolve this? If you propose a fix, please make it concise.
`auditFetch` bypasses the standard client's token refresh `lib/api/client.ts` has automatic token refresh on 401 (lines 103-173). this custom fetch duplicates error handling but lacks that retry mechanism. if the access token expires, audit log calls fail instead of auto-refreshing use `apiRequest` from `client.ts` instead (note: may need to adjust endpoint format to match) <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: lib/api/audit.ts Line: 32:55 Comment: `auditFetch` bypasses the standard client's token refresh `lib/api/client.ts` has automatic token refresh on 401 (lines 103-173). this custom fetch duplicates error handling but lacks that retry mechanism. if the access token expires, audit log calls fail instead of auto-refreshing use `apiRequest` from `client.ts` instead (note: may need to adjust endpoint format to match) How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-05 13:32:36 +00:00 (Migrated from github.com)
Review

Replaced the custom auditFetch with the standard apiRequest from lib/api/client.ts, which includes automatic token refresh logic.
Crucial Update in client.ts: Modified apiRequest to support legacy endpoints starting with /api/ (like /api/audit) without double-prepending /api/v1, ensuring compatibility with the backend.

Replaced the custom auditFetch with the standard apiRequest from lib/api/client.ts, which includes automatic token refresh logic. Crucial Update in client.ts: Modified apiRequest to support legacy endpoints starting with /api/ (like /api/audit) without double-prepending /api/v1, ensuring compatibility with the backend.
* Audit log API client (org-scoped; requires org admin role)
*/
import { API_URL } from './client'
export interface AuditLogEntry {
id: string
org_id: string
actor_id?: string
action: string
resource_type: string
resource_id?: string
occurred_at: string
payload?: Record<string, unknown>
}
export interface GetAuditLogParams {
limit?: number
offset?: number
action?: string
start_date?: string
end_date?: string
}
export interface GetAuditLogResponse {
entries: AuditLogEntry[]
total: number
}
async function auditFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${API_URL}${endpoint}`
const headers: HeadersInit = {
'Content-Type': 'application/json',
...options.headers,
}
const response = await fetch(url, {
...options,
headers,
credentials: 'include',
})
if (!response.ok) {
const errorBody = await response.json().catch(() => ({
error: 'Unknown error',
message: `HTTP ${response.status}: ${response.statusText}`,
}))
throw new Error(errorBody.message || errorBody.error || 'Request failed')
}
return response.json()
}
/**
* Fetches paginated audit log entries for the current org (org from JWT; admin-only on backend).
*/
export async function getAuditLog(params: GetAuditLogParams = {}): Promise<GetAuditLogResponse> {
const search = new URLSearchParams()
if (params.limit != null) search.set('limit', String(params.limit))
if (params.offset != null) search.set('offset', String(params.offset))
if (params.action) search.set('action', params.action)
if (params.start_date) search.set('start_date', params.start_date)
if (params.end_date) search.set('end_date', params.end_date)
const qs = search.toString()
const url = qs ? `/api/audit?${qs}` : '/api/audit'
return await auditFetch<GetAuditLogResponse>(url, { method: 'GET' })
}