1- import { useState } from 'react' ;
1+ import { useState , useEffect } from 'react' ;
22import { useNavigate } from 'react-router-dom' ;
33import { motion } from 'framer-motion' ;
44import { supabase } from '@/integrations/supabase/client' ;
@@ -7,9 +7,20 @@ import { Button } from '@/components/ui/button';
77import { Input } from '@/components/ui/input' ;
88import { Label } from '@/components/ui/label' ;
99import { Tabs , TabsContent , TabsList , TabsTrigger } from '@/components/ui/tabs' ;
10- import { Card , CardContent } from '@/components/ui/card' ;
11- import { Chrome } from 'lucide-react' ;
10+ import { Card } from '@/components/ui/card' ;
1211import { Navigation } from '@/components/Navigation' ;
12+ import { Turnstile } from '@marsidev/react-turnstile' ;
13+ import { Shield } from 'lucide-react' ;
14+
15+ // Turnstile site key - this is public and safe to expose
16+ const TURNSTILE_SITE_KEY = '0x4AAAAAAAgS7Pz7KlPJf9Em' ;
17+
18+ interface SecurityCheckResponse {
19+ allowed : boolean ;
20+ requireCaptcha : boolean ;
21+ reason ?: string ;
22+ lockoutMinutes ?: number ;
23+ }
1324
1425const Auth = ( ) => {
1526 const navigate = useNavigate ( ) ;
@@ -18,12 +29,93 @@ const Auth = () => {
1829 const [ password , setPassword ] = useState ( '' ) ;
1930 const [ username , setUsername ] = useState ( '' ) ;
2031 const [ fullName , setFullName ] = useState ( '' ) ;
32+
33+ // Security states
34+ const [ showCaptcha , setShowCaptcha ] = useState ( false ) ;
35+ const [ captchaToken , setCaptchaToken ] = useState < string | null > ( null ) ;
36+ const [ failedAttempts , setFailedAttempts ] = useState ( 0 ) ;
37+ const [ securityMessage , setSecurityMessage ] = useState < string | null > ( null ) ;
38+
39+ // Reset captcha when email changes
40+ useEffect ( ( ) => {
41+ setShowCaptcha ( false ) ;
42+ setCaptchaToken ( null ) ;
43+ setSecurityMessage ( null ) ;
44+ } , [ email ] ) ;
45+
46+ const checkSecurity = async ( endpoint : 'login' | 'register' ) : Promise < SecurityCheckResponse > => {
47+ try {
48+ const { data, error } = await supabase . functions . invoke ( 'auth-security' , {
49+ body : { action : 'check' , endpoint, email : endpoint === 'login' ? email : undefined }
50+ } ) ;
51+
52+ if ( error ) throw error ;
53+ return data as SecurityCheckResponse ;
54+ } catch ( error ) {
55+ console . error ( 'Security check failed:' , error ) ;
56+ // Allow the request if security check fails (fail-open for availability)
57+ return { allowed : true , requireCaptcha : false } ;
58+ }
59+ } ;
60+
61+ const recordAttempt = async ( endpoint : 'login' | 'register' , success : boolean ) => {
62+ try {
63+ await supabase . functions . invoke ( 'auth-security' , {
64+ body : { action : 'record' , endpoint, email, success }
65+ } ) ;
66+ } catch ( error ) {
67+ console . error ( 'Failed to record attempt:' , error ) ;
68+ }
69+ } ;
70+
71+ const verifyCaptcha = async ( token : string ) : Promise < boolean > => {
72+ try {
73+ const { data, error } = await supabase . functions . invoke ( 'auth-security' , {
74+ body : { action : 'verify-captcha' , captchaToken : token }
75+ } ) ;
76+
77+ if ( error ) throw error ;
78+ return data ?. success === true ;
79+ } catch ( error ) {
80+ console . error ( 'CAPTCHA verification failed:' , error ) ;
81+ return false ;
82+ }
83+ } ;
2184
2285 const handleSignUp = async ( e : React . FormEvent ) => {
2386 e . preventDefault ( ) ;
2487 setLoading ( true ) ;
88+ setSecurityMessage ( null ) ;
2589
2690 try {
91+ // Security check
92+ const security = await checkSecurity ( 'register' ) ;
93+
94+ if ( ! security . allowed ) {
95+ setSecurityMessage ( security . reason || 'Registration temporarily unavailable.' ) ;
96+ toast . error ( security . reason || 'Registration temporarily unavailable.' ) ;
97+ setLoading ( false ) ;
98+ return ;
99+ }
100+
101+ if ( security . requireCaptcha && ! captchaToken ) {
102+ setShowCaptcha ( true ) ;
103+ setSecurityMessage ( 'Please complete the security verification.' ) ;
104+ setLoading ( false ) ;
105+ return ;
106+ }
107+
108+ // Verify CAPTCHA if required
109+ if ( security . requireCaptcha && captchaToken ) {
110+ const captchaValid = await verifyCaptcha ( captchaToken ) ;
111+ if ( ! captchaValid ) {
112+ toast . error ( 'Security verification failed. Please try again.' ) ;
113+ setCaptchaToken ( null ) ;
114+ setLoading ( false ) ;
115+ return ;
116+ }
117+ }
118+
27119 const { data, error } = await supabase . auth . signUp ( {
28120 email,
29121 password,
@@ -36,10 +128,12 @@ const Auth = () => {
36128 } ,
37129 } ) ;
38130
131+ // Record the attempt
132+ await recordAttempt ( 'register' , ! error ) ;
133+
39134 if ( error ) throw error ;
40135
41136 if ( data . user ) {
42- // Check if phone is verified
43137 const { data : profile } = await supabase
44138 . from ( 'profiles' )
45139 . select ( 'phone_verified' )
@@ -54,8 +148,14 @@ const Auth = () => {
54148 navigate ( '/' ) ;
55149 }
56150 }
57- } catch ( error : any ) {
58- toast . error ( error . message || 'Failed to sign up' ) ;
151+ } catch ( error : unknown ) {
152+ // Generic error message to prevent user existence leaks
153+ const errorMessage = error instanceof Error ? error . message : 'Registration failed' ;
154+ if ( errorMessage . includes ( 'already registered' ) ) {
155+ toast . error ( 'Unable to complete registration. Please try again.' ) ;
156+ } else {
157+ toast . error ( 'Registration failed. Please try again.' ) ;
158+ }
59159 } finally {
60160 setLoading ( false ) ;
61161 }
@@ -64,16 +164,64 @@ const Auth = () => {
64164 const handleSignIn = async ( e : React . FormEvent ) => {
65165 e . preventDefault ( ) ;
66166 setLoading ( true ) ;
167+ setSecurityMessage ( null ) ;
67168
68169 try {
170+ // Security check
171+ const security = await checkSecurity ( 'login' ) ;
172+
173+ if ( ! security . allowed ) {
174+ setSecurityMessage ( security . reason || 'Login temporarily unavailable.' ) ;
175+ toast . error ( security . reason || 'Login temporarily unavailable.' ) ;
176+ setLoading ( false ) ;
177+ return ;
178+ }
179+
180+ if ( security . requireCaptcha && ! captchaToken ) {
181+ setShowCaptcha ( true ) ;
182+ setSecurityMessage ( 'Please complete the security verification.' ) ;
183+ setLoading ( false ) ;
184+ return ;
185+ }
186+
187+ // Verify CAPTCHA if required
188+ if ( security . requireCaptcha && captchaToken ) {
189+ const captchaValid = await verifyCaptcha ( captchaToken ) ;
190+ if ( ! captchaValid ) {
191+ toast . error ( 'Security verification failed. Please try again.' ) ;
192+ setCaptchaToken ( null ) ;
193+ setLoading ( false ) ;
194+ return ;
195+ }
196+ }
197+
69198 const { data, error } = await supabase . auth . signInWithPassword ( {
70199 email,
71200 password,
72201 } ) ;
73202
74- if ( error ) throw error ;
203+ // Record the attempt
204+ const success = ! error ;
205+ await recordAttempt ( 'login' , success ) ;
206+
207+ if ( error ) {
208+ // Increment local failed attempts counter for CAPTCHA trigger
209+ const newFailedAttempts = failedAttempts + 1 ;
210+ setFailedAttempts ( newFailedAttempts ) ;
211+
212+ if ( newFailedAttempts >= 3 ) {
213+ setShowCaptcha ( true ) ;
214+ }
215+
216+ // Generic error message - don't reveal if user exists or not
217+ throw new Error ( 'Invalid credentials' ) ;
218+ }
219+
220+ // Reset failed attempts on success
221+ setFailedAttempts ( 0 ) ;
222+ setShowCaptcha ( false ) ;
223+ setCaptchaToken ( null ) ;
75224
76- // Check if phone is verified
77225 if ( data . user ) {
78226 const { data : profile } = await supabase
79227 . from ( 'profiles' )
@@ -89,28 +237,22 @@ const Auth = () => {
89237 navigate ( '/' ) ;
90238 }
91239 }
92- } catch ( error : any ) {
93- toast . error ( error . message || 'Failed to sign in' ) ;
240+ } catch ( error : unknown ) {
241+ const errorMessage = error instanceof Error ? error . message : 'Invalid credentials' ;
242+ toast . error ( errorMessage ) ;
94243 } finally {
95244 setLoading ( false ) ;
96245 }
97246 } ;
98247
99- const handleGoogleSignIn = async ( ) => {
100- setLoading ( true ) ;
101- try {
102- const { error } = await supabase . auth . signInWithOAuth ( {
103- provider : 'google' ,
104- options : {
105- redirectTo : `${ window . location . origin } /verify-phone` ,
106- } ,
107- } ) ;
248+ const handleCaptchaSuccess = ( token : string ) => {
249+ setCaptchaToken ( token ) ;
250+ setSecurityMessage ( null ) ;
251+ } ;
108252
109- if ( error ) throw error ;
110- } catch ( error : any ) {
111- toast . error ( error . message || 'Failed to sign in with Google' ) ;
112- setLoading ( false ) ;
113- }
253+ const handleCaptchaError = ( ) => {
254+ setCaptchaToken ( null ) ;
255+ setSecurityMessage ( 'Security verification failed. Please try again.' ) ;
114256 } ;
115257
116258 return (
@@ -142,27 +284,6 @@ const Auth = () => {
142284
143285 < TabsContent value = "signin" >
144286 < form onSubmit = { handleSignIn } className = "space-y-4" >
145- < Button
146- type = "button"
147- variant = "outline"
148- className = "w-full bg-secondary/50 hover:bg-secondary border-border/50"
149- onClick = { handleGoogleSignIn }
150- disabled = { loading }
151- >
152- < Chrome className = "w-5 h-5 mr-2" />
153- Sign in with Google
154- </ Button >
155-
156- < div className = "relative" >
157- < div className = "absolute inset-0 flex items-center" >
158- < span className = "w-full border-t border-border/50" />
159- </ div >
160- < div className = "relative flex justify-center text-xs uppercase" >
161- < span className = "bg-card/80 px-2 text-muted-foreground" >
162- Or continue with email
163- </ span >
164- </ div >
165- </ div >
166287 < div >
167288 < Label htmlFor = "signin-email" > Email</ Label >
168289 < Input
@@ -187,11 +308,32 @@ const Auth = () => {
187308 className = "bg-secondary/30 border-border/50 focus:border-primary/50"
188309 />
189310 </ div >
311+
312+ { /* Security message */ }
313+ { securityMessage && (
314+ < div className = "flex items-center gap-2 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20 text-yellow-500 text-sm" >
315+ < Shield className = "w-4 h-4 flex-shrink-0" />
316+ < span > { securityMessage } </ span >
317+ </ div >
318+ ) }
319+
320+ { /* Conditional CAPTCHA */ }
321+ { showCaptcha && (
322+ < div className = "flex justify-center py-2" >
323+ < Turnstile
324+ siteKey = { TURNSTILE_SITE_KEY }
325+ onSuccess = { handleCaptchaSuccess }
326+ onError = { handleCaptchaError }
327+ onExpire = { ( ) => setCaptchaToken ( null ) }
328+ />
329+ </ div >
330+ ) }
331+
190332 < Button
191333 type = "submit"
192334 variant = "glow"
193335 className = "w-full"
194- disabled = { loading }
336+ disabled = { loading || ( showCaptcha && ! captchaToken ) }
195337 >
196338 { loading ? 'Signing in...' : 'Sign In' }
197339 </ Button >
@@ -200,27 +342,6 @@ const Auth = () => {
200342
201343 < TabsContent value = "signup" >
202344 < form onSubmit = { handleSignUp } className = "space-y-4" >
203- < Button
204- type = "button"
205- variant = "outline"
206- className = "w-full bg-secondary/50 hover:bg-secondary border-border/50"
207- onClick = { handleGoogleSignIn }
208- disabled = { loading }
209- >
210- < Chrome className = "w-5 h-5 mr-2" />
211- Sign up with Google
212- </ Button >
213-
214- < div className = "relative" >
215- < div className = "absolute inset-0 flex items-center" >
216- < span className = "w-full border-t border-border/50" />
217- </ div >
218- < div className = "relative flex justify-center text-xs uppercase" >
219- < span className = "bg-card/80 px-2 text-muted-foreground" >
220- Or create account with email
221- </ span >
222- </ div >
223- </ div >
224345 < div >
225346 < Label htmlFor = "fullname" > Full Name</ Label >
226347 < Input
@@ -270,11 +391,32 @@ const Auth = () => {
270391 className = "bg-secondary/30 border-border/50 focus:border-primary/50"
271392 />
272393 </ div >
394+
395+ { /* Security message */ }
396+ { securityMessage && (
397+ < div className = "flex items-center gap-2 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20 text-yellow-500 text-sm" >
398+ < Shield className = "w-4 h-4 flex-shrink-0" />
399+ < span > { securityMessage } </ span >
400+ </ div >
401+ ) }
402+
403+ { /* Conditional CAPTCHA */ }
404+ { showCaptcha && (
405+ < div className = "flex justify-center py-2" >
406+ < Turnstile
407+ siteKey = { TURNSTILE_SITE_KEY }
408+ onSuccess = { handleCaptchaSuccess }
409+ onError = { handleCaptchaError }
410+ onExpire = { ( ) => setCaptchaToken ( null ) }
411+ />
412+ </ div >
413+ ) }
414+
273415 < Button
274416 type = "submit"
275417 variant = "glow"
276418 className = "w-full"
277- disabled = { loading }
419+ disabled = { loading || ( showCaptcha && ! captchaToken ) }
278420 >
279421 { loading ? 'Creating account...' : 'Create Account' }
280422 </ Button >
0 commit comments