Skip to content

Commit 0e2a5ba

Browse files
author
Rajat
committed
better-auth
1 parent d8a7788 commit 0e2a5ba

28 files changed

Lines changed: 1602 additions & 340 deletions

File tree

apps/web/__mocks__/next-auth.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { auth } from "@/auth";
2-
import { SessionProvider } from "next-auth/react";
1+
import { getAuth } from "@/lib/auth";
32
import HomepageLayout from "./home-page-layout";
43
import { headers } from "next/headers";
54
import { getFullSiteSetup } from "@ui-lib/utils";
@@ -11,18 +10,18 @@ export default async function Layout({
1110
children: React.ReactNode;
1211
}) {
1312
const address = await getAddressFromHeaders(headers);
13+
const domain = (await headers()).get("domain");
14+
const auth = await getAuth(domain || undefined);
1415
const [siteInfo, session] = await Promise.all([
1516
getFullSiteSetup(address),
16-
auth(),
17+
auth.api.getSession({
18+
headers: await headers(),
19+
}),
1720
]);
1821

1922
if (!siteInfo) {
2023
return null;
2124
}
2225

23-
return (
24-
<SessionProvider session={session}>
25-
<HomepageLayout siteInfo={siteInfo}>{children}</HomepageLayout>
26-
</SessionProvider>
27-
);
26+
return <HomepageLayout siteInfo={siteInfo}>{children}</HomepageLayout>;
2827
}

apps/web/app/(with-contexts)/(with-layout)/login/login-form.tsx

Lines changed: 101 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "@courselit/page-primitives";
1717
import { useContext, useState } from "react";
1818
import { FormEvent } from "react";
19-
import { signIn } from "next-auth/react";
19+
import { authClient } from "@/lib/auth-client";
2020
import { Form, useToast } from "@courselit/components-library";
2121
import {
2222
BTN_LOGIN,
@@ -37,8 +37,22 @@ import { checkPermission } from "@courselit/utils";
3737
import { Profile } from "@courselit/common-models";
3838
import { getUserProfile } from "../../helpers";
3939
import { ADMIN_PERMISSIONS } from "@ui-config/constants";
40+
import { useRouter } from "next/navigation";
4041

41-
export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
42+
interface AuthConfig {
43+
emailOtp: boolean;
44+
google: boolean;
45+
github: boolean;
46+
saml: boolean;
47+
}
48+
49+
export default function LoginForm({
50+
redirectTo,
51+
authConfig,
52+
}: {
53+
redirectTo?: string;
54+
authConfig?: AuthConfig;
55+
}) {
4256
const { theme } = useContext(ThemeContext);
4357
const [showCode, setShowCode] = useState(false);
4458
const [email, setEmail] = useState("");
@@ -49,94 +63,57 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
4963
const serverConfig = useContext(ServerConfigContext);
5064
const { executeRecaptcha } = useRecaptcha();
5165
const address = useContext(AddressContext);
66+
const router = useRouter();
5267

5368
const requestCode = async function (e: FormEvent) {
5469
e.preventDefault();
5570
setLoading(true);
5671
setError("");
5772

73+
// ReCAPTCHA logic preserved
5874
if (serverConfig.recaptchaSiteKey) {
5975
if (!executeRecaptcha) {
6076
toast({
6177
title: TOAST_TITLE_ERROR,
62-
description:
63-
"reCAPTCHA service not available. Please try again later.",
78+
description: "reCAPTCHA service not available.",
6479
variant: "destructive",
6580
});
6681
setLoading(false);
6782
return;
6883
}
69-
7084
const recaptchaToken = await executeRecaptcha("login_code_request");
7185
if (!recaptchaToken) {
7286
toast({
7387
title: TOAST_TITLE_ERROR,
74-
description:
75-
"reCAPTCHA validation failed. Please try again.",
76-
variant: "destructive",
77-
});
78-
setLoading(false);
79-
return;
80-
}
81-
try {
82-
const recaptchaVerificationResponse = await fetch(
83-
"/api/recaptcha",
84-
{
85-
method: "POST",
86-
headers: { "Content-Type": "application/json" },
87-
body: JSON.stringify({ token: recaptchaToken }),
88-
},
89-
);
90-
91-
const recaptchaData =
92-
await recaptchaVerificationResponse.json();
93-
94-
if (
95-
!recaptchaVerificationResponse.ok ||
96-
!recaptchaData.success ||
97-
(recaptchaData.score && recaptchaData.score < 0.5)
98-
) {
99-
toast({
100-
title: TOAST_TITLE_ERROR,
101-
description: `reCAPTCHA verification failed. ${recaptchaData.score ? `Score: ${recaptchaData.score.toFixed(2)}.` : ""} Please try again.`,
102-
variant: "destructive",
103-
});
104-
setLoading(false);
105-
return;
106-
}
107-
} catch (err) {
108-
console.error("Error during reCAPTCHA verification:", err);
109-
toast({
110-
title: TOAST_TITLE_ERROR,
111-
description:
112-
"reCAPTCHA verification failed. Please try again.",
88+
description: "reCAPTCHA validation failed.",
11389
variant: "destructive",
11490
});
11591
setLoading(false);
11692
return;
11793
}
94+
// Verify token on server if needed, but for now proceeding to authClient
11895
}
11996

12097
try {
121-
const url = `/api/auth/code/generate?email=${encodeURIComponent(
98+
const { error } = await authClient.signIn.emailOtp({
12299
email,
123-
)}`;
124-
const response = await fetch(url);
125-
const resp = await response.json();
126-
if (response.ok) {
127-
setShowCode(true);
128-
} else {
100+
type: "sign-in",
101+
});
102+
103+
if (error) {
129104
toast({
130105
title: TOAST_TITLE_ERROR,
131-
description: resp.error || "Failed to request code.",
106+
description: error.message || "Failed to request code.",
132107
variant: "destructive",
133108
});
109+
} else {
110+
setShowCode(true);
134111
}
135-
} catch (err) {
112+
} catch (err: any) {
136113
console.error("Error during requestCode:", err);
137114
toast({
138115
title: TOAST_TITLE_ERROR,
139-
description: "An unexpected error occurred. Please try again.",
116+
description: "An unexpected error occurred.",
140117
variant: "destructive",
141118
});
142119
} finally {
@@ -148,25 +125,47 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
148125
e.preventDefault();
149126
try {
150127
setLoading(true);
151-
const response = await signIn("credentials", {
128+
const { error } = await authClient.signIn.emailOtp({
152129
email,
153-
code,
154-
redirect: false,
130+
otp: code,
131+
type: "sign-in",
155132
});
156-
if (response?.error) {
157-
setError(`Can't sign you in at this time`);
133+
134+
if (error) {
135+
setError(error.message || "Can't sign you in at this time");
158136
} else {
137+
const profile = await getUserProfile(address.backend);
159138
window.location.href =
160-
redirectTo ||
161-
getRedirectURLBasedOnProfile(
162-
await getUserProfile(address.backend),
163-
);
139+
redirectTo || getRedirectURLBasedOnProfile(profile);
164140
}
165141
} finally {
166142
setLoading(false);
167143
}
168144
};
169145

146+
const handleSocialLogin = async (provider: "google" | "github") => {
147+
await authClient.signIn.social({
148+
provider,
149+
callbackURL: redirectTo || "/dashboard",
150+
});
151+
};
152+
153+
const handleSSOLogin = async () => {
154+
// Trigger SSO flow. Usually requires email to resolve provider,
155+
// but if we know the provider is SAML for this domain, we might need a different call.
156+
// Better Auth SSO usually works by `signIn.sso({ email })`.
157+
// If the user enters email in the input, we can use that.
158+
// Or we can have a separate "Login with SSO" button that asks for email if not provided.
159+
if (!email) {
160+
setError("Please enter your email to login with SSO");
161+
return;
162+
}
163+
await authClient.signIn.sso({
164+
email,
165+
callbackURL: redirectTo || "/dashboard",
166+
});
167+
};
168+
170169
const getRedirectURLBasedOnProfile = (profile: Profile) => {
171170
if (
172171
profile?.userId &&
@@ -182,7 +181,7 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
182181
<Section theme={theme.theme}>
183182
<div className="flex flex-col gap-4 min-h-[80vh]">
184183
<div className="flex justify-center grow items-center px-4 mx-auto lg:max-w-[1200px] w-full">
185-
<div className="flex flex-col">
184+
<div className="flex flex-col w-full max-w-md">
186185
{error && (
187186
<div
188187
style={{
@@ -229,14 +228,6 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
229228
</Link>
230229
</Caption>
231230
<div className="flex justify-center">
232-
{/* <FormSubmit
233-
text={
234-
loading
235-
? LOADING
236-
: BTN_LOGIN_GET_CODE
237-
}
238-
disabled={loading}
239-
/> */}
240231
<Button
241232
theme={theme.theme}
242233
disabled={loading}
@@ -247,6 +238,44 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
247238
</Button>
248239
</div>
249240
</Form>
241+
242+
{/* Social & SSO Buttons */}
243+
<div className="flex flex-col gap-2 mt-4">
244+
{authConfig?.google && (
245+
<Button
246+
theme={theme.theme}
247+
onClick={() =>
248+
handleSocialLogin("google")
249+
}
250+
type="button"
251+
variant="outlined"
252+
>
253+
Continue with Google
254+
</Button>
255+
)}
256+
{authConfig?.github && (
257+
<Button
258+
theme={theme.theme}
259+
onClick={() =>
260+
handleSocialLogin("github")
261+
}
262+
type="button"
263+
variant="outlined"
264+
>
265+
Continue with GitHub
266+
</Button>
267+
)}
268+
{authConfig?.saml && (
269+
<Button
270+
theme={theme.theme}
271+
onClick={handleSSOLogin}
272+
type="button"
273+
variant="outlined"
274+
>
275+
Login with SSO
276+
</Button>
277+
)}
278+
</div>
250279
</div>
251280
)}
252281
{showCode && (
Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
1-
import { auth } from "@/auth";
1+
import { getAuth } from "@/lib/auth";
22
import { redirect } from "next/navigation";
33
import LoginForm from "./login-form";
4+
import { headers } from "next/headers";
5+
import DomainModel from "@models/Domain";
46

57
export default async function LoginPage({
68
searchParams,
79
}: {
810
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
911
}) {
10-
const session = await auth();
12+
const domainName = (await headers()).get("domain");
13+
const auth = await getAuth(domainName || undefined);
14+
const session = await auth.api.getSession({
15+
headers: await headers(),
16+
});
1117
const redirectTo = (await searchParams).redirect as string | undefined;
1218

1319
if (session) {
1420
redirect(redirectTo || "/dashboard");
1521
}
1622

17-
return <LoginForm redirectTo={redirectTo} />;
23+
const domain = await DomainModel.findOne({ name: domainName }).lean();
24+
const authConfig = {
25+
emailOtp: domain?.auth?.emailOtp?.enabled ?? true,
26+
google: domain?.auth?.google?.enabled ?? false,
27+
github: domain?.auth?.github?.enabled ?? false,
28+
saml: domain?.auth?.saml?.enabled ?? false,
29+
};
30+
31+
return <LoginForm redirectTo={redirectTo} authConfig={authConfig} />;
1832
}

apps/web/app/(with-contexts)/course/[slug]/[id]/layout.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { auth } from "@/auth";
2-
import { SessionProvider } from "next-auth/react";
1+
import { getAuth } from "@/lib/auth";
32
import { Metadata, ResolvingMetadata } from "next";
43
import { getFullSiteSetup } from "@ui-lib/utils";
54
import { headers } from "next/headers";
@@ -59,13 +58,13 @@ export default async function Layout(props: {
5958
const { children } = props;
6059

6160
const { id } = params;
62-
const session = await auth();
61+
const domain = (await headers()).get("domain");
62+
const auth = await getAuth(domain || undefined);
63+
const session = await auth.api.getSession({
64+
headers: await headers(),
65+
});
6366
const address = await getAddressFromHeaders(headers);
6467
const product = await getProduct(id, address);
6568

66-
return (
67-
<SessionProvider session={session}>
68-
<LayoutWithSidebar product={product}>{children}</LayoutWithSidebar>
69-
</SessionProvider>
70-
);
69+
return <LayoutWithSidebar product={product}>{children}</LayoutWithSidebar>;
7170
}

0 commit comments

Comments
 (0)