diff --git a/.replit b/.replit index d95dcdf..cfe55fd 100644 --- a/.replit +++ b/.replit @@ -41,3 +41,4 @@ waitForPort = 5000 [agent] mockupState = "FULLSTACK" +integrations = ["javascript_openai_ai_integrations:1.0.0"] diff --git a/client/public/opengraph.jpg b/client/public/opengraph.jpg index dc30e0f..814cfbb 100644 Binary files a/client/public/opengraph.jpg and b/client/public/opengraph.jpg differ diff --git a/client/src/App.tsx b/client/src/App.tsx index 438611a..9a3f249 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -21,6 +21,9 @@ import AdminSites from "@/pages/admin-sites"; import AdminLogs from "@/pages/admin-logs"; import AdminAchievements from "@/pages/admin-achievements"; import AdminApplications from "@/pages/admin-applications"; +import AdminActivity from "@/pages/admin-activity"; +import AdminNotifications from "@/pages/admin-notifications"; +import { Chatbot } from "@/components/Chatbot"; function Router() { return ( @@ -40,6 +43,8 @@ function Router() { + + @@ -53,6 +58,7 @@ function App() { + diff --git a/client/src/components/Chatbot.tsx b/client/src/components/Chatbot.tsx new file mode 100644 index 0000000..dd5e3f5 --- /dev/null +++ b/client/src/components/Chatbot.tsx @@ -0,0 +1,210 @@ +import { useState, useRef, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { MessageCircle, X, Send, Bot, User, Loader2 } from "lucide-react"; + +interface Message { + id: string; + role: "user" | "assistant"; + content: string; + timestamp: Date; +} + +export function Chatbot() { + const [isOpen, setIsOpen] = useState(false); + const [messages, setMessages] = useState([ + { + id: "welcome", + role: "assistant", + content: "Hi! I'm the AeThex assistant. I can help you navigate the platform, explain our certification system, or answer questions about the ecosystem. How can I help you today?", + timestamp: new Date(), + }, + ]); + const [input, setInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + const sendMessage = async () => { + if (!input.trim() || isLoading) return; + + const userMessage: Message = { + id: Date.now().toString(), + role: "user", + content: input.trim(), + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInput(""); + setIsLoading(true); + + try { + const conversationHistory = messages.slice(-10).map(m => ({ + role: m.role, + content: m.content + })); + + const response = await fetch("/api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + message: userMessage.content, + history: conversationHistory + }), + }); + + if (!response.ok) throw new Error("Failed to get response"); + + const data = await response.json(); + + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + role: "assistant", + content: data.response || "I apologize, but I'm having trouble responding right now. Please try again.", + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, assistantMessage]); + } catch (error) { + const errorMessage: Message = { + id: (Date.now() + 1).toString(), + role: "assistant", + content: "I'm sorry, I encountered an error. Please try again in a moment.", + timestamp: new Date(), + }; + setMessages((prev) => [...prev, errorMessage]); + } finally { + setIsLoading(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + return ( + <> + {/* Chat Button */} + + {!isOpen && ( + setIsOpen(true)} + className="fixed bottom-6 left-6 z-50 bg-secondary text-background p-4 rounded-full shadow-lg hover:bg-secondary/90 transition-colors" + data-testid="button-open-chatbot" + > + + + )} + + + {/* Chat Window */} + + {isOpen && ( + + {/* Header */} +
+
+
+ +
+
+
AeThex Assistant
+
+ Online +
+
+
+ +
+ + {/* Messages */} +
+ {messages.map((message) => ( +
+
+ {message.role === "user" ? ( + + ) : ( + + )} +
+
+ {message.content} +
+
+ ))} + {isLoading && ( +
+
+ +
+
+ +
+
+ )} +
+
+ + {/* Input */} +
+
+ setInput(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Ask me anything..." + className="flex-1 bg-black/20 border border-white/10 px-4 py-2 text-sm text-white placeholder:text-muted-foreground focus:outline-none focus:border-secondary/50" + data-testid="input-chat-message" + /> + +
+
+ + )} + + + ); +} diff --git a/client/src/components/ThemeToggle.tsx b/client/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..af79693 --- /dev/null +++ b/client/src/components/ThemeToggle.tsx @@ -0,0 +1,58 @@ +import { useState, useEffect } from "react"; +import { Moon, Sun } from "lucide-react"; + +export function useTheme() { + const [theme, setTheme] = useState<"light" | "dark">("dark"); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + if (typeof window !== "undefined") { + const stored = localStorage.getItem("aethex_theme"); + if (stored === "light" || stored === "dark") { + setTheme(stored); + } + } + }, []); + + useEffect(() => { + if (!mounted) return; + + const root = document.documentElement; + if (theme === "light") { + root.classList.remove("dark"); + root.classList.add("light"); + } else { + root.classList.remove("light"); + root.classList.add("dark"); + } + if (typeof window !== "undefined") { + localStorage.setItem("aethex_theme", theme); + } + }, [theme, mounted]); + + const toggleTheme = () => { + setTheme((prev) => (prev === "dark" ? "light" : "dark")); + }; + + return { theme, setTheme, toggleTheme }; +} + +export function ThemeToggle() { + const { theme, toggleTheme } = useTheme(); + + return ( + + ); +} diff --git a/client/src/index.css b/client/src/index.css index 96fcee0..b33e184 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -96,6 +96,43 @@ --chart-5: 0 100% 50%; } +/* LIGHT MODE - AE-THEX THEME */ +.light { + --background: 0 0% 98%; + --foreground: 240 10% 10%; + + --card: 0 0% 100%; + --card-foreground: 240 10% 10%; + + --popover: 0 0% 100%; + --popover-foreground: 240 10% 10%; + + --primary: 45 100% 40%; + --primary-foreground: 0 0% 100%; + + --secondary: 180 80% 35%; + --secondary-foreground: 0 0% 100%; + + --muted: 240 5% 90%; + --muted-foreground: 240 5% 40%; + + --accent: 200 70% 90%; + --accent-foreground: 200 80% 25%; + + --destructive: 0 80% 50%; + --destructive-foreground: 0 0% 100%; + + --border: 240 5% 85%; + --input: 240 5% 85%; + --ring: 45 100% 40%; + + --chart-1: 45 100% 45%; + --chart-2: 180 80% 45%; + --chart-3: 200 80% 45%; + --chart-4: 280 80% 45%; + --chart-5: 0 80% 45%; +} + @layer base { * { @apply border-border; diff --git a/client/src/pages/admin-activity.tsx b/client/src/pages/admin-activity.tsx new file mode 100644 index 0000000..1cc4b92 --- /dev/null +++ b/client/src/pages/admin-activity.tsx @@ -0,0 +1,277 @@ +import { useEffect, useState } from "react"; +import { Link, useLocation } from "wouter"; +import { useQuery } from "@tanstack/react-query"; +import { useAuth } from "@/lib/auth"; +import { + Users, FileCode, Shield, Activity, LogOut, + BarChart3, User, Globe, Award, Key, Inbox, + Circle, Clock, TrendingUp, Wifi +} from "lucide-react"; + +interface ActivityEvent { + id: string; + type: string; + user: string; + action: string; + timestamp: Date; + details?: string; +} + +export default function AdminActivity() { + const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const [, setLocation] = useLocation(); + const [liveEvents, setLiveEvents] = useState([]); + const [lastRefresh, setLastRefresh] = useState(new Date()); + const [seenEventIds, setSeenEventIds] = useState>(new Set()); + + useEffect(() => { + if (!authLoading && !isAuthenticated) { + setLocation("/login"); + } + }, [authLoading, isAuthenticated, setLocation]); + + const { data: profiles } = useQuery({ + queryKey: ["profiles"], + queryFn: async () => { + const res = await fetch("/api/profiles", { credentials: "include" }); + if (!res.ok) return []; + return res.json(); + }, + enabled: isAuthenticated, + refetchInterval: 10000, + }); + + const { data: authLogs } = useQuery({ + queryKey: ["auth-logs"], + queryFn: async () => { + const res = await fetch("/api/auth-logs", { credentials: "include" }); + if (!res.ok) return []; + return res.json(); + }, + enabled: isAuthenticated, + refetchInterval: 5000, + }); + + const { data: metrics } = useQuery({ + queryKey: ["metrics"], + queryFn: async () => { + const res = await fetch("/api/metrics", { credentials: "include" }); + return res.json(); + }, + refetchInterval: 5000, + }); + + useEffect(() => { + if (authLogs && authLogs.length > 0) { + const newEvents: ActivityEvent[] = []; + const newIds = new Set(seenEventIds); + + for (const log of authLogs.slice(0, 30)) { + const eventId = log.id || `log-${log.created_at}`; + if (!newIds.has(eventId)) { + newIds.add(eventId); + newEvents.push({ + id: eventId, + type: log.event_type?.includes('success') ? 'login' : log.event_type?.includes('fail') ? 'failed' : 'auth', + user: log.username || log.user_id || 'Unknown', + action: log.event_type || 'Activity', + timestamp: new Date(log.created_at), + details: log.ip_address, + }); + } + } + + if (newEvents.length > 0) { + setLiveEvents(prev => [...newEvents, ...prev].slice(0, 50)); + setSeenEventIds(newIds); + } + setLastRefresh(new Date()); + } + }, [authLogs]); + + if (authLoading || !isAuthenticated) { + return ( +
+
Loading...
+
+ ); + } + + const handleLogout = async () => { + await logout(); + setLocation("/"); + }; + + const onlineProfiles = profiles?.filter((p: any) => p.status === 'online') || []; + const recentlyActive = profiles?.filter((p: any) => { + if (!p.last_seen) return false; + const lastSeen = new Date(p.last_seen); + const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); + return lastSeen > fiveMinutesAgo; + }) || []; + + return ( +
+ + +
+
+
+
+

+ Real-Time Activity +

+

+ Live monitoring of user activity across the platform +

+
+
+
+ + Last updated: {lastRefresh.toLocaleTimeString()} +
+
+ + Live +
+
+
+ +
+
+
+
Online Now
+ +
+
{metrics?.onlineUsers || 0}
+
Active users
+
+
+
Total Architects
+
{metrics?.totalProfiles || 0}
+
Registered
+
+
+
Auth Events
+
{authLogs?.length || 0}
+
Last 100
+
+
+
Verified Users
+
{metrics?.verifiedUsers || 0}
+
Certified
+
+
+ +
+
+

+ + Online Users ({onlineProfiles.length}) +

+
+ {onlineProfiles.length === 0 ? ( +
No users currently online
+ ) : ( + onlineProfiles.map((profile: any) => ( +
+
+
+
+ +
+
+
+
+
{profile.username || profile.display_name}
+
Level {profile.level || 1}
+
+
+
Online
+
+ )) + )} +
+
+ +
+

+ + Live Activity Feed +

+
+ {liveEvents.length === 0 ? ( +
No recent activity
+ ) : ( + liveEvents.map((event) => ( +
+
+
+ {event.user} + — {event.action} +
+
+ {event.timestamp.toLocaleTimeString()} +
+
+ )) + )} +
+
+
+
+
+
+ ); +} + +function Sidebar({ user, onLogout, active }: { user: any; onLogout: () => void; active: string }) { + return ( +
+
+

AeThex

+

Command Center

+
+ +
+
+
+ +
+
+
{user?.username}
+
{user?.isAdmin ? "Administrator" : "Member"}
+
+
+ +
+
+ ); +} + +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-notifications.tsx b/client/src/pages/admin-notifications.tsx new file mode 100644 index 0000000..8949485 --- /dev/null +++ b/client/src/pages/admin-notifications.tsx @@ -0,0 +1,251 @@ +import { useEffect, useState } from "react"; +import { Link, useLocation } from "wouter"; +import { useAuth } from "@/lib/auth"; +import { + Users, FileCode, Shield, Activity, LogOut, + BarChart3, User, Globe, Award, Key, Inbox, + Bell, Mail, AlertTriangle, CheckCircle, Settings +} from "lucide-react"; + +interface NotificationSetting { + id: string; + name: string; + description: string; + enabled: boolean; + category: "security" | "users" | "system"; +} + +const defaultSettings: NotificationSetting[] = [ + { id: "failed_logins", name: "Failed Login Attempts", description: "Alert when multiple failed login attempts are detected", enabled: true, category: "security" }, + { id: "new_user", name: "New User Registration", description: "Notify when a new architect joins the platform", enabled: true, category: "users" }, + { id: "critical_alert", name: "Critical Security Alerts", description: "Immediate notification for high-severity security events", enabled: true, category: "security" }, + { id: "verification", name: "Verification Requests", description: "Alert when an architect requests credential verification", enabled: false, category: "users" }, + { id: "system_down", name: "System Downtime", description: "Notify when any AeThex service goes offline", enabled: true, category: "system" }, + { id: "weekly_report", name: "Weekly Summary Report", description: "Receive a weekly digest of platform activity", enabled: false, category: "system" }, + { id: "application", name: "New Applications", description: "Alert when new job applications are submitted", enabled: true, category: "users" }, + { id: "threat_detected", name: "Threat Detection", description: "Real-time alerts from Aegis threat monitoring", enabled: true, category: "security" }, +]; + +export default function AdminNotifications() { + const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth(); + const [, setLocation] = useLocation(); + const [settings, setSettings] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("aethex_notification_settings"); + if (saved) return JSON.parse(saved); + } + return defaultSettings; + }); + const [email, setEmail] = useState(() => { + if (typeof window !== "undefined") { + return localStorage.getItem("aethex_notification_email") || ""; + } + return ""; + }); + const [saved, setSaved] = useState(false); + + useEffect(() => { + if (!authLoading && !isAuthenticated) { + setLocation("/login"); + } + }, [authLoading, isAuthenticated, setLocation]); + + const toggleSetting = (id: string) => { + setSettings((prev) => + prev.map((s) => (s.id === id ? { ...s, enabled: !s.enabled } : s)) + ); + setSaved(false); + }; + + const saveSettings = () => { + localStorage.setItem("aethex_notification_settings", JSON.stringify(settings)); + localStorage.setItem("aethex_notification_email", email); + setSaved(true); + setTimeout(() => setSaved(false), 3000); + }; + + if (authLoading || !isAuthenticated) { + return ( +
+
Loading...
+
+ ); + } + + const handleLogout = async () => { + await logout(); + setLocation("/"); + }; + + const groupedSettings = { + security: settings.filter((s) => s.category === "security"), + users: settings.filter((s) => s.category === "users"), + system: settings.filter((s) => s.category === "system"), + }; + + return ( +
+ + +
+
+
+

+ Notification Settings +

+

+ Configure email alerts for critical system events +

+
+ +
+

+ + Email Configuration +

+
+
+ + { setEmail(e.target.value); setSaved(false); }} + placeholder="admin@example.com" + className="w-full max-w-md bg-black/20 border border-white/10 px-4 py-3 text-white placeholder:text-muted-foreground focus:outline-none focus:border-primary/50" + data-testid="input-notification-email" + /> +
+
+
+ +
+
+

+ + Security Alerts +

+
+ {groupedSettings.security.map((setting) => ( + + ))} +
+
+ +
+

+ + User Activity +

+
+ {groupedSettings.users.map((setting) => ( + + ))} +
+
+ +
+

+ + System Notifications +

+
+ {groupedSettings.system.map((setting) => ( + + ))} +
+
+
+ +
+ + {saved && ( + + Settings saved + + )} +
+
+
+
+ ); +} + +function SettingRow({ setting, onToggle }: { setting: NotificationSetting; onToggle: (id: string) => void }) { + return ( +
+
+
{setting.name}
+
{setting.description}
+
+ +
+ ); +} + +function Sidebar({ user, onLogout, active }: { user: any; onLogout: () => void; active: string }) { + return ( +
+
+

AeThex

+

Command Center

+
+ +
+
+
+ +
+
+
{user?.username}
+
{user?.isAdmin ? "Administrator" : "Member"}
+
+
+ +
+
+ ); +} + +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 index 552067a..44f4b1c 100644 --- a/client/src/pages/admin.tsx +++ b/client/src/pages/admin.tsx @@ -76,11 +76,11 @@ export default function Admin() {
diff --git a/package-lock.json b/package-lock.json index 228629e..4fc438f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "lucide-react": "^0.545.0", "memorystore": "^1.6.7", "next-themes": "^0.4.6", + "openai": "^6.13.0", "passport": "^0.7.0", "passport-local": "^1.0.0", "pg": "^8.16.3", @@ -6656,6 +6657,27 @@ "node": ">= 0.8" } }, + "node_modules/openai": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.13.0.tgz", + "integrity": "sha512-yHbMo+EpNGPG3sRrXvmo0LhUPFN4bAURJw3G17bE+ax1G4tcTFCa9ZjvCWh3cvni0aHY0uWlk2IxcsPH4NR9Ow==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", diff --git a/package.json b/package.json index b32633f..1e15f2e 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "lucide-react": "^0.545.0", "memorystore": "^1.6.7", "next-themes": "^0.4.6", + "openai": "^6.13.0", "passport": "^0.7.0", "passport-local": "^1.0.0", "pg": "^8.16.3", diff --git a/server/openai.ts b/server/openai.ts new file mode 100644 index 0000000..9a1cd14 --- /dev/null +++ b/server/openai.ts @@ -0,0 +1,59 @@ +import OpenAI from "openai"; + +// This is using Replit's AI Integrations service, which provides OpenAI-compatible API access without requiring your own OpenAI API key. +// the newest OpenAI model is "gpt-5" which was released August 7, 2025. do not change this unless explicitly requested by the user +const openai = new OpenAI({ + baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL, + apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY +}); + +const SYSTEM_PROMPT = `You are the AeThex Assistant, a helpful AI guide for the AeThex ecosystem - "The Operating System for the Metaverse." + +About AeThex: +- AeThex is built on a dual-entity model: The Foundation (non-profit, training) and The Corporation (for-profit, security) +- The "Holy Trinity" consists of: Axiom (The Law - foundational protocol), Codex (The Standard - certification system), and Aegis (The Shield - security layer) +- Architects are certified professionals trained through the Codex curriculum +- The platform offers gamified learning, XP progression, and verified credentials + +You help users with: +- Navigating the platform features (Passport, Terminal, Curriculum, Dashboard) +- Understanding the certification process and how to become an Architect +- Explaining the Aegis security features +- Answering questions about the ecosystem and its mission + +Be concise, friendly, and helpful. Use the platform's terminology when appropriate. If you don't know something specific about the platform, be honest about it.`; + +interface ChatMessage { + role: "user" | "assistant"; + content: string; +} + +export async function getChatResponse(userMessage: string, history?: ChatMessage[]): Promise { + try { + const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [ + { role: "system", content: SYSTEM_PROMPT } + ]; + + if (history && Array.isArray(history)) { + for (const msg of history.slice(-8)) { + if (msg.role === "user" || msg.role === "assistant") { + messages.push({ role: msg.role, content: msg.content }); + } + } + } + + messages.push({ role: "user", content: userMessage }); + + const response = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages, + max_tokens: 500, + temperature: 0.7, + }); + + return response.choices[0]?.message?.content || "I'm sorry, I couldn't generate a response."; + } catch (error: any) { + console.error("OpenAI chat error:", error); + throw new Error("Failed to get AI response"); + } +} diff --git a/server/routes.ts b/server/routes.ts index e4698bc..a144119 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -4,6 +4,7 @@ import { storage } from "./storage"; import { loginSchema } from "@shared/schema"; import bcrypt from "bcrypt"; import crypto from "crypto"; +import { getChatResponse } from "./openai"; // Extend session type declare module 'express-session' { @@ -296,5 +297,45 @@ export async function registerRoutes( } }); + // ========== CHATBOT API (Auth + Rate limited) ========== + + const chatRateLimits = new Map(); + + app.post("/api/chat", requireAuth, async (req, res) => { + try { + const userId = req.session?.userId; + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + const now = Date.now(); + const rateLimit = chatRateLimits.get(userId); + + if (rateLimit) { + if (now < rateLimit.resetTime) { + if (rateLimit.count >= 30) { + return res.status(429).json({ error: "Rate limit exceeded. Please wait before sending more messages." }); + } + rateLimit.count++; + } else { + chatRateLimits.set(userId, { count: 1, resetTime: now + 60000 }); + } + } else { + chatRateLimits.set(userId, { count: 1, resetTime: now + 60000 }); + } + + const { message, history } = req.body; + if (!message || typeof message !== "string") { + return res.status(400).json({ error: "Message is required" }); + } + + const response = await getChatResponse(message, history); + res.json({ response }); + } catch (err: any) { + console.error("Chat error:", err); + res.status(500).json({ error: "Failed to get response" }); + } + }); + return httpServer; }