1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-12 09:43:31 +00:00

feat: global audit log (#320)

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Kyle Mendell
2025-04-03 10:11:49 -05:00
committed by GitHub
parent 734c6813ea
commit b65e693e12
33 changed files with 865 additions and 21 deletions

View File

@@ -1,8 +1,9 @@
<script lang="ts">
import AuditLogList from '$lib/components/audit-log-list.svelte';
import * as Card from '$lib/components/ui/card';
import { m } from '$lib/paraglide/messages';
import { LogsIcon } from 'lucide-svelte';
import AuditLogList from './audit-log-list.svelte';
import AuditLogSwitcher from './audit-log-switcher.svelte';
let { data } = $props();
let auditLogsRequestOptions = $state(data.auditLogsRequestOptions);
@@ -12,6 +13,8 @@
<title>{m.audit_log()}</title>
</svelte:head>
<AuditLogSwitcher currentPage="personal" />
<div>
<Card.Root>
<Card.Header>

View File

@@ -1,52 +0,0 @@
<script lang="ts">
import AdvancedTable from '$lib/components/advanced-table.svelte';
import { Badge } from '$lib/components/ui/badge';
import * as Table from '$lib/components/ui/table';
import { m } from '$lib/paraglide/messages';
import AuditLogService from '$lib/services/audit-log-service';
import type { AuditLog } from '$lib/types/audit-log.type';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
let {
auditLogs,
requestOptions
}: { auditLogs: Paginated<AuditLog>; requestOptions: SearchPaginationSortRequest } = $props();
const auditLogService = new AuditLogService();
function toFriendlyEventString(event: string) {
const words = event.split('_');
const capitalizedWords = words.map((word) => {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
});
return capitalizedWords.join(' ');
}
</script>
<AdvancedTable
items={auditLogs}
{requestOptions}
onRefresh={async (options) => (auditLogs = await auditLogService.list(options))}
columns={[
{ label: m.time(), sortColumn: 'createdAt' },
{ label: m.event(), sortColumn: 'event' },
{ label: m.approximate_location(), sortColumn: 'city' },
{ label: m.ip_address(), sortColumn: 'ipAddress' },
{ label: m.device(), sortColumn: 'device' },
{ label: m.client() }
]}
withoutSearch
>
{#snippet rows({ item })}
<Table.Cell>{new Date(item.createdAt).toLocaleString()}</Table.Cell>
<Table.Cell>
<Badge variant="outline">{toFriendlyEventString(item.event)}</Badge>
</Table.Cell>
<Table.Cell
>{item.city && item.country ? `${item.city}, ${item.country}` : m.unknown()}</Table.Cell
>
<Table.Cell>{item.ipAddress}</Table.Cell>
<Table.Cell>{item.device}</Table.Cell>
<Table.Cell>{item.data.clientName}</Table.Cell>
{/snippet}
</AdvancedTable>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { goto } from '$app/navigation';
import * as Tabs from '$lib/components/ui/tabs';
import { m } from '$lib/paraglide/messages';
let { currentPage }: { currentPage: 'personal' | 'global' } = $props();
</script>
<div class="flex justify-end no-fade">
<Tabs.Root value={currentPage} >
<Tabs.List>
<Tabs.Trigger onclick={() => goto('/settings/audit-log')} value="personal"
>{m.personal()}</Tabs.Trigger
>
<Tabs.Trigger onclick={() => goto('/settings/audit-log/global')} value="global"
>{m.global()}</Tabs.Trigger
>
</Tabs.List>
</Tabs.Root>
</div>

View File

@@ -0,0 +1,22 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import AuditLogService from '$lib/services/audit-log-service';
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { PageServerLoad } from '../../global-audit-log/$types';
export const load: PageServerLoad = async ({ cookies }) => {
const auditLogService = new AuditLogService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const requestOptions: SearchPaginationSortRequest = {
sort: {
column: 'createdAt',
direction: 'desc'
}
};
const auditLogs = await auditLogService.listAllLogs(requestOptions);
return {
auditLogs,
requestOptions
};
};

View File

@@ -0,0 +1,116 @@
<script lang="ts">
import AuditLogList from '$lib/components/audit-log-list.svelte';
import SearchableSelect from '$lib/components/form/searchable-select.svelte';
import * as Card from '$lib/components/ui/card';
import * as Select from '$lib/components/ui/select';
import { m } from '$lib/paraglide/messages';
import AuditLogService from '$lib/services/audit-log-service';
import type { AuditLogFilter } from '$lib/types/audit-log.type';
import AuditLogSwitcher from '../audit-log-switcher.svelte';
let { data } = $props();
const auditLogService = new AuditLogService();
let auditLogs = $state(data.auditLogs);
let requestOptions = $state(data.requestOptions);
let filters: AuditLogFilter = $state({
userId: '',
event: '',
clientName: ''
});
const eventTypes = $state({
SIGN_IN: m.sign_in(),
TOKEN_SIGN_IN: m.token_sign_in(),
CLIENT_AUTHORIZATION: m.client_authorization(),
NEW_CLIENT_AUTHORIZATION: m.new_client_authorization()
});
$effect(() => {
auditLogService.listAllLogs(requestOptions, filters).then((response) => (auditLogs = response));
});
</script>
<svelte:head>
<title>{m.global_audit_log()}</title>
</svelte:head>
<AuditLogSwitcher currentPage="global" />
<Card.Root>
<Card.Header>
<Card.Title>{m.global_audit_log()}</Card.Title>
<Card.Description class="mt-1"
>{m.see_all_account_activities_from_the_last_3_months()}</Card.Description
>
</Card.Header>
<Card.Content>
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-3">
<div>
{#await auditLogService.listUsers()}
<Select.Root>
<Select.Trigger class="w-full" disabled>
<Select.Value placeholder={m.all_users()} />
</Select.Trigger>
</Select.Root>
{:then users}
<SearchableSelect
class="w-full"
items={[
{ value: '', label: m.all_users() },
...Object.entries(users).map(([id, username]) => ({
value: id,
label: username
}))
]}
bind:value={filters.userId}
/>
{/await}
</div>
<div>
<Select.Root
selected={{
value: filters.event,
label: eventTypes[filters.event as keyof typeof eventTypes]
}}
onSelectedChange={(v) => (filters.event = v!.value)}
>
<Select.Trigger class="w-full">
<Select.Value placeholder={m.all_events()} />
</Select.Trigger>
<Select.Content>
<Select.Item value="">{m.all_events()}</Select.Item>
{#each Object.entries(eventTypes) as [value, label]}
<Select.Item {value}>{label}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div>
{#await auditLogService.listClientNames()}
<Select.Root>
<Select.Trigger class="w-full" disabled>
<Select.Value placeholder={m.all_clients()} />
</Select.Trigger>
</Select.Root>
{:then clientNames}
<SearchableSelect
class="w-full"
items={[
{ value: '', label: m.all_clients() },
...clientNames.map((name) => ({
value: name,
label: name
}))
]}
bind:value={filters.clientName}
/>
{/await}
</div>
</div>
<AuditLogList isAdmin={true} {auditLogs} {requestOptions} />
</Card.Content>
</Card.Root>