mirror of
https://github.com/agentrhq/authsome.git
synced 2026-09-19 01:34:19 +08:00
refactor: remove audit event filters
This commit is contained in:
@@ -53,8 +53,7 @@ authsome log --json # Output JSON format
|
||||
|
||||
`GET /api/audit/events` is role-aware. Admin principals can review the global audit log.
|
||||
Non-admin principals receive only events scoped to their own principal, including their
|
||||
claimed identities, vault, providers, and credential lifecycle activity. Passing another
|
||||
`principal_id`, vault, or identity in query parameters cannot widen a non-admin query.
|
||||
claimed identities, vault, providers, and credential lifecycle activity.
|
||||
|
||||
Supported query parameters:
|
||||
|
||||
@@ -62,11 +61,6 @@ Supported query parameters:
|
||||
| --- | --- |
|
||||
| `limit` | Number of events to return, clamped to the server maximum. |
|
||||
| `cursor` | Cursor returned by the previous page. |
|
||||
| `event` | Exact event name. |
|
||||
| `provider` | Exact provider name, such as `github`. |
|
||||
| `identity` | Exact identity handle. |
|
||||
| `from` | Inclusive lower timestamp bound. |
|
||||
| `to` | Exclusive upper timestamp bound. |
|
||||
|
||||
Results are sorted newest-first and include `next_cursor` when another page is available.
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Audit event routes."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Any, Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
|
||||
from authsome import audit
|
||||
from authsome.identity.principal import PrincipalRole
|
||||
@@ -17,29 +16,18 @@ router = APIRouter(prefix="/audit", tags=["audit"])
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
async def list_audit_events( # noqa: PLR0913
|
||||
async def list_audit_events(
|
||||
request: Request,
|
||||
limit: int = 50,
|
||||
cursor: str | None = None,
|
||||
event: str | None = None,
|
||||
provider: str | None = None,
|
||||
identity: str | None = None,
|
||||
principal_id: str | None = None,
|
||||
from_: Annotated[datetime | None, Query(alias="from")] = None,
|
||||
to: datetime | None = None,
|
||||
auth: CredentialService = Depends(get_daemon_or_browser_auth_service),
|
||||
) -> dict[str, Any]:
|
||||
effective_principal_id = principal_id if auth.principal_role == PrincipalRole.ADMIN else auth.principal_id
|
||||
effective_principal_id = None if auth.principal_role == PrincipalRole.ADMIN else auth.principal_id
|
||||
scope: Literal["global", "principal"] = "global" if effective_principal_id is None else "principal"
|
||||
try:
|
||||
page = await request.app.state.audit_log.query_events(
|
||||
limit=limit,
|
||||
principal_id=effective_principal_id,
|
||||
event=event,
|
||||
provider=provider,
|
||||
identity=identity,
|
||||
since=from_,
|
||||
until=to,
|
||||
cursor=cursor,
|
||||
)
|
||||
except ValueError as exc:
|
||||
|
||||
@@ -134,29 +134,20 @@ class AuditEventRegistry:
|
||||
],
|
||||
)
|
||||
|
||||
async def query_events( # noqa: PLR0913
|
||||
async def query_events(
|
||||
self,
|
||||
*,
|
||||
limit: int = 50,
|
||||
principal_id: str | None = None,
|
||||
event: str | None = None,
|
||||
provider: str | None = None,
|
||||
identity: str | None = None,
|
||||
since: datetime | None = None,
|
||||
until: datetime | None = None,
|
||||
cursor: str | None = None,
|
||||
) -> AuditEventPage:
|
||||
bounded_limit = min(max(limit, 1), 500)
|
||||
filter_specs = (
|
||||
("principal_id = ?", principal_id, principal_id is not None),
|
||||
("event = ?", event, bool(event)),
|
||||
("provider = ?", provider, bool(provider)),
|
||||
("identity = ?", identity, bool(identity)),
|
||||
("timestamp >= ?", _dump_dt(since) if since is not None else None, since is not None),
|
||||
("timestamp < ?", _dump_dt(until) if until is not None else None, until is not None),
|
||||
)
|
||||
conditions = [condition for condition, _value, enabled in filter_specs if enabled]
|
||||
params = [value for _condition, value, enabled in filter_specs if enabled]
|
||||
conditions: list[str] = []
|
||||
params: list[Any] = []
|
||||
|
||||
if principal_id is not None:
|
||||
conditions.append("principal_id = ?")
|
||||
params.append(principal_id)
|
||||
|
||||
if cursor:
|
||||
cursor_timestamp, cursor_event_id = _decode_audit_cursor(cursor)
|
||||
@@ -339,27 +330,17 @@ class ServerAuditLog:
|
||||
_delegating_audit_exporter.set_active(None)
|
||||
self._exporter.close()
|
||||
|
||||
async def query_events( # noqa: PLR0913
|
||||
async def query_events(
|
||||
self,
|
||||
*,
|
||||
limit: int = 50,
|
||||
principal_id: str | None = None,
|
||||
event: str | None = None,
|
||||
provider: str | None = None,
|
||||
identity: str | None = None,
|
||||
since: datetime | None = None,
|
||||
until: datetime | None = None,
|
||||
cursor: str | None = None,
|
||||
) -> AuditEventPage:
|
||||
await self.async_force_flush()
|
||||
return await self._registry.query_events(
|
||||
limit=limit,
|
||||
principal_id=principal_id,
|
||||
event=event,
|
||||
provider=provider,
|
||||
identity=identity,
|
||||
since=since,
|
||||
until=until,
|
||||
cursor=cursor,
|
||||
)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ def _emit_audit_event( # noqa: PLR0913
|
||||
emit(
|
||||
AuditEvent(
|
||||
event_id=event_id,
|
||||
timestamp=timestamp or datetime(2026, 6, 15, 8, 0, tzinfo=UTC),
|
||||
timestamp=timestamp or datetime(2099, 1, 1, 8, 0, tzinfo=UTC),
|
||||
event=event,
|
||||
principal_id=principal_id,
|
||||
identity=identity,
|
||||
@@ -82,6 +82,17 @@ def test_audit_events_endpoint_returns_internal_events_for_admin(monkeypatch, tm
|
||||
assert entries[0]["provider"] == "github"
|
||||
|
||||
|
||||
def test_audit_events_endpoint_only_documents_pagination_params(monkeypatch, tmp_path: Path) -> None:
|
||||
monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path))
|
||||
|
||||
with create_server_test_client() as client:
|
||||
response = client.get("/openapi.json")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
params = response.json()["paths"]["/api/audit/events"]["get"]["parameters"]
|
||||
assert {param["name"] for param in params} == {"limit", "cursor"}
|
||||
|
||||
|
||||
def test_external_audit_post_is_enriched_from_pop_identity(monkeypatch, tmp_path: Path) -> None:
|
||||
monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path))
|
||||
payload = {"event": {"event": "proxy_deny", "metadata": {"host": "api.example.com", "reason": "no_match"}}}
|
||||
@@ -156,7 +167,7 @@ def test_admin_sees_all_audit_events_and_user_sees_only_own_principal(monkeypatc
|
||||
assert all(entry["principal_id"] == user_whoami["principal_id"] for entry in user_entries)
|
||||
|
||||
|
||||
def test_non_admin_audit_filters_stay_within_own_principal(monkeypatch, tmp_path: Path) -> None:
|
||||
def test_non_admin_audit_query_params_do_not_filter_or_widen_scope(monkeypatch, tmp_path: Path) -> None:
|
||||
monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path))
|
||||
|
||||
with create_server_test_client() as client:
|
||||
@@ -205,10 +216,9 @@ def test_non_admin_audit_filters_stay_within_own_principal(monkeypatch, tmp_path
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
body = response.json()
|
||||
assert body["scope"] == "principal"
|
||||
assert body["next_cursor"] is None
|
||||
assert [entry["event_id"] for entry in body["entries"]] == ["audit_002"]
|
||||
assert body["entries"][0]["principal_id"] == user_whoami["principal_id"]
|
||||
assert body["entries"][0]["provider"] == "github"
|
||||
manual_entries = [entry for entry in body["entries"] if entry["event_id"].startswith("audit_00")]
|
||||
assert [entry["event_id"] for entry in manual_entries] == ["audit_003", "audit_002"]
|
||||
assert all(entry["principal_id"] == user_whoami["principal_id"] for entry in body["entries"])
|
||||
|
||||
|
||||
def test_non_admin_audit_query_cannot_widen_scope_with_principal_or_identity(
|
||||
@@ -259,10 +269,13 @@ def test_non_admin_audit_query_cannot_widen_scope_with_principal_or_identity(
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
body = response.json()
|
||||
assert body["scope"] == "principal"
|
||||
assert body["entries"] == []
|
||||
event_ids = {entry["event_id"] for entry in body["entries"]}
|
||||
assert "audit_011" in event_ids
|
||||
assert "audit_010" not in event_ids
|
||||
assert all(entry["principal_id"] == user_whoami["principal_id"] for entry in body["entries"])
|
||||
|
||||
|
||||
def test_admin_audit_events_support_filters_and_cursor_pagination(monkeypatch, tmp_path: Path) -> None:
|
||||
def test_admin_audit_events_support_cursor_pagination(monkeypatch, tmp_path: Path) -> None:
|
||||
monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path))
|
||||
|
||||
with create_server_test_client() as client:
|
||||
@@ -282,7 +295,15 @@ def test_admin_audit_events_support_filters_and_cursor_pagination(monkeypatch, t
|
||||
principal_id=admin_whoami["principal_id"],
|
||||
identity="admin-ready-boldly-0001",
|
||||
provider="github",
|
||||
timestamp=datetime(2026, 6, 15, 8, 0, tzinfo=UTC),
|
||||
timestamp=datetime(2099, 1, 1, 8, 0, tzinfo=UTC),
|
||||
)
|
||||
_emit_audit_event(
|
||||
"audit_099",
|
||||
"connection.logout",
|
||||
principal_id=admin_whoami["principal_id"],
|
||||
identity="admin-ready-boldly-0001",
|
||||
provider="linear",
|
||||
timestamp=datetime(2099, 1, 1, 7, 59, tzinfo=UTC),
|
||||
)
|
||||
_emit_audit_event(
|
||||
"audit_101",
|
||||
@@ -290,7 +311,7 @@ def test_admin_audit_events_support_filters_and_cursor_pagination(monkeypatch, t
|
||||
principal_id=user_whoami["principal_id"],
|
||||
identity="steady-wisely-boldly-0042",
|
||||
provider="github",
|
||||
timestamp=datetime(2026, 6, 15, 8, 1, tzinfo=UTC),
|
||||
timestamp=datetime(2099, 1, 1, 8, 1, tzinfo=UTC),
|
||||
)
|
||||
_emit_audit_event(
|
||||
"audit_102",
|
||||
@@ -298,10 +319,10 @@ def test_admin_audit_events_support_filters_and_cursor_pagination(monkeypatch, t
|
||||
principal_id=user_whoami["principal_id"],
|
||||
identity="steady-wisely-boldly-0042",
|
||||
provider="github",
|
||||
timestamp=datetime(2026, 6, 15, 8, 2, tzinfo=UTC),
|
||||
timestamp=datetime(2099, 1, 1, 8, 2, tzinfo=UTC),
|
||||
)
|
||||
|
||||
first_path = "/api/audit/events?provider=github&limit=2"
|
||||
first_path = "/api/audit/events?limit=2"
|
||||
first_response = client.get(
|
||||
first_path,
|
||||
headers=_auth_header(
|
||||
@@ -313,7 +334,7 @@ def test_admin_audit_events_support_filters_and_cursor_pagination(monkeypatch, t
|
||||
)
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
first_body = first_response.json()
|
||||
second_path = f"/api/audit/events?provider=github&limit=2&cursor={first_body['next_cursor']}"
|
||||
second_path = f"/api/audit/events?limit=2&cursor={first_body['next_cursor']}"
|
||||
second_response = client.get(
|
||||
second_path,
|
||||
headers=_auth_header(
|
||||
@@ -331,8 +352,7 @@ def test_admin_audit_events_support_filters_and_cursor_pagination(monkeypatch, t
|
||||
assert second_response.status_code == status.HTTP_200_OK
|
||||
second_body = second_response.json()
|
||||
assert second_body["scope"] == "global"
|
||||
assert [entry["event_id"] for entry in second_body["entries"]] == ["audit_100"]
|
||||
assert second_body["next_cursor"] is None
|
||||
assert [entry["event_id"] for entry in second_body["entries"]] == ["audit_100", "audit_099"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -33,7 +33,6 @@ import useSWR from "swr";
|
||||
|
||||
import {
|
||||
ApiError,
|
||||
AuditFilters,
|
||||
ConnectionDetail,
|
||||
DashboardData,
|
||||
GlobalConnectionRow,
|
||||
@@ -1437,62 +1436,20 @@ export function PrincipalsView() {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeAuditFilters(filters: AuditFilters): AuditFilters {
|
||||
return Object.fromEntries(
|
||||
Object.entries(filters)
|
||||
.map(([key, value]) => [key, typeof value === "string" ? value.trim() : value])
|
||||
.filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== ""),
|
||||
) as AuditFilters;
|
||||
}
|
||||
|
||||
function localDateTimeToIso(value: string): string | undefined {
|
||||
if (!value) return undefined;
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.valueOf()) ? undefined : parsed.toISOString();
|
||||
}
|
||||
|
||||
export function AuditView({ data }: { data: DashboardData }) {
|
||||
const [filters, setFilters] = useState<AuditFilters>({
|
||||
event: "",
|
||||
provider: "",
|
||||
identity: "",
|
||||
from: "",
|
||||
to: "",
|
||||
});
|
||||
const [auditResult, setAuditResult] = useState<{
|
||||
activeFilters: AuditFilters;
|
||||
events: DashboardData["audit"]["events"];
|
||||
nextCursor: string | null;
|
||||
} | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [loadingAction, setLoadingAction] = useState<"apply" | "more" | null>(null);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const requestSequence = useRef(0);
|
||||
const events = auditResult?.events ?? data.audit.events;
|
||||
const nextCursor = auditResult?.nextCursor ?? data.audit.nextCursor;
|
||||
const activeFilters = auditResult?.activeFilters ?? {};
|
||||
const providerNames = useMemo(() => data.providers.map((provider) => provider.name).sort(), [data.providers]);
|
||||
const identityHandles = useMemo(() => data.identities.map((identity) => identity.handle).sort(), [data.identities]);
|
||||
const eventNames = useMemo(() => Array.from(new Set(events.map((event) => event.eventName))).sort(), [events]);
|
||||
const description = data.account.isAdmin
|
||||
? "Recent administrative and credential events."
|
||||
: "Recent account, identity, vault, and credential events for this principal.";
|
||||
|
||||
function updateFilter(name: keyof Pick<AuditFilters, "event" | "provider" | "identity" | "from" | "to">, value: string) {
|
||||
setFilters((current) => ({ ...current, [name]: value }));
|
||||
}
|
||||
|
||||
function filtersForRequest(cursor?: string | null): AuditFilters {
|
||||
return normalizeAuditFilters({
|
||||
event: filters.event,
|
||||
provider: filters.provider,
|
||||
identity: filters.identity,
|
||||
from: localDateTimeToIso(filters.from || ""),
|
||||
to: localDateTimeToIso(filters.to || ""),
|
||||
cursor,
|
||||
limit: 50,
|
||||
});
|
||||
}
|
||||
|
||||
function nextRequestId(): number {
|
||||
requestSequence.current += 1;
|
||||
return requestSequence.current;
|
||||
@@ -1502,64 +1459,15 @@ export function AuditView({ data }: { data: DashboardData }) {
|
||||
return requestSequence.current === requestId;
|
||||
}
|
||||
|
||||
async function applyFilters(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const requestFilters = filtersForRequest();
|
||||
const requestId = nextRequestId();
|
||||
setLoadingAction("apply");
|
||||
setErrorMessage("");
|
||||
try {
|
||||
const result = await fetchAuditEvents(requestFilters);
|
||||
if (!isLatestRequest(requestId)) return;
|
||||
setAuditResult({
|
||||
activeFilters: requestFilters,
|
||||
events: result.events,
|
||||
nextCursor: result.nextCursor,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isLatestRequest(requestId)) return;
|
||||
setErrorMessage(error instanceof Error ? error.message : "Failed to load audit events.");
|
||||
} finally {
|
||||
if (isLatestRequest(requestId)) {
|
||||
setLoadingAction(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function clearFilters() {
|
||||
const emptyFilters = { event: "", provider: "", identity: "", from: "", to: "" };
|
||||
const requestId = nextRequestId();
|
||||
setFilters(emptyFilters);
|
||||
setLoadingAction("apply");
|
||||
setErrorMessage("");
|
||||
try {
|
||||
const result = await fetchAuditEvents({ limit: 50 });
|
||||
if (!isLatestRequest(requestId)) return;
|
||||
setAuditResult({
|
||||
activeFilters: {},
|
||||
events: result.events,
|
||||
nextCursor: result.nextCursor,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isLatestRequest(requestId)) return;
|
||||
setErrorMessage(error instanceof Error ? error.message : "Failed to load audit events.");
|
||||
} finally {
|
||||
if (isLatestRequest(requestId)) {
|
||||
setLoadingAction(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (!nextCursor) return;
|
||||
const requestId = nextRequestId();
|
||||
setLoadingAction("more");
|
||||
setLoadingMore(true);
|
||||
setErrorMessage("");
|
||||
try {
|
||||
const result = await fetchAuditEvents({ ...activeFilters, cursor: nextCursor, limit: 50 });
|
||||
const result = await fetchAuditEvents({ cursor: nextCursor, limit: 50 });
|
||||
if (!isLatestRequest(requestId)) return;
|
||||
setAuditResult({
|
||||
activeFilters,
|
||||
events: [...events, ...result.events],
|
||||
nextCursor: result.nextCursor,
|
||||
});
|
||||
@@ -1568,7 +1476,7 @@ export function AuditView({ data }: { data: DashboardData }) {
|
||||
setErrorMessage(error instanceof Error ? error.message : "Failed to load more audit events.");
|
||||
} finally {
|
||||
if (isLatestRequest(requestId)) {
|
||||
setLoadingAction(null);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1576,80 +1484,7 @@ export function AuditView({ data }: { data: DashboardData }) {
|
||||
return (
|
||||
<div className="grid gap-5">
|
||||
<SectionHeader description={description} title="Audit Log" />
|
||||
<Card className="shadow-none border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle>Filters</CardTitle>
|
||||
<CardDescription>{data.audit.scope === "global" ? "Global audit scope" : "Principal audit scope"}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="grid gap-4 md:grid-cols-2 xl:grid-cols-5" onSubmit={(event) => void applyFilters(event)}>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="text-muted-foreground">Event</span>
|
||||
<Input
|
||||
list="audit-event-options"
|
||||
onChange={(event) => updateFilter("event", event.target.value)}
|
||||
value={filters.event || ""}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="text-muted-foreground">Provider</span>
|
||||
<Input
|
||||
list="audit-provider-options"
|
||||
onChange={(event) => updateFilter("provider", event.target.value)}
|
||||
value={filters.provider || ""}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="text-muted-foreground">Identity</span>
|
||||
<Input
|
||||
list="audit-identity-options"
|
||||
onChange={(event) => updateFilter("identity", event.target.value)}
|
||||
value={filters.identity || ""}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="text-muted-foreground">From</span>
|
||||
<Input
|
||||
onChange={(event) => updateFilter("from", event.target.value)}
|
||||
type="datetime-local"
|
||||
value={filters.from || ""}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<span className="text-muted-foreground">To</span>
|
||||
<Input
|
||||
onChange={(event) => updateFilter("to", event.target.value)}
|
||||
type="datetime-local"
|
||||
value={filters.to || ""}
|
||||
/>
|
||||
</label>
|
||||
<datalist id="audit-event-options">
|
||||
{eventNames.map((event) => (
|
||||
<option key={event} value={event} />
|
||||
))}
|
||||
</datalist>
|
||||
<datalist id="audit-provider-options">
|
||||
{providerNames.map((provider) => (
|
||||
<option key={provider} value={provider} />
|
||||
))}
|
||||
</datalist>
|
||||
<datalist id="audit-identity-options">
|
||||
{identityHandles.map((identity) => (
|
||||
<option key={identity} value={identity} />
|
||||
))}
|
||||
</datalist>
|
||||
<div className="flex items-end gap-2 md:col-span-2 xl:col-span-5">
|
||||
<Button disabled={loadingAction !== null} type="submit">
|
||||
Apply filters
|
||||
</Button>
|
||||
<Button disabled={loadingAction !== null} onClick={() => void clearFilters()} type="button" variant="outline">
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{errorMessage ? <p className="mt-4 text-sm text-destructive">{errorMessage}</p> : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{errorMessage ? <p className="text-sm text-destructive">{errorMessage}</p> : null}
|
||||
<Card className="shadow-none border-border/50">
|
||||
<CardContent className="p-0">
|
||||
{events.length ? (
|
||||
@@ -1694,8 +1529,8 @@ export function AuditView({ data }: { data: DashboardData }) {
|
||||
</Card>
|
||||
{nextCursor ? (
|
||||
<div className="flex justify-center">
|
||||
<Button disabled={loadingAction !== null} onClick={() => void loadMore()} type="button" variant="outline">
|
||||
{loadingAction === "more" ? "Loading..." : "Load more"}
|
||||
<Button disabled={loadingMore} onClick={() => void loadMore()} type="button" variant="outline">
|
||||
{loadingMore ? "Loading..." : "Load more"}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -51,12 +51,7 @@ export type AuditRow = {
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type AuditFilters = {
|
||||
event?: string;
|
||||
provider?: string;
|
||||
identity?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
export type AuditEventsQuery = {
|
||||
cursor?: string | null;
|
||||
limit?: number;
|
||||
};
|
||||
@@ -517,20 +512,15 @@ function buildAuditRows(entries: AuditResponse["entries"]): AuditRow[] {
|
||||
});
|
||||
}
|
||||
|
||||
function auditQueryString(filters: AuditFilters = {}): string {
|
||||
function auditQueryString(query: AuditEventsQuery = {}): string {
|
||||
const params = new URLSearchParams();
|
||||
params.set("limit", String(filters.limit ?? 50));
|
||||
if (filters.event) params.set("event", filters.event);
|
||||
if (filters.provider) params.set("provider", filters.provider);
|
||||
if (filters.identity) params.set("identity", filters.identity);
|
||||
if (filters.from) params.set("from", filters.from);
|
||||
if (filters.to) params.set("to", filters.to);
|
||||
if (filters.cursor) params.set("cursor", filters.cursor);
|
||||
params.set("limit", String(query.limit ?? 50));
|
||||
if (query.cursor) params.set("cursor", query.cursor);
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
export async function fetchAuditEvents(filters: AuditFilters = {}): Promise<AuditEventsData> {
|
||||
const data = await requestJson<AuditResponse>(`/api/audit/events?${auditQueryString(filters)}`);
|
||||
export async function fetchAuditEvents(query: AuditEventsQuery = {}): Promise<AuditEventsData> {
|
||||
const data = await requestJson<AuditResponse>(`/api/audit/events?${auditQueryString(query)}`);
|
||||
const events = buildAuditRows(data.entries);
|
||||
return {
|
||||
scope: data.scope ?? "principal",
|
||||
|
||||
Reference in New Issue
Block a user