mirror of
https://github.com/nikdoof/pocket-id.git
synced 2025-12-14 15:22:18 +00:00
feat: add audit log with email notification (#26)
This commit is contained in:
@@ -1,19 +1,17 @@
|
||||
import ApplicationConfigurationService from '$lib/services/application-configuration-service';
|
||||
import AppConfigService from '$lib/services/app-config-service';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ cookies }) => {
|
||||
const userService = new UserService(cookies.get('access_token'));
|
||||
const applicationConfigurationService = new ApplicationConfigurationService(
|
||||
cookies.get('access_token')
|
||||
);
|
||||
const appConfigService = new AppConfigService(cookies.get('access_token'));
|
||||
|
||||
const user = await userService
|
||||
.getCurrent()
|
||||
.then((user) => user)
|
||||
.catch(() => null);
|
||||
|
||||
const applicationConfiguration = await applicationConfigurationService
|
||||
const appConfig = await appConfigService
|
||||
.list()
|
||||
.then((config) => config)
|
||||
.catch((e) => {
|
||||
@@ -24,6 +22,6 @@ export const load: LayoutServerLoad = async ({ cookies }) => {
|
||||
});
|
||||
return {
|
||||
user,
|
||||
applicationConfiguration
|
||||
appConfig
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Error from '$lib/components/error.svelte';
|
||||
import Header from '$lib/components/header/header.svelte';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
import applicationConfigurationStore from '$lib/stores/application-configuration-store';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import type { Snippet } from 'svelte';
|
||||
@@ -19,17 +19,17 @@
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
const { user, applicationConfiguration } = data;
|
||||
const { user, appConfig } = data;
|
||||
|
||||
if (browser && user) {
|
||||
userStore.setUser(user);
|
||||
}
|
||||
if (applicationConfiguration) {
|
||||
applicationConfigurationStore.set(applicationConfiguration);
|
||||
if (appConfig) {
|
||||
appConfigStore.set(appConfig);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !applicationConfiguration}
|
||||
{#if !appConfig}
|
||||
<Error
|
||||
message="A critical error occured. Please contact your administrator."
|
||||
showButton={false}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import OidcService from '$lib/services/oidc-service';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import applicationConfigurationStore from '$lib/stores/application-configuration-store';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
|
||||
import { startAuthentication } from '@simplewebauthn/browser';
|
||||
@@ -91,7 +91,7 @@
|
||||
{#if !authorizationRequired && !errorMessage}
|
||||
<p class="text-muted-foreground mb-10 mt-2">
|
||||
Do you want to sign in to <b>{client.name}</b> with your
|
||||
<b>{$applicationConfigurationStore.appName}</b> account?
|
||||
<b>{$appConfigStore.appName}</b> account?
|
||||
</p>
|
||||
{:else if authorizationRequired}
|
||||
<div transition:slide={{ duration: 300 }}>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Logo from '$lib/components/logo.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import applicationConfigurationStore from '$lib/stores/application-configuration-store';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
|
||||
import { startAuthentication } from '@simplewebauthn/browser';
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||
Sign in to {$applicationConfigurationStore.appName}
|
||||
Sign in to {$appConfigStore.appName}
|
||||
</h1>
|
||||
<p class="text-muted-foreground mt-2">
|
||||
Authenticate yourself with your passkey to access the admin panel
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Logo from '$lib/components/logo.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import applicationConfigurationStore from '$lib/stores/application-configuration-store.js';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store.js';
|
||||
import userStore from '$lib/stores/user-store.js';
|
||||
import type { User } from '$lib/types/user.type.js';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
@@ -18,9 +18,9 @@
|
||||
isLoading = true;
|
||||
userService
|
||||
.exchangeOneTimeAccessToken(data.token)
|
||||
.then((user :User) => {
|
||||
.then((user: User) => {
|
||||
userStore.setUser(user);
|
||||
goto('/settings')
|
||||
goto('/settings');
|
||||
})
|
||||
.catch(axiosErrorToast);
|
||||
isLoading = false;
|
||||
@@ -29,15 +29,15 @@
|
||||
|
||||
<SignInWrapper>
|
||||
<div class="flex justify-center">
|
||||
<div class="rounded-2xl bg-muted p-3">
|
||||
<div class="bg-muted rounded-2xl p-3">
|
||||
<Logo class="h-10 w-10" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="mt-5 font-playfair text-4xl font-bold">One Time Access</h1>
|
||||
<p class="mt-2 text-muted-foreground">
|
||||
You've been granted one-time access to your {$applicationConfigurationStore.appName} account. Please note that if you continue,
|
||||
this link will become invalid. To avoid this, make sure to add a passkey. Otherwise, you'll need
|
||||
to request a new link.
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">One Time Access</h1>
|
||||
<p class="text-muted-foreground mt-2">
|
||||
You've been granted one-time access to your {$appConfigStore.appName} account. Please note that if
|
||||
you continue, this link will become invalid. To avoid this, make sure to add a passkey. Otherwise,
|
||||
you'll need to request a new link.
|
||||
</p>
|
||||
<Button class="mt-5" {isLoading} on:click={authenticate}>Continue</Button>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
let links = $state([{ href: '/settings/account', label: 'My Account' }]);
|
||||
let links = $state([
|
||||
{ href: '/settings/account', label: 'My Account' },
|
||||
{ href: '/settings/audit-log', label: 'Audit Log' }
|
||||
]);
|
||||
|
||||
if ($userStore?.isAdmin) {
|
||||
links = [
|
||||
@@ -22,10 +25,8 @@
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="bg-muted/40 h-screen w-full">
|
||||
<main
|
||||
class="mx-auto flex min-h-screen max-w-[1640px] flex-col gap-x-4 gap-y-10 p-4 md:p-10 lg:flex-row"
|
||||
>
|
||||
<div class="bg-muted/40 min-h-screen w-full">
|
||||
<main class="mx-auto flex max-w-[1640px] flex-col gap-x-4 gap-y-10 p-4 md:p-10 lg:flex-row">
|
||||
<div>
|
||||
<div class="mx-auto grid w-full gap-2">
|
||||
<h1 class="mb-5 text-3xl font-semibold">Settings</h1>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import ApplicationConfigurationService from '$lib/services/application-configuration-service';
|
||||
import AppConfigService from '$lib/services/app-config-service';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const applicationConfigurationService = new ApplicationConfigurationService(
|
||||
cookies.get('access_token')
|
||||
);
|
||||
const applicationConfiguration = await applicationConfigurationService.list(true);
|
||||
return { applicationConfiguration };
|
||||
const appConfigService = new AppConfigService(cookies.get('access_token'));
|
||||
const appConfig = await appConfigService.list(true);
|
||||
return { appConfig };
|
||||
};
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
<script lang="ts">
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import ApplicationConfigurationService from '$lib/services/application-configuration-service';
|
||||
import applicationConfigurationStore from '$lib/stores/application-configuration-store';
|
||||
import type { AllApplicationConfiguration } from '$lib/types/application-configuration';
|
||||
import AppConfigService from '$lib/services/app-config-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import ApplicationConfigurationForm from './application-configuration-form.svelte';
|
||||
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
||||
import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
|
||||
import UpdateApplicationImages from './update-application-images.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let applicationConfiguration = $state(data.applicationConfiguration);
|
||||
let appConfig = $state(data.appConfig);
|
||||
|
||||
const applicationConfigurationService = new ApplicationConfigurationService();
|
||||
const appConfigService = new AppConfigService();
|
||||
|
||||
async function updateConfiguration(configuration: AllApplicationConfiguration) {
|
||||
await applicationConfigurationService
|
||||
.update(configuration)
|
||||
.then(() => toast.success('Application configuration updated successfully'))
|
||||
.catch(axiosErrorToast);
|
||||
await applicationConfigurationStore.reload();
|
||||
async function updateAppConfig(updatedAppConfig: Partial<AllAppConfig>) {
|
||||
await appConfigService
|
||||
.update({
|
||||
...appConfig,
|
||||
...updatedAppConfig
|
||||
})
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
throw e;
|
||||
});
|
||||
await appConfigStore.reload();
|
||||
}
|
||||
|
||||
async function updateImages(
|
||||
@@ -26,12 +32,10 @@
|
||||
backgroundImage: File | null,
|
||||
favicon: File | null
|
||||
) {
|
||||
const faviconPromise = favicon
|
||||
? applicationConfigurationService.updateFavicon(favicon)
|
||||
: Promise.resolve();
|
||||
const logoPromise = logo ? applicationConfigurationService.updateLogo(logo) : Promise.resolve();
|
||||
const faviconPromise = favicon ? appConfigService.updateFavicon(favicon) : Promise.resolve();
|
||||
const logoPromise = logo ? appConfigService.updateLogo(logo) : Promise.resolve();
|
||||
const backgroundImagePromise = backgroundImage
|
||||
? applicationConfigurationService.updateBackgroundImage(backgroundImage)
|
||||
? appConfigService.updateBackgroundImage(backgroundImage)
|
||||
: Promise.resolve();
|
||||
|
||||
await Promise.all([logoPromise, backgroundImagePromise, faviconPromise])
|
||||
@@ -49,7 +53,20 @@
|
||||
<Card.Title>General</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<ApplicationConfigurationForm {applicationConfiguration} callback={updateConfiguration} />
|
||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Email</Card.Title>
|
||||
<Card.Description>
|
||||
Enable email notifications to alert users when a login is detected from a new device or
|
||||
location.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import FormInput from '$lib/components/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
let {
|
||||
callback,
|
||||
appConfig
|
||||
}: {
|
||||
appConfig: AllAppConfig;
|
||||
callback: (appConfig: Partial<AllAppConfig>) => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
let isLoading = $state(false);
|
||||
let emailEnabled = $state(appConfig.emailEnabled == 'true');
|
||||
|
||||
const updatedAppConfig = {
|
||||
emailEnabled: emailEnabled.toString(),
|
||||
smtpHost: appConfig.smtpHost,
|
||||
smtpPort: appConfig.smtpPort,
|
||||
smtpUser: appConfig.smtpUser,
|
||||
smtpPassword: appConfig.smtpPassword,
|
||||
smtpFrom: appConfig.smtpFrom
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
smtpHost: z.string().min(1),
|
||||
smtpPort: z.string().min(1),
|
||||
smtpUser: z.string().min(1),
|
||||
smtpPassword: z.string().min(1),
|
||||
smtpFrom: z.string().email()
|
||||
});
|
||||
|
||||
const { inputs, ...form } = createForm< typeof formSchema>(formSchema, updatedAppConfig);
|
||||
|
||||
async function onSubmit() {
|
||||
const data = form.validate();
|
||||
if (!data) return false;
|
||||
isLoading = true;
|
||||
await callback({
|
||||
...data,
|
||||
emailEnabled: 'true'
|
||||
}).finally(() => (isLoading = false));
|
||||
toast.success('Email configuration updated successfully');
|
||||
return true;
|
||||
}
|
||||
|
||||
async function onDisable() {
|
||||
await callback({ emailEnabled: 'false' });
|
||||
emailEnabled = false;
|
||||
toast.success('Email disabled successfully');
|
||||
}
|
||||
|
||||
async function onEnable() {
|
||||
if (await onSubmit()) {
|
||||
emailEnabled = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="mt-5 grid grid-cols-2 gap-5">
|
||||
<FormInput label="SMTP Host" bind:input={$inputs.smtpHost} />
|
||||
<FormInput label="SMTP Port" bind:input={$inputs.smtpPort} />
|
||||
<FormInput label="SMTP User" bind:input={$inputs.smtpUser} />
|
||||
<FormInput label="SMTP Password" type="password" bind:input={$inputs.smtpPassword} />
|
||||
<FormInput label="SMTP From" bind:input={$inputs.smtpFrom} />
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
{#if emailEnabled}
|
||||
<Button variant="secondary" onclick={onDisable}>Disable</Button>
|
||||
<Button {isLoading} onclick={onSubmit} type="submit">Save</Button>
|
||||
{:else}
|
||||
<Button {isLoading} onclick={onEnable} type="submit">Enable</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,23 +1,24 @@
|
||||
<script lang="ts">
|
||||
import FormInput from '$lib/components/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import type { AllApplicationConfiguration } from '$lib/types/application-configuration';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
let {
|
||||
callback,
|
||||
applicationConfiguration
|
||||
appConfig
|
||||
}: {
|
||||
applicationConfiguration: AllApplicationConfiguration;
|
||||
callback: (user: AllApplicationConfiguration) => Promise<void>;
|
||||
appConfig: AllAppConfig;
|
||||
callback: (appConfig: Partial<AllAppConfig>) => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
let isLoading = $state(false);
|
||||
|
||||
const updatedApplicationConfiguration: AllApplicationConfiguration = {
|
||||
appName: applicationConfiguration.appName,
|
||||
sessionDuration: applicationConfiguration.sessionDuration
|
||||
const updatedAppConfig = {
|
||||
appName: appConfig.appName,
|
||||
sessionDuration: appConfig.sessionDuration
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -32,15 +33,14 @@
|
||||
}
|
||||
)
|
||||
});
|
||||
type FormSchema = typeof formSchema;
|
||||
|
||||
const { inputs, ...form } = createForm<FormSchema>(formSchema, updatedApplicationConfiguration);
|
||||
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
|
||||
async function onSubmit() {
|
||||
const data = form.validate();
|
||||
if (!data) return;
|
||||
isLoading = true;
|
||||
await callback(data);
|
||||
isLoading = false;
|
||||
await callback(data).finally(() => (isLoading = false));
|
||||
toast.success('Application configuration updated successfully');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import OIDCService from '$lib/services/oidc-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import clientSecretStore from '$lib/stores/client-secret-store';
|
||||
import type { OidcClientCreateWithLogo } from '$lib/types/oidc.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideMinus } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { slide } from 'svelte/transition';
|
||||
import OIDCClientForm from './oidc-client-form.svelte';
|
||||
import OIDCClientList from './oidc-client-list.svelte';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import clientSecretStore from '$lib/stores/client-secret-store';
|
||||
import { goto } from '$app/navigation';
|
||||
import applicationConfigurationStore from '$lib/stores/application-configuration-store';
|
||||
|
||||
let { data } = $props();
|
||||
let clients = $state(data);
|
||||
@@ -22,7 +22,7 @@
|
||||
async function createOIDCClient(client: OidcClientCreateWithLogo) {
|
||||
try {
|
||||
const createdClient = await oidcService.createClient(client);
|
||||
if(client.logo){
|
||||
if (client.logo) {
|
||||
await oidcService.updateClientLogo(createdClient, client.logo);
|
||||
}
|
||||
const clientSecret = await oidcService.createClientSecret(createdClient.id);
|
||||
@@ -31,7 +31,7 @@
|
||||
toast.success('OIDC client created successfully');
|
||||
return true;
|
||||
} catch (e) {
|
||||
axiosErrorToast(e)
|
||||
axiosErrorToast(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Create OIDC Client</Card.Title>
|
||||
<Card.Description>Add a new OIDC client to {$applicationConfigurationStore.appName}.</Card.Description>
|
||||
<Card.Description>Add a new OIDC client to {$appConfigStore.appName}.</Card.Description>
|
||||
</div>
|
||||
{#if !expandAddClient}
|
||||
<Button on:click={() => (expandAddClient = true)}>Add OIDC Client</Button>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import applicationConfigurationStore from '$lib/stores/application-configuration-store';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { Paginated } from '$lib/types/pagination.type';
|
||||
import type { User, UserCreate } from '$lib/types/user.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
@@ -42,9 +42,7 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Create User</Card.Title>
|
||||
<Card.Description
|
||||
>Add a new user to {$applicationConfigurationStore.appName}.</Card.Description
|
||||
>
|
||||
<Card.Description>Add a new user to {$appConfigStore.appName}.</Card.Description>
|
||||
</div>
|
||||
{#if !expandAddUser}
|
||||
<Button on:click={() => (expandAddUser = true)}>Add User</Button>
|
||||
|
||||
13
frontend/src/routes/settings/audit-log/+page.server.ts
Normal file
13
frontend/src/routes/settings/audit-log/+page.server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import AuditLogService from '$lib/services/audit-log-service';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const auditLogService = new AuditLogService(cookies.get('access_token'));
|
||||
const auditLogs = await auditLogService.list({
|
||||
limit: 15,
|
||||
page: 1,
|
||||
});
|
||||
return {
|
||||
auditLogs
|
||||
};
|
||||
};
|
||||
20
frontend/src/routes/settings/audit-log/+page.svelte
Normal file
20
frontend/src/routes/settings/audit-log/+page.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import AuditLogList from './audit-log-list.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Audit Log</title>
|
||||
</svelte:head>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Audit Log</Card.Title>
|
||||
<Card.Description class="mt-1">See your account activities from the last 3 months.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AuditLogList auditLogs={data.auditLogs} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
95
frontend/src/routes/settings/audit-log/audit-log-list.svelte
Normal file
95
frontend/src/routes/settings/audit-log/audit-log-list.svelte
Normal file
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import * as Pagination from '$lib/components/ui/pagination';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import AuditLogService from '$lib/services/audit-log-service';
|
||||
import type { AuditLog } from '$lib/types/audit-log.type';
|
||||
import type { Paginated, PaginationRequest } from '$lib/types/pagination.type';
|
||||
|
||||
let { auditLogs: initialAuditLog }: { auditLogs: Paginated<AuditLog> } = $props();
|
||||
let auditLogs = $state<Paginated<AuditLog>>(initialAuditLog);
|
||||
|
||||
const auditLogService = new AuditLogService();
|
||||
|
||||
let pagination = $state<PaginationRequest>({
|
||||
page: 1,
|
||||
limit: 15
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<Table.Root>
|
||||
<Table.Header class="whitespace-nowrap">
|
||||
<Table.Row>
|
||||
<Table.Head>Time</Table.Head>
|
||||
<Table.Head>Event</Table.Head>
|
||||
<Table.Head>IP Address</Table.Head>
|
||||
<Table.Head>Device</Table.Head>
|
||||
<Table.Head>Client</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body class="whitespace-nowrap">
|
||||
{#if auditLogs.data.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={6} class="text-center">No logs found</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each auditLogs.data as auditLog}
|
||||
<Table.Row>
|
||||
<Table.Cell>{new Date(auditLog.createdAt).toLocaleString()}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant="outline">{toFriendlyEventString(auditLog.event)}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{auditLog.ipAddress}</Table.Cell>
|
||||
<Table.Cell>{auditLog.device}</Table.Cell>
|
||||
<Table.Cell>{auditLog.data.clientName}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
|
||||
{#if auditLogs?.data?.length ?? 0 > 0}
|
||||
<Pagination.Root
|
||||
class="mt-5"
|
||||
count={auditLogs.pagination.totalItems}
|
||||
perPage={pagination.limit}
|
||||
onPageChange={async (p) =>
|
||||
(auditLogs = await auditLogService.list({
|
||||
page: p,
|
||||
limit: pagination.limit
|
||||
}))}
|
||||
bind:page={auditLogs.pagination.currentPage}
|
||||
let:pages
|
||||
let:currentPage
|
||||
>
|
||||
<Pagination.Content class="flex justify-end">
|
||||
<Pagination.Item>
|
||||
<Pagination.PrevButton />
|
||||
</Pagination.Item>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === 'ellipsis'}
|
||||
<Pagination.Item>
|
||||
<Pagination.Ellipsis />
|
||||
</Pagination.Item>
|
||||
{:else}
|
||||
<Pagination.Item>
|
||||
<Pagination.Link {page} isActive={auditLogs.pagination.currentPage === page.value}>
|
||||
{page.value}
|
||||
</Pagination.Link>
|
||||
</Pagination.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.Item>
|
||||
<Pagination.NextButton />
|
||||
</Pagination.Item>
|
||||
</Pagination.Content>
|
||||
</Pagination.Root>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user