mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 16:11:12 -05:00
Implement MFA Enforcement
This commit is contained in:
24
src/components/auth/MFARequiredAlert.tsx
Normal file
24
src/components/auth/MFARequiredAlert.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
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 MFARequiredAlert() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Alert variant="destructive" className="my-4">
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertTitle>Two-Factor Authentication Required</AlertTitle>
|
||||
<AlertDescription className="mt-2 space-y-3">
|
||||
<p>Your role requires two-factor authentication to access this area.</p>
|
||||
<Button
|
||||
onClick={() => navigate('/settings?tab=security')}
|
||||
size="sm"
|
||||
>
|
||||
Set up MFA
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -113,16 +113,14 @@ export function TOTPSetup() {
|
||||
|
||||
toast({
|
||||
title: 'TOTP Enabled',
|
||||
description: 'Two-factor authentication has been successfully enabled for your account.'
|
||||
description: 'Please sign in again to activate MFA protection.'
|
||||
});
|
||||
|
||||
// Reset state and refresh factors
|
||||
setEnrolling(false);
|
||||
setQrCode('');
|
||||
setSecret('');
|
||||
setFactorId('');
|
||||
setVerificationCode('');
|
||||
fetchTOTPFactors();
|
||||
// Force sign out to get new session with AAL2
|
||||
setTimeout(async () => {
|
||||
await supabase.auth.signOut();
|
||||
window.location.href = '/auth';
|
||||
}, 2000);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { authLog, authWarn, authError } from '@/lib/authLogger';
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
aal: 'aal1' | 'aal2' | null;
|
||||
loading: boolean;
|
||||
pendingEmail: string | null;
|
||||
sessionError: string | null;
|
||||
@@ -21,6 +22,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [aal, setAal] = useState<'aal1' | 'aal2' | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pendingEmail, setPendingEmail] = useState<string | null>(null);
|
||||
const [sessionError, setSessionError] = useState<string | null>(null);
|
||||
@@ -84,17 +86,22 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
||||
authLog('[Auth] SIGNED_IN - user authenticated');
|
||||
setSession(session);
|
||||
setUser(session.user);
|
||||
const userAal = (session.user as any).aal as 'aal1' | 'aal2' | undefined;
|
||||
setAal(userAal || 'aal1');
|
||||
setLoading(false);
|
||||
} else if (event === 'INITIAL_SESSION') {
|
||||
if (session?.user) {
|
||||
authLog('[Auth] INITIAL_SESSION - user exists');
|
||||
setSession(session);
|
||||
setUser(session.user);
|
||||
const userAal = (session.user as any).aal as 'aal1' | 'aal2' | undefined;
|
||||
setAal(userAal || 'aal1');
|
||||
setLoading(false);
|
||||
} else {
|
||||
authLog('[Auth] INITIAL_SESSION - no user');
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
setAal(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -102,11 +109,14 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
||||
authLog('[Auth] SIGNED_OUT - clearing state');
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
setAal(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
} else {
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
const userAal = session?.user ? ((session.user as any).aal as 'aal1' | 'aal2' | undefined) : null;
|
||||
setAal(userAal || null);
|
||||
}
|
||||
|
||||
// Detect confirmed email change: email changed AND no longer pending
|
||||
@@ -214,6 +224,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
||||
const value = {
|
||||
user,
|
||||
session,
|
||||
aal,
|
||||
loading,
|
||||
pendingEmail,
|
||||
sessionError,
|
||||
|
||||
21
src/hooks/useRequireMFA.ts
Normal file
21
src/hooks/useRequireMFA.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useAuth } from './useAuth';
|
||||
import { useUserRole } from './useUserRole';
|
||||
|
||||
export function useRequireMFA() {
|
||||
const { aal } = useAuth();
|
||||
const { isModerator, isAdmin, loading } = useUserRole();
|
||||
|
||||
// MFA is required for moderators and admins
|
||||
const requiresMFA = isModerator() || isAdmin();
|
||||
|
||||
// User has MFA if they have AAL2
|
||||
const hasMFA = aal === 'aal2';
|
||||
|
||||
return {
|
||||
requiresMFA,
|
||||
hasMFA,
|
||||
needsEnrollment: requiresMFA && !hasMFA,
|
||||
aal,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
@@ -3012,6 +3012,10 @@ export type Database = {
|
||||
Args: { _user_id: string }
|
||||
Returns: Json
|
||||
}
|
||||
has_aal2: {
|
||||
Args: Record<PropertyKey, never>
|
||||
Returns: boolean
|
||||
}
|
||||
has_pending_dependents: {
|
||||
Args: { item_id: string }
|
||||
Returns: boolean
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useNavigate } from 'react-router-dom';
|
||||
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 { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
@@ -20,6 +22,7 @@ import { QueueSkeleton } from '@/components/moderation/QueueSkeleton';
|
||||
export default function AdminDashboard() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const { isModerator, loading: roleLoading } = useUserRole();
|
||||
const { needsEnrollment, loading: mfaLoading } = useRequireMFA();
|
||||
const navigate = useNavigate();
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('moderation');
|
||||
@@ -110,7 +113,7 @@ export default function AdminDashboard() {
|
||||
}
|
||||
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
||||
|
||||
if (authLoading || roleLoading) {
|
||||
if (authLoading || roleLoading || mfaLoading) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
@@ -149,6 +152,15 @@ export default function AdminDashboard() {
|
||||
if (!user || !isModerator()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// MFA enforcement
|
||||
if (needsEnrollment) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<MFARequiredAlert />
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const statCards = [
|
||||
{
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useRef, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useUserRole } from '@/hooks/useUserRole';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useRequireMFA } from '@/hooks/useRequireMFA';
|
||||
import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert';
|
||||
import { AdminLayout } from '@/components/layout/AdminLayout';
|
||||
import { ModerationQueue, ModerationQueueRef } from '@/components/moderation/ModerationQueue';
|
||||
import { QueueSkeleton } from '@/components/moderation/QueueSkeleton';
|
||||
@@ -11,6 +13,7 @@ import { useModerationStats } from '@/hooks/useModerationStats';
|
||||
export default function AdminModeration() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const { isModerator, loading: roleLoading } = useUserRole();
|
||||
const { needsEnrollment, loading: mfaLoading } = useRequireMFA();
|
||||
const navigate = useNavigate();
|
||||
const moderationQueueRef = useRef<ModerationQueueRef>(null);
|
||||
|
||||
@@ -46,7 +49,7 @@ export default function AdminModeration() {
|
||||
}
|
||||
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
||||
|
||||
if (authLoading || roleLoading) {
|
||||
if (authLoading || roleLoading || mfaLoading) {
|
||||
return (
|
||||
<AdminLayout
|
||||
onRefresh={handleRefresh}
|
||||
@@ -71,6 +74,15 @@ export default function AdminModeration() {
|
||||
if (!user || !isModerator()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// MFA enforcement
|
||||
if (needsEnrollment) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<MFARequiredAlert />
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useRef, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useUserRole } from '@/hooks/useUserRole';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useRequireMFA } from '@/hooks/useRequireMFA';
|
||||
import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert';
|
||||
import { AdminLayout } from '@/components/layout/AdminLayout';
|
||||
import { ReportsQueue, ReportsQueueRef } from '@/components/moderation/ReportsQueue';
|
||||
import { QueueSkeleton } from '@/components/moderation/QueueSkeleton';
|
||||
@@ -11,6 +13,7 @@ import { useModerationStats } from '@/hooks/useModerationStats';
|
||||
export default function AdminReports() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const { isModerator, loading: roleLoading } = useUserRole();
|
||||
const { needsEnrollment, loading: mfaLoading } = useRequireMFA();
|
||||
const navigate = useNavigate();
|
||||
const reportsQueueRef = useRef<ReportsQueueRef>(null);
|
||||
|
||||
@@ -47,7 +50,7 @@ export default function AdminReports() {
|
||||
}
|
||||
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
||||
|
||||
if (authLoading || roleLoading) {
|
||||
if (authLoading || roleLoading || mfaLoading) {
|
||||
return (
|
||||
<AdminLayout
|
||||
onRefresh={handleRefresh}
|
||||
@@ -72,6 +75,15 @@ export default function AdminReports() {
|
||||
if (!user || !isModerator()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// MFA enforcement
|
||||
if (needsEnrollment) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<MFARequiredAlert />
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useUserRole } from '@/hooks/useUserRole';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useRequireMFA } from '@/hooks/useRequireMFA';
|
||||
import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert';
|
||||
import { AdminLayout } from '@/components/layout/AdminLayout';
|
||||
import { UserManagement } from '@/components/admin/UserManagement';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@@ -10,6 +12,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||
export default function AdminUsers() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const { isModerator, loading: roleLoading } = useUserRole();
|
||||
const { needsEnrollment, loading: mfaLoading } = useRequireMFA();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -26,7 +29,7 @@ export default function AdminUsers() {
|
||||
}
|
||||
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
||||
|
||||
if (authLoading || roleLoading) {
|
||||
if (authLoading || roleLoading || mfaLoading) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
@@ -59,6 +62,15 @@ export default function AdminUsers() {
|
||||
if (!user || !isModerator()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// MFA enforcement
|
||||
if (needsEnrollment) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<MFARequiredAlert />
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
|
||||
Reference in New Issue
Block a user