diff --git a/src/interface/web/app/components/loginPrompt/loginPrompt.tsx b/src/interface/web/app/components/loginPrompt/loginPrompt.tsx index f0cef1ee..70b62b33 100644 --- a/src/interface/web/app/components/loginPrompt/loginPrompt.tsx +++ b/src/interface/web/app/components/loginPrompt/loginPrompt.tsx @@ -26,6 +26,7 @@ import { CarouselPrevious, } from "@/components/ui/carousel"; import { Card, CardContent } from "@/components/ui/card"; +import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"; export interface LoginPromptProps { loginRedirectMessage: string; @@ -227,6 +228,34 @@ function EmailSignInContext({ setRecheckEmail: (recheckEmail: boolean) => void; handleMagicLinkSignIn: () => void; }) { + const [otp, setOTP] = useState(""); + const [otpError, setOTPError] = useState(""); + + function checkOTPAndRedirect() { + const verifyUrl = `/auth/magic?code=${otp}`; + + fetch(verifyUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((res) => { + if (res.ok) { + // Check if the response is a redirect + if (res.redirected) { + window.location.href = res.url; + } + } else { + setOTPError("Invalid OTP"); + throw new Error("Failed to verify OTP"); + } + }) + .catch((err) => { + console.error(err); + }); + } + return (
{!checkEmail && ( <> @@ -269,10 +298,35 @@ function EmailSignInContext({ disabled={checkEmail} > - {checkEmail ? "Check your email" : "Send sign in link"} + {checkEmail ? "Check your email" : "Send sign in code"} )} + {checkEmail && ( +
+ + setTimeout(() => { + checkOTPAndRedirect(); + }, 1000) + } + > + + + + + + + + + +
{otpError}
+
+ )} {checkEmail && (
diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index 6f08d986..1a99d04c 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -238,7 +238,9 @@ async def aget_or_create_user_by_email(email: str) -> tuple[KhojUser, bool]: await user.asave() if user: - user.email_verification_code = secrets.token_urlsafe(18) + # Generate a secure 6-digit numeric code + user.email_verification_code = f"{secrets.randbelow(1000000):06}" + user.email_verification_code_expiry = datetime.now(tz=timezone.utc) + timedelta(minutes=30) await user.asave() user_subscription = await Subscription.objects.filter(user=user).afirst() @@ -272,6 +274,9 @@ async def aget_user_validated_by_email_verification_code(code: str) -> KhojUser: if not user: return None + if user.email_verification_code_expiry < datetime.now(tz=timezone.utc): + return None + user.email_verification_code = None user.verified_email = True await user.asave() diff --git a/src/khoj/database/migrations/0078_khojuser_email_verification_code_expiry.py b/src/khoj/database/migrations/0078_khojuser_email_verification_code_expiry.py new file mode 100644 index 00000000..09eac302 --- /dev/null +++ b/src/khoj/database/migrations/0078_khojuser_email_verification_code_expiry.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.9 on 2024-12-14 18:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0077_chatmodel_alter_agent_chat_model_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="khojuser", + name="email_verification_code_expiry", + field=models.DateTimeField(blank=True, default=None, null=True), + ), + ] diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index 12a303d3..cea4b056 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -138,6 +138,7 @@ class KhojUser(AbstractUser): verified_phone_number = models.BooleanField(default=False) verified_email = models.BooleanField(default=False) email_verification_code = models.CharField(max_length=200, null=True, default=None, blank=True) + email_verification_code_expiry = models.DateTimeField(null=True, default=None, blank=True) def save(self, *args, **kwargs): if not self.uuid: diff --git a/src/khoj/interface/email/magic_link.html b/src/khoj/interface/email/magic_link.html index 7e0eb2fe..0ddab90e 100644 --- a/src/khoj/interface/email/magic_link.html +++ b/src/khoj/interface/email/magic_link.html @@ -1,17 +1,53 @@ - - - Welcome to Khoj - - - - - -

Hi! Click here to sign in on this browser.

+ -

- The Khoj Team

+ + + + Welcome to Khoj + + +
+ + Khoj Logo + + +

+ Merge AI with your brain

+ +

Hi!

+ +

Use this code (valid for 30 minutes) to login to Khoj:

+ +

{{ code }}

+ +

Alternatively, Click here to sign in on this + browser.

+ +

You're about to get a whole lot more productive. +

+ +
- The Khoj Team
+ +
+ Docs + GitHub + Twitter + LinkedIn + Discord +
+
+ diff --git a/src/khoj/routers/email.py b/src/khoj/routers/email.py index 9d0d3600..27dd8f5d 100644 --- a/src/khoj/routers/email.py +++ b/src/khoj/routers/email.py @@ -41,13 +41,13 @@ async def send_magic_link_email(email, unique_id, host): template = env.get_template("magic_link.html") - html_content = template.render(link=f"{host}auth/magic?code={unique_id}") + html_content = template.render(link=f"{host}auth/magic?code={unique_id}", code=unique_id) resend.Emails.send( { "sender": os.environ.get("RESEND_EMAIL", "noreply@khoj.dev"), "to": email, - "subject": "Your Sign-In Link for Khoj 🚀", + "subject": f"{unique_id} - Sign in to Khoj 🚀", "html": html_content, } )