From cf72b31513078430ec4b78850d1f4bd218cba828 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Tue, 16 Dec 2025 01:02:07 +0000 Subject: [PATCH] Secure all admin routes and fix achievement icon display Introduces a ProtectedRoute component to secure all admin routes, centralizing authentication logic. Removes redundant individual auth checks from admin pages. Updates admin-achievements.tsx to use a new `iconMap` for consistent icon rendering, resolving display issues. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 5a2fcd4b-aa24-41db-8542-c8df6e959cd0 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/ugufFZw Replit-Helium-Checkpoint-Created: true --- client/src/App.tsx | 23 +++---- client/src/components/ProtectedRoute.tsx | 32 ++++++++++ client/src/lib/iconMap.tsx | 81 ++++++++++++++++++++++++ client/src/pages/admin-achievements.tsx | 24 +------ client/src/pages/admin-activity.tsx | 18 +----- client/src/pages/admin-aegis.tsx | 22 +------ client/src/pages/admin-applications.tsx | 22 +------ client/src/pages/admin-architects.tsx | 22 +------ client/src/pages/admin-credentials.tsx | 20 +----- client/src/pages/admin-logs.tsx | 21 +----- client/src/pages/admin-notifications.tsx | 21 +----- client/src/pages/admin-projects.tsx | 21 +----- client/src/pages/admin-sites.tsx | 21 +----- client/src/pages/admin.tsx | 27 +------- 14 files changed, 141 insertions(+), 234 deletions(-) create mode 100644 client/src/components/ProtectedRoute.tsx create mode 100644 client/src/lib/iconMap.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 9a3f249..def52dc 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,6 +4,7 @@ import { QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/ui/toaster"; import { AuthProvider } from "@/lib/auth"; import { TutorialProvider } from "@/components/Tutorial"; +import { ProtectedRoute } from "@/components/ProtectedRoute"; import NotFound from "@/pages/not-found"; import Home from "@/pages/home"; import Passport from "@/pages/passport"; @@ -34,17 +35,17 @@ function Router() { - - - - - - - - - - - + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } + {() => } diff --git a/client/src/components/ProtectedRoute.tsx b/client/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..adc163c --- /dev/null +++ b/client/src/components/ProtectedRoute.tsx @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import { useLocation } from "wouter"; +import { useAuth } from "@/lib/auth"; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export function ProtectedRoute({ children }: ProtectedRouteProps) { + const { isAuthenticated, isLoading } = useAuth(); + const [, setLocation] = useLocation(); + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + setLocation("/login"); + } + }, [isLoading, isAuthenticated, setLocation]); + + if (isLoading) { + return ( +
+
Loading...
+
+ ); + } + + if (!isAuthenticated) { + return null; + } + + return <>{children}; +} diff --git a/client/src/lib/iconMap.tsx b/client/src/lib/iconMap.tsx new file mode 100644 index 0000000..10dc147 --- /dev/null +++ b/client/src/lib/iconMap.tsx @@ -0,0 +1,81 @@ +import { + Award, Star, Trophy, Crown, Shield, Zap, Heart, ThumbsUp, + Users, UserPlus, UserCheck, UserCog, User, + MessageSquare, MessageCircle, MessagesSquare, Hash, Send, + Newspaper, PenSquare, PenTool, + Code, FolderGit2, ClipboardCheck, Ticket, CheckCircle2, BugOff, + Rocket, Footprints, Gift, Gem, Diamond, Magnet, + Calendar, CalendarDays, CalendarHeart, + Video, Clapperboard, Flame, + Globe, Network, Brain, ShieldCheck, ShieldEllipsis, + Swords, LogIn, GraduationCap +} from "lucide-react"; + +const iconMap: Record> = { + "award": Award, + "star": Star, + "trophy": Trophy, + "crown": Crown, + "shield": Shield, + "zap": Zap, + "heart": Heart, + "thumbs-up": ThumbsUp, + "users": Users, + "user-plus": UserPlus, + "user-check": UserCheck, + "user-cog": UserCog, + "user": User, + "message-square": MessageSquare, + "message-circle": MessageCircle, + "messages-square": MessagesSquare, + "message-square-plus": MessageSquare, + "hash": Hash, + "send": Send, + "newspaper": Newspaper, + "pen-square": PenSquare, + "pen-tool": PenTool, + "code": Code, + "folder-git-2": FolderGit2, + "clipboard-check": ClipboardCheck, + "ticket": Ticket, + "check-circle-2": CheckCircle2, + "bug-off": BugOff, + "rocket": Rocket, + "footprints": Footprints, + "gift": Gift, + "gem": Gem, + "diamond": Diamond, + "magnet": Magnet, + "calendar": Calendar, + "calendar-days": CalendarDays, + "calendar-heart": CalendarHeart, + "video": Video, + "clapperboard": Clapperboard, + "flame": Flame, + "globe": Globe, + "network": Network, + "brain-circuit": Brain, + "shield-check": ShieldCheck, + "shield-ellipsis": ShieldEllipsis, + "swords": Swords, + "login": LogIn, + "logins": LogIn, + "graduationcap": GraduationCap, +}; + +export function getIcon(iconName: string | null | undefined): React.ReactNode { + if (!iconName) return "🏆"; + + const normalized = iconName.toLowerCase().trim(); + const IconComponent = iconMap[normalized]; + + if (IconComponent) { + return ; + } + + if (iconName.length <= 4) { + return iconName; + } + + return "🏆"; +} diff --git a/client/src/pages/admin-achievements.tsx b/client/src/pages/admin-achievements.tsx index c54c0f3..8f44a8d 100644 --- a/client/src/pages/admin-achievements.tsx +++ b/client/src/pages/admin-achievements.tsx @@ -1,26 +1,17 @@ -import { useEffect } from "react"; import { motion } from "framer-motion"; import { Link, useLocation } from "wouter"; import { useQuery } from "@tanstack/react-query"; import { useAuth } from "@/lib/auth"; +import { getIcon } from "@/lib/iconMap"; import { Users, FileCode, Shield, Activity, LogOut, BarChart3, User, Globe, Key, Award, Star, Trophy } from "lucide-react"; export default function AdminAchievements() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); - useEffect(() => { - const timer = setTimeout(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, 200); - return () => clearTimeout(timer); - }, [authLoading, isAuthenticated, setLocation]); - const { data: achievements, isLoading } = useQuery({ queryKey: ["achievements"], queryFn: async () => { @@ -28,17 +19,8 @@ export default function AdminAchievements() { if (!res.ok) throw new Error("Failed to fetch achievements"); return res.json(); }, - enabled: isAuthenticated, }); - if (authLoading || !isAuthenticated) { - return ( -
-
Loading...
-
- ); - } - const handleLogout = async () => { await logout(); setLocation("/"); @@ -84,7 +66,7 @@ export default function AdminAchievements() { >
- {achievement.icon || '🏆'} + {getIcon(achievement.icon)}

{achievement.name}

diff --git a/client/src/pages/admin-activity.tsx b/client/src/pages/admin-activity.tsx index 1cc4b92..f418b3b 100644 --- a/client/src/pages/admin-activity.tsx +++ b/client/src/pages/admin-activity.tsx @@ -18,18 +18,12 @@ interface ActivityEvent { } export default function AdminActivity() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); const [liveEvents, setLiveEvents] = useState([]); const [lastRefresh, setLastRefresh] = useState(new Date()); const [seenEventIds, setSeenEventIds] = useState>(new Set()); - useEffect(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, [authLoading, isAuthenticated, setLocation]); - const { data: profiles } = useQuery({ queryKey: ["profiles"], queryFn: async () => { @@ -37,7 +31,6 @@ export default function AdminActivity() { if (!res.ok) return []; return res.json(); }, - enabled: isAuthenticated, refetchInterval: 10000, }); @@ -48,7 +41,6 @@ export default function AdminActivity() { if (!res.ok) return []; return res.json(); }, - enabled: isAuthenticated, refetchInterval: 5000, }); @@ -89,14 +81,6 @@ export default function AdminActivity() { } }, [authLogs]); - if (authLoading || !isAuthenticated) { - return ( -
-
Loading...
-
- ); - } - const handleLogout = async () => { await logout(); setLocation("/"); diff --git a/client/src/pages/admin-aegis.tsx b/client/src/pages/admin-aegis.tsx index 59e9e27..20e0d00 100644 --- a/client/src/pages/admin-aegis.tsx +++ b/client/src/pages/admin-aegis.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { Link, useLocation } from "wouter"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useAuth } from "@/lib/auth"; @@ -8,19 +7,10 @@ import { } from "lucide-react"; export default function AdminAegis() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); const queryClient = useQueryClient(); - useEffect(() => { - const timer = setTimeout(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, 200); - return () => clearTimeout(timer); - }, [authLoading, isAuthenticated, setLocation]); - const { data: alerts } = useQuery({ queryKey: ["alerts"], queryFn: async () => { @@ -28,7 +18,6 @@ export default function AdminAegis() { if (!res.ok) return []; return res.json(); }, - enabled: isAuthenticated, }); const { data: authLogs } = useQuery({ @@ -38,7 +27,6 @@ export default function AdminAegis() { if (!res.ok) return []; return res.json(); }, - enabled: isAuthenticated, }); const resolveAlertMutation = useMutation({ @@ -56,14 +44,6 @@ export default function AdminAegis() { }, }); - if (authLoading || !isAuthenticated) { - return ( -
-
Loading...
-
- ); - } - const handleLogout = async () => { await logout(); setLocation("/"); diff --git a/client/src/pages/admin-applications.tsx b/client/src/pages/admin-applications.tsx index e63fede..e9417a4 100644 --- a/client/src/pages/admin-applications.tsx +++ b/client/src/pages/admin-applications.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { motion } from "framer-motion"; import { Link, useLocation } from "wouter"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; @@ -10,20 +10,11 @@ import { } from "lucide-react"; export default function AdminApplications() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); const [selectedApp, setSelectedApp] = useState(null); const queryClient = useQueryClient(); - useEffect(() => { - const timer = setTimeout(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, 200); - return () => clearTimeout(timer); - }, [authLoading, isAuthenticated, setLocation]); - const { data: applications, isLoading } = useQuery({ queryKey: ["applications"], queryFn: async () => { @@ -31,7 +22,6 @@ export default function AdminApplications() { if (!res.ok) throw new Error("Failed to fetch applications"); return res.json(); }, - enabled: isAuthenticated, }); const updateApplicationMutation = useMutation({ @@ -50,14 +40,6 @@ export default function AdminApplications() { }, }); - if (authLoading || !isAuthenticated) { - return ( -
-
Loading...
-
- ); - } - const handleLogout = async () => { await logout(); setLocation("/"); diff --git a/client/src/pages/admin-architects.tsx b/client/src/pages/admin-architects.tsx index ff8d5cc..f41a1f0 100644 --- a/client/src/pages/admin-architects.tsx +++ b/client/src/pages/admin-architects.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { motion } from "framer-motion"; import { Link, useLocation } from "wouter"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; @@ -10,28 +10,18 @@ import { } from "lucide-react"; export default function AdminArchitects() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); const [searchQuery, setSearchQuery] = useState(""); const [selectedProfile, setSelectedProfile] = useState(null); const queryClient = useQueryClient(); - useEffect(() => { - const timer = setTimeout(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, 200); - return () => clearTimeout(timer); - }, [authLoading, isAuthenticated, setLocation]); - const { data: profiles, isLoading } = useQuery({ queryKey: ["profiles"], queryFn: async () => { const res = await fetch("/api/profiles"); return res.json(); }, - enabled: isAuthenticated, }); const updateProfileMutation = useMutation({ @@ -50,14 +40,6 @@ export default function AdminArchitects() { }, }); - if (authLoading || !isAuthenticated) { - return ( -
-
Loading...
-
- ); - } - const filteredProfiles = profiles?.filter((p: any) => p.username?.toLowerCase().includes(searchQuery.toLowerCase()) || p.email?.toLowerCase().includes(searchQuery.toLowerCase()) diff --git a/client/src/pages/admin-credentials.tsx b/client/src/pages/admin-credentials.tsx index 42cafda..7bdeba9 100644 --- a/client/src/pages/admin-credentials.tsx +++ b/client/src/pages/admin-credentials.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { Link, useLocation } from "wouter"; import { useAuth } from "@/lib/auth"; import { @@ -7,26 +6,9 @@ import { } from "lucide-react"; export default function AdminCredentials() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); - useEffect(() => { - const timer = setTimeout(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, 200); - return () => clearTimeout(timer); - }, [authLoading, isAuthenticated, setLocation]); - - if (authLoading || !isAuthenticated) { - return ( -
-
Loading...
-
- ); - } - const handleLogout = async () => { await logout(); setLocation("/"); diff --git a/client/src/pages/admin-logs.tsx b/client/src/pages/admin-logs.tsx index 23ada3c..41b9fab 100644 --- a/client/src/pages/admin-logs.tsx +++ b/client/src/pages/admin-logs.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { motion } from "framer-motion"; import { Link, useLocation } from "wouter"; import { useQuery } from "@tanstack/react-query"; @@ -9,18 +8,9 @@ import { } from "lucide-react"; export default function AdminLogs() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); - useEffect(() => { - const timer = setTimeout(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, 200); - return () => clearTimeout(timer); - }, [authLoading, isAuthenticated, setLocation]); - const { data: logs, isLoading } = useQuery({ queryKey: ["auth-logs"], queryFn: async () => { @@ -28,17 +18,8 @@ export default function AdminLogs() { if (!res.ok) throw new Error("Failed to fetch logs"); return res.json(); }, - enabled: isAuthenticated, }); - if (authLoading || !isAuthenticated) { - return ( -
-
Loading...
-
- ); - } - const handleLogout = async () => { await logout(); setLocation("/"); diff --git a/client/src/pages/admin-notifications.tsx b/client/src/pages/admin-notifications.tsx index 3252f11..ad65246 100644 --- a/client/src/pages/admin-notifications.tsx +++ b/client/src/pages/admin-notifications.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Link, useLocation } from "wouter"; import { useAuth } from "@/lib/auth"; import { @@ -27,7 +27,7 @@ const defaultSettings: NotificationSetting[] = [ ]; export default function AdminNotifications() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); const [settings, setSettings] = useState(() => { if (typeof window !== "undefined") { @@ -44,15 +44,6 @@ export default function AdminNotifications() { }); const [saved, setSaved] = useState(false); - useEffect(() => { - const timer = setTimeout(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, 200); - return () => clearTimeout(timer); - }, [authLoading, isAuthenticated, setLocation]); - const toggleSetting = (id: string) => { setSettings((prev) => prev.map((s) => (s.id === id ? { ...s, enabled: !s.enabled } : s)) @@ -67,14 +58,6 @@ export default function AdminNotifications() { setTimeout(() => setSaved(false), 3000); }; - if (authLoading || !isAuthenticated) { - return ( -
-
Loading...
-
- ); - } - const handleLogout = async () => { await logout(); setLocation("/"); diff --git a/client/src/pages/admin-projects.tsx b/client/src/pages/admin-projects.tsx index 849a11f..86eb837 100644 --- a/client/src/pages/admin-projects.tsx +++ b/client/src/pages/admin-projects.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { motion } from "framer-motion"; import { Link, useLocation } from "wouter"; import { useQuery } from "@tanstack/react-query"; @@ -9,35 +8,17 @@ import { } from "lucide-react"; export default function AdminProjects() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); - useEffect(() => { - const timer = setTimeout(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, 200); - return () => clearTimeout(timer); - }, [authLoading, isAuthenticated, setLocation]); - const { data: projects, isLoading } = useQuery({ queryKey: ["projects"], queryFn: async () => { const res = await fetch("/api/projects"); return res.json(); }, - enabled: isAuthenticated, }); - if (authLoading || !isAuthenticated) { - return ( -
-
Loading...
-
- ); - } - const handleLogout = async () => { await logout(); setLocation("/"); diff --git a/client/src/pages/admin-sites.tsx b/client/src/pages/admin-sites.tsx index 9adb82b..6a6cb47 100644 --- a/client/src/pages/admin-sites.tsx +++ b/client/src/pages/admin-sites.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { motion } from "framer-motion"; import { Link, useLocation } from "wouter"; import { useQuery } from "@tanstack/react-query"; @@ -9,18 +8,9 @@ import { } from "lucide-react"; export default function AdminSites() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); - useEffect(() => { - const timer = setTimeout(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, 200); - return () => clearTimeout(timer); - }, [authLoading, isAuthenticated, setLocation]); - const { data: sites, isLoading } = useQuery({ queryKey: ["sites"], queryFn: async () => { @@ -28,17 +18,8 @@ export default function AdminSites() { if (!res.ok) throw new Error("Failed to fetch sites"); return res.json(); }, - enabled: isAuthenticated, }); - if (authLoading || !isAuthenticated) { - return ( -
-
Loading...
-
- ); - } - const handleLogout = async () => { await logout(); setLocation("/"); diff --git a/client/src/pages/admin.tsx b/client/src/pages/admin.tsx index f378036..697ae98 100644 --- a/client/src/pages/admin.tsx +++ b/client/src/pages/admin.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { motion } from "framer-motion"; import { Link, useLocation } from "wouter"; import { useQuery } from "@tanstack/react-query"; @@ -10,25 +9,15 @@ import { import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png'; export default function Admin() { - const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const { user, logout } = useAuth(); const [, setLocation] = useLocation(); - useEffect(() => { - const timer = setTimeout(() => { - if (!authLoading && !isAuthenticated) { - setLocation("/login"); - } - }, 200); - return () => clearTimeout(timer); - }, [authLoading, isAuthenticated, setLocation]); - const { data: metrics } = useQuery({ queryKey: ["metrics"], queryFn: async () => { const res = await fetch("/api/metrics"); return res.json(); }, - enabled: isAuthenticated, }); const { data: profiles } = useQuery({ @@ -37,7 +26,6 @@ export default function Admin() { const res = await fetch("/api/profiles"); return res.json(); }, - enabled: isAuthenticated, }); const { data: projects } = useQuery({ @@ -46,21 +34,8 @@ export default function Admin() { const res = await fetch("/api/projects"); return res.json(); }, - enabled: isAuthenticated, }); - if (authLoading) { - return ( -
-
Loading...
-
- ); - } - - if (!isAuthenticated) { - return null; - } - const handleLogout = async () => { await logout(); setLocation("/");