mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(frontend): more turnstile experiments (2)
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Link } from "@/components/atoms/Link/Link";
|
||||
@@ -7,17 +6,18 @@ 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 Turnstile from "@/components/auth/Turnstile";
|
||||
|
||||
export default function LoginPage() {
|
||||
const {
|
||||
form,
|
||||
feedback,
|
||||
captchaToken,
|
||||
turnstile,
|
||||
captchaKey,
|
||||
isLoading,
|
||||
isLoggedIn,
|
||||
isCloudEnv,
|
||||
@@ -28,9 +28,6 @@ export default function LoginPage() {
|
||||
handleSubmit,
|
||||
handleProviderLogin,
|
||||
handleCloseNotAllowedModal,
|
||||
handleCaptchaVerify,
|
||||
setCaptchaWidgetId,
|
||||
captchaResetNonce,
|
||||
} = useLoginPage();
|
||||
|
||||
if (isUserLoading || isLoggedIn) {
|
||||
@@ -87,21 +84,17 @@ export default function LoginPage() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<Turnstile
|
||||
siteKey={
|
||||
process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY || ""
|
||||
}
|
||||
onVerify={handleCaptchaVerify}
|
||||
shouldRender={Boolean(
|
||||
isCloudEnv &&
|
||||
!captchaToken &&
|
||||
process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY,
|
||||
)}
|
||||
setWidgetId={setCaptchaWidgetId}
|
||||
resetSignal={captchaResetNonce}
|
||||
/>
|
||||
</div>
|
||||
{/* Turnstile CAPTCHA Component */}
|
||||
<Turnstile
|
||||
key={captchaKey}
|
||||
siteKey={turnstile.siteKey}
|
||||
onVerify={turnstile.handleVerify}
|
||||
onExpire={turnstile.handleExpire}
|
||||
onError={turnstile.handleError}
|
||||
setWidgetId={turnstile.setWidgetId}
|
||||
action="login"
|
||||
shouldRender={turnstile.shouldRender}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
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 { useEffect, useState } from "react";
|
||||
import { useCallback, 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";
|
||||
// Captcha integration handled via widget ID reset in page
|
||||
|
||||
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 [captchaWidgetId, setCaptchaWidgetId] = useState<string | null>(null);
|
||||
const [captchaResetNonce, setCaptchaResetNonce] = useState(0);
|
||||
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: {
|
||||
@@ -32,6 +36,11 @@ export function useLoginPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const resetCaptcha = useCallback(() => {
|
||||
setCaptchaKey((k) => k + 1);
|
||||
turnstile.reset();
|
||||
}, [turnstile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) router.push("/");
|
||||
}, [user]);
|
||||
@@ -39,13 +48,14 @@ export function useLoginPage() {
|
||||
async function handleProviderLogin(provider: LoginProvider) {
|
||||
setIsGoogleLoading(true);
|
||||
|
||||
if (isCloudEnv && !captchaToken && !isVercelPreview) {
|
||||
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
|
||||
toast({
|
||||
title: "Please complete the CAPTCHA challenge.",
|
||||
variant: "info",
|
||||
});
|
||||
|
||||
setIsGoogleLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,10 +64,7 @@ export function useLoginPage() {
|
||||
if (error) throw error;
|
||||
setFeedback(null);
|
||||
} catch (error) {
|
||||
setCaptchaToken(null);
|
||||
if (captchaWidgetId && window?.turnstile)
|
||||
window.turnstile.reset(captchaWidgetId);
|
||||
setCaptchaResetNonce((n) => n + 1);
|
||||
resetCaptcha();
|
||||
setIsGoogleLoading(false);
|
||||
const errorString = JSON.stringify(error);
|
||||
if (errorString.includes("not_allowed")) {
|
||||
@@ -70,13 +77,14 @@ export function useLoginPage() {
|
||||
|
||||
async function handleLogin(data: z.infer<typeof loginFormSchema>) {
|
||||
setIsLoading(true);
|
||||
if (isCloudEnv && !captchaToken && !isVercelPreview) {
|
||||
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
|
||||
toast({
|
||||
title: "Please complete the CAPTCHA challenge.",
|
||||
variant: "info",
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,10 +95,11 @@ export function useLoginPage() {
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await login(data, captchaToken as string);
|
||||
const error = await login(data, turnstile.token as string);
|
||||
await supabase?.auth.refreshSession();
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
@@ -99,23 +108,19 @@ export function useLoginPage() {
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
setCaptchaToken(null);
|
||||
if (captchaWidgetId && window?.turnstile)
|
||||
window.turnstile.reset(captchaWidgetId);
|
||||
setCaptchaResetNonce((n) => n + 1);
|
||||
resetCaptcha();
|
||||
// Always reset the turnstile on any error
|
||||
turnstile.reset();
|
||||
return;
|
||||
}
|
||||
setFeedback(null);
|
||||
}
|
||||
|
||||
function handleCaptchaVerify(token: string) {
|
||||
setCaptchaToken(token);
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
feedback,
|
||||
captchaToken,
|
||||
turnstile,
|
||||
captchaKey,
|
||||
isLoggedIn: !!user,
|
||||
isLoading,
|
||||
isCloudEnv,
|
||||
@@ -126,8 +131,5 @@ export function useLoginPage() {
|
||||
handleSubmit: form.handleSubmit(handleLogin),
|
||||
handleProviderLogin,
|
||||
handleCloseNotAllowedModal: () => setShowNotAllowedModal(false),
|
||||
handleCaptchaVerify,
|
||||
setCaptchaWidgetId,
|
||||
captchaResetNonce,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ export interface TurnstileProps {
|
||||
id?: string;
|
||||
shouldRender?: boolean;
|
||||
setWidgetId?: (id: string | null) => void;
|
||||
// Changing this value will trigger a widget reset safely inside the component
|
||||
resetSignal?: number | string | boolean;
|
||||
}
|
||||
|
||||
export function Turnstile({
|
||||
@@ -27,7 +25,6 @@ export function Turnstile({
|
||||
id = "cf-turnstile",
|
||||
shouldRender = true,
|
||||
setWidgetId,
|
||||
resetSignal,
|
||||
}: TurnstileProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const widgetIdRef = useRef<string | null>(null);
|
||||
@@ -43,37 +40,63 @@ export function Turnstile({
|
||||
return;
|
||||
}
|
||||
|
||||
// Create script element
|
||||
const script = document.createElement("script");
|
||||
script.src =
|
||||
const scriptSrc =
|
||||
"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
||||
|
||||
// If a script already exists, reuse it and attach listeners
|
||||
const existingScript = Array.from(document.scripts).find(
|
||||
(s) => s.src === scriptSrc,
|
||||
);
|
||||
|
||||
if (existingScript) {
|
||||
if (window.turnstile) {
|
||||
setLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleLoad: EventListener = () => {
|
||||
setLoaded(true);
|
||||
};
|
||||
const handleError: EventListener = () => {
|
||||
onError?.(new Error("Failed to load Turnstile script"));
|
||||
};
|
||||
|
||||
existingScript.addEventListener("load", handleLoad);
|
||||
existingScript.addEventListener("error", handleError);
|
||||
|
||||
return () => {
|
||||
existingScript.removeEventListener("load", handleLoad);
|
||||
existingScript.removeEventListener("error", handleError);
|
||||
};
|
||||
}
|
||||
|
||||
// Create a single script element if not present and keep it in the document
|
||||
const script = document.createElement("script");
|
||||
script.src = scriptSrc;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
|
||||
script.onload = () => {
|
||||
const handleLoad: EventListener = () => {
|
||||
setLoaded(true);
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
const handleError: EventListener = () => {
|
||||
onError?.(new Error("Failed to load Turnstile script"));
|
||||
};
|
||||
|
||||
script.addEventListener("load", handleLoad);
|
||||
script.addEventListener("error", handleError);
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Do not remove the script on unmount to avoid race conditions with
|
||||
// other potential widgets or future mounts.
|
||||
return () => {};
|
||||
return () => {
|
||||
script.removeEventListener("load", handleLoad);
|
||||
script.removeEventListener("error", handleError);
|
||||
};
|
||||
}, [onError, shouldRender]);
|
||||
|
||||
// Initialize and render the widget when script is loaded
|
||||
useEffect(() => {
|
||||
if (
|
||||
!loaded ||
|
||||
!containerRef.current ||
|
||||
!window.turnstile ||
|
||||
!shouldRender ||
|
||||
!siteKey
|
||||
)
|
||||
if (!loaded || !containerRef.current || !window.turnstile || !shouldRender)
|
||||
return;
|
||||
|
||||
// Reset any existing widget
|
||||
@@ -87,20 +110,19 @@ 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);
|
||||
@@ -130,20 +152,13 @@ export function Turnstile({
|
||||
|
||||
// Method to reset the widget manually
|
||||
useEffect(() => {
|
||||
if (
|
||||
loaded &&
|
||||
widgetIdRef.current &&
|
||||
window.turnstile &&
|
||||
shouldRender &&
|
||||
siteKey
|
||||
) {
|
||||
if (loaded && widgetIdRef.current && window.turnstile && shouldRender) {
|
||||
window.turnstile.reset(widgetIdRef.current);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loaded, shouldRender, resetSignal, siteKey]);
|
||||
}, [loaded, shouldRender]);
|
||||
|
||||
// If shouldRender is false, don't render anything
|
||||
if (!shouldRender || !siteKey) {
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user