mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
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
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "10.0.0",
|
||||
"@hookform/resolvers": "5.2.1",
|
||||
"@marsidev/react-turnstile": "1.3.1",
|
||||
"@next/third-parties": "15.4.6",
|
||||
"@phosphor-icons/react": "2.1.10",
|
||||
"@radix-ui/react-alert-dialog": "1.1.15",
|
||||
|
||||
14
autogpt_platform/frontend/pnpm-lock.yaml
generated
14
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@hookform/resolvers':
|
||||
specifier: 5.2.1
|
||||
version: 5.2.1(react-hook-form@7.62.0(react@18.3.1))
|
||||
'@marsidev/react-turnstile':
|
||||
specifier: 1.3.1
|
||||
version: 1.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@next/third-parties':
|
||||
specifier: 15.4.6
|
||||
version: 15.4.6(next@15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
@@ -1428,6 +1431,12 @@ packages:
|
||||
peerDependencies:
|
||||
jsep: ^0.4.0||^1.0.0
|
||||
|
||||
'@marsidev/react-turnstile@1.3.1':
|
||||
resolution: {integrity: sha512-h2THG/75k4Y049hgjSGPIcajxXnh+IZAiXVbryQyVmagkboN7pJtBgR16g8akjwUBSfRrg6jw6KvPDjscQflog==}
|
||||
peerDependencies:
|
||||
react: ^17.0.2 || ^18.0.0 || ^19.0
|
||||
react-dom: ^17.0.2 || ^18.0.0 || ^19.0
|
||||
|
||||
'@mdx-js/react@3.1.1':
|
||||
resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
|
||||
peerDependencies:
|
||||
@@ -8668,6 +8677,11 @@ snapshots:
|
||||
dependencies:
|
||||
jsep: 1.4.0
|
||||
|
||||
'@marsidev/react-turnstile@1.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@mdx-js/react@3.1.1(@types/react@18.3.17)(react@18.3.1)':
|
||||
dependencies:
|
||||
'@types/mdx': 2.0.13
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Link } from "@/components/atoms/Link/Link";
|
||||
@@ -6,18 +7,16 @@ import { AuthCard } from "@/components/auth/AuthCard";
|
||||
import AuthFeedback from "@/components/auth/AuthFeedback";
|
||||
import { EmailNotAllowedModal } from "@/components/auth/EmailNotAllowedModal";
|
||||
import { GoogleOAuthButton } from "@/components/auth/GoogleOAuthButton";
|
||||
import Turnstile from "@/components/auth/Turnstile";
|
||||
import { Form, FormField } from "@/components/__legacy__/ui/form";
|
||||
import { getBehaveAs } from "@/lib/utils";
|
||||
import { LoadingLogin } from "./components/LoadingLogin";
|
||||
import { useLoginPage } from "./useLoginPage";
|
||||
import { Turnstile2 } from "@/components/auth/Turnstile2";
|
||||
|
||||
export default function LoginPage() {
|
||||
const {
|
||||
form,
|
||||
feedback,
|
||||
turnstile,
|
||||
captchaKey,
|
||||
isLoading,
|
||||
isLoggedIn,
|
||||
isCloudEnv,
|
||||
@@ -28,6 +27,8 @@ export default function LoginPage() {
|
||||
handleSubmit,
|
||||
handleProviderLogin,
|
||||
handleCloseNotAllowedModal,
|
||||
handleCaptchaVerify,
|
||||
handleCaptchaReady,
|
||||
} = useLoginPage();
|
||||
|
||||
if (isUserLoading || isLoggedIn) {
|
||||
@@ -84,19 +85,12 @@ export default function LoginPage() {
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Turnstile CAPTCHA Component */}
|
||||
{turnstile.shouldRender ? (
|
||||
<Turnstile
|
||||
key={captchaKey}
|
||||
siteKey={turnstile.siteKey}
|
||||
onVerify={turnstile.handleVerify}
|
||||
onExpire={turnstile.handleExpire}
|
||||
onError={turnstile.handleError}
|
||||
setWidgetId={turnstile.setWidgetId}
|
||||
action="login"
|
||||
shouldRender={turnstile.shouldRender}
|
||||
<div className="flex items-center justify-center">
|
||||
<Turnstile2
|
||||
onVerified={handleCaptchaVerify}
|
||||
onReady={handleCaptchaReady}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
import { useTurnstile } from "@/hooks/useTurnstile";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { BehaveAs, getBehaveAs } from "@/lib/utils";
|
||||
import { loginFormSchema, LoginProvider } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import z from "zod";
|
||||
import { login, providerLogin } from "./actions";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { TurnstileInstance } from "@marsidev/react-turnstile";
|
||||
|
||||
export function useLoginPage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const [captchaKey, setCaptchaKey] = useState(0);
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||
const [captchaRef, setCaptchaRef] = useState<TurnstileInstance | null>(null);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
|
||||
const isCloudEnv = getBehaveAs() === BehaveAs.CLOUD;
|
||||
const isVercelPreview = process.env.NEXT_PUBLIC_VERCEL_ENV === "preview";
|
||||
|
||||
const turnstile = useTurnstile({
|
||||
action: "login",
|
||||
autoVerify: false,
|
||||
resetOnError: true,
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof loginFormSchema>>({
|
||||
resolver: zodResolver(loginFormSchema),
|
||||
defaultValues: {
|
||||
@@ -36,11 +31,6 @@ export function useLoginPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const resetCaptcha = useCallback(() => {
|
||||
setCaptchaKey((k) => k + 1);
|
||||
turnstile.reset();
|
||||
}, [turnstile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) router.push("/");
|
||||
}, [user]);
|
||||
@@ -48,14 +38,14 @@ export function useLoginPage() {
|
||||
async function handleProviderLogin(provider: LoginProvider) {
|
||||
setIsGoogleLoading(true);
|
||||
|
||||
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
|
||||
if (isCloudEnv && !captchaToken && !isVercelPreview) {
|
||||
toast({
|
||||
title: "Please complete the CAPTCHA challenge.",
|
||||
variant: "info",
|
||||
});
|
||||
|
||||
setIsGoogleLoading(false);
|
||||
resetCaptcha();
|
||||
captchaRef?.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,7 +54,7 @@ export function useLoginPage() {
|
||||
if (error) throw error;
|
||||
setFeedback(null);
|
||||
} catch (error) {
|
||||
resetCaptcha();
|
||||
captchaRef?.reset();
|
||||
setIsGoogleLoading(false);
|
||||
const errorString = JSON.stringify(error);
|
||||
if (errorString.includes("not_allowed")) {
|
||||
@@ -77,14 +67,14 @@ export function useLoginPage() {
|
||||
|
||||
async function handleLogin(data: z.infer<typeof loginFormSchema>) {
|
||||
setIsLoading(true);
|
||||
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
|
||||
if (isCloudEnv && !captchaToken && !isVercelPreview) {
|
||||
toast({
|
||||
title: "Please complete the CAPTCHA challenge.",
|
||||
variant: "info",
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
captchaRef?.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,11 +85,11 @@ export function useLoginPage() {
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
captchaRef?.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await login(data, turnstile.token as string);
|
||||
const error = await login(data, captchaToken as string);
|
||||
await supabase?.auth.refreshSession();
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
@@ -108,19 +98,24 @@ export function useLoginPage() {
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
resetCaptcha();
|
||||
// Always reset the turnstile on any error
|
||||
turnstile.reset();
|
||||
captchaRef?.reset();
|
||||
return;
|
||||
}
|
||||
setFeedback(null);
|
||||
}
|
||||
|
||||
function handleCaptchaVerify(token: string) {
|
||||
setCaptchaToken(token);
|
||||
}
|
||||
|
||||
function handleCaptchaReady(ref: TurnstileInstance) {
|
||||
setCaptchaRef(ref);
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
feedback,
|
||||
turnstile,
|
||||
captchaKey,
|
||||
captchaRef,
|
||||
isLoggedIn: !!user,
|
||||
isLoading,
|
||||
isCloudEnv,
|
||||
@@ -131,5 +126,7 @@ export function useLoginPage() {
|
||||
handleSubmit: form.handleSubmit(handleLogin),
|
||||
handleProviderLogin,
|
||||
handleCloseNotAllowedModal: () => setShowNotAllowedModal(false),
|
||||
handleCaptchaVerify,
|
||||
handleCaptchaReady,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface TaskGroup {
|
||||
|
||||
export default function Wallet() {
|
||||
const { state, updateState } = useOnboarding();
|
||||
|
||||
const groups = useMemo<TaskGroup[]>(() => {
|
||||
return [
|
||||
{
|
||||
@@ -348,10 +349,11 @@ export default function Wallet() {
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={cn(
|
||||
"absolute -right-[7.9rem] -top-[3.2rem] z-50 w-[28.5rem] px-[0.625rem] py-2",
|
||||
"rounded-xl border-zinc-100 bg-white shadow-[0_3px_3px] shadow-zinc-200",
|
||||
)}
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={12}
|
||||
collisionPadding={16}
|
||||
className={cn("z-50 w-[28.5rem] px-[0.625rem] py-2")}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="mx-1 flex items-center justify-between border-b border-zinc-200 pb-3">
|
||||
|
||||
@@ -80,19 +80,20 @@ export function Turnstile({
|
||||
|
||||
// Render a new widget
|
||||
if (window.turnstile) {
|
||||
widgetIdRef.current = window.turnstile.render(containerRef.current, {
|
||||
sitekey: siteKey,
|
||||
callback: (token: string) => {
|
||||
onVerify(token);
|
||||
},
|
||||
"expired-callback": () => {
|
||||
onExpire?.();
|
||||
},
|
||||
"error-callback": () => {
|
||||
onError?.(new Error("Turnstile widget encountered an error"));
|
||||
},
|
||||
action,
|
||||
});
|
||||
widgetIdRef.current =
|
||||
window.turnstile.render(containerRef.current, {
|
||||
sitekey: siteKey,
|
||||
callback: (token: string) => {
|
||||
onVerify(token);
|
||||
},
|
||||
"expired-callback": () => {
|
||||
onExpire?.();
|
||||
},
|
||||
"error-callback": () => {
|
||||
onError?.(new Error("Turnstile widget encountered an error"));
|
||||
},
|
||||
action,
|
||||
}) ?? "";
|
||||
|
||||
// Notify the hook about the widget ID
|
||||
setWidgetId?.(widgetIdRef.current);
|
||||
@@ -144,6 +145,7 @@ export function Turnstile({
|
||||
// Add TypeScript interface to Window to include turnstile property
|
||||
declare global {
|
||||
interface Window {
|
||||
// @ts-expect-error - turnstile is not defined in the window object
|
||||
turnstile?: {
|
||||
render: (
|
||||
container: HTMLElement,
|
||||
|
||||
52
autogpt_platform/frontend/src/components/auth/Turnstile2.tsx
Normal file
52
autogpt_platform/frontend/src/components/auth/Turnstile2.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { BehaveAs, getBehaveAs } from "@/lib/utils";
|
||||
import { Turnstile, TurnstileInstance } from "@marsidev/react-turnstile";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const TURNSTILE_SITE_KEY =
|
||||
process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY || "";
|
||||
|
||||
type Props = {
|
||||
onVerified: (token: string) => void;
|
||||
onReady: (ref: TurnstileInstance) => void;
|
||||
};
|
||||
|
||||
export function Turnstile2(props: Props) {
|
||||
const captchaRef = useRef<TurnstileInstance>(null);
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||
const behaveAs = getBehaveAs();
|
||||
|
||||
useEffect(() => {
|
||||
if (captchaRef.current) {
|
||||
props.onReady(captchaRef.current);
|
||||
}
|
||||
}, [captchaRef]);
|
||||
|
||||
function handleCaptchaVerify(token: string) {
|
||||
setCaptchaToken(token);
|
||||
props.onVerified(token);
|
||||
}
|
||||
|
||||
// Only render in cloud environment
|
||||
if (behaveAs !== BehaveAs.CLOUD) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!TURNSTILE_SITE_KEY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it is already verified, no need to render
|
||||
if (captchaToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Turnstile
|
||||
ref={captchaRef}
|
||||
siteKey={TURNSTILE_SITE_KEY}
|
||||
onSuccess={handleCaptchaVerify}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user