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