← back to Dear Bubbe Admin

backend/models/User.js

270 lines

const mongoose = require('mongoose')

const userSchema = new mongoose.Schema({
  userId: {
    type: String,
    required: true,
    unique: true,
    index: true
  },
  email: {
    type: String,
    lowercase: true,
    index: true
  },
  profile: {
    name: { type: String },
    preferredName: { type: String },
    gender: { type: String },
    age: { type: Number },
    birthYear: { type: String },
    relationshipStatus: { type: String },
    sameGenderPartner: { type: Boolean },
    hasKids: { type: String },
    numberOfKids: { type: Number },
    kidsInfo: [{ type: mongoose.Schema.Types.Mixed }],
    religiousLevel: { type: String },
    education: { type: String },
    school: { type: String },
    degree: { type: String },
    graduationYear: { type: String },
    jobTitle: { type: String },
    company: { type: String },
    industry: { type: String },
    incomeLevel: { type: String },
    yearsAtJob: { type: String },
    city: { type: String },
    state: { type: String },
    country: { type: String },
    onboardingCompleted: { type: Boolean, default: false }
  },
  stats: {
    totalMessages: { type: Number, default: 0 },
    totalSessions: { type: Number, default: 0 },
    firstVisit: { type: Date, default: Date.now },
    lastVisit: { type: Date, default: Date.now },
    averageSessionLength: { type: Number, default: 0 }, // in minutes
    favoriteTopics: [{ type: String }],
    preferredMode: { type: String, default: 'bubbe' }, // bubbe, meshugana, comedian
    voiceEnabled: { type: Boolean, default: false },
    totalVoiceMessages: { type: Number, default: 0 }
  },
  location: {
    country: { type: String },
    state: { type: String },
    city: { type: String },
    coordinates: {
      lat: { type: Number },
      lon: { type: Number }
    },
    timezone: { type: String },
    lastKnownIP: { type: String }
  },
  security: {
    blocked: { type: Boolean, default: false },
    blockReason: { type: String },
    blockedBy: { type: String }, // Admin user ID
    blockedAt: { type: Date },
    suspiciousActivity: { type: Boolean, default: false },
    alertCount: { type: Number, default: 0 },
    lastAlert: { type: Date },
    riskLevel: { 
      type: String, 
      enum: ['low', 'medium', 'high', 'critical'], 
      default: 'low' 
    },
    whitelisted: { type: Boolean, default: false }
  },
  preferences: {
    colorTheme: { type: String, enum: ['warm', 'cool'], default: 'warm' },
    language: { type: String, default: 'en' },
    yiddishTranslations: { type: Boolean, default: true },
    pushNotifications: { type: Boolean, default: false },
    emailUpdates: { type: Boolean, default: false },
    dataSharing: { type: Boolean, default: false }
  },
  deviceInfo: {
    lastUserAgent: { type: String },
    deviceType: { type: String }, // mobile, tablet, desktop
    browser: { type: String },
    os: { type: String }
  },
  notes: [{ 
    note: { type: String },
    addedBy: { type: String }, // Admin user ID
    addedAt: { type: Date, default: Date.now },
    type: { type: String, enum: ['info', 'warning', 'alert'], default: 'info' }
  }]
}, {
  timestamps: true
})

// Indexes
userSchema.index({ 'security.blocked': 1 })
userSchema.index({ 'security.riskLevel': 1 })
userSchema.index({ 'stats.lastVisit': -1 })
userSchema.index({ 'location.country': 1, 'location.state': 1 })

// Virtual for full name
userSchema.virtual('fullName').get(function() {
  return this.profile?.preferredName || this.profile?.name || 'Unknown User'
})

// Method to update user activity
userSchema.methods.updateActivity = function(sessionData) {
  this.stats.lastVisit = new Date()
  this.stats.totalMessages += sessionData.messageCount || 0
  
  if (sessionData.location) {
    this.location = { ...this.location, ...sessionData.location }
  }
  
  if (sessionData.deviceInfo) {
    this.deviceInfo = { ...this.deviceInfo, ...sessionData.deviceInfo }
  }
  
  return this.save()
}

// Method to add security note
userSchema.methods.addNote = function(note, adminId, type = 'info') {
  this.notes.push({
    note,
    addedBy: adminId,
    type
  })
  return this.save()
}

// Method to block user
userSchema.methods.block = function(reason, adminId) {
  this.security.blocked = true
  this.security.blockReason = reason
  this.security.blockedBy = adminId
  this.security.blockedAt = new Date()
  return this.save()
}

// Method to unblock user
userSchema.methods.unblock = function(adminId) {
  this.security.blocked = false
  this.security.blockReason = null
  this.security.blockedBy = null
  this.security.blockedAt = null
  
  this.notes.push({
    note: `User unblocked by admin`,
    addedBy: adminId,
    type: 'info'
  })
  
  return this.save()
}

// Method to increment alert count
userSchema.methods.addAlert = function(alertType, severity) {
  this.security.alertCount++
  this.security.lastAlert = new Date()
  
  // Update risk level based on alert count and severity
  if (this.security.alertCount >= 5 || severity === 'critical') {
    this.security.riskLevel = 'critical'
  } else if (this.security.alertCount >= 3 || severity === 'high') {
    this.security.riskLevel = 'high'
  } else if (this.security.alertCount >= 1 || severity === 'medium') {
    this.security.riskLevel = 'medium'
  }
  
  return this.save()
}

// Static method to find or create user
userSchema.statics.findByIdOrCreate = function(userId, profileData = {}) {
  return this.findOneAndUpdate(
    { userId },
    { 
      $setOnInsert: { 
        userId, 
        profile: profileData,
        stats: {
          totalMessages: 0,
          totalSessions: 0,
          firstVisit: new Date(),
          lastVisit: new Date()
        }
      }
    },
    { 
      upsert: true, 
      new: true, 
      runValidators: true 
    }
  )
}

// Static method to get user statistics
userSchema.statics.getStats = function() {
  return this.aggregate([
    {
      $group: {
        _id: null,
        totalUsers: { $sum: 1 },
        blockedUsers: { $sum: { $cond: ['$security.blocked', 1, 0] } },
        activeUsers: { 
          $sum: { 
            $cond: [
              { $gte: ['$stats.lastVisit', new Date(Date.now() - 24 * 60 * 60 * 1000)] }, 
              1, 
              0
            ] 
          } 
        },
        newUsersToday: {
          $sum: {
            $cond: [
              { $gte: ['$createdAt', new Date(Date.now() - 24 * 60 * 60 * 1000)] },
              1,
              0
            ]
          }
        },
        avgMessagesPerUser: { $avg: '$stats.totalMessages' },
        riskDistribution: {
          low: { $sum: { $cond: [{ $eq: ['$security.riskLevel', 'low'] }, 1, 0] } },
          medium: { $sum: { $cond: [{ $eq: ['$security.riskLevel', 'medium'] }, 1, 0] } },
          high: { $sum: { $cond: [{ $eq: ['$security.riskLevel', 'high'] }, 1, 0] } },
          critical: { $sum: { $cond: [{ $eq: ['$security.riskLevel', 'critical'] }, 1, 0] } }
        }
      }
    }
  ])
}

// Static method to get geographic distribution
userSchema.statics.getGeoDistribution = function() {
  return this.aggregate([
    {
      $group: {
        _id: {
          country: '$location.country',
          state: '$location.state'
        },
        userCount: { $sum: 1 },
        activeUsers: {
          $sum: {
            $cond: [
              { $gte: ['$stats.lastVisit', new Date(Date.now() - 24 * 60 * 60 * 1000)] },
              1,
              0
            ]
          }
        }
      }
    },
    {
      $sort: { userCount: -1 }
    }
  ])
}

module.exports = mongoose.model('User', userSchema)