Initial commit: CMS backend for Old Vine Hotel
- Complete Express.js API server - MongoDB integration with Mongoose - Admin authentication and authorization - Room management (CRUD operations) - Booking management system - Guest management - Payment processing (Stripe integration) - Content management (pages, blog, gallery) - Media upload and management - Integration services (Booking.com, Expedia, Opera PMS, Trip.com) - Email notifications - Comprehensive logging and error handling
This commit is contained in:
212
models/Admin.js
Normal file
212
models/Admin.js
Normal file
@@ -0,0 +1,212 @@
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user