Skip to content

Commit ff58ce1

Browse files
authored
fix(frontend): possible login issues related to turnstile (#11094)
## Changes 🏗️ We are seeing login and authentication issues in production and staging. Locally though, the app behaves fine. We also had issues related to the CAPTCHA in the past. Our CAPTCHA code is less than ideal, with some heavy `useEffect` that will load the Turnstile script into the DOM. I have the impression that is loading the script multiple times ( due to dependencies on the effects array not being well set ), or the like causing associated login issues. Created a new Turnstile component using [`react-turnstile`](https://docs.page/marsidev/react-turnstile) that is way simpler and should hopefully be more stable. I also fixed an issue with the Credits popover layout rendering cropped on the window. ## Checklist 📋 ### For code changes - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Login/logout on the app multiple times with Turnstile ON, everything is stable - [x] Credits popover appears on the right place ### For configuration changes: None
1 parent 2d8ab6b commit ff58ce1

File tree

7 files changed

+120
-58
lines changed

7 files changed

+120
-58
lines changed

autogpt_platform/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"dependencies": {
2828
"@faker-js/faker": "10.0.0",
2929
"@hookform/resolvers": "5.2.1",
30+
"@marsidev/react-turnstile": "1.3.1",
3031
"@next/third-parties": "15.4.6",
3132
"@phosphor-icons/react": "2.1.10",
3233
"@radix-ui/react-alert-dialog": "1.1.15",

autogpt_platform/frontend/pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

autogpt_platform/frontend/src/app/(platform)/login/page.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
"use client";
2+
23
import { Button } from "@/components/atoms/Button/Button";
34
import { Input } from "@/components/atoms/Input/Input";
45
import { Link } from "@/components/atoms/Link/Link";
56
import { AuthCard } from "@/components/auth/AuthCard";
67
import AuthFeedback from "@/components/auth/AuthFeedback";
78
import { EmailNotAllowedModal } from "@/components/auth/EmailNotAllowedModal";
89
import { GoogleOAuthButton } from "@/components/auth/GoogleOAuthButton";
9-
import Turnstile from "@/components/auth/Turnstile";
1010
import { Form, FormField } from "@/components/__legacy__/ui/form";
1111
import { getBehaveAs } from "@/lib/utils";
1212
import { LoadingLogin } from "./components/LoadingLogin";
1313
import { useLoginPage } from "./useLoginPage";
14+
import { Turnstile2 } from "@/components/auth/Turnstile2";
1415

1516
export default function LoginPage() {
1617
const {
1718
form,
1819
feedback,
19-
turnstile,
20-
captchaKey,
2120
isLoading,
2221
isLoggedIn,
2322
isCloudEnv,
@@ -28,6 +27,8 @@ export default function LoginPage() {
2827
handleSubmit,
2928
handleProviderLogin,
3029
handleCloseNotAllowedModal,
30+
handleCaptchaVerify,
31+
handleCaptchaReady,
3132
} = useLoginPage();
3233

3334
if (isUserLoading || isLoggedIn) {
@@ -84,19 +85,12 @@ export default function LoginPage() {
8485
)}
8586
/>
8687

87-
{/* Turnstile CAPTCHA Component */}
88-
{turnstile.shouldRender ? (
89-
<Turnstile
90-
key={captchaKey}
91-
siteKey={turnstile.siteKey}
92-
onVerify={turnstile.handleVerify}
93-
onExpire={turnstile.handleExpire}
94-
onError={turnstile.handleError}
95-
setWidgetId={turnstile.setWidgetId}
96-
action="login"
97-
shouldRender={turnstile.shouldRender}
88+
<div className="flex items-center justify-center">
89+
<Turnstile2
90+
onVerified={handleCaptchaVerify}
91+
onReady={handleCaptchaReady}
9892
/>
99-
) : null}
93+
</div>
10094

10195
<Button
10296
variant="primary"
Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,28 @@
1-
import { useTurnstile } from "@/hooks/useTurnstile";
21
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
32
import { BehaveAs, getBehaveAs } from "@/lib/utils";
43
import { loginFormSchema, LoginProvider } from "@/types/auth";
54
import { zodResolver } from "@hookform/resolvers/zod";
65
import { useRouter } from "next/navigation";
7-
import { useCallback, useEffect, useState } from "react";
6+
import { useEffect, useState } from "react";
87
import { useForm } from "react-hook-form";
98
import z from "zod";
109
import { login, providerLogin } from "./actions";
1110
import { useToast } from "@/components/molecules/Toast/use-toast";
11+
import { TurnstileInstance } from "@marsidev/react-turnstile";
1212

1313
export function useLoginPage() {
1414
const { supabase, user, isUserLoading } = useSupabase();
1515
const [feedback, setFeedback] = useState<string | null>(null);
16-
const [captchaKey, setCaptchaKey] = useState(0);
1716
const router = useRouter();
1817
const { toast } = useToast();
1918
const [isLoading, setIsLoading] = useState(false);
19+
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
20+
const [captchaRef, setCaptchaRef] = useState<TurnstileInstance | null>(null);
2021
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
2122
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
2223
const isCloudEnv = getBehaveAs() === BehaveAs.CLOUD;
2324
const isVercelPreview = process.env.NEXT_PUBLIC_VERCEL_ENV === "preview";
2425

25-
const turnstile = useTurnstile({
26-
action: "login",
27-
autoVerify: false,
28-
resetOnError: true,
29-
});
30-
3126
const form = useForm<z.infer<typeof loginFormSchema>>({
3227
resolver: zodResolver(loginFormSchema),
3328
defaultValues: {
@@ -36,26 +31,21 @@ export function useLoginPage() {
3631
},
3732
});
3833

39-
const resetCaptcha = useCallback(() => {
40-
setCaptchaKey((k) => k + 1);
41-
turnstile.reset();
42-
}, [turnstile]);
43-
4434
useEffect(() => {
4535
if (user) router.push("/");
4636
}, [user]);
4737

4838
async function handleProviderLogin(provider: LoginProvider) {
4939
setIsGoogleLoading(true);
5040

51-
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
41+
if (isCloudEnv && !captchaToken && !isVercelPreview) {
5242
toast({
5343
title: "Please complete the CAPTCHA challenge.",
5444
variant: "info",
5545
});
5646

5747
setIsGoogleLoading(false);
58-
resetCaptcha();
48+
captchaRef?.reset();
5949
return;
6050
}
6151

@@ -64,7 +54,7 @@ export function useLoginPage() {
6454
if (error) throw error;
6555
setFeedback(null);
6656
} catch (error) {
67-
resetCaptcha();
57+
captchaRef?.reset();
6858
setIsGoogleLoading(false);
6959
const errorString = JSON.stringify(error);
7060
if (errorString.includes("not_allowed")) {
@@ -77,14 +67,14 @@ export function useLoginPage() {
7767

7868
async function handleLogin(data: z.infer<typeof loginFormSchema>) {
7969
setIsLoading(true);
80-
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
70+
if (isCloudEnv && !captchaToken && !isVercelPreview) {
8171
toast({
8272
title: "Please complete the CAPTCHA challenge.",
8373
variant: "info",
8474
});
8575

8676
setIsLoading(false);
87-
resetCaptcha();
77+
captchaRef?.reset();
8878
return;
8979
}
9080

@@ -95,11 +85,11 @@ export function useLoginPage() {
9585
});
9686

9787
setIsLoading(false);
98-
resetCaptcha();
88+
captchaRef?.reset();
9989
return;
10090
}
10191

102-
const error = await login(data, turnstile.token as string);
92+
const error = await login(data, captchaToken as string);
10393
await supabase?.auth.refreshSession();
10494
setIsLoading(false);
10595
if (error) {
@@ -108,19 +98,24 @@ export function useLoginPage() {
10898
variant: "destructive",
10999
});
110100

111-
resetCaptcha();
112-
// Always reset the turnstile on any error
113-
turnstile.reset();
101+
captchaRef?.reset();
114102
return;
115103
}
116104
setFeedback(null);
117105
}
118106

107+
function handleCaptchaVerify(token: string) {
108+
setCaptchaToken(token);
109+
}
110+
111+
function handleCaptchaReady(ref: TurnstileInstance) {
112+
setCaptchaRef(ref);
113+
}
114+
119115
return {
120116
form,
121117
feedback,
122-
turnstile,
123-
captchaKey,
118+
captchaRef,
124119
isLoggedIn: !!user,
125120
isLoading,
126121
isCloudEnv,
@@ -131,5 +126,7 @@ export function useLoginPage() {
131126
handleSubmit: form.handleSubmit(handleLogin),
132127
handleProviderLogin,
133128
handleCloseNotAllowedModal: () => setShowNotAllowedModal(false),
129+
handleCaptchaVerify,
130+
handleCaptchaReady,
134131
};
135132
}

autogpt_platform/frontend/src/components/__legacy__/Wallet.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface TaskGroup {
3939

4040
export default function Wallet() {
4141
const { state, updateState } = useOnboarding();
42+
4243
const groups = useMemo<TaskGroup[]>(() => {
4344
return [
4445
{
@@ -348,10 +349,11 @@ export default function Wallet() {
348349
</div>
349350
</PopoverTrigger>
350351
<PopoverContent
351-
className={cn(
352-
"absolute -right-[7.9rem] -top-[3.2rem] z-50 w-[28.5rem] px-[0.625rem] py-2",
353-
"rounded-xl border-zinc-100 bg-white shadow-[0_3px_3px] shadow-zinc-200",
354-
)}
352+
side="bottom"
353+
align="end"
354+
sideOffset={12}
355+
collisionPadding={16}
356+
className={cn("z-50 w-[28.5rem] px-[0.625rem] py-2")}
355357
>
356358
{/* Header */}
357359
<div className="mx-1 flex items-center justify-between border-b border-zinc-200 pb-3">

autogpt_platform/frontend/src/components/auth/Turnstile.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,20 @@ export function Turnstile({
8080

8181
// Render a new widget
8282
if (window.turnstile) {
83-
widgetIdRef.current = window.turnstile.render(containerRef.current, {
84-
sitekey: siteKey,
85-
callback: (token: string) => {
86-
onVerify(token);
87-
},
88-
"expired-callback": () => {
89-
onExpire?.();
90-
},
91-
"error-callback": () => {
92-
onError?.(new Error("Turnstile widget encountered an error"));
93-
},
94-
action,
95-
});
83+
widgetIdRef.current =
84+
window.turnstile.render(containerRef.current, {
85+
sitekey: siteKey,
86+
callback: (token: string) => {
87+
onVerify(token);
88+
},
89+
"expired-callback": () => {
90+
onExpire?.();
91+
},
92+
"error-callback": () => {
93+
onError?.(new Error("Turnstile widget encountered an error"));
94+
},
95+
action,
96+
}) ?? "";
9697

9798
// Notify the hook about the widget ID
9899
setWidgetId?.(widgetIdRef.current);
@@ -144,6 +145,7 @@ export function Turnstile({
144145
// Add TypeScript interface to Window to include turnstile property
145146
declare global {
146147
interface Window {
148+
// @ts-expect-error - turnstile is not defined in the window object
147149
turnstile?: {
148150
render: (
149151
container: HTMLElement,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use client";
2+
3+
import { BehaveAs, getBehaveAs } from "@/lib/utils";
4+
import { Turnstile, TurnstileInstance } from "@marsidev/react-turnstile";
5+
import { useEffect, useRef, useState } from "react";
6+
7+
const TURNSTILE_SITE_KEY =
8+
process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY || "";
9+
10+
type Props = {
11+
onVerified: (token: string) => void;
12+
onReady: (ref: TurnstileInstance) => void;
13+
};
14+
15+
export function Turnstile2(props: Props) {
16+
const captchaRef = useRef<TurnstileInstance>(null);
17+
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
18+
const behaveAs = getBehaveAs();
19+
20+
useEffect(() => {
21+
if (captchaRef.current) {
22+
props.onReady(captchaRef.current);
23+
}
24+
}, [captchaRef]);
25+
26+
function handleCaptchaVerify(token: string) {
27+
setCaptchaToken(token);
28+
props.onVerified(token);
29+
}
30+
31+
// Only render in cloud environment
32+
if (behaveAs !== BehaveAs.CLOUD) {
33+
return null;
34+
}
35+
36+
if (!TURNSTILE_SITE_KEY) {
37+
return null;
38+
}
39+
40+
// If it is already verified, no need to render
41+
if (captchaToken) {
42+
return null;
43+
}
44+
45+
return (
46+
<Turnstile
47+
ref={captchaRef}
48+
siteKey={TURNSTILE_SITE_KEY}
49+
onSuccess={handleCaptchaVerify}
50+
/>
51+
);
52+
}

0 commit comments

Comments
 (0)