@ideal-photography/shared
Version:
Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.
207 lines (176 loc) • 5.67 kB
JavaScript
import mongoose from 'mongoose';
import { PERMISSIONS, getPermissionsForRole } from '../constants/permissions.js';
const adminInviteSchema = new mongoose.Schema({
code: {
type: String,
required: true,
unique: true
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Admin',
required: true
},
role: {
type: String,
enum: ['admin', 'manager', 'super_admin'],
default: 'admin',
required: true
},
permissions: {
type: [String],
default: function () {
// Auto-assign permissions based on role
return getPermissionsForRole(this.role);
},
validate: {
validator: function (permissions) {
// Validate that all permissions are valid
const allValidPermissions = Object.values(PERMISSIONS).reduce((acc, category) => {
return acc.concat(Object.values(category));
}, []);
return permissions.every(permission => allValidPermissions.includes(permission));
},
message: 'Invalid permission provided'
}
},
expiresAt: {
type: Date,
required: true
},
maxUses: {
type: Number,
default: 1,
min: 1,
max: 100
},
usedCount: {
type: Number,
default: 0
},
used: {
type: Boolean,
default: false
},
usedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Admin'
},
usedAt: {
type: Date
},
// Additional metadata
description: {
type: String,
maxlength: 500
},
// Usage tracking
usageHistory: [{
usedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'Admin' },
usedAt: { type: Date, default: Date.now },
ipAddress: String,
userAgent: String
}],
// Audit trail
createdByInfo: {
username: String,
role: String,
ipAddress: String
}
}, {
timestamps: true
});
// Virtual for remaining uses
adminInviteSchema.virtual('remainingUses').get(function () {
return Math.max(0, this.maxUses - this.usedCount);
});
// Virtual for isExpired
adminInviteSchema.virtual('isExpired').get(function () {
return new Date() > this.expiresAt;
});
// Virtual for isFullyUsed
adminInviteSchema.virtual('isFullyUsed').get(function () {
return this.usedCount >= this.maxUses;
});
// Virtual for isValid
adminInviteSchema.virtual('isValid').get(function () {
return !this.isExpired && !this.isFullyUsed && !this.used;
});
// Pre-save middleware
adminInviteSchema.pre('save', function (next) {
// Auto-assign permissions if role changed and no custom permissions set
if (this.isModified('role') && (!this.permissions || this.permissions.length === 0)) {
this.permissions = getPermissionsForRole(this.role);
}
// Ensure expiresAt is in the future
if (this.expiresAt && this.expiresAt <= new Date()) {
return next(new Error('Invite must expire in the future'));
}
next();
});
// Pre-update middleware for permission changes
adminInviteSchema.pre('findOneAndUpdate', function (next) {
const update = this.getUpdate();
// If permissions are being updated, validate them
if (update.permissions) {
const allValidPermissions = Object.values(PERMISSIONS).reduce((acc, category) => {
return acc.concat(Object.values(category));
}, []);
const isValid = update.permissions.every(permission => allValidPermissions.includes(permission));
if (!isValid) {
return next(new Error('Invalid permission provided'));
}
}
next();
});
// Instance methods
adminInviteSchema.methods.useInvite = function (adminId, ipAddress, userAgent) {
if (!this.isValid) {
throw new Error('Invite is not valid');
}
this.usedCount += 1;
this.used = this.usedCount >= this.maxUses;
if (this.usedCount === 1) {
this.usedBy = adminId;
this.usedAt = new Date();
}
// Add to usage history
this.usageHistory.push({
usedBy: adminId,
usedAt: new Date(),
ipAddress,
userAgent
});
return this.save();
};
adminInviteSchema.methods.canBeUsedBy = function (adminRole) {
// Check if the invite can be used by someone with the given role
// This prevents privilege escalation
if (this.role === 'manager' && adminRole === 'admin') {
return false; // Can't use manager invite to create admin
}
return true;
};
// Static methods
adminInviteSchema.statics.findValidInvites = function () {
return this.find({
expiresAt: { $gt: new Date() },
used: false,
$expr: { $lt: ['$usedCount', '$maxUses'] }
});
};
adminInviteSchema.statics.findByCode = function (code) {
return this.findOne({ code, used: false });
};
adminInviteSchema.statics.findExpiredInvites = function () {
return this.find({ expiresAt: { $lte: new Date() } });
};
adminInviteSchema.statics.findUnusedInvites = function () {
return this.find({ used: false });
};
// Indexes
// Note: expiresAt already has TTL index from schema definition
adminInviteSchema.index({ createdBy: 1 });
adminInviteSchema.index({ role: 1 });
adminInviteSchema.index({ used: 1 });
export default mongoose.model('AdminInvite', adminInviteSchema);