Use encoded email, otp in login URL in email & web app sign-in flow

Previously emails with url special characters would not get
successfully identified for login. Account creation was fine due to
email being in POST request body. But login with such emails did not
work due to query params not being escaped before being sent to server

This change escapes both the code and email in login URL sent to
server. So login with emails containing special characters like
`email+khoj@gmail.com' works. It fixes both the URL web app sent by
web app directly and the magic link sent to users to their email

This change also fixes accessibility issue of having a DialogTitle in
DialogContent for screen readers.

Resolves #1090
This commit is contained in:
Debanjum
2025-01-19 13:05:15 +07:00
parent 51f3af11b5
commit 2d4633d298
3 changed files with 12 additions and 5 deletions

View File

@@ -2,7 +2,7 @@
import styles from "./loginPrompt.module.css"; import styles from "./loginPrompt.module.css";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import Autoplay from "embla-carousel-autoplay"; import Autoplay from "embla-carousel-autoplay";
import { import {
@@ -27,6 +27,7 @@ import {
} 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"; import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
export interface LoginPromptProps { export interface LoginPromptProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@@ -181,6 +182,9 @@ export default function LoginPrompt(props: LoginPromptProps) {
<DialogContent <DialogContent
className={`flex flex-col gap-4 ${!useEmailSignIn ? "p-0 pb-4 m-0 max-w-xl" : "w-fit"}`} className={`flex flex-col gap-4 ${!useEmailSignIn ? "p-0 pb-4 m-0 max-w-xl" : "w-fit"}`}
> >
<VisuallyHidden.Root>
<DialogTitle>Login Dialog</DialogTitle>
</VisuallyHidden.Root>
<div> <div>
{useEmailSignIn ? ( {useEmailSignIn ? (
<EmailSignInContext <EmailSignInContext
@@ -232,7 +236,7 @@ function EmailSignInContext({
const [numFailures, setNumFailures] = useState(0); const [numFailures, setNumFailures] = useState(0);
function checkOTPAndRedirect() { function checkOTPAndRedirect() {
const verifyUrl = `/auth/magic?code=${otp}&email=${email}`; const verifyUrl = `/auth/magic?code=${encodeURIComponent(otp)}&email=${encodeURIComponent(email)}`;
if (numFailures >= ALLOWED_OTP_ATTEMPTS) { if (numFailures >= ALLOWED_OTP_ATTEMPTS) {
setOTPError("Too many failed attempts. Please try again tomorrow."); setOTPError("Too many failed attempts. Please try again tomorrow.");

View File

@@ -1,6 +1,7 @@
import csv import csv
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from urllib.parse import quote
from apscheduler.job import Job from apscheduler.job import Job
from django.contrib import admin, messages from django.contrib import admin, messages
@@ -154,8 +155,9 @@ class KhojUserAdmin(UserAdmin, unfold_admin.ModelAdmin):
for user in queryset: for user in queryset:
if user.email: if user.email:
host = request.get_host() host = request.get_host()
unique_id = user.email_verification_code otp = quote(user.email_verification_code)
login_url = f"{host}/auth/magic?code={unique_id}&email={user.email}" encoded_email = quote(user.email)
login_url = f"{host}/auth/magic?code={otp}&email={encoded_email}"
messages.info(request, f"Email login URL for {user.email}: {login_url}") messages.info(request, f"Email login URL for {user.email}: {login_url}")
get_email_login_url.short_description = "Get email login URL" # type: ignore get_email_login_url.short_description = "Get email login URL" # type: ignore

View File

@@ -1,5 +1,6 @@
import logging import logging
import os import os
from urllib.parse import quote
import markdown_it import markdown_it
import resend import resend
@@ -29,7 +30,7 @@ def is_resend_enabled():
async def send_magic_link_email(email, unique_id, host): async def send_magic_link_email(email, unique_id, host):
sign_in_link = f"{host}auth/magic?code={unique_id}&email={email}" sign_in_link = f"{host}auth/magic?code={quote(unique_id)}&email={quote(email)}"
if not is_resend_enabled(): if not is_resend_enabled():
logger.debug(f"Email sending disabled. Share this sign-in link with the user: {sign_in_link}") logger.debug(f"Email sending disabled. Share this sign-in link with the user: {sign_in_link}")