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
This commit is contained in:
sirpiglr 2025-12-16 01:02:07 +00:00
parent c4e451da90
commit cf72b31513
14 changed files with 141 additions and 234 deletions

View file

@ -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() {
<Route path="/dashboard" component={Dashboard} />
<Route path="/curriculum" component={Curriculum} />
<Route path="/login" component={Login} />
<Route path="/admin" component={Admin} />
<Route path="/admin/architects" component={AdminArchitects} />
<Route path="/admin/projects" component={AdminProjects} />
<Route path="/admin/credentials" component={AdminCredentials} />
<Route path="/admin/aegis" component={AdminAegis} />
<Route path="/admin/sites" component={AdminSites} />
<Route path="/admin/logs" component={AdminLogs} />
<Route path="/admin/achievements" component={AdminAchievements} />
<Route path="/admin/applications" component={AdminApplications} />
<Route path="/admin/activity" component={AdminActivity} />
<Route path="/admin/notifications" component={AdminNotifications} />
<Route path="/admin">{() => <ProtectedRoute><Admin /></ProtectedRoute>}</Route>
<Route path="/admin/architects">{() => <ProtectedRoute><AdminArchitects /></ProtectedRoute>}</Route>
<Route path="/admin/projects">{() => <ProtectedRoute><AdminProjects /></ProtectedRoute>}</Route>
<Route path="/admin/credentials">{() => <ProtectedRoute><AdminCredentials /></ProtectedRoute>}</Route>
<Route path="/admin/aegis">{() => <ProtectedRoute><AdminAegis /></ProtectedRoute>}</Route>
<Route path="/admin/sites">{() => <ProtectedRoute><AdminSites /></ProtectedRoute>}</Route>
<Route path="/admin/logs">{() => <ProtectedRoute><AdminLogs /></ProtectedRoute>}</Route>
<Route path="/admin/achievements">{() => <ProtectedRoute><AdminAchievements /></ProtectedRoute>}</Route>
<Route path="/admin/applications">{() => <ProtectedRoute><AdminApplications /></ProtectedRoute>}</Route>
<Route path="/admin/activity">{() => <ProtectedRoute><AdminActivity /></ProtectedRoute>}</Route>
<Route path="/admin/notifications">{() => <ProtectedRoute><AdminNotifications /></ProtectedRoute>}</Route>
<Route path="/pitch" component={Pitch} />
<Route component={NotFound} />
</Switch>

View file

@ -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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
if (!isAuthenticated) {
return null;
}
return <>{children}</>;
}

View file

@ -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<string, React.ComponentType<{ className?: string }>> = {
"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 <IconComponent className="w-6 h-6 text-primary" />;
}
if (iconName.length <= 4) {
return iconName;
}
return "🏆";
}

View file

@ -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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");
@ -84,7 +66,7 @@ export default function AdminAchievements() {
>
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-black/20 rounded-lg flex items-center justify-center text-2xl">
{achievement.icon || '🏆'}
{getIcon(achievement.icon)}
</div>
<div className="flex-1">
<h3 className="font-display text-white uppercase text-sm">{achievement.name}</h3>

View file

@ -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<ActivityEvent[]>([]);
const [lastRefresh, setLastRefresh] = useState(new Date());
const [seenEventIds, setSeenEventIds] = useState<Set<string>>(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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");

View file

@ -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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");

View file

@ -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<any>(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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");

View file

@ -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<any>(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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const filteredProfiles = profiles?.filter((p: any) =>
p.username?.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.email?.toLowerCase().includes(searchQuery.toLowerCase())

View file

@ -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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");

View file

@ -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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");

View file

@ -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<NotificationSetting[]>(() => {
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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");

View file

@ -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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");

View file

@ -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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");

View file

@ -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 (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
if (!isAuthenticated) {
return null;
}
const handleLogout = async () => {
await logout();
setLocation("/");