File size: 5,627 Bytes
c904036
 
 
 
 
 
 
 
 
 
 
 
 
6502ceb
c904036
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6502ceb
c904036
 
 
 
 
 
6502ceb
c904036
 
 
 
 
 
 
 
6502ceb
c904036
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
const mongoose = require('mongoose');
const crypto = require('crypto');

/**
 * Session Schema
 * Stores active user sessions with refresh tokens for session management
 * Supports: Login History, Active Sessions, Token Refresh
 */
const sessionSchema = new mongoose.Schema({
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true,
    index: false
  },
  
  // Refresh token (hashed for security)
  refreshToken: {
    type: String,
    required: true,
    unique: true
  },
  
  // Device & Browser Info
  device: {
    type: String,
    default: 'Unknown Device'
  },
  browser: {
    type: String,
    default: 'Unknown Browser'
  },
  os: {
    type: String,
    default: 'Unknown OS'
  },
  userAgent: {
    type: String,
    default: ''
  },
  
  // Location Info (from IP)
  ipAddress: {
    type: String,
    default: ''
  },
  location: {
    city: { type: String, default: '' },
    region: { type: String, default: '' },
    country: { type: String, default: '' },
    formatted: { type: String, default: 'Unknown Location' }
  },
  
  // Session Status
  isActive: {
    type: Boolean,
    default: true,
    index: false
  },
  
  // Timestamps
  createdAt: {
    type: Date,
    default: Date.now,
    index: false
  },
  lastActivity: {
    type: Date,
    default: Date.now
  },
  expiresAt: {
    type: Date,
    required: true,
    index: false
  },
  
  // Revocation info
  revokedAt: {
    type: Date
  },
  revokedBy: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  revokeReason: {
    type: String,
    enum: ['logout', 'password_change', 'security', 'admin_action', 'expired', 'manual'],
    default: null
  }
}, {
  timestamps: true
});

// Indexes for efficient queries
sessionSchema.index({ userId: 1, isActive: 1 });
sessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); // TTL index - auto-delete expired sessions

// Instance method: Check if session is valid
sessionSchema.methods.isValid = function() {
  return this.isActive && this.expiresAt > new Date();
};

// Instance method: Revoke session
sessionSchema.methods.revoke = async function(reason = 'manual', revokedBy = null) {
  this.isActive = false;
  this.revokedAt = new Date();
  this.revokedBy = revokedBy;
  this.revokeReason = reason;
  return this.save();
};

// Instance method: Update last activity
sessionSchema.methods.touch = async function() {
  this.lastActivity = new Date();
  return this.save();
};

// Static method: Create new session with refresh token
// Handles duplicate sessions for same device - replaces existing session
sessionSchema.statics.createSession = async function(userId, deviceInfo, expiresInDays = 7) {
  const refreshToken = crypto.randomBytes(64).toString('hex');
  const hashedToken = crypto.createHash('sha256').update(refreshToken).digest('hex');
  
  // Check for existing active session from same device
  // Match by device + browser + OS combination to identify same device
  const existingSession = await this.findOne({
    userId,
    isActive: true,
    device: deviceInfo.device || 'Unknown Device',
    browser: deviceInfo.browser || 'Unknown Browser',
    os: deviceInfo.os || 'Unknown OS'
  });
  
  // If same device already has a session, revoke it first
  if (existingSession) {
    await existingSession.revoke('manual');
  }
  
  const session = await this.create({
    userId,
    refreshToken: hashedToken,
    device: deviceInfo.device || 'Unknown Device',
    browser: deviceInfo.browser || 'Unknown Browser',
    os: deviceInfo.os || 'Unknown OS',
    userAgent: deviceInfo.userAgent || '',
    ipAddress: deviceInfo.ipAddress || '',
    location: deviceInfo.location || { formatted: 'Unknown Location' },
    expiresAt: new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000)
  });
  
  // Return unhashed token for client, session for reference
  return { session, refreshToken };
};

// Static method: Find session by refresh token
sessionSchema.statics.findByRefreshToken = async function(refreshToken) {
  const hashedToken = crypto.createHash('sha256').update(refreshToken).digest('hex');
  return this.findOne({ 
    refreshToken: hashedToken, 
    isActive: true,
    expiresAt: { $gt: new Date() }
  }).populate('userId', 'name email roles isVerified profilePhoto');
};

// Static method: Get active sessions for user
sessionSchema.statics.getActiveSessions = async function(userId) {
  return this.find({ 
    userId, 
    isActive: true,
    expiresAt: { $gt: new Date() }
  }).sort({ lastActivity: -1 });
};

// Static method: Revoke all sessions for user (except current)
sessionSchema.statics.revokeAllExcept = async function(userId, currentSessionId, reason = 'security') {
  return this.updateMany(
    { 
      userId, 
      _id: { $ne: currentSessionId },
      isActive: true 
    },
    { 
      isActive: false, 
      revokedAt: new Date(),
      revokeReason: reason 
    }
  );
};

// Static method: Revoke all sessions for user
sessionSchema.statics.revokeAll = async function(userId, reason = 'logout') {
  return this.updateMany(
    { userId, isActive: true },
    { 
      isActive: false, 
      revokedAt: new Date(),
      revokeReason: reason 
    }
  );
};

// Static method: Cleanup expired sessions (run periodically)
sessionSchema.statics.cleanupExpired = async function() {
  return this.deleteMany({
    $or: [
      { expiresAt: { $lt: new Date() } },
      { isActive: false, revokedAt: { $lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } } // Delete revoked sessions after 30 days
    ]
  });
};

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