Reverted to commit 0091584677

This commit is contained in:
gpt-engineer-app[bot]
2025-11-01 15:22:30 +00:00
parent 26e5753807
commit 133141d474
125 changed files with 2316 additions and 9102 deletions

View File

@@ -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}
/>

View File

@@ -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>
);
}

View File

@@ -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}

View File

@@ -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" />

View File

@@ -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>
);
}