From cdd6b0bbd5c882688e76ac302cbb2110bf2cdc11 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 12:06:51 +0000 Subject: [PATCH] Refactor: Implement Phase 2 improvements --- src/hooks/moderation/useModerationFilters.ts | 24 +++++-- src/hooks/useAdminGuard.ts | 71 ++++++++++++++++++++ src/pages/AdminModeration.tsx | 35 ++-------- src/pages/AdminReports.tsx | 12 ++-- src/pages/AdminSystemLog.tsx | 22 +----- src/pages/AdminUsers.tsx | 32 ++------- 6 files changed, 107 insertions(+), 89 deletions(-) create mode 100644 src/hooks/useAdminGuard.ts diff --git a/src/hooks/moderation/useModerationFilters.ts b/src/hooks/moderation/useModerationFilters.ts index 0b0a4ce6..dc38d828 100644 --- a/src/hooks/moderation/useModerationFilters.ts +++ b/src/hooks/moderation/useModerationFilters.ts @@ -85,6 +85,9 @@ export interface ModerationFilters { /** Reset sort to default */ resetSort: () => void; + + /** Reset pagination to page 1 (callback) */ + onFilterChange?: () => void; } /** @@ -106,7 +109,9 @@ export interface ModerationFilters { * * ``` */ -export function useModerationFilters(config: ModerationFiltersConfig = {}): ModerationFilters { +export function useModerationFilters( + config: ModerationFiltersConfig & { onFilterChange?: () => void } = {} +): ModerationFilters { const { initialEntityFilter = 'all', initialStatusFilter = 'pending', @@ -115,6 +120,7 @@ export function useModerationFilters(config: ModerationFiltersConfig = {}): Mode persist = true, storageKey = 'moderationQueue_filters', initialSortConfig = { field: 'created_at', direction: 'asc' }, + onFilterChange, } = config; // Load persisted filters on mount @@ -204,23 +210,26 @@ export function useModerationFilters(config: ModerationFiltersConfig = {}): Mode } }, [sortConfig, persist, storageKey]); - // Set entity filter with logging + // Set entity filter with logging and pagination reset const setEntityFilter = useCallback((filter: EntityFilter) => { logger.log('🔍 Entity filter changed:', filter); setEntityFilterState(filter); - }, []); + onFilterChange?.(); + }, [onFilterChange]); - // Set status filter with logging + // Set status filter with logging and pagination reset const setStatusFilter = useCallback((filter: StatusFilter) => { logger.log('🔍 Status filter changed:', filter); setStatusFilterState(filter); - }, []); + onFilterChange?.(); + }, [onFilterChange]); - // Set active tab with logging + // Set active tab with logging and pagination reset const setActiveTab = useCallback((tab: QueueTab) => { logger.log('🔍 Tab changed:', tab); setActiveTabState(tab); - }, []); + onFilterChange?.(); + }, [onFilterChange]); // Sort callbacks const setSortConfig = useCallback((config: SortConfig) => { @@ -283,5 +292,6 @@ export function useModerationFilters(config: ModerationFiltersConfig = {}): Mode sortBy, toggleSortDirection, resetSort, + onFilterChange, }; } diff --git a/src/hooks/useAdminGuard.ts b/src/hooks/useAdminGuard.ts new file mode 100644 index 00000000..eed95641 --- /dev/null +++ b/src/hooks/useAdminGuard.ts @@ -0,0 +1,71 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from './useAuth'; +import { useUserRole } from './useUserRole'; +import { useRequireMFA } from './useRequireMFA'; + +export interface AdminGuardState { + /** Whether auth/role/MFA checks are still loading */ + isLoading: boolean; + + /** Whether user is authenticated and authorized */ + isAuthorized: boolean; + + /** Whether user needs to enroll in MFA */ + needsMFA: boolean; + + /** Current authenticated user */ + user: any; +} + +/** + * Consolidated admin guard hook for all admin pages + * + * Handles: + * - Authentication check (redirects to /auth) + * - Role authorization check (redirects to /) + * - MFA enrollment check + * - Loading states + * + * @param requireMFA - Whether to enforce MFA requirement (default: true) + * @returns AdminGuardState with loading, authorization, and MFA status + * + * @example + * ```tsx + * const { isLoading, isAuthorized, needsMFA } = useAdminGuard(); + * + * if (isLoading) return ; + * if (!isAuthorized) return null; + * if (needsMFA) return ; + * + * return ; + * ``` + */ +export function useAdminGuard(requireMFA: boolean = true): AdminGuardState { + const { user, loading: authLoading } = useAuth(); + const { isModerator, loading: roleLoading } = useUserRole(); + const { needsEnrollment, loading: mfaLoading } = useRequireMFA(); + const navigate = useNavigate(); + + // Auto-redirect based on auth state + useEffect(() => { + if (!authLoading && !roleLoading) { + if (!user) { + navigate('/auth'); + } else if (!isModerator()) { + navigate('/'); + } + } + }, [user, authLoading, roleLoading, navigate, isModerator]); + + const isLoading = authLoading || roleLoading || mfaLoading; + const isAuthorized = !!user && isModerator(); + const needsMFA = requireMFA && needsEnrollment; + + return { + isLoading, + isAuthorized, + needsMFA, + user, + }; +} diff --git a/src/pages/AdminModeration.tsx b/src/pages/AdminModeration.tsx index 41e504e6..de929f8c 100644 --- a/src/pages/AdminModeration.tsx +++ b/src/pages/AdminModeration.tsx @@ -1,8 +1,5 @@ -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 { useRef, useCallback } from 'react'; +import { useAdminGuard } from '@/hooks/useAdminGuard'; import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert'; import { AdminLayout } from '@/components/layout/AdminLayout'; import { ModerationQueue, ModerationQueueRef } from '@/components/moderation/ModerationQueue'; @@ -11,10 +8,7 @@ import { useAdminSettings } from '@/hooks/useAdminSettings'; 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 { isLoading, isAuthorized, needsMFA, user } = useAdminGuard(); const moderationQueueRef = useRef(null); const { @@ -26,7 +20,7 @@ export default function AdminModeration() { const pollInterval = getAdminPanelPollInterval(); const { lastUpdated } = useModerationStats({ - enabled: !!user && !authLoading && !roleLoading && isModerator(), + enabled: isAuthorized, pollingEnabled: refreshMode === 'auto', pollingInterval: pollInterval, }); @@ -35,21 +29,7 @@ export default function AdminModeration() { moderationQueueRef.current?.refresh(); }, []); - useEffect(() => { - if (!authLoading && !roleLoading) { - if (!user) { - navigate('/auth'); - return; - } - - if (!isModerator()) { - navigate('/'); - return; - } - } - }, [user, authLoading, roleLoading, navigate, isModerator]); - - if (authLoading || roleLoading || mfaLoading) { + if (isLoading) { return ( diff --git a/src/pages/AdminReports.tsx b/src/pages/AdminReports.tsx index 92a38faf..8861bfc0 100644 --- a/src/pages/AdminReports.tsx +++ b/src/pages/AdminReports.tsx @@ -1,8 +1,5 @@ -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 { useRef, useCallback } from 'react'; +import { useAdminGuard } from '@/hooks/useAdminGuard'; import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert'; import { AdminLayout } from '@/components/layout/AdminLayout'; import { ReportsQueue, ReportsQueueRef } from '@/components/moderation/ReportsQueue'; @@ -72,12 +69,11 @@ export default function AdminReports() { ); } - if (!user || !isModerator()) { + if (!isAuthorized) { return null; } - // MFA enforcement - if (needsEnrollment) { + if (needsMFA) { return ( diff --git a/src/pages/AdminSystemLog.tsx b/src/pages/AdminSystemLog.tsx index ed32941d..938113ed 100644 --- a/src/pages/AdminSystemLog.tsx +++ b/src/pages/AdminSystemLog.tsx @@ -50,25 +50,9 @@ class ErrorBoundary extends Component { } export default function AdminSystemLog() { - const { user, loading: authLoading } = useAuth(); - const { isModerator, loading: roleLoading } = useUserRole(); - const navigate = useNavigate(); + const { isLoading, isAuthorized } = useAdminGuard(false); // No MFA required for viewing logs - useEffect(() => { - if (!authLoading && !roleLoading) { - if (!user) { - navigate('/auth'); - return; - } - - if (!isModerator()) { - navigate('/'); - return; - } - } - }, [user, authLoading, roleLoading, navigate, isModerator]); - - if (authLoading || roleLoading) { + if (isLoading) { return (
@@ -97,7 +81,7 @@ export default function AdminSystemLog() { ); } - if (!user || !isModerator()) { + if (!isAuthorized) { return null; } diff --git a/src/pages/AdminUsers.tsx b/src/pages/AdminUsers.tsx index 4b27392d..2d96460d 100644 --- a/src/pages/AdminUsers.tsx +++ b/src/pages/AdminUsers.tsx @@ -1,8 +1,4 @@ -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 { useAdminGuard } from '@/hooks/useAdminGuard'; import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert'; import { AdminLayout } from '@/components/layout/AdminLayout'; import { UserManagement } from '@/components/admin/UserManagement'; @@ -10,26 +6,9 @@ import { Skeleton } from '@/components/ui/skeleton'; 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(); + const { isLoading, isAuthorized, needsMFA } = useAdminGuard(); - useEffect(() => { - if (!authLoading && !roleLoading) { - if (!user) { - navigate('/auth'); - return; - } - - if (!isModerator()) { - navigate('/'); - return; - } - } - }, [user, authLoading, roleLoading, navigate, isModerator]); - - if (authLoading || roleLoading || mfaLoading) { + if (isLoading) { return (
@@ -59,12 +38,11 @@ export default function AdminUsers() { ); } - if (!user || !isModerator()) { + if (!isAuthorized) { return null; } - // MFA enforcement - if (needsEnrollment) { + if (needsMFA) { return (