Skip to content

Commit 0243525

Browse files
committed
add Turnstile captcha to login flow
1 parent 035fcf6 commit 0243525

File tree

2 files changed

+84
-31
lines changed

2 files changed

+84
-31
lines changed

apps/dashboard/src/app/login/LoginPage.tsx

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { redirectToCheckout } from "@/actions/billing";
44
import { getRawAccountAction } from "@/actions/getAccount";
55
import { ToggleThemeButton } from "@/components/color-mode-toggle";
66
import { Spinner } from "@/components/ui/Spinner/Spinner";
7+
import { TURNSTILE_SITE_KEY } from "@/constants/env";
78
import { useThirdwebClient } from "@/constants/thirdweb.client";
89
import { useDashboardRouter } from "@/lib/DashboardRouter";
910
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
11+
import { Turnstile } from "@marsidev/react-turnstile";
1012
import { useTheme } from "next-themes";
1113
import Link from "next/link";
1214
import { Suspense, lazy, useEffect, useState } from "react";
@@ -216,36 +218,49 @@ function CustomConnectEmbed(props: {
216218
}) {
217219
const { theme } = useTheme();
218220
const client = useThirdwebClient();
221+
const [turnstileToken, setTurnstileToken] = useState("");
219222

220223
return (
221-
<ConnectEmbed
222-
auth={{
223-
getLoginPayload,
224-
doLogin: async (params) => {
225-
try {
226-
await doLogin(params);
227-
props.onLogin();
228-
} catch (e) {
229-
console.error("Failed to login", e);
230-
throw e;
231-
}
232-
},
233-
doLogout,
234-
isLoggedIn: async (x) => {
235-
const isLoggedInResult = await isLoggedIn(x);
236-
if (isLoggedInResult) {
237-
props.onLogin();
238-
}
239-
return isLoggedInResult;
240-
},
241-
}}
242-
wallets={wallets}
243-
client={client}
244-
modalSize="wide"
245-
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
246-
className="shadow-lg"
247-
privacyPolicyUrl="/privacy-policy"
248-
termsOfServiceUrl="/terms"
249-
/>
224+
<div className="flex flex-col items-center gap-4">
225+
<ConnectEmbed
226+
auth={{
227+
getLoginPayload,
228+
doLogin: async (params) => {
229+
try {
230+
await doLogin(params, turnstileToken);
231+
props.onLogin();
232+
} catch (e) {
233+
console.error("Failed to login", e);
234+
throw e;
235+
}
236+
},
237+
doLogout,
238+
isLoggedIn: async (x) => {
239+
const isLoggedInResult = await isLoggedIn(x);
240+
if (isLoggedInResult) {
241+
props.onLogin();
242+
}
243+
return isLoggedInResult;
244+
},
245+
}}
246+
wallets={wallets}
247+
client={client}
248+
modalSize="wide"
249+
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
250+
className="shadow-lg"
251+
privacyPolicyUrl="/privacy-policy"
252+
termsOfServiceUrl="/terms"
253+
/>
254+
<Turnstile
255+
options={{
256+
// only show if interaction is required
257+
appearance: "interaction-only",
258+
// match the theme of the rest of the app
259+
theme: theme === "light" ? "light" : "dark",
260+
}}
261+
siteKey={TURNSTILE_SITE_KEY}
262+
onSuccess={(token) => setTurnstileToken(token)}
263+
/>
264+
</div>
250265
);
251266
}

apps/dashboard/src/app/login/auth-actions.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import "server-only";
33

44
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
55
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "@/constants/env";
6-
import { cookies } from "next/headers";
6+
import { ipAddress } from "@vercel/functions";
7+
import { cookies, headers } from "next/headers";
78
import { getAddress } from "thirdweb";
89
import type {
910
GenerateLoginPayloadParams,
@@ -36,11 +37,48 @@ export async function getLoginPayload(
3637
return (await res.json()).data.payload;
3738
}
3839

39-
export async function doLogin(payload: VerifyLoginPayloadParams) {
40+
export async function doLogin(
41+
payload: VerifyLoginPayloadParams,
42+
turnstileToken: string,
43+
) {
4044
if (!THIRDWEB_API_SECRET) {
4145
throw new Error("API_SERVER_SECRET is not set");
4246
}
4347

48+
if (!turnstileToken) {
49+
throw new Error("Missing Turnstile token.");
50+
}
51+
52+
// get the request headers
53+
const requestHeaders = await headers();
54+
// CF header, fallback to req.ip, then X-Forwarded-For
55+
const ip =
56+
requestHeaders.get("CF-Connecting-IP") ||
57+
ipAddress(requestHeaders) ||
58+
requestHeaders.get("X-Forwarded-For");
59+
60+
// https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
61+
// Validate the token by calling the "/siteverify" API endpoint.
62+
const result = await fetch(
63+
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
64+
{
65+
body: JSON.stringify({
66+
secret: process.env.TURNSTILE_SECRET_KEY,
67+
response: turnstileToken,
68+
remoteip: ip,
69+
}),
70+
method: "POST",
71+
headers: {
72+
"Content-Type": "application/json",
73+
},
74+
},
75+
);
76+
77+
const outcome = await result.json();
78+
if (!outcome.success) {
79+
throw new Error("Could not validate captcha.");
80+
}
81+
4482
const cookieStore = await cookies();
4583
const utmCookies = cookieStore
4684
.getAll()

0 commit comments

Comments
 (0)