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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
116
frontend/src/routes/settings/audit-log/global/+page.svelte
Normal file
116
frontend/src/routes/settings/audit-log/global/+page.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user