mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-07 22:33:57 -05:00
refactor(turnstile): Remove turnstile (#11387)
This PR removes turnstile from the platform. #### 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: <!-- Put your test plan here: --> - [x] Test to make sure that turnstile is gone, it will be. - [x] Test logging in with out turnstile to make sure it still works - [x] Test registering a new account with out turnstile and it works
This commit is contained in:
@@ -134,13 +134,6 @@ POSTMARK_WEBHOOK_TOKEN=
|
||||
# Error Tracking
|
||||
SENTRY_DSN=
|
||||
|
||||
# Cloudflare Turnstile (CAPTCHA) Configuration
|
||||
# Get these from the Cloudflare Turnstile dashboard: https://dash.cloudflare.com/?to=/:account/turnstile
|
||||
# This is the backend secret key
|
||||
TURNSTILE_SECRET_KEY=
|
||||
# This is the verify URL
|
||||
TURNSTILE_VERIFY_URL=https://challenges.cloudflare.com/turnstile/v0/siteverify
|
||||
|
||||
# Feature Flags
|
||||
LAUNCH_DARKLY_SDK_KEY=
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ import backend.server.v2.library.routes
|
||||
import backend.server.v2.otto.routes
|
||||
import backend.server.v2.store.model
|
||||
import backend.server.v2.store.routes
|
||||
import backend.server.v2.turnstile.routes
|
||||
import backend.util.service
|
||||
import backend.util.settings
|
||||
from backend.blocks.llm import LlmModel
|
||||
@@ -281,11 +280,6 @@ app.include_router(
|
||||
app.include_router(
|
||||
backend.server.v2.otto.routes.router, tags=["v2", "otto"], prefix="/api/otto"
|
||||
)
|
||||
app.include_router(
|
||||
backend.server.v2.turnstile.routes.router,
|
||||
tags=["v2", "turnstile"],
|
||||
prefix="/api/turnstile",
|
||||
)
|
||||
|
||||
app.include_router(
|
||||
backend.server.routers.postmark.postmark.router,
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class TurnstileVerifyRequest(BaseModel):
|
||||
"""Request model for verifying a Turnstile token."""
|
||||
|
||||
token: str = Field(description="The Turnstile token to verify")
|
||||
action: Optional[str] = Field(
|
||||
default=None, description="The action that the user is attempting to perform"
|
||||
)
|
||||
|
||||
|
||||
class TurnstileVerifyResponse(BaseModel):
|
||||
"""Response model for the Turnstile verification endpoint."""
|
||||
|
||||
success: bool = Field(description="Whether the token verification was successful")
|
||||
error: Optional[str] = Field(
|
||||
default=None, description="Error message if verification failed"
|
||||
)
|
||||
challenge_timestamp: Optional[str] = Field(
|
||||
default=None, description="Timestamp of the challenge (ISO format)"
|
||||
)
|
||||
hostname: Optional[str] = Field(
|
||||
default=None, description="Hostname of the site where the challenge was solved"
|
||||
)
|
||||
action: Optional[str] = Field(
|
||||
default=None, description="The action associated with this verification"
|
||||
)
|
||||
@@ -1,112 +0,0 @@
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from fastapi import APIRouter
|
||||
|
||||
from backend.util.settings import Settings
|
||||
|
||||
from .models import TurnstileVerifyRequest, TurnstileVerifyResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
settings = Settings()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/verify", response_model=TurnstileVerifyResponse, summary="Verify Turnstile Token"
|
||||
)
|
||||
async def verify_turnstile_token(
|
||||
request: TurnstileVerifyRequest,
|
||||
) -> TurnstileVerifyResponse:
|
||||
"""
|
||||
Verify a Cloudflare Turnstile token.
|
||||
This endpoint verifies a token returned by the Cloudflare Turnstile challenge
|
||||
on the client side. It returns whether the verification was successful.
|
||||
"""
|
||||
logger.info(f"Verifying Turnstile token for action: {request.action}")
|
||||
return await verify_token(request)
|
||||
|
||||
|
||||
async def verify_token(request: TurnstileVerifyRequest) -> TurnstileVerifyResponse:
|
||||
"""
|
||||
Verify a Cloudflare Turnstile token by making a request to the Cloudflare API.
|
||||
"""
|
||||
# Get the secret key from settings
|
||||
turnstile_secret_key = settings.secrets.turnstile_secret_key
|
||||
turnstile_verify_url = settings.secrets.turnstile_verify_url
|
||||
|
||||
if not turnstile_secret_key:
|
||||
logger.error(
|
||||
"Turnstile secret key missing. Set TURNSTILE_SECRET_KEY to enable verification."
|
||||
)
|
||||
return TurnstileVerifyResponse(
|
||||
success=False,
|
||||
error="CONFIGURATION_ERROR",
|
||||
challenge_timestamp=None,
|
||||
hostname=None,
|
||||
action=None,
|
||||
)
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
payload = {
|
||||
"secret": turnstile_secret_key,
|
||||
"response": request.token,
|
||||
}
|
||||
|
||||
if request.action:
|
||||
payload["action"] = request.action
|
||||
|
||||
logger.debug(f"Verifying Turnstile token with action: {request.action}")
|
||||
|
||||
async with session.post(
|
||||
turnstile_verify_url,
|
||||
data=payload,
|
||||
timeout=aiohttp.ClientTimeout(total=10),
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
logger.error(f"Turnstile API error: {error_text}")
|
||||
return TurnstileVerifyResponse(
|
||||
success=False,
|
||||
error=f"API_ERROR: {response.status}",
|
||||
challenge_timestamp=None,
|
||||
hostname=None,
|
||||
action=None,
|
||||
)
|
||||
|
||||
data = await response.json()
|
||||
logger.debug(f"Turnstile API response: {data}")
|
||||
|
||||
# Parse the response and return a structured object
|
||||
return TurnstileVerifyResponse(
|
||||
success=data.get("success", False),
|
||||
error=(
|
||||
data.get("error-codes", None)[0]
|
||||
if data.get("error-codes")
|
||||
else None
|
||||
),
|
||||
challenge_timestamp=data.get("challenge_timestamp"),
|
||||
hostname=data.get("hostname"),
|
||||
action=data.get("action"),
|
||||
)
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"Connection error to Turnstile API: {str(e)}")
|
||||
return TurnstileVerifyResponse(
|
||||
success=False,
|
||||
error=f"CONNECTION_ERROR: {str(e)}",
|
||||
challenge_timestamp=None,
|
||||
hostname=None,
|
||||
action=None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in Turnstile verification: {str(e)}")
|
||||
return TurnstileVerifyResponse(
|
||||
success=False,
|
||||
error=f"UNEXPECTED_ERROR: {str(e)}",
|
||||
challenge_timestamp=None,
|
||||
hostname=None,
|
||||
action=None,
|
||||
)
|
||||
@@ -1,32 +0,0 @@
|
||||
import fastapi
|
||||
import fastapi.testclient
|
||||
import pytest_mock
|
||||
|
||||
import backend.server.v2.turnstile.routes as turnstile_routes
|
||||
|
||||
app = fastapi.FastAPI()
|
||||
app.include_router(turnstile_routes.router)
|
||||
|
||||
client = fastapi.testclient.TestClient(app)
|
||||
|
||||
|
||||
def test_verify_turnstile_token_no_secret_key(mocker: pytest_mock.MockFixture) -> None:
|
||||
"""Test token verification without secret key configured"""
|
||||
# Mock the settings with no secret key
|
||||
mock_settings = mocker.patch("backend.server.v2.turnstile.routes.settings")
|
||||
mock_settings.secrets.turnstile_secret_key = None
|
||||
|
||||
request_data = {"token": "test_token", "action": "login"}
|
||||
response = client.post("/verify", json=request_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
response_data = response.json()
|
||||
assert response_data["success"] is False
|
||||
assert response_data["error"] == "CONFIGURATION_ERROR"
|
||||
|
||||
|
||||
def test_verify_turnstile_token_invalid_request() -> None:
|
||||
"""Test token verification with invalid request data"""
|
||||
# Missing token
|
||||
response = client.post("/verify", json={"action": "login"})
|
||||
assert response.status_code == 422
|
||||
@@ -537,16 +537,6 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
||||
description="The secret key to use for the unsubscribe user by token",
|
||||
)
|
||||
|
||||
# Cloudflare Turnstile credentials
|
||||
turnstile_secret_key: str = Field(
|
||||
default="",
|
||||
description="Cloudflare Turnstile backend secret key",
|
||||
)
|
||||
turnstile_verify_url: str = Field(
|
||||
default="https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||
description="Cloudflare Turnstile verify URL",
|
||||
)
|
||||
|
||||
# OAuth server credentials for integrations
|
||||
# --8<-- [start:OAuthServerCredentialsExample]
|
||||
github_client_id: str = Field(default="", description="GitHub OAuth client ID")
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false
|
||||
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=687ab1372f497809b131e06e
|
||||
|
||||
NEXT_PUBLIC_TURNSTILE=disabled
|
||||
NEXT_PUBLIC_REACT_QUERY_DEVTOOL=true
|
||||
|
||||
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-FH2XK2W4GN
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "10.0.0",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@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",
|
||||
|
||||
24
autogpt_platform/frontend/pnpm-lock.yaml
generated
24
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -14,9 +14,6 @@ importers:
|
||||
'@hookform/resolvers':
|
||||
specifier: 5.2.2
|
||||
version: 5.2.2(react-hook-form@7.66.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.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
@@ -1583,12 +1580,6 @@ 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:
|
||||
@@ -8919,11 +8910,6 @@ 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
|
||||
@@ -12447,7 +12433,7 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
@@ -12467,7 +12453,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
@@ -12482,14 +12468,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -12504,7 +12490,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
||||
@@ -2,17 +2,11 @@
|
||||
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import { verifyTurnstileToken } from "@/lib/turnstile";
|
||||
import { environment } from "@/services/environment";
|
||||
import { loginFormSchema } from "@/types/auth";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { shouldShowOnboarding } from "../../api/helpers";
|
||||
|
||||
export async function login(
|
||||
email: string,
|
||||
password: string,
|
||||
turnstileToken?: string,
|
||||
) {
|
||||
export async function login(email: string, password: string) {
|
||||
try {
|
||||
const parsed = loginFormSchema.safeParse({ email, password });
|
||||
|
||||
@@ -23,14 +17,6 @@ export async function login(
|
||||
};
|
||||
}
|
||||
|
||||
const captchaOk = await verifyTurnstileToken(turnstileToken ?? "", "login");
|
||||
if (!captchaOk && !environment.isVercelPreview()) {
|
||||
return {
|
||||
success: false,
|
||||
error: "CAPTCHA verification failed. Please try again.",
|
||||
};
|
||||
}
|
||||
|
||||
const supabase = await getServerSupabase();
|
||||
if (!supabase) {
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,6 @@ 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 { environment } from "@/services/environment";
|
||||
import { LoadingLogin } from "./components/LoadingLogin";
|
||||
import { useLoginPage } from "./useLoginPage";
|
||||
@@ -18,8 +17,6 @@ export default function LoginPage() {
|
||||
user,
|
||||
form,
|
||||
feedback,
|
||||
turnstile,
|
||||
captchaKey,
|
||||
isLoading,
|
||||
isGoogleLoading,
|
||||
isCloudEnv,
|
||||
@@ -85,20 +82,6 @@ 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}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
loading={isLoading}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useTurnstile } from "@/hooks/useTurnstile";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { environment } from "@/services/environment";
|
||||
import { loginFormSchema, LoginProvider } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import z from "zod";
|
||||
import { login as loginAction } from "./actions";
|
||||
@@ -13,20 +12,12 @@ import { login as loginAction } from "./actions";
|
||||
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 [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
|
||||
const isCloudEnv = environment.isCloud();
|
||||
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),
|
||||
@@ -36,25 +27,9 @@ export function useLoginPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const resetCaptcha = useCallback(() => {
|
||||
setCaptchaKey((k) => k + 1);
|
||||
turnstile.reset();
|
||||
}, [turnstile]);
|
||||
|
||||
async function handleProviderLogin(provider: LoginProvider) {
|
||||
setIsGoogleLoading(true);
|
||||
|
||||
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
|
||||
toast({
|
||||
title: "Please complete the CAPTCHA challenge.",
|
||||
variant: "info",
|
||||
});
|
||||
|
||||
setIsGoogleLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/provider", {
|
||||
method: "POST",
|
||||
@@ -70,7 +45,6 @@ export function useLoginPage() {
|
||||
const { url } = await response.json();
|
||||
if (url) window.location.href = url as string;
|
||||
} catch (error) {
|
||||
resetCaptcha();
|
||||
setIsGoogleLoading(false);
|
||||
setFeedback(
|
||||
error instanceof Error ? error.message : "Failed to start OAuth flow",
|
||||
@@ -81,17 +55,6 @@ export function useLoginPage() {
|
||||
async function handleLogin(data: z.infer<typeof loginFormSchema>) {
|
||||
setIsLoading(true);
|
||||
|
||||
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
|
||||
toast({
|
||||
title: "Please complete the CAPTCHA challenge.",
|
||||
variant: "info",
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.email.includes("@agpt.co")) {
|
||||
toast({
|
||||
title: "Please use Google SSO to login using an AutoGPT email.",
|
||||
@@ -99,16 +62,11 @@ export function useLoginPage() {
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await loginAction(
|
||||
data.email,
|
||||
data.password,
|
||||
turnstile.token ?? undefined,
|
||||
);
|
||||
const result = await loginAction(data.email, data.password);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Login failed");
|
||||
@@ -128,16 +86,12 @@ export function useLoginPage() {
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
turnstile.reset();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
feedback,
|
||||
turnstile,
|
||||
captchaKey,
|
||||
user,
|
||||
isLoading,
|
||||
isGoogleLoading,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use server";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import { verifyTurnstileToken } from "@/lib/turnstile";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export async function sendResetEmail(email: string, turnstileToken: string) {
|
||||
export async function sendResetEmail(email: string) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"sendResetEmail",
|
||||
{},
|
||||
@@ -17,15 +16,6 @@ export async function sendResetEmail(email: string, turnstileToken: string) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
// Verify Turnstile token if provided
|
||||
const success = await verifyTurnstileToken(
|
||||
turnstileToken,
|
||||
"reset-password",
|
||||
);
|
||||
if (!success) {
|
||||
return "CAPTCHA verification failed. Please try again.";
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${origin}/api/auth/callback/reset-password`,
|
||||
});
|
||||
@@ -38,7 +28,7 @@ export async function sendResetEmail(email: string, turnstileToken: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function changePassword(password: string, turnstileToken: string) {
|
||||
export async function changePassword(password: string) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"changePassword",
|
||||
{},
|
||||
@@ -49,15 +39,6 @@ export async function changePassword(password: string, turnstileToken: string) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
// Verify Turnstile token if provided
|
||||
const success = await verifyTurnstileToken(
|
||||
turnstileToken,
|
||||
"change_password",
|
||||
);
|
||||
if (!success) {
|
||||
return "CAPTCHA verification failed. Please try again.";
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.updateUser({ password });
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { AuthCard } from "@/components/auth/AuthCard";
|
||||
import Turnstile from "@/components/auth/Turnstile";
|
||||
import { Form, FormField } from "@/components/__legacy__/ui/form";
|
||||
import LoadingBox from "@/components/__legacy__/ui/loading";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useTurnstile } from "@/hooks/useTurnstile";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { changePasswordFormSchema, sendEmailFormSchema } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -23,8 +21,6 @@ function ResetPasswordContent() {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [sendEmailCaptchaKey, setSendEmailCaptchaKey] = useState(0);
|
||||
const [changePasswordCaptchaKey, setChangePasswordCaptchaKey] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const error = searchParams.get("error");
|
||||
@@ -41,18 +37,6 @@ function ResetPasswordContent() {
|
||||
}
|
||||
}, [searchParams, toast, router]);
|
||||
|
||||
const sendEmailTurnstile = useTurnstile({
|
||||
action: "reset_password",
|
||||
autoVerify: false,
|
||||
resetOnError: true,
|
||||
});
|
||||
|
||||
const changePasswordTurnstile = useTurnstile({
|
||||
action: "change_password",
|
||||
autoVerify: false,
|
||||
resetOnError: true,
|
||||
});
|
||||
|
||||
const sendEmailForm = useForm<z.infer<typeof sendEmailFormSchema>>({
|
||||
resolver: zodResolver(sendEmailFormSchema),
|
||||
defaultValues: {
|
||||
@@ -68,16 +52,6 @@ function ResetPasswordContent() {
|
||||
},
|
||||
});
|
||||
|
||||
const resetSendEmailCaptcha = useCallback(() => {
|
||||
setSendEmailCaptchaKey((k) => k + 1);
|
||||
sendEmailTurnstile.reset();
|
||||
}, [sendEmailTurnstile]);
|
||||
|
||||
const resetChangePasswordCaptcha = useCallback(() => {
|
||||
setChangePasswordCaptchaKey((k) => k + 1);
|
||||
changePasswordTurnstile.reset();
|
||||
}, [changePasswordTurnstile]);
|
||||
|
||||
const onSendEmail = useCallback(
|
||||
async (data: z.infer<typeof sendEmailFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
@@ -87,21 +61,7 @@ function ResetPasswordContent() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sendEmailTurnstile.verified) {
|
||||
toast({
|
||||
title: "CAPTCHA Required",
|
||||
description: "Please complete the CAPTCHA challenge.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsLoading(false);
|
||||
resetSendEmailCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await sendResetEmail(
|
||||
data.email,
|
||||
sendEmailTurnstile.token as string,
|
||||
);
|
||||
const error = await sendResetEmail(data.email);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
toast({
|
||||
@@ -109,7 +69,6 @@ function ResetPasswordContent() {
|
||||
description: error,
|
||||
variant: "destructive",
|
||||
});
|
||||
resetSendEmailCaptcha();
|
||||
return;
|
||||
}
|
||||
setDisabled(true);
|
||||
@@ -120,7 +79,7 @@ function ResetPasswordContent() {
|
||||
variant: "default",
|
||||
});
|
||||
},
|
||||
[sendEmailForm, sendEmailTurnstile, resetSendEmailCaptcha, toast],
|
||||
[sendEmailForm, toast],
|
||||
);
|
||||
|
||||
const onChangePassword = useCallback(
|
||||
@@ -132,21 +91,7 @@ function ResetPasswordContent() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changePasswordTurnstile.verified) {
|
||||
toast({
|
||||
title: "CAPTCHA Required",
|
||||
description: "Please complete the CAPTCHA challenge.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsLoading(false);
|
||||
resetChangePasswordCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await changePassword(
|
||||
data.password,
|
||||
changePasswordTurnstile.token as string,
|
||||
);
|
||||
const error = await changePassword(data.password);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
toast({
|
||||
@@ -154,7 +99,6 @@ function ResetPasswordContent() {
|
||||
description: error,
|
||||
variant: "destructive",
|
||||
});
|
||||
resetChangePasswordCaptcha();
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
@@ -163,12 +107,7 @@ function ResetPasswordContent() {
|
||||
variant: "default",
|
||||
});
|
||||
},
|
||||
[
|
||||
changePasswordForm,
|
||||
changePasswordTurnstile,
|
||||
resetChangePasswordCaptcha,
|
||||
toast,
|
||||
],
|
||||
[changePasswordForm, toast],
|
||||
);
|
||||
|
||||
if (isUserLoading) {
|
||||
@@ -226,18 +165,6 @@ function ResetPasswordContent() {
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Turnstile CAPTCHA Component for password change */}
|
||||
<Turnstile
|
||||
key={changePasswordCaptchaKey}
|
||||
siteKey={changePasswordTurnstile.siteKey}
|
||||
onVerify={changePasswordTurnstile.handleVerify}
|
||||
onExpire={changePasswordTurnstile.handleExpire}
|
||||
onError={changePasswordTurnstile.handleError}
|
||||
setWidgetId={changePasswordTurnstile.setWidgetId}
|
||||
action="change_password"
|
||||
shouldRender={changePasswordTurnstile.shouldRender}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
loading={isLoading}
|
||||
@@ -270,20 +197,6 @@ function ResetPasswordContent() {
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Turnstile CAPTCHA Component for reset email */}
|
||||
{!sendEmailTurnstile.verified ? (
|
||||
<Turnstile
|
||||
key={sendEmailCaptchaKey}
|
||||
siteKey={sendEmailTurnstile.siteKey}
|
||||
onVerify={sendEmailTurnstile.handleVerify}
|
||||
onExpire={sendEmailTurnstile.handleExpire}
|
||||
onError={sendEmailTurnstile.handleError}
|
||||
setWidgetId={sendEmailTurnstile.setWidgetId}
|
||||
action="reset_password"
|
||||
shouldRender={sendEmailTurnstile.shouldRender}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
loading={isLoading}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import { verifyTurnstileToken } from "@/lib/turnstile";
|
||||
import { environment } from "@/services/environment";
|
||||
import { signupFormSchema } from "@/types/auth";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { isWaitlistError, logWaitlistError } from "../../api/auth/utils";
|
||||
@@ -13,7 +11,6 @@ export async function signup(
|
||||
password: string,
|
||||
confirmPassword: string,
|
||||
agreeToTerms: boolean,
|
||||
turnstileToken?: string,
|
||||
) {
|
||||
try {
|
||||
const parsed = signupFormSchema.safeParse({
|
||||
@@ -30,18 +27,6 @@ export async function signup(
|
||||
};
|
||||
}
|
||||
|
||||
const captchaOk = await verifyTurnstileToken(
|
||||
turnstileToken ?? "",
|
||||
"signup",
|
||||
);
|
||||
|
||||
if (!captchaOk && !environment.isVercelPreview()) {
|
||||
return {
|
||||
success: false,
|
||||
error: "CAPTCHA verification failed. Please try again.",
|
||||
};
|
||||
}
|
||||
|
||||
const supabase = await getServerSupabase();
|
||||
if (!supabase) {
|
||||
return {
|
||||
|
||||
@@ -16,7 +16,6 @@ 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 { environment } from "@/services/environment";
|
||||
import { WarningOctagonIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import { LoadingSignup } from "./components/LoadingSignup";
|
||||
@@ -27,8 +26,6 @@ export default function SignupPage() {
|
||||
const {
|
||||
form,
|
||||
feedback,
|
||||
turnstile,
|
||||
captchaKey,
|
||||
isLoggedIn,
|
||||
isLoading,
|
||||
isGoogleLoading,
|
||||
@@ -162,20 +159,6 @@ export default function SignupPage() {
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Turnstile CAPTCHA Component */}
|
||||
{isCloudEnv && !turnstile.verified ? (
|
||||
<Turnstile
|
||||
key={captchaKey}
|
||||
siteKey={turnstile.siteKey}
|
||||
onVerify={turnstile.handleVerify}
|
||||
onExpire={turnstile.handleExpire}
|
||||
onError={turnstile.handleError}
|
||||
setWidgetId={turnstile.setWidgetId}
|
||||
action="signup"
|
||||
shouldRender={turnstile.shouldRender}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
loading={isLoading}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useTurnstile } from "@/hooks/useTurnstile";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { environment } from "@/services/environment";
|
||||
import { LoginProvider, signupFormSchema } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import z from "zod";
|
||||
import { signup as signupAction } from "./actions";
|
||||
@@ -13,25 +12,12 @@ import { signup as signupAction } from "./actions";
|
||||
export function useSignupPage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const [captchaKey, setCaptchaKey] = useState(0);
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
|
||||
const isCloudEnv = environment.isCloud();
|
||||
const isVercelPreview = process.env.NEXT_PUBLIC_VERCEL_ENV === "preview";
|
||||
|
||||
const turnstile = useTurnstile({
|
||||
action: "signup",
|
||||
autoVerify: false,
|
||||
resetOnError: true,
|
||||
});
|
||||
|
||||
const resetCaptcha = useCallback(() => {
|
||||
setCaptchaKey((k) => k + 1);
|
||||
turnstile.reset();
|
||||
}, [turnstile]);
|
||||
|
||||
const form = useForm<z.infer<typeof signupFormSchema>>({
|
||||
resolver: zodResolver(signupFormSchema),
|
||||
@@ -46,16 +32,6 @@ export function useSignupPage() {
|
||||
async function handleProviderSignup(provider: LoginProvider) {
|
||||
setIsGoogleLoading(true);
|
||||
|
||||
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
|
||||
toast({
|
||||
title: "Please complete the CAPTCHA challenge.",
|
||||
variant: "default",
|
||||
});
|
||||
setIsGoogleLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/provider", {
|
||||
method: "POST",
|
||||
@@ -78,7 +54,6 @@ export function useSignupPage() {
|
||||
if (url) window.location.href = url as string;
|
||||
} catch (error) {
|
||||
setIsGoogleLoading(false);
|
||||
resetCaptcha();
|
||||
toast({
|
||||
title:
|
||||
error instanceof Error ? error.message : "Failed to start OAuth flow",
|
||||
@@ -90,16 +65,6 @@ export function useSignupPage() {
|
||||
async function handleSignup(data: z.infer<typeof signupFormSchema>) {
|
||||
setIsLoading(true);
|
||||
|
||||
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
|
||||
toast({
|
||||
title: "Please complete the CAPTCHA challenge.",
|
||||
variant: "default",
|
||||
});
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.email.includes("@agpt.co")) {
|
||||
toast({
|
||||
title:
|
||||
@@ -108,7 +73,6 @@ export function useSignupPage() {
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,7 +82,6 @@ export function useSignupPage() {
|
||||
data.password,
|
||||
data.confirmPassword,
|
||||
data.agreeToTerms,
|
||||
turnstile.token ?? undefined,
|
||||
);
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -126,7 +89,6 @@ export function useSignupPage() {
|
||||
if (!result.success) {
|
||||
if (result.error === "user_already_exists") {
|
||||
setFeedback("User with this email already exists");
|
||||
turnstile.reset();
|
||||
return;
|
||||
}
|
||||
if (result.error === "not_allowed") {
|
||||
@@ -138,8 +100,6 @@ export function useSignupPage() {
|
||||
title: result.error || "Signup failed",
|
||||
variant: "destructive",
|
||||
});
|
||||
resetCaptcha();
|
||||
turnstile.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,16 +114,12 @@ export function useSignupPage() {
|
||||
: "Unexpected error during signup",
|
||||
variant: "destructive",
|
||||
});
|
||||
resetCaptcha();
|
||||
turnstile.reset();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
feedback,
|
||||
turnstile,
|
||||
captchaKey,
|
||||
isLoggedIn: !!user,
|
||||
isLoading,
|
||||
isGoogleLoading,
|
||||
|
||||
@@ -4715,44 +4715,6 @@
|
||||
"security": [{ "HTTPBearerJWT": [] }]
|
||||
}
|
||||
},
|
||||
"/api/turnstile/verify": {
|
||||
"post": {
|
||||
"tags": ["v2", "turnstile"],
|
||||
"summary": "Verify Turnstile Token",
|
||||
"description": "Verify a Cloudflare Turnstile token.\nThis endpoint verifies a token returned by the Cloudflare Turnstile challenge\non the client side. It returns whether the verification was successful.",
|
||||
"operationId": "postV2Verify turnstile token",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TurnstileVerifyRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TurnstileVerifyResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/email/unsubscribe": {
|
||||
"post": {
|
||||
"tags": ["v1", "email"],
|
||||
@@ -9082,57 +9044,6 @@
|
||||
"required": ["name", "graph_id", "graph_version", "trigger_config"],
|
||||
"title": "TriggeredPresetSetupRequest"
|
||||
},
|
||||
"TurnstileVerifyRequest": {
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"title": "Token",
|
||||
"description": "The Turnstile token to verify"
|
||||
},
|
||||
"action": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Action",
|
||||
"description": "The action that the user is attempting to perform"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["token"],
|
||||
"title": "TurnstileVerifyRequest",
|
||||
"description": "Request model for verifying a Turnstile token."
|
||||
},
|
||||
"TurnstileVerifyResponse": {
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"title": "Success",
|
||||
"description": "Whether the token verification was successful"
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Error",
|
||||
"description": "Error message if verification failed"
|
||||
},
|
||||
"challenge_timestamp": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Challenge Timestamp",
|
||||
"description": "Timestamp of the challenge (ISO format)"
|
||||
},
|
||||
"hostname": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Hostname",
|
||||
"description": "Hostname of the site where the challenge was solved"
|
||||
},
|
||||
"action": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Action",
|
||||
"description": "The action associated with this verification"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["success"],
|
||||
"title": "TurnstileVerifyResponse",
|
||||
"description": "Response model for the Turnstile verification endpoint."
|
||||
},
|
||||
"UpdatePermissionsRequest": {
|
||||
"properties": {
|
||||
"permissions": {
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { environment } from "@/services/environment";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface TurnstileProps {
|
||||
siteKey: string;
|
||||
onVerify: (token: string) => void;
|
||||
onExpire?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
action?: string;
|
||||
className?: string;
|
||||
id?: string;
|
||||
shouldRender?: boolean;
|
||||
setWidgetId?: (id: string | null) => void;
|
||||
}
|
||||
|
||||
export function Turnstile({
|
||||
siteKey,
|
||||
onVerify,
|
||||
onExpire,
|
||||
onError,
|
||||
action,
|
||||
className,
|
||||
id = "cf-turnstile",
|
||||
shouldRender = true,
|
||||
setWidgetId,
|
||||
}: TurnstileProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const widgetIdRef = useRef<string | null>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
// Load the Turnstile script
|
||||
useEffect(() => {
|
||||
if (environment.isServerSide() || !shouldRender) return;
|
||||
|
||||
// Skip if already loaded
|
||||
if (window.turnstile) {
|
||||
setLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const handleLoad: EventListener = () => {
|
||||
setLoaded(true);
|
||||
};
|
||||
const handleError: EventListener = () => {
|
||||
onError?.(new Error("Failed to load Turnstile script"));
|
||||
};
|
||||
|
||||
script.addEventListener("load", handleLoad);
|
||||
script.addEventListener("error", handleError);
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
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)
|
||||
return;
|
||||
|
||||
// Reset any existing widget
|
||||
if (widgetIdRef.current && window.turnstile) {
|
||||
try {
|
||||
window.turnstile.reset(widgetIdRef.current);
|
||||
} catch (err) {
|
||||
console.warn("Failed to reset existing Turnstile widget:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
// Notify the hook about the widget ID
|
||||
setWidgetId?.(widgetIdRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (widgetIdRef.current && window.turnstile) {
|
||||
try {
|
||||
window.turnstile.remove(widgetIdRef.current);
|
||||
} catch (err) {
|
||||
console.warn("Failed to remove Turnstile widget:", err);
|
||||
}
|
||||
setWidgetId?.(null);
|
||||
widgetIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [
|
||||
loaded,
|
||||
siteKey,
|
||||
onVerify,
|
||||
onExpire,
|
||||
onError,
|
||||
action,
|
||||
shouldRender,
|
||||
setWidgetId,
|
||||
]);
|
||||
|
||||
// Method to reset the widget manually
|
||||
useEffect(() => {
|
||||
if (loaded && widgetIdRef.current && window.turnstile && shouldRender) {
|
||||
window.turnstile.reset(widgetIdRef.current);
|
||||
}
|
||||
}, [loaded, shouldRender]);
|
||||
|
||||
// If shouldRender is false, don't render anything
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
ref={containerRef}
|
||||
className={cn("my-4 flex items-center justify-center", className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Add TypeScript interface to Window to include turnstile property
|
||||
declare global {
|
||||
interface Window {
|
||||
turnstile?: {
|
||||
render: (
|
||||
container: HTMLElement,
|
||||
options: {
|
||||
sitekey: string;
|
||||
callback: (token: string) => void;
|
||||
"expired-callback"?: () => void;
|
||||
"error-callback"?: () => void;
|
||||
action?: string;
|
||||
},
|
||||
) => string;
|
||||
reset: (widgetId: string) => void;
|
||||
remove: (widgetId: string) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Turnstile;
|
||||
@@ -1,182 +0,0 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { verifyTurnstileToken } from "@/lib/turnstile";
|
||||
import { environment } from "@/services/environment";
|
||||
|
||||
interface UseTurnstileOptions {
|
||||
action?: string;
|
||||
autoVerify?: boolean;
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
resetOnError?: boolean;
|
||||
}
|
||||
|
||||
interface UseTurnstileResult {
|
||||
token: string | null;
|
||||
verifying: boolean;
|
||||
verified: boolean;
|
||||
error: Error | null;
|
||||
handleVerify: (token: string) => Promise<boolean>;
|
||||
handleExpire: () => void;
|
||||
handleError: (error: Error) => void;
|
||||
reset: () => void;
|
||||
siteKey: string;
|
||||
shouldRender: boolean;
|
||||
setWidgetId: (id: string | null) => void;
|
||||
}
|
||||
|
||||
const TURNSTILE_SITE_KEY =
|
||||
process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY || "";
|
||||
|
||||
/**
|
||||
* Custom hook for managing Turnstile state in forms
|
||||
*/
|
||||
export function useTurnstile({
|
||||
action,
|
||||
autoVerify = true,
|
||||
onSuccess,
|
||||
onError,
|
||||
resetOnError = true,
|
||||
}: UseTurnstileOptions = {}): UseTurnstileResult {
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [verifying, setVerifying] = useState(false);
|
||||
const [verified, setVerified] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [shouldRender, setShouldRender] = useState(false);
|
||||
const [widgetId, setWidgetId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const isCloud = environment.isCloud();
|
||||
const hasTurnstileKey = !!TURNSTILE_SITE_KEY;
|
||||
const turnstileDisabled = process.env.NEXT_PUBLIC_TURNSTILE !== "enabled";
|
||||
|
||||
// Only render Turnstile in cloud environment if not explicitly disabled
|
||||
setShouldRender(isCloud && hasTurnstileKey && !turnstileDisabled);
|
||||
|
||||
// Skip verification if disabled, in local development, or no key
|
||||
if (turnstileDisabled || !isCloud || !hasTurnstileKey) {
|
||||
setVerified(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (token && !autoVerify && shouldRender) {
|
||||
setVerified(true);
|
||||
}
|
||||
}, [token, autoVerify, shouldRender]);
|
||||
|
||||
const setWidgetIdCallback = useCallback((id: string | null) => {
|
||||
setWidgetId(id);
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
// Always reset the state when reset is called, regardless of shouldRender
|
||||
// This ensures users can retry CAPTCHA after failed attempts
|
||||
setToken(null);
|
||||
setVerified(false);
|
||||
setVerifying(false);
|
||||
setError(null);
|
||||
|
||||
// Only reset the actual Turnstile widget if it exists and shouldRender is true
|
||||
if (
|
||||
shouldRender &&
|
||||
!environment.isServerSide() &&
|
||||
window.turnstile &&
|
||||
widgetId
|
||||
) {
|
||||
try {
|
||||
window.turnstile.reset(widgetId);
|
||||
} catch (err) {
|
||||
console.warn("Failed to reset Turnstile widget:", err);
|
||||
}
|
||||
}
|
||||
}, [shouldRender, widgetId]);
|
||||
|
||||
const handleVerify = useCallback(
|
||||
async (newToken: string) => {
|
||||
if (!shouldRender) {
|
||||
return true;
|
||||
}
|
||||
|
||||
setToken(newToken);
|
||||
setError(null);
|
||||
|
||||
if (autoVerify) {
|
||||
setVerifying(true);
|
||||
|
||||
try {
|
||||
const success = await verifyTurnstileToken(newToken, action);
|
||||
setVerified(success);
|
||||
|
||||
if (success && onSuccess) {
|
||||
onSuccess();
|
||||
} else if (!success) {
|
||||
const newError = new Error("Turnstile verification failed");
|
||||
setError(newError);
|
||||
if (onError) onError(newError);
|
||||
if (resetOnError) {
|
||||
setToken(null);
|
||||
setVerified(false);
|
||||
}
|
||||
}
|
||||
|
||||
setVerifying(false);
|
||||
return success;
|
||||
} catch (err) {
|
||||
const newError =
|
||||
err instanceof Error
|
||||
? err
|
||||
: new Error("Unknown error during verification");
|
||||
setError(newError);
|
||||
if (resetOnError) {
|
||||
setToken(null);
|
||||
setVerified(false);
|
||||
}
|
||||
setVerifying(false);
|
||||
if (onError) onError(newError);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
setVerified(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[action, autoVerify, onSuccess, onError, resetOnError, shouldRender],
|
||||
);
|
||||
|
||||
const handleExpire = useCallback(() => {
|
||||
if (shouldRender) {
|
||||
setToken(null);
|
||||
setVerified(false);
|
||||
setError(null);
|
||||
}
|
||||
}, [shouldRender]);
|
||||
|
||||
const handleError = useCallback(
|
||||
(err: Error) => {
|
||||
if (shouldRender) {
|
||||
setError(err);
|
||||
if (resetOnError) {
|
||||
setToken(null);
|
||||
setVerified(false);
|
||||
}
|
||||
if (onError) onError(err);
|
||||
}
|
||||
},
|
||||
[onError, shouldRender, resetOnError],
|
||||
);
|
||||
|
||||
return {
|
||||
token,
|
||||
verifying,
|
||||
verified,
|
||||
error,
|
||||
handleVerify,
|
||||
handleExpire,
|
||||
handleError,
|
||||
reset,
|
||||
siteKey: TURNSTILE_SITE_KEY,
|
||||
shouldRender,
|
||||
setWidgetId: setWidgetIdCallback,
|
||||
};
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { environment } from "@/services/environment";
|
||||
|
||||
export async function verifyTurnstileToken(
|
||||
token: string,
|
||||
action?: string,
|
||||
): Promise<boolean> {
|
||||
if (!environment.isCAPTCHAEnabled() || environment.isLocal()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${environment.getAGPTServerApiUrl()}/turnstile/verify`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
action,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Turnstile verification failed:", await response.text());
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.success === true;
|
||||
} catch (error) {
|
||||
console.error("Error verifying Turnstile token:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -88,10 +88,6 @@ function isVercelPreview() {
|
||||
return process.env.VERCEL_ENV === "preview";
|
||||
}
|
||||
|
||||
function isCAPTCHAEnabled() {
|
||||
return process.env.NEXT_PUBLIC_TURNSTILE === "enabled";
|
||||
}
|
||||
|
||||
function areFeatureFlagsEnabled() {
|
||||
return process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "enabled";
|
||||
}
|
||||
@@ -115,6 +111,5 @@ export const environment = {
|
||||
isCloud,
|
||||
isLocal,
|
||||
isVercelPreview,
|
||||
isCAPTCHAEnabled,
|
||||
areFeatureFlagsEnabled,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user