Skip to content

Commit 90dadef

Browse files
committed
feat: recaptcha
1 parent b5a3ae7 commit 90dadef

File tree

15 files changed

+81
-59
lines changed

15 files changed

+81
-59
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"@testing-library/dom": "10.4.0",
5252
"@testing-library/jest-dom": "6.6.3",
5353
"@testing-library/react": "16.2.0",
54+
"@types/cloudflare-turnstile": "0.2.2",
5455
"@types/react": "18.3.18",
5556
"@typescript-eslint/eslint-plugin": "8.27.0",
5657
"@typescript-eslint/parser": "8.27.0",

src/components/auth/AuthLayout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from 'react';
2-
32
interface AuthLayoutProps {
43
children: React.ReactNode;
54
title: string;
@@ -16,8 +15,10 @@ export const AuthLayout: React.FC<AuthLayoutProps> = ({ children, title, subtitl
1615
</div>
1716
<div className="mt-8 bg-gray-900/90 py-8 px-4 shadow-lg rounded-lg border border-gray-800">
1817
{children}
18+
<div id="cf-captcha-container" className="hidden"></div>
1919
</div>
2020
</div>
21+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback"></script>
2122
</div>
2223
);
2324
};

src/components/auth/LoginForm.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import AuthInput from './AuthInput';
66
import { loginSchema } from '../../types/auth';
77
import type { LoginFormData } from '../../types/auth';
88
import { useAuth } from '../../hooks/useAuth';
9+
import { useCaptcha } from '../../hooks/useCaptcha';
910

1011
interface LoginFormProps {
1112
cfCaptchaSiteKey: string;
1213
}
1314

1415
export const LoginForm: React.FC<LoginFormProps> = ({ cfCaptchaSiteKey }) => {
15-
const { login, error: apiError, isLoading } = useAuth(cfCaptchaSiteKey);
16+
const { login, error: apiError, isLoading } = useAuth();
17+
const { isCaptchaVerified } = useCaptcha(cfCaptchaSiteKey);
18+
1619
const {
1720
register,
1821
handleSubmit,
@@ -23,6 +26,9 @@ export const LoginForm: React.FC<LoginFormProps> = ({ cfCaptchaSiteKey }) => {
2326

2427
const onSubmit = async (data: LoginFormData) => {
2528
try {
29+
if (!isCaptchaVerified) {
30+
throw new Error('Captcha verification failed');
31+
}
2632
await login(data);
2733
window.location.href = '/';
2834
} catch (error) {
@@ -58,10 +64,6 @@ export const LoginForm: React.FC<LoginFormProps> = ({ cfCaptchaSiteKey }) => {
5864
{...register('password')}
5965
/>
6066

61-
<div className="flex flex-row justify-center">
62-
<div className="cf-turnstile" data-sitekey={cfCaptchaSiteKey} data-size="normal"></div>
63-
</div>
64-
6567
<div className="flex items-center justify-between">
6668
<div className="text-sm">
6769
<a

src/components/auth/ResetPasswordForm.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import AuthInput from './AuthInput';
66
import { resetPasswordSchema } from '../../types/auth';
77
import type { ResetPasswordFormData } from '../../types/auth';
88
import { useAuth } from '../../hooks/useAuth';
9+
import { useCaptcha } from '../../hooks/useCaptcha';
910

1011
interface ResetPasswordFormProps {
1112
cfCaptchaSiteKey: string;
1213
}
1314

1415
export const ResetPasswordForm: React.FC<ResetPasswordFormProps> = ({ cfCaptchaSiteKey }) => {
15-
const { resetPassword, error: apiError, isLoading } = useAuth(cfCaptchaSiteKey);
16+
const { resetPassword, error: apiError, isLoading } = useAuth();
17+
const { isCaptchaVerified } = useCaptcha(cfCaptchaSiteKey);
1618
const [error, setError] = useState<string | null>(null);
1719
const {
1820
register,
@@ -33,6 +35,9 @@ export const ResetPasswordForm: React.FC<ResetPasswordFormProps> = ({ cfCaptchaS
3335

3436
const onSubmit = async (data: ResetPasswordFormData) => {
3537
try {
38+
if (!isCaptchaVerified) {
39+
throw new Error('Captcha verification failed');
40+
}
3641
await resetPassword(data);
3742
} catch (error) {
3843
console.error(error);
@@ -80,10 +85,6 @@ export const ResetPasswordForm: React.FC<ResetPasswordFormProps> = ({ cfCaptchaS
8085
{...register('email')}
8186
/>
8287

83-
<div className="flex flex-row justify-center">
84-
<div className="cf-turnstile" data-sitekey={cfCaptchaSiteKey} data-size="normal"></div>
85-
</div>
86-
8788
<button
8889
type="submit"
8990
disabled={isLoading}

src/components/auth/SignupForm.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import AuthInput from './AuthInput';
66
import { signupSchema } from '../../types/auth';
77
import type { SignupFormData } from '../../types/auth';
88
import { useAuth } from '../../hooks/useAuth';
9+
import { useCaptcha } from '../../hooks/useCaptcha';
910

1011
interface SignupFormProps {
1112
cfCaptchaSiteKey: string;
1213
}
1314

1415
export const SignupForm: React.FC<SignupFormProps> = ({ cfCaptchaSiteKey }) => {
15-
const { signup, error: apiError, isLoading } = useAuth(cfCaptchaSiteKey);
16+
const { signup, error: apiError, isLoading } = useAuth();
17+
const { isCaptchaVerified } = useCaptcha(cfCaptchaSiteKey);
1618
const {
1719
register,
1820
handleSubmit,
@@ -23,6 +25,9 @@ export const SignupForm: React.FC<SignupFormProps> = ({ cfCaptchaSiteKey }) => {
2325

2426
const onSubmit = async (data: SignupFormData) => {
2527
try {
28+
if (!isCaptchaVerified) {
29+
throw new Error('Captcha verification failed');
30+
}
2631
await signup(data);
2732
} catch (error) {
2833
console.error(error);
@@ -86,10 +91,6 @@ export const SignupForm: React.FC<SignupFormProps> = ({ cfCaptchaSiteKey }) => {
8691
{...register('confirmPassword')}
8792
/>
8893

89-
<div className="flex flex-row justify-center">
90-
<div className="cf-turnstile" data-sitekey={cfCaptchaSiteKey} data-size="normal"></div>
91-
</div>
92-
9394
<div className="space-y-2">
9495
<div className="flex items-start">
9596
<div className="flex items-center h-5">

src/components/auth/UpdatePasswordResetForm.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { updatePasswordSchema } from '../../types/auth';
77
import type { UpdatePasswordFormData } from '../../types/auth';
88
import { useAuth } from '../../hooks/useAuth';
99
import { useTokenHashVerification } from '../../hooks/useTokenHashVerification';
10+
import { useCaptcha } from '../../hooks/useCaptcha';
1011

1112
interface UpdatePasswordResetFormProps {
1213
cfCaptchaSiteKey: string;
@@ -15,8 +16,9 @@ interface UpdatePasswordResetFormProps {
1516
export const UpdatePasswordResetForm: React.FC<UpdatePasswordResetFormProps> = ({
1617
cfCaptchaSiteKey,
1718
}) => {
18-
const { updatePassword, error: apiError, isLoading } = useAuth(cfCaptchaSiteKey);
19+
const { updatePassword, error: apiError, isLoading } = useAuth();
1920
const { verificationError, isVerified } = useTokenHashVerification();
21+
const { isCaptchaVerified } = useCaptcha(cfCaptchaSiteKey);
2022

2123
const {
2224
register,
@@ -28,6 +30,9 @@ export const UpdatePasswordResetForm: React.FC<UpdatePasswordResetFormProps> = (
2830

2931
const onSubmit = async (data: UpdatePasswordFormData) => {
3032
try {
33+
if (!isCaptchaVerified) {
34+
throw new Error('Captcha verification failed');
35+
}
3136
await updatePassword(data);
3237
window.location.href = '/auth/login?message=password-updated';
3338
} catch (error) {

src/env.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import type { Database } from './db/database.types.ts';
55
import type { Env } from './features/featureFlags';
66

77
declare global {
8+
interface Window {
9+
onloadTurnstileCallback: () => void;
10+
}
811
namespace App {
912
interface Locals {
1013
supabase: SupabaseClient<Database>;

src/hooks/useAuth.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,22 @@
1-
import axios from 'axios';
2-
import { useEffect, useState } from 'react';
1+
import { useState } from 'react';
32
import type {
43
LoginFormData,
54
SignupFormData,
65
ResetPasswordFormData,
76
UpdatePasswordFormData,
87
} from '../types/auth';
98
import { authService } from '../services/auth';
10-
import { type CaptchaResponse } from '../services/captcha';
119

1210
interface User {
1311
id: string;
1412
email: string | null;
1513
}
1614

17-
export const useAuth = (cfCaptchaSiteKey: string) => {
15+
export const useAuth = () => {
1816
const [error, setError] = useState<string | null>(null);
1917
const [isLoading, setIsLoading] = useState(false);
20-
const [isCaptchaVerified, setIsCaptchaVerified] = useState(false);
21-
22-
useEffect(() => {
23-
const verifyCaptcha = async () => {
24-
try {
25-
const captchaResult = await axios.post<CaptchaResponse>('/api/captcha/verify', {
26-
captchaToken: cfCaptchaSiteKey,
27-
});
28-
setIsCaptchaVerified(captchaResult.data.success);
29-
} catch (error) {
30-
console.error('Captcha verification error:', error);
31-
setError('Captcha verification failed');
32-
}
33-
};
34-
35-
verifyCaptcha();
36-
}, []);
3718

3819
const handleAuthAction = async <T>(action: (data: T) => Promise<{ user: User }>, data: T) => {
39-
if (!isCaptchaVerified) {
40-
setError('Captcha verification failed');
41-
return;
42-
}
4320
try {
4421
setIsLoading(true);
4522
setError(null);

src/hooks/useCaptcha.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import axios from 'axios';
2+
import type { CaptchaResponse } from '../services/captcha';
3+
import { useState, useEffect } from 'react';
4+
5+
export const useCaptcha = (cfCaptchaSiteKey: string) => {
6+
const [isCaptchaVerified, setIsCaptchaVerified] = useState(false);
7+
8+
useEffect(() => {
9+
window.onloadTurnstileCallback = function () {
10+
turnstile.render('#cf-captcha-container', {
11+
theme: 'dark',
12+
sitekey: cfCaptchaSiteKey,
13+
callback: async function (captchaToken) {
14+
try {
15+
const captchaResult = await axios.post<CaptchaResponse>('/api/captcha/verify', {
16+
captchaToken,
17+
});
18+
setIsCaptchaVerified(captchaResult.data.success);
19+
} catch (error) {
20+
console.error('Captcha verification error:', error);
21+
setIsCaptchaVerified(false);
22+
}
23+
},
24+
});
25+
};
26+
}, []);
27+
28+
return { isCaptchaVerified };
29+
};

0 commit comments

Comments
 (0)