add changes

This commit is contained in:
AD2025
2025-11-11 00:25:50 +02:00
commit e3ca132c5e
86 changed files with 22238 additions and 0 deletions

333
backend/models/User.js Normal file
View 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;
};