289 lines
7.3 KiB
JavaScript
289 lines
7.3 KiB
JavaScript
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
|
|
});
|
|
}
|
|
};
|