Files
tasks-backend/controllers/guest.controller.js
2025-12-26 23:56:32 +02:00

448 lines
13 KiB
JavaScript

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
});
}
};