← 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)