From 8ee5f71ef4bdd690d9f5c006e0b3b2f305531f05 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Mon, 15 Dec 2025 22:15:36 +0000 Subject: [PATCH] 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 --- client/src/App.tsx | 21 +- client/src/lib/auth.tsx | 89 +++++++ client/src/pages/admin-aegis.tsx | 194 ++++++++++++++ client/src/pages/admin-architects.tsx | 354 +++++++++++++++++++++++++ client/src/pages/admin-credentials.tsx | 167 ++++++++++++ client/src/pages/admin-projects.tsx | 193 ++++++++++++++ client/src/pages/admin.tsx | 284 ++++++++++++++++++++ client/src/pages/dashboard.tsx | 122 ++++++--- client/src/pages/home.tsx | 130 +++++++-- client/src/pages/login.tsx | 118 +++++++++ client/src/pages/pitch.tsx | 208 +++++++++++++++ package-lock.json | 43 ++- package.json | 2 + server/index.ts | 22 ++ server/routes.ts | 224 +++++++++++----- server/storage.ts | 161 +++++++++-- shared/schema.ts | 79 +++++- 17 files changed, 2258 insertions(+), 153 deletions(-) create mode 100644 client/src/lib/auth.tsx create mode 100644 client/src/pages/admin-aegis.tsx create mode 100644 client/src/pages/admin-architects.tsx create mode 100644 client/src/pages/admin-credentials.tsx create mode 100644 client/src/pages/admin-projects.tsx create mode 100644 client/src/pages/admin.tsx create mode 100644 client/src/pages/login.tsx create mode 100644 client/src/pages/pitch.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 91cbd05..cad758f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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() { + + + + + + + ); @@ -25,8 +40,10 @@ function Router() { function App() { return ( - - + + + + ); } diff --git a/client/src/lib/auth.tsx b/client/src/lib/auth.tsx new file mode 100644 index 0000000..682dd75 --- /dev/null +++ b/client/src/lib/auth.tsx @@ -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; + logout: () => Promise; +} + +const AuthContext = createContext(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 ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/client/src/pages/admin-aegis.tsx b/client/src/pages/admin-aegis.tsx new file mode 100644 index 0000000..4aa54b7 --- /dev/null +++ b/client/src/pages/admin-aegis.tsx @@ -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 ( +
+
Loading...
+
+ ); + } + + 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 ( +
+ {/* Sidebar */} +
+
+

+ AeThex +

+

Command Center

+
+ + + +
+
+
+ +
+
+
{user?.username}
+
+ {user?.isAdmin ? "Administrator" : "Member"} +
+
+
+ +
+
+ + {/* Main Content */} +
+
+
+
+

+ Aegis Monitor +

+

+ Real-time security monitoring and threat intervention +

+
+
+ + Shield Active +
+
+ + {/* Stats */} +
+
+
Threats Blocked
+
247
+
Last 24 hours
+
+
+
PII Scrubbed
+
1,892
+
Instances protected
+
+
+
Active Sessions
+
34
+
Being monitored
+
+
+
Uptime
+
99.9%
+
Last 30 days
+
+
+ + {/* Recent Activity */} +
+

+ + Recent Threat Activity +

+ +
+ {mockThreats.map((threat) => ( +
+
+
+ +
+
+
{threat.type}
+
{threat.timestamp}
+
+
+
+ + {threat.status} + + +
+
+ ))} +
+ +
+

+ Aegis security layer is monitoring all active sessions +

+
+
+
+
+
+ ); +} + +function NavItem({ icon, label, href, active = false }: { + icon: React.ReactNode; + label: string; + href: string; + active?: boolean; +}) { + return ( + +
+ {icon} + {label} +
+ + ); +} diff --git a/client/src/pages/admin-architects.tsx b/client/src/pages/admin-architects.tsx new file mode 100644 index 0000000..8bd7751 --- /dev/null +++ b/client/src/pages/admin-architects.tsx @@ -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(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 ( +
+
Loading...
+
+ ); + } + + 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 ( +
+ {/* Sidebar */} +
+
+

+ AeThex +

+

Command Center

+
+ + + +
+
+
+ +
+
+
{user?.username}
+
+ {user?.isAdmin ? "Administrator" : "Member"} +
+
+
+ +
+
+ + {/* Main Content */} +
+
+
+
+

+ Architects +

+

+ {profiles?.length || 0} registered architects +

+
+ + {/* Search */} +
+ + 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" + /> +
+
+ + {/* Table */} +
+ + + + + + + + + + + + + + {isLoading ? ( + + + + ) : filteredProfiles.length === 0 ? ( + + + + ) : ( + filteredProfiles.map((profile: any) => ( + + + + + + + + + + )) + )} + +
UserRoleLevelXPStatusVerifiedActions
+ Loading... +
+ No architects found +
+
+ {profile.username} +
+
{profile.username}
+
{profile.email}
+
+
+
+ + {profile.role} + + {profile.level}{profile.total_xp} + + {profile.status} + + + + + +
+
+
+
+ + {/* Profile Detail Modal */} + {selectedProfile && ( +
+ +
+
+ {selectedProfile.username} +
+

{selectedProfile.username}

+

{selectedProfile.email}

+
+
+ +
+ +
+
+ +
{selectedProfile.role}
+
+
+ +
{selectedProfile.level}
+
+
+ +
{selectedProfile.total_xp}
+
+
+ +
+ {selectedProfile.status} +
+
+
+ +
{selectedProfile.aethex_passport_id}
+
+
+ +
+ {selectedProfile.is_verified ? 'Yes' : 'No'} +
+
+
+ +
{selectedProfile.bio || 'No bio'}
+
+
+ +
+ {selectedProfile.skills?.map((skill: string, i: number) => ( + + {skill} + + )) || No skills listed} +
+
+
+ +
+ + +
+
+
+ )} +
+ ); +} + +function NavItem({ icon, label, href, active = false }: { + icon: React.ReactNode; + label: string; + href: string; + active?: boolean; +}) { + return ( + +
+ {icon} + {label} +
+ + ); +} diff --git a/client/src/pages/admin-credentials.tsx b/client/src/pages/admin-credentials.tsx new file mode 100644 index 0000000..563ba37 --- /dev/null +++ b/client/src/pages/admin-credentials.tsx @@ -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 ( +
+
Loading...
+
+ ); + } + + const handleLogout = async () => { + await logout(); + setLocation("/"); + }; + + return ( +
+ {/* Sidebar */} +
+
+

+ AeThex +

+

Command Center

+
+ + + +
+
+
+ +
+
+
{user?.username}
+
+ {user?.isAdmin ? "Administrator" : "Member"} +
+
+
+ +
+
+ + {/* Main Content */} +
+
+
+

+ Credential Issuance +

+

+ Manage Codex certifications and passport credentials +

+
+ + {/* Coming Soon Notice */} +
+
+ +
+

Codex Certification System

+

Under Development

+
+
+

+ The credential issuance system will allow administrators to: +

+
    +
  • +
    + Issue Architect Passports with unique identifiers +
  • +
  • +
    + Award skill certifications based on completed curriculum +
  • +
  • +
    + Verify and validate credentials across platforms +
  • +
  • +
    + Revoke credentials when necessary +
  • +
+
+ + {/* Placeholder Stats */} +
+
+
+ + Issued +
+
0
+
Passports Issued
+
+
+
+ + Pending +
+
0
+
Awaiting Review
+
+
+
+ + Revoked +
+
0
+
Credentials Revoked
+
+
+
+
+
+ ); +} + +function NavItem({ icon, label, href, active = false }: { + icon: React.ReactNode; + label: string; + href: string; + active?: boolean; +}) { + return ( + +
+ {icon} + {label} +
+ + ); +} diff --git a/client/src/pages/admin-projects.tsx b/client/src/pages/admin-projects.tsx new file mode 100644 index 0000000..41bd101 --- /dev/null +++ b/client/src/pages/admin-projects.tsx @@ -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 ( +
+
Loading...
+
+ ); + } + + const handleLogout = async () => { + await logout(); + setLocation("/"); + }; + + return ( +
+ {/* Sidebar */} +
+
+

+ AeThex +

+

Command Center

+
+ + + +
+
+
+ +
+
+
{user?.username}
+
+ {user?.isAdmin ? "Administrator" : "Member"} +
+
+
+ +
+
+ + {/* Main Content */} +
+
+
+

+ Projects +

+

+ {projects?.length || 0} active projects +

+
+ + {/* Projects Grid */} +
+ {isLoading ? ( +
+ Loading projects... +
+ ) : projects?.length === 0 ? ( +
+ No projects found +
+ ) : ( + projects?.map((project: any) => ( + +
+
+

{project.title}

+
{project.engine}
+
+ + {project.status} + +
+ +

+ {project.description || 'No description'} +

+ + {/* Progress Bar */} +
+
+ Progress + {project.progress}% +
+
+
+
+
+ +
+ + {project.priority} + + + {project.github_url && ( + + GitHub + + )} +
+ + )) + )} +
+
+
+
+ ); +} + +function NavItem({ icon, label, href, active = false }: { + icon: React.ReactNode; + label: string; + href: string; + active?: boolean; +}) { + return ( + +
+ {icon} + {label} +
+ + ); +} diff --git a/client/src/pages/admin.tsx b/client/src/pages/admin.tsx new file mode 100644 index 0000000..9cb7169 --- /dev/null +++ b/client/src/pages/admin.tsx @@ -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 ( +
+
Loading...
+
+ ); + } + + if (!isAuthenticated) { + return null; + } + + const handleLogout = async () => { + await logout(); + setLocation("/"); + }; + + return ( +
+ {/* Sidebar */} +
+
+

+ AeThex +

+

Command Center

+
+ + + +
+
+
+ +
+
+
{user?.username}
+
+ {user?.isAdmin ? "Administrator" : "Member"} +
+
+
+ +
+
+ + {/* Main Content */} +
+
+ +
+
+

+ Dashboard +

+

+ Real-time ecosystem metrics from Supabase +

+
+ + {/* Metrics Grid */} +
+ } + /> + } + /> + } + /> + } + /> +
+ + {/* Recent Activity */} +
+ {/* Recent Profiles */} +
+

+ + Recent Architects +

+
+ {profiles?.slice(0, 5).map((profile: any) => ( +
+
+ {profile.username} +
+
{profile.username}
+
+ Level {profile.level} • {profile.role} +
+
+
+
+ {profile.status} +
+
+ ))} +
+
+ + {/* Recent Projects */} +
+

+ + Active Projects +

+
+ {projects?.slice(0, 5).map((project: any) => ( +
+
+
{project.title}
+
{project.engine}
+
+
+
+
+
+
{project.progress}%
+
+
+ ))} +
+
+
+ + {/* XP Stats */} +
+

+ Ecosystem Stats +

+
+
+
+ {metrics?.totalXP?.toLocaleString() || 0} +
+
Total XP Earned
+
+
+
+ {metrics?.avgLevel || 1} +
+
Avg Level
+
+
+
+ {profiles?.filter((p: any) => p.onboarded).length || 0} +
+
Onboarded
+
+
+
+ {profiles?.filter((p: any) => p.role === 'admin').length || 0} +
+
Admins
+
+
+
+
+
+
+ ); +} + +function NavItem({ icon, label, href, active = false }: { + icon: React.ReactNode; + label: string; + href: string; + active?: boolean; +}) { + return ( + +
+ {icon} + {label} +
+ + ); +} + +function MetricCard({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) { + return ( + +
+
{title}
+ {icon} +
+
{value}
+
+ ); +} diff --git a/client/src/pages/dashboard.tsx b/client/src/pages/dashboard.tsx index 887c1fd..9a69561 100644 --- a/client/src/pages/dashboard.tsx +++ b/client/src/pages/dashboard.tsx @@ -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 (
@@ -47,7 +56,7 @@ export default function Dashboard() { Axiom Command -

Global Ecosystem Status // Real-time Telemetry

+

Global Ecosystem Status // Live Data from Supabase

@@ -60,18 +69,34 @@ export default function Dashboard() {
- {/* KPI Grid */} + {/* KPI Grid - Live Data */}
- } /> - } /> - } /> - } /> + } + /> + } + /> + } + /> + } + />
{/* Charts Section */}
- {/* Map / Main Viz (Placeholder for now, using background) */} + {/* Map / Main Viz */}

@@ -83,14 +108,11 @@ export default function Dashboard() {
-
+
-
+
- - {/* Grid Overlay */} -
@@ -125,12 +147,45 @@ export default function Dashboard() {
+ {/* XP Stats */} +
+

+ Ecosystem Stats (Live) +

+
+
+
+ {metrics?.totalXP?.toLocaleString() || 0} +
+
Total XP Earned
+
+
+
+ {metrics?.avgLevel || 1} +
+
Avg Level
+
+
+
+ {metrics?.totalProfiles || 0} +
+
Registered
+
+
+
+ {metrics?.onlineUsers || 0} +
+
Online Now
+
+
+
+
); } -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 (
{value}
-
{change}
) diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index d4534aa..8c8b5b3 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -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 (
{/* Background Texture */} @@ -12,7 +21,37 @@ export default function Home() { style={{ backgroundImage: `url(${gridBg})`, backgroundSize: 'cover' }} /> -
+ {/* Navigation */} + + +
{/* Header */}
- System Online: v4.2 + The Operating System for the Metaverse

AeThex

- The Operating System for the Metaverse. + We train Architects. We build the Shield. We define the Law.

+ {/* Live Metrics */} + {metrics && ( + +
+
{metrics.totalProfiles}
+
Architects
+
+
+
{metrics.totalProjects}
+
Projects
+
+
+
{metrics.onlineUsers}
+
Online Now
+
+
+
{metrics.totalXP?.toLocaleString()}
+
Total XP
+
+
+ )} + {/* The Trinity Cards */}
- {/* Axiom -> Dashboard */} - + {/* Axiom -> Pitch */} +

Axiom

- The Foundation. View the global command center, active architect metrics, and ecosystem health. + The Law. Our dual-entity protocol creates a self-sustaining ecosystem. The Foundation trains; the Corporation secures.

- Open Dashboard + View Investor Pitch
- {/* Codex -> Curriculum (with Passport link inside) */} - + {/* Codex -> Foundation */} +

Codex

- The Standard. Explore the skill tree, mastery nodes, and view your Architect Credential. + The Standard. Elite training through gamified curriculum. Certifications that employers trust. The Passport to the Metaverse.

- View Tech Tree + Enter Foundation
- +
- {/* Aegis -> Terminal */} - + {/* Aegis -> Studio */} +

Aegis

- The Shield. Enter the secure build environment. New: Live Threat Simulation available. + The Shield. Real-time security for the build environment. PII scrubbing. Threat intervention. Protection for every line of code.

- Launch Terminal + Launch Studio
- +
+ {/* Demo Links */} + + + + + + + + + + + + {/* Footer */} - AeThex Foundry © 2025 // Authorized Personnel Only + AeThex Foundry © 2025 // Building the Future of the Metaverse
diff --git a/client/src/pages/login.tsx b/client/src/pages/login.tsx new file mode 100644 index 0000000..0430672 --- /dev/null +++ b/client/src/pages/login.tsx @@ -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 ( +
+
+ + +
+
+ +
+

+ AeThex Command +

+

+ Authorized Personnel Only +

+
+ +
+ {error && ( +
+ + {error} +
+ )} + +
+ + 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 + /> +
+ +
+ + 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 + /> +
+ + +
+ + +
+
+ ); +} diff --git a/client/src/pages/pitch.tsx b/client/src/pages/pitch.tsx new file mode 100644 index 0000000..bb69aa7 --- /dev/null +++ b/client/src/pages/pitch.tsx @@ -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 ( +
+
+ +
+ + + + + + {/* Title */} + +
+ The Axiom Protocol +
+

+ Investor Brief +

+

+ AeThex is a self-sustaining ecosystem built on a dual-entity model that transforms raw talent into certified Metaverse Architects. +

+
+ + {/* The Dual Entity Model */} + + {/* Foundation */} +
+
+
+
+ +
+
+
Non-Profit
+

The Foundation

+
+
+

+ Uses The Codex to train elite Architects. + Gamified curriculum, verified certifications, and a clear path from beginner to master. +

+
+
+ + Codex Standard Certification +
+
+ + {metrics?.totalProfiles || 0} Active Architects +
+
+
+ + {/* Corporation */} +
+
+
+
+ +
+
+
For-Profit
+

The Corporation

+
+
+

+ Builds The Aegis to secure the Metaverse. + Enterprise security layer, PII scrubbing, threat intervention—sold to platforms and publishers. +

+
+
+ + Real-time Security Layer +
+
+ + {metrics?.totalProjects || 0} Protected Projects +
+
+
+ + + {/* The Holy Trinity */} + +

+ The Holy Trinity +

+ +
+
+ +

Axiom

+

The Law. The foundational protocol governing the ecosystem.

+
+
+ +

Codex

+

The Standard. Certification that proves mastery.

+
+
+ +

Aegis

+

The Shield. Real-time protection for builders.

+
+
+
+ + {/* The Pitch Quote */} + +
+ "We take raw talent, train them on our laws, and arm them with our weapons. + The Foundation creates the workforce; The Corporation sells the security." +
+
+ + {/* Live Metrics */} + {metrics && ( + +

+ Live Ecosystem Metrics +

+
+
+
{metrics.totalProfiles}
+
Architects
+
+
+
{metrics.totalProjects}
+
Projects
+
+
+
{metrics.totalXP?.toLocaleString()}
+
Total XP
+
+
+
{metrics.onlineUsers}
+
Online Now
+
+
+
+ )} + + {/* CTA */} + +

+ Ready to learn more about investment opportunities? +

+ + Contact Our Team + +
+ +
+
+ ); +} diff --git a/package-lock.json b/package-lock.json index 5476984..228629e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bee0729..b32633f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/index.ts b/server/index.ts index f4219e2..dd21822 100644 --- a/server/index.ts +++ b/server/index.ts @@ -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) => { diff --git a/server/routes.ts b/server/routes.ts index 5839775..a315b2b 100644 --- a/server/routes.ts +++ b/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 { - // 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 = {}; - - 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 }); } diff --git a/server/storage.ts b/server/storage.ts index ee25bd1..2c5af7e 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -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; getUserByUsername(username: string): Promise; - createUser(user: InsertUser): Promise; + + // Profiles + getProfiles(): Promise; + getProfile(id: string): Promise; + getProfileByUsername(username: string): Promise; + updateProfile(id: string, data: Partial): Promise; + + // Projects + getProjects(): Promise; + getProject(id: string): Promise; + + // Metrics + getMetrics(): Promise<{ + totalProfiles: number; + totalProjects: number; + onlineUsers: number; + verifiedUsers: number; + totalXP: number; + avgLevel: number; + }>; } -export class MemStorage implements IStorage { - private users: Map; - - constructor() { - this.users = new Map(); - } - +export class SupabaseStorage implements IStorage { + async getUser(id: string): Promise { - 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 { - 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 { - const id = randomUUID(); - const user: User = { ...insertUser, id }; - this.users.set(id, user); - return user; + + async getProfiles(): Promise { + 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 { + 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 { + 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): Promise { + 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 { + 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 { + 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(); diff --git a/shared/schema.ts b/shared/schema.ts index 8e71891..99d0bb5 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -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; 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(), + avatar_url: text("avatar_url"), + banner_url: text("banner_url"), + social_links: json("social_links").$type>(), + 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; +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(), +}); + +export const insertProjectSchema = createInsertSchema(projects).omit({ + id: true, + created_at: true, + updated_at: true, +}); + +export type InsertProject = z.infer; +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;