add changes
This commit is contained in:
333
models/User.js
Normal file
333
models/User.js
Normal file
@@ -0,0 +1,333 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user