← back to Dear Bubbe Admin

backend/models/Session.js

272 lines

const mongoose = require('mongoose')

const sessionSchema = new mongoose.Schema({
  sessionId: {
    type: String,
    required: true,
    unique: true,
    index: true
  },
  userId: {
    type: String,
    required: true,
    index: true
  },
  startTime: {
    type: Date,
    default: Date.now,
    index: true
  },
  endTime: {
    type: Date
  },
  lastActivity: {
    type: Date,
    default: Date.now,
    index: true
  },
  duration: {
    type: Number, // in seconds
    default: 0
  },
  ended: {
    type: Boolean,
    default: false
  },
  userInfo: {
    ip: { type: String, index: true },
    userAgent: { type: String },
    browser: { type: String },
    os: { type: String },
    deviceType: { type: String } // mobile, tablet, desktop
  },
  location: {
    country: { type: String, index: true },
    state: { type: String, index: true },
    city: { type: String },
    coordinates: {
      lat: { type: Number },
      lon: { type: Number }
    },
    timezone: { type: String },
    isp: { type: String }
  },
  activity: {
    messageCount: { type: Number, default: 0 },
    voiceMessagesUsed: { type: Number, default: 0 },
    modesUsed: [{ type: String }], // bubbe, meshugana, comedian
    topicsDiscussed: [{ type: String }],
    featuresUsed: [{ type: String }], // news, weather, voice, etc.
    errorsEncountered: { type: Number, default: 0 },
    averageResponseTime: { type: Number } // in ms
  },
  flags: {
    suspicious: { type: Boolean, default: false },
    blocked: { type: Boolean, default: false },
    alertsTriggered: { type: Number, default: 0 },
    riskLevel: { type: String, enum: ['low', 'medium', 'high', 'critical'], default: 'low' }
  },
  referrer: {
    source: { type: String }, // direct, google, social, etc.
    campaign: { type: String },
    medium: { type: String }
  },
  technicalData: {
    initialLoadTime: { type: Number },
    averageApiResponseTime: { type: Number },
    errors: [{
      timestamp: { type: Date, default: Date.now },
      error: { type: String },
      context: { type: String }
    }],
    performanceMetrics: {
      pageLoadTime: { type: Number },
      apiCallCount: { type: Number },
      totalDataTransferred: { type: Number } // in bytes
    }
  }
}, {
  timestamps: true
})

// Indexes for performance
sessionSchema.index({ startTime: -1 })
sessionSchema.index({ 'userInfo.ip': 1, startTime: -1 })
sessionSchema.index({ ended: 1, lastActivity: -1 })
sessionSchema.index({ userId: 1, startTime: -1 })

// Virtual for session status
sessionSchema.virtual('status').get(function() {
  if (this.ended) return 'ended'
  
  const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000)
  if (this.lastActivity < fiveMinutesAgo) return 'inactive'
  
  return 'active'
})

// Method to update activity
sessionSchema.methods.updateActivity = function(activityData = {}) {
  this.lastActivity = new Date()
  
  if (activityData.messageCount) {
    this.activity.messageCount += activityData.messageCount
  }
  
  if (activityData.mode && !this.activity.modesUsed.includes(activityData.mode)) {
    this.activity.modesUsed.push(activityData.mode)
  }
  
  if (activityData.topics) {
    activityData.topics.forEach(topic => {
      if (!this.activity.topicsDiscussed.includes(topic)) {
        this.activity.topicsDiscussed.push(topic)
      }
    })
  }
  
  if (activityData.feature && !this.activity.featuresUsed.includes(activityData.feature)) {
    this.activity.featuresUsed.push(activityData.feature)
  }
  
  if (activityData.responseTime) {
    if (this.activity.averageResponseTime) {
      this.activity.averageResponseTime = (this.activity.averageResponseTime + activityData.responseTime) / 2
    } else {
      this.activity.averageResponseTime = activityData.responseTime
    }
  }
  
  return this.save()
}

// Method to end session
sessionSchema.methods.endSession = function() {
  this.ended = true
  this.endTime = new Date()
  this.duration = Math.round((this.endTime - this.startTime) / 1000) // in seconds
  return this.save()
}

// Method to add error
sessionSchema.methods.addError = function(error, context = '') {
  this.activity.errorsEncountered++
  this.technicalData.errors.push({
    error,
    context,
    timestamp: new Date()
  })
  return this.save()
}

// Method to flag as suspicious
sessionSchema.methods.flagSuspicious = function(reason) {
  this.flags.suspicious = true
  this.flags.riskLevel = 'high'
  return this.save()
}

// Method to increment alert count
sessionSchema.methods.addAlert = function(alertType, severity) {
  this.flags.alertsTriggered++
  
  // Update risk level based on alerts and severity
  if (this.flags.alertsTriggered >= 3 || severity === 'critical') {
    this.flags.riskLevel = 'critical'
  } else if (this.flags.alertsTriggered >= 2 || severity === 'high') {
    this.flags.riskLevel = 'high'
  } else if (this.flags.alertsTriggered >= 1 || severity === 'medium') {
    this.flags.riskLevel = 'medium'
  }
  
  return this.save()
}

// Static method to get active sessions
sessionSchema.statics.getActiveSessions = function(minutesThreshold = 5) {
  const threshold = new Date(Date.now() - minutesThreshold * 60 * 1000)
  return this.find({
    ended: false,
    lastActivity: { $gte: threshold }
  }).lean()
}

// Static method to get session statistics
sessionSchema.statics.getStats = function(timeframe = '24h') {
  const now = new Date()
  let startTime
  
  switch (timeframe) {
    case '1h':
      startTime = new Date(now - 60 * 60 * 1000)
      break
    case '24h':
      startTime = new Date(now - 24 * 60 * 60 * 1000)
      break
    case '7d':
      startTime = new Date(now - 7 * 24 * 60 * 60 * 1000)
      break
    case '30d':
      startTime = new Date(now - 30 * 24 * 60 * 60 * 1000)
      break
    default:
      startTime = new Date(now - 24 * 60 * 60 * 1000)
  }
  
  return this.aggregate([
    {
      $match: {
        startTime: { $gte: startTime }
      }
    },
    {
      $group: {
        _id: null,
        totalSessions: { $sum: 1 },
        uniqueUsers: { $addToSet: '$userId' },
        uniqueIPs: { $addToSet: '$userInfo.ip' },
        totalMessages: { $sum: '$activity.messageCount' },
        averageDuration: { $avg: '$duration' },
        flaggedSessions: { $sum: { $cond: ['$flags.suspicious', 1, 0] } },
        totalErrors: { $sum: '$activity.errorsEncountered' },
        averageResponseTime: { $avg: '$activity.averageResponseTime' }
      }
    },
    {
      $addFields: {
        uniqueUserCount: { $size: '$uniqueUsers' },
        uniqueIPCount: { $size: '$uniqueIPs' }
      }
    }
  ])
}

// Static method to find or create session
sessionSchema.statics.findOrCreate = function(sessionId, userId, sessionData = {}) {
  return this.findOneAndUpdate(
    { sessionId },
    {
      $setOnInsert: {
        sessionId,
        userId,
        startTime: new Date(),
        ...sessionData
      }
    },
    {
      upsert: true,
      new: true,
      runValidators: true
    }
  )
}

// Static method to cleanup old sessions
sessionSchema.statics.cleanup = function(daysToKeep = 30) {
  const cutoff = new Date(Date.now() - daysToKeep * 24 * 60 * 60 * 1000)
  return this.deleteMany({
    startTime: { $lt: cutoff }
  })
}

module.exports = mongoose.model('Session', sessionSchema)