Add a new sign in modal that is triggered from the login prompt screen, rather than redirecting user to another screen to sign in

This commit is contained in:
sabaimran 2024-11-23 11:55:34 -08:00
parent 7f5bf35806
commit eb1b21baaa
4 changed files with 188 additions and 42 deletions

View file

@ -8,40 +8,159 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { ArrowLeft, GoogleCardboardLogo, GoogleLogo, Spinner } from "@phosphor-icons/react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react";
import useSWR from "swr";
export interface LoginPromptProps { export interface LoginPromptProps {
loginRedirectMessage: string; loginRedirectMessage: string;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
const fetcher = (url: string) => fetch(url).then((res) => res.json());
interface Provider {
client_id: string;
redirect_uri: string;
}
interface CredentialsData {
[provider: string]: Provider;
}
export default function LoginPrompt(props: LoginPromptProps) { export default function LoginPrompt(props: LoginPromptProps) {
const { data, error, isLoading } = useSWR<CredentialsData>("/auth/oauth/metadata", fetcher);
const [useEmailSignIn, setUseEmailSignIn] = useState(false);
const [email, setEmail] = useState("");
const [checkEmail, setCheckEmail] = useState(false);
const handleGoogleSignIn = () => {
if (!data?.google?.client_id || !data?.google?.redirect_uri) return;
// Create full redirect URL using current origin
const fullRedirectUri = `${window.location.origin}${data.google.redirect_uri}`;
const params = new URLSearchParams({
client_id: data.google.client_id,
redirect_uri: fullRedirectUri,
response_type: "code",
scope: "email profile openid",
state: window.location.pathname,
access_type: "offline",
prompt: "consent select_account",
include_granted_scopes: "true",
});
window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
};
function handleMagicLinkSignIn() {
fetch("/auth/magic", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: email }),
})
.then((res) => {
if (res.ok) {
setCheckEmail(true);
return res.json();
} else {
throw new Error("Failed to send magic link");
}
})
.then((data) => {
console.log(data);
})
.catch((err) => {
console.error(err);
});
}
return ( return (
<AlertDialog open={true} onOpenChange={props.onOpenChange}> <Dialog open={true} onOpenChange={props.onOpenChange}>
<AlertDialogContent> <DialogContent className="flex flex-row gap-4 max-w-3xl">
<AlertDialogHeader> <div>
<AlertDialogTitle>Sign in to Khoj to continue</AlertDialogTitle> <DialogHeader>
</AlertDialogHeader> <DialogTitle>Sign in to Khoj to continue</DialogTitle>
<AlertDialogDescription> </DialogHeader>
{props.loginRedirectMessage}. By logging in, you agree to our{" "} <DialogDescription className="py-4">
<Link href="https://khoj.dev/terms-of-service">Terms of Service.</Link> {props.loginRedirectMessage}.
</AlertDialogDescription> </DialogDescription>
<AlertDialogFooter> {useEmailSignIn && (
<AlertDialogCancel>Dismiss</AlertDialogCancel> <div className="flex flex-col gap-4 py-4">
<AlertDialogAction <Button
className="bg-slate-400 hover:bg-slate-500" variant="ghost"
onClick={() => { className="w-fit p-0 m-0 flex gap-2 items-center justify-center text-sm"
window.location.href = `/login?next=${encodeURIComponent(window.location.pathname)}`; onClick={() => {
}} setUseEmailSignIn(false);
> }}
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}> >
{" "} <ArrowLeft className="h-6 w-6" />
{/* Redirect to login page */} </Button>
Login <Input
</Link> placeholder="Email"
</AlertDialogAction> value={email}
</AlertDialogFooter> onChange={(e) => setEmail(e.target.value)}
</AlertDialogContent> />
</AlertDialog> <Button
variant="default"
onClick={handleMagicLinkSignIn}
disabled={isLoading || checkEmail}
>
{checkEmail ? "Check your email" : "Send magic link"}
</Button>
</div>
)}
{!useEmailSignIn && (
<div className="flex flex-col gap-4 py-4">
<Button
variant="outline"
className="w-full flex gap-2 items-center justify-center"
onClick={handleGoogleSignIn}
disabled={isLoading || !data?.google}
>
{isLoading ? (
<Spinner className="h-6 w-6" />
) : (
<GoogleLogo className="h-6 w-6" />
)}
Continue with Google
</Button>
<Button
variant="default"
onClick={() => {
setUseEmailSignIn(true);
}}
>
Continue with Email
</Button>
</div>
)}
<DialogDescription>
By logging in, you agree to our{" "}
<Link href="https://khoj.dev/terms-of-service">Terms of Service.</Link>
</DialogDescription>
</div>
<div className="flex flex-col gap-4">
<img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGl0NHR5Nm0wdmFreGRoYjJmanJqYnZ1dzd3OHBqNGY3OGxiczZldyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9dg/SVZ7jzFPStbMsnjDWA/giphy.gif" />
</div>
</DialogContent>
</Dialog>
); );
} }

View file

@ -40,7 +40,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<meta {/* <meta
httpEquiv="Content-Security-Policy" httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev; content="default-src 'self' https://assets.khoj.dev;
media-src * blob:; media-src * blob:;
@ -51,7 +51,7 @@ export default function RootLayout({
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com; font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
child-src 'none'; child-src 'none';
object-src 'none';" object-src 'none';"
></meta> ></meta> */}
<body className={inter.className}>{children}</body> <body className={inter.className}>{children}</body>
</html> </html>
); );

View file

@ -4174,6 +4174,7 @@ string-argv@~0.3.2:
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==

View file

@ -4,6 +4,7 @@ import logging
import os import os
from typing import Optional from typing import Optional
import requests
from fastapi import APIRouter from fastapi import APIRouter
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
from starlette.authentication import requires from starlette.authentication import requires
@ -139,26 +140,40 @@ async def delete_token(request: Request, token: str):
return await delete_khoj_token(user=request.user.object, token=token) return await delete_khoj_token(user=request.user.object, token=token)
@auth_router.post("/redirect") @auth_router.get("/redirect")
async def auth(request: Request): async def auth(request: Request):
form = await request.form()
next_url = get_next_url(request) next_url = get_next_url(request)
for q in request.query_params: for q in request.query_params:
if q in ["code", "state", "scope", "authuser", "prompt", "session_state", "access_type"]:
continue
if not q == "next": if not q == "next":
next_url += f"&{q}={request.query_params[q]}" next_url += f"&{q}={request.query_params[q]}"
credential = form.get("credential") code = request.query_params.get("code")
csrf_token_cookie = request.cookies.get("g_csrf_token") # 1. Construct the full redirect URI including domain
if not csrf_token_cookie: base_url = str(request.base_url).rstrip("/")
logger.info("Missing CSRF token. Redirecting user to login page") redirect_uri = f"{base_url}{request.app.url_path_for('auth')}"
return RedirectResponse(url=next_url)
csrf_token_body = form.get("g_csrf_token") verified_data = requests.post(
if not csrf_token_body: "https://oauth2.googleapis.com/token",
logger.info("Missing CSRF token body. Redirecting user to login page") headers={"Content-Type": "application/x-www-form-urlencoded"},
return RedirectResponse(url=next_url) data={
if csrf_token_cookie != csrf_token_body: "code": code,
return Response("Invalid CSRF token", status_code=400) "client_id": os.environ["GOOGLE_CLIENT_ID"],
"client_secret": os.environ["GOOGLE_CLIENT_SECRET"],
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
},
)
verified_data.raise_for_status()
credential = verified_data.json().get("id_token")
if not credential:
logger.error("Missing id_token in OAuth response")
return RedirectResponse(url="/login?error=invalid_token", status_code=HTTP_302_FOUND)
try: try:
idinfo = id_token.verify_oauth2_token(credential, google_requests.Request(), os.environ["GOOGLE_CLIENT_ID"]) idinfo = id_token.verify_oauth2_token(credential, google_requests.Request(), os.environ["GOOGLE_CLIENT_ID"])
@ -178,7 +193,6 @@ async def auth(request: Request):
metadata={"server_id": str(khoj_user.uuid)}, metadata={"server_id": str(khoj_user.uuid)},
) )
logger.log(logging.INFO, f"🥳 New User Created: {khoj_user.uuid}") logger.log(logging.INFO, f"🥳 New User Created: {khoj_user.uuid}")
return RedirectResponse(url=next_url, status_code=HTTP_302_FOUND)
return RedirectResponse(url=next_url, status_code=HTTP_302_FOUND) return RedirectResponse(url=next_url, status_code=HTTP_302_FOUND)
@ -187,3 +201,15 @@ async def auth(request: Request):
async def logout(request: Request): async def logout(request: Request):
request.session.pop("user", None) request.session.pop("user", None)
return RedirectResponse(url="/") return RedirectResponse(url="/")
@auth_router.get("/oauth/metadata")
async def oauth_metadata(request: Request):
redirect_uri = str(request.app.url_path_for("auth"))
return {
"google": {
"client_id": os.environ.get("GOOGLE_CLIENT_ID"),
"redirect_uri": f"{redirect_uri}",
}
}