diff --git a/src/components/auth/TOTPSetup.tsx b/src/components/auth/TOTPSetup.tsx new file mode 100644 index 00000000..567c3654 --- /dev/null +++ b/src/components/auth/TOTPSetup.tsx @@ -0,0 +1,328 @@ +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; +import { useToast } from '@/hooks/use-toast'; +import { useAuth } from '@/hooks/useAuth'; +import { supabase } from '@/integrations/supabase/client'; +import { Smartphone, Shield, Copy, Eye, EyeOff, Trash2, AlertCircle } from 'lucide-react'; + +interface TOTPFactor { + id: string; + friendly_name?: string; + factor_type: string; + status: string; + created_at: string; +} + +export function TOTPSetup() { + const { user } = useAuth(); + const { toast } = useToast(); + const [factors, setFactors] = useState([]); + const [loading, setLoading] = useState(false); + const [enrolling, setEnrolling] = useState(false); + const [qrCode, setQrCode] = useState(''); + const [secret, setSecret] = useState(''); + const [factorId, setFactorId] = useState(''); + const [verificationCode, setVerificationCode] = useState(''); + const [showSecret, setShowSecret] = useState(false); + + useEffect(() => { + fetchTOTPFactors(); + }, [user]); + + const fetchTOTPFactors = async () => { + if (!user) return; + + try { + const { data, error } = await supabase.auth.mfa.listFactors(); + if (error) throw error; + + const totpFactors = (data.totp || []).map(factor => ({ + id: factor.id, + friendly_name: factor.friendly_name || 'Authenticator App', + factor_type: factor.factor_type || 'totp', + status: factor.status, + created_at: factor.created_at + })); + setFactors(totpFactors); + } catch (error: any) { + console.error('Error fetching TOTP factors:', error); + } + }; + + const startEnrollment = async () => { + if (!user) return; + + setLoading(true); + try { + const { data, error } = await supabase.auth.mfa.enroll({ + factorType: 'totp', + friendlyName: 'Authenticator App' + }); + + if (error) throw error; + + setQrCode(data.totp.qr_code); + setSecret(data.totp.secret); + setFactorId(data.id); + setEnrolling(true); + } catch (error: any) { + toast({ + title: 'Error', + description: error.message || 'Failed to start TOTP enrollment', + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + const verifyAndEnable = async () => { + if (!factorId || !verificationCode.trim()) { + toast({ + title: 'Error', + description: 'Please enter the verification code', + variant: 'destructive' + }); + return; + } + + setLoading(true); + try { + const { error } = await supabase.auth.mfa.verify({ + factorId, + challengeId: factorId, // For enrollment, challengeId is the same as factorId + code: verificationCode.trim() + }); + + if (error) throw error; + + toast({ + title: 'TOTP Enabled', + description: 'Two-factor authentication has been successfully enabled for your account.' + }); + + // Reset state and refresh factors + setEnrolling(false); + setQrCode(''); + setSecret(''); + setFactorId(''); + setVerificationCode(''); + fetchTOTPFactors(); + } catch (error: any) { + toast({ + title: 'Error', + description: error.message || 'Invalid verification code. Please try again.', + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + const unenrollFactor = async (factorId: string) => { + setLoading(true); + try { + const { error } = await supabase.auth.mfa.unenroll({ + factorId + }); + + if (error) throw error; + + toast({ + title: 'TOTP Disabled', + description: 'Two-factor authentication has been disabled for your account.' + }); + + fetchTOTPFactors(); + } catch (error: any) { + toast({ + title: 'Error', + description: error.message || 'Failed to disable TOTP', + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + const copySecret = () => { + navigator.clipboard.writeText(secret); + toast({ + title: 'Copied', + description: 'Secret key copied to clipboard' + }); + }; + + const cancelEnrollment = () => { + setEnrolling(false); + setQrCode(''); + setSecret(''); + setFactorId(''); + setVerificationCode(''); + }; + + const activeFactor = factors.find(f => f.status === 'verified'); + + if (enrolling) { + return ( + + + + + Set Up Authenticator App + + + Scan the QR code with your authenticator app, then enter the verification code below. + + + + {/* QR Code */} +
+
+ TOTP QR Code +
+
+ + {/* Manual Entry */} +
+ +
+ + + +
+
+ + {/* Verification */} +
+
+ + setVerificationCode(e.target.value)} + placeholder="000000" + maxLength={6} + className="text-center text-lg tracking-widest font-mono" + /> +
+ +
+ + +
+
+
+
+ ); + } + + return ( + + + + Add an extra layer of security to your account with two-factor authentication. + + + + {activeFactor ? ( +
+ + + + Two-factor authentication is enabled for your account. You'll be prompted for a verification code when signing in. + + + +
+
+
+ +
+
+

{activeFactor.friendly_name || 'Authenticator App'}

+

+ Enabled {new Date(activeFactor.created_at).toLocaleDateString()} +

+
+
+
+ + Active + + + + + + + + + + Disable Two-Factor Authentication + + + Are you sure you want to disable two-factor authentication? This will make your account less secure. + + + + Cancel + unenrollFactor(activeFactor.id)} + className="bg-destructive hover:bg-destructive/90" + > + Disable TOTP + + + + +
+
+
+ ) : ( +
+
+
+ +
+
+

Authenticator App

+

+ Use an authenticator app to generate verification codes +

+
+
+ +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/icons/DiscordIcon.tsx b/src/components/icons/DiscordIcon.tsx new file mode 100644 index 00000000..6fe24ec0 --- /dev/null +++ b/src/components/icons/DiscordIcon.tsx @@ -0,0 +1,7 @@ +export function DiscordIcon({ className = "w-5 h-5" }: { className?: string }) { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/components/icons/GoogleIcon.tsx b/src/components/icons/GoogleIcon.tsx new file mode 100644 index 00000000..f76d4eb0 --- /dev/null +++ b/src/components/icons/GoogleIcon.tsx @@ -0,0 +1,10 @@ +export function GoogleIcon({ className = "w-5 h-5" }: { className?: string }) { + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/settings/SecurityTab.tsx b/src/components/settings/SecurityTab.tsx index 0366a333..2fab34fe 100644 --- a/src/components/settings/SecurityTab.tsx +++ b/src/components/settings/SecurityTab.tsx @@ -12,6 +12,9 @@ import { useToast } from '@/hooks/use-toast'; import { useAuth } from '@/hooks/useAuth'; import { supabase } from '@/integrations/supabase/client'; import { Shield, Key, Smartphone, Globe, ExternalLink } from 'lucide-react'; +import { TOTPSetup } from '@/components/auth/TOTPSetup'; +import { GoogleIcon } from '@/components/icons/GoogleIcon'; +import { DiscordIcon } from '@/components/icons/DiscordIcon'; const passwordSchema = z.object({ currentPassword: z.string().min(1, 'Current password is required'), @@ -112,13 +115,13 @@ export function SecurityTab() { provider: 'google', connected: user?.identities?.some(identity => identity.provider === 'google') || false, email: user?.identities?.find(identity => identity.provider === 'google')?.identity_data?.email || user?.email, - icon: '🔍' + icon: }, { provider: 'discord', connected: user?.identities?.some(identity => identity.provider === 'discord') || false, email: user?.identities?.find(identity => identity.provider === 'discord')?.identity_data?.email, - icon: '🎮' + icon: } ]; @@ -212,7 +215,7 @@ export function SecurityTab() { {connectedAccounts.map((account) => (
-
+
{account.icon}
@@ -259,26 +262,15 @@ export function SecurityTab() {

Two-Factor Authentication

- - - - Add an extra layer of security to your account with two-factor authentication. - - - -
-
-

Authenticator App

-

- Use an authenticator app to generate verification codes -

-
- -
-
-
+ {/* Two-Factor Authentication */} +
+
+ +

Two-Factor Authentication

+
+ + +