From efed2cc324e6399963479d1853ccf9faa75b8f9e Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Sun, 28 Sep 2025 22:36:19 +0530 Subject: [PATCH 1/5] feat: Implement email OTP authentication --- .../(public)/(guest)/login/+page.svelte | 38 +++++- .../(guest)/login/email-otp/+page.svelte | 118 ++++++++++++++++++ .../(public)/(guest)/login/email-otp/+page.ts | 39 ++++++ 3 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 src/routes/(public)/(guest)/login/email-otp/+page.svelte create mode 100644 src/routes/(public)/(guest)/login/email-otp/+page.ts diff --git a/src/routes/(public)/(guest)/login/+page.svelte b/src/routes/(public)/(guest)/login/+page.svelte index e9f8f25b4d..1efc26447e 100644 --- a/src/routes/(public)/(guest)/login/+page.svelte +++ b/src/routes/(public)/(guest)/login/+page.svelte @@ -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 @@ Sign in -
+ - + + {#if showPasswordLogin} + + {:else} + + {/if} + {#if isCloud} or + +
+ + + Didn't get it? + Resend code + + +
+
diff --git a/src/routes/(public)/(guest)/login/email-otp/+page.ts b/src/routes/(public)/(guest)/login/email-otp/+page.ts new file mode 100644 index 0000000000..974a7a0039 --- /dev/null +++ b/src/routes/(public)/(guest)/login/email-otp/+page.ts @@ -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; +}; From 32180c5d38e082d82d0e174569265b2fe86c9130 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:02:39 +0530 Subject: [PATCH 2/5] added timer to prevent abuse --- .../account/sendVerificationEmailModal.svelte | 109 +++--------------- src/lib/components/resendCooldown.svelte | 83 +++++++++++++ .../(guest)/login/email-otp/+page.svelte | 10 +- 3 files changed, 106 insertions(+), 96 deletions(-) create mode 100644 src/lib/components/resendCooldown.svelte diff --git a/src/lib/components/account/sendVerificationEmailModal.svelte b/src/lib/components/account/sendVerificationEmailModal.svelte index 1ad53ed1bb..100ff63090 100644 --- a/src/lib/components/account/sendVerificationEmailModal.svelte +++ b/src/lib/components/account/sendVerificationEmailModal.svelte @@ -9,10 +9,9 @@ import Link from '$lib/elements/link.svelte'; import { Card, Layout, Typography } from '@appwrite.io/pink-svelte'; import { Dependencies } from '$lib/constants'; - import { onMount, onDestroy } from 'svelte'; + import { onDestroy } from 'svelte'; import { resolve } from '$app/paths'; - import { browser } from '$app/environment'; - import { slide } from 'svelte/transition'; + import ResendCooldown from '$lib/components/resendCooldown.svelte'; let { show = $bindable(false), @@ -25,8 +24,6 @@ let error = $state(null); let creating = $state(false); let emailSent = $state(false); - let resendTimer = $state(0); - let timerInterval: ReturnType | null = null; async function logout() { error = null; @@ -41,72 +38,15 @@ const cleanUrl = $derived(page.url.origin + page.url.pathname); - // manage resend timer in localStorage - const EMAIL_SENT_KEY = 'email_verification_sent'; - const TIMER_END_KEY = 'email_verification_timer_end'; - - function startResendTimer() { - resendTimer = 60; - emailSent = true; - const timerEndTime = Date.now() + 60 * 1000; - - if (browser) { - localStorage.setItem(EMAIL_SENT_KEY, 'true'); - localStorage.setItem(TIMER_END_KEY, timerEndTime.toString()); - } - - startTimerCountdown(timerEndTime); - } - - function restoreTimerState() { - if (!browser) return; - const savedTimerEnd = localStorage.getItem(TIMER_END_KEY); - const savedEmailSent = localStorage.getItem(EMAIL_SENT_KEY); - - if (savedTimerEnd && savedEmailSent) { - const timerEndTime = parseInt(savedTimerEnd); - const now = Date.now(); - const remainingTime = Math.max(0, Math.ceil((timerEndTime - now) / 1000)); - - if (remainingTime > 0) { - resendTimer = remainingTime; - emailSent = true; - startTimerCountdown(timerEndTime); - } else { - // timer has expired, clean up - localStorage.removeItem(TIMER_END_KEY); - localStorage.removeItem(EMAIL_SENT_KEY); - - resendTimer = 0; - emailSent = false; - } - } - } - - function startTimerCountdown(timerEndTime: number) { - timerInterval = setInterval(() => { - const now = Date.now(); - const remainingTime = Math.max(0, Math.ceil((timerEndTime - now) / 1000)); - resendTimer = remainingTime; - if (remainingTime <= 0) { - clearInterval(timerInterval); - timerInterval = null; - if (browser) { - localStorage.removeItem(TIMER_END_KEY); - localStorage.removeItem(EMAIL_SENT_KEY); - } - } - }, 1000); - } + // Timer UI handled by ResendCooldown component async function onSubmit() { - if (creating || resendTimer > 0) return; + if (creating) return; error = null; creating = true; try { await sdk.forConsole.account.createVerification({ url: cleanUrl }); emailSent = true; - startResendTimer(); } catch (err) { error = err.message; } finally { @@ -114,18 +54,7 @@ } } - onMount(restoreTimerState); - - onDestroy(() => { - if (timerInterval) { - clearInterval(timerInterval); - } - - if (browser) { - localStorage.removeItem(TIMER_END_KEY); - localStorage.removeItem(EMAIL_SENT_KEY); - } - }); + onDestroy(() => {});
@@ -147,27 +76,21 @@ logout()}>Switch account - - {#if emailSent && resendTimer > 0} -
- - Didn't get the email? Try again in {resendTimer}s - -
- {/if} - + {#if emailSent} + + {:else} + + {/if}
diff --git a/src/lib/components/resendCooldown.svelte b/src/lib/components/resendCooldown.svelte new file mode 100644 index 0000000000..f390f06648 --- /dev/null +++ b/src/lib/components/resendCooldown.svelte @@ -0,0 +1,83 @@ + + +{#if remaining > 0} + Try again in {remaining}s +{:else} + Resend code +{/if} diff --git a/src/routes/(public)/(guest)/login/email-otp/+page.svelte b/src/routes/(public)/(guest)/login/email-otp/+page.svelte index 474c46c213..50048c67f6 100644 --- a/src/routes/(public)/(guest)/login/email-otp/+page.svelte +++ b/src/routes/(public)/(guest)/login/email-otp/+page.svelte @@ -9,8 +9,9 @@ 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 { Layout, Typography } from '@appwrite.io/pink-svelte'; import { page } from '$app/state'; + import ResendCooldown from '$lib/components/resendCooldown.svelte'; let otpCode: string = ''; let disabled: boolean = false; @@ -74,7 +75,6 @@ email: email }); userId = sessionToken.userId; - addNotification({ type: 'success', message: 'New sign in code sent to your email.' @@ -111,7 +111,11 @@ Didn't get it? - Resend code + From fe1803247cf38b005b6a8713778bb63a8f9202b0 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Thu, 23 Oct 2025 18:11:55 +0530 Subject: [PATCH 3/5] svelte 5 migartion and some fixes --- .../account/sendVerificationEmailModal.svelte | 3 --- src/lib/components/resendCooldown.svelte | 4 ++-- .../(guest)/login/email-otp/+page.svelte | 22 +++++++++---------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/lib/components/account/sendVerificationEmailModal.svelte b/src/lib/components/account/sendVerificationEmailModal.svelte index 100ff63090..c6e7ab906f 100644 --- a/src/lib/components/account/sendVerificationEmailModal.svelte +++ b/src/lib/components/account/sendVerificationEmailModal.svelte @@ -9,7 +9,6 @@ import Link from '$lib/elements/link.svelte'; import { Card, Layout, Typography } from '@appwrite.io/pink-svelte'; import { Dependencies } from '$lib/constants'; - import { onDestroy } from 'svelte'; import { resolve } from '$app/paths'; import ResendCooldown from '$lib/components/resendCooldown.svelte'; @@ -53,8 +52,6 @@ creating = false; } } - - onDestroy(() => {});
diff --git a/src/lib/components/resendCooldown.svelte b/src/lib/components/resendCooldown.svelte index f390f06648..26118ac217 100644 --- a/src/lib/components/resendCooldown.svelte +++ b/src/lib/components/resendCooldown.svelte @@ -4,12 +4,12 @@ import { Link } from '@appwrite.io/pink-svelte'; let { - storageKey = 'resend_cooldown_default', + storageKey, seconds = 60, disabled = $bindable(false), onResend }: { - storageKey?: string; + storageKey: string; seconds?: number; disabled?: boolean; onResend: () => Promise | void; diff --git a/src/routes/(public)/(guest)/login/email-otp/+page.svelte b/src/routes/(public)/(guest)/login/email-otp/+page.svelte index 50048c67f6..2b1a42a2f2 100644 --- a/src/routes/(public)/(guest)/login/email-otp/+page.svelte +++ b/src/routes/(public)/(guest)/login/email-otp/+page.svelte @@ -12,19 +12,17 @@ import { Layout, Typography } from '@appwrite.io/pink-svelte'; import { page } from '$app/state'; import ResendCooldown from '$lib/components/resendCooldown.svelte'; + import { ID } from '@appwrite.io/console'; - let otpCode: string = ''; - let disabled: boolean = false; - let email: string = ''; - let userId: string = ''; + let { data } = $props(); - export let data; + let otpCode = $state(''); + let disabled = $state(false); + let email = $derived(page.url.searchParams.get('email') || ''); + let userId = $derived(page.url.searchParams.get('userId') || ''); - // Get email and userId from URL params - $: email = page.url.searchParams.get('email') || ''; - $: userId = page.url.searchParams.get('userId') || ''; - - async function verifyOTP() { + async function verifyOTP(event) { + event.preventDefault(); try { disabled = true; // Use createSession with the userId from createEmailToken @@ -71,7 +69,7 @@ try { disabled = true; const sessionToken = await sdk.forConsole.account.createEmailToken({ - userId: 'unique', + userId: ID.unique(), email: email }); userId = sessionToken.userId; @@ -102,7 +100,7 @@ We sent a 6-digit code to {email} -
+ From 89b69e1459875f157d57cb8f44b762d8b93f8c7a Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:51:50 +0530 Subject: [PATCH 4/5] Update src/routes/(public)/(guest)/login/+page.svelte Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/routes/(public)/(guest)/login/+page.svelte | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/routes/(public)/(guest)/login/+page.svelte b/src/routes/(public)/(guest)/login/+page.svelte index 1efc26447e..a41b0bf722 100644 --- a/src/routes/(public)/(guest)/login/+page.svelte +++ b/src/routes/(public)/(guest)/login/+page.svelte @@ -31,8 +31,11 @@ }); await goto( - `${base}/login/email-otp?email=${encodeURIComponent(mail)}&userId=${sessionToken.userId}` - ); + const params = new URLSearchParams(window.location.search); + params.set('email', mail); + params.set('userId', sessionToken.userId); + + await goto(`${base}/login/email-otp?${params.toString()}`); } catch (error) { disabled = false; addNotification({ From 30bfd2b2451974518437ea5a34090884f52c6687 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:55:47 +0530 Subject: [PATCH 5/5] Update src/routes/(public)/(guest)/login/+page.svelte Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/routes/(public)/(guest)/login/+page.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/(public)/(guest)/login/+page.svelte b/src/routes/(public)/(guest)/login/+page.svelte index a41b0bf722..f5a0c84ebd 100644 --- a/src/routes/(public)/(guest)/login/+page.svelte +++ b/src/routes/(public)/(guest)/login/+page.svelte @@ -30,7 +30,6 @@ email: mail }); - await goto( const params = new URLSearchParams(window.location.search); params.set('email', mail); params.set('userId', sessionToken.userId);