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
JavaScript
"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();
}