mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-04-06 14:46:37 +00:00
feat: self-service user signup (#672)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
90
frontend/src/routes/signup/+page.svelte
Normal file
90
frontend/src/routes/signup/+page.svelte
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import SignupForm from '$lib/components/signup/signup-form.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import type { UserSignUp } from '$lib/types/user.type';
|
||||
import { getAxiosErrorMessage } from '$lib/utils/error-util';
|
||||
import { tryCatch } from '$lib/utils/try-catch-util';
|
||||
import { LucideChevronLeft } from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoginLogoErrorSuccessIndicator from '../login/components/login-logo-error-success-indicator.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
const userService = new UserService();
|
||||
|
||||
let isLoading = $state(false);
|
||||
let error: string | undefined = $state();
|
||||
|
||||
async function handleSignup(userData: UserSignUp) {
|
||||
isLoading = true;
|
||||
|
||||
const result = await tryCatch(userService.signup({ ...userData, token: data.token }));
|
||||
|
||||
if (result.error) {
|
||||
error = getAxiosErrorMessage(result.error);
|
||||
isLoading = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
userStore.setUser(result.data);
|
||||
isLoading = false;
|
||||
|
||||
goto('/signup/add-passkey');
|
||||
return true;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!$appConfigStore.allowUserSignups || $appConfigStore.allowUserSignups === 'disabled') {
|
||||
error = m.user_signups_are_disabled();
|
||||
return;
|
||||
}
|
||||
|
||||
// For token-based signups, check if we have a valid token
|
||||
if ($appConfigStore.allowUserSignups === 'withToken' && !data.token) {
|
||||
error = m.signup_requires_valid_token();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.signup()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper animate={!$appConfigStore.disableAnimations}>
|
||||
<div class="flex justify-center">
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||
{m.signup_to_appname({ appName: $appConfigStore.appName })}
|
||||
</h1>
|
||||
|
||||
{#if !error}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
{m.create_your_account_to_get_started()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
{error}.
|
||||
</p>
|
||||
{/if}
|
||||
{#if $appConfigStore.allowUserSignups === 'open' || data.token}
|
||||
<SignupForm callback={handleSignup} {isLoading} />
|
||||
<div class="mt-10 flex w-full items-center justify-between gap-2">
|
||||
<a class="text-muted-foreground mt-5 flex text-sm" href="/login"
|
||||
><LucideChevronLeft class="size-5" /> {m.back()}</a
|
||||
>
|
||||
<Button type="submit" form="sign-up-form" onclick={() => (error = undefined)}
|
||||
>{m.signup()}</Button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<Button class="mt-10" href="/login">{m.go_to_login()}</Button>
|
||||
{/if}
|
||||
</SignInWrapper>
|
||||
7
frontend/src/routes/signup/+page.ts
Normal file
7
frontend/src/routes/signup/+page.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ url }) => {
|
||||
return {
|
||||
token: url.searchParams.get('token') || undefined
|
||||
};
|
||||
};
|
||||
82
frontend/src/routes/signup/add-passkey/+page.svelte
Normal file
82
frontend/src/routes/signup/add-passkey/+page.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
|
||||
import { tryCatch } from '$lib/utils/try-catch-util';
|
||||
import { startRegistration } from '@simplewebauthn/browser';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoginLogoErrorSuccessIndicator from '../../login/components/login-logo-error-success-indicator.svelte';
|
||||
|
||||
const webauthnService = new WebAuthnService();
|
||||
|
||||
let isLoading = $state(false);
|
||||
let error: string | undefined = $state();
|
||||
|
||||
async function createPasskeyAndContinue() {
|
||||
isLoading = true;
|
||||
error = undefined;
|
||||
|
||||
const optsResult = await tryCatch(webauthnService.getRegistrationOptions());
|
||||
if (optsResult.error) {
|
||||
error = getWebauthnErrorMessage(optsResult.error);
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const attRespResult = await tryCatch(startRegistration({ optionsJSON: optsResult.data }));
|
||||
if (attRespResult.error) {
|
||||
error = getWebauthnErrorMessage(attRespResult.error);
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const finishResult = await tryCatch(webauthnService.finishRegistration(attRespResult.data));
|
||||
if (finishResult.error) {
|
||||
error = getWebauthnErrorMessage(finishResult.error);
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
goto('/settings/account');
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.add_passkey()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper animate={!$appConfigStore.disableAnimations}>
|
||||
<div class="w-full text-center">
|
||||
<div class="flex justify-center">
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||
{m.setup_your_passkey()}
|
||||
</h1>
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
{#if !error}
|
||||
{m.create_a_passkey_to_securely_access_your_account()}
|
||||
{:else}
|
||||
{error}. {m.please_try_again()}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="mt-10 flex w-full justify-between gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => goto('/settings/account')}
|
||||
disabled={isLoading}
|
||||
class="flex-1"
|
||||
>
|
||||
{m.skip_for_now()}
|
||||
</Button>
|
||||
<Button onclick={createPasskeyAndContinue} {isLoading} class="flex-1">
|
||||
{m.add_passkey()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SignInWrapper>
|
||||
Reference in New Issue
Block a user