Files
Tasks/backend/controllers/auth.controller.js
2025-11-11 00:25:50 +02:00

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