@@ -149,6 +152,15 @@ export default function AdminDashboard() {
if (!user || !isModerator()) {
return null;
}
+
+ // MFA enforcement
+ if (needsEnrollment) {
+ return (
+
+
+
+ );
+ }
const statCards = [
{
diff --git a/src/pages/AdminModeration.tsx b/src/pages/AdminModeration.tsx
index a4853112..41e504e6 100644
--- a/src/pages/AdminModeration.tsx
+++ b/src/pages/AdminModeration.tsx
@@ -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
(null);
@@ -46,7 +49,7 @@ export default function AdminModeration() {
}
}, [user, authLoading, roleLoading, navigate, isModerator]);
- if (authLoading || roleLoading) {
+ if (authLoading || roleLoading || mfaLoading) {
return (
+
+
+ );
+ }
return (
(null);
@@ -47,7 +50,7 @@ export default function AdminReports() {
}
}, [user, authLoading, roleLoading, navigate, isModerator]);
- if (authLoading || roleLoading) {
+ if (authLoading || roleLoading || mfaLoading) {
return (
+
+
+ );
+ }
return (
{
@@ -26,7 +29,7 @@ export default function AdminUsers() {
}
}, [user, authLoading, roleLoading, navigate, isModerator]);
- if (authLoading || roleLoading) {
+ if (authLoading || roleLoading || mfaLoading) {
return (
@@ -59,6 +62,15 @@ export default function AdminUsers() {
if (!user || !isModerator()) {
return null;
}
+
+ // MFA enforcement
+ if (needsEnrollment) {
+ return (
+
+
+
+ );
+ }
return (
diff --git a/supabase/migrations/20251014134306_adbf79c0-f924-4285-b0fa-ab2e54ca5e65.sql b/supabase/migrations/20251014134306_adbf79c0-f924-4285-b0fa-ab2e54ca5e65.sql
new file mode 100644
index 00000000..3c36e6c8
--- /dev/null
+++ b/supabase/migrations/20251014134306_adbf79c0-f924-4285-b0fa-ab2e54ca5e65.sql
@@ -0,0 +1,98 @@
+-- Create helper function to check AAL2 (Authenticator Assurance Level 2)
+CREATE OR REPLACE FUNCTION public.has_aal2()
+RETURNS boolean
+LANGUAGE sql
+STABLE
+SECURITY DEFINER
+SET search_path = public
+AS $$
+ SELECT COALESCE((auth.jwt()->>'aal')::text = 'aal2', false);
+$$;
+
+-- Update admin_settings policies to require MFA
+DROP POLICY IF EXISTS "Superusers can manage settings" ON public.admin_settings;
+CREATE POLICY "Superusers can manage settings with MFA"
+ON public.admin_settings
+FOR ALL
+USING (
+ is_superuser(auth.uid())
+ AND public.has_aal2()
+);
+
+-- Update user_roles policies to require MFA for role management
+DROP POLICY IF EXISTS "Admins can insert user roles" ON public.user_roles;
+CREATE POLICY "Admins can insert user roles with MFA"
+ON public.user_roles
+FOR INSERT
+WITH CHECK (
+ (has_role(auth.uid(), 'admin'::app_role) OR is_superuser(auth.uid()))
+ AND public.has_aal2()
+);
+
+DROP POLICY IF EXISTS "Admins can delete user roles" ON public.user_roles;
+CREATE POLICY "Admins can delete user roles with MFA"
+ON public.user_roles
+FOR DELETE
+USING (
+ (has_role(auth.uid(), 'admin'::app_role) OR is_superuser(auth.uid()))
+ AND public.has_aal2()
+);
+
+-- Update content_submissions moderation policies to require MFA
+DROP POLICY IF EXISTS "Moderators can update content submissions" ON public.content_submissions;
+CREATE POLICY "Moderators can update submissions with MFA"
+ON public.content_submissions
+FOR UPDATE
+USING (
+ is_moderator(auth.uid())
+ AND public.has_aal2()
+);
+
+DROP POLICY IF EXISTS "Moderators can delete content submissions" ON public.content_submissions;
+CREATE POLICY "Moderators can delete submissions with MFA"
+ON public.content_submissions
+FOR DELETE
+USING (
+ is_moderator(auth.uid())
+ AND public.has_aal2()
+);
+
+-- Update submission_items policies to require MFA
+DROP POLICY IF EXISTS "Moderators can update submission items" ON public.submission_items;
+CREATE POLICY "Moderators can update submission items with MFA"
+ON public.submission_items
+FOR UPDATE
+USING (
+ is_moderator(auth.uid())
+ AND public.has_aal2()
+);
+
+-- Update reports policies to require MFA
+DROP POLICY IF EXISTS "Moderators can update reports" ON public.reports;
+CREATE POLICY "Moderators can update reports with MFA"
+ON public.reports
+FOR UPDATE
+USING (
+ is_moderator(auth.uid())
+ AND public.has_aal2()
+);
+
+-- Update admin_audit_log policies to require MFA
+DROP POLICY IF EXISTS "Admins can insert audit log" ON public.admin_audit_log;
+CREATE POLICY "Admins can insert audit log with MFA"
+ON public.admin_audit_log
+FOR INSERT
+WITH CHECK (
+ is_moderator(auth.uid())
+ AND public.has_aal2()
+);
+
+-- Update profiles policies for sensitive operations
+DROP POLICY IF EXISTS "Admins can update any profile" ON public.profiles;
+CREATE POLICY "Admins can update any profile with MFA"
+ON public.profiles
+FOR UPDATE
+USING (
+ (auth.uid() = user_id) OR
+ ((has_role(auth.uid(), 'admin'::app_role) OR is_superuser(auth.uid())) AND public.has_aal2())
+);
\ No newline at end of file