import mongoose, { Document, Schema, Types } from 'mongoose'; import bcrypt from 'bcryptjs'; export interface IUser extends Document { _id: Types.ObjectId; username: string; email: string; password?: string; firstName?: string; lastName?: string; profilePicture?: string; profileImageUrl?: string; googleId?: string; authProvider: 'local' | 'google'; isEmailVerified: boolean; emailVerificationToken?: string; passwordResetToken?: string; passwordResetExpires?: Date; refreshTokens: string[]; roles: Types.ObjectId[]; isActive: boolean; lastLogin?: Date; isDeleted: boolean; deletedAt?: Date; deletedBy?: Types.ObjectId; createdAt: Date; updatedAt: Date; } const UserSchema = new Schema({ username: { type: String, required: true, unique: true, trim: true, minlength: 3, maxlength: 30 }, email: { type: String, required: true, unique: true, lowercase: true, trim: true }, password: { type: String, minlength: 6, select: false // Don't include password in queries by default }, firstName: { type: String, trim: true, maxlength: 50 }, lastName: { type: String, trim: true, maxlength: 50 }, profilePicture: { type: String, trim: true }, profileImageUrl: { type: String, trim: true }, googleId: { type: String, unique: true, sparse: true // Allow null values }, authProvider: { type: String, enum: ['local', 'google'], default: 'local' }, isEmailVerified: { type: Boolean, default: false }, emailVerificationToken: { type: String, select: false }, passwordResetToken: { type: String, select: false }, passwordResetExpires: { type: Date, select: false }, refreshTokens: [{ type: String, select: false }], roles: [{ type: Schema.Types.ObjectId, ref: 'Role', }], isActive: { type: Boolean, default: true }, lastLogin: { type: Date }, isDeleted: { type: Boolean, default: false, index: true }, deletedAt: { type: Date }, deletedBy: { type: Schema.Types.ObjectId, ref: 'User' } }, { timestamps: true, toJSON: { virtuals: true, transform: function(doc: any, ret: any) { ret.id = ret._id; delete ret._id; delete ret.__v; delete ret.password; delete ret.passwordResetToken; delete ret.passwordResetExpires; delete ret.emailVerificationToken; delete ret.refreshTokens; return ret; } }, toObject: { virtuals: true } }); // Indexes for better performance UserSchema.index({ email: 1 }); UserSchema.index({ username: 1 }); UserSchema.index({ googleId: 1 }); UserSchema.index({ passwordResetToken: 1 }); UserSchema.index({ emailVerificationToken: 1 }); // Virtual for full name UserSchema.virtual('fullName').get(function() { if (this.firstName && this.lastName) { return `${this.firstName} ${this.lastName}`; } return this.firstName || this.lastName || this.username; }); // Pre-save middleware to hash password UserSchema.pre('save', async function(next) { if (!this.isModified('password') || !this.password) { return next(); } try { const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_ROUNDS || '12')); this.password = await bcrypt.hash(this.password, salt); next(); } catch (error: any) { next(error); } }); // Instance method to compare password UserSchema.methods.comparePassword = async function(candidatePassword: string): Promise { if (!this.password) return false; return bcrypt.compare(candidatePassword, this.password); }; // Instance method to generate password reset token UserSchema.methods.createPasswordResetToken = function(): string { const resetToken = require('crypto').randomBytes(32).toString('hex'); this.passwordResetToken = require('crypto') .createHash('sha256') .update(resetToken) .digest('hex'); this.passwordResetExpires = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes return resetToken; }; // Instance method to generate email verification token UserSchema.methods.createEmailVerificationToken = function(): string { const verificationToken = require('crypto').randomBytes(32).toString('hex'); this.emailVerificationToken = require('crypto') .createHash('sha256') .update(verificationToken) .digest('hex'); return verificationToken; }; // Static method to find by email with password UserSchema.statics.findByEmailWithPassword = function(email: string) { return this.findOne({ email }).select('+password +refreshTokens'); }; // Static method to find active users UserSchema.statics.findActive = function() { return this.find({ isActive: true }); }; export default mongoose.model('User', UserSchema);