diff --git a/src/components/admin/AdminPageLayout.tsx b/src/components/admin/AdminPageLayout.tsx index 6f3a01fc..73c7a18c 100644 --- a/src/components/admin/AdminPageLayout.tsx +++ b/src/components/admin/AdminPageLayout.tsx @@ -1,6 +1,6 @@ import { ReactNode, useCallback } from 'react'; import { AdminLayout } from '@/components/layout/AdminLayout'; -import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert'; +import { MFAGuard } from '@/components/auth/MFAGuard'; import { QueueSkeleton } from '@/components/moderation/QueueSkeleton'; import { useAdminGuard } from '@/hooks/useAdminGuard'; import { useAdminSettings } from '@/hooks/useAdminSettings'; @@ -104,15 +104,6 @@ export function AdminPageLayout({ return null; } - // MFA required - if (needsMFA) { - return ( - - - - ); - } - // Main content return ( -
-
-

{title}

-

{description}

+ +
+
+

{title}

+

{description}

+
+ {children}
- {children} -
+ ); } diff --git a/src/components/auth/AutoMFAVerificationModal.tsx b/src/components/auth/AutoMFAVerificationModal.tsx new file mode 100644 index 00000000..63c45df7 --- /dev/null +++ b/src/components/auth/AutoMFAVerificationModal.tsx @@ -0,0 +1,106 @@ +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { MFAChallenge } from './MFAChallenge'; +import { Shield, AlertCircle, Loader2 } from 'lucide-react'; +import { getEnrolledFactors } from '@/lib/authService'; +import { useAuth } from '@/hooks/useAuth'; + +interface AutoMFAVerificationModalProps { + open: boolean; + onSuccess: () => void; + onCancel: () => void; +} + +export function AutoMFAVerificationModal({ + open, + onSuccess, + onCancel +}: AutoMFAVerificationModalProps) { + const { session } = useAuth(); + const [factorId, setFactorId] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch enrolled factor automatically when modal opens + useEffect(() => { + if (!open || !session) return; + + const fetchFactor = async () => { + setLoading(true); + setError(null); + + try { + const factors = await getEnrolledFactors(); + + if (factors.length === 0) { + setError('No MFA method enrolled. Please set up MFA in settings.'); + return; + } + + // Use the first verified TOTP factor + const totpFactor = factors.find(f => f.factor_type === 'totp'); + if (totpFactor) { + setFactorId(totpFactor.id); + } else { + setError('No valid MFA method found. Please check your security settings.'); + } + } catch (err) { + setError('Failed to load MFA settings. Please try again.'); + console.error('Failed to fetch MFA factors:', err); + } finally { + setLoading(false); + } + }; + + fetchFactor(); + }, [open, session]); + + return ( + { + if (!isOpen) { + onCancel(); + } + }} + > + e.preventDefault()} + onEscapeKeyDown={(e) => e.preventDefault()} + > + +
+ + Verification Required +
+ + Your session requires Multi-Factor Authentication to access this area. + +
+ + {loading && ( +
+ +

Loading verification...

+
+ )} + + {error && ( +
+ +

{error}

+
+ )} + + {!loading && !error && factorId && ( + + )} +
+
+ ); +} diff --git a/src/components/auth/MFAEnrollmentRequired.tsx b/src/components/auth/MFAEnrollmentRequired.tsx new file mode 100644 index 00000000..376954a4 --- /dev/null +++ b/src/components/auth/MFAEnrollmentRequired.tsx @@ -0,0 +1,26 @@ +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { Shield } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; + +export function MFAEnrollmentRequired() { + const navigate = useNavigate(); + + return ( + + + Multi-Factor Authentication Setup Required + +

+ Your role requires Multi-Factor Authentication. Please set up MFA to access this area. +

+ +
+
+ ); +} diff --git a/src/components/auth/MFAGuard.tsx b/src/components/auth/MFAGuard.tsx new file mode 100644 index 00000000..8758ce63 --- /dev/null +++ b/src/components/auth/MFAGuard.tsx @@ -0,0 +1,65 @@ +import { useRequireMFA } from '@/hooks/useRequireMFA'; +import { AutoMFAVerificationModal } from './AutoMFAVerificationModal'; +import { MFAEnrollmentRequired } from './MFAEnrollmentRequired'; +import { useAuth } from '@/hooks/useAuth'; +import { useToast } from '@/hooks/use-toast'; + +interface MFAGuardProps { + children: React.ReactNode; +} + +/** + * Smart MFA guard that automatically shows verification modal or enrollment alert + * + * Usage: + * ```tsx + * + * + * + * ``` + */ +export function MFAGuard({ children }: MFAGuardProps) { + const { needsEnrollment, needsVerification, loading } = useRequireMFA(); + const { verifySession } = useAuth(); + const { toast } = useToast(); + + const handleVerificationSuccess = async () => { + // Refresh the session to get updated AAL level + await verifySession(); + + toast({ + title: 'Verification Successful', + description: 'You can now access this area.', + }); + }; + + const handleVerificationCancel = () => { + // Redirect back to main dashboard + window.location.href = '/'; + }; + + // Show verification modal automatically when needed + if (needsVerification) { + return ( + <> + + {/* Show blurred content behind modal */} +
+ {children} +
+ + ); + } + + // Show enrollment alert when user hasn't set up MFA + if (needsEnrollment) { + return ; + } + + // User has MFA and is verified - show content + return <>{children}; +} diff --git a/src/components/auth/MFARequiredAlert.tsx b/src/components/auth/MFARequiredAlert.tsx deleted file mode 100644 index e5bd143d..00000000 --- a/src/components/auth/MFARequiredAlert.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Button } from '@/components/ui/button'; -import { Shield } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; -import { useAuth } from '@/hooks/useAuth'; -import { useEffect, useState } from 'react'; - -export function MFARequiredAlert() { - const navigate = useNavigate(); - const { checkAalStepUp } = useAuth(); - const [needsVerification, setNeedsVerification] = useState(false); - - useEffect(() => { - checkAalStepUp().then(result => { - setNeedsVerification(result.needsStepUp); - }); - }, [checkAalStepUp]); - - const handleAction = () => { - if (needsVerification) { - // User has MFA enrolled but needs to verify - sessionStorage.setItem('mfa_step_up_required', 'true'); - navigate('/auth/mfa-step-up'); - } else { - // User needs to enroll in MFA - navigate('/settings?tab=security'); - } - }; - - return ( - - - Multi-Factor Authentication Required - -

- {needsVerification - ? 'Please verify your identity with Multi-Factor Authentication to access this area.' - : 'Your role requires Multi-Factor Authentication to access this area.'} -

- -
-
- ); -} diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index 3b629254..1f31d293 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -4,7 +4,7 @@ import { FileText, Flag, AlertCircle, Activity, ShieldAlert } from 'lucide-react import { useUserRole } from '@/hooks/useUserRole'; import { useAuth } from '@/hooks/useAuth'; import { useRequireMFA } from '@/hooks/useRequireMFA'; -import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert'; +import { MFAGuard } from '@/components/auth/MFAGuard'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; @@ -158,15 +158,6 @@ export default function AdminDashboard() { if (!user || !isModerator()) { return null; } - - // MFA enforcement - if (needsEnrollment) { - return ( - - - - ); - } const statCards = [ { @@ -200,113 +191,115 @@ export default function AdminDashboard() { lastUpdated={lastUpdated ?? undefined} isRefreshing={isRefreshing} > -
-
-

Admin Dashboard

-

- Central hub for all moderation activity -

-
+ +
+
+

Admin Dashboard

+

+ Central hub for all moderation activity +

+
- {/* Security Warning for Suspicious Versions */} - {suspiciousVersionsCount > 0 && ( - - - - Security Alert: {suspiciousVersionsCount} entity version{suspiciousVersionsCount !== 1 ? 's' : ''} detected without user attribution. - This may indicate submission flow bypass. Check admin audit logs for details. - - - )} + {/* Security Warning for Suspicious Versions */} + {suspiciousVersionsCount > 0 && ( + + + + Security Alert: {suspiciousVersionsCount} entity version{suspiciousVersionsCount !== 1 ? 's' : ''} detected without user attribution. + This may indicate submission flow bypass. Check admin audit logs for details. + + + )} -
- {statCards.map((card) => { - const Icon = card.icon; - const colorClasses = { - amber: { - card: 'hover:border-amber-500/50', - bg: 'bg-amber-500/10', - icon: 'text-amber-600 dark:text-amber-400', - }, - red: { - card: 'hover:border-red-500/50', - bg: 'bg-red-500/10', - icon: 'text-red-600 dark:text-red-400', - }, - orange: { - card: 'hover:border-orange-500/50', - bg: 'bg-orange-500/10', - icon: 'text-orange-600 dark:text-orange-400', - }, - }; - const colors = colorClasses[card.color as keyof typeof colorClasses]; - - return ( - handleStatCardClick(card.type)} - > - -
-
- +
+ {statCards.map((card) => { + const Icon = card.icon; + const colorClasses = { + amber: { + card: 'hover:border-amber-500/50', + bg: 'bg-amber-500/10', + icon: 'text-amber-600 dark:text-amber-400', + }, + red: { + card: 'hover:border-red-500/50', + bg: 'bg-red-500/10', + icon: 'text-red-600 dark:text-red-400', + }, + orange: { + card: 'hover:border-orange-500/50', + bg: 'bg-orange-500/10', + icon: 'text-orange-600 dark:text-orange-400', + }, + }; + const colors = colorClasses[card.color as keyof typeof colorClasses]; + + return ( + handleStatCardClick(card.type)} + > + +
+
+ +
+
+

+ {card.label} +

+
-
-

- {card.label} -

-
-
-
{card.value}
- - - ); - })} +
{card.value}
+ + + ); + })} +
+ + + + + + Moderation Queue + Queue + {stats.pendingSubmissions > 0 && ( + + {stats.pendingSubmissions} + + )} + + + + Reports + Reports + {stats.openReports > 0 && ( + + {stats.openReports} + + )} + + + + Recent Activity + Activity + + + + + + + + +
- - - - - - Moderation Queue - Queue - {stats.pendingSubmissions > 0 && ( - - {stats.pendingSubmissions} - - )} - - - - Reports - Reports - {stats.openReports > 0 && ( - - {stats.openReports} - - )} - - - - Recent Activity - Activity - - - - - - - - - -
+ ); } diff --git a/src/pages/AdminModeration.tsx b/src/pages/AdminModeration.tsx index 5dea1bbf..72e581ff 100644 --- a/src/pages/AdminModeration.tsx +++ b/src/pages/AdminModeration.tsx @@ -1,6 +1,6 @@ import { useRef, useCallback } from 'react'; import { useAdminGuard } from '@/hooks/useAdminGuard'; -import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert'; +import { MFAGuard } from '@/components/auth/MFAGuard'; import { AdminLayout } from '@/components/layout/AdminLayout'; import { ModerationQueue, ModerationQueueRef } from '@/components/moderation/ModerationQueue'; import { QueueSkeleton } from '@/components/moderation/QueueSkeleton'; @@ -56,14 +56,6 @@ export default function AdminModeration() { if (!isAuthorized) { return null; } - - if (needsMFA) { - return ( - - - - ); - } return ( -
-
-

Moderation Queue

-

- Review and manage pending content submissions -

-
+ +
+
+

Moderation Queue

+

+ Review and manage pending content submissions +

+
- -
+ +
+
); } diff --git a/src/pages/AdminReports.tsx b/src/pages/AdminReports.tsx index 4aa2dbaa..2f42b103 100644 --- a/src/pages/AdminReports.tsx +++ b/src/pages/AdminReports.tsx @@ -1,6 +1,6 @@ import { useRef, useCallback } from 'react'; import { useAdminGuard } from '@/hooks/useAdminGuard'; -import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert'; +import { MFAGuard } from '@/components/auth/MFAGuard'; import { AdminLayout } from '@/components/layout/AdminLayout'; import { ReportsQueue, ReportsQueueRef } from '@/components/moderation/ReportsQueue'; import { QueueSkeleton } from '@/components/moderation/QueueSkeleton'; @@ -57,14 +57,6 @@ export default function AdminReports() { if (!isAuthorized) { return null; } - - if (needsMFA) { - return ( - - - - ); - } return ( -
-
-

User Reports

-

- Review and resolve user-submitted reports -

-
+ +
+
+

User Reports

+

+ Review and resolve user-submitted reports +

+
- -
+ +
+
); } diff --git a/src/pages/AdminUsers.tsx b/src/pages/AdminUsers.tsx index d5bc9cc6..76c7b1e2 100644 --- a/src/pages/AdminUsers.tsx +++ b/src/pages/AdminUsers.tsx @@ -1,5 +1,5 @@ import { useAdminGuard } from '@/hooks/useAdminGuard'; -import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert'; +import { MFAGuard } from '@/components/auth/MFAGuard'; import { AdminLayout } from '@/components/layout/AdminLayout'; import { UserManagement } from '@/components/admin/UserManagement'; import { Skeleton } from '@/components/ui/skeleton'; @@ -43,27 +43,21 @@ export default function AdminUsers() { if (!isAuthorized) { return null; } - - if (needsMFA) { - return ( - - - - ); - } return ( -
-
-

User Management

-

- Manage user profiles, roles, and permissions -

-
+ +
+
+

User Management

+

+ Manage user profiles, roles, and permissions +

+
- -
+ +
+
); }