add changes
This commit is contained in:
447
backend/controllers/guest.controller.js
Normal file
447
backend/controllers/guest.controller.js
Normal 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
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user