mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-21 01:11:33 +03:00
feat(account): add ability to sign in with login code (#271)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
</script>
|
||||
|
||||
<Tooltip.Root closeOnPointerDown={false} {onOpenChange} {open}>
|
||||
<Tooltip.Trigger class="text-start" onclick={onClick}>{@render children()}</Tooltip.Trigger>
|
||||
<Tooltip.Trigger class="text-start" tabindex={-1} onclick={onClick}>{@render children()}</Tooltip.Trigger>
|
||||
<Tooltip.Content onclick={copyToClipboard}>
|
||||
{#if copied}
|
||||
<span class="flex items-center"><LucideCheck class="mr-1 h-4 w-4" /> Copied</span>
|
||||
|
||||
@@ -1,46 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { browserSupportsWebAuthn } from '@simplewebauthn/browser';
|
||||
import { page } from '$app/state';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { Button } from './ui/button';
|
||||
import * as Card from './ui/card';
|
||||
import WebAuthnUnsupported from './web-authn-unsupported.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let {
|
||||
children,
|
||||
showEmailOneTimeAccessButton = false
|
||||
showAlternativeSignInMethodButton = false
|
||||
}: {
|
||||
children: Snippet;
|
||||
showEmailOneTimeAccessButton?: boolean;
|
||||
showAlternativeSignInMethodButton?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="hidden h-screen items-center text-center lg:flex">
|
||||
<div class="h-full min-w-[650px] p-16 {showEmailOneTimeAccessButton ? 'pb-0' : ''}">
|
||||
{#if browser && !browserSupportsWebAuthn()}
|
||||
<WebAuthnUnsupported />
|
||||
{:else}
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex flex-grow flex-col items-center justify-center">
|
||||
{@render children()}
|
||||
</div>
|
||||
{#if showEmailOneTimeAccessButton}
|
||||
<div class="mb-4 flex justify-center">
|
||||
<Button
|
||||
href="/login/email?redirect={encodeURIComponent(
|
||||
$page.url.pathname + $page.url.search
|
||||
)}"
|
||||
variant="link"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
Don't have access to your passkey?
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="h-full min-w-[650px] p-16 {showAlternativeSignInMethodButton ? 'pb-0' : ''}">
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex flex-grow flex-col items-center justify-center">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
{#if showAlternativeSignInMethodButton}
|
||||
<div class="mb-4 flex justify-center">
|
||||
<a
|
||||
href={page.url.pathname == '/login'
|
||||
? '/login/alternative'
|
||||
: `/login/alternative?redirect=${encodeURIComponent(
|
||||
page.url.pathname + page.url.search
|
||||
)}`}
|
||||
class="text-muted-foreground text-xs"
|
||||
>
|
||||
Don't have access to your passkey?
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src="/api/application-configuration/background-image"
|
||||
@@ -55,25 +48,20 @@
|
||||
>
|
||||
<Card.Root class="mx-3">
|
||||
<Card.CardContent
|
||||
class="px-4 py-10 sm:p-10 {showEmailOneTimeAccessButton ? 'pb-3 sm:pb-3' : ''}"
|
||||
class="px-4 py-10 sm:p-10 {showAlternativeSignInMethodButton ? 'pb-3 sm:pb-3' : ''}"
|
||||
>
|
||||
{#if browser && !browserSupportsWebAuthn()}
|
||||
<WebAuthnUnsupported />
|
||||
{:else}
|
||||
{@render children()}
|
||||
{#if showEmailOneTimeAccessButton}
|
||||
<div class="mt-5">
|
||||
<Button
|
||||
href="/login/email?redirect={encodeURIComponent(
|
||||
$page.url.pathname + $page.url.search
|
||||
)}"
|
||||
variant="link"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
Don't have access to your passkey?
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{@render children()}
|
||||
{#if showAlternativeSignInMethodButton}
|
||||
<a
|
||||
href={page.url.pathname == '/login'
|
||||
? '/login/alternative'
|
||||
: `/login/alternative?redirect=${encodeURIComponent(
|
||||
page.url.pathname + page.url.search
|
||||
)}`}
|
||||
class="text-muted-foreground mt-5 text-xs"
|
||||
>
|
||||
Don't have access to your passkey?
|
||||
</a>
|
||||
{/if}
|
||||
</Card.CardContent>
|
||||
</Card.Root>
|
||||
|
||||
85
frontend/src/lib/components/one-time-link-modal.svelte
Normal file
85
frontend/src/lib/components/one-time-link-modal.svelte
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import * as Select from '$lib/components/ui/select/index.js';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
|
||||
let {
|
||||
userId = $bindable()
|
||||
}: {
|
||||
userId: string | null;
|
||||
} = $props();
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
let oneTimeLink: string | null = $state(null);
|
||||
let selectedExpiration: keyof typeof availableExpirations = $state('1 hour');
|
||||
|
||||
let availableExpirations = {
|
||||
'1 hour': 60 * 60,
|
||||
'12 hours': 60 * 60 * 12,
|
||||
'1 day': 60 * 60 * 24,
|
||||
'1 week': 60 * 60 * 24 * 7,
|
||||
'1 month': 60 * 60 * 24 * 30
|
||||
};
|
||||
|
||||
async function createOneTimeAccessToken() {
|
||||
try {
|
||||
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
||||
const token = await userService.createOneTimeAccessToken(expiration, userId!);
|
||||
oneTimeLink = `${page.url.origin}/lc/${token}`;
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
oneTimeLink = null;
|
||||
userId = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root open={!!userId} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Login Code</Dialog.Title>
|
||||
<Dialog.Description
|
||||
>Create a login code that the user can use to sign in without a passkey once.</Dialog.Description
|
||||
>
|
||||
</Dialog.Header>
|
||||
{#if oneTimeLink === null}
|
||||
<div>
|
||||
<Label for="expiration">Expiration</Label>
|
||||
<Select.Root
|
||||
selected={{
|
||||
label: Object.keys(availableExpirations)[0],
|
||||
value: Object.keys(availableExpirations)[0]
|
||||
}}
|
||||
onSelectedChange={(v) =>
|
||||
(selectedExpiration = v!.value as keyof typeof availableExpirations)}
|
||||
>
|
||||
<Select.Trigger class="h-9 ">
|
||||
<Select.Value>{selectedExpiration}</Select.Value>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each Object.keys(availableExpirations) as key}
|
||||
<Select.Item value={key}>{key}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<Button onclick={() => createOneTimeAccessToken()} disabled={!selectedExpiration}>
|
||||
Generate Code
|
||||
</Button>
|
||||
{:else}
|
||||
<Label for="login-code" class="sr-only">Login Code</Label>
|
||||
<Input id="login-code" value={oneTimeLink} readonly />
|
||||
{/if}
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -3,11 +3,11 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col justify-center">
|
||||
<div class="mx-auto rounded-2xl bg-muted p-3">
|
||||
<div class="bg-muted mx-auto rounded-2xl p-3">
|
||||
<Logo class="h-10 w-10" />
|
||||
</div>
|
||||
<p class="mt-5 font-playfair text-3xl font-bold sm:text-4xl">Browser unsupported</p>
|
||||
<p class="mt-3 text-muted-foreground">
|
||||
This browser doesn't support passkeys. Please use a browser that supports WebAuthn to sign in.
|
||||
<p class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Browser unsupported</p>
|
||||
<p class="text-muted-foreground mt-3">
|
||||
This browser doesn't support passkeys. Please or use a alternative sign in method.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user