mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-31 19:56:35 +00:00
initial commit
This commit is contained in:
14
frontend/src/routes/settings/account/+page.server.ts
Normal file
14
frontend/src/routes/settings/account/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
84
frontend/src/routes/settings/account/+page.svelte
Normal file
84
frontend/src/routes/settings/account/+page.svelte
Normal 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())}
|
||||
/>
|
||||
58
frontend/src/routes/settings/account/account-form.svelte
Normal file
58
frontend/src/routes/settings/account/account-form.svelte
Normal 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>
|
||||
79
frontend/src/routes/settings/account/passkey-list.svelte
Normal file
79
frontend/src/routes/settings/account/passkey-list.svelte
Normal 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())}
|
||||
/>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user