add changes

This commit is contained in:
AD2025
2025-11-11 00:25:50 +02:00
commit e3ca132c5e
86 changed files with 22238 additions and 0 deletions

View File

@@ -0,0 +1,288 @@
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const { User, GuestSession, QuizSession, sequelize } = require('../models');
const config = require('../config/config');
/**
* @desc Register a new user
* @route POST /api/auth/register
* @access Public
*/
exports.register = async (req, res) => {
const transaction = await sequelize.transaction();
try {
const { username, email, password, guestSessionId } = req.body;
// Check if user already exists
const existingUser = await User.findOne({
where: {
[sequelize.Sequelize.Op.or]: [
{ email: email.toLowerCase() },
{ username: username.toLowerCase() }
]
}
});
if (existingUser) {
await transaction.rollback();
if (existingUser.email === email.toLowerCase()) {
return res.status(400).json({
success: false,
message: 'Email already registered'
});
} else {
return res.status(400).json({
success: false,
message: 'Username already taken'
});
}
}
// Create new user (password will be hashed by beforeCreate hook)
const user = await User.create({
id: uuidv4(),
username: username.toLowerCase(),
email: email.toLowerCase(),
password: password,
role: 'user',
is_active: true
}, { transaction });
// Handle guest session migration if provided
let migratedData = null;
if (guestSessionId) {
try {
const guestSession = await GuestSession.findOne({
where: { guest_id: guestSessionId }
});
if (guestSession && !guestSession.is_converted) {
// Migrate quiz sessions from guest to user
const migratedSessions = await QuizSession.update(
{
user_id: user.id,
guest_session_id: null
},
{
where: { guest_session_id: guestSession.id },
transaction
}
);
// Mark guest session as converted
await guestSession.update({
is_converted: true,
converted_user_id: user.id,
converted_at: new Date()
}, { transaction });
// Recalculate user stats from migrated sessions
const quizSessions = await QuizSession.findAll({
where: {
user_id: user.id,
status: 'completed'
},
transaction
});
let totalQuizzes = quizSessions.length;
let quizzesPassed = 0;
let totalQuestionsAnswered = 0;
let correctAnswers = 0;
quizSessions.forEach(session => {
if (session.is_passed) quizzesPassed++;
totalQuestionsAnswered += session.questions_answered || 0;
correctAnswers += session.correct_answers || 0;
});
// Update user stats
await user.update({
total_quizzes: totalQuizzes,
quizzes_passed: quizzesPassed,
total_questions_answered: totalQuestionsAnswered,
correct_answers: correctAnswers
}, { transaction });
migratedData = {
quizzes: migratedSessions[0],
stats: {
totalQuizzes,
quizzesPassed,
accuracy: totalQuestionsAnswered > 0
? ((correctAnswers / totalQuestionsAnswered) * 100).toFixed(2)
: 0
}
};
}
} catch (guestError) {
// Log error but don't fail registration
console.error('Guest migration error:', guestError.message);
// Continue with registration even if migration fails
}
}
// Commit transaction before generating JWT
await transaction.commit();
// Generate JWT token (after commit to avoid rollback issues)
const token = jwt.sign(
{
userId: user.id,
email: user.email,
username: user.username,
role: user.role
},
config.jwt.secret,
{ expiresIn: config.jwt.expire }
);
// Return user data (exclude password)
const userData = user.toSafeJSON();
res.status(201).json({
success: true,
message: 'User registered successfully',
data: {
user: userData,
token,
migratedData
}
});
} catch (error) {
// Only rollback if transaction is still active
if (!transaction.finished) {
await transaction.rollback();
}
console.error('Registration error:', error);
res.status(500).json({
success: false,
message: 'Error registering user',
error: error.message
});
}
};
/**
* @desc Login user
* @route POST /api/auth/login
* @access Public
*/
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Find user by email
const user = await User.findOne({
where: {
email: email.toLowerCase(),
is_active: true
}
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid email or password'
});
}
// Verify password
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: 'Invalid email or password'
});
}
// Update last_login
await user.update({ last_login: new Date() });
// Generate JWT token
const token = jwt.sign(
{
userId: user.id,
email: user.email,
username: user.username,
role: user.role
},
config.jwt.secret,
{ expiresIn: config.jwt.expire }
);
// Return user data (exclude password)
const userData = user.toSafeJSON();
res.status(200).json({
success: true,
message: 'Login successful',
data: {
user: userData,
token
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Error logging in',
error: error.message
});
}
};
/**
* @desc Logout user (client-side token removal)
* @route POST /api/auth/logout
* @access Public
*/
exports.logout = async (req, res) => {
// Since we're using JWT (stateless), logout is handled client-side
// by removing the token from storage
res.status(200).json({
success: true,
message: 'Logout successful. Please remove token from client storage.'
});
};
/**
* @desc Verify JWT token and return user info
* @route GET /api/auth/verify
* @access Private
*/
exports.verifyToken = async (req, res) => {
try {
// User is already attached to req by verifyToken middleware
const user = await User.findByPk(req.user.userId);
if (!user || !user.isActive) {
return res.status(404).json({
success: false,
message: 'User not found or inactive'
});
}
// Return user data (exclude password)
const userData = user.toSafeJSON();
res.status(200).json({
success: true,
message: 'Token valid',
data: {
user: userData
}
});
} catch (error) {
console.error('Token verification error:', error);
res.status(500).json({
success: false,
message: 'Error verifying token',
error: error.message
});
}
};

View File

@@ -0,0 +1,481 @@
const { Category, Question } = require('../models');
/**
* @desc Get all active categories
* @route GET /api/categories
* @access Public
*/
exports.getAllCategories = async (req, res) => {
try {
// Check if request is from guest or authenticated user
const isGuest = !req.user; // If no user attached, it's a guest/public request
// Build query conditions
const whereConditions = {
isActive: true
};
// If guest, only show guest-accessible categories
if (isGuest) {
whereConditions.guestAccessible = true;
}
// Fetch categories
const categories = await Category.findAll({
where: whereConditions,
attributes: [
'id',
'name',
'slug',
'description',
'icon',
'color',
'questionCount',
'displayOrder',
'guestAccessible'
],
order: [
['displayOrder', 'ASC'],
['name', 'ASC']
]
});
res.status(200).json({
success: true,
count: categories.length,
data: categories,
message: isGuest
? `${categories.length} guest-accessible categories available`
: `${categories.length} categories available`
});
} catch (error) {
console.error('Error fetching categories:', error);
res.status(500).json({
success: false,
message: 'Error fetching categories',
error: error.message
});
}
};
/**
* @desc Get category details by ID
* @route GET /api/categories/:id
* @access Public (with optional auth for access control)
*/
exports.getCategoryById = async (req, res) => {
try {
const { id } = req.params;
const isGuest = !req.user;
// Validate ID (accepts UUID or numeric)
if (!id) {
return res.status(400).json({
success: false,
message: 'Invalid category ID'
});
}
// UUID format validation (basic check)
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
const isNumeric = !isNaN(id) && Number.isInteger(Number(id));
if (!isUUID && !isNumeric) {
return res.status(400).json({
success: false,
message: 'Invalid category ID format'
});
}
// Find category
const category = await Category.findByPk(id, {
attributes: [
'id',
'name',
'slug',
'description',
'icon',
'color',
'questionCount',
'displayOrder',
'guestAccessible',
'isActive'
]
});
// Check if category exists
if (!category) {
return res.status(404).json({
success: false,
message: 'Category not found'
});
}
// Check if category is active
if (!category.isActive) {
return res.status(404).json({
success: false,
message: 'Category not found'
});
}
// Check guest access
if (isGuest && !category.guestAccessible) {
return res.status(403).json({
success: false,
message: 'This category requires authentication. Please register or login to access.',
requiresAuth: true
});
}
// Get question preview (first 5 questions)
const questionPreview = await Question.findAll({
where: {
categoryId: id,
isActive: true
},
attributes: [
'id',
'questionText',
'questionType',
'difficulty',
'points',
'timesAttempted',
'timesCorrect'
],
order: [['createdAt', 'ASC']],
limit: 5
});
// Calculate category stats
const allQuestions = await Question.findAll({
where: {
categoryId: id,
isActive: true
},
attributes: ['difficulty', 'timesAttempted', 'timesCorrect']
});
const stats = {
totalQuestions: allQuestions.length,
questionsByDifficulty: {
easy: allQuestions.filter(q => q.difficulty === 'easy').length,
medium: allQuestions.filter(q => q.difficulty === 'medium').length,
hard: allQuestions.filter(q => q.difficulty === 'hard').length
},
totalAttempts: allQuestions.reduce((sum, q) => sum + (q.timesAttempted || 0), 0),
totalCorrect: allQuestions.reduce((sum, q) => sum + (q.timesCorrect || 0), 0)
};
// Calculate average accuracy
stats.averageAccuracy = stats.totalAttempts > 0
? Math.round((stats.totalCorrect / stats.totalAttempts) * 100)
: 0;
// Prepare response
const categoryData = {
id: category.id,
name: category.name,
slug: category.slug,
description: category.description,
icon: category.icon,
color: category.color,
questionCount: category.questionCount,
displayOrder: category.displayOrder,
guestAccessible: category.guestAccessible
};
res.status(200).json({
success: true,
data: {
category: categoryData,
questionPreview: questionPreview.map(q => ({
id: q.id,
questionText: q.questionText,
questionType: q.questionType,
difficulty: q.difficulty,
points: q.points,
accuracy: q.timesAttempted > 0
? Math.round((q.timesCorrect / q.timesAttempted) * 100)
: 0
})),
stats
},
message: `Category details retrieved successfully`
});
} catch (error) {
console.error('Error fetching category details:', error);
res.status(500).json({
success: false,
message: 'Error fetching category details',
error: error.message
});
}
};
/**
* @desc Create new category (Admin only)
* @route POST /api/categories
* @access Private/Admin
*/
exports.createCategory = async (req, res) => {
try {
const {
name,
slug,
description,
icon,
color,
guestAccessible,
displayOrder
} = req.body;
// Validate required fields
if (!name) {
return res.status(400).json({
success: false,
message: 'Category name is required'
});
}
// Check if category with same name exists
const existingByName = await Category.findOne({
where: { name }
});
if (existingByName) {
return res.status(400).json({
success: false,
message: 'A category with this name already exists'
});
}
// Check if custom slug provided and if it exists
if (slug) {
const existingBySlug = await Category.findOne({
where: { slug }
});
if (existingBySlug) {
return res.status(400).json({
success: false,
message: 'A category with this slug already exists'
});
}
}
// Create category (slug will be auto-generated by model hook if not provided)
const category = await Category.create({
name,
slug,
description: description || null,
icon: icon || null,
color: color || '#3B82F6',
guestAccessible: guestAccessible !== undefined ? guestAccessible : false,
displayOrder: displayOrder || 0,
isActive: true,
questionCount: 0,
quizCount: 0
});
res.status(201).json({
success: true,
data: {
id: category.id,
name: category.name,
slug: category.slug,
description: category.description,
icon: category.icon,
color: category.color,
guestAccessible: category.guestAccessible,
displayOrder: category.displayOrder,
questionCount: category.questionCount,
isActive: category.isActive
},
message: 'Category created successfully'
});
} catch (error) {
console.error('Error creating category:', error);
res.status(500).json({
success: false,
message: 'Error creating category',
error: error.message
});
}
};
/**
* @desc Update category (Admin only)
* @route PUT /api/categories/:id
* @access Private/Admin
*/
exports.updateCategory = async (req, res) => {
try {
const { id } = req.params;
const {
name,
slug,
description,
icon,
color,
guestAccessible,
displayOrder,
isActive
} = req.body;
// Validate ID
if (!id) {
return res.status(400).json({
success: false,
message: 'Category ID is required'
});
}
// Find category
const category = await Category.findByPk(id);
if (!category) {
return res.status(404).json({
success: false,
message: 'Category not found'
});
}
// Check if new name conflicts with existing category
if (name && name !== category.name) {
const existingByName = await Category.findOne({
where: { name }
});
if (existingByName) {
return res.status(400).json({
success: false,
message: 'A category with this name already exists'
});
}
}
// Check if new slug conflicts with existing category
if (slug && slug !== category.slug) {
const existingBySlug = await Category.findOne({
where: { slug }
});
if (existingBySlug) {
return res.status(400).json({
success: false,
message: 'A category with this slug already exists'
});
}
}
// Update category
const updateData = {};
if (name !== undefined) updateData.name = name;
if (slug !== undefined) updateData.slug = slug;
if (description !== undefined) updateData.description = description;
if (icon !== undefined) updateData.icon = icon;
if (color !== undefined) updateData.color = color;
if (guestAccessible !== undefined) updateData.guestAccessible = guestAccessible;
if (displayOrder !== undefined) updateData.displayOrder = displayOrder;
if (isActive !== undefined) updateData.isActive = isActive;
await category.update(updateData);
res.status(200).json({
success: true,
data: {
id: category.id,
name: category.name,
slug: category.slug,
description: category.description,
icon: category.icon,
color: category.color,
guestAccessible: category.guestAccessible,
displayOrder: category.displayOrder,
questionCount: category.questionCount,
isActive: category.isActive
},
message: 'Category updated successfully'
});
} catch (error) {
console.error('Error updating category:', error);
res.status(500).json({
success: false,
message: 'Error updating category',
error: error.message
});
}
};
/**
* @desc Delete category (soft delete - Admin only)
* @route DELETE /api/categories/:id
* @access Private/Admin
*/
exports.deleteCategory = async (req, res) => {
try {
const { id } = req.params;
// Validate ID
if (!id) {
return res.status(400).json({
success: false,
message: 'Category ID is required'
});
}
// Find category
const category = await Category.findByPk(id);
if (!category) {
return res.status(404).json({
success: false,
message: 'Category not found'
});
}
// Check if already deleted
if (!category.isActive) {
return res.status(400).json({
success: false,
message: 'Category is already deleted'
});
}
// Check if category has questions
const questionCount = await Question.count({
where: {
categoryId: id,
isActive: true
}
});
// Soft delete - set isActive to false
await category.update({ isActive: false });
res.status(200).json({
success: true,
data: {
id: category.id,
name: category.name,
questionCount: questionCount
},
message: questionCount > 0
? `Category deleted successfully. ${questionCount} questions are still associated with this category.`
: 'Category deleted successfully'
});
} catch (error) {
console.error('Error deleting category:', error);
res.status(500).json({
success: false,
message: 'Error deleting category',
error: error.message
});
}
};

View File

@@ -0,0 +1,447 @@
const { GuestSession, Category, User, QuizSession, sequelize } = require('../models');
const { v4: uuidv4 } = require('uuid');
const jwt = require('jsonwebtoken');
const config = require('../config/config');
/**
* @desc Start a new guest session
* @route POST /api/guest/start-session
* @access Public
*/
exports.startGuestSession = async (req, res) => {
try {
const { deviceId } = req.body;
// Get IP address
const ipAddress = req.ip || req.connection.remoteAddress || 'unknown';
// Get user agent
const userAgent = req.headers['user-agent'] || 'unknown';
// Generate unique guest_id
const timestamp = Date.now();
const randomString = Math.random().toString(36).substring(2, 10);
const guestId = `guest_${timestamp}_${randomString}`;
// Calculate expiry (24 hours from now by default)
const expiryHours = parseInt(config.guest.sessionExpireHours) || 24;
const expiresAt = new Date(Date.now() + expiryHours * 60 * 60 * 1000);
const maxQuizzes = parseInt(config.guest.maxQuizzes) || 3;
// Generate session token (JWT) before creating session
const sessionToken = jwt.sign(
{ guestId },
config.jwt.secret,
{ expiresIn: `${expiryHours}h` }
);
// Create guest session
const guestSession = await GuestSession.create({
guestId: guestId,
sessionToken: sessionToken,
deviceId: deviceId || null,
ipAddress: ipAddress,
userAgent: userAgent,
expiresAt: expiresAt,
maxQuizzes: maxQuizzes,
quizzesAttempted: 0,
isConverted: false
});
// Get guest-accessible categories
const categories = await Category.findAll({
where: {
isActive: true,
guestAccessible: true
},
attributes: ['id', 'name', 'slug', 'description', 'icon', 'color', 'questionCount'],
order: [['displayOrder', 'ASC'], ['name', 'ASC']]
});
// Return response
res.status(201).json({
success: true,
message: 'Guest session created successfully',
data: {
guestId: guestSession.guestId,
sessionToken,
expiresAt: guestSession.expiresAt,
expiresIn: `${expiryHours} hours`,
restrictions: {
maxQuizzes: guestSession.maxQuizzes,
quizzesRemaining: guestSession.maxQuizzes - guestSession.quizzesAttempted,
features: {
canTakeQuizzes: true,
canViewResults: true,
canBookmarkQuestions: false,
canTrackProgress: false,
canEarnAchievements: false
}
},
availableCategories: categories,
upgradeMessage: 'Sign up to unlock unlimited quizzes, track progress, and earn achievements!'
}
});
} catch (error) {
console.error('Error creating guest session:', error);
res.status(500).json({
success: false,
message: 'Error creating guest session',
error: error.message
});
}
};
/**
* @desc Get guest session details
* @route GET /api/guest/session/:guestId
* @access Public
*/
exports.getGuestSession = async (req, res) => {
try {
const { guestId } = req.params;
// Find guest session
const guestSession = await GuestSession.findOne({
where: { guestId: guestId }
});
if (!guestSession) {
return res.status(404).json({
success: false,
message: 'Guest session not found'
});
}
// Check if session is expired
if (guestSession.isExpired()) {
return res.status(410).json({
success: false,
message: 'Guest session has expired. Please start a new session.',
expired: true
});
}
// Check if session is converted
if (guestSession.isConverted) {
return res.status(410).json({
success: false,
message: 'This guest session has been converted to a user account',
converted: true,
userId: guestSession.convertedUserId
});
}
// Get guest-accessible categories
const categories = await Category.findAll({
where: {
isActive: true,
guestAccessible: true
},
attributes: ['id', 'name', 'slug', 'description', 'icon', 'color', 'questionCount'],
order: [['displayOrder', 'ASC'], ['name', 'ASC']]
});
// Calculate time until expiry
const now = new Date();
const expiresAt = new Date(guestSession.expiresAt);
const hoursRemaining = Math.max(0, Math.floor((expiresAt - now) / (1000 * 60 * 60)));
const minutesRemaining = Math.max(0, Math.floor(((expiresAt - now) % (1000 * 60 * 60)) / (1000 * 60)));
// Return session details
res.status(200).json({
success: true,
data: {
guestId: guestSession.guestId,
expiresAt: guestSession.expiresAt,
expiresIn: `${hoursRemaining}h ${minutesRemaining}m`,
isExpired: false,
restrictions: {
maxQuizzes: guestSession.maxQuizzes,
quizzesAttempted: guestSession.quizzesAttempted,
quizzesRemaining: Math.max(0, guestSession.maxQuizzes - guestSession.quizzesAttempted),
features: {
canTakeQuizzes: guestSession.quizzesAttempted < guestSession.maxQuizzes,
canViewResults: true,
canBookmarkQuestions: false,
canTrackProgress: false,
canEarnAchievements: false
}
},
availableCategories: categories,
upgradeMessage: 'Sign up to unlock unlimited quizzes, track progress, and earn achievements!'
}
});
} catch (error) {
console.error('Error getting guest session:', error);
res.status(500).json({
success: false,
message: 'Error retrieving guest session',
error: error.message
});
}
};
/**
* @desc Check guest quiz limit
* @route GET /api/guest/quiz-limit
* @access Protected (Guest Token Required)
*/
exports.checkQuizLimit = async (req, res) => {
try {
// Guest session is already verified and attached by middleware
const guestSession = req.guestSession;
// Calculate remaining quizzes
const quizzesRemaining = guestSession.maxQuizzes - guestSession.quizzesAttempted;
const hasReachedLimit = quizzesRemaining <= 0;
// Calculate time until reset (session expiry)
const now = new Date();
const expiresAt = new Date(guestSession.expiresAt);
const timeRemainingMs = expiresAt - now;
const hoursRemaining = Math.floor(timeRemainingMs / (1000 * 60 * 60));
const minutesRemaining = Math.floor((timeRemainingMs % (1000 * 60 * 60)) / (1000 * 60));
// Format reset time
let resetTime;
if (hoursRemaining > 0) {
resetTime = `${hoursRemaining}h ${minutesRemaining}m`;
} else {
resetTime = `${minutesRemaining}m`;
}
// Prepare response
const response = {
success: true,
data: {
guestId: guestSession.guestId,
quizLimit: {
maxQuizzes: guestSession.maxQuizzes,
quizzesAttempted: guestSession.quizzesAttempted,
quizzesRemaining: Math.max(0, quizzesRemaining),
hasReachedLimit: hasReachedLimit
},
session: {
expiresAt: guestSession.expiresAt,
timeRemaining: resetTime,
resetTime: resetTime
}
}
};
// Add upgrade prompt if limit reached
if (hasReachedLimit) {
response.data.upgradePrompt = {
message: 'You have reached your quiz limit!',
benefits: [
'Unlimited quizzes',
'Track your progress over time',
'Earn achievements and badges',
'Bookmark questions for review',
'Compete on leaderboards'
],
callToAction: 'Sign up now to continue learning!'
};
response.message = 'Quiz limit reached. Sign up to continue!';
} else {
response.message = `You have ${quizzesRemaining} quiz${quizzesRemaining === 1 ? '' : 'zes'} remaining`;
}
res.status(200).json(response);
} catch (error) {
console.error('Error checking quiz limit:', error);
res.status(500).json({
success: false,
message: 'Error checking quiz limit',
error: error.message
});
}
};
/**
* @desc Convert guest session to registered user account
* @route POST /api/guest/convert
* @access Protected (Guest Token Required)
*/
exports.convertGuestToUser = async (req, res) => {
const transaction = await sequelize.transaction();
try {
const { username, email, password } = req.body;
const guestSession = req.guestSession; // Attached by middleware
// Validate required fields
if (!username || !email || !password) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Username, email, and password are required'
});
}
// Validate username length
if (username.length < 3 || username.length > 50) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Username must be between 3 and 50 characters'
});
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Invalid email format'
});
}
// Validate password strength
if (password.length < 8) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Password must be at least 8 characters long'
});
}
// Check if email already exists
const existingEmail = await User.findOne({
where: { email: email.toLowerCase() },
transaction
});
if (existingEmail) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Email already registered'
});
}
// Check if username already exists
const existingUsername = await User.findOne({
where: { username },
transaction
});
if (existingUsername) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Username already taken'
});
}
// Create new user account (password will be hashed by User model hook)
const user = await User.create({
username,
email: email.toLowerCase(),
password,
role: 'user'
}, { transaction });
// Migrate quiz sessions from guest to user
const migratedSessions = await QuizSession.update(
{
userId: user.id,
guestSessionId: null
},
{
where: { guestSessionId: guestSession.id },
transaction
}
);
// Mark guest session as converted
await guestSession.update({
isConverted: true,
convertedUserId: user.id
}, { transaction });
// Recalculate user stats from migrated sessions
const quizSessions = await QuizSession.findAll({
where: {
userId: user.id,
status: 'completed'
},
transaction
});
let totalQuizzes = quizSessions.length;
let quizzesPassed = 0;
let totalQuestionsAnswered = 0;
let correctAnswers = 0;
quizSessions.forEach(session => {
if (session.isPassed) quizzesPassed++;
totalQuestionsAnswered += session.questionsAnswered || 0;
correctAnswers += session.correctAnswers || 0;
});
// Update user stats
await user.update({
totalQuizzes,
quizzesPassed,
totalQuestionsAnswered,
correctAnswers
}, { transaction });
// Commit transaction
await transaction.commit();
// Generate JWT token for the new user
const token = jwt.sign(
{
userId: user.id,
email: user.email,
username: user.username,
role: user.role
},
config.jwt.secret,
{ expiresIn: config.jwt.expire }
);
// Return response
res.status(201).json({
success: true,
message: 'Guest account successfully converted to registered user',
data: {
user: user.toSafeJSON(),
token,
migration: {
quizzesTransferred: migratedSessions[0],
stats: {
totalQuizzes,
quizzesPassed,
totalQuestionsAnswered,
correctAnswers,
accuracy: totalQuestionsAnswered > 0
? ((correctAnswers / totalQuestionsAnswered) * 100).toFixed(2)
: 0
}
}
}
});
} catch (error) {
if (!transaction.finished) {
await transaction.rollback();
}
console.error('Error converting guest to user:', error);
console.error('Error stack:', error.stack);
res.status(500).json({
success: false,
message: 'Error converting guest account',
error: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
});
}
};

File diff suppressed because it is too large Load Diff