UNPKG

@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
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);