diff --git a/apps/dashboard/src/app/login/LoginPage.tsx b/apps/dashboard/src/app/login/LoginPage.tsx index f3ab1479128..4a3c1d05860 100644 --- a/apps/dashboard/src/app/login/LoginPage.tsx +++ b/apps/dashboard/src/app/login/LoginPage.tsx @@ -4,9 +4,11 @@ import { redirectToCheckout } from "@/actions/billing"; import { getRawAccountAction } from "@/actions/getAccount"; import { ToggleThemeButton } from "@/components/color-mode-toggle"; import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { TURNSTILE_SITE_KEY } from "@/constants/env"; import { useThirdwebClient } from "@/constants/thirdweb.client"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; +import { Turnstile } from "@marsidev/react-turnstile"; import { useTheme } from "next-themes"; import Link from "next/link"; import { Suspense, lazy, useEffect, useState } from "react"; @@ -216,36 +218,53 @@ function CustomConnectEmbed(props: { }) { const { theme } = useTheme(); const client = useThirdwebClient(); + const [turnstileToken, setTurnstileToken] = useState(""); return ( - { - try { - await doLogin(params); - props.onLogin(); - } catch (e) { - console.error("Failed to login", e); - throw e; - } - }, - doLogout, - isLoggedIn: async (x) => { - const isLoggedInResult = await isLoggedIn(x); - if (isLoggedInResult) { - props.onLogin(); - } - return isLoggedInResult; - }, - }} - wallets={wallets} - client={client} - modalSize="wide" - theme={getSDKTheme(theme === "light" ? "light" : "dark")} - className="shadow-lg" - privacyPolicyUrl="/privacy-policy" - termsOfServiceUrl="/terms" - /> +
+ { + try { + const result = await doLogin(params, turnstileToken); + if (result.error) { + console.error("Failed to login", result.error, result.context); + throw new Error(result.error); + } + props.onLogin(); + } catch (e) { + console.error("Failed to login", e); + throw e; + } + }, + doLogout, + isLoggedIn: async (x) => { + const isLoggedInResult = await isLoggedIn(x); + if (isLoggedInResult) { + props.onLogin(); + } + return isLoggedInResult; + }, + }} + wallets={wallets} + client={client} + modalSize="wide" + theme={getSDKTheme(theme === "light" ? "light" : "dark")} + className="shadow-lg" + privacyPolicyUrl="/privacy-policy" + termsOfServiceUrl="/terms" + /> + setTurnstileToken(token)} + /> +
); } diff --git a/apps/dashboard/src/app/login/auth-actions.ts b/apps/dashboard/src/app/login/auth-actions.ts index a83352cc5c6..18e0fd03eac 100644 --- a/apps/dashboard/src/app/login/auth-actions.ts +++ b/apps/dashboard/src/app/login/auth-actions.ts @@ -3,7 +3,8 @@ import "server-only"; import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; import { API_SERVER_URL, THIRDWEB_API_SECRET } from "@/constants/env"; -import { cookies } from "next/headers"; +import { ipAddress } from "@vercel/functions"; +import { cookies, headers } from "next/headers"; import { getAddress } from "thirdweb"; import type { GenerateLoginPayloadParams, @@ -36,11 +37,90 @@ export async function getLoginPayload( return (await res.json()).data.payload; } -export async function doLogin(payload: VerifyLoginPayloadParams) { +export async function doLogin( + payload: VerifyLoginPayloadParams, + turnstileToken: string, +) { if (!THIRDWEB_API_SECRET) { throw new Error("API_SERVER_SECRET is not set"); } + if (!turnstileToken) { + return { + error: "Missing Turnstile token.", + }; + } + + // get the request headers + const requestHeaders = await headers(); + if (!requestHeaders) { + return { + error: "Failed to get request headers. Please try again.", + }; + } + // CF header, fallback to req.ip, then X-Forwarded-For + const [ip, errors] = (() => { + let ip: string | null = null; + const errors: string[] = []; + try { + ip = requestHeaders.get("CF-Connecting-IP") || null; + } catch (err) { + console.error("failed to get IP address from CF-Connecting-IP", err); + errors.push("failed to get IP address from CF-Connecting-IP"); + } + if (!ip) { + try { + ip = ipAddress(requestHeaders) || null; + } catch (err) { + console.error( + "failed to get IP address from ipAddress() function", + err, + ); + errors.push("failed to get IP address from ipAddress() function"); + } + } + if (!ip) { + try { + ip = requestHeaders.get("X-Forwarded-For"); + } catch (err) { + console.error("failed to get IP address from X-Forwarded-For", err); + errors.push("failed to get IP address from X-Forwarded-For"); + } + } + return [ip, errors]; + })(); + + if (!ip) { + return { + error: "Could not get IP address. Please try again.", + context: errors, + }; + } + + // https://developers.cloudflare.com/turnstile/get-started/server-side-validation/ + // Validate the token by calling the "/siteverify" API endpoint. + const result = await fetch( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + body: JSON.stringify({ + secret: process.env.TURNSTILE_SECRET_KEY, + response: turnstileToken, + remoteip: ip, + }), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }, + ); + + const outcome = await result.json(); + if (!outcome.success) { + return { + error: "Could not validate captcha.", + }; + } + const cookieStore = await cookies(); const utmCookies = cookieStore .getAll() @@ -86,7 +166,9 @@ export async function doLogin(payload: VerifyLoginPayloadParams) { res.statusText, response, ); - throw new Error("Failed to login - api call failed"); + return { + error: "Failed to login. Please try again later.", + }; } catch { // just log the basics console.error( @@ -95,7 +177,9 @@ export async function doLogin(payload: VerifyLoginPayloadParams) { res.statusText, ); } - throw new Error("Failed to login - api call failed"); + return { + error: "Failed to login. Please try again later.", + }; } const json = await res.json(); @@ -104,7 +188,9 @@ export async function doLogin(payload: VerifyLoginPayloadParams) { if (!jwt) { console.error("Failed to login - invalid json", json); - throw new Error("Failed to login - invalid json"); + return { + error: "Failed to login. Please try again later.", + }; } // set the token cookie @@ -128,6 +214,10 @@ export async function doLogin(payload: VerifyLoginPayloadParams) { // 3 days maxAge: 3 * 24 * 60 * 60, }); + + return { + success: true, + }; } export async function doLogout() {