mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-09-07 08:26:07 -05:00
Add Cloudflare Turnstile for login/signup
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import React, { useActionState, useEffect} from "react";
|
||||
import Link from "next/link";
|
||||
import { FiEye, FiEyeOff } from "react-icons/fi";
|
||||
import { Turnstile } from "next-turnstile";
|
||||
import { AuthActionState, login } from "@/app/login/actions";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
@@ -13,6 +14,9 @@ export default function LoginForm() {
|
||||
const [email, setEmail] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
const [turnstileToken, setTurnstileToken] = React.useState<string | undefined>(undefined);
|
||||
const [turnstileError, setTurnstileError] = React.useState<string | null>(null);
|
||||
const [turnstileKey, setTurnstileKey] = React.useState(0);
|
||||
const searchParams = useSearchParams();
|
||||
const urlError = searchParams.get("error");
|
||||
const [state, formAction] = useActionState<AuthActionState, FormData>(login, null);
|
||||
@@ -26,6 +30,16 @@ export default function LoginForm() {
|
||||
const passwordValid = password.length > 1;
|
||||
const isValid = emailValid && passwordValid;
|
||||
|
||||
// Reset Turnstile token and widget on error to allow retry
|
||||
useEffect(() => {
|
||||
if (state?.error && state.error !== null) {
|
||||
setTurnstileToken(undefined);
|
||||
setTurnstileError(null);
|
||||
// Force Turnstile widget to reset by changing key
|
||||
setTurnstileKey((prev) => prev + 1);
|
||||
}
|
||||
}, [state?.error]);
|
||||
|
||||
// Update context and immediately redirect after successful login
|
||||
useEffect(() => {
|
||||
if (state && state.error === null && !navigatedRef.current) {
|
||||
@@ -55,6 +69,11 @@ export default function LoginForm() {
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
{turnstileError && (
|
||||
<div className="rounded-md bg-red-500/10 ring-1 ring-red-600/40 px-3 py-2 text-sm text-red-300">
|
||||
{turnstileError}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="email" className="text-sm text-foreground/80">Email</label>
|
||||
<input
|
||||
@@ -117,10 +136,27 @@ export default function LoginForm() {
|
||||
) : (
|
||||
<div className="h-3" />
|
||||
)}
|
||||
<Turnstile
|
||||
key={turnstileKey}
|
||||
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
setTurnstileError(null);
|
||||
}}
|
||||
onError={(error) => {
|
||||
setTurnstileToken(undefined);
|
||||
setTurnstileError("Verification failed. Please try again.");
|
||||
console.error("Turnstile error:", error);
|
||||
}}
|
||||
onExpire={() => {
|
||||
setTurnstileToken(undefined);
|
||||
}}
|
||||
theme="auto"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
formAction={formAction}
|
||||
disabled={!isValid}
|
||||
disabled={!isValid || !turnstileToken}
|
||||
className="shine-wrap btn-premium h-11 min-w-[7.5rem] text-sm font-semibold dark:disabled:opacity-70 disabled:cursor-not-allowed disabled:[box-shadow:0_0_0_1px_var(--border)]"
|
||||
>
|
||||
<span>Log in</span>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import React, { useActionState, useEffect } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { FiEye, FiEyeOff } from "react-icons/fi";
|
||||
import { Turnstile } from "next-turnstile";
|
||||
import { AuthActionState, signup } from "@/app/signup/actions";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { validateEmail, validatePassword } from "@/utils/auth";
|
||||
@@ -17,11 +18,24 @@ export default function SignupForm() {
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
const [emailError, setEmailError] = React.useState<string | null>(null);
|
||||
const [passwordError, setPasswordError] = React.useState<string | null>(null);
|
||||
const [turnstileToken, setTurnstileToken] = React.useState<string | undefined>(undefined);
|
||||
const [turnstileError, setTurnstileError] = React.useState<string | null>(null);
|
||||
const [turnstileKey, setTurnstileKey] = React.useState(0);
|
||||
|
||||
const [state, formAction, isPending] = useActionState<AuthActionState, FormData>(signup, { error: null });
|
||||
const passwordsMatch = password === confirm;
|
||||
const isValid = !emailError && !passwordError && passwordsMatch;
|
||||
|
||||
// Reset Turnstile token and widget on error to allow retry
|
||||
useEffect(() => {
|
||||
if (state?.error && !isPending) {
|
||||
setTurnstileToken(undefined);
|
||||
setTurnstileError(null);
|
||||
// Force Turnstile widget to reset by changing key
|
||||
setTurnstileKey((prev) => prev + 1);
|
||||
}
|
||||
}, [state?.error, isPending]);
|
||||
|
||||
useEffect(() => {
|
||||
const { error } = validateEmail(email);
|
||||
setEmailError(error);
|
||||
@@ -52,6 +66,11 @@ export default function SignupForm() {
|
||||
{state?.error}
|
||||
</div>
|
||||
)}
|
||||
{turnstileError && (
|
||||
<div className="rounded-md bg-red-500/10 ring-1 ring-red-600/40 px-3 py-2 text-sm text-red-300">
|
||||
{turnstileError}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="email" className="text-sm text-foreground/80">Email</label>
|
||||
<input
|
||||
@@ -138,10 +157,27 @@ export default function SignupForm() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Turnstile
|
||||
key={turnstileKey}
|
||||
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
setTurnstileError(null);
|
||||
}}
|
||||
onError={(error) => {
|
||||
setTurnstileToken(undefined);
|
||||
setTurnstileError("Verification failed. Please try again.");
|
||||
console.error("Turnstile error:", error);
|
||||
}}
|
||||
onExpire={() => {
|
||||
setTurnstileToken(undefined);
|
||||
}}
|
||||
theme="auto"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
formAction={formAction}
|
||||
disabled={!isValid || isPending}
|
||||
disabled={!isValid || isPending || !turnstileToken}
|
||||
className="shine-wrap btn-premium h-11 min-w-[7.5rem] text-sm font-semibold hover:cursor-pointer dark:disabled:opacity-70 disabled:cursor-not-allowed disabled:[box-shadow:0_0_0_1px_var(--border)]"
|
||||
>
|
||||
<span>Sign up</span>
|
||||
|
||||
Reference in New Issue
Block a user