UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

443 lines (442 loc) • 16.5 kB
"use strict"; /** * @fileoverview Dashboard API Routes - Pure API endpoints for dashboard data * @version 2.0.0 * @since 2025-08-04 * @module DashboardApiRoutes * @description RESTful API endpoints for dashboard data, replacing server-rendered HTML * @contributors Claude Code Agent * @requirements REQ-API-002, REQ-AUTH-002 (Dashboard API) */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.DashboardApiRoutes = void 0; exports.createDashboardApiRoutes = createDashboardApiRoutes; const express = __importStar(require("express")); /** * Dashboard API Routes Handler * * @description Pure API endpoints for dashboard functionality. * Serves JSON data instead of rendered HTML. */ class DashboardApiRoutes { constructor(authService) { this.router = express.Router(); this.authService = authService; this.setupRoutes(); } getRouter() { return this.router; } /** * Setup API routes */ setupRoutes() { // GET /dashboard - Serve static HTML file this.router.get('/', this.serveDashboardPage.bind(this)); // GET /dashboard/api/user - Current user information this.router.get('/api/user', this.authenticateToken.bind(this), this.getUserInfo.bind(this)); // GET /dashboard/api/stats - Dashboard statistics this.router.get('/api/stats', this.authenticateToken.bind(this), this.getDashboardStats.bind(this)); // GET /dashboard/api/quizzes - User's quizzes this.router.get('/api/quizzes', this.authenticateToken.bind(this), this.getUserQuizzes.bind(this)); // GET /dashboard/api/activity - Recent activity this.router.get('/api/activity', this.authenticateToken.bind(this), this.getRecentActivity.bind(this)); // GET /dashboard/api/analytics - Quiz analytics this.router.get('/api/analytics', this.authenticateToken.bind(this), this.getQuizAnalytics.bind(this)); // PUT /dashboard/api/user/profile - Update user profile this.router.put('/api/user/profile', this.authenticateToken.bind(this), this.updateUserProfile.bind(this)); // Legacy routes for backward compatibility this.router.get('/profile', this.authenticateToken.bind(this), this.serveDashboardPage.bind(this)); this.router.get('/quizzes', this.authenticateToken.bind(this), this.serveDashboardPage.bind(this)); this.router.get('/analytics', this.authenticateToken.bind(this), this.serveDashboardPage.bind(this)); } /** * Serve static dashboard HTML page */ serveDashboardPage(req, res) { // Serve the static dashboard.html file with progressive enhancement res.sendFile('dashboard.html', { root: 'public/pages', headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', Pragma: 'no-cache', Expires: '0', }, }, err => { if (err) { console.error('Failed to serve dashboard.html:', err); res.status(404).json({ success: false, error: { code: 'DASHBOARD_NOT_FOUND', message: 'Dashboard page not found', }, }); } }); } /** * Token authentication middleware */ async authenticateToken(req, res, next) { var _a; try { const authHeader = req.headers.authorization; const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN // Also check for token in cookies (for web dashboard) const cookieToken = (_a = req.cookies) === null || _a === void 0 ? void 0 : _a.auth_token; const finalToken = token || cookieToken; if (!finalToken) { // Return JSON error for API requests res.status(401).json({ success: false, error: { code: 'AUTHENTICATION_REQUIRED', message: 'Authentication token required', }, }); return; } const authResult = await this.authService.validateToken(finalToken); req.user = authResult.user; req.sessionId = authResult.sessionId; next(); } catch (error) { res.status(401).json({ success: false, error: { code: 'INVALID_TOKEN', message: 'Invalid or expired token', }, }); } } /** * Get current user information */ async getUserInfo(req, res) { try { const user = req.user; res.json({ success: true, data: { id: user.id, username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName, roles: user.roles, permissions: user.permissions, isActive: user.isActive, createdAt: user.createdAt, lastLoginAt: user.lastLoginAt, }, meta: { timestamp: new Date().toISOString(), requestId: req.requestId, }, }); } catch (error) { console.error('Failed to get user info:', error); res.status(500).json({ success: false, error: { code: 'USER_INFO_ERROR', message: 'Failed to retrieve user information', }, }); } } /** * Get dashboard statistics */ async getDashboardStats(req, res) { try { const user = req.user; // Mock data - replace with actual database queries const stats = { totalQuizzes: 12, totalResponses: 156, recentActivity: 8, completionRate: 87.5, averageScore: 78.2, popularQuizzes: 3, thisWeekActivity: 24, lastWeekActivity: 18, }; res.json({ success: true, data: stats, meta: { timestamp: new Date().toISOString(), userId: user.id, requestId: req.requestId, }, }); } catch (error) { console.error('Failed to get dashboard stats:', error); res.status(500).json({ success: false, error: { code: 'STATS_ERROR', message: 'Failed to retrieve dashboard statistics', }, }); } } /** * Get user's quizzes */ async getUserQuizzes(req, res) { try { const user = req.user; const limit = parseInt(req.query.limit) || 10; const offset = parseInt(req.query.offset) || 0; const sort = req.query.sort || 'recent'; // Mock data - replace with actual database queries const mockQuizzes = [ { id: 'quiz-001', title: 'JavaScript Fundamentals', description: 'Test your knowledge of JavaScript basics', status: 'published', questions: [ { id: 'q1', question: 'What is a closure?', options: ['A', 'B', 'C'], answer: 'A' }, ], createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), updatedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), createdBy: user.id, }, { id: 'quiz-002', title: 'React Hooks', description: 'Understanding React Hooks', status: 'draft', questions: [], createdAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), updatedAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), createdBy: user.id, }, ]; const sortedQuizzes = sort === 'recent' ? mockQuizzes.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) : mockQuizzes; const paginatedQuizzes = sortedQuizzes.slice(offset, offset + limit); res.json({ success: true, data: { quizzes: paginatedQuizzes, total: mockQuizzes.length, limit, offset, hasMore: offset + limit < mockQuizzes.length, }, meta: { timestamp: new Date().toISOString(), userId: user.id, requestId: req.requestId, }, }); } catch (error) { console.error('Failed to get user quizzes:', error); res.status(500).json({ success: false, error: { code: 'QUIZZES_ERROR', message: 'Failed to retrieve quizzes', }, }); } } /** * Get recent activity */ async getRecentActivity(req, res) { try { const user = req.user; const limit = parseInt(req.query.limit) || 10; // Mock data - replace with actual database queries const activities = [ { id: 'activity-001', type: 'quiz_created', title: 'Created new quiz', description: 'JavaScript Fundamentals quiz was created', timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), relatedId: 'quiz-001', }, { id: 'activity-002', type: 'response_received', title: 'New quiz response', description: 'Someone completed your React Hooks quiz', timestamp: new Date(Date.now() - 5 * 60 * 60 * 1000).toISOString(), relatedId: 'quiz-002', }, ].slice(0, limit); res.json({ success: true, data: { activities, total: activities.length, }, meta: { timestamp: new Date().toISOString(), userId: user.id, requestId: req.requestId, }, }); } catch (error) { console.error('Failed to get recent activity:', error); res.status(500).json({ success: false, error: { code: 'ACTIVITY_ERROR', message: 'Failed to retrieve recent activity', }, }); } } /** * Get quiz analytics */ async getQuizAnalytics(req, res) { try { const user = req.user; const timeRange = req.query.timeRange || '30d'; // Mock analytics data - replace with actual calculations const analytics = { totalResponses: 156, averageScore: 78.2, completionRate: 87.5, responsesByDay: [ { date: '2025-08-01', count: 12 }, { date: '2025-08-02', count: 8 }, { date: '2025-08-03', count: 15 }, { date: '2025-08-04', count: 10 }, ], topPerformingQuizzes: [ { id: 'quiz-001', title: 'JavaScript Fundamentals', avgScore: 82.3, responses: 45 }, { id: 'quiz-002', title: 'React Hooks', avgScore: 74.1, responses: 32 }, ], questionAnalytics: [ { questionId: 'q1', correctAnswers: 38, totalAnswers: 45, accuracy: 84.4 }, ], }; res.json({ success: true, data: analytics, meta: { timestamp: new Date().toISOString(), timeRange, userId: user.id, requestId: req.requestId, }, }); } catch (error) { console.error('Failed to get quiz analytics:', error); res.status(500).json({ success: false, error: { code: 'ANALYTICS_ERROR', message: 'Failed to retrieve analytics', }, }); } } /** * Update user profile */ async updateUserProfile(req, res) { try { const user = req.user; const { firstName, lastName, bio, preferences } = req.body; // Validate input if (firstName && typeof firstName !== 'string') { res.status(400).json({ success: false, error: { code: 'INVALID_INPUT', message: 'firstName must be a string', }, }); return; } // Mock update - replace with actual database update const updatedUser = { ...user, firstName: firstName || user.firstName, lastName: lastName || user.lastName, bio: bio || user.bio, preferences: preferences || user.preferences, updatedAt: new Date().toISOString(), }; res.json({ success: true, data: updatedUser, meta: { timestamp: new Date().toISOString(), userId: user.id, requestId: req.requestId, }, }); } catch (error) { console.error('Failed to update user profile:', error); res.status(500).json({ success: false, error: { code: 'PROFILE_UPDATE_ERROR', message: 'Failed to update profile', }, }); } } } exports.DashboardApiRoutes = DashboardApiRoutes; /** * Factory function to create dashboard routes */ function createDashboardApiRoutes(authService) { const dashboardRoutes = new DashboardApiRoutes(authService); return dashboardRoutes.getRouter(); }