initial commit

This commit is contained in:
Elias Schneider
2024-08-12 11:00:25 +02:00
commit eaff977b22
241 changed files with 14378 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import UserService from '$lib/services/user-service';
import WebAuthnService from '$lib/services/webauthn-service';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => {
const webauthnService = new WebAuthnService(cookies.get('access_token'));
const userService = new UserService(cookies.get('access_token'));
const account = await userService.getCurrent();
const passkeys = await webauthnService.listCredentials();
return {
account,
passkeys
};
};

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import UserService from '$lib/services/user-service';
import WebAuthnService from '$lib/services/webauthn-service';
import type { Passkey } from '$lib/types/passkey.type';
import type { UserCreate } from '$lib/types/user.type';
import { axiosErrorToast, getWebauthnErrorMessage } from '$lib/utils/error-util';
import { startRegistration } from '@simplewebauthn/browser';
import { toast } from 'svelte-sonner';
import AccountForm from './account-form.svelte';
import PasskeyList from './passkey-list.svelte';
import RenamePasskeyModal from './rename-passkey-modal.svelte';
let { data } = $props();
let account = $state(data.account);
let passkeys = $state(data.passkeys);
let passkeyToRename: Passkey | null = $state(null);
const userService = new UserService();
const webauthnService = new WebAuthnService();
async function updateAccount(user: UserCreate) {
let success = true;
await userService
.updateCurrent(user)
.then(() => toast.success('Account details updated successfully'))
.catch((e) => {
axiosErrorToast(e);
success = false;
});
return success;
}
async function createPasskey() {
try {
const opts = await webauthnService.getRegistrationOptions();
const attResp = await startRegistration(opts);
const passkey = await webauthnService.finishRegistration(attResp);
passkeys = await webauthnService.listCredentials();
passkeyToRename = passkey;
} catch (e) {
toast.error(getWebauthnErrorMessage(e));
}
}
</script>
<svelte:head>
<title>Account Settings</title>
</svelte:head>
<Card.Root>
<Card.Header>
<Card.Title>Account Details</Card.Title>
</Card.Header>
<Card.Content>
<AccountForm {account} callback={updateAccount} />
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Passkeys</Card.Title>
<Card.Description class="mt-1">
Manage your passkeys that you can use to authenticate yourself.
</Card.Description>
</div>
<Button size="sm" on:click={createPasskey}>Add Passkey</Button>
</div>
</Card.Header>
{#if passkeys.length != 0}
<Card.Content>
<PasskeyList {passkeys} />
</Card.Content>
{/if}
</Card.Root>
<RenamePasskeyModal
bind:passkey={passkeyToRename}
callback={async () => (passkeys = await webauthnService.listCredentials())}
/>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import FormInput from '$lib/components/form-input.svelte';
import { Button } from '$lib/components/ui/button';
import type { UserCreate } from '$lib/types/user.type';
import { createForm } from '$lib/utils/form-util';
import { z } from 'zod';
let {
callback,
account
}: {
account: UserCreate;
callback: (user: UserCreate) => Promise<boolean>;
} = $props();
let isLoading = $state(false);
const formSchema = z.object({
firstName: z.string().min(2).max(50),
lastName: z.string().min(2).max(50),
username: z.string().min(2).max(50),
email: z.string().email(),
isAdmin: z.boolean()
});
type FormSchema = typeof formSchema;
const { inputs, ...form } = createForm<FormSchema>(formSchema, account);
async function onSubmit() {
const data = form.validate();
if (!data) return;
isLoading = true;
const success = await callback(data);
// Reset form if user was successfully created
isLoading = false;
}
</script>
<form onsubmit={onSubmit}>
<div class="flex flex-col gap-3 sm:flex-row">
<div class="w-full">
<FormInput label="Firstname" bind:input={$inputs.firstName} />
</div>
<div class="w-full">
<FormInput label="Lastname" bind:input={$inputs.lastName} />
</div>
</div>
<div class="mt-3 flex flex-col gap-3 sm:flex-row">
<div class="w-full">
<FormInput label="Email" bind:input={$inputs.email} />
</div>
<div class="w-full">
<FormInput label="Username" bind:input={$inputs.username} />
</div>
</div>
<div class="mt-5 flex justify-end">
<Button {isLoading} type="submit">Save</Button>
</div>
</form>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
import { Button } from '$lib/components/ui/button';
import { Separator } from '$lib/components/ui/separator';
import WebauthnService from '$lib/services/webauthn-service';
import type { Passkey } from '$lib/types/passkey.type';
import { axiosErrorToast } from '$lib/utils/error-util';
import { LucideKeyRound, LucidePencil, LucideTrash } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import RenamePasskeyModal from './rename-passkey-modal.svelte';
let { passkeys: initialsPasskeys }: { passkeys: Passkey[] } = $props();
let passkeys = $state<Passkey[]>(initialsPasskeys);
const webauthnService = new WebauthnService();
$effect(() => {
passkeys = initialsPasskeys;
});
let passkeyToRename: Passkey | null = $state(null);
async function deletePasskey(passkey: Passkey) {
openConfirmDialog({
title: `Delete ${passkey.name}`,
message: 'Are you sure you want to delete this passkey?',
confirm: {
label: 'Delete',
destructive: true,
action: async () => {
try {
await webauthnService.removeCredential(passkey.id);
passkeys = await webauthnService.listCredentials();
toast.success('Passkey deleted successfully');
} catch (e) {
axiosErrorToast(e);
}
}
}
});
}
</script>
<div class="flex flex-col">
{#each passkeys as passkey, i}
<div class="flex justify-between">
<div class="flex items-center">
<LucideKeyRound class="mr-4 inline h-6 w-6" />
<div>
<p>{passkey.name}</p>
<p class="text-xs text-muted-foreground">
Added on {new Date(passkey.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<div>
<Button
on:click={() => (passkeyToRename = passkey)}
size="sm"
variant="outline"
aria-label="Rename"><LucidePencil class="h-3 w-3" /></Button
>
<Button
on:click={() => deletePasskey(passkey)}
size="sm"
variant="outline"
aria-label="Delete"><LucideTrash class="h-3 w-3 text-red-500" /></Button
>
</div>
</div>
{#if i !== passkeys.length - 1}
<Separator class="my-2" />
{/if}
{/each}
</div>
<RenamePasskeyModal
bind:passkey={passkeyToRename}
callback={async () => (passkeys = await webauthnService.listCredentials())}
/>

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import WebAuthnService from '$lib/services/webauthn-service';
import type { Passkey } from '$lib/types/passkey.type';
import { axiosErrorToast } from '$lib/utils/error-util';
import { toast } from 'svelte-sonner';
let {
passkey = $bindable(),
callback
}: {
passkey: Passkey | null;
callback?: () => void;
} = $props();
let name = $state('');
$effect(() => {
if (passkey) name = passkey.name;
});
const webauthnService = new WebAuthnService();
function onOpenChange(open: boolean) {
if (!open) {
passkey = null;
}
}
async function onSubmit() {
await webauthnService
.updateCredentialName(passkey!.id, name)
.then(() => {
passkey = null;
toast.success('Passkey name updated successfully');
callback?.();
})
.catch(axiosErrorToast);
}
</script>
<Dialog.Root open={!!passkey} {onOpenChange}>
<Dialog.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>Name Passkey</Dialog.Title>
<Dialog.Description>Name your passkey to easily identify it later.</Dialog.Description>
</Dialog.Header>
<form onsubmit={onSubmit}>
<div class="grid items-center gap-4 sm:grid-cols-4">
<Label for="name" class="sm:text-right">Name</Label>
<Input id="name" bind:value={name} class="col-span-3" />
</div>
<Dialog.Footer class="mt-4">
<Button type="submit">Save</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>