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,
|
||||
} 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">
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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_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:
|
||||
|
|
|
@ -1,17 +1,53 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<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>
|
||||
<html lang="en">
|
||||
|
||||
<p style="color: #333; font-size: large; margin-top: 20px; padding: 0; line-height: 1.5;">- The Khoj Team</p>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to Khoj</title>
|
||||
</head>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue