const bcrypt = require('bcrypt'); const { v4: uuidv4 } = require('uuid'); module.exports = (sequelize, DataTypes) => { const User = sequelize.define('User', { id: { type: DataTypes.CHAR(36), primaryKey: true, defaultValue: () => uuidv4(), allowNull: false, comment: 'UUID primary key' }, username: { type: DataTypes.STRING(50), allowNull: false, unique: { msg: 'Username already exists' }, validate: { notEmpty: { msg: 'Username cannot be empty' }, len: { args: [3, 50], msg: 'Username must be between 3 and 50 characters' }, isAlphanumeric: { msg: 'Username must contain only letters and numbers' } }, comment: 'Unique username' }, email: { type: DataTypes.STRING(100), allowNull: false, unique: { msg: 'Email already exists' }, validate: { notEmpty: { msg: 'Email cannot be empty' }, isEmail: { msg: 'Must be a valid email address' } }, comment: 'User email address' }, password: { type: DataTypes.STRING(255), allowNull: false, validate: { notEmpty: { msg: 'Password cannot be empty' }, len: { args: [6, 255], msg: 'Password must be at least 6 characters' } }, comment: 'Hashed password' }, role: { type: DataTypes.ENUM('admin', 'user'), allowNull: false, defaultValue: 'user', validate: { isIn: { args: [['admin', 'user']], msg: 'Role must be either admin or user' } }, comment: 'User role' }, profileImage: { type: DataTypes.STRING(255), allowNull: true, field: 'profile_image', comment: 'Profile image URL' }, isActive: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, field: 'is_active', comment: 'Account active status' }, // Statistics totalQuizzes: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: 'total_quizzes', validate: { min: 0 }, comment: 'Total number of quizzes taken' }, quizzesPassed: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: 'quizzes_passed', validate: { min: 0 }, comment: 'Number of quizzes passed' }, totalQuestionsAnswered: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: 'total_questions_answered', validate: { min: 0 }, comment: 'Total questions answered' }, correctAnswers: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: 'correct_answers', validate: { min: 0 }, comment: 'Number of correct answers' }, currentStreak: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: 'current_streak', validate: { min: 0 }, comment: 'Current daily streak' }, longestStreak: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: 'longest_streak', validate: { min: 0 }, comment: 'Longest daily streak achieved' }, // Timestamps lastLogin: { type: DataTypes.DATE, allowNull: true, field: 'last_login', comment: 'Last login timestamp' }, lastQuizDate: { type: DataTypes.DATE, allowNull: true, field: 'last_quiz_date', comment: 'Date of last quiz taken' } }, { sequelize, modelName: 'User', tableName: 'users', timestamps: true, underscored: true, indexes: [ { unique: true, fields: ['email'] }, { unique: true, fields: ['username'] }, { fields: ['role'] }, { fields: ['is_active'] }, { fields: ['created_at'] } ] }); // Instance methods User.prototype.comparePassword = async function(candidatePassword) { try { return await bcrypt.compare(candidatePassword, this.password); } catch (error) { throw new Error('Password comparison failed'); } }; User.prototype.toJSON = function() { const values = { ...this.get() }; delete values.password; // Never expose password in JSON return values; }; User.prototype.updateStreak = function() { const today = new Date(); today.setHours(0, 0, 0, 0); if (this.lastQuizDate) { const lastQuiz = new Date(this.lastQuizDate); lastQuiz.setHours(0, 0, 0, 0); const daysDiff = Math.floor((today - lastQuiz) / (1000 * 60 * 60 * 24)); if (daysDiff === 1) { // Consecutive day - increment streak this.currentStreak += 1; if (this.currentStreak > this.longestStreak) { this.longestStreak = this.currentStreak; } } else if (daysDiff > 1) { // Streak broken - reset this.currentStreak = 1; } // If daysDiff === 0, same day - no change to streak } else { // First quiz this.currentStreak = 1; this.longestStreak = 1; } this.lastQuizDate = new Date(); }; User.prototype.calculateAccuracy = function() { if (this.totalQuestionsAnswered === 0) return 0; return ((this.correctAnswers / this.totalQuestionsAnswered) * 100).toFixed(2); }; User.prototype.getPassRate = function() { if (this.totalQuizzes === 0) return 0; return ((this.quizzesPassed / this.totalQuizzes) * 100).toFixed(2); }; User.prototype.toSafeJSON = function() { const values = { ...this.get() }; delete values.password; return values; }; // Class methods User.findByEmail = async function(email) { return await this.findOne({ where: { email, isActive: true } }); }; User.findByUsername = async function(username) { return await this.findOne({ where: { username, isActive: true } }); }; // Hooks User.beforeCreate(async (user) => { // Hash password before creating user if (user.password) { const salt = await bcrypt.genSalt(10); user.password = await bcrypt.hash(user.password, salt); } // Ensure UUID is set if (!user.id) { user.id = uuidv4(); } }); User.beforeUpdate(async (user) => { // Hash password if it was changed if (user.changed('password')) { const salt = await bcrypt.genSalt(10); user.password = await bcrypt.hash(user.password, salt); } }); User.beforeBulkCreate(async (users) => { for (const user of users) { if (user.password) { const salt = await bcrypt.genSalt(10); user.password = await bcrypt.hash(user.password, salt); } if (!user.id) { user.id = uuidv4(); } } }); // Define associations User.associate = function(models) { // User has many quiz sessions (when QuizSession model exists) if (models.QuizSession) { User.hasMany(models.QuizSession, { foreignKey: 'userId', as: 'quizSessions' }); } // User has many bookmarks (when Question model exists) if (models.Question) { User.belongsToMany(models.Question, { through: 'user_bookmarks', foreignKey: 'userId', otherKey: 'questionId', as: 'bookmarkedQuestions' }); // User has created questions (if admin) User.hasMany(models.Question, { foreignKey: 'createdBy', as: 'createdQuestions' }); } // User has many achievements (when Achievement model exists) if (models.Achievement) { User.belongsToMany(models.Achievement, { through: 'user_achievements', foreignKey: 'userId', otherKey: 'achievementId', as: 'achievements' }); } }; return User; };