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