mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-12-18 18:47:11 +00:00
Allow users to directly enter their unique code when logging in
- Code automatically becomes invalid after 30 minutes
This commit is contained in:
parent
c25174e8d4
commit
6a56140360
6 changed files with 132 additions and 19 deletions
|
@ -26,6 +26,7 @@ import {
|
||||||
CarouselPrevious,
|
CarouselPrevious,
|
||||||
} from "@/components/ui/carousel";
|
} from "@/components/ui/carousel";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
|
||||||
|
|
||||||
export interface LoginPromptProps {
|
export interface LoginPromptProps {
|
||||||
loginRedirectMessage: string;
|
loginRedirectMessage: string;
|
||||||
|
@ -227,6 +228,34 @@ function EmailSignInContext({
|
||||||
setRecheckEmail: (recheckEmail: boolean) => void;
|
setRecheckEmail: (recheckEmail: boolean) => void;
|
||||||
handleMagicLinkSignIn: () => 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 (
|
return (
|
||||||
<div className="flex flex-col gap-4 p-4">
|
<div className="flex flex-col gap-4 p-4">
|
||||||
<Button
|
<Button
|
||||||
|
@ -244,9 +273,9 @@ function EmailSignInContext({
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
{checkEmail
|
{checkEmail
|
||||||
? recheckEmail
|
? recheckEmail
|
||||||
? `A new link has been sent to ${email}. Click on the link in your email to sign-in`
|
? `A new link has been sent to ${email}.`
|
||||||
: `A one time sign in link has been sent to ${email}. Click on it to sign in on any browser.`
|
: `A one time sign in code has been sent to ${email}.`
|
||||||
: "You will receive a sign-in link on the email address you provide below"}
|
: "You will receive a sign-in code on the email address you provide below"}
|
||||||
</div>
|
</div>
|
||||||
{!checkEmail && (
|
{!checkEmail && (
|
||||||
<>
|
<>
|
||||||
|
@ -269,10 +298,35 @@ function EmailSignInContext({
|
||||||
disabled={checkEmail}
|
disabled={checkEmail}
|
||||||
>
|
>
|
||||||
<PaperPlaneTilt className="h-6 w-6 mr-2 font-bold" />
|
<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>
|
</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 && (
|
{checkEmail && (
|
||||||
<div className="flex items-center justify-center gap-4 text-muted-foreground flex-col md:flex-row">
|
<div className="flex items-center justify-center gap-4 text-muted-foreground flex-col md:flex-row">
|
||||||
|
|
|
@ -238,7 +238,9 @@ async def aget_or_create_user_by_email(email: str) -> tuple[KhojUser, bool]:
|
||||||
await user.asave()
|
await user.asave()
|
||||||
|
|
||||||
if user:
|
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()
|
await user.asave()
|
||||||
|
|
||||||
user_subscription = await Subscription.objects.filter(user=user).afirst()
|
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:
|
if not user:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if user.email_verification_code_expiry < datetime.now(tz=timezone.utc):
|
||||||
|
return None
|
||||||
|
|
||||||
user.email_verification_code = None
|
user.email_verification_code = None
|
||||||
user.verified_email = True
|
user.verified_email = True
|
||||||
await user.asave()
|
await user.asave()
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -138,6 +138,7 @@ class KhojUser(AbstractUser):
|
||||||
verified_phone_number = models.BooleanField(default=False)
|
verified_phone_number = models.BooleanField(default=False)
|
||||||
verified_email = 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 = 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):
|
def save(self, *args, **kwargs):
|
||||||
if not self.uuid:
|
if not self.uuid:
|
||||||
|
|
|
@ -1,17 +1,53 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Welcome to Khoj</title>
|
<title>Welcome to Khoj</title>
|
||||||
</head>
|
</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;">
|
<body
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
style="font-family: 'Arial', sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f5f5f5;">
|
||||||
<a class="logo" href="https://khoj.dev" target="_blank" style="text-decoration: none; text-decoration: underline dotted;">
|
<div
|
||||||
<img src="https://assets.khoj.dev/khoj_logo.png" alt="Khoj Logo" style="width: 100px;">
|
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>
|
</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>
|
|
||||||
|
|
||||||
<p style="color: #333; font-size: large; margin-top: 20px; padding: 0; line-height: 1.5;">- The Khoj Team</p>
|
<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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -41,13 +41,13 @@ async def send_magic_link_email(email, unique_id, host):
|
||||||
|
|
||||||
template = env.get_template("magic_link.html")
|
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(
|
resend.Emails.send(
|
||||||
{
|
{
|
||||||
"sender": os.environ.get("RESEND_EMAIL", "noreply@khoj.dev"),
|
"sender": os.environ.get("RESEND_EMAIL", "noreply@khoj.dev"),
|
||||||
"to": email,
|
"to": email,
|
||||||
"subject": "Your Sign-In Link for Khoj 🚀",
|
"subject": f"{unique_id} - Sign in to Khoj 🚀",
|
||||||
"html": html_content,
|
"html": html_content,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue