Allow users to directly enter their unique code when logging in

- Code automatically becomes invalid after 30 minutes
This commit is contained in:
sabaimran 2024-12-14 11:06:05 -08:00
parent c25174e8d4
commit 6a56140360
6 changed files with 132 additions and 19 deletions

View file

@ -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 (
<div className="flex flex-col gap-4 p-4">
<Button
@ -244,9 +273,9 @@ function EmailSignInContext({
<div className="text-center text-sm text-muted-foreground">
{checkEmail
? recheckEmail
? `A new link has been sent to ${email}. Click on the link in your email to sign-in`
: `A one time sign in link has been sent to ${email}. Click on it to sign in on any browser.`
: "You will receive a sign-in link on the email address you provide below"}
? `A new link has been sent to ${email}.`
: `A one time sign in code has been sent to ${email}.`
: "You will receive a sign-in code on the email address you provide below"}
</div>
{!checkEmail && (
<>
@ -269,10 +298,35 @@ function EmailSignInContext({
disabled={checkEmail}
>
<PaperPlaneTilt className="h-6 w-6 mr-2 font-bold" />
{checkEmail ? "Check your email" : "Send sign in link"}
{checkEmail ? "Check your email" : "Send sign in code"}
</Button>
</>
)}
{checkEmail && (
<div className="flex items-center justify-center gap-4 text-muted-foreground flex-col">
<InputOTP
autoFocus={true}
maxLength={6}
value={otp || ""}
onChange={setOTP}
onComplete={() =>
setTimeout(() => {
checkOTPAndRedirect();
}, 1000)
}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<div className="text-red-500 text-sm">{otpError}</div>
</div>
)}
{checkEmail && (
<div className="flex items-center justify-center gap-4 text-muted-foreground flex-col md:flex-row">

View file

@ -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()

View file

@ -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),
),
]

View file

@ -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:

View file

@ -1,17 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Khoj</title>
</head>
<body>
<body style="font-family: 'Verdana', sans-serif; font-weight: 400; font-style: normal; padding: 0; text-align: left; width: 600px; margin: 20px auto;">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<a class="logo" href="https://khoj.dev" target="_blank" style="text-decoration: none; text-decoration: underline dotted;">
<img src="https://assets.khoj.dev/khoj_logo.png" alt="Khoj Logo" style="width: 100px;">
</a>
<p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">Hi! <a href="{{ link }}" target="_blank" style="text-decoration: none; text-decoration: underline dotted;">Click here to sign in on this browser.</a></p>
</head>
<p style="color: #333; font-size: large; margin-top: 20px; padding: 0; line-height: 1.5;">- The Khoj Team</p>
<body
style="font-family: 'Arial', sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f5f5f5;">
<div
style="background-color: #ffffff; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 30px;">
<a href="https://khoj.dev" target="_blank"
style="display: block; text-align: center; margin-bottom: 20px; text-decoration: none;">
<img src="https://assets.khoj.dev/khoj_logo.png" alt="Khoj Logo" style="width: 120px;">
</a>
<h1
style="font-size: 24px; color: #2c3e50; margin-bottom: 20px; text-align: center; border-bottom: 2px solid #FFA07A; padding-bottom: 10px;">
Merge AI with your brain</h1>
<p style="font-size: 16px; color: #333; margin-bottom: 20px;">Hi!</p>
<p style="font-size: 16px; color: #333; margin-bottom: 20px;">Use this code (valid for 30 minutes) to login to Khoj:</p>
<h1 style="font-size: 24px; color: #2c3e50; margin-bottom: 20px; text-align: center;">{{ code }}</h1>
<p style="font-size: 16px; color: #333; margin-bottom: 20px;">Alternatively, <a href="{{ link }}" target="_blank"
style="color: #FFA07A; text-decoration: none; font-weight: bold;">Click here to sign in on this
browser.</a></p>
<p style="font-size: 16px; color: #333; margin-bottom: 20px;">You're about to get a whole lot more productive.
</p>
<div style="font-size: 18px; font-weight: bold; margin-top: 30px; text-align: right;">- The Khoj Team</div>
<div style="margin-top: 30px; text-align: center;">
<a href="https://docs.khoj.dev" target="_blank"
style="display: inline-block; margin: 0 10px; padding: 8px 15px; background-color: #FFA07A; color: #ffffff; text-decoration: none; border-radius: 5px;">Docs</a>
<a href="https://github.com/khoj-ai/khoj" target="_blank"
style="display: inline-block; margin: 0 10px; padding: 8px 15px; background-color: #FFA07A; color: #ffffff; text-decoration: none; border-radius: 5px;">GitHub</a>
<a href="https://twitter.com/khoj_ai" target="_blank"
style="display: inline-block; margin: 0 10px; padding: 8px 15px; background-color: #FFA07A; color: #ffffff; text-decoration: none; border-radius: 5px;">Twitter</a>
<a href="https://www.linkedin.com/company/khoj-ai" target="_blank"
style="display: inline-block; margin: 0 10px; padding: 8px 15px; background-color: #FFA07A; color: #ffffff; text-decoration: none; border-radius: 5px;">LinkedIn</a>
<a href="https://discord.gg/BDgyabRM6e" target="_blank"
style="display: inline-block; margin: 0 10px; padding: 8px 15px; background-color: #FFA07A; color: #ffffff; text-decoration: none; border-radius: 5px;">Discord</a>
</div>
</div>
</body>
</html>

View file

@ -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,
}
)