Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions apps/web/app/(with-contexts)/(with-layout)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { auth } from "@/auth";
import { SessionProvider } from "next-auth/react";
import HomepageLayout from "./home-page-layout";
import { headers } from "next/headers";
import { getFullSiteSetup } from "@ui-lib/utils";
Expand All @@ -11,18 +9,11 @@ export default async function Layout({
children: React.ReactNode;
}) {
const address = await getAddressFromHeaders(headers);
const [siteInfo, session] = await Promise.all([
getFullSiteSetup(address),
auth(),
]);
const siteInfo = await getFullSiteSetup(address);

if (!siteInfo) {
return null;
}

return (
<SessionProvider session={session}>
<HomepageLayout siteInfo={siteInfo}>{children}</HomepageLayout>
</SessionProvider>
);
return <HomepageLayout siteInfo={siteInfo}>{children}</HomepageLayout>;
}
251 changes: 121 additions & 130 deletions apps/web/app/(with-contexts)/(with-layout)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,15 @@
Input,
Section,
Text1,
Text2,
Link as PageLink,
} from "@courselit/page-primitives";
import { useContext, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { FormEvent } from "react";
import { signIn } from "next-auth/react";
import { Form, useToast } from "@courselit/components-library";
import {
BTN_LOGIN,
BTN_LOGIN_GET_CODE,
BTN_LOGIN_CODE_INTIMATION,
LOGIN_CODE_INTIMATION_MESSAGE,
LOGIN_NO_CODE,
BTN_LOGIN_NO_CODE,
LOGIN_FORM_LABEL,
Expand All @@ -37,6 +35,7 @@
import { Profile } from "@courselit/common-models";
import { getUserProfile } from "../../helpers";
import { ADMIN_PERMISSIONS } from "@ui-config/constants";
import { authClient } from "@/lib/auth-client";

export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
const { theme } = useContext(ThemeContext);
Expand All @@ -49,112 +48,82 @@
const serverConfig = useContext(ServerConfigContext);
const { executeRecaptcha } = useRecaptcha();
const address = useContext(AddressContext);
const codeInputRef = useRef<HTMLInputElement>(null);

const requestCode = async function (e: FormEvent) {
e.preventDefault();
setLoading(true);
setError("");
const validateRecaptcha = useCallback(async (): Promise<boolean> => {
if (!serverConfig.recaptchaSiteKey) {
return true;
}

if (serverConfig.recaptchaSiteKey) {
if (!executeRecaptcha) {
toast({
title: TOAST_TITLE_ERROR,
description:
"reCAPTCHA service not available. Please try again later.",
variant: "destructive",
});
setLoading(false);
return;
}
if (!executeRecaptcha) {
toast({
title: TOAST_TITLE_ERROR,
description:
"reCAPTCHA service not available. Please try again later.",
variant: "destructive",
});
setLoading(false);
return false;
}

const recaptchaToken = await executeRecaptcha("login_code_request");
if (!recaptchaToken) {
toast({
title: TOAST_TITLE_ERROR,
description:
"reCAPTCHA validation failed. Please try again.",
variant: "destructive",
});
setLoading(false);
return;
}
try {
const recaptchaVerificationResponse = await fetch(
"/api/recaptcha",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: recaptchaToken }),
},
);
const recaptchaToken = await executeRecaptcha("login_code_request");
if (!recaptchaToken) {
toast({
title: TOAST_TITLE_ERROR,
description: "reCAPTCHA validation failed. Please try again.",
variant: "destructive",
});
setLoading(false);
return false;
}
try {
const recaptchaVerificationResponse = await fetch(
"/api/recaptcha",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: recaptchaToken }),
},
);

const recaptchaData =
await recaptchaVerificationResponse.json();
const recaptchaData = await recaptchaVerificationResponse.json();

if (
!recaptchaVerificationResponse.ok ||
!recaptchaData.success ||
(recaptchaData.score && recaptchaData.score < 0.5)
) {
toast({
title: TOAST_TITLE_ERROR,
description: `reCAPTCHA verification failed. ${recaptchaData.score ? `Score: ${recaptchaData.score.toFixed(2)}.` : ""} Please try again.`,
variant: "destructive",
});
setLoading(false);
return;
}
} catch (err) {
console.error("Error during reCAPTCHA verification:", err);
if (
!recaptchaVerificationResponse.ok ||
!recaptchaData.success ||
(recaptchaData.score && recaptchaData.score < 0.5)
) {
toast({
title: TOAST_TITLE_ERROR,
description:
"reCAPTCHA verification failed. Please try again.",
description: `reCAPTCHA verification failed. ${recaptchaData.score ? `Score: ${recaptchaData.score.toFixed(2)}.` : ""} Please try again.`,
variant: "destructive",
});
setLoading(false);
return;
}
}

try {
const url = `/api/auth/code/generate?email=${encodeURIComponent(
email,
)}`;
const response = await fetch(url);
const resp = await response.json();
if (response.ok) {
setShowCode(true);
} else {
toast({
title: TOAST_TITLE_ERROR,
description: resp.error || "Failed to request code.",
variant: "destructive",
});
return false;
}
} catch (err) {
console.error("Error during requestCode:", err);
toast({
title: TOAST_TITLE_ERROR,
description: "An unexpected error occurred. Please try again.",
description: "reCAPTCHA verification failed. Please try again.",
variant: "destructive",
});
} finally {
setLoading(false);
return false;
}
};

return true;
}, []);

const signInUser = async function (e: FormEvent) {
e.preventDefault();
try {
setLoading(true);
const response = await signIn("credentials", {
email,
code,
redirect: false,
const { data, error } = await authClient.signIn.emailOtp({
Comment thread Fixed
email: email.trim().toLowerCase(),
otp: code,
});
if (response?.error) {
setError(`Can't sign you in at this time`);
if (error) {
setError(`Can't sign you in at this time: ${error.message}`);
} else {
window.location.href =
redirectTo ||
Expand All @@ -178,6 +147,38 @@
}
};

useEffect(() => {
if (showCode) {
codeInputRef.current?.focus();
}
}, [showCode]);

const requestCode = async function (e: FormEvent) {
e.preventDefault();
setLoading(true);
setError("");

if (!validateRecaptcha()) {
return;
}

try {
const { data, error } =
Comment thread Fixed
await authClient.emailOtp.sendVerificationOtp({
email: email.trim().toLowerCase(),
type: "sign-in",
});

if (error) {
setError(error.message as any);
} else {
setShowCode(true);
}
} finally {
setLoading(false);
}
};

return (
<Section theme={theme.theme}>
<div className="flex flex-col gap-4 min-h-[80vh]">
Expand All @@ -204,7 +205,7 @@
</Text1>
<Form
onSubmit={requestCode}
className="flex flex-col gap-4"
className="flex flex-col gap-4 w-full lg:w-[360px] mx-auto"
>
<Input
type="email"
Expand All @@ -216,7 +217,12 @@
}
theme={theme.theme}
/>

<Button
theme={theme.theme}
disabled={loading}
>
{loading ? LOADING : BTN_LOGIN_GET_CODE}
</Button>
<Caption
theme={theme.theme}
className="text-center"
Expand All @@ -228,35 +234,17 @@
</span>
</Link>
</Caption>
<div className="flex justify-center">
{/* <FormSubmit
text={
loading
? LOADING
: BTN_LOGIN_GET_CODE
}
disabled={loading}
/> */}
<Button
theme={theme.theme}
disabled={loading}
>
{loading
? LOADING
: BTN_LOGIN_GET_CODE}
</Button>
</div>
</Form>
</div>
)}
{showCode && (
<div>
<Text1 theme={theme.theme} className="mb-4">
{BTN_LOGIN_CODE_INTIMATION}{" "}
{LOGIN_CODE_INTIMATION_MESSAGE}{" "}
<strong>{email}</strong>
</Text1>
<Form
className="flex flex-col gap-4 mb-4"
className="flex flex-col gap-4 mb-4 w-full lg:w-[360px] mx-auto"
onSubmit={signInUser}
>
<Input
Expand All @@ -268,34 +256,37 @@
setCode(e.target.value)
}
theme={theme.theme}
ref={codeInputRef}
/>
<div className="flex justify-center">
<Button
theme={theme.theme}
disabled={loading}
>
{loading ? LOADING : BTN_LOGIN}
</Button>
</div>
<Button
theme={theme.theme}
disabled={loading}
>
{loading ? LOADING : BTN_LOGIN}
</Button>
{/* </div> */}
</Form>
<div className="flex justify-center items-center gap-1 text-sm">
<Text2
<Caption
theme={theme.theme}
className="text-slate-500"
className="text-center flex items-center gap-1"
>
{LOGIN_NO_CODE}
</Text2>
<button
onClick={requestCode}
className="underline"
disabled={loading}
>
<PageLink theme={theme.theme}>
{loading
? LOADING
: BTN_LOGIN_NO_CODE}
</PageLink>
</button>
<button
onClick={requestCode}
className="underline"
disabled={loading}
>
<PageLink
theme={theme.theme}
className="text-xs"
>
{loading
? LOADING
: BTN_LOGIN_NO_CODE}
</PageLink>
</button>
</Caption>
</div>
</div>
)}
Expand Down
7 changes: 6 additions & 1 deletion apps/web/app/(with-contexts)/(with-layout)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import LoginForm from "./login-form";
import { headers } from "next/headers";

export default async function LoginPage({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const session = await auth();
const headersList = await headers();
const session = await auth.api.getSession({
headers: headersList,
});

const redirectTo = (await searchParams).redirect as string | undefined;

if (session) {
Expand Down
Loading
Loading