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 (
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
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
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,
}
)