const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); const adminSchema = new mongoose.Schema({ // Basic information username: { type: String, required: true, unique: true, trim: true, lowercase: true }, email: { type: String, required: true, unique: true, trim: true, lowercase: true }, password: { type: String, required: true, minlength: 8 }, // Profile information firstName: { type: String, required: true, trim: true }, lastName: { type: String, required: true, trim: true }, avatar: { type: String, default: '' }, // Role and permissions role: { type: String, enum: ['admin', 'super-admin', 'editor', 'manager'], default: 'admin' }, permissions: [{ type: String, enum: [ 'manage_content', 'manage_rooms', 'manage_bookings', 'manage_users', 'manage_blog', 'manage_gallery', 'manage_settings', 'view_analytics', 'manage_admins' ] }], // Status isActive: { type: Boolean, default: true }, isSuperAdmin: { type: Boolean, default: false }, // Security lastLogin: Date, loginAttempts: { type: Number, default: 0 }, lockUntil: Date, passwordResetToken: String, passwordResetExpires: Date, // Session tracking currentSessions: [{ token: String, createdAt: Date, expiresAt: Date, ipAddress: String, userAgent: String }] }, { timestamps: true, toJSON: { virtuals: true, transform: function(doc, ret) { delete ret.password; delete ret.passwordResetToken; delete ret.currentSessions; return ret; } }, toObject: { virtuals: true, transform: function(doc, ret) { delete ret.password; delete ret.passwordResetToken; delete ret.currentSessions; return ret; } } }); // Indexes adminSchema.index({ username: 1 }); adminSchema.index({ email: 1 }); adminSchema.index({ role: 1 }); // Virtual for full name adminSchema.virtual('fullName').get(function() { return `${this.firstName} ${this.lastName}`; }); // Virtual for account locked status adminSchema.virtual('isLocked').get(function() { return !!(this.lockUntil && this.lockUntil > Date.now()); }); // Pre-save middleware to hash password adminSchema.pre('save', async function(next) { if (!this.isModified('password')) { return next(); } try { const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); next(); } catch (error) { next(error); } }); // Method to compare password adminSchema.methods.comparePassword = async function(candidatePassword) { try { return await bcrypt.compare(candidatePassword, this.password); } catch (error) { throw error; } }; // Method to increment login attempts adminSchema.methods.incLoginAttempts = function() { // If we have a previous lock that has expired, restart at 1 if (this.lockUntil && this.lockUntil < Date.now()) { return this.updateOne({ $set: { loginAttempts: 1 }, $unset: { lockUntil: 1 } }); } // Otherwise increment const updates = { $inc: { loginAttempts: 1 } }; // Lock the account after 5 attempts for 2 hours const needsLock = this.loginAttempts + 1 >= 5 && !this.isLocked; if (needsLock) { updates.$set = { lockUntil: Date.now() + 2 * 60 * 60 * 1000 }; } return this.updateOne(updates); }; // Method to reset login attempts adminSchema.methods.resetLoginAttempts = function() { return this.updateOne({ $set: { loginAttempts: 0 }, $unset: { lockUntil: 1 } }); }; // Static method to find by credentials adminSchema.statics.findByCredentials = async function(username, password) { const admin = await this.findOne({ $or: [{ username }, { email: username }], isActive: true }); if (!admin) { throw new Error('Invalid credentials'); } // Check if account is locked if (admin.isLocked) { throw new Error('Account is temporarily locked. Please try again later.'); } const isMatch = await admin.comparePassword(password); if (!isMatch) { await admin.incLoginAttempts(); throw new Error('Invalid credentials'); } // Reset login attempts on successful login if (admin.loginAttempts > 0) { await admin.resetLoginAttempts(); } // Update last login admin.lastLogin = new Date(); await admin.save(); return admin; }; module.exports = mongoose.model('Admin', adminSchema);