Skip to content

Commit 9a01794

Browse files
Remove Google login and add CAPTCHA
Eliminate Google OAuth UI and flows from Auth.tsx, replace with email/password only, and integrate Cloudflare Turnstile CAPTCHA for suspicious activity. Add conditional CAPTCHA rendering and groundwork for security checks (structure in place to enable future rate-limiting and lockout). Update auth config accordingly. X-Lovable-Edit-ID: edt-deb7d7dc-f3fe-44a7-aed8-2271223e2d18
2 parents 77663e9 + 4080f17 commit 9a01794

File tree

4 files changed

+477
-68
lines changed

4 files changed

+477
-68
lines changed

src/pages/Auth.tsx

Lines changed: 210 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useState, useEffect } from 'react';
22
import { useNavigate } from 'react-router-dom';
33
import { motion } from 'framer-motion';
44
import { supabase } from '@/integrations/supabase/client';
@@ -7,9 +7,20 @@ import { Button } from '@/components/ui/button';
77
import { Input } from '@/components/ui/input';
88
import { Label } from '@/components/ui/label';
99
import { 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';
1211
import { 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

1425
const 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>

supabase/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ verify_jwt = true
1111

1212
[functions.generate-topic-notes]
1313
verify_jwt = false
14+
15+
[functions.auth-security]
16+
verify_jwt = false

0 commit comments

Comments
 (0)