mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 06:17:21 +00:00
Add login, admin panel, and user management features
Introduces authentication via JWT, session management with CSRF protection, and new admin routes for managing users, projects, and monitoring security. Enhances dashboard and home pages with dynamic metrics fetched from the backend. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: dcd55177-c240-4288-8fc0-652032c758f2 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/2riq6Ir Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
fde3523413
commit
8ee5f71ef4
17 changed files with 2258 additions and 153 deletions
|
|
@ -2,12 +2,20 @@ import { Switch, Route } from "wouter";
|
|||
import { queryClient } from "./lib/queryClient";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { AuthProvider } from "@/lib/auth";
|
||||
import NotFound from "@/pages/not-found";
|
||||
import Home from "@/pages/home";
|
||||
import Passport from "@/pages/passport";
|
||||
import Terminal from "@/pages/terminal";
|
||||
import Dashboard from "@/pages/dashboard";
|
||||
import Curriculum from "@/pages/curriculum";
|
||||
import Login from "@/pages/login";
|
||||
import Admin from "@/pages/admin";
|
||||
import Pitch from "@/pages/pitch";
|
||||
import AdminArchitects from "@/pages/admin-architects";
|
||||
import AdminProjects from "@/pages/admin-projects";
|
||||
import AdminCredentials from "@/pages/admin-credentials";
|
||||
import AdminAegis from "@/pages/admin-aegis";
|
||||
|
||||
function Router() {
|
||||
return (
|
||||
|
|
@ -17,6 +25,13 @@ function Router() {
|
|||
<Route path="/terminal" component={Terminal} />
|
||||
<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="/pitch" component={Pitch} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
);
|
||||
|
|
@ -25,8 +40,10 @@ function Router() {
|
|||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Toaster />
|
||||
<Router />
|
||||
<AuthProvider>
|
||||
<Toaster />
|
||||
<Router />
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
89
client/src/lib/auth.tsx
Normal file
89
client/src/lib/auth.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
isAdmin: boolean;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: session, isLoading } = useQuery({
|
||||
queryKey: ["session"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/auth/session");
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationFn: async ({ username, password }: { username: string; password: string }) => {
|
||||
const res = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Login failed");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["session"] });
|
||||
},
|
||||
});
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await fetch("/api/auth/logout", { method: "POST" });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["session"] });
|
||||
},
|
||||
});
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
await loginMutation.mutateAsync({ username, password });
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await logoutMutation.mutateAsync();
|
||||
};
|
||||
|
||||
const value: AuthContextType = {
|
||||
user: session?.authenticated ? session.user : null,
|
||||
isLoading,
|
||||
isAuthenticated: !!session?.authenticated,
|
||||
isAdmin: session?.user?.isAdmin || false,
|
||||
login,
|
||||
logout,
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
194
client/src/pages/admin-aegis.tsx
Normal file
194
client/src/pages/admin-aegis.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import { useEffect } from "react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import {
|
||||
Users, FileCode, Shield, Activity, LogOut,
|
||||
BarChart3, User, AlertTriangle, CheckCircle, XCircle, Eye
|
||||
} from "lucide-react";
|
||||
|
||||
export default function AdminAegis() {
|
||||
const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth();
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !isAuthenticated) {
|
||||
setLocation("/login");
|
||||
}
|
||||
}, [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("/");
|
||||
};
|
||||
|
||||
// Mock threat data for demo
|
||||
const mockThreats = [
|
||||
{ id: 1, type: "PII Exposure", severity: "high", status: "blocked", timestamp: "2 min ago" },
|
||||
{ id: 2, type: "Suspicious Pattern", severity: "medium", status: "flagged", timestamp: "15 min ago" },
|
||||
{ id: 3, type: "Rate Limit", severity: "low", status: "allowed", timestamp: "1 hour ago" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-mono flex">
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-card border-r border-white/10 flex flex-col">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<h1 className="text-xl font-display font-bold text-white uppercase tracking-wider">
|
||||
AeThex
|
||||
</h1>
|
||||
<p className="text-xs text-primary mt-1">Command Center</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
<NavItem icon={<BarChart3 className="w-4 h-4" />} label="Dashboard" href="/admin" />
|
||||
<NavItem icon={<Users className="w-4 h-4" />} label="Architects" href="/admin/architects" />
|
||||
<NavItem icon={<FileCode className="w-4 h-4" />} label="Credentials" href="/admin/credentials" />
|
||||
<NavItem icon={<Activity className="w-4 h-4" />} label="Projects" href="/admin/projects" />
|
||||
<NavItem icon={<Shield className="w-4 h-4" />} label="Aegis Monitor" href="/admin/aegis" active />
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-white/10">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-white font-bold">{user?.username}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{user?.isAdmin ? "Administrator" : "Member"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-2 text-muted-foreground hover:text-white text-sm py-2 px-3 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" /> Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-8">
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-wider">
|
||||
Aegis Monitor
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
Real-time security monitoring and threat intervention
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-green-500/10 text-green-500 px-4 py-2 text-sm">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
Shield Active
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-card/50 border border-white/10 p-6">
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-2">Threats Blocked</div>
|
||||
<div className="text-3xl font-display font-bold text-destructive">247</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">Last 24 hours</div>
|
||||
</div>
|
||||
<div className="bg-card/50 border border-white/10 p-6">
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-2">PII Scrubbed</div>
|
||||
<div className="text-3xl font-display font-bold text-primary">1,892</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">Instances protected</div>
|
||||
</div>
|
||||
<div className="bg-card/50 border border-white/10 p-6">
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-2">Active Sessions</div>
|
||||
<div className="text-3xl font-display font-bold text-white">34</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">Being monitored</div>
|
||||
</div>
|
||||
<div className="bg-card/50 border border-white/10 p-6">
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-2">Uptime</div>
|
||||
<div className="text-3xl font-display font-bold text-green-500">99.9%</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">Last 30 days</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="bg-card/50 border border-white/10 p-6">
|
||||
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-6 flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-primary" />
|
||||
Recent Threat Activity
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{mockThreats.map((threat) => (
|
||||
<div key={threat.id} className="flex items-center justify-between p-4 bg-black/20 border border-white/5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
threat.severity === 'high' ? 'bg-destructive/10' :
|
||||
threat.severity === 'medium' ? 'bg-yellow-500/10' :
|
||||
'bg-white/5'
|
||||
}`}>
|
||||
<AlertTriangle className={`w-5 h-5 ${
|
||||
threat.severity === 'high' ? 'text-destructive' :
|
||||
threat.severity === 'medium' ? 'text-yellow-500' :
|
||||
'text-muted-foreground'
|
||||
}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white font-bold">{threat.type}</div>
|
||||
<div className="text-xs text-muted-foreground">{threat.timestamp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className={`text-xs px-2 py-1 rounded uppercase font-bold ${
|
||||
threat.status === 'blocked' ? 'bg-destructive/10 text-destructive' :
|
||||
threat.status === 'flagged' ? 'bg-yellow-500/10 text-yellow-500' :
|
||||
'bg-green-500/10 text-green-500'
|
||||
}`}>
|
||||
{threat.status}
|
||||
</span>
|
||||
<button className="text-muted-foreground hover:text-white p-2">
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Aegis security layer is monitoring all active sessions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({ icon, label, href, active = false }: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
href: string;
|
||||
active?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<div className={`flex items-center gap-3 px-3 py-2 text-sm transition-colors cursor-pointer ${
|
||||
active
|
||||
? 'bg-primary/10 text-primary border-l-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-white hover:bg-white/5'
|
||||
}`}>
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
354
client/src/pages/admin-architects.tsx
Normal file
354
client/src/pages/admin-architects.tsx
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import {
|
||||
Users, FileCode, Shield, Activity, LogOut,
|
||||
Home, BarChart3, Settings, User, Search,
|
||||
CheckCircle, XCircle, Eye, Edit, ChevronRight
|
||||
} from "lucide-react";
|
||||
|
||||
export default function AdminArchitects() {
|
||||
const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth();
|
||||
const [, setLocation] = useLocation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedProfile, setSelectedProfile] = useState<any>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !isAuthenticated) {
|
||||
setLocation("/login");
|
||||
}
|
||||
}, [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({
|
||||
mutationFn: async ({ id, updates }: { id: string; updates: any }) => {
|
||||
const res = await fetch(`/api/profiles/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to update");
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["profiles"] });
|
||||
setSelectedProfile(null);
|
||||
},
|
||||
});
|
||||
|
||||
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())
|
||||
) || [];
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
setLocation("/");
|
||||
};
|
||||
|
||||
const toggleVerified = (profile: any) => {
|
||||
updateProfileMutation.mutate({
|
||||
id: profile.id,
|
||||
updates: { is_verified: !profile.is_verified }
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-mono flex">
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-card border-r border-white/10 flex flex-col">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<h1 className="text-xl font-display font-bold text-white uppercase tracking-wider">
|
||||
AeThex
|
||||
</h1>
|
||||
<p className="text-xs text-primary mt-1">Command Center</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
<NavItem icon={<BarChart3 className="w-4 h-4" />} label="Dashboard" href="/admin" />
|
||||
<NavItem icon={<Users className="w-4 h-4" />} label="Architects" href="/admin/architects" active />
|
||||
<NavItem icon={<FileCode className="w-4 h-4" />} label="Credentials" href="/admin/credentials" />
|
||||
<NavItem icon={<Activity className="w-4 h-4" />} label="Projects" href="/admin/projects" />
|
||||
<NavItem icon={<Shield className="w-4 h-4" />} label="Aegis Monitor" href="/admin/aegis" />
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-white/10">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-white font-bold">{user?.username}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{user?.isAdmin ? "Administrator" : "Member"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-2 text-muted-foreground hover:text-white text-sm py-2 px-3 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" /> Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-8">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-wider">
|
||||
Architects
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
{profiles?.length || 0} registered architects
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search architects..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="bg-card border border-white/10 pl-10 pr-4 py-2 text-sm text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none w-64"
|
||||
data-testid="input-search"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="bg-card/50 border border-white/10 overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10 text-left">
|
||||
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">User</th>
|
||||
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">Role</th>
|
||||
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">Level</th>
|
||||
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">XP</th>
|
||||
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">Status</th>
|
||||
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">Verified</th>
|
||||
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="p-8 text-center text-muted-foreground">
|
||||
Loading...
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredProfiles.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="p-8 text-center text-muted-foreground">
|
||||
No architects found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredProfiles.map((profile: any) => (
|
||||
<tr key={profile.id} className="border-b border-white/5 hover:bg-white/5 transition-colors">
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={profile.avatar_url}
|
||||
alt={profile.username}
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm text-white font-bold">{profile.username}</div>
|
||||
<div className="text-xs text-muted-foreground">{profile.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className={`text-xs px-2 py-1 rounded uppercase font-bold ${
|
||||
profile.role === 'admin' ? 'bg-primary/10 text-primary' :
|
||||
profile.role === 'employee' ? 'bg-secondary/10 text-secondary' :
|
||||
'bg-white/5 text-muted-foreground'
|
||||
}`}>
|
||||
{profile.role}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4 text-white font-bold">{profile.level}</td>
|
||||
<td className="p-4 text-primary font-bold">{profile.total_xp}</td>
|
||||
<td className="p-4">
|
||||
<span className={`text-xs ${
|
||||
profile.status === 'online' ? 'text-green-500' : 'text-muted-foreground'
|
||||
}`}>
|
||||
{profile.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<button
|
||||
onClick={() => toggleVerified(profile)}
|
||||
className={`p-1 rounded transition-colors ${
|
||||
profile.is_verified
|
||||
? 'text-green-500 hover:bg-green-500/10'
|
||||
: 'text-muted-foreground hover:bg-white/5'
|
||||
}`}
|
||||
data-testid={`button-verify-${profile.id}`}
|
||||
>
|
||||
{profile.is_verified ? (
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
) : (
|
||||
<XCircle className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<button
|
||||
onClick={() => setSelectedProfile(profile)}
|
||||
className="text-muted-foreground hover:text-white transition-colors p-1"
|
||||
data-testid={`button-view-${profile.id}`}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Detail Modal */}
|
||||
{selectedProfile && (
|
||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="bg-card border border-white/10 w-full max-w-2xl max-h-[80vh] overflow-auto"
|
||||
>
|
||||
<div className="p-6 border-b border-white/10 flex justify-between items-start">
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src={selectedProfile.avatar_url}
|
||||
alt={selectedProfile.username}
|
||||
className="w-16 h-16 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-xl font-display text-white uppercase">{selectedProfile.username}</h3>
|
||||
<p className="text-muted-foreground text-sm">{selectedProfile.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedProfile(null)}
|
||||
className="text-muted-foreground hover:text-white"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-wider">Role</label>
|
||||
<div className="text-white font-bold">{selectedProfile.role}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-wider">Level</label>
|
||||
<div className="text-white font-bold">{selectedProfile.level}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-wider">Total XP</label>
|
||||
<div className="text-primary font-bold">{selectedProfile.total_xp}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-wider">Status</label>
|
||||
<div className={selectedProfile.status === 'online' ? 'text-green-500' : 'text-muted-foreground'}>
|
||||
{selectedProfile.status}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-wider">Passport ID</label>
|
||||
<div className="text-white font-mono text-xs">{selectedProfile.aethex_passport_id}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-wider">Verified</label>
|
||||
<div className={selectedProfile.is_verified ? 'text-green-500' : 'text-destructive'}>
|
||||
{selectedProfile.is_verified ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-wider">Bio</label>
|
||||
<div className="text-white">{selectedProfile.bio || 'No bio'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-wider">Skills</label>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{selectedProfile.skills?.map((skill: string, i: number) => (
|
||||
<span key={i} className="bg-primary/10 text-primary px-2 py-1 text-xs rounded">
|
||||
{skill}
|
||||
</span>
|
||||
)) || <span className="text-muted-foreground">No skills listed</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 border-t border-white/10 flex justify-end gap-4">
|
||||
<button
|
||||
onClick={() => setSelectedProfile(null)}
|
||||
className="px-4 py-2 text-muted-foreground hover:text-white transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleVerified(selectedProfile)}
|
||||
className={`px-4 py-2 font-bold uppercase tracking-wider transition-colors ${
|
||||
selectedProfile.is_verified
|
||||
? 'bg-destructive/10 text-destructive hover:bg-destructive/20'
|
||||
: 'bg-green-500/10 text-green-500 hover:bg-green-500/20'
|
||||
}`}
|
||||
>
|
||||
{selectedProfile.is_verified ? 'Revoke Verification' : 'Verify Architect'}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({ icon, label, href, active = false }: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
href: string;
|
||||
active?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<div className={`flex items-center gap-3 px-3 py-2 text-sm transition-colors cursor-pointer ${
|
||||
active
|
||||
? 'bg-primary/10 text-primary border-l-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-white hover:bg-white/5'
|
||||
}`}>
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
167
client/src/pages/admin-credentials.tsx
Normal file
167
client/src/pages/admin-credentials.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { useEffect } from "react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import {
|
||||
Users, FileCode, Shield, Activity, LogOut,
|
||||
BarChart3, User, Award, Clock, AlertTriangle
|
||||
} from "lucide-react";
|
||||
|
||||
export default function AdminCredentials() {
|
||||
const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth();
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !isAuthenticated) {
|
||||
setLocation("/login");
|
||||
}
|
||||
}, [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("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-mono flex">
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-card border-r border-white/10 flex flex-col">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<h1 className="text-xl font-display font-bold text-white uppercase tracking-wider">
|
||||
AeThex
|
||||
</h1>
|
||||
<p className="text-xs text-primary mt-1">Command Center</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
<NavItem icon={<BarChart3 className="w-4 h-4" />} label="Dashboard" href="/admin" />
|
||||
<NavItem icon={<Users className="w-4 h-4" />} label="Architects" href="/admin/architects" />
|
||||
<NavItem icon={<FileCode className="w-4 h-4" />} label="Credentials" href="/admin/credentials" active />
|
||||
<NavItem icon={<Activity className="w-4 h-4" />} label="Projects" href="/admin/projects" />
|
||||
<NavItem icon={<Shield className="w-4 h-4" />} label="Aegis Monitor" href="/admin/aegis" />
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-white/10">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-white font-bold">{user?.username}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{user?.isAdmin ? "Administrator" : "Member"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-2 text-muted-foreground hover:text-white text-sm py-2 px-3 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" /> Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-8">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-wider">
|
||||
Credential Issuance
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
Manage Codex certifications and passport credentials
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Coming Soon Notice */}
|
||||
<div className="bg-card/50 border border-secondary/30 p-8 mb-8">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<Award className="w-12 h-12 text-secondary" />
|
||||
<div>
|
||||
<h3 className="text-xl font-display text-white uppercase">Codex Certification System</h3>
|
||||
<p className="text-muted-foreground text-sm">Under Development</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
The credential issuance system will allow administrators to:
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-secondary rounded-full" />
|
||||
Issue Architect Passports with unique identifiers
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-secondary rounded-full" />
|
||||
Award skill certifications based on completed curriculum
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-secondary rounded-full" />
|
||||
Verify and validate credentials across platforms
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-secondary rounded-full" />
|
||||
Revoke credentials when necessary
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Placeholder Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-card/50 border border-white/10 p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Award className="w-5 h-5 text-primary" />
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wider">Issued</span>
|
||||
</div>
|
||||
<div className="text-3xl font-display font-bold text-white">0</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">Passports Issued</div>
|
||||
</div>
|
||||
<div className="bg-card/50 border border-white/10 p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Clock className="w-5 h-5 text-secondary" />
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wider">Pending</span>
|
||||
</div>
|
||||
<div className="text-3xl font-display font-bold text-white">0</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">Awaiting Review</div>
|
||||
</div>
|
||||
<div className="bg-card/50 border border-white/10 p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<AlertTriangle className="w-5 h-5 text-destructive" />
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wider">Revoked</span>
|
||||
</div>
|
||||
<div className="text-3xl font-display font-bold text-white">0</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">Credentials Revoked</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({ icon, label, href, active = false }: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
href: string;
|
||||
active?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<div className={`flex items-center gap-3 px-3 py-2 text-sm transition-colors cursor-pointer ${
|
||||
active
|
||||
? 'bg-primary/10 text-primary border-l-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-white hover:bg-white/5'
|
||||
}`}>
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
193
client/src/pages/admin-projects.tsx
Normal file
193
client/src/pages/admin-projects.tsx
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
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 {
|
||||
Users, FileCode, Shield, Activity, LogOut,
|
||||
BarChart3, User, ExternalLink
|
||||
} from "lucide-react";
|
||||
|
||||
export default function AdminProjects() {
|
||||
const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth();
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !isAuthenticated) {
|
||||
setLocation("/login");
|
||||
}
|
||||
}, [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("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-mono flex">
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-card border-r border-white/10 flex flex-col">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<h1 className="text-xl font-display font-bold text-white uppercase tracking-wider">
|
||||
AeThex
|
||||
</h1>
|
||||
<p className="text-xs text-primary mt-1">Command Center</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
<NavItem icon={<BarChart3 className="w-4 h-4" />} label="Dashboard" href="/admin" />
|
||||
<NavItem icon={<Users className="w-4 h-4" />} label="Architects" href="/admin/architects" />
|
||||
<NavItem icon={<FileCode className="w-4 h-4" />} label="Credentials" href="/admin/credentials" />
|
||||
<NavItem icon={<Activity className="w-4 h-4" />} label="Projects" href="/admin/projects" active />
|
||||
<NavItem icon={<Shield className="w-4 h-4" />} label="Aegis Monitor" href="/admin/aegis" />
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-white/10">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-white font-bold">{user?.username}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{user?.isAdmin ? "Administrator" : "Member"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-2 text-muted-foreground hover:text-white text-sm py-2 px-3 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" /> Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-8">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-wider">
|
||||
Projects
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
{projects?.length || 0} active projects
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Projects Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{isLoading ? (
|
||||
<div className="col-span-full text-center text-muted-foreground py-12">
|
||||
Loading projects...
|
||||
</div>
|
||||
) : projects?.length === 0 ? (
|
||||
<div className="col-span-full text-center text-muted-foreground py-12">
|
||||
No projects found
|
||||
</div>
|
||||
) : (
|
||||
projects?.map((project: any) => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-card/50 border border-white/10 p-6 hover:border-primary/30 transition-colors"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-display text-white uppercase">{project.title}</h3>
|
||||
<div className="text-xs text-muted-foreground mt-1">{project.engine}</div>
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-1 rounded uppercase font-bold ${
|
||||
project.status === 'In Progress' ? 'bg-secondary/10 text-secondary' :
|
||||
project.status === 'planning' ? 'bg-primary/10 text-primary' :
|
||||
'bg-white/5 text-muted-foreground'
|
||||
}`}>
|
||||
{project.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground mb-4 line-clamp-2">
|
||||
{project.description || 'No description'}
|
||||
</p>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span className="text-muted-foreground">Progress</span>
|
||||
<span className="text-secondary font-bold">{project.progress}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-white/10 rounded overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-secondary transition-all"
|
||||
style={{ width: `${project.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
<span className={`px-2 py-1 rounded ${
|
||||
project.priority === 'Critical' ? 'bg-destructive/10 text-destructive' :
|
||||
project.priority === 'High' ? 'bg-primary/10 text-primary' :
|
||||
'bg-white/5 text-muted-foreground'
|
||||
}`}>
|
||||
{project.priority}
|
||||
</span>
|
||||
|
||||
{project.github_url && (
|
||||
<a
|
||||
href={project.github_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-white flex items-center gap-1"
|
||||
>
|
||||
GitHub <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({ icon, label, href, active = false }: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
href: string;
|
||||
active?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<div className={`flex items-center gap-3 px-3 py-2 text-sm transition-colors cursor-pointer ${
|
||||
active
|
||||
? 'bg-primary/10 text-primary border-l-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-white hover:bg-white/5'
|
||||
}`}>
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
284
client/src/pages/admin.tsx
Normal file
284
client/src/pages/admin.tsx
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
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 {
|
||||
Users, FileCode, Shield, Activity, LogOut,
|
||||
Home, BarChart3, Settings, ChevronRight, User
|
||||
} from "lucide-react";
|
||||
import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png';
|
||||
|
||||
export default function Admin() {
|
||||
const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth();
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !isAuthenticated) {
|
||||
setLocation("/login");
|
||||
}
|
||||
}, [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({
|
||||
queryKey: ["profiles"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/profiles");
|
||||
return res.json();
|
||||
},
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
|
||||
const { data: projects } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: async () => {
|
||||
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("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-mono flex">
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-card border-r border-white/10 flex flex-col">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<h1 className="text-xl font-display font-bold text-white uppercase tracking-wider">
|
||||
AeThex
|
||||
</h1>
|
||||
<p className="text-xs text-primary mt-1">Command Center</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
<NavItem icon={<BarChart3 className="w-4 h-4" />} label="Dashboard" href="/admin" active />
|
||||
<NavItem icon={<Users className="w-4 h-4" />} label="Architects" href="/admin/architects" />
|
||||
<NavItem icon={<FileCode className="w-4 h-4" />} label="Credentials" href="/admin/credentials" />
|
||||
<NavItem icon={<Activity className="w-4 h-4" />} label="Projects" href="/admin/projects" />
|
||||
<NavItem icon={<Shield className="w-4 h-4" />} label="Aegis Monitor" href="/admin/aegis" />
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-white/10">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-white font-bold">{user?.username}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{user?.isAdmin ? "Administrator" : "Member"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-2 text-muted-foreground hover:text-white text-sm py-2 px-3 hover:bg-white/5 transition-colors"
|
||||
data-testid="button-logout"
|
||||
>
|
||||
<LogOut className="w-4 h-4" /> Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 relative overflow-auto">
|
||||
<div
|
||||
className="absolute inset-0 opacity-10 pointer-events-none z-0"
|
||||
style={{ backgroundImage: `url(${gridBg})`, backgroundSize: 'cover' }}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 p-8">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-wider">
|
||||
Dashboard
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
Real-time ecosystem metrics from Supabase
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-10">
|
||||
<MetricCard
|
||||
title="Total Architects"
|
||||
value={metrics?.totalProfiles || 0}
|
||||
icon={<Users className="w-5 h-5 text-primary" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Active Projects"
|
||||
value={metrics?.totalProjects || 0}
|
||||
icon={<Activity className="w-5 h-5 text-secondary" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Online Now"
|
||||
value={metrics?.onlineUsers || 0}
|
||||
icon={<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Verified Users"
|
||||
value={metrics?.verifiedUsers || 0}
|
||||
icon={<Shield className="w-5 h-5 text-primary" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Recent Profiles */}
|
||||
<div className="bg-card/50 border border-white/10 p-6">
|
||||
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-4 flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-primary" />
|
||||
Recent Architects
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{profiles?.slice(0, 5).map((profile: any) => (
|
||||
<div key={profile.id} className="flex items-center justify-between p-3 bg-black/20 border border-white/5">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={profile.avatar_url}
|
||||
alt={profile.username}
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm text-white font-bold">{profile.username}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Level {profile.level} • {profile.role}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-xs px-2 py-1 rounded ${
|
||||
profile.status === 'online'
|
||||
? 'bg-green-500/10 text-green-500'
|
||||
: 'bg-white/5 text-muted-foreground'
|
||||
}`}>
|
||||
{profile.status}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Projects */}
|
||||
<div className="bg-card/50 border border-white/10 p-6">
|
||||
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-4 flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-secondary" />
|
||||
Active Projects
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{projects?.slice(0, 5).map((project: any) => (
|
||||
<div key={project.id} className="p-3 bg-black/20 border border-white/5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-white font-bold">{project.title}</div>
|
||||
<div className="text-xs text-muted-foreground">{project.engine}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 h-1 bg-white/10 rounded overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-secondary"
|
||||
style={{ width: `${project.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-secondary font-bold">{project.progress}%</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* XP Stats */}
|
||||
<div className="mt-8 bg-card/50 border border-white/10 p-6">
|
||||
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-4">
|
||||
Ecosystem Stats
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<div className="text-3xl font-display font-bold text-primary">
|
||||
{metrics?.totalXP?.toLocaleString() || 0}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">Total XP Earned</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-display font-bold text-white">
|
||||
{metrics?.avgLevel || 1}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">Avg Level</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-display font-bold text-secondary">
|
||||
{profiles?.filter((p: any) => p.onboarded).length || 0}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">Onboarded</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-display font-bold text-white">
|
||||
{profiles?.filter((p: any) => p.role === 'admin').length || 0}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">Admins</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({ icon, label, href, active = false }: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
href: string;
|
||||
active?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<div className={`flex items-center gap-3 px-3 py-2 text-sm transition-colors cursor-pointer ${
|
||||
active
|
||||
? 'bg-primary/10 text-primary border-l-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-white hover:bg-white/5'
|
||||
}`}>
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-card/50 border border-white/10 p-6"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-widest font-bold">{title}</div>
|
||||
{icon}
|
||||
</div>
|
||||
<div className="text-3xl font-display font-bold text-white">{value}</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,29 +1,38 @@
|
|||
import { motion } from "framer-motion";
|
||||
import { Link } from "wouter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowLeft, Users, ShieldAlert, Globe, Activity, TrendingUp, Target } from "lucide-react";
|
||||
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip, LineChart, Line } from "recharts";
|
||||
import { Bar, BarChart, ResponsiveContainer, LineChart, Line, Tooltip } from "recharts";
|
||||
import mapBg from '@assets/generated_images/abstract_holographic_world_map_data_visualization.png';
|
||||
|
||||
const MOCK_DATA = [
|
||||
{ name: "Mon", value: 400 },
|
||||
{ name: "Tue", value: 300 },
|
||||
{ name: "Wed", value: 550 },
|
||||
{ name: "Thu", value: 450 },
|
||||
{ name: "Fri", value: 700 },
|
||||
{ name: "Sat", value: 600 },
|
||||
{ name: "Sun", value: 800 },
|
||||
];
|
||||
|
||||
const THREAT_DATA = [
|
||||
{ name: "00:00", value: 12 },
|
||||
{ name: "04:00", value: 8 },
|
||||
{ name: "08:00", value: 45 },
|
||||
{ name: "12:00", value: 120 },
|
||||
{ name: "16:00", value: 90 },
|
||||
{ name: "20:00", value: 35 },
|
||||
];
|
||||
|
||||
export default function Dashboard() {
|
||||
const { data: metrics } = useQuery({
|
||||
queryKey: ["metrics"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/metrics");
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
const MOCK_DATA = [
|
||||
{ name: "Mon", value: 400 },
|
||||
{ name: "Tue", value: 300 },
|
||||
{ name: "Wed", value: 550 },
|
||||
{ name: "Thu", value: 450 },
|
||||
{ name: "Fri", value: 700 },
|
||||
{ name: "Sat", value: 600 },
|
||||
{ name: "Sun", value: 800 },
|
||||
];
|
||||
|
||||
const THREAT_DATA = [
|
||||
{ name: "00:00", value: 12 },
|
||||
{ name: "04:00", value: 8 },
|
||||
{ name: "08:00", value: 45 },
|
||||
{ name: "12:00", value: 120 },
|
||||
{ name: "16:00", value: 90 },
|
||||
{ name: "20:00", value: 35 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-mono relative overflow-hidden">
|
||||
|
||||
|
|
@ -47,7 +56,7 @@ export default function Dashboard() {
|
|||
<Globe className="w-8 h-8 text-primary" />
|
||||
Axiom Command
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm font-tech">Global Ecosystem Status // Real-time Telemetry</p>
|
||||
<p className="text-muted-foreground text-sm font-tech">Global Ecosystem Status // Live Data from Supabase</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
|
|
@ -60,18 +69,34 @@ export default function Dashboard() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Grid */}
|
||||
{/* KPI Grid - Live Data */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-10">
|
||||
<Card title="Active Architects" value="12,405" change="+12%" icon={<Users className="w-5 h-5 text-primary" />} />
|
||||
<Card title="Threats Blocked" value="1.2M" change="+5%" icon={<ShieldAlert className="w-5 h-5 text-destructive" />} />
|
||||
<Card title="Projects Deployed" value="8,932" change="+24%" icon={<Activity className="w-5 h-5 text-secondary" />} />
|
||||
<Card title="Avg. Skill Rating" value="94.2" change="+1.2" icon={<Target className="w-5 h-5 text-white" />} />
|
||||
<Card
|
||||
title="Active Architects"
|
||||
value={metrics?.totalProfiles || 0}
|
||||
icon={<Users className="w-5 h-5 text-primary" />}
|
||||
/>
|
||||
<Card
|
||||
title="Total Projects"
|
||||
value={metrics?.totalProjects || 0}
|
||||
icon={<Activity className="w-5 h-5 text-secondary" />}
|
||||
/>
|
||||
<Card
|
||||
title="Online Now"
|
||||
value={metrics?.onlineUsers || 0}
|
||||
icon={<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse" />}
|
||||
/>
|
||||
<Card
|
||||
title="Verified Users"
|
||||
value={metrics?.verifiedUsers || 0}
|
||||
icon={<ShieldAlert className="w-5 h-5 text-primary" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 flex-1">
|
||||
|
||||
{/* Map / Main Viz (Placeholder for now, using background) */}
|
||||
{/* Map / Main Viz */}
|
||||
<div className="md:col-span-2 bg-card/50 border border-white/10 p-6 backdrop-blur-sm flex flex-col relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-background/80 pointer-events-none" />
|
||||
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-6 flex items-center gap-2 relative z-10">
|
||||
|
|
@ -83,14 +108,11 @@ export default function Dashboard() {
|
|||
<div className="absolute top-1/4 left-1/4 w-3 h-3 bg-primary rounded-full animate-ping" />
|
||||
<div className="absolute top-1/4 left-1/4 w-3 h-3 bg-primary rounded-full" />
|
||||
|
||||
<div className="absolute top-1/3 left-1/2 w-3 h-3 bg-secondary rounded-full animate-ping delay-300" />
|
||||
<div className="absolute top-1/3 left-1/2 w-3 h-3 bg-secondary rounded-full animate-ping" style={{ animationDelay: '0.3s' }} />
|
||||
<div className="absolute top-1/3 left-1/2 w-3 h-3 bg-secondary rounded-full" />
|
||||
|
||||
<div className="absolute bottom-1/3 right-1/4 w-3 h-3 bg-destructive rounded-full animate-ping delay-700" />
|
||||
<div className="absolute bottom-1/3 right-1/4 w-3 h-3 bg-destructive rounded-full animate-ping" style={{ animationDelay: '0.7s' }} />
|
||||
<div className="absolute bottom-1/3 right-1/4 w-3 h-3 bg-destructive rounded-full" />
|
||||
|
||||
{/* Grid Overlay */}
|
||||
<div className="absolute inset-0 bg-[url(@assets/generated_images/dark_subtle_digital_grid_texture.png)] opacity-30 mix-blend-overlay" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -125,12 +147,45 @@ export default function Dashboard() {
|
|||
|
||||
</div>
|
||||
|
||||
{/* XP Stats */}
|
||||
<div className="mt-8 bg-card/50 border border-white/10 p-6">
|
||||
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-4">
|
||||
Ecosystem Stats (Live)
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<div className="text-3xl font-display font-bold text-primary">
|
||||
{metrics?.totalXP?.toLocaleString() || 0}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">Total XP Earned</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-display font-bold text-white">
|
||||
{metrics?.avgLevel || 1}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">Avg Level</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-display font-bold text-secondary">
|
||||
{metrics?.totalProfiles || 0}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">Registered</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-display font-bold text-green-500">
|
||||
{metrics?.onlineUsers || 0}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase">Online Now</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Card({ title, value, change, icon }: { title: string, value: string, change: string, icon: React.ReactNode }) {
|
||||
function Card({ title, value, icon }: { title: string, value: number, icon: React.ReactNode }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
|
|
@ -143,7 +198,6 @@ function Card({ title, value, change, icon }: { title: string, value: string, ch
|
|||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="text-3xl font-display font-bold text-white">{value}</div>
|
||||
<div className="text-xs text-green-500 font-bold mb-1">{change}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
import { motion } from "framer-motion";
|
||||
import { Link } from "wouter";
|
||||
import { Shield, FileCode, Terminal as TerminalIcon, ChevronRight, BarChart3, Network } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Shield, FileCode, Terminal as TerminalIcon, ChevronRight, BarChart3, Network, ExternalLink, Lock } from "lucide-react";
|
||||
import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png';
|
||||
|
||||
export default function Home() {
|
||||
const { data: metrics } = useQuery({
|
||||
queryKey: ["metrics"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/metrics");
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-mono selection:bg-primary selection:text-background relative overflow-hidden">
|
||||
{/* Background Texture */}
|
||||
|
|
@ -12,7 +21,37 @@ export default function Home() {
|
|||
style={{ backgroundImage: `url(${gridBg})`, backgroundSize: 'cover' }}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 container mx-auto px-4 py-20 flex flex-col items-center justify-center min-h-screen">
|
||||
{/* Navigation */}
|
||||
<nav className="relative z-20 flex justify-between items-center px-8 py-6 border-b border-white/5">
|
||||
<div className="text-xl font-display font-bold text-white uppercase tracking-widest">
|
||||
AeThex
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<a
|
||||
href="https://aethex.foundation"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-white transition-colors flex items-center gap-1"
|
||||
>
|
||||
Foundation <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
<a
|
||||
href="https://aethex.studio"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-white transition-colors flex items-center gap-1"
|
||||
>
|
||||
Studio <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
<Link href="/login">
|
||||
<button className="text-sm bg-white/5 border border-white/10 px-4 py-2 text-white hover:bg-white/10 transition-colors flex items-center gap-2" data-testid="button-admin-login">
|
||||
<Lock className="w-3 h-3" /> Admin
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="relative z-10 container mx-auto px-4 py-20 flex flex-col items-center justify-center">
|
||||
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
|
|
@ -21,21 +60,48 @@ export default function Home() {
|
|||
className="text-center mb-16 space-y-4"
|
||||
>
|
||||
<div className="inline-block border border-primary/30 px-3 py-1 text-xs text-primary tracking-widest uppercase mb-4 bg-primary/5">
|
||||
System Online: v4.2
|
||||
The Operating System for the Metaverse
|
||||
</div>
|
||||
<h1 className="text-6xl md:text-8xl font-display font-bold tracking-tighter uppercase text-white mb-2 text-glow">
|
||||
AeThex
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-lg md:text-xl max-w-2xl mx-auto font-tech">
|
||||
The Operating System for the Metaverse.
|
||||
We train Architects. We build the Shield. We define the Law.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Live Metrics */}
|
||||
{metrics && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-16 w-full max-w-4xl"
|
||||
>
|
||||
<div className="text-center p-4 border border-white/5 bg-card/30">
|
||||
<div className="text-3xl font-display font-bold text-primary">{metrics.totalProfiles}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-wider">Architects</div>
|
||||
</div>
|
||||
<div className="text-center p-4 border border-white/5 bg-card/30">
|
||||
<div className="text-3xl font-display font-bold text-secondary">{metrics.totalProjects}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-wider">Projects</div>
|
||||
</div>
|
||||
<div className="text-center p-4 border border-white/5 bg-card/30">
|
||||
<div className="text-3xl font-display font-bold text-green-500">{metrics.onlineUsers}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-wider">Online Now</div>
|
||||
</div>
|
||||
<div className="text-center p-4 border border-white/5 bg-card/30">
|
||||
<div className="text-3xl font-display font-bold text-white">{metrics.totalXP?.toLocaleString()}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-wider">Total XP</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* The Trinity Cards */}
|
||||
<div className="grid md:grid-cols-3 gap-6 w-full max-w-6xl">
|
||||
|
||||
{/* Axiom -> Dashboard */}
|
||||
<Link href="/dashboard">
|
||||
{/* Axiom -> Pitch */}
|
||||
<Link href="/pitch">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -49,16 +115,16 @@ export default function Home() {
|
|||
</div>
|
||||
<h2 className="text-2xl font-display text-white mb-4">Axiom</h2>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed mb-6">
|
||||
The Foundation. View the global command center, active architect metrics, and ecosystem health.
|
||||
<span className="text-primary font-bold">The Law.</span> Our dual-entity protocol creates a self-sustaining ecosystem. The Foundation trains; the Corporation secures.
|
||||
</p>
|
||||
<div className="flex items-center text-primary hover:text-primary/80 text-sm font-bold uppercase tracking-wider mt-auto">
|
||||
Open Dashboard <ChevronRight className="w-4 h-4 ml-1" />
|
||||
View Investor Pitch <ChevronRight className="w-4 h-4 ml-1" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
|
||||
{/* Codex -> Curriculum (with Passport link inside) */}
|
||||
<Link href="/curriculum">
|
||||
{/* Codex -> Foundation */}
|
||||
<a href="https://aethex.foundation" target="_blank" rel="noopener noreferrer" className="block">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -72,16 +138,16 @@ export default function Home() {
|
|||
</div>
|
||||
<h2 className="text-2xl font-display text-white mb-4">Codex</h2>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed mb-6">
|
||||
The Standard. Explore the skill tree, mastery nodes, and view your Architect Credential.
|
||||
<span className="text-secondary font-bold">The Standard.</span> Elite training through gamified curriculum. Certifications that employers trust. The Passport to the Metaverse.
|
||||
</p>
|
||||
<div className="flex items-center text-secondary hover:text-secondary/80 text-sm font-bold uppercase tracking-wider mt-auto">
|
||||
View Tech Tree <ChevronRight className="w-4 h-4 ml-1" />
|
||||
Enter Foundation <ExternalLink className="w-4 h-4 ml-1" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
</a>
|
||||
|
||||
{/* Aegis -> Terminal */}
|
||||
<Link href="/terminal">
|
||||
{/* Aegis -> Studio */}
|
||||
<a href="https://aethex.studio" target="_blank" rel="noopener noreferrer" className="block">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -92,24 +158,48 @@ export default function Home() {
|
|||
<TerminalIcon className="w-12 h-12 text-muted-foreground group-hover:text-destructive mb-6 transition-colors" />
|
||||
<h2 className="text-2xl font-display text-white mb-4">Aegis</h2>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed mb-6">
|
||||
The Shield. Enter the secure build environment. <span className="text-destructive font-bold">New:</span> Live Threat Simulation available.
|
||||
<span className="text-destructive font-bold">The Shield.</span> Real-time security for the build environment. PII scrubbing. Threat intervention. Protection for every line of code.
|
||||
</p>
|
||||
<div className="flex items-center text-destructive hover:text-destructive/80 text-sm font-bold uppercase tracking-wider mt-auto">
|
||||
Launch Terminal <ChevronRight className="w-4 h-4 ml-1" />
|
||||
Launch Studio <ExternalLink className="w-4 h-4 ml-1" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Demo Links */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="mt-16 flex flex-wrap justify-center gap-4"
|
||||
>
|
||||
<Link href="/passport">
|
||||
<button className="text-sm border border-white/10 px-6 py-3 text-muted-foreground hover:text-white hover:border-white/30 transition-colors">
|
||||
View Sample Passport
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/terminal">
|
||||
<button className="text-sm border border-white/10 px-6 py-3 text-muted-foreground hover:text-white hover:border-white/30 transition-colors">
|
||||
Try Terminal Demo
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/curriculum">
|
||||
<button className="text-sm border border-white/10 px-6 py-3 text-muted-foreground hover:text-white hover:border-white/30 transition-colors">
|
||||
Explore Tech Tree
|
||||
</button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Footer */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="mt-24 text-center text-xs text-muted-foreground/50 uppercase tracking-widest"
|
||||
>
|
||||
AeThex Foundry © 2025 // Authorized Personnel Only
|
||||
AeThex Foundry © 2025 // Building the Future of the Metaverse
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
118
client/src/pages/login.tsx
Normal file
118
client/src/pages/login.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useLocation } from "wouter";
|
||||
import { Shield, Lock, AlertCircle } from "lucide-react";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png';
|
||||
|
||||
export default function Login() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await login(username, password);
|
||||
setLocation("/admin");
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Login failed");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-mono flex items-center justify-center relative overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 opacity-20 pointer-events-none z-0"
|
||||
style={{ backgroundImage: `url(${gridBg})`, backgroundSize: 'cover' }}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="relative z-10 w-full max-w-md p-8"
|
||||
>
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 border border-primary/30 mb-4">
|
||||
<Shield className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-display font-bold text-white uppercase tracking-wider">
|
||||
AeThex Command
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm mt-2">
|
||||
Authorized Personnel Only
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/30 text-destructive text-sm" data-testid="error-message">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-widest">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full bg-card border border-white/10 px-4 py-3 text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none transition-colors"
|
||||
placeholder="Enter username"
|
||||
data-testid="input-username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-muted-foreground uppercase tracking-widest">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-card border border-white/10 px-4 py-3 text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none transition-colors"
|
||||
placeholder="Enter password"
|
||||
data-testid="input-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full bg-primary text-background py-3 font-bold uppercase tracking-wider hover:bg-primary/90 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
data-testid="button-login"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>Processing...</>
|
||||
) : (
|
||||
<>
|
||||
<Lock className="w-4 h-4" />
|
||||
Authenticate
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 text-center text-xs text-muted-foreground/50">
|
||||
<a href="/" className="hover:text-primary transition-colors">
|
||||
Return to Public Site
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
208
client/src/pages/pitch.tsx
Normal file
208
client/src/pages/pitch.tsx
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import { motion } from "framer-motion";
|
||||
import { Link } from "wouter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowLeft, Shield, FileCode, Terminal, ChevronRight, Building, GraduationCap, Users, Activity } from "lucide-react";
|
||||
import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png';
|
||||
|
||||
export default function Pitch() {
|
||||
const { data: metrics } = useQuery({
|
||||
queryKey: ["metrics"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/metrics");
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-mono relative overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 opacity-10 pointer-events-none z-0"
|
||||
style={{ backgroundImage: `url(${gridBg})`, backgroundSize: 'cover' }}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 container mx-auto px-4 py-12 max-w-5xl">
|
||||
|
||||
<Link href="/">
|
||||
<button className="text-muted-foreground hover:text-primary transition-colors flex items-center gap-2 uppercase text-xs tracking-widest mb-12">
|
||||
<ArrowLeft className="w-4 h-4" /> Return Home
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
{/* Title */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<div className="inline-block border border-primary/30 px-3 py-1 text-xs text-primary tracking-widest uppercase mb-6 bg-primary/5">
|
||||
The Axiom Protocol
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-6xl font-display font-bold text-white uppercase tracking-tight mb-4">
|
||||
Investor Brief
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
|
||||
AeThex is a self-sustaining ecosystem built on a dual-entity model that transforms raw talent into certified Metaverse Architects.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* The Dual Entity Model */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="grid md:grid-cols-2 gap-8 mb-20"
|
||||
>
|
||||
{/* Foundation */}
|
||||
<div className="bg-card/50 border border-secondary/30 p-8 relative overflow-hidden group hover:border-secondary/50 transition-colors">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-secondary/50" />
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-12 h-12 bg-secondary/10 rounded-full flex items-center justify-center">
|
||||
<GraduationCap className="w-6 h-6 text-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-secondary uppercase tracking-widest font-bold">Non-Profit</div>
|
||||
<h3 className="text-xl font-display text-white uppercase">The Foundation</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed mb-6">
|
||||
Uses <span className="text-secondary font-bold">The Codex</span> to train elite Architects.
|
||||
Gamified curriculum, verified certifications, and a clear path from beginner to master.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<FileCode className="w-4 h-4 text-secondary" />
|
||||
Codex Standard Certification
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Users className="w-4 h-4 text-secondary" />
|
||||
{metrics?.totalProfiles || 0} Active Architects
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Corporation */}
|
||||
<div className="bg-card/50 border border-primary/30 p-8 relative overflow-hidden group hover:border-primary/50 transition-colors">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-primary/50" />
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
|
||||
<Building className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-primary uppercase tracking-widest font-bold">For-Profit</div>
|
||||
<h3 className="text-xl font-display text-white uppercase">The Corporation</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed mb-6">
|
||||
Builds <span className="text-primary font-bold">The Aegis</span> to secure the Metaverse.
|
||||
Enterprise security layer, PII scrubbing, threat intervention—sold to platforms and publishers.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Shield className="w-4 h-4 text-primary" />
|
||||
Real-time Security Layer
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Activity className="w-4 h-4 text-primary" />
|
||||
{metrics?.totalProjects || 0} Protected Projects
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* The Holy Trinity */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="mb-20"
|
||||
>
|
||||
<h2 className="text-center text-sm font-bold text-muted-foreground uppercase tracking-widest mb-8">
|
||||
The Holy Trinity
|
||||
</h2>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<div className="text-center p-6 border border-white/10 bg-card/30">
|
||||
<Shield className="w-10 h-10 text-primary mx-auto mb-4" />
|
||||
<h3 className="font-display text-white uppercase mb-2">Axiom</h3>
|
||||
<p className="text-xs text-muted-foreground">The Law. The foundational protocol governing the ecosystem.</p>
|
||||
</div>
|
||||
<div className="text-center p-6 border border-white/10 bg-card/30">
|
||||
<FileCode className="w-10 h-10 text-secondary mx-auto mb-4" />
|
||||
<h3 className="font-display text-white uppercase mb-2">Codex</h3>
|
||||
<p className="text-xs text-muted-foreground">The Standard. Certification that proves mastery.</p>
|
||||
</div>
|
||||
<div className="text-center p-6 border border-white/10 bg-card/30">
|
||||
<Terminal className="w-10 h-10 text-destructive mx-auto mb-4" />
|
||||
<h3 className="font-display text-white uppercase mb-2">Aegis</h3>
|
||||
<p className="text-xs text-muted-foreground">The Shield. Real-time protection for builders.</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* The Pitch Quote */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="bg-primary/5 border border-primary/30 p-10 mb-20"
|
||||
>
|
||||
<blockquote className="text-xl md:text-2xl text-white leading-relaxed font-display italic">
|
||||
"We take raw talent, train them on our laws, and arm them with our weapons.
|
||||
<span className="text-primary"> The Foundation creates the workforce; The Corporation sells the security.</span>"
|
||||
</blockquote>
|
||||
</motion.div>
|
||||
|
||||
{/* Live Metrics */}
|
||||
{metrics && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="mb-20"
|
||||
>
|
||||
<h2 className="text-center text-sm font-bold text-muted-foreground uppercase tracking-widest mb-8">
|
||||
Live Ecosystem Metrics
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div className="text-center p-6 border border-white/10 bg-card/30">
|
||||
<div className="text-4xl font-display font-bold text-primary">{metrics.totalProfiles}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase mt-2">Architects</div>
|
||||
</div>
|
||||
<div className="text-center p-6 border border-white/10 bg-card/30">
|
||||
<div className="text-4xl font-display font-bold text-secondary">{metrics.totalProjects}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase mt-2">Projects</div>
|
||||
</div>
|
||||
<div className="text-center p-6 border border-white/10 bg-card/30">
|
||||
<div className="text-4xl font-display font-bold text-white">{metrics.totalXP?.toLocaleString()}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase mt-2">Total XP</div>
|
||||
</div>
|
||||
<div className="text-center p-6 border border-white/10 bg-card/30">
|
||||
<div className="text-4xl font-display font-bold text-green-500">{metrics.onlineUsers}</div>
|
||||
<div className="text-xs text-muted-foreground uppercase mt-2">Online Now</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* CTA */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="text-center"
|
||||
>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Ready to learn more about investment opportunities?
|
||||
</p>
|
||||
<a
|
||||
href="mailto:invest@aethex.dev"
|
||||
className="inline-block bg-primary text-background px-8 py-4 font-bold uppercase tracking-wider hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Contact Our Team
|
||||
</a>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
package-lock.json
generated
43
package-lock.json
generated
|
|
@ -40,6 +40,8 @@
|
|||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@supabase/supabase-js": "^2.87.3",
|
||||
"@tanstack/react-query": "^5.60.5",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
|
@ -4449,6 +4451,15 @@
|
|||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
|
|
@ -4828,6 +4839,20 @@
|
|||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
|
|
@ -6545,12 +6570,20 @@
|
|||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.3",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
|
||||
"integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==",
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@
|
|||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@supabase/supabase-js": "^2.87.3",
|
||||
"@tanstack/react-query": "^5.60.5",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import session from "express-session";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { serveStatic } from "./static";
|
||||
import { createServer } from "http";
|
||||
|
|
@ -12,6 +13,27 @@ declare module "http" {
|
|||
}
|
||||
}
|
||||
|
||||
// Require session secret in production
|
||||
const sessionSecret = process.env.SESSION_SECRET;
|
||||
if (process.env.NODE_ENV === "production" && !sessionSecret) {
|
||||
throw new Error("SESSION_SECRET environment variable is required in production");
|
||||
}
|
||||
|
||||
// Session configuration with security best practices
|
||||
app.use(
|
||||
session({
|
||||
secret: sessionSecret || "dev-only-secret-not-for-prod",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
httpOnly: true,
|
||||
sameSite: "strict", // CSRF protection
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
app.use(
|
||||
express.json({
|
||||
verify: (req, _res, buf) => {
|
||||
|
|
|
|||
224
server/routes.ts
224
server/routes.ts
|
|
@ -1,90 +1,186 @@
|
|||
import type { Express } from "express";
|
||||
import type { Express, Request, Response, NextFunction } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import { supabase } from "./supabase";
|
||||
import { loginSchema } from "@shared/schema";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
// Extend session type
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
userId?: string;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
// Auth middleware - requires any authenticated user
|
||||
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.session.userId) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Admin middleware - requires authenticated admin user
|
||||
function requireAdmin(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.session.userId) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
if (!req.session.isAdmin) {
|
||||
return res.status(403).json({ error: "Admin access required" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
export async function registerRoutes(
|
||||
httpServer: Server,
|
||||
app: Express
|
||||
): Promise<Server> {
|
||||
|
||||
// API: Explore database schema (temporary - for development)
|
||||
app.get("/api/schema", async (req, res) => {
|
||||
// ========== AUTH ROUTES ==========
|
||||
|
||||
// Login
|
||||
app.post("/api/auth/login", async (req, res) => {
|
||||
try {
|
||||
// Query information_schema to get all tables
|
||||
const { data, error } = await supabase
|
||||
.from('information_schema.tables')
|
||||
.select('table_name')
|
||||
.eq('table_schema', 'public');
|
||||
|
||||
if (error) {
|
||||
// Alternative: try querying a known table or use RPC
|
||||
// Let's try to get tables via a different approach
|
||||
const tablesQuery = await supabase.rpc('get_tables');
|
||||
if (tablesQuery.error) {
|
||||
return res.json({
|
||||
error: error.message,
|
||||
hint: "Could not query schema. Tables may need to be accessed directly.",
|
||||
supabaseConnected: true
|
||||
});
|
||||
}
|
||||
return res.json({ tables: tablesQuery.data });
|
||||
const result = loginSchema.safeParse(req.body);
|
||||
if (!result.success) {
|
||||
return res.status(400).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
res.json({ tables: data });
|
||||
const { username, password } = result.data;
|
||||
const user = await storage.getUserByUsername(username);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
if (!isValid) {
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
// Regenerate session on login to prevent session fixation
|
||||
req.session.regenerate((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: "Session error" });
|
||||
}
|
||||
|
||||
req.session.userId = user.id;
|
||||
req.session.isAdmin = user.is_admin ?? false;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
isAdmin: user.is_admin
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API: Test specific tables that might exist
|
||||
app.get("/api/explore", async (req, res) => {
|
||||
const potentialTables = [
|
||||
'users', 'architects', 'profiles', 'credentials', 'certificates',
|
||||
'skills', 'curriculum', 'courses', 'modules', 'lessons',
|
||||
'threats', 'events', 'logs', 'projects', 'teams',
|
||||
'organizations', 'members', 'enrollments', 'progress'
|
||||
];
|
||||
|
||||
const results: Record<string, any> = {};
|
||||
|
||||
for (const table of potentialTables) {
|
||||
try {
|
||||
const { data, error, count } = await supabase
|
||||
.from(table)
|
||||
.select('*', { count: 'exact', head: true });
|
||||
|
||||
if (!error) {
|
||||
results[table] = { exists: true, count };
|
||||
}
|
||||
} catch (e) {
|
||||
// Table doesn't exist, skip
|
||||
|
||||
// Logout
|
||||
app.post("/api/auth/logout", (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: "Logout failed" });
|
||||
}
|
||||
res.clearCookie('connect.sid');
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
// Get current session
|
||||
app.get("/api/auth/session", async (req, res) => {
|
||||
if (!req.session.userId) {
|
||||
return res.json({ authenticated: false });
|
||||
}
|
||||
|
||||
const user = await storage.getUser(req.session.userId);
|
||||
if (!user) {
|
||||
return res.json({ authenticated: false });
|
||||
}
|
||||
|
||||
res.json({
|
||||
foundTables: Object.keys(results),
|
||||
details: results,
|
||||
supabaseUrl: process.env.SUPABASE_URL ? 'configured' : 'missing'
|
||||
authenticated: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
isAdmin: user.is_admin
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API: Get sample data from a specific table
|
||||
app.get("/api/table/:name", async (req, res) => {
|
||||
const { name } = req.params;
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
|
||||
|
||||
// ========== PUBLIC API ROUTES ==========
|
||||
|
||||
// Get ecosystem metrics (public - for dashboard)
|
||||
app.get("/api/metrics", async (req, res) => {
|
||||
try {
|
||||
const { data, error, count } = await supabase
|
||||
.from(name)
|
||||
.select('*', { count: 'exact' })
|
||||
.limit(limit);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.message });
|
||||
const metrics = await storage.getMetrics();
|
||||
res.json(metrics);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ========== ADMIN-PROTECTED API ROUTES ==========
|
||||
|
||||
// Get all profiles (admin only)
|
||||
app.get("/api/profiles", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const profiles = await storage.getProfiles();
|
||||
res.json(profiles);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single profile (admin only)
|
||||
app.get("/api/profiles/:id", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const profile = await storage.getProfile(req.params.id);
|
||||
if (!profile) {
|
||||
return res.status(404).json({ error: "Profile not found" });
|
||||
}
|
||||
|
||||
res.json({ table: name, count, sample: data });
|
||||
res.json(profile);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Update profile (admin only)
|
||||
app.patch("/api/profiles/:id", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const profile = await storage.updateProfile(req.params.id, req.body);
|
||||
if (!profile) {
|
||||
return res.status(404).json({ error: "Profile not found" });
|
||||
}
|
||||
res.json(profile);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all projects (admin only)
|
||||
app.get("/api/projects", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const projects = await storage.getProjects();
|
||||
res.json(projects);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single project (admin only)
|
||||
app.get("/api/projects/:id", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const project = await storage.getProject(req.params.id);
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: "Project not found" });
|
||||
}
|
||||
res.json(project);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,149 @@
|
|||
import { type User, type InsertUser } from "@shared/schema";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
// modify the interface with any CRUD methods
|
||||
// you might need
|
||||
import { type User, type Profile, type Project } from "@shared/schema";
|
||||
import { supabase } from "./supabase";
|
||||
|
||||
export interface IStorage {
|
||||
// Users
|
||||
getUser(id: string): Promise<User | undefined>;
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
createUser(user: InsertUser): Promise<User>;
|
||||
|
||||
// Profiles
|
||||
getProfiles(): Promise<Profile[]>;
|
||||
getProfile(id: string): Promise<Profile | undefined>;
|
||||
getProfileByUsername(username: string): Promise<Profile | undefined>;
|
||||
updateProfile(id: string, data: Partial<Profile>): Promise<Profile | undefined>;
|
||||
|
||||
// Projects
|
||||
getProjects(): Promise<Project[]>;
|
||||
getProject(id: string): Promise<Project | undefined>;
|
||||
|
||||
// Metrics
|
||||
getMetrics(): Promise<{
|
||||
totalProfiles: number;
|
||||
totalProjects: number;
|
||||
onlineUsers: number;
|
||||
verifiedUsers: number;
|
||||
totalXP: number;
|
||||
avgLevel: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class MemStorage implements IStorage {
|
||||
private users: Map<string, User>;
|
||||
|
||||
constructor() {
|
||||
this.users = new Map();
|
||||
}
|
||||
|
||||
export class SupabaseStorage implements IStorage {
|
||||
|
||||
async getUser(id: string): Promise<User | undefined> {
|
||||
return this.users.get(id);
|
||||
const { data, error } = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (error || !data) return undefined;
|
||||
return data as User;
|
||||
}
|
||||
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
return Array.from(this.users.values()).find(
|
||||
(user) => user.username === username,
|
||||
);
|
||||
const { data, error } = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('username', username)
|
||||
.single();
|
||||
|
||||
if (error || !data) return undefined;
|
||||
return data as User;
|
||||
}
|
||||
|
||||
async createUser(insertUser: InsertUser): Promise<User> {
|
||||
const id = randomUUID();
|
||||
const user: User = { ...insertUser, id };
|
||||
this.users.set(id, user);
|
||||
return user;
|
||||
|
||||
async getProfiles(): Promise<Profile[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) return [];
|
||||
return data as Profile[];
|
||||
}
|
||||
|
||||
async getProfile(id: string): Promise<Profile | undefined> {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (error || !data) return undefined;
|
||||
return data as Profile;
|
||||
}
|
||||
|
||||
async getProfileByUsername(username: string): Promise<Profile | undefined> {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('username', username)
|
||||
.single();
|
||||
|
||||
if (error || !data) return undefined;
|
||||
return data as Profile;
|
||||
}
|
||||
|
||||
async updateProfile(id: string, updates: Partial<Profile>): Promise<Profile | undefined> {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.update({ ...updates, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error || !data) return undefined;
|
||||
return data as Profile;
|
||||
}
|
||||
|
||||
async getProjects(): Promise<Project[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) return [];
|
||||
return data as Project[];
|
||||
}
|
||||
|
||||
async getProject(id: string): Promise<Project | undefined> {
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (error || !data) return undefined;
|
||||
return data as Project;
|
||||
}
|
||||
|
||||
async getMetrics(): Promise<{
|
||||
totalProfiles: number;
|
||||
totalProjects: number;
|
||||
onlineUsers: number;
|
||||
verifiedUsers: number;
|
||||
totalXP: number;
|
||||
avgLevel: number;
|
||||
}> {
|
||||
// Get profiles for metrics
|
||||
const profiles = await this.getProfiles();
|
||||
const projects = await this.getProjects();
|
||||
|
||||
const onlineUsers = profiles.filter(p => p.status === 'online').length;
|
||||
const verifiedUsers = profiles.filter(p => p.is_verified).length;
|
||||
const totalXP = profiles.reduce((sum, p) => sum + (p.total_xp || 0), 0);
|
||||
const avgLevel = profiles.length > 0
|
||||
? profiles.reduce((sum, p) => sum + (p.level || 1), 0) / profiles.length
|
||||
: 1;
|
||||
|
||||
return {
|
||||
totalProfiles: profiles.length,
|
||||
totalProjects: projects.length,
|
||||
onlineUsers,
|
||||
verifiedUsers,
|
||||
totalXP,
|
||||
avgLevel: Math.round(avgLevel * 10) / 10,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new MemStorage();
|
||||
export const storage = new SupabaseStorage();
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { sql } from "drizzle-orm";
|
||||
import { pgTable, text, varchar } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, varchar, boolean, integer, timestamp, json } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
// Users table (auth)
|
||||
export const users = pgTable("users", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
id: varchar("id").primaryKey(),
|
||||
username: text("username").notNull().unique(),
|
||||
password: text("password").notNull(),
|
||||
is_active: boolean("is_active").default(true),
|
||||
is_admin: boolean("is_admin").default(false),
|
||||
created_at: timestamp("created_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const insertUserSchema = createInsertSchema(users).pick({
|
||||
|
|
@ -16,3 +19,73 @@ export const insertUserSchema = createInsertSchema(users).pick({
|
|||
|
||||
export type InsertUser = z.infer<typeof insertUserSchema>;
|
||||
export type User = typeof users.$inferSelect;
|
||||
|
||||
// Profiles table (rich user data)
|
||||
export const profiles = pgTable("profiles", {
|
||||
id: varchar("id").primaryKey(),
|
||||
username: text("username"),
|
||||
role: text("role").default("member"),
|
||||
onboarded: boolean("onboarded").default(false),
|
||||
bio: text("bio"),
|
||||
skills: json("skills").$type<string[] | null>(),
|
||||
avatar_url: text("avatar_url"),
|
||||
banner_url: text("banner_url"),
|
||||
social_links: json("social_links").$type<Record<string, string>>(),
|
||||
loyalty_points: integer("loyalty_points").default(0),
|
||||
email: text("email"),
|
||||
created_at: timestamp("created_at").defaultNow(),
|
||||
updated_at: timestamp("updated_at").defaultNow(),
|
||||
user_type: text("user_type").default("community_member"),
|
||||
experience_level: text("experience_level").default("beginner"),
|
||||
full_name: text("full_name"),
|
||||
location: text("location"),
|
||||
total_xp: integer("total_xp").default(0),
|
||||
level: integer("level").default(1),
|
||||
aethex_passport_id: varchar("aethex_passport_id"),
|
||||
status: text("status").default("offline"),
|
||||
is_verified: boolean("is_verified").default(false),
|
||||
});
|
||||
|
||||
export const insertProfileSchema = createInsertSchema(profiles).omit({
|
||||
id: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
});
|
||||
|
||||
export type InsertProfile = z.infer<typeof insertProfileSchema>;
|
||||
export type Profile = typeof profiles.$inferSelect;
|
||||
|
||||
// Projects table
|
||||
export const projects = pgTable("projects", {
|
||||
id: varchar("id").primaryKey(),
|
||||
owner_id: varchar("owner_id"),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
status: text("status").default("planning"),
|
||||
github_url: text("github_url"),
|
||||
created_at: timestamp("created_at").defaultNow(),
|
||||
updated_at: timestamp("updated_at").defaultNow(),
|
||||
user_id: varchar("user_id"),
|
||||
engine: text("engine"),
|
||||
priority: text("priority").default("medium"),
|
||||
progress: integer("progress").default(0),
|
||||
live_url: text("live_url"),
|
||||
technologies: json("technologies").$type<string[] | null>(),
|
||||
});
|
||||
|
||||
export const insertProjectSchema = createInsertSchema(projects).omit({
|
||||
id: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
});
|
||||
|
||||
export type InsertProject = z.infer<typeof insertProjectSchema>;
|
||||
export type Project = typeof projects.$inferSelect;
|
||||
|
||||
// Login schema for validation
|
||||
export const loginSchema = z.object({
|
||||
username: z.string().min(1, "Username is required"),
|
||||
password: z.string().min(1, "Password is required"),
|
||||
});
|
||||
|
||||
export type LoginInput = z.infer<typeof loginSchema>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue