feat: Implement email OTP authentication

This commit is contained in:
Harsh Mahajan
2025-09-28 22:36:19 +05:30
parent 9e3b6c5893
commit efed2cc324
3 changed files with 190 additions and 5 deletions
+33 -5
View File
@@ -15,9 +15,33 @@
import { Layout } from '@appwrite.io/pink-svelte';
let mail: string, pass: string, disabled: boolean;
let showPasswordLogin: boolean = false;
export let data;
$: showPasswordLogin = pass && pass.length > 0;
async function sendSignInCode() {
try {
disabled = true;
// use createEmailToken for sign in with code
const sessionToken = await sdk.forConsole.account.createEmailToken({
userId: 'unique',
email: mail
});
await goto(
`${base}/login/email-otp?email=${encodeURIComponent(mail)}&userId=${sessionToken.userId}`
);
} catch (error) {
disabled = false;
addNotification({
type: 'error',
message: error.message
});
}
}
async function login() {
try {
disabled = true;
@@ -52,7 +76,6 @@
return;
}
// no specific redirect, so redirect will happen through invalidating the account
await invalidate(Dependencies.ACCOUNT);
} catch (error) {
disabled = false;
@@ -92,22 +115,27 @@
<Unauthenticated coupon={data?.couponData} campaign={data?.campaign}>
<svelte:fragment slot="title">Sign in</svelte:fragment>
<svelte:fragment>
<Form onSubmit={login}>
<Form onSubmit={showPasswordLogin ? login : sendSignInCode}>
<Layout.Stack>
<InputEmail
id="email"
label="Email"
placeholder="Email"
autofocus={true}
required={true}
bind:value={mail} />
<InputPassword
id="password"
label="Password"
placeholder="Password"
required={true}
required={false}
bind:value={pass} />
<Button fullWidth submit {disabled}>Sign in</Button>
{#if showPasswordLogin}
<Button fullWidth submit {disabled}>Sign in</Button>
{:else}
<Button fullWidth submit {disabled}>Get sign in code</Button>
{/if}
{#if isCloud}
<span class="with-separators eyebrow-heading-3">or</span>
<Button secondary fullWidth on:click={onGithubLogin} {disabled}>
@@ -0,0 +1,118 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { InputDigits, Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Unauthenticated } from '$lib/layout';
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { redirectTo } from '$routes/store';
import { user } from '$lib/stores/user';
import { Layout, Typography, Link } from '@appwrite.io/pink-svelte';
import { page } from '$app/state';
let otpCode: string = '';
let disabled: boolean = false;
let email: string = '';
let userId: string = '';
export let data;
// Get email and userId from URL params
$: email = page.url.searchParams.get('email') || '';
$: userId = page.url.searchParams.get('userId') || '';
async function verifyOTP() {
try {
disabled = true;
// Use createSession with the userId from createEmailToken
await sdk.forConsole.account.createSession({ userId: userId, secret: otpCode });
if ($user) {
trackEvent(Submit.AccountLogin, { mfa_used: 'none' });
addNotification({
type: 'success',
message: 'Successfully logged in.'
});
}
if (data?.couponData?.code) {
trackEvent(Submit.AccountCreate, {
campaign_name: data?.couponData?.code,
email: email,
name: $user?.name
});
await goto(`${base}/apply-credit?code=${data?.couponData?.code}`);
return;
}
if (data?.campaign?.$id) {
await goto(`${base}/apply-credit?campaign=${data.campaign?.$id}`);
return;
}
if ($redirectTo) {
window.location.href = $redirectTo;
return;
}
await invalidate(Dependencies.ACCOUNT);
} catch (error) {
disabled = false;
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.AccountLogin);
}
}
async function resendCode() {
try {
disabled = true;
const sessionToken = await sdk.forConsole.account.createEmailToken({
userId: 'unique',
email: email
});
userId = sessionToken.userId;
addNotification({
type: 'success',
message: 'New sign in code sent to your email.'
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
} finally {
disabled = false;
}
}
</script>
<svelte:head>
<title>Enter your sign-in code - Appwrite</title>
</svelte:head>
<Unauthenticated coupon={data?.couponData} campaign={data?.campaign} align="center">
<svelte:fragment slot="title">Enter your sign-in code</svelte:fragment>
<svelte:fragment>
<Layout.Stack gap="l" justifyContent="center" alignContent="center" alignItems="center">
<Typography.Text align="center">
We sent a 6-digit code to {email}
</Typography.Text>
<form on:submit|preventDefault={verifyOTP}>
<Layout.Stack align="center">
<InputDigits bind:value={otpCode} required />
<Button submit {disabled} class="verify-button">Verify</Button>
</Layout.Stack>
</form>
<Typography.Text align="center">
Didn't get it?
<Link.Button on:click={resendCode} {disabled}>Resend code</Link.Button>
</Typography.Text>
</Layout.Stack>
</svelte:fragment>
</Unauthenticated>
@@ -0,0 +1,39 @@
import { base } from '$app/paths';
import type { Campaign } from '$lib/stores/campaigns';
import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url }) => {
if (!url.searchParams.has('email')) {
redirect(303, `${base}/login`);
}
if (url.searchParams.has('code')) {
const code = url.searchParams.get('code');
let campaign: Campaign;
try {
const couponData = await sdk.forConsole.billing.getCoupon(code);
if (couponData.campaign) {
campaign = await sdk.forConsole.billing.getCampaign(couponData.campaign);
return {
couponData,
campaign
};
} else redirect(303, `${base}/login`);
} catch (e) {
redirect(303, `${base}/login`);
}
}
if (url.searchParams.has('campaign')) {
const campaignId = url.searchParams.get('campaign');
let campaign: Campaign;
try {
campaign = await sdk.forConsole.billing.getCampaign(campaignId);
return { campaign };
} catch (e) {
redirect(303, `${base}/login`);
}
}
return;
};