feat: drop user initiated one time access token login method

This commit is contained in:
Elias Schneider
2026-07-29 23:49:08 +02:00
parent 2ef7c93fee
commit 0c27f733b9
11 changed files with 35 additions and 198 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ export default class UserService extends APIService {
cachedProfilePicture.bustCache(userId);
};
createOneTimeAccessToken = async (userId: string = 'me', ttl?: string | number) => {
createOneTimeAccessToken = async (userId: string, ttl?: string | number) => {
const res = await this.api.post(`/users/${userId}/one-time-access-token`, { ttl });
return res.data.token;
};
+7 -6
View File
@@ -229,6 +229,7 @@
class="mt-7 flex w-full max-w-112.5 justify-center"
>
<InputOTP.Root
class="gap-1 sm:gap-2"
maxlength={8}
aria-label={m.code()}
bind:value={userCode}
@@ -236,15 +237,15 @@
pasteTransformer={(value) => value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}
>
{#snippet children({ cells })}
<InputOTP.Group>
<InputOTP.Group class="gap-0.5 sm:gap-1.5">
{#each cells.slice(0, 4) as cell (cell)}
<InputOTP.Slot {cell} />
<InputOTP.Slot class="h-12 w-8 sm:h-13 sm:w-10" {cell} />
{/each}
</InputOTP.Group>
<InputOTP.Separator />
<InputOTP.Group>
<InputOTP.Group class="gap-0.5 sm:gap-1.5">
{#each cells.slice(4) as cell (cell)}
<InputOTP.Slot {cell} />
<InputOTP.Slot class="h-12 w-8 sm:h-13 sm:w-10" {cell} />
{/each}
</InputOTP.Group>
{/snippet}
@@ -274,10 +275,10 @@
<Button
form="device-code-form"
class="flex-1"
disabled={isLoading || !codeComplete}
disabled={!codeComplete}
{isLoading}
onclick={authorize}
>
{#if isLoading}<Spinner data-icon="inline-start" />{/if}
{m.authorize()}
</Button>
{/if}
@@ -13,18 +13,18 @@
} from '@lucide/svelte';
const methods = [
{
icon: LucideQrCode,
title: m.sign_in_with_another_device(),
description: m.sign_in_with_another_device_description(),
href: '/login/alternative/device'
},
{
icon: LucideRectangleEllipsis,
title: m.login_code(),
description: m.enter_a_login_code_to_sign_in(),
href: '/login/alternative/code'
},
{
icon: LucideQrCode,
title: m.sign_in_with_another_device(),
description: m.sign_in_with_another_device_description(),
href: '/login/alternative/device'
}
];
if ($appConfigStore.emailOneTimeAccessAsUnauthenticatedEnabled) {
@@ -6,6 +6,7 @@
import Input from '$lib/components/ui/input/input.svelte';
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.js';
import { getAxiosErrorMessage } from '$lib/utils/error-util';
import { preventDefault } from '$lib/utils/event-util';
@@ -18,8 +19,10 @@
let error: string | undefined = $state();
let backHref = $state('/login/alternative');
let longCodeRequested = $state(code.length > 6);
let showLongCodeOption = $state(true);
let longCodeRequested = $state(
code.length > 6 || !$appConfigStore.emailOneTimeAccessAsUnauthenticatedEnabled
);
let showLongCodeOption = $state($appConfigStore.emailOneTimeAccessAsUnauthenticatedEnabled);
let codeComplete = $derived(longCodeRequested ? code.length === 16 : code.length === 6);
const userService = new UserService();
@@ -12,18 +12,11 @@
import type { Passkey } from '$lib/types/passkey.type';
import type { AccountUpdate } from '$lib/types/user.type';
import { axiosErrorToast, getWebauthnErrorMessage } from '$lib/utils/error-util';
import {
KeyRound,
Languages,
LucideAlertTriangle,
RectangleEllipsis,
UserCog
} from '@lucide/svelte';
import { KeyRound, Languages, LucideAlertTriangle, UserCog } from '@lucide/svelte';
import { startRegistration } from '@simplewebauthn/browser';
import { toast } from 'svelte-sonner';
import AccountForm from './account-form.svelte';
import LocalePicker from './locale-picker.svelte';
import LoginCodeModal from './login-code-modal.svelte';
import PasskeyList from './passkey-list.svelte';
import RenamePasskeyModal from './rename-passkey-modal.svelte';
@@ -31,8 +24,6 @@
let account = $state(data.account);
let passkeys = $state(data.passkeys);
let passkeyToRename: Passkey | null = $state(null);
let showLoginCodeModal: boolean = $state(false);
const userService = new UserService();
const webauthnService = new WebAuthnService();
@@ -103,26 +94,6 @@
</Alert.Root>
{/if}
<!-- Login code card mobile -->
<div class="block sm:hidden">
<Item.Root variant="outline">
<Item.Media class="text-primary/80">
<RectangleEllipsis class="size-5" />
</Item.Media>
<Item.Content>
<Item.Title>{m.login_code()}</Item.Title>
<Item.Description>
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
</Item.Description>
</Item.Content>
<Item.Actions class="w-full sm:w-auto">
<Button variant="outline" class="w-full" onclick={() => (showLoginCodeModal = true)}>
{m.create()}
</Button>
</Item.Actions>
</Item.Root>
</div>
<Card.Root>
<Card.Header>
<Card.Title>
@@ -163,25 +134,6 @@
{/if}
</Item.Group>
<div class="hidden sm:block">
<Item.Root variant="card" class="border-border">
<Item.Media class="text-primary/80">
<RectangleEllipsis class="size-5" />
</Item.Media>
<Item.Content>
<Item.Title>{m.login_code()}</Item.Title>
<Item.Description>
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
</Item.Description>
</Item.Content>
<Item.Actions>
<Button variant="outline" onclick={() => (showLoginCodeModal = true)}>
{m.create()}
</Button>
</Item.Actions>
</Item.Root>
</div>
<Item.Root variant="card" class="border-border mb-2">
<Item.Media class="text-primary/80">
<Languages class="size-5" />
@@ -203,4 +155,3 @@
bind:passkey={passkeyToRename}
callback={async () => (passkeys = await webauthnService.listCredentials())}
/>
<LoginCodeModal bind:show={showLoginCodeModal} />
@@ -1,73 +0,0 @@
<script lang="ts">
import { page } from '$app/state';
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
import Qrcode from '$lib/components/qrcode/qrcode.svelte';
import * as Dialog from '$lib/components/ui/dialog';
import { Separator } from '$lib/components/ui/separator';
import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service';
import { axiosErrorToast } from '$lib/utils/error-util';
import { mode } from 'mode-watcher';
let {
show = $bindable()
}: {
show: boolean;
} = $props();
const userService = new UserService();
let code: string | null = $state(null);
let loginCodeLink: string | null = $state(null);
$effect(() => {
if (show) {
userService
.createOneTimeAccessToken('me')
.then((c) => {
code = c;
loginCodeLink = page.url.origin + '/lc/' + code;
})
.catch((e) => axiosErrorToast(e));
}
});
function onOpenChange(open: boolean) {
if (!open) {
code = null;
show = false;
}
}
</script>
<Dialog.Root open={!!code} {onOpenChange}>
<Dialog.Content class="max-w-md" onOpenAutoFocus={(e) => e.preventDefault()}>
<Dialog.Header>
<Dialog.Title>{m.login_code()}</Dialog.Title>
<Dialog.Description
>{m.sign_in_using_the_following_code_the_code_will_expire_in_minutes()}
</Dialog.Description>
</Dialog.Header>
<div class="flex flex-col items-center gap-2">
<CopyToClipboard value={code!}>
<p class="text-3xl font-bold">{code}</p>
</CopyToClipboard>
<div class="flex items-center justify-center gap-3 my-2 text-muted-foreground">
<Separator />
<p class="text-xs text-nowrap">{m.or_visit()}</p>
<Separator />
</div>
<Qrcode
class="mb-2"
value={loginCodeLink}
size={150}
color={mode.current === 'dark' ? '#FFFFFF' : '#000000'}
/>
<CopyToClipboard value={loginCodeLink!}>
<p class="text-sm" data-testId="login-code-link">{loginCodeLink!}</p>
</CopyToClipboard>
</div>
</Dialog.Content>
</Dialog.Root>