mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 06:51:13 -05:00
Reverted to commit 0091584677
This commit is contained in:
@@ -35,8 +35,6 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
|
||||
const [signInCaptchaToken, setSignInCaptchaToken] = useState<string | null>(null);
|
||||
const [signInCaptchaKey, setSignInCaptchaKey] = useState(0);
|
||||
const [mfaFactorId, setMfaFactorId] = useState<string | null>(null);
|
||||
const [mfaChallengeId, setMfaChallengeId] = useState<string | null>(null);
|
||||
const [mfaPendingUserId, setMfaPendingUserId] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
@@ -72,57 +70,73 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
|
||||
setSignInCaptchaToken(null);
|
||||
|
||||
try {
|
||||
// Call server-side auth check with MFA detection
|
||||
const { data: authResult, error: authError } = await supabase.functions.invoke(
|
||||
'auth-with-mfa-check',
|
||||
{
|
||||
body: {
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
captchaToken: tokenToUse,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (authError || authResult.error) {
|
||||
throw new Error(authResult?.error || authError?.message || 'Authentication failed');
|
||||
const signInOptions: any = {
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
};
|
||||
|
||||
if (tokenToUse) {
|
||||
signInOptions.options = { captchaToken: tokenToUse };
|
||||
}
|
||||
|
||||
// Check if user is banned
|
||||
if (authResult.banned) {
|
||||
const reason = authResult.banReason
|
||||
? `Reason: ${authResult.banReason}`
|
||||
: 'Contact support for assistance.';
|
||||
const { data, error } = await supabase.auth.signInWithPassword(signInOptions);
|
||||
if (error) throw error;
|
||||
|
||||
// CRITICAL: Check ban status immediately after successful authentication
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('banned, ban_reason')
|
||||
.eq('user_id', data.user.id)
|
||||
.single();
|
||||
|
||||
if (profile?.banned) {
|
||||
// Sign out immediately
|
||||
await supabase.auth.signOut();
|
||||
|
||||
const reason = profile.ban_reason
|
||||
? `Reason: ${profile.ban_reason}`
|
||||
: 'Contact support for assistance.';
|
||||
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Account Suspended",
|
||||
description: `Your account has been suspended. ${reason}`,
|
||||
duration: 10000,
|
||||
duration: 10000
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
return; // Stop authentication flow
|
||||
}
|
||||
|
||||
// Check if MFA is required
|
||||
if (authResult.mfaRequired) {
|
||||
// NO SESSION EXISTS YET - show MFA challenge
|
||||
console.log('[AuthModal] MFA required - no session created yet');
|
||||
setMfaFactorId(authResult.factorId);
|
||||
setMfaChallengeId(authResult.challengeId);
|
||||
setMfaPendingUserId(authResult.userId);
|
||||
setLoading(false);
|
||||
return; // User has NO session - MFA modal will show
|
||||
// Check if MFA is required (user exists but no session)
|
||||
if (data.user && !data.session) {
|
||||
const totpFactor = data.user.factors?.find(f => f.factor_type === 'totp' && f.status === 'verified');
|
||||
|
||||
if (totpFactor) {
|
||||
setMfaFactorId(totpFactor.id);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No MFA required - user has session
|
||||
console.log('[AuthModal] No MFA required - user authenticated');
|
||||
|
||||
// Set the session in Supabase client
|
||||
if (authResult.session) {
|
||||
await supabase.auth.setSession(authResult.session);
|
||||
}
|
||||
// Track auth method for audit logging
|
||||
setAuthMethod('password');
|
||||
|
||||
// Check if MFA step-up is required
|
||||
const { handlePostAuthFlow } = await import('@/lib/authService');
|
||||
const postAuthResult = await handlePostAuthFlow(data.session, 'password');
|
||||
|
||||
if (postAuthResult.success && postAuthResult.data.shouldRedirect) {
|
||||
// Get the TOTP factor ID
|
||||
const { data: factors } = await supabase.auth.mfa.listFactors();
|
||||
const totpFactor = factors?.totp?.find(f => f.status === 'verified');
|
||||
|
||||
if (totpFactor) {
|
||||
setMfaFactorId(totpFactor.id);
|
||||
setLoading(false);
|
||||
return; // Stay in modal, show MFA challenge
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Welcome back!",
|
||||
description: "You've been signed in successfully."
|
||||
@@ -148,34 +162,30 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
|
||||
};
|
||||
|
||||
const handleMfaSuccess = async () => {
|
||||
console.log('[AuthModal] MFA verification succeeded - no further action needed');
|
||||
// Verify AAL upgrade was successful
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
const verification = await verifyMfaUpgrade(session);
|
||||
|
||||
if (!verification.success) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "MFA Verification Failed",
|
||||
description: verification.error || "Failed to upgrade session. Please try again."
|
||||
});
|
||||
|
||||
// Force sign out on verification failure
|
||||
await supabase.auth.signOut();
|
||||
setMfaFactorId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear state
|
||||
setMfaFactorId(null);
|
||||
setMfaChallengeId(null);
|
||||
setMfaPendingUserId(null);
|
||||
|
||||
toast({
|
||||
title: "Authentication complete",
|
||||
description: "You've been signed in successfully.",
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleMfaCancel = async () => {
|
||||
console.log('[AuthModal] User cancelled MFA verification');
|
||||
|
||||
// Clear state
|
||||
const handleMfaCancel = () => {
|
||||
setMfaFactorId(null);
|
||||
setMfaChallengeId(null);
|
||||
setMfaPendingUserId(null);
|
||||
setSignInCaptchaKey(prev => prev + 1);
|
||||
|
||||
toast({
|
||||
title: "Authentication cancelled",
|
||||
description: "Please sign in again when you're ready to complete two-factor authentication.",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignUp = async (e: React.FormEvent) => {
|
||||
@@ -234,7 +244,6 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}/auth/callback`,
|
||||
data: {
|
||||
username: formData.username,
|
||||
display_name: formData.displayName
|
||||
@@ -369,8 +378,6 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
|
||||
{mfaFactorId ? (
|
||||
<MFAChallenge
|
||||
factorId={mfaFactorId}
|
||||
challengeId={mfaChallengeId}
|
||||
userId={mfaPendingUserId}
|
||||
onSuccess={handleMfaSuccess}
|
||||
onCancel={handleMfaCancel}
|
||||
/>
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Mail } from 'lucide-react';
|
||||
|
||||
interface EmailOTPInputProps {
|
||||
email: string;
|
||||
onVerify: (code: string) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
onResend: () => Promise<void>;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function EmailOTPInput({
|
||||
email,
|
||||
onVerify,
|
||||
onCancel,
|
||||
onResend,
|
||||
loading = false
|
||||
}: EmailOTPInputProps) {
|
||||
const [code, setCode] = useState('');
|
||||
const [resending, setResending] = useState(false);
|
||||
|
||||
const handleVerify = async () => {
|
||||
if (code.length === 6) {
|
||||
await onVerify(code);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResend = async () => {
|
||||
setResending(true);
|
||||
try {
|
||||
await onResend();
|
||||
setCode(''); // Reset code input
|
||||
} finally {
|
||||
setResending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Alert>
|
||||
<Mail className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
We've sent a 6-digit verification code to <strong>{email}</strong>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="text-sm text-muted-foreground text-center">
|
||||
Enter the 6-digit code
|
||||
</div>
|
||||
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
disabled={loading}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
|
||||
<div className="flex gap-2 w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
className="flex-1"
|
||||
disabled={loading || resending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleVerify}
|
||||
className="flex-1"
|
||||
disabled={code.length !== 6 || loading || resending}
|
||||
>
|
||||
{loading ? 'Verifying...' : 'Verify'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleResend}
|
||||
disabled={loading || resending}
|
||||
className="text-xs"
|
||||
>
|
||||
{resending ? 'Sending...' : "Didn't receive a code? Resend"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,18 +5,15 @@ import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Shield, AlertCircle } from 'lucide-react';
|
||||
import { Shield } from 'lucide-react';
|
||||
|
||||
interface MFAChallengeProps {
|
||||
factorId: string;
|
||||
challengeId?: string | null;
|
||||
userId?: string | null;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function MFAChallenge({ factorId, challengeId, userId, onSuccess, onCancel }: MFAChallengeProps) {
|
||||
export function MFAChallenge({ factorId, onSuccess, onCancel }: MFAChallengeProps) {
|
||||
const { toast } = useToast();
|
||||
const [code, setCode] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -26,38 +23,6 @@ export function MFAChallenge({ factorId, challengeId, userId, onSuccess, onCance
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// NEW SERVER-SIDE FLOW: If we have challengeId and userId, use edge function
|
||||
if (challengeId && userId) {
|
||||
const { data: result, error: verifyError } = await supabase.functions.invoke(
|
||||
'verify-mfa-and-login',
|
||||
{
|
||||
body: {
|
||||
challengeId,
|
||||
factorId,
|
||||
code: code.trim(),
|
||||
userId,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (verifyError || result.error) {
|
||||
throw new Error(result?.error || verifyError?.message || 'Verification failed');
|
||||
}
|
||||
|
||||
// Set the session in Supabase client
|
||||
if (result.session) {
|
||||
await supabase.auth.setSession(result.session);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Welcome back!",
|
||||
description: "MFA verification successful."
|
||||
});
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
// OLD FLOW: For OAuth/Magic Link step-up (existing session)
|
||||
// Create fresh challenge for each verification attempt
|
||||
const { data: challengeData, error: challengeError } =
|
||||
await supabase.auth.mfa.challenge({ factorId });
|
||||
@@ -94,14 +59,6 @@ export function MFAChallenge({ factorId, challengeId, userId, onSuccess, onCance
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Alert className="border-destructive/50 text-destructive dark:border-destructive dark:text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Security Verification Required</AlertTitle>
|
||||
<AlertDescription>
|
||||
Cancelling will sign you out completely. Two-factor authentication must be completed to access your account.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="flex items-center gap-2 text-primary">
|
||||
<Shield className="w-5 h-5" />
|
||||
<h3 className="font-semibold">Two-Factor Authentication</h3>
|
||||
@@ -139,7 +96,7 @@ export function MFAChallenge({ factorId, challengeId, userId, onSuccess, onCance
|
||||
className="flex-1"
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel & Sign Out
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleVerify}
|
||||
|
||||
@@ -12,11 +12,7 @@ interface MFAStepUpModalProps {
|
||||
export function MFAStepUpModal({ open, factorId, onSuccess, onCancel }: MFAStepUpModalProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onCancel()}>
|
||||
<DialogContent
|
||||
className="sm:max-w-md"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md" onInteractOutside={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2 justify-center mb-2">
|
||||
<Shield className="h-6 w-6 text-primary" />
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* Migration Banner Component
|
||||
*
|
||||
* Alerts existing Supabase users about Auth0 migration
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Info, X } from 'lucide-react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useProfile } from '@/hooks/useProfile';
|
||||
import { isAuth0Configured } from '@/lib/auth0Config';
|
||||
|
||||
const DISMISSED_KEY = 'auth0_migration_dismissed';
|
||||
const DISMISS_DURATION_DAYS = 7;
|
||||
|
||||
export function MigrationBanner() {
|
||||
const { user } = useAuth();
|
||||
const { data: profile } = useProfile(user?.id);
|
||||
const [isDismissed, setIsDismissed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if banner should be shown
|
||||
if (!user || !profile || !isAuth0Configured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show if user already has Auth0 sub
|
||||
if ((profile as any).auth0_sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user dismissed the banner
|
||||
const dismissedUntil = localStorage.getItem(DISMISSED_KEY);
|
||||
if (dismissedUntil) {
|
||||
const dismissedDate = new Date(dismissedUntil);
|
||||
if (dismissedDate > new Date()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsDismissed(false);
|
||||
}, [user, profile]);
|
||||
|
||||
const handleDismiss = () => {
|
||||
const dismissUntil = new Date();
|
||||
dismissUntil.setDate(dismissUntil.getDate() + DISMISS_DURATION_DAYS);
|
||||
localStorage.setItem(DISMISSED_KEY, dismissUntil.toISOString());
|
||||
setIsDismissed(true);
|
||||
};
|
||||
|
||||
const handleMigrate = () => {
|
||||
// TODO: Implement migration flow
|
||||
// For now, just direct to settings
|
||||
window.location.href = '/settings?tab=security';
|
||||
};
|
||||
|
||||
if (isDismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert className="mb-4 border-blue-500 bg-blue-50 dark:bg-blue-950">
|
||||
<Info className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<div className="flex items-start justify-between gap-4 flex-1">
|
||||
<div className="flex-1">
|
||||
<AlertDescription className="text-sm">
|
||||
<strong className="text-blue-900 dark:text-blue-100">
|
||||
Important: Account Security Upgrade
|
||||
</strong>
|
||||
<p className="mt-1 text-blue-800 dark:text-blue-200">
|
||||
We're migrating to Auth0 for improved security and authentication.
|
||||
Your account needs to be migrated to continue using all features.
|
||||
</p>
|
||||
</AlertDescription>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleMigrate}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Migrate Now
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => window.open('/docs/auth0-migration', '_blank')}
|
||||
>
|
||||
Learn More
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDismiss}
|
||||
className="shrink-0 h-6 w-6 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user