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.

243 lines (212 loc) 7.38 kB
import mongoose from 'mongoose'; import bcrypt from 'bcryptjs'; import { v4 as uuidv4 } from 'uuid'; const userSchema = new mongoose.Schema({ // Basic Info name: { type: String, required: [true, 'Name is required'], trim: true }, email: { type: String, required: [true, 'Email is required'], unique: true, lowercase: true, match: [/.+@.+\..+/, 'Enter a valid email address'], }, password: { type: String, required: [true, 'Password is required'], minlength: [6, 'Password must be at least 6 characters'] }, // Role & Permissions role: { type: String, default: 'client' }, permissions: [{ type: String, enum: [ 'users.view', 'users.edit', 'users.delete', 'users.verify', 'products.view', 'products.edit', 'products.delete', 'bookings.view', 'bookings.edit', 'bookings.delete', 'galleries.view', 'galleries.edit', 'galleries.delete', 'campaigns.view', 'campaigns.edit', 'campaigns.delete', 'notifications.send', 'emails.send', 'settings.edit', 'logs.view', 'admins.manage' ] }], // Contact Information phone: { type: String, required: [true, 'Phone number is required'], trim: true }, alternatePhone: String, address: { street: String, city: String, state: String, country: { type: String, default: 'Nigeria' }, postalCode: String }, // Account Status isActive: { type: Boolean, default: true }, isEmailVerified: { type: Boolean, default: false }, emailVerificationToken: String, emailVerificationExpires: Date, // Password Reset passwordResetToken: String, passwordResetExpires: Date, // ID Verification for bookings/rentals verification: { nin: { number: String, status: { type: String, enum: ['not_submitted', 'pending', 'verified', 'rejected'], default: 'not_submitted' }, document: String, // file path/URL submittedAt: Date, verifiedAt: Date, rejectionReason: String }, driversLicense: { number: String, status: { type: String, enum: ['not_submitted', 'pending', 'verified', 'rejected'], default: 'not_submitted' }, document: String, // file path/URL submittedAt: Date, verifiedAt: Date, rejectionReason: String, expiryDate: Date } }, // Referrer Information (required for checkout) referrerInfo: { name: String, phone: String, email: String, relationship: String // friend, family, colleague, etc. }, // Preferences preferences: { theme: { type: String, enum: ['light', 'dark', 'auto'], default: 'light' }, notifications: { email: { type: Boolean, default: true }, sms: { type: Boolean, default: false }, push: { type: Boolean, default: true } }, newsletter: { type: Boolean, default: false } }, // Password Reset passwordResetToken: String, passwordResetExpires: Date, // Account Activity lastLogin: Date, loginAttempts: { type: Number, default: 0 }, lockUntil: Date, // UUID for references (32-bit) uuid: { type: String, default: () => uuidv4().replace(/-/g, '').substring(0, 32), unique: true }, // Avatar/Profile Image avatar: String, // Biography for admin users bio: String, // Social Media Links socialMedia: { instagram: String, facebook: String, twitter: String, linkedin: String } }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); // Virtual for full verification status userSchema.virtual('isFullyVerified').get(function () { const hasNin = this.verification.nin.number && this.verification.nin.status === 'verified'; const hasDriversLicense = this.verification.driversLicense.number && this.verification.driversLicense.status === 'verified'; // User is fully verified if they have at least one verified document return hasNin || hasDriversLicense; }); // Virtual for verification status userSchema.virtual('verificationStatus').get(function () { const ninStatus = this.verification.nin.status; const driversLicenseStatus = this.verification.driversLicense.status; if (ninStatus === 'verified' || driversLicenseStatus === 'verified') { return 'verified'; } else if (ninStatus === 'pending' || driversLicenseStatus === 'pending') { return 'pending'; } else if (ninStatus === 'rejected' || driversLicenseStatus === 'rejected') { return 'rejected'; } else { return 'not_submitted'; } }); // Virtual for account lock status userSchema.virtual('isLocked').get(function () { return !!(this.lockUntil && this.lockUntil > Date.now()); }); // Indexes for efficient queries // Note: email and uuid already have unique indexes from schema definition userSchema.index({ role: 1 }); userSchema.index({ 'verification.nin.status': 1 }); userSchema.index({ 'verification.driversLicense.status': 1 }); userSchema.index({ createdAt: -1 }); // Hash password before saving userSchema.pre('save', async function (next) { if (!this.isModified('password')) return next(); this.password = await bcrypt.hash(this.password, 12); next(); }); // Compare password method userSchema.methods.comparePassword = async function (candidatePassword) { return await bcrypt.compare(candidatePassword, this.password); }; // Increment login attempts userSchema.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({ $unset: { lockUntil: 1 }, $set: { loginAttempts: 1 } }); } const updates = { $inc: { loginAttempts: 1 } }; // Lock account after 5 failed attempts for 14 days if (this.loginAttempts + 1 >= 5 && !this.isLocked) { updates.$set = { lockUntil: Date.now() + 14 * 24 * 60 * 60 * 1000 }; // 14 days } return this.updateOne(updates); }; // Reset login attempts userSchema.methods.resetLoginAttempts = function () { return this.updateOne({ $unset: { loginAttempts: 1, lockUntil: 1 } }); }; // Lock account manually userSchema.methods.lockAccount = function (days = 14) { const lockUntil = Date.now() + (days * 24 * 60 * 60 * 1000); return this.updateOne({ $set: { lockUntil: lockUntil } }); }; // Unlock account manually userSchema.methods.unlockAccount = function () { return this.updateOne({ $unset: { lockUntil: 1 } }); }; export default mongoose.model('User', userSchema);