Skip to content

Commit c468b30

Browse files
author
Rajat Saxena
committed
Tested code
1 parent 3aa4d8d commit c468b30

6 files changed

Lines changed: 76 additions & 74 deletions

File tree

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

Lines changed: 59 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { ThemeContext } from "@components/contexts";
3+
import { ServerConfigContext, ThemeContext } from "@components/contexts";
44
import {
55
Button,
66
Caption,
@@ -33,7 +33,7 @@ import {
3333
import Link from "next/link";
3434
import { TriangleAlert } from "lucide-react";
3535
import { useRecaptcha } from "@/hooks/use-recaptcha";
36-
import { useRouter } from "next/navigation";
36+
import RecaptchaScriptLoader from "@/components/recaptcha-script-loader";
3737

3838
export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
3939
const { theme } = useContext(ThemeContext);
@@ -43,55 +43,77 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
4343
const [error, setError] = useState("");
4444
const [loading, setLoading] = useState(false);
4545
const { toast } = useToast();
46+
const serverConfig = useContext(ServerConfigContext);
4647
const { executeRecaptcha } = useRecaptcha();
47-
const router = useRouter();
4848

4949
const requestCode = async function (e: FormEvent) {
5050
e.preventDefault();
5151
setLoading(true);
5252
setError("");
5353

54-
if (!executeRecaptcha) {
55-
toast({
56-
title: TOAST_TITLE_ERROR,
57-
description: "reCAPTCHA service not available. Please try again later.",
58-
variant: "destructive",
59-
});
60-
setLoading(false);
61-
return;
62-
}
63-
64-
const recaptchaToken = await executeRecaptcha("login_code_request");
65-
if (!recaptchaToken) {
66-
toast({
67-
title: TOAST_TITLE_ERROR,
68-
description: "reCAPTCHA validation failed. Please try again.",
69-
variant: "destructive",
70-
});
71-
setLoading(false);
72-
return;
73-
}
54+
if (serverConfig.recaptchaSiteKey) {
55+
if (!executeRecaptcha) {
56+
toast({
57+
title: TOAST_TITLE_ERROR,
58+
description:
59+
"reCAPTCHA service not available. Please try again later.",
60+
variant: "destructive",
61+
});
62+
setLoading(false);
63+
return;
64+
}
7465

75-
try {
76-
const recaptchaVerificationResponse = await fetch("/api/recaptcha", {
77-
method: "POST",
78-
headers: { "Content-Type": "application/json" },
79-
body: JSON.stringify({ token: recaptchaToken }),
80-
});
66+
const recaptchaToken = await executeRecaptcha("login_code_request");
67+
if (!recaptchaToken) {
68+
toast({
69+
title: TOAST_TITLE_ERROR,
70+
description:
71+
"reCAPTCHA validation failed. Please try again.",
72+
variant: "destructive",
73+
});
74+
setLoading(false);
75+
return;
76+
}
77+
try {
78+
const recaptchaVerificationResponse = await fetch(
79+
"/api/recaptcha",
80+
{
81+
method: "POST",
82+
headers: { "Content-Type": "application/json" },
83+
body: JSON.stringify({ token: recaptchaToken }),
84+
},
85+
);
8186

82-
const recaptchaData = await recaptchaVerificationResponse.json();
87+
const recaptchaData =
88+
await recaptchaVerificationResponse.json();
8389

84-
if (!recaptchaVerificationResponse.ok || !recaptchaData.success || (recaptchaData.score && recaptchaData.score < 0.5)) {
90+
if (
91+
!recaptchaVerificationResponse.ok ||
92+
!recaptchaData.success ||
93+
(recaptchaData.score && recaptchaData.score < 0.5)
94+
) {
95+
toast({
96+
title: TOAST_TITLE_ERROR,
97+
description: `reCAPTCHA verification failed. ${recaptchaData.score ? `Score: ${recaptchaData.score.toFixed(2)}.` : ""} Please try again.`,
98+
variant: "destructive",
99+
});
100+
setLoading(false);
101+
return;
102+
}
103+
} catch (err) {
104+
console.error("Error during reCAPTCHA verification:", err);
85105
toast({
86106
title: TOAST_TITLE_ERROR,
87-
description: `reCAPTCHA verification failed. ${recaptchaData.score ? `Score: ${recaptchaData.score.toFixed(2)}.` : ''} Please try again.`,
107+
description:
108+
"reCAPTCHA verification failed. Please try again.",
88109
variant: "destructive",
89110
});
90111
setLoading(false);
91112
return;
92113
}
114+
}
93115

94-
// Proceed with code generation if reCAPTCHA is successful
116+
try {
95117
const url = `/api/auth/code/generate?email=${encodeURIComponent(
96118
email,
97119
)}`;
@@ -113,8 +135,7 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
113135
description: "An unexpected error occurred. Please try again.",
114136
variant: "destructive",
115137
});
116-
}
117-
finally {
138+
} finally {
118139
setLoading(false);
119140
}
120141
};
@@ -131,11 +152,6 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
131152
if (response?.error) {
132153
setError(`Can't sign you in at this time`);
133154
} else {
134-
// toast({
135-
// title: TOAST_TITLE_SUCCESS,
136-
// description: LOGIN_SUCCESS,
137-
// });
138-
// router.replace(redirectTo || "/dashboard/my-content");
139155
window.location.href = redirectTo || "/dashboard/my-content";
140156
}
141157
} finally {
@@ -151,7 +167,8 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
151167
{error && (
152168
<div
153169
style={{
154-
color: theme?.theme?.colors?.error,
170+
color: theme?.theme?.colors?.light
171+
?.destructive,
155172
}}
156173
className="flex items-center gap-2 mb-4"
157174
>
@@ -270,6 +287,7 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
270287
</div>
271288
</div>
272289
</div>
290+
<RecaptchaScriptLoader />
273291
</Section>
274292
);
275293
}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { Session } from "next-auth";
1616
import { Theme } from "@courselit/page-models";
1717
import { ThemeProvider as NextThemesProvider } from "@components/next-theme-provider";
1818
import { defaultState } from "@components/default-state";
19-
import RecaptchaScriptLoader from "@components/recaptcha-script-loader";
2019

2120
function LayoutContent({
2221
address,
@@ -110,7 +109,6 @@ function LayoutContent({
110109
>
111110
<Suspense fallback={null}>{children}</Suspense>
112111
</ProfileContext.Provider>
113-
<RecaptchaScriptLoader />
114112
</NextThemesProvider>
115113
</ServerConfigContext.Provider>
116114
</ThemeContext.Provider>

apps/web/app/api/recaptcha/route.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
import { NextRequest, NextResponse } from "next/server";
22

3-
/**
4-
* API route handler for verifying a Google reCAPTCHA v3 token.
5-
* Expects a POST request with a JSON body containing a `token` property.
6-
*
7-
* @param {NextRequest} request - The incoming Next.js API request object.
8-
* @returns {Promise<NextResponse>} A Next.js API response object.
9-
*/
103
export async function POST(request: NextRequest) {
114
const secretKey = process.env.RECAPTCHA_SECRET_KEY;
125
if (!secretKey) {
136
console.error("reCAPTCHA secret key not found.");
147
return NextResponse.json(
158
{ error: "Internal server error" },
16-
{ status: 500 }
9+
{ status: 500 },
1710
);
1811
}
1912

@@ -23,7 +16,7 @@ export async function POST(request: NextRequest) {
2316
} catch (error) {
2417
return NextResponse.json(
2518
{ error: "Invalid request body" },
26-
{ status: 400 }
19+
{ status: 400 },
2720
);
2821
}
2922

@@ -32,7 +25,7 @@ export async function POST(request: NextRequest) {
3225
if (!token) {
3326
return NextResponse.json(
3427
{ error: "reCAPTCHA token not found" },
35-
{ status: 400 }
28+
{ status: 400 },
3629
);
3730
}
3831

@@ -47,14 +40,14 @@ export async function POST(request: NextRequest) {
4740
"Content-Type": "application/x-www-form-urlencoded",
4841
},
4942
body: formData,
50-
}
43+
},
5144
);
5245

5346
if (!response.ok) {
5447
console.error("Failed to verify reCAPTCHA token with Google");
5548
return NextResponse.json(
5649
{ error: "Failed to verify reCAPTCHA token" },
57-
{ status: 500 }
50+
{ status: 500 },
5851
);
5952
}
6053

@@ -71,7 +64,7 @@ export async function POST(request: NextRequest) {
7164
console.error("Error verifying reCAPTCHA token:", error);
7265
return NextResponse.json(
7366
{ error: "Error verifying reCAPTCHA token" },
74-
{ status: 500 }
67+
{ status: 500 },
7568
);
7669
}
7770
}

apps/web/components/recaptcha-script-loader.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,8 @@ import { useContext } from "react";
44
import Script from "next/script";
55
import { ServerConfigContext } from "@components/contexts";
66

7-
/**
8-
* RecaptchaScriptLoader component.
9-
* This client component loads the Google reCAPTCHA v3 script if the site key is available in ServerConfigContext.
10-
*/
117
const RecaptchaScriptLoader = () => {
12-
const { config } = useContext(ServerConfigContext);
13-
const recaptchaSiteKey = config?.recaptchaSiteKey;
8+
const { recaptchaSiteKey } = useContext(ServerConfigContext);
149

1510
if (recaptchaSiteKey) {
1611
return (

apps/web/hooks/use-recaptcha.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,10 @@ export const useRecaptcha = () => {
1212
const recaptchaSiteKey = serverConfig?.recaptchaSiteKey;
1313

1414
const executeRecaptcha = useCallback(
15-
/**
16-
* Executes the reCAPTCHA challenge.
17-
*
18-
* @param {string} action - The action to perform.
19-
* @returns {Promise<string | null>} A promise that resolves with the reCAPTCHA token, or null if reCAPTCHA is not available or the site key is not set.
20-
*/
2115
async (action: string): Promise<string | null> => {
2216
if (!recaptchaSiteKey) {
2317
console.error(
24-
"reCAPTCHA site key not found in ServerConfigContext."
18+
"reCAPTCHA site key not found in ServerConfigContext.",
2519
);
2620
return null;
2721
}
@@ -36,26 +30,26 @@ export const useRecaptcha = () => {
3630
if (!recaptchaSiteKey) {
3731
// Double check, though already checked above
3832
console.error(
39-
"reCAPTCHA site key became unavailable before execution."
33+
"reCAPTCHA site key became unavailable before execution.",
4034
);
4135
resolve(null);
4236
return;
4337
}
4438
const token = await window.grecaptcha.execute(
4539
recaptchaSiteKey,
46-
{ action }
40+
{ action },
4741
);
4842
resolve(token);
4943
});
5044
});
5145
} else {
5246
console.error(
53-
"reCAPTCHA (window.grecaptcha) not available. Ensure the script is loaded."
47+
"reCAPTCHA (window.grecaptcha) not available. Ensure the script is loaded.",
5448
);
5549
return null;
5650
}
5751
},
58-
[recaptchaSiteKey] // Dependency array includes recaptchaSiteKey
52+
[recaptchaSiteKey], // Dependency array includes recaptchaSiteKey
5953
);
6054

6155
return { executeRecaptcha };

deployment/docker/docker-compose.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ services:
5757
# checking the logs for the API key.
5858
# - MEDIALIT_APIKEY=${MEDIALIT_APIKEY}
5959
# - MEDIALIT_SERVER=http://medialit
60-
60+
#
61+
# Google reCAPTCHA v3 is used to prevent abuse of the login functionality.
62+
# Uncomment the following lines to use reCAPTCHA.
63+
# - RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY}
64+
# - RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY}
6165
expose:
6266
- "${PORT:-80}"
6367

0 commit comments

Comments
 (0)