import express from "express"; import path from "path"; import cors from "cors"; import cookieParser from "cookie-parser"; import jwt from "jsonwebtoken"; import bcrypt from "bcryptjs"; import admin from "firebase-admin"; import { getFirestore, FieldValue, FieldPath } from "firebase-admin/firestore"; import { createServer as createViteServer } from "vite"; import fs from "fs"; import crypto from "crypto"; // Initialize Firebase Admin const configPath = path.join(process.cwd(), 'firebase-applet-config.json'); const firebaseConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); const appAdmin = admin.initializeApp({ projectId: firebaseConfig.projectId, }); const db = firebaseConfig.firestoreDatabaseId ? getFirestore(appAdmin, firebaseConfig.firestoreDatabaseId) : getFirestore(appAdmin); const JWT_SECRET = process.env.JWT_SECRET || "exam-forge-secret-key-2024"; const app = express(); app.use(cors()); app.use(express.json()); app.use(cookieParser()); // Local File-Based Databases to bypass server-side Firestore PERMISSION_DENIED const USERS_FILE = path.join(process.cwd(), 'users-local-db.json'); const COUNTERS_FILE = path.join(process.cwd(), 'counters-local-db.json'); const AI_LOGS_FILE = path.join(process.cwd(), 'ai-logs-local-db.json'); const EXAMS_FILE = path.join(process.cwd(), 'exams-local-db.json'); const QUESTIONS_FILE = path.join(process.cwd(), 'questions-local-db.json'); const GROUPS_FILE = path.join(process.cwd(), 'groups-local-db.json'); const ATTEMPTS_FILE = path.join(process.cwd(), 'attempts-local-db.json'); function loadLocalUsers(): any[] { try { if (fs.existsSync(USERS_FILE)) { return JSON.parse(fs.readFileSync(USERS_FILE, 'utf-8')); } } catch (err) { console.error("Failed to load local users:", err); } return []; } function saveLocalUsers(users: any[]) { try { fs.writeFileSync(USERS_FILE, JSON.stringify(users, null, 2), 'utf-8'); } catch (err) { console.error("Failed to save local users:", err); } } function loadLocalExams(): any[] { try { if (fs.existsSync(EXAMS_FILE)) { return JSON.parse(fs.readFileSync(EXAMS_FILE, 'utf-8')); } } catch (err) { console.error("Failed to load local exams:", err); } return []; } function saveLocalExams(exams: any[]) { try { fs.writeFileSync(EXAMS_FILE, JSON.stringify(exams, null, 2), 'utf-8'); } catch (err) { console.error("Failed to save local exams:", err); } } function loadLocalQuestions(): any[] { try { if (fs.existsSync(QUESTIONS_FILE)) { return JSON.parse(fs.readFileSync(QUESTIONS_FILE, 'utf-8')); } } catch (err) { console.error("Failed to load local questions:", err); } return []; } function saveLocalQuestions(questions: any[]) { try { fs.writeFileSync(QUESTIONS_FILE, JSON.stringify(questions, null, 2), 'utf-8'); } catch (err) { console.error("Failed to save local questions:", err); } } function loadLocalGroups(): any[] { try { if (fs.existsSync(GROUPS_FILE)) { return JSON.parse(fs.readFileSync(GROUPS_FILE, 'utf-8')); } } catch (err) { console.error("Failed to load local groups:", err); } return []; } function saveLocalGroups(groups: any[]) { try { fs.writeFileSync(GROUPS_FILE, JSON.stringify(groups, null, 2), 'utf-8'); } catch (err) { console.error("Failed to save local groups:", err); } } function loadLocalAttempts(): any[] { try { if (fs.existsSync(ATTEMPTS_FILE)) { return JSON.parse(fs.readFileSync(ATTEMPTS_FILE, 'utf-8')); } } catch (err) { console.error("Failed to load local attempts:", err); } return []; } function saveLocalAttempts(attempts: any[]) { try { fs.writeFileSync(ATTEMPTS_FILE, JSON.stringify(attempts, null, 2), 'utf-8'); } catch (err) { console.error("Failed to save local attempts:", err); } } // Session Management Local DB const SESSIONS_FILE = path.join(process.cwd(), 'sessions-local-db.json'); function loadLocalSessions(): any[] { try { if (fs.existsSync(SESSIONS_FILE)) { return JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf-8')); } } catch (err) { console.error("Failed to load local sessions:", err); } return []; } function saveLocalSessions(sessions: any[]) { try { fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2), 'utf-8'); } catch (err) { console.error("Failed to save local sessions:", err); } } // Enterprise Audit Logging Local DB const AUDIT_LOGS_FILE = path.join(process.cwd(), 'audit-logs-local-db.json'); function loadLocalAudits(): any[] { try { if (fs.existsSync(AUDIT_LOGS_FILE)) { return JSON.parse(fs.readFileSync(AUDIT_LOGS_FILE, 'utf-8')); } } catch (err) { console.error("Failed to load local audits:", err); } return []; } function saveLocalAudits(audits: any[]) { try { fs.writeFileSync(AUDIT_LOGS_FILE, JSON.stringify(audits, null, 2), 'utf-8'); } catch (err) { console.error("Failed to save local audits:", err); } } async function logAudit(action: string, actor: string, details: string) { try { const audits = loadLocalAudits(); const entry = { id: `AUD-${Date.now()}-${Math.random().toString(36).substr(2, 5).toUpperCase()}`, action, actor, details, timestamp: new Date().toISOString() }; audits.push(entry); saveLocalAudits(audits); } catch (err) { console.error("Failed to log audit:", err); } } // User-level: 60 requests/minute. Org-level: 500 requests/hour. // Excess requests are automatically queued and flushed when slots open. const userRequestTimestamps: Record = {}; const orgRequestTimestamps: Record = {}; interface QueuedTask { req: any; res: any; next: any; } const requestQueue: QueuedTask[] = []; function checkAndProcessQueue() { if (requestQueue.length === 0) return; const now = Date.now(); const minuteAgo = now - 60 * 1000; const hourAgo = now - 60 * 60 * 1000; const task = requestQueue[0]; const ip = task.req.ip || 'unknown-ip'; const userId = task.req.cookies?.jwt_session ? (jwt.decode(task.req.cookies.jwt_session) as any)?.uid || ip : ip; const orgId = task.req.body?.organization_id || 'org-default'; if (!userRequestTimestamps[userId]) userRequestTimestamps[userId] = []; userRequestTimestamps[userId] = userRequestTimestamps[userId].filter(t => t > minuteAgo); if (!orgRequestTimestamps[orgId]) orgRequestTimestamps[orgId] = []; orgRequestTimestamps[orgId] = orgRequestTimestamps[orgId].filter(t => t > hourAgo); if (userRequestTimestamps[userId].length < 60 && orgRequestTimestamps[orgId].length < 500) { userRequestTimestamps[userId].push(now); orgRequestTimestamps[orgId].push(now); requestQueue.shift(); // remove task task.next(); setTimeout(checkAndProcessQueue, 50); } } function nvidiaRateLimiter(req: any, res: any, next: any) { const ip = req.ip || 'unknown-ip'; const userId = req.cookies?.jwt_session ? (jwt.decode(req.cookies.jwt_session) as any)?.uid || ip : ip; const orgId = req.body?.organization_id || 'org-default'; const now = Date.now(); const minuteAgo = now - 60 * 1000; const hourAgo = now - 60 * 60 * 1000; if (!userRequestTimestamps[userId]) userRequestTimestamps[userId] = []; userRequestTimestamps[userId] = userRequestTimestamps[userId].filter(t => t > minuteAgo); if (!orgRequestTimestamps[orgId]) orgRequestTimestamps[orgId] = []; orgRequestTimestamps[orgId] = orgRequestTimestamps[orgId].filter(t => t > hourAgo); if (userRequestTimestamps[userId].length < 60 && orgRequestTimestamps[orgId].length < 500) { userRequestTimestamps[userId].push(now); orgRequestTimestamps[orgId].push(now); return next(); } // Queue excess requests if (requestQueue.length >= 100) { return res.status(429).json({ error: "AI gateway limit exceeded and request queue is full. Try again shortly." }); } requestQueue.push({ req, res, next }); setTimeout(checkAndProcessQueue, 1000); } function loadLocalCounters(): Record { try { if (fs.existsSync(COUNTERS_FILE)) { return JSON.parse(fs.readFileSync(COUNTERS_FILE, 'utf-8')); } } catch (err) { console.error("Failed to load local counters:", err); } return {}; } function saveLocalCounters(counters: Record) { try { fs.writeFileSync(COUNTERS_FILE, JSON.stringify(counters, null, 2), 'utf-8'); } catch (err) { console.error("Failed to save local counters:", err); } } async function getNextNumber(prefix: string): Promise { const counters = loadLocalCounters(); const nextNum = (counters[prefix] || 0) + 1; counters[prefix] = nextNum; saveLocalCounters(counters); return nextNum.toString().padStart(3, '0'); } function generateShortName(name: string): string { const parts = name.trim().split(/\s+/); if (parts.length === 1) return parts[0].slice(0, 3).toLowerCase(); // Take first letters of first 3 words return parts.slice(0, 3).map(p => p[0]).join('').toLowerCase(); } // Enterprise-Grade Auto-Transition status calculations function getTransitionedStatus(exam: any): string { if (['draft', 'paused', 'archived'].includes(exam.status)) { return exam.status; } const now = Date.now(); const startTime = exam.startTime ? new Date(exam.startTime).getTime() : null; const endTime = exam.endTime ? new Date(exam.endTime).getTime() : null; const durationMs = (exam.duration || 60) * 60 * 1000; if (startTime) { if (now < startTime) { return 'scheduled'; } const calculatedEndTime = endTime || (startTime + durationMs); if (now > calculatedEndTime) { return 'completed'; } return 'active'; } if (exam.status === 'published' || exam.status === 'active') { return 'active'; } return exam.status || 'draft'; } // Global collision-resistant enterprise EXM-YYYYMMDD-XXXXXX style keys function generateExamId(existingIds: string[]): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const today = new Date(); const yyyy = today.getFullYear(); const mm = String(today.getMonth() + 1).padStart(2, '0'); const dd = String(today.getDate()).padStart(2, '0'); const dateStr = `${yyyy}${mm}${dd}`; while (true) { let entropy = ''; for (let i = 0; i < 6; i++) { entropy += chars.charAt(Math.floor(Math.random() * chars.length)); } const examId = `EXM-${dateStr}-${entropy}`; if (!existingIds.includes(examId)) { return examId; } } } // Standardized DB helpers with concurrent Cloud Firestore synchronization and fallback async function loadUsersFromDb(): Promise { const local = loadLocalUsers(); try { const snapshot = await db.collection("users").get(); if (snapshot.empty) { if (local.length > 0) { const batch = db.batch(); local.forEach(u => batch.set(db.collection("users").doc(u.id), u)); await batch.commit(); } return local; } const items: any[] = []; snapshot.forEach(doc => items.push({ ...doc.data(), id: doc.id })); saveLocalUsers(items); return items; } catch (err) { console.error("Firestore users loading failed, fallback to local:", err); return local; } } async function saveUserToDb(user: any) { const users = loadLocalUsers(); const idx = users.findIndex(u => u.id === user.id); if (idx !== -1) users[idx] = user; else users.push(user); saveLocalUsers(users); try { await db.collection("users").doc(user.id).set(user); } catch (err) { console.error("Firestore user saving failed:", err); } } async function loadExamsFromDb(): Promise { const local = loadLocalExams(); try { const snapshot = await db.collection("exams").get(); if (snapshot.empty) { if (local.length > 0) { const batch = db.batch(); local.forEach(e => batch.set(db.collection("exams").doc(e.id), e)); await batch.commit(); } return local; } const items: any[] = []; snapshot.forEach(doc => items.push({ ...doc.data(), id: doc.id })); // Apply automatic transitions const transitioned = items.map(e => { const original = e.status; const status = getTransitionedStatus(e); if (status !== original) { e.status = status; e.updatedAt = new Date().toISOString(); db.collection("exams").doc(e.id).update({ status, updatedAt: e.updatedAt }).catch(() => {}); } return e; }); saveLocalExams(transitioned); return transitioned; } catch (err) { console.error("Firestore exams loading failed, fallback to local:", err); const transitioned = local.map(e => { const original = e.status; const status = getTransitionedStatus(e); if (status !== original) { e.status = status; e.updatedAt = new Date().toISOString(); } return e; }); saveLocalExams(transitioned); return transitioned; } } async function saveExamToDb(exam: any) { const exams = loadLocalExams(); const idx = exams.findIndex(e => e.id === exam.id); if (idx !== -1) exams[idx] = exam; else exams.push(exam); saveLocalExams(exams); try { await db.collection("exams").doc(exam.id).set(exam); } catch (err) { console.error("Firestore exam saving failed:", err); } } async function loadQuestionsFromDb(): Promise { const local = loadLocalQuestions(); try { const snapshot = await db.collection("questions").get(); if (snapshot.empty) { if (local.length > 0) { const batch = db.batch(); local.forEach(q => batch.set(db.collection("questions").doc(q.id), q)); await batch.commit(); } return local; } const items: any[] = []; snapshot.forEach(doc => items.push({ ...doc.data(), id: doc.id })); saveLocalQuestions(items); return items; } catch (err) { console.error("Firestore questions loading failed, fallback to local:", err); return local; } } async function saveQuestionToDb(question: any) { const questions = loadLocalQuestions(); const idx = questions.findIndex(q => q.id === question.id); if (idx !== -1) questions[idx] = question; else questions.push(question); saveLocalQuestions(questions); try { await db.collection("questions").doc(question.id).set(question); } catch (err) { console.error("Firestore question saving failed:", err); } } async function loadGroupsFromDb(): Promise { const local = loadLocalGroups(); try { const snapshot = await db.collection("groups").get(); if (snapshot.empty) { if (local.length > 0) { const batch = db.batch(); local.forEach(g => batch.set(db.collection("groups").doc(g.id), g)); await batch.commit(); } return local; } const items: any[] = []; snapshot.forEach(doc => items.push({ ...doc.data(), id: doc.id })); saveLocalGroups(items); return items; } catch (err) { console.error("Firestore groups loading failed, fallback to local:", err); return local; } } async function saveGroupToDb(group: any) { const groups = loadLocalGroups(); const idx = groups.findIndex(g => g.id === group.id); if (idx !== -1) groups[idx] = group; else groups.push(group); saveLocalGroups(groups); try { await db.collection("groups").doc(group.id).set(group); } catch (err) { console.error("Firestore group saving failed:", err); } } async function loadAttemptsFromDb(): Promise { const local = loadLocalAttempts(); try { const snapshot = await db.collection("attempts").get(); if (snapshot.empty) { if (local.length > 0) { const batch = db.batch(); local.forEach(a => batch.set(db.collection("attempts").doc(a.id), a)); await batch.commit(); } return local; } const items: any[] = []; snapshot.forEach(doc => items.push({ ...doc.data(), id: doc.id })); saveLocalAttempts(items); return items; } catch (err) { console.error("Firestore attempts loading failed, fallback to local:", err); return local; } } async function saveAttemptToDb(attempt: any) { const attempts = loadLocalAttempts(); const idx = attempts.findIndex(a => a.id === attempt.id); if (idx !== -1) attempts[idx] = attempt; else attempts.push(attempt); saveLocalAttempts(attempts); try { await db.collection("attempts").doc(attempt.id).set(attempt); } catch (err) { console.error("Firestore attempt saving failed:", err); } } const SUBJECT_CODES: Record = { "Mathematics": "mth", "English": "eng", "Physics": "phy", "Chemistry": "chm", "Biology": "bio", "ICT": "ict" }; // Helper: Sign a secure standard JWT and place it in an HttpOnly cookie (never exposed to frontend JavaScript) function setJwtCookie(res: any, uid: string, role: string) { const sessionToken = jwt.sign( { uid, role }, JWT_SECRET, { expiresIn: '24h' } ); res.cookie('jwt_session', sessionToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: 'strict', maxAge: 24 * 60 * 60 * 1000 // 24 hours }); } // Helper: Resolve authenticated user ID from either secure HttpOnly JWT cookie or Firebase authorization header async function getAuthenticatedUserId(req: any): Promise { const tokenCookie = req.cookies?.jwt_session; if (tokenCookie) { try { const decoded = jwt.verify(tokenCookie, JWT_SECRET) as any; if (decoded && decoded.uid) { return decoded.uid; } } catch (err) { console.warn("Secure JWT Cookie verification failed:", err); } } // Fallback to Bearer token const authHeader = req.headers.authorization; if (!authHeader) throw new Error("Unauthorized: Missing credential details"); const idToken = authHeader.split(' ')[1]; try { const decodedToken = await admin.auth().verifyIdToken(idToken); return decodedToken.uid; } catch (error) { // Graceful fallback to local JWT signature decoding if Firebase admin fails try { const decoded = jwt.verify(idToken, JWT_SECRET) as any; if (decoded && decoded.uid) { return decoded.uid; } } catch (jwtErr) { console.warn("Local fallback JWT verification also failed:", jwtErr); } throw new Error("Unauthorized: Invalid authorization credentials"); } } // Auth Endpoints app.post("/api/auth/register/student", async (req, res) => { try { const { fullName, class: className, department, pin } = req.body; const shortName = generateShortName(fullName); const classPrefix = className.toLowerCase().replace(/\s+/g, '').replace(/[^-a-z0-9]/g, ''); const prefix = `${classPrefix}-${shortName}-`; const num = await getNextNumber(prefix); const studentId = `${prefix}${num}`; const pinHash = await bcrypt.hash(pin, 10); const userProfile = { id: studentId, fullName, role: 'student', class: className, department: department || null, pinHash, createdAt: new Date().toISOString(), examHistory: [] }; const users = loadLocalUsers(); users.push(userProfile); saveLocalUsers(users); const customToken = jwt.sign({ uid: studentId }, JWT_SECRET); setJwtCookie(res, studentId, 'student'); // Do not send pinHash to client const { pinHash: _, ...publicProfile } = userProfile; res.json({ customToken, user: publicProfile }); } catch (error: any) { console.error("Register Error:", error); res.status(500).json({ error: error.message }); } }); app.post("/api/auth/register/teacher", async (req, res) => { try { const { fullName, subject, position, classesManaged, pin } = req.body; const initials = fullName.trim().split(/\s+/).map(p => p[0]).join('').toLowerCase().slice(0, 2); const subCode = SUBJECT_CODES[subject] || subject.slice(0, 3).toLowerCase(); const prefix = `tch-${subCode}-${initials}-`; const num = await getNextNumber(prefix); const teacherId = `${prefix}${num}`; const pinHash = await bcrypt.hash(pin, 10); const userProfile = { id: teacherId, fullName, role: 'teacher', subject, position, classesManaged: classesManaged || [], pinHash, createdAt: new Date().toISOString() }; const users = loadLocalUsers(); users.push(userProfile); saveLocalUsers(users); const customToken = jwt.sign({ uid: teacherId }, JWT_SECRET); setJwtCookie(res, teacherId, 'teacher'); // Do not send pinHash to client const { pinHash: _, ...publicProfile } = userProfile; res.json({ customToken, user: publicProfile }); } catch (error: any) { console.error("Register Error:", error); res.status(500).json({ error: error.message }); } }); app.post("/api/auth/login", async (req, res) => { try { const { id, pin } = req.body; if (!id || !pin) return res.status(400).json({ error: "Missing identity or pin" }); const userId = id.toLowerCase().trim(); const users = loadLocalUsers(); const user = users.find(u => u.id === userId); if (!user) { return res.status(401).json({ error: "Invalid ID or PIN" }); } const isValid = await bcrypt.compare(pin, user.pinHash); if (!isValid) { return res.status(401).json({ error: "Invalid ID or PIN" }); } const customToken = jwt.sign({ uid: userId }, JWT_SECRET); const role = user.role || 'student'; setJwtCookie(res, userId, role); // Do not send pinHash to client const { pinHash: _, ...publicProfile } = user; res.json({ customToken, user: publicProfile }); } catch (error: any) { console.error("Login Error:", error); res.status(500).json({ error: error.message }); } }); // Server-side Email/Password student registration (bypasses Firebase configuration limits) app.post("/api/auth/register/student-email", async (req, res) => { try { const { fullName, class: className, department, email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: "Email and password are required" }); } const cleanEmail = email.trim().toLowerCase(); // Check if email already exists const users = loadLocalUsers(); const emailExists = users.some(u => u.email === cleanEmail); if (emailExists) { return res.status(400).json({ error: "An account with this email already exists." }); } const shortName = generateShortName(fullName); const classPrefix = className.toLowerCase().replace(/\s+/g, '').replace(/[^-a-z0-9]/g, ''); const prefix = `${classPrefix}-${shortName}-`; const num = await getNextNumber(prefix); const studentId = `${prefix}${num}`; const pinHash = await bcrypt.hash(password, 10); const userProfile = { id: studentId, fullName, role: 'student', class: className, department: department || null, email: cleanEmail, pinHash, // store password hash under pinHash for schema compatibility createdAt: new Date().toISOString(), examHistory: [] }; users.push(userProfile); saveLocalUsers(users); const customToken = jwt.sign({ uid: studentId }, JWT_SECRET); setJwtCookie(res, studentId, 'student'); const { pinHash: _, ...publicProfile } = userProfile; res.json({ customToken, user: publicProfile }); } catch (error: any) { console.error("Student Email Register Error:", error); res.status(500).json({ error: error.message }); } }); // Server-side Email/Password teacher registration (bypasses Firebase configuration limits) app.post("/api/auth/register/teacher-email", async (req, res) => { try { const { fullName, subject, position, classesManaged, email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: "Email and password are required" }); } const cleanEmail = email.trim().toLowerCase(); // Check if email already exists const users = loadLocalUsers(); const emailExists = users.some(u => u.email === cleanEmail); if (emailExists) { return res.status(400).json({ error: "An account with this email already exists." }); } const initials = fullName.trim().split(/\s+/).map(p => p[0]).join('').toLowerCase().slice(0, 2); const subCode = SUBJECT_CODES[subject] || subject.slice(0, 3).toLowerCase(); const prefix = `tch-${subCode}-${initials}-`; const num = await getNextNumber(prefix); const teacherId = `${prefix}${num}`; const pinHash = await bcrypt.hash(password, 10); const userProfile = { id: teacherId, fullName, role: 'teacher', subject, position, classesManaged: classesManaged || [], email: cleanEmail, pinHash, // store password hash under pinHash for schema compatibility createdAt: new Date().toISOString(), examHistory: [] }; users.push(userProfile); saveLocalUsers(users); const customToken = jwt.sign({ uid: teacherId }, JWT_SECRET); setJwtCookie(res, teacherId, 'teacher'); const { pinHash: _, ...publicProfile } = userProfile; res.json({ customToken, user: publicProfile }); } catch (error: any) { console.error("Teacher Email Register Error:", error); res.status(500).json({ error: error.message }); } }); // Server-side Email/Password login (bypasses Firebase configuration limits) app.post("/api/auth/login-email", async (req, res) => { try { const { email, password } = req.body; if (!email || !password) return res.status(400).json({ error: "Missing email or password" }); const cleanEmail = email.trim().toLowerCase(); const users = loadLocalUsers(); const user = users.find(u => u.email === cleanEmail); if (!user) { return res.status(401).json({ error: "Invalid Email or Password" }); } const isValid = await bcrypt.compare(password, user.pinHash); if (!isValid) { return res.status(401).json({ error: "Invalid Email or Password" }); } const customToken = jwt.sign({ uid: user.id }, JWT_SECRET); const role = user.role || 'student'; setJwtCookie(res, user.id, role); const { pinHash: _, ...publicProfile } = user; res.json({ customToken, user: publicProfile }); } catch (error: any) { console.error("Email Login Error:", error); res.status(500).json({ error: error.message }); } }); app.get("/api/auth/me", async (req, res) => { try { // In custom token flow, the client has the Firebase Auth session. // If we want a server-side "me", we'd pass the ID token or use our HttpOnly cookie. const userId = await getAuthenticatedUserId(req); const users = loadLocalUsers(); const user = users.find(u => u.id === userId); if (!user) return res.status(404).json({ error: "User not found" }); const { pinHash: _, ...publicProfile } = user; res.json(publicProfile); } catch (error) { res.status(401).json({ error: "Invalid session" }); } }); app.post("/api/auth/logout", (req, res) => { res.clearCookie('jwt_session'); res.json({ success: true }); }); // Student CBT Exam Verification & Access Session Initialization app.post("/api/exams/verify-entry", async (req, res) => { try { const { examId, studentId, deviceInfo = "Unknown Device" } = req.body; const ip = req.ip || 'unknown-ip'; if (!examId || !studentId) { return res.status(400).json({ error: "Exam ID and Student ID are required inputs." }); } const cleanExamId = examId.trim().toUpperCase(); const cleanStudentId = studentId.trim().toLowerCase(); // 1. Exam exists validation const exams = loadLocalExams(); const exam = exams.find(e => e.id === cleanExamId); if (!exam) { await logAudit("Failed Exam Access Attempt", cleanStudentId, `Input Exam ID: ${cleanExamId} not found`); return res.status(400).json({ error: "Invalid Exam ID" }); // Returning 400 for consistency } // 2. Exam is active validation (active or published) if (exam.status !== 'active' && exam.status !== 'published') { await logAudit("Failed Exam Access Attempt", cleanStudentId, `Exam ID: ${cleanExamId} is in status: ${exam.status}`); return res.status(400).json({ error: "Exam is currently not active" }); } // 3. Exam schedule validation (start time arrived, end time has not passed) const nowTimestamp = Date.now(); if (exam.startTime && nowTimestamp < exam.startTime) { await logAudit("Failed Exam Access Attempt", cleanStudentId, `Exam ID: ${cleanExamId} has not reached start schedule`); return res.status(400).json({ error: "Exam not started" }); } if (exam.endTime && nowTimestamp > exam.endTime) { await logAudit("Failed Exam Access Attempt", cleanStudentId, `Exam ID: ${cleanExamId} is past end schedule`); return res.status(400).json({ error: "Exam ended" }); } // 4. Student existence validation const users = loadLocalUsers(); const student = users.find(u => u.id === cleanStudentId && u.role === 'student'); if (!student) { await logAudit("Failed Exam Access Attempt", cleanStudentId, `Input Student ID: ${cleanStudentId} not found`); return res.status(400).json({ error: "Invalid Student ID" }); } // 5. Registration validation const hasStudentRestrictions = exam.studentIds && exam.studentIds.length > 0; const hasGroupRestrictions = exam.groupIds && exam.groupIds.length > 0; if (hasStudentRestrictions || hasGroupRestrictions) { let isRegistered = false; if (hasStudentRestrictions && exam.studentIds.includes(cleanStudentId)) { isRegistered = true; } if (!isRegistered && hasGroupRestrictions) { const groups = loadLocalGroups(); const studentGroupIds = groups .filter(g => g.studentIds && g.studentIds.includes(cleanStudentId)) .map(g => g.id); const overlaps = exam.groupIds.some((gId: string) => studentGroupIds.includes(gId)); if (overlaps) { isRegistered = true; } } if (!isRegistered) { await logAudit("Failed Exam Access Attempt", cleanStudentId, `Student is not assigned to exam ${cleanExamId}`); return res.status(400).json({ error: "Student not registered" }); } } // 6. Attempt limit validation const attempts = loadLocalAttempts(); const completedAttempts = attempts.filter( a => a.examId === cleanExamId && a.studentId === cleanStudentId && a.status === 'completed' ); if (exam.settings && exam.settings.attemptLimit > 0) { if (completedAttempts.length >= exam.settings.attemptLimit) { await logAudit("Failed Exam Access Attempt", cleanStudentId, `Attempt limit reached for exam ${cleanExamId}`); return res.status(400).json({ error: "Attempt limit reached" }); } } // --- Create / Resume Connection Session --- const sessions = loadLocalSessions(); let session = sessions.find( s => s.examId === cleanExamId && s.studentId === cleanStudentId && s.status === 'active' ); const sessionUUID = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2, 10); const newSessionToken = `SESSION-${sessionUUID}`; if (session) { // Resume existing active session, generating a fresh multi-login preventing token await logAudit("Exam Resumed", cleanStudentId, `Resumed active session. New token: ${newSessionToken}`); session.oldToken = session.id; session.id = newSessionToken; session.ip = ip; session.deviceInfo = deviceInfo; session.lastActive = nowTimestamp; } else { // Initialize new active session await logAudit("Exam Started", cleanStudentId, `Created new session token: ${newSessionToken}`); session = { id: newSessionToken, studentId: cleanStudentId, studentName: student.fullName, studentClass: student.class || "General", examId: cleanExamId, examName: exam.title, subject: exam.subject || "General", duration: exam.duration, questionsCount: exam.questionIds ? exam.questionIds.length : 0, tempTimer: exam.duration * 60, // save remaining time in seconds answers: {}, currentQuestionId: exam.questionIds && exam.questionIds.length > 0 ? exam.questionIds[0] : "", startedAt: nowTimestamp, lastActive: nowTimestamp, ip, deviceInfo, status: 'active' }; sessions.push(session); } saveLocalSessions(sessions); // Auto authenticate via cookie so student can proceed setJwtCookie(res, cleanStudentId, 'student'); const customToken = jwt.sign({ uid: cleanStudentId }, JWT_SECRET); res.json({ sessionToken: newSessionToken, customToken, session, studentName: student.fullName, examName: exam.title, subject: exam.subject || "General", duration: exam.duration, questionsCount: exam.questionIds ? exam.questionIds.length : 0, }); } catch (error: any) { console.error("verify-entry endpoints error:", error); res.status(500).json({ error: error.message }); } }); // Update session state in background (Auto-Saving answers, remaining time, navigation) app.post("/api/sessions/update-state", async (req, res) => { try { const { sessionToken, answers, currentQuestionId, remainingTime } = req.body; if (!sessionToken) { return res.status(400).json({ error: "Missing session token" }); } const sessions = loadLocalSessions(); const session = sessions.find(s => s.id === sessionToken && s.status === 'active'); if (!session) { return res.status(404).json({ error: "Active session not found or has expired." }); } // Save browser backup indices if (answers !== undefined) session.answers = answers; if (currentQuestionId !== undefined) session.currentQuestionId = currentQuestionId; if (remainingTime !== undefined) session.tempTimer = remainingTime; session.lastActive = Date.now(); saveLocalSessions(sessions); res.json({ success: true, lastActive: session.lastActive }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // Fetch active session state to ensure robust browser closing recovery app.get("/api/sessions/status/:token", async (req, res) => { try { const { token } = req.params; const sessions = loadLocalSessions(); const session = sessions.find(s => s.id === token); if (!session) { return res.status(404).json({ error: "Session token index not recognized" }); } res.json(session); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // Exam Operations app.post("/api/exams/create", async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); const users = await loadUsersFromDb(); const teacher = users.find(u => u.id === teacherId); if (!teacher || teacher.role !== 'teacher') { return res.status(403).json({ error: "Only teachers can create exams" }); } const examData = req.body; const exams = await loadExamsFromDb(); const existingIds = exams.map(e => e.id); const examId = generateExamId(existingIds); const now = new Date().toISOString(); const newExam = { ...examData, id: examId, examId: examId, examName: examData.title || examData.examName || "", title: examData.title || examData.examName || "", teacherId, createdBy: teacherId, schoolId: teacher.schoolId || teacher.class || "school-primary", createdAt: now, updatedAt: now, status: 'draft' }; await saveExamToDb(newExam); res.json({ id: examId }); } catch (error: any) { console.error("Exam Create Error:", error); res.status(500).json({ error: error.message }); } }); // Question Bank Operations app.post("/api/questions/create", async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); const prefix = 'qst-'; const num = await getNextNumber(prefix); const questionId = `${prefix}${num}`; const questionData = { ...req.body, id: questionId, teacherId, createdAt: new Date().toISOString(), }; await saveQuestionToDb(questionData); res.json(questionData); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // Group Operations app.post("/api/groups/create", async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); const prefix = 'grp-'; const num = await getNextNumber(prefix); const groupId = `${prefix}${num}`; const groupData = { ...req.body, id: groupId, teacherId, createdAt: new Date().toISOString(), }; const groups = loadLocalGroups(); groups.push(groupData); saveLocalGroups(groups); res.json(groupData); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // Exam Submission & Auto-Grading app.post("/api/exams/submit", async (req, res) => { try { const studentId = await getAuthenticatedUserId(req); const { attemptId, examId, answers, timeSpent, offlineDuration = 0, submissionTimestamp } = req.body; // Fetch exam and questions for grading from local files const exams = loadLocalExams(); const exam = exams.find(e => e.id === examId); if (!exam) return res.status(404).json({ error: "Exam not found" }); // 1. Validate Schedule bounds (Start / End times) const now = Date.now(); if (exam.startTime && now < exam.startTime) { return res.status(400).json({ error: "This exam schedule has not started yet." }); } if (exam.endTime) { const gracePeriod = 15 * 60 * 1000; // 15 minutes grace for offline syncing and submission delays if (now > exam.endTime + gracePeriod) { return res.status(400).json({ error: "The exam window has officially closed. Delayed submission rejected." }); } } // 2. Validate Submission Timestamp to prevent client system clock manipulation if (submissionTimestamp) { const clientSubmitTime = new Date(submissionTimestamp).getTime(); const clockSkew = Math.abs(now - clientSubmitTime); if (clockSkew > 10 * 60 * 1000) { // Reject if clock skew is more than 10 minutes console.warn(`Clock tampering suspected for ${studentId}. Skew: ${clockSkew}ms`); } } const questionIds = exam.questionIds || []; const questionsList = loadLocalQuestions(); const questionsMap: Record = {}; questionsList.forEach(q => { if (questionIds.includes(q.id)) { questionsMap[q.id] = q; } }); let score = 0; const total = questionIds.length; questionIds.forEach((qId: string) => { if (questionsMap[qId] && answers[qId] === questionsMap[qId].correctOptionId) { score++; } }); const percentage = total > 0 ? Math.round((score / total) * 100) : 0; const completedAt = new Date().toISOString(); const attempts = loadLocalAttempts(); const attemptIdx = attempts.findIndex(a => a.id === attemptId); // 3. Prevent timer manipulation/freezing by matching elapsed server duration let validatedTimeSpent = timeSpent; if (attemptIdx !== -1) { const attemptObj = attempts[attemptIdx]; if (attemptObj.startedAt) { const startedTimeMs = new Date(attemptObj.startedAt).getTime(); const serverElapsedSeconds = Math.floor((now - startedTimeMs) / 1000); const maxExpectedSeconds = (exam.duration * 60) + offlineDuration + 600; // 10 minutes total network sync grace // If reported timeSpent is ridiculously low compared to real elapsed time, // and student has not been offline, we detect timer freezing if (serverElapsedSeconds > maxExpectedSeconds) { console.warn(`Timer manipulation warning for student ${studentId}. Elapsed: ${serverElapsedSeconds}s, Reported Spent: ${timeSpent}s, Limit: ${maxExpectedSeconds}s`); validatedTimeSpent = Math.min(serverElapsedSeconds, exam.duration * 60); } } } if (attemptIdx !== -1) { attempts[attemptIdx] = { ...attempts[attemptIdx], answers, score, percentage, timeSpent: validatedTimeSpent, offlineDuration, status: 'completed', completedAt }; saveLocalAttempts(attempts); } else { attempts.push({ id: attemptId, examId, studentId, studentName: 'Student', studentClass: 'Class', status: 'completed', answers, score, percentage, timeSpent: validatedTimeSpent, offlineDuration, startedAt: completedAt, completedAt }); saveLocalAttempts(attempts); } // Update user exam history const users = loadLocalUsers(); const student = users.find(u => u.id === studentId); if (student) { if (!student.examHistory) student.examHistory = []; if (!student.examHistory.includes(attemptId)) { student.examHistory.push(attemptId); } saveLocalUsers(users); } res.json({ score, percentage, total }); } catch (error: any) { console.error("Submission Error:", error); res.status(500).json({ error: error.message }); } }); // --- Local Database REST API endpoints --- app.get("/api/students", async (req, res) => { try { const users = loadLocalUsers(); const students = users.filter(u => u.role === 'student'); res.json(students); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.get("/api/questions/list", async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); const questions = loadLocalQuestions(); const filtered = questions.filter(q => q.teacherId === teacherId); res.json(filtered); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.delete("/api/questions/delete/:id", async (req, res) => { try { await getAuthenticatedUserId(req); const { id } = req.params; let questions = loadLocalQuestions(); questions = questions.filter(q => q.id !== id); saveLocalQuestions(questions); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.delete("/api/groups/delete/:id", async (req, res) => { try { await getAuthenticatedUserId(req); const { id } = req.params; let groups = loadLocalGroups(); groups = groups.filter(g => g.id !== id); saveLocalGroups(groups); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.get("/api/groups/list", async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); const groups = loadLocalGroups(); const filtered = groups.filter(g => g.teacherId === teacherId); res.json(filtered); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.get("/api/student/groups", async (req, res) => { try { const studentId = await getAuthenticatedUserId(req); const groups = loadLocalGroups(); const filtered = groups.filter(g => g.studentIds && g.studentIds.includes(studentId)); res.json(filtered); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.get("/api/attempts/get/:id", async (req, res) => { try { const { id } = req.params; const attempts = loadLocalAttempts(); const attempt = attempts.find(a => a.id === id); if (!attempt) return res.status(404).json({ error: "Attempt not found" }); res.json(attempt); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.post("/api/questions/batch", async (req, res) => { try { const { ids } = req.body; const questions = loadLocalQuestions(); const filtered = questions.filter(q => ids.includes(q.id)); res.json(filtered); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.get("/api/exams/get/:id", async (req, res) => { try { const { id } = req.params; const exams = await loadExamsFromDb(); const exam = exams.find(e => e.id === id); if (!exam) return res.status(404).json({ error: "Exam not found" }); res.json(exam); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.get("/api/exams/list", async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); const exams = await loadExamsFromDb(); const filtered = exams.filter(e => e.teacherId === teacherId); filtered.sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); // Enrich with dynamic average performance from Firebase const attempts = await loadAttemptsFromDb(); const enriched = filtered.map((exam: any) => { const examAttempts = attempts.filter((a: any) => a.examId === exam.id && a.status === 'completed'); const totalScore = examAttempts.reduce((acc: number, curr: any) => acc + (curr.percentage || 0), 0); const avg = examAttempts.length > 0 ? Math.round(totalScore / examAttempts.length) : 0; return { ...exam, avgPerformance: avg, attemptsCount: examAttempts.length }; }); res.json(enriched); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.post("/api/exams/update/:id", async (req, res) => { try { await getAuthenticatedUserId(req); const { id } = req.params; const updateData = req.body; const exams = await loadExamsFromDb(); const idx = exams.findIndex(e => e.id === id); if (idx !== -1) { const updatedExam = { ...exams[idx], ...updateData, updatedAt: new Date().toISOString() }; // Re-trigger auto status calculation of update parameters updatedExam.status = getTransitionedStatus(updatedExam); await saveExamToDb(updatedExam); res.json({ success: true }); } else { res.status(404).json({ error: "Exam not found" }); } } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.get("/api/exams/attempts/:examId", async (req, res) => { try { const { examId } = req.params; const attempts = loadLocalAttempts(); const filtered = attempts.filter(a => a.examId === examId); res.json(filtered); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.post("/api/attempts/start", async (req, res) => { try { const { examId, student } = req.body; const attempts = loadLocalAttempts(); const existing = attempts.find(a => a.examId === examId && a.studentId === student.id && a.status === 'started'); if (existing) { return res.json({ attemptId: existing.id }); } const attemptId = `ATT-${Math.random().toString(36).substring(2, 6).toUpperCase()}`; const attempt = { id: attemptId, examId, studentId: student.id, studentName: student.fullName, studentClass: student.class || 'Unknown', status: 'started', answers: {}, score: 0, percentage: 0, timeSpent: 0, startedAt: new Date().toISOString() }; attempts.push(attempt); saveLocalAttempts(attempts); res.json({ attemptId }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.post("/api/attempts/progress/:id", async (req, res) => { try { const { id } = req.params; const { answers, timeSpent } = req.body; const attempts = loadLocalAttempts(); const idx = attempts.findIndex(a => a.id === id); if (idx !== -1) { attempts[idx].answers = answers; attempts[idx].timeSpent = timeSpent; attempts[idx].updatedAt = new Date().toISOString(); saveLocalAttempts(attempts); res.json({ success: true }); } else { res.status(404).json({ error: "Attempt not found" }); } } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.get("/api/student/attempts", async (req, res) => { try { const studentId = await getAuthenticatedUserId(req); const attempts = loadLocalAttempts(); const filtered = attempts.filter(a => a.studentId === studentId); res.json(filtered); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // Draft Persistence & Recovery Cache API (Level 2 Cloud Synchronization) app.post("/api/drafts/sync", async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); const draftData = req.body; draftData.updatedAt = new Date().toISOString(); // Save to Firestore Sync Cache try { await db.collection("drafts").doc(teacherId).set(draftData); } catch (e) { console.warn("Firestore draft sync failed:", e); } // Also save a local backup copy const file = path.join(process.cwd(), `draft-${teacherId}.json`); fs.writeFileSync(file, JSON.stringify(draftData, null, 2), 'utf-8'); res.json({ success: true, draft: draftData }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.get("/api/drafts/get", async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); // Check local copy first let localDraft: any = null; const file = path.join(process.cwd(), `draft-${teacherId}.json`); if (fs.existsSync(file)) { try { localDraft = JSON.parse(fs.readFileSync(file, 'utf-8')); } catch {} } // Check Firestore copy let remoteDraft: any = null; try { const doc = await db.collection("drafts").doc(teacherId).get(); if (doc.exists) { remoteDraft = doc.data(); } } catch (e) { console.warn("Firestore draft retrieving failed:", e); } // Sync Reconciliation: return the most recently updated draft if (localDraft && remoteDraft) { const localTime = new Date(localDraft.updatedAt || 0).getTime(); const remoteTime = new Date(remoteDraft.updatedAt || 0).getTime(); if (remoteTime > localTime) { res.json(remoteDraft); } else { res.json(localDraft); } } else { res.json(remoteDraft || localDraft || null); } } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.post("/api/drafts/clear", async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); const file = path.join(process.cwd(), `draft-${teacherId}.json`); if (fs.existsSync(file)) { fs.unlinkSync(file); } try { await db.collection("drafts").doc(teacherId).delete(); } catch {} res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); app.get("/api/teacher/stats", async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); const exams = (await loadExamsFromDb()).filter(e => e.teacherId === teacherId); const questions = (await loadQuestionsFromDb()).filter(q => q.teacherId === teacherId); const examIds = exams.map(e => e.id); const attempts = (await loadAttemptsFromDb()).filter(a => examIds.includes(a.examId)); let totalScore = 0; attempts.forEach(a => { totalScore += a.percentage || 0; }); const users = await loadUsersFromDb(); const students = users.filter(u => u.role === 'student'); res.json({ totalExams: exams.length, totalQuestions: questions.length, totalSubmissions: attempts.length, totalStudents: students.length, avgScore: attempts.length > 0 ? Math.round(totalScore / attempts.length) : 0 }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // NVIDIA NIM Caching & Rate Limiting Configurations const aiCache = new Map(); const clientAiUsage: Record = {}; // Rate Limiting MiddleWare with professional feedback messages function aiRateLimiter(req: any, res: any, next: any) { const ip = req.ip || 'unknown-ip'; const userId = req.cookies?.jwt_session ? (jwt.decode(req.cookies.jwt_session) as any)?.uid : ip; const key = `${userId || 'anon'}`; const now = Date.now(); const windowMs = 60 * 1000; // 1 minute window const maxRequestsPerMinute = 15; if (!clientAiUsage[key]) { clientAiUsage[key] = { requests: 1, windowStart: now }; } else { const limit = clientAiUsage[key]; if (now - limit.windowStart > windowMs) { limit.requests = 1; limit.windowStart = now; } else { limit.requests++; if (limit.requests > maxRequestsPerMinute) { return res.status(429).json({ error: "AI processing limit reached. Please try again shortly." }); } } } next(); } // Smart Routing & Cost Optimization Engine function routeModel(taskType: string, inputLength: number): { model: string; reason: string } { // Llama 3.1 8B Instruct -> fast formatting, formatting cleanup, JSON extraction, low-latency normalizations // Llama 3.1 70B Instruct -> deep evaluations, malformed question audits, duplicate checks, difficulty breakdowns, analytics const isHeavyTask = [ 'integrity_validation', 'difficulty_analysis', 'duplicate_detection', 'analytics_interpretation' ].includes(taskType); if (isHeavyTask || inputLength > 8000) { return { model: 'meta/llama-3.1-70b-instruct', reason: isHeavyTask ? `Escalated to Llama 3.1 70B Instruct due to cognitive reasoning category: [${taskType}]` : 'Escalated to Llama 3.1 70B Instruct due to heavy input sequence size (>8KB).' }; } return { model: 'meta/llama-3.1-8b-instruct', reason: `Routed to Llama 3.1 8B Instruct for cost-aware rapid throughput: [${taskType}]` }; } // Helper code to safely communicate with NVIDIA NIM APIs async function callNvidiaNim(model: string, systemPrompt: string, userPrompt: string, maxTokens = 4096): Promise { const apiKey = process.env.NVIDIA_API_KEY; if (!apiKey) { throw new Error("NVIDIA_API_KEY is not configured on the server. Please enter your NVIDIA API token in the settings panel."); } const nvResponse = await fetch("https://integrate.api.nvidia.com/v1/chat/completions", { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], temperature: 0.1, max_tokens: maxTokens }) }); if (!nvResponse.ok) { const errText = await nvResponse.text(); throw new Error(`NVIDIA NIM API call failed (${nvResponse.status}): ${errText}`); } const data: any = await nvResponse.json(); return data.choices[0]?.message?.content || ""; } // In-Memory Asynchronous Job Tracker (to prevent frontend freezing on heavier operations) interface AsyncJob { id: string; status: 'queued' | 'parsing' | 'validating' | 'completed' | 'failed'; progressLabel: string; progressPercent: number; result?: any; error?: string; createdAt: number; } const activeJobs: Record = {}; // Active Cleanups & Prompt Shielding sanitizer function sanitizePromptInput(text: string): string { if (!text) return ""; // strip potential instruction overrides return text .replace(/ignore previous instructions/gi, "[blocked injection]") .replace(/you are now an agent/gi, "[blocked injection]") .replace(/system: /gi, "user-content: ") .trim(); } // ROBUST JSON REPAIR & PARSE UTILITY function robustJsonParse(raw: string): any { let cleaned = raw.trim(); // 1. Strip potential Markdown code blocks if (cleaned.startsWith("```json")) { cleaned = cleaned.replace(/^```json/, "").replace(/```$/, "").trim(); } else if (cleaned.startsWith("```")) { cleaned = cleaned.replace(/^```/, "").replace(/```$/, "").trim(); } // 2. Remove comments (both single line // and multi line /* */) cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, ""); cleaned = cleaned.replace(/(?:^|\s)\/\/.*$/gm, ""); const performBasicRegexCleanup = (s: string) => { // Fix trailing commas in objects and arrays return s.replace(/,(\s*[\]\}])/g, "$1"); }; try { return JSON.parse(performBasicRegexCleanup(cleaned)); } catch (e) { // 3. Try to extract JSON array or object bounding boxed arrays const arrayMatch = cleaned.match(/\[\s*\{[\s\S]*\}\s*\]/); if (arrayMatch) { try { return JSON.parse(performBasicRegexCleanup(arrayMatch[0])); } catch (err) {} } const objMatch = cleaned.match(/\{\s*[\s\S]*\}/); if (objMatch) { try { return JSON.parse(performBasicRegexCleanup(objMatch[0])); } catch (err) {} } // 4. Character-by-character cleaning to escape literal unescaped newlines/tabs inside quotes try { let insideQuote = false; let escaped = false; let buffer = ''; for (let i = 0; i < cleaned.length; i++) { const char = cleaned[i]; if (escaped) { buffer += char; escaped = false; continue; } if (char === '\\') { buffer += char; escaped = true; continue; } if (char === '"') { insideQuote = !insideQuote; buffer += char; continue; } if (insideQuote && (char === '\n' || char === '\r')) { buffer += '\\n'; } else if (insideQuote && char === '\t') { buffer += '\\t'; } else { buffer += char; } } return JSON.parse(performBasicRegexCleanup(buffer)); } catch (eInner) { throw e; // Throw original error if character pass fails } } } // DATABASE LOGGING UTIL: Local JSON audit logging of AI workloads async function logAiOperation(userId: string, taskType: string, model: string, routingReason: string, inputLength: number, durationMs: number, status: 'success' | 'failed', errorMsg?: string) { try { let logs: any[] = []; if (fs.existsSync(AI_LOGS_FILE)) { logs = JSON.parse(fs.readFileSync(AI_LOGS_FILE, 'utf-8')); } logs.push({ userId, taskType, model, routingReason, inputLength, durationMs, status, errorMsg: errorMsg || null, timestamp: new Date().toISOString() }); fs.writeFileSync(AI_LOGS_FILE, JSON.stringify(logs, null, 2), 'utf-8'); } catch (err) { console.warn("Failed to log AI trace locally:", err); } } // ENDPOINT 1: Start a Multi-Stage Asynchronous Exam Construction & Validation Pipeline app.post("/api/ai/jobs/start", aiRateLimiter, async (req, res) => { try { const teacherId = await getAuthenticatedUserId(req); const { text, title = "Exam Sheet" } = req.body; if (!text || text.trim().length === 0) { return res.status(400).json({ error: "Missing document text parameters to compile" }); } const sanitizedText = sanitizePromptInput(text); const textHash = crypto.createHash ? crypto.createHash('sha256').update(sanitizedText).digest('hex') : sanitizedText.length.toString(); // Check In-Memory Cache const cachedResult = aiCache.get(textHash); if (cachedResult && Date.now() < cachedResult.expiresAt) { const cachedJobId = `job-cached-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`; activeJobs[cachedJobId] = { id: cachedJobId, status: 'completed', progressLabel: "Exam retrieved from local high-speed cache!", progressPercent: 100, result: cachedResult.result, createdAt: Date.now() }; return res.json({ jobId: cachedJobId }); } // Spawn new real-time Async job const jobId = `job-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; activeJobs[jobId] = { id: jobId, status: 'queued', progressLabel: "Exam compilation task queued successfully...", progressPercent: 10, createdAt: Date.now() }; // Kick off non-blocking background orchestration (async () => { const startTime = Date.now(); try { // --- STAGE 2: 8B Extracts structured questions --- activeJobs[jobId].status = 'parsing'; activeJobs[jobId].progressLabel = "Parsing document structure using fast Llama 3.1 8B..."; activeJobs[jobId].progressPercent = 40; const routerDecisionParser = routeModel('extraction', sanitizedText.length); const systemPromptParser = `You are a strict, secure educational text-to-JSON parser. Your task is to extract multiple choice questions (MCQ) from the raw user text input. Output MUST be return as a valid JSON array matching the schema: [ { "text": "The full question text?", "options": [ { "id": "opt-1", "text": "Option text" }, { "id": "opt-2", "text": "Option text" }, { "id": "opt-3", "text": "Option text" }, { "id": "opt-4", "text": "Option text" } ], "correctOptionId": "opt-1", "difficulty": "easy" | "medium" | "hard", "category": "Algebra" } ] Format Rules: 1. Every question MUST have exactly 4 options with exact IDs: opt-1, opt-2, opt-3, opt-4. 2. If correct answer is missing or not declared, deduce it realistically with your knowledge. 3. Keep the JSON clean and valid. Do NOT append any markdown wrappers or comments. Only respond with raw parseable JSON string.`; const parserOutput = await callNvidiaNim(routerDecisionParser.model, systemPromptParser, sanitizedText, 4096); let cleanedParser = parserOutput.trim(); let questionsList: any[] = []; try { questionsList = robustJsonParse(cleanedParser); if (!Array.isArray(questionsList)) { questionsList = [questionsList]; } } catch (jsonErr: any) { console.warn("Llama 3.1 8B JSON parse recovery triggered:", jsonErr.message); // Quick fallback strip or regex fallback const jsonMatch = cleanedParser.match(/\[\s*\{[\s\S]*\}\s*\]/); if (jsonMatch) { try { questionsList = robustJsonParse(jsonMatch[0]); } catch (fallbackErr: any) { console.error("Llama 3.1 8B fallback parsing also failed:", fallbackErr.message); throw new Error("Target response formatting was not compliant with schema standards. Please adjust inputs."); } } else { throw new Error("Target response formatting was not compliant with schema standards. Please adjust inputs."); } } // --- STAGE 3: Llama 3.1 70B runs Deep Validation & Integrity Checks --- activeJobs[jobId].status = 'validating'; activeJobs[jobId].progressLabel = "Auditing questions and academic integrity using Llama 3.1 70B..."; activeJobs[jobId].progressPercent = 75; const routerDecisionAudit = routeModel('integrity_validation', JSON.stringify(questionsList).length); const systemPromptAudit = `You are an elite academic curriculum developer operating Llama 3.1 70B. Evaluate the structured questions for deep instructional integrity: 1. Examine structural errors (missing options, duplicates options list). 2. Detect ambiguous options or questions that might contain multiple correct answers. 3. Detect identical duplicate questions or near-duplicate phrasings. 4. Calculate a quality grade score (0-100) and general difficulty index. Output must be in exact JSON layout: { "examQualityScore": 0-100, "warnings": [ { "type": "critical" | "warning" | "info", "message": "Clear specific warning of question index" } ], "duplicates": [ { "indices": [0, 1], "reason": "Explanation" } ] } Only output the JSON object without code blocks or markdown wrappers.`; const auditOutput = await callNvidiaNim(routerDecisionAudit.model, systemPromptAudit, JSON.stringify(questionsList), 2048); let cleanedAudit = auditOutput.trim(); let auditReport: any = { examQualityScore: 90, warnings: [], duplicates: [] }; try { auditReport = robustJsonParse(cleanedAudit); } catch (auditErr: any) { console.warn("Llama 3.1 70B audit parsing fallback:", auditErr.message); } // Merge findings const compileSummary = { questions: questionsList, auditReport, timeSpentMs: Date.now() - startTime }; // Update local Cache (expires in 1 hour) aiCache.set(textHash, { result: compileSummary, expiresAt: Date.now() + 60 * 60 * 1000 }); // Update Job Status activeJobs[jobId].status = 'completed'; activeJobs[jobId].progressLabel = "Exam structure audited & successfully compiled!"; activeJobs[jobId].progressPercent = 100; activeJobs[jobId].result = compileSummary; // Log operation trace in Firestore await logAiOperation( teacherId, 'document_compilation_pipeline', `${routerDecisionParser.model} + ${routerDecisionAudit.model}`, `${routerDecisionParser.reason} | ${routerDecisionAudit.reason}`, text.length, Date.now() - startTime, 'success' ); } catch (bgError: any) { console.error("Background AI Orchestrator crashed:", bgError); activeJobs[jobId].status = 'failed'; activeJobs[jobId].progressLabel = "Compilation pipeline failed."; activeJobs[jobId].error = bgError.message || "An unexpected failure occurred inside NVIDIA NIM API pipelines."; await logAiOperation( teacherId, 'document_compilation_pipeline', 'hybrid', 'Failed inside async promise pipeline handler', text.length, Date.now() - startTime, 'failed', bgError.message ); } })(); res.json({ jobId }); } catch (err: any) { console.error("Jobs Start Error:", err); res.status(500).json({ error: err.message }); } }); // ENDPOINT 2: Fetch and query async background parsing jobs status app.get("/api/ai/jobs/status/:id", async (req, res) => { try { const jobId = req.params.id; const job = activeJobs[jobId]; if (!job) { return res.status(404).json({ error: "Compilation job task not found." }); } res.json(job); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ENDPOINT 3: Quick Direct Questions Validation (Uses Deep Model - Llama 3.1 70B) app.post("/api/ai/validate", aiRateLimiter, async (req, res) => { const startTime = Date.now(); try { const userId = await getAuthenticatedUserId(req); const { questions } = req.body; if (!questions || !Array.isArray(questions)) { return res.status(400).json({ error: "Missing structured questions list parameters" }); } const inputDataStr = JSON.stringify(questions); const hashKey = crypto.createHash ? crypto.createHash('sha256').update("validate:" + inputDataStr).digest('hex') : "validate:" + inputDataStr.length; // Check Cache const cached = aiCache.get(hashKey); if (cached) { return res.json(cached); } const systemPrompt = `You are an elite educational quality inspector operating Llama 3.1 70B. Scan the questions for any errors: malformed MCQs, duplicate options, missing options, multiple correct options, ambiguous questions, near-duplicates, clarity score (0-100). Output must follow the JSON schema ONLY: { "examQualityScore": 0-100, "warnings": [ { "type": "critical"|"warning"|"info", "message": "Short actionable academic feedback" } ], "duplicates": [ { "indices": [0,1], "reason": "Explanation" } ] } No conversational padding! Format as pure parseable JSON.`; const rawResponse = await callNvidiaNim('meta/llama-3.1-70b-instruct', systemPrompt, inputDataStr, 2048); let cleaned = rawResponse.trim(); const auditReport = robustJsonParse(cleaned); aiCache.set(hashKey, { result: auditReport, expiresAt: Date.now() + 20 * 60 * 1000 }); // 20 min TTL cache await logAiOperation(userId, 'integrity_validation', 'meta/llama-3.1-70b-instruct', 'Explicit request validation', inputDataStr.length, Date.now() - startTime, 'success'); res.json(auditReport); } catch (err: any) { console.error("AI Validate Error:", err); res.status(500).json({ error: err.message }); } }); // ENDPOINT 4: Analytics Interpretation Engine converting quantitative details to structured academic feedback app.post("/api/ai/analytics-explain", aiRateLimiter, async (req, res) => { const startTime = Date.now(); try { const userId = await getAuthenticatedUserId(req); const { metrics } = req.body; if (!metrics) { return res.status(400).json({ error: "Missing quantitative assessment parameters to explain" }); } const contentStr = JSON.stringify(metrics); const hashKey = "analytics:" + contentStr; const cached = aiCache.get(hashKey); if (cached) return res.json(cached); const systemPrompt = `You are an expert educational psychologist & strategic consultant. Translate raw CBT student metrics (scores, failed items, category details) into highly scannable, actionable educational insights. Do not wrap responses in conversational noise or markdown. Provide response inside a clean standardized schema layout: { "summary": "High level expert review summary", "strengths": ["Item strength 1", "Item strength 2"], "weaknesses": ["Improvement point 1", "Improvement point 2"], "recommendations": ["Instructional adjustment advice 1", "Instructional adjustment advice 2"] }`; const rawResult = await callNvidiaNim('meta/llama-3.1-70b-instruct', systemPrompt, contentStr, 2048); let cleaned = rawResult.trim(); const explanation = robustJsonParse(cleaned); aiCache.set(hashKey, { result: explanation, expiresAt: Date.now() + 60 * 60 * 1000 }); // cached for 1 hour await logAiOperation(userId, 'analytics_interpretation', 'meta/llama-3.1-70b-instruct', 'High-level analytics explanation', contentStr.length, Date.now() - startTime, 'success'); res.json(explanation); } catch (err: any) { console.error("Analytics Interpretation Error:", err); res.status(500).json({ error: err.message }); } }); // ENDPOINT 5: Academic contextual assistant for classroom teachers app.post("/api/ai/assistant", aiRateLimiter, async (req, res) => { const startTime = Date.now(); try { const userId = await getAuthenticatedUserId(req); const { prompt, context } = req.body; if (!prompt) { return res.status(400).json({ error: "Missing conversational instructions" }); } const cleanPrompt = sanitizePromptInput(prompt); const complexityString = cleanPrompt + JSON.stringify(context || {}); // Choose model based on query size and content const routerDecision = routeModel('teacher_assistant', complexityString.length); const systemPrompt = `You are ExamForge's integrated educational assistant. Provide academic, concise, highly professional feedback on assessment structure, topics, and question formulations. Never speak in overly casual greetings, do not use excessive emojis, and do NOT larp. Keep responses humble, intelligent and direct. Current operational context: ${JSON.stringify(context || {})}`; const output = await callNvidiaNim(routerDecision.model, systemPrompt, cleanPrompt, 2048); await logAiOperation( userId, 'teacher_assistant', routerDecision.model, routerDecision.reason, complexityString.length, Date.now() - startTime, 'success' ); res.json({ response: output.trim(), modelUsed: routerDecision.model }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // NVIDIA AI SERVICE LAYER PROXY ENDPOINTS // 1. generateQuestions app.post("/api/ai/generate-questions", nvidiaRateLimiter, async (req, res) => { const startTime = Date.now(); try { const userId = await getAuthenticatedUserId(req).catch(() => "anonymous-student"); const { prompt, count = 5 } = req.body; if (!prompt) return res.status(400).json({ error: "Instruction prompt is required" }); const systemPrompt = `You are an elite academic curriculum architect using Llama 3.1 70B. Generate exactly ${count} multiple-choice questions (MCQ) regarding the subject/text: "${prompt}". Your response MUST be valid JSON array of objects strictly following this format (no conversational chatter, no backticks, no markdown): [ { "id": "qst-random8", "text": "Correct academic question formulation?", "options": [ { "id": "opt-1", "text": "Option A" }, { "id": "opt-2", "text": "Option B" }, { "id": "opt-3", "text": "Option C" }, { "id": "opt-4", "text": "Option D" } ], "correctOptionId": "opt-1", "difficulty": "easy" | "medium" | "hard", "category": "${prompt}", "tags": [] } ]`; const rawResponse = await callNvidiaNim('meta/llama-3.1-70b-instruct', systemPrompt, prompt, 3000); const parsed = robustJsonParse(rawResponse.trim()); await logAiOperation(userId, 'generate_questions', 'meta/llama-3.1-70b-instruct', 'NvidiaAIService API call', prompt.length, Date.now() - startTime, 'success'); res.json({ questions: parsed }); } catch (err: any) { console.error("AI Generate Questions Error:", err); res.status(500).json({ error: err.message }); } }); // 2. evaluateAnswers app.post("/api/ai/evaluate-answers", nvidiaRateLimiter, async (req, res) => { const startTime = Date.now(); try { const userId = await getAuthenticatedUserId(req).catch(() => "anonymous-student"); const { questions, answers } = req.body; if (!questions || !answers) return res.status(400).json({ error: "Questions list and answers list are required" }); const systemPrompt = `You are an elite academic scorer running on NVIDIA Nemotron-4-340B. Evaluate the correctness of the student's selected answers against the core questions context. Provide an objective grading, percentage, and constructive diagnostic summary feedback. Your output MUST be a clean parseable JSON object matching: { "score": number, "total": number, "percentage": number, "feedback": "Constructive instructional diagnostics" }`; const rawResponse = await callNvidiaNim('nvidia/nemotron-4-340b-instruct', systemPrompt, JSON.stringify({ questions, answers }), 2000); const parsed = robustJsonParse(rawResponse.trim()); await logAiOperation(userId, 'evaluate_answers', 'nvidia/nemotron-4-340b-instruct', 'NvidiaAIService API call', JSON.stringify({ questions, answers }).length, Date.now() - startTime, 'success'); res.json(parsed); } catch (err: any) { console.error("AI Evaluate Answers Error:", err); res.status(500).json({ error: err.message }); } }); // 3. explainAnswer app.post("/api/ai/explain-answer", nvidiaRateLimiter, async (req, res) => { const startTime = Date.now(); try { const userId = await getAuthenticatedUserId(req).catch(() => "anonymous-student"); const { question, selectedOptionId } = req.body; if (!question) return res.status(400).json({ error: "Question context is required" }); const systemPrompt = `You are a helpful, clear academic mentor operating Meta Llama 3.1 70B. Analyze this MCQ question and provide a highly scannable, textbook-accurate explanation of: - Why the declared correct option is correct. - Why the student's selected option (ID: '${selectedOptionId}') is correct or incorrect. Keep the explanation clear, professional, direct and humble. No conversational fluff or markdown code fence blocks.`; const rawResponse = await callNvidiaNim('meta/llama-3.1-70b-instruct', systemPrompt, JSON.stringify({ question, selectedOptionId }), 2000); await logAiOperation(userId, 'explain_answer', 'meta/llama-3.1-70b-instruct', 'NvidiaAIService API call', JSON.stringify({ question, selectedOptionId }).length, Date.now() - startTime, 'success'); res.json({ explanation: rawResponse.trim() }); } catch (err: any) { console.error("AI Explain Answer Error:", err); res.status(500).json({ error: err.message }); } }); // 4. generateDistractors app.post("/api/ai/generate-distractors", nvidiaRateLimiter, async (req, res) => { const startTime = Date.now(); try { const userId = await getAuthenticatedUserId(req).catch(() => "anonymous-student"); const { questionText, correctOptionText } = req.body; if (!questionText || !correctOptionText) return res.status(400).json({ error: "Question and correct option are required" }); const systemPrompt = `You are a micro-instructional parser operating fast Llama 3.1 8B. Generate exactly 3 believable, common-misconception multiple choice distractors (wrong answers) for: Question: "${questionText}" Correct Option: "${correctOptionText}" Your response MUST be strict JSON object only: { "distractors": ["Wrong Option A", "Wrong Option B", "Wrong Option C"] }`; const rawResponse = await callNvidiaNim('meta/llama-3.1-8b-instruct', systemPrompt, `${questionText} | ${correctOptionText}`, 1000); const parsed = robustJsonParse(rawResponse.trim()); await logAiOperation(userId, 'generate_distractors', 'meta/llama-3.1-8b-instruct', 'NvidiaAIService API call', questionText.length, Date.now() - startTime, 'success'); res.json(parsed); } catch (err: any) { console.error("AI Distractors Error:", err); res.status(500).json({ error: err.message }); } }); // 5. difficultyAnalysis app.post("/api/ai/difficulty-analysis", nvidiaRateLimiter, async (req, res) => { const startTime = Date.now(); try { const userId = await getAuthenticatedUserId(req).catch(() => "anonymous-student"); const { questions } = req.body; if (!questions) return res.status(400).json({ error: "Questions list is required" }); const systemPrompt = `You are an elite educational psychometrics officer operating Llama 3.1 70B. Analyze the cognitive load, logical rigor, and semantic difficulty of this question bank. Your response MUST be valid JSON only: { "difficultyIndex": "easy" | "medium" | "hard", "reasoningLoad": "low" | "medium" | "high", "skillsRequired": ["Skill A", "Skill B"] }`; const rawResponse = await callNvidiaNim('meta/llama-3.1-70b-instruct', systemPrompt, JSON.stringify(questions), 2000); const parsed = robustJsonParse(rawResponse.trim()); await logAiOperation(userId, 'difficulty_analysis', 'meta/llama-3.1-70b-instruct', 'NvidiaAIService API call', JSON.stringify(questions).length, Date.now() - startTime, 'success'); res.json(parsed); } catch (err: any) { console.error("AI Difficulty Analysis Error:", err); res.status(500).json({ error: err.message }); } }); // 6. syllabusMapping app.post("/api/ai/syllabus-mapping", nvidiaRateLimiter, async (req, res) => { const startTime = Date.now(); try { const userId = await getAuthenticatedUserId(req).catch(() => "anonymous-student"); const { questions, syllabusTerms } = req.body; if (!questions || !syllabusTerms) return res.status(400).json({ error: "Questions list and syllabus terms are required" }); const systemPrompt = `You are an elite curriculum mapping engine operating Llama 3.1 70B. Map the provided set of questions against the listed core syllabus terms. Your response MUST be valid JSON only: { "mappings": [ { "questionId": "string-id", "mappedTopic": "Matched syllabus term", "confidence": "high" | "medium" | "low", "reasoning": "Reason explanation string" } ] }`; const rawResponse = await callNvidiaNim('meta/llama-3.1-70b-instruct', systemPrompt, JSON.stringify({ questions, syllabusTerms }), 2000); const parsed = robustJsonParse(rawResponse.trim()); await logAiOperation(userId, 'syllabus_mapping', 'meta/llama-3.1-70b-instruct', 'NvidiaAIService API call', JSON.stringify({ questions, syllabusTerms }).length, Date.now() - startTime, 'success'); res.json(parsed); } catch (err: any) { console.error("AI Syllabus Mapping Error:", err); res.status(500).json({ error: err.message }); } }); async function startServer() { const PORT = 3000; if (process.env.NODE_ENV !== "production") { const vite = await createViteServer({ server: { middlewareMode: true }, appType: "spa", }); app.use(vite.middlewares); } else { const distPath = path.join(process.cwd(), 'dist'); app.use(express.static(distPath)); app.get('*', (req, res) => { res.sendFile(path.join(distPath, 'index.html')); }); } app.listen(PORT, "0.0.0.0", () => { console.log(`Server running on http://localhost:${PORT}`); }); } startServer();