strapi-plugin-magic-sessionmanager 4.2.4 → 4.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/index.js +1 -1
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -3
- package/admin/jsconfig.json +0 -10
- package/admin/src/components/Initializer.jsx +0 -11
- package/admin/src/components/LicenseGuard.jsx +0 -591
- package/admin/src/components/OnlineUsersWidget.jsx +0 -212
- package/admin/src/components/PluginIcon.jsx +0 -8
- package/admin/src/components/SessionDetailModal.jsx +0 -449
- package/admin/src/components/SessionInfoCard.jsx +0 -151
- package/admin/src/components/SessionInfoPanel.jsx +0 -385
- package/admin/src/components/index.jsx +0 -5
- package/admin/src/hooks/useLicense.js +0 -103
- package/admin/src/index.js +0 -149
- package/admin/src/pages/ActiveSessions.jsx +0 -12
- package/admin/src/pages/Analytics.jsx +0 -735
- package/admin/src/pages/App.jsx +0 -12
- package/admin/src/pages/HomePage.jsx +0 -1212
- package/admin/src/pages/License.jsx +0 -603
- package/admin/src/pages/Settings.jsx +0 -1646
- package/admin/src/pages/SettingsNew.jsx +0 -1204
- package/admin/src/pages/UpgradePage.jsx +0 -448
- package/admin/src/pages/index.jsx +0 -3
- package/admin/src/pluginId.js +0 -4
- package/admin/src/translations/de.json +0 -299
- package/admin/src/translations/en.json +0 -299
- package/admin/src/translations/es.json +0 -287
- package/admin/src/translations/fr.json +0 -287
- package/admin/src/translations/pt.json +0 -287
- package/admin/src/utils/getTranslation.js +0 -5
- package/admin/src/utils/index.js +0 -2
- package/admin/src/utils/parseUserAgent.js +0 -79
- package/admin/src/utils/theme.js +0 -85
- package/server/jsconfig.json +0 -10
- package/server/src/bootstrap.js +0 -492
- package/server/src/config/index.js +0 -23
- package/server/src/content-types/index.js +0 -9
- package/server/src/content-types/session/schema.json +0 -84
- package/server/src/controllers/controller.js +0 -11
- package/server/src/controllers/index.js +0 -11
- package/server/src/controllers/license.js +0 -266
- package/server/src/controllers/session.js +0 -433
- package/server/src/controllers/settings.js +0 -122
- package/server/src/destroy.js +0 -22
- package/server/src/index.js +0 -23
- package/server/src/middlewares/index.js +0 -5
- package/server/src/middlewares/last-seen.js +0 -62
- package/server/src/policies/index.js +0 -3
- package/server/src/register.js +0 -36
- package/server/src/routes/admin.js +0 -149
- package/server/src/routes/content-api.js +0 -60
- package/server/src/routes/index.js +0 -9
- package/server/src/services/geolocation.js +0 -182
- package/server/src/services/index.js +0 -13
- package/server/src/services/license-guard.js +0 -316
- package/server/src/services/notifications.js +0 -319
- package/server/src/services/service.js +0 -7
- package/server/src/services/session.js +0 -393
- package/server/src/utils/encryption.js +0 -121
- package/server/src/utils/getClientIp.js +0 -118
- package/server/src/utils/logger.js +0 -84
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { encryptToken, decryptToken, generateSessionId } = require('../utils/encryption');
|
|
4
|
-
const { createLogger } = require('../utils/logger');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Session Service
|
|
8
|
-
* Uses plugin::magic-sessionmanager.session content type with relation to users
|
|
9
|
-
* All session tracking happens in the Session collection
|
|
10
|
-
*
|
|
11
|
-
* SECURITY: JWT tokens are encrypted before storing in database using AES-256-GCM
|
|
12
|
-
*
|
|
13
|
-
* [SUCCESS] Migrated to strapi.documents() API (Strapi v5 Best Practice)
|
|
14
|
-
*
|
|
15
|
-
* TODO: For production multi-instance deployments, use Redis for:
|
|
16
|
-
* - Session store instead of DB
|
|
17
|
-
* - Rate limiting locks
|
|
18
|
-
* - Distributed session state
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const SESSION_UID = 'plugin::magic-sessionmanager.session';
|
|
22
|
-
const USER_UID = 'plugin::users-permissions.user';
|
|
23
|
-
|
|
24
|
-
module.exports = ({ strapi }) => {
|
|
25
|
-
const log = createLogger(strapi);
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
/**
|
|
29
|
-
* Create a new session record
|
|
30
|
-
* @param {Object} params - { userId, ip, userAgent, token, refreshToken }
|
|
31
|
-
* @returns {Promise<Object>} Created session
|
|
32
|
-
*/
|
|
33
|
-
async createSession({ userId, ip = 'unknown', userAgent = 'unknown', token, refreshToken }) {
|
|
34
|
-
try {
|
|
35
|
-
const now = new Date();
|
|
36
|
-
|
|
37
|
-
// Generate unique session ID
|
|
38
|
-
const sessionId = generateSessionId(userId);
|
|
39
|
-
|
|
40
|
-
// Encrypt JWT tokens before storing (both access and refresh)
|
|
41
|
-
const encryptedToken = token ? encryptToken(token) : null;
|
|
42
|
-
const encryptedRefreshToken = refreshToken ? encryptToken(refreshToken) : null;
|
|
43
|
-
|
|
44
|
-
// Using Document Service API (Strapi v5)
|
|
45
|
-
const session = await strapi.documents(SESSION_UID).create({
|
|
46
|
-
data: {
|
|
47
|
-
user: userId, // userId should be documentId (string)
|
|
48
|
-
ipAddress: ip.substring(0, 45),
|
|
49
|
-
userAgent: userAgent.substring(0, 500),
|
|
50
|
-
loginTime: now,
|
|
51
|
-
lastActive: now,
|
|
52
|
-
isActive: true,
|
|
53
|
-
token: encryptedToken, // [SUCCESS] Encrypted Access Token
|
|
54
|
-
refreshToken: encryptedRefreshToken, // [SUCCESS] Encrypted Refresh Token
|
|
55
|
-
sessionId: sessionId, // [SUCCESS] Unique identifier
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
log.info(`[SUCCESS] Session ${session.documentId} (${sessionId}) created for user ${userId}`);
|
|
60
|
-
|
|
61
|
-
return session;
|
|
62
|
-
} catch (err) {
|
|
63
|
-
log.error('Error creating session:', err);
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Terminate a session or all sessions for a user
|
|
70
|
-
* Supports both numeric id (legacy) and documentId (Strapi v5)
|
|
71
|
-
* @param {Object} params - { sessionId | userId }
|
|
72
|
-
* @returns {Promise<void>}
|
|
73
|
-
*/
|
|
74
|
-
async terminateSession({ sessionId, userId }) {
|
|
75
|
-
try {
|
|
76
|
-
const now = new Date();
|
|
77
|
-
|
|
78
|
-
if (sessionId) {
|
|
79
|
-
// Using Document Service API (Strapi v5)
|
|
80
|
-
await strapi.documents(SESSION_UID).update({
|
|
81
|
-
documentId: sessionId,
|
|
82
|
-
data: {
|
|
83
|
-
isActive: false,
|
|
84
|
-
logoutTime: now,
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
log.info(`Session ${sessionId} terminated`);
|
|
89
|
-
} else if (userId) {
|
|
90
|
-
// Strapi v5: If numeric id provided, look up documentId first
|
|
91
|
-
let userDocumentId = userId;
|
|
92
|
-
if (!isNaN(userId)) {
|
|
93
|
-
const user = await strapi.entityService.findOne(USER_UID, parseInt(userId, 10));
|
|
94
|
-
if (user) {
|
|
95
|
-
userDocumentId = user.documentId;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Find all active sessions for user - use Deep Filtering (Strapi v5)
|
|
100
|
-
const activeSessions = await strapi.documents(SESSION_UID).findMany({
|
|
101
|
-
filters: {
|
|
102
|
-
user: { documentId: userDocumentId }, // Deep filtering syntax
|
|
103
|
-
isActive: true,
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Terminate all active sessions
|
|
108
|
-
for (const session of activeSessions) {
|
|
109
|
-
await strapi.documents(SESSION_UID).update({
|
|
110
|
-
documentId: session.documentId,
|
|
111
|
-
data: {
|
|
112
|
-
isActive: false,
|
|
113
|
-
logoutTime: now,
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
log.info(`All sessions terminated for user ${userDocumentId}`);
|
|
119
|
-
}
|
|
120
|
-
} catch (err) {
|
|
121
|
-
log.error('Error terminating session:', err);
|
|
122
|
-
throw err;
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Get ALL sessions (active + inactive) with accurate online status
|
|
128
|
-
* @returns {Promise<Array>} All sessions with enhanced data
|
|
129
|
-
*/
|
|
130
|
-
async getAllSessions() {
|
|
131
|
-
try {
|
|
132
|
-
const sessions = await strapi.documents(SESSION_UID).findMany( {
|
|
133
|
-
populate: { user: { fields: ['id', 'email', 'username'] } },
|
|
134
|
-
sort: { loginTime: 'desc' },
|
|
135
|
-
limit: 1000, // Reasonable limit
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Get inactivity timeout from config (default: 15 minutes)
|
|
139
|
-
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
140
|
-
const inactivityTimeout = config.inactivityTimeout || 15 * 60 * 1000; // 15 min in ms
|
|
141
|
-
|
|
142
|
-
// Enhance sessions with accurate online status
|
|
143
|
-
const now = new Date();
|
|
144
|
-
const enhancedSessions = sessions.map(session => {
|
|
145
|
-
const lastActiveTime = session.lastActive ? new Date(session.lastActive) : new Date(session.loginTime);
|
|
146
|
-
const timeSinceActive = now - lastActiveTime;
|
|
147
|
-
|
|
148
|
-
// Session is "truly active" if within timeout window AND isActive is true
|
|
149
|
-
const isTrulyActive = session.isActive && (timeSinceActive < inactivityTimeout);
|
|
150
|
-
|
|
151
|
-
// Remove sensitive token field for security
|
|
152
|
-
const { token, ...sessionWithoutToken } = session;
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
...sessionWithoutToken,
|
|
156
|
-
isTrulyActive,
|
|
157
|
-
minutesSinceActive: Math.floor(timeSinceActive / 1000 / 60),
|
|
158
|
-
};
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
return enhancedSessions;
|
|
162
|
-
} catch (err) {
|
|
163
|
-
log.error('Error getting all sessions:', err);
|
|
164
|
-
throw err;
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Get all active sessions with accurate online status
|
|
170
|
-
* @returns {Promise<Array>} Active sessions with user data and online status
|
|
171
|
-
*/
|
|
172
|
-
async getActiveSessions() {
|
|
173
|
-
try {
|
|
174
|
-
const sessions = await strapi.documents(SESSION_UID).findMany( {
|
|
175
|
-
filters: { isActive: true },
|
|
176
|
-
populate: { user: { fields: ['id', 'email', 'username'] } },
|
|
177
|
-
sort: { loginTime: 'desc' },
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Get inactivity timeout from config (default: 15 minutes)
|
|
181
|
-
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
182
|
-
const inactivityTimeout = config.inactivityTimeout || 15 * 60 * 1000; // 15 min in ms
|
|
183
|
-
|
|
184
|
-
// Enhance sessions with accurate online status
|
|
185
|
-
const now = new Date();
|
|
186
|
-
const enhancedSessions = sessions.map(session => {
|
|
187
|
-
const lastActiveTime = session.lastActive ? new Date(session.lastActive) : new Date(session.loginTime);
|
|
188
|
-
const timeSinceActive = now - lastActiveTime;
|
|
189
|
-
|
|
190
|
-
// Session is "truly active" if within timeout window
|
|
191
|
-
const isTrulyActive = timeSinceActive < inactivityTimeout;
|
|
192
|
-
|
|
193
|
-
// Remove sensitive token field for security
|
|
194
|
-
const { token, ...sessionWithoutToken } = session;
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
...sessionWithoutToken,
|
|
198
|
-
isTrulyActive,
|
|
199
|
-
minutesSinceActive: Math.floor(timeSinceActive / 1000 / 60),
|
|
200
|
-
};
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// Only return truly active sessions
|
|
204
|
-
return enhancedSessions.filter(s => s.isTrulyActive);
|
|
205
|
-
} catch (err) {
|
|
206
|
-
log.error('Error getting active sessions:', err);
|
|
207
|
-
throw err;
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Get all sessions for a specific user
|
|
213
|
-
* Supports both numeric id (legacy) and documentId (Strapi v5)
|
|
214
|
-
* @param {string|number} userId - User documentId or numeric id
|
|
215
|
-
* @returns {Promise<Array>} User's sessions with accurate online status
|
|
216
|
-
*/
|
|
217
|
-
async getUserSessions(userId) {
|
|
218
|
-
try {
|
|
219
|
-
// Strapi v5: If numeric id provided, look up documentId first
|
|
220
|
-
let userDocumentId = userId;
|
|
221
|
-
if (!isNaN(userId)) {
|
|
222
|
-
const user = await strapi.entityService.findOne(USER_UID, parseInt(userId, 10));
|
|
223
|
-
if (user) {
|
|
224
|
-
userDocumentId = user.documentId;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const sessions = await strapi.documents(SESSION_UID).findMany( {
|
|
229
|
-
filters: { user: { documentId: userDocumentId } },
|
|
230
|
-
sort: { loginTime: 'desc' },
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// Get inactivity timeout from config (default: 15 minutes)
|
|
234
|
-
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
235
|
-
const inactivityTimeout = config.inactivityTimeout || 15 * 60 * 1000; // 15 min in ms
|
|
236
|
-
|
|
237
|
-
// Enhance sessions with accurate online status
|
|
238
|
-
const now = new Date();
|
|
239
|
-
const enhancedSessions = sessions.map(session => {
|
|
240
|
-
const lastActiveTime = session.lastActive ? new Date(session.lastActive) : new Date(session.loginTime);
|
|
241
|
-
const timeSinceActive = now - lastActiveTime;
|
|
242
|
-
|
|
243
|
-
// Session is "truly active" if:
|
|
244
|
-
// 1. isActive = true AND
|
|
245
|
-
// 2. lastActive is within timeout window
|
|
246
|
-
const isTrulyActive = session.isActive && (timeSinceActive < inactivityTimeout);
|
|
247
|
-
|
|
248
|
-
// Remove sensitive token field for security
|
|
249
|
-
const { token, ...sessionWithoutToken } = session;
|
|
250
|
-
|
|
251
|
-
return {
|
|
252
|
-
...sessionWithoutToken,
|
|
253
|
-
isTrulyActive,
|
|
254
|
-
minutesSinceActive: Math.floor(timeSinceActive / 1000 / 60),
|
|
255
|
-
};
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
return enhancedSessions;
|
|
259
|
-
} catch (err) {
|
|
260
|
-
log.error('Error getting user sessions:', err);
|
|
261
|
-
throw err;
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Update lastActive timestamp on session (rate-limited to avoid DB noise)
|
|
267
|
-
* @param {Object} params - { userId, sessionId }
|
|
268
|
-
* @returns {Promise<void>}
|
|
269
|
-
*/
|
|
270
|
-
async touch({ userId, sessionId }) {
|
|
271
|
-
try {
|
|
272
|
-
const now = new Date();
|
|
273
|
-
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
274
|
-
const rateLimit = config.lastSeenRateLimit || 30000;
|
|
275
|
-
|
|
276
|
-
// Update session lastActive only
|
|
277
|
-
if (sessionId) {
|
|
278
|
-
const session = await strapi.documents(SESSION_UID).findOne({ documentId: sessionId });
|
|
279
|
-
|
|
280
|
-
if (session && session.lastActive) {
|
|
281
|
-
const lastActiveTime = new Date(session.lastActive).getTime();
|
|
282
|
-
const currentTime = now.getTime();
|
|
283
|
-
|
|
284
|
-
if (currentTime - lastActiveTime > rateLimit) {
|
|
285
|
-
await strapi.documents(SESSION_UID).update({ documentId: sessionId,
|
|
286
|
-
data: { lastActive: now },
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
} else if (session) {
|
|
290
|
-
// First time or null
|
|
291
|
-
await strapi.documents(SESSION_UID).update({ documentId: sessionId,
|
|
292
|
-
data: { lastActive: now },
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
} catch (err) {
|
|
297
|
-
log.debug('Error touching session:', err.message);
|
|
298
|
-
// Don't throw - this is a non-critical operation
|
|
299
|
-
}
|
|
300
|
-
},
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Cleanup inactive sessions - set isActive to false for sessions older than inactivityTimeout
|
|
304
|
-
* Should be called on bootstrap to clean up stale sessions
|
|
305
|
-
*/
|
|
306
|
-
async cleanupInactiveSessions() {
|
|
307
|
-
try {
|
|
308
|
-
// Get inactivity timeout from config (default: 15 minutes)
|
|
309
|
-
const config = strapi.config.get('plugin::magic-sessionmanager') || {};
|
|
310
|
-
const inactivityTimeout = config.inactivityTimeout || 15 * 60 * 1000; // 15 min in ms
|
|
311
|
-
|
|
312
|
-
// Calculate cutoff time
|
|
313
|
-
const now = new Date();
|
|
314
|
-
const cutoffTime = new Date(now.getTime() - inactivityTimeout);
|
|
315
|
-
|
|
316
|
-
log.info(`[CLEANUP] Cleaning up sessions inactive since before ${cutoffTime.toISOString()}`);
|
|
317
|
-
|
|
318
|
-
// Find all active sessions
|
|
319
|
-
const activeSessions = await strapi.documents(SESSION_UID).findMany({
|
|
320
|
-
filters: { isActive: true },
|
|
321
|
-
fields: ['lastActive', 'loginTime'],
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
// Deactivate old sessions
|
|
325
|
-
let deactivatedCount = 0;
|
|
326
|
-
for (const session of activeSessions) {
|
|
327
|
-
const lastActiveTime = session.lastActive ? new Date(session.lastActive) : new Date(session.loginTime);
|
|
328
|
-
|
|
329
|
-
if (lastActiveTime < cutoffTime) {
|
|
330
|
-
await strapi.documents(SESSION_UID).update({
|
|
331
|
-
documentId: session.documentId,
|
|
332
|
-
data: { isActive: false },
|
|
333
|
-
});
|
|
334
|
-
deactivatedCount++;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
log.info(`[SUCCESS] Cleanup complete: ${deactivatedCount} sessions deactivated`);
|
|
339
|
-
return deactivatedCount;
|
|
340
|
-
} catch (err) {
|
|
341
|
-
log.error('Error cleaning up inactive sessions:', err);
|
|
342
|
-
throw err;
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Delete a single session from database
|
|
348
|
-
* WARNING: This permanently deletes the record!
|
|
349
|
-
* @param {number} sessionId - Session ID to delete
|
|
350
|
-
* @returns {Promise<boolean>} Success status
|
|
351
|
-
*/
|
|
352
|
-
async deleteSession(sessionId) {
|
|
353
|
-
try {
|
|
354
|
-
await strapi.documents(SESSION_UID).delete({ documentId: sessionId });
|
|
355
|
-
log.info(`[DELETE] Session ${sessionId} permanently deleted`);
|
|
356
|
-
return true;
|
|
357
|
-
} catch (err) {
|
|
358
|
-
log.error('Error deleting session:', err);
|
|
359
|
-
throw err;
|
|
360
|
-
}
|
|
361
|
-
},
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Delete all inactive sessions from database
|
|
365
|
-
* WARNING: This permanently deletes records!
|
|
366
|
-
* @returns {Promise<number>} Number of deleted sessions
|
|
367
|
-
*/
|
|
368
|
-
async deleteInactiveSessions() {
|
|
369
|
-
try {
|
|
370
|
-
log.info('[DELETE] Deleting all inactive sessions...');
|
|
371
|
-
|
|
372
|
-
// Find all inactive sessions (documentId is always included automatically)
|
|
373
|
-
const inactiveSessions = await strapi.documents(SESSION_UID).findMany({
|
|
374
|
-
filters: { isActive: false },
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
let deletedCount = 0;
|
|
378
|
-
|
|
379
|
-
// Delete each inactive session
|
|
380
|
-
for (const session of inactiveSessions) {
|
|
381
|
-
await strapi.documents(SESSION_UID).delete({ documentId: session.documentId });
|
|
382
|
-
deletedCount++;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
log.info(`[SUCCESS] Deleted ${deletedCount} inactive sessions`);
|
|
386
|
-
return deletedCount;
|
|
387
|
-
} catch (err) {
|
|
388
|
-
log.error('Error deleting inactive sessions:', err);
|
|
389
|
-
throw err;
|
|
390
|
-
}
|
|
391
|
-
},
|
|
392
|
-
};
|
|
393
|
-
};
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* JWT Encryption Utility
|
|
7
|
-
* Uses AES-256-GCM for secure token storage
|
|
8
|
-
*
|
|
9
|
-
* SECURITY: Tokens are encrypted before storing in database
|
|
10
|
-
* This prevents exposure if database is compromised
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
14
|
-
const IV_LENGTH = 16;
|
|
15
|
-
const AUTH_TAG_LENGTH = 16;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Get encryption key from environment or generate one
|
|
19
|
-
* IMPORTANT: Set SESSION_ENCRYPTION_KEY in .env for production!
|
|
20
|
-
*/
|
|
21
|
-
function getEncryptionKey() {
|
|
22
|
-
const envKey = process.env.SESSION_ENCRYPTION_KEY;
|
|
23
|
-
|
|
24
|
-
if (envKey) {
|
|
25
|
-
// Use provided key (must be 32 bytes for AES-256)
|
|
26
|
-
const key = crypto.createHash('sha256').update(envKey).digest();
|
|
27
|
-
return key;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Fallback: Use Strapi's app keys (not recommended for production)
|
|
31
|
-
const strapiKeys = process.env.APP_KEYS || process.env.API_TOKEN_SALT || 'default-insecure-key';
|
|
32
|
-
const key = crypto.createHash('sha256').update(strapiKeys).digest();
|
|
33
|
-
|
|
34
|
-
console.warn('[magic-sessionmanager/encryption] [WARNING] No SESSION_ENCRYPTION_KEY found. Using fallback (not recommended for production).');
|
|
35
|
-
console.warn('[magic-sessionmanager/encryption] Set SESSION_ENCRYPTION_KEY in .env for better security.');
|
|
36
|
-
|
|
37
|
-
return key;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Encrypt JWT token before storing in database
|
|
42
|
-
* @param {string} token - JWT token to encrypt
|
|
43
|
-
* @returns {string} Encrypted token with IV and auth tag
|
|
44
|
-
*/
|
|
45
|
-
function encryptToken(token) {
|
|
46
|
-
if (!token) return null;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const key = getEncryptionKey();
|
|
50
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
51
|
-
|
|
52
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
53
|
-
|
|
54
|
-
let encrypted = cipher.update(token, 'utf8', 'hex');
|
|
55
|
-
encrypted += cipher.final('hex');
|
|
56
|
-
|
|
57
|
-
const authTag = cipher.getAuthTag();
|
|
58
|
-
|
|
59
|
-
// Format: iv:authTag:encryptedData
|
|
60
|
-
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
61
|
-
} catch (err) {
|
|
62
|
-
console.error('[magic-sessionmanager/encryption] Encryption failed:', err);
|
|
63
|
-
throw new Error('Failed to encrypt token');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Decrypt JWT token from database
|
|
69
|
-
* @param {string} encryptedToken - Encrypted token from database
|
|
70
|
-
* @returns {string} Decrypted JWT token
|
|
71
|
-
*/
|
|
72
|
-
function decryptToken(encryptedToken) {
|
|
73
|
-
if (!encryptedToken) return null;
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const key = getEncryptionKey();
|
|
77
|
-
|
|
78
|
-
// Parse: iv:authTag:encryptedData
|
|
79
|
-
const parts = encryptedToken.split(':');
|
|
80
|
-
|
|
81
|
-
if (parts.length !== 3) {
|
|
82
|
-
throw new Error('Invalid encrypted token format');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const iv = Buffer.from(parts[0], 'hex');
|
|
86
|
-
const authTag = Buffer.from(parts[1], 'hex');
|
|
87
|
-
const encrypted = parts[2];
|
|
88
|
-
|
|
89
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
90
|
-
decipher.setAuthTag(authTag);
|
|
91
|
-
|
|
92
|
-
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
93
|
-
decrypted += decipher.final('utf8');
|
|
94
|
-
|
|
95
|
-
return decrypted;
|
|
96
|
-
} catch (err) {
|
|
97
|
-
console.error('[magic-sessionmanager/encryption] Decryption failed:', err);
|
|
98
|
-
return null; // Return null if decryption fails (invalid/tampered token)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Generate unique session ID
|
|
104
|
-
* Combines timestamp + random bytes + user ID for uniqueness
|
|
105
|
-
* @param {number} userId - User ID
|
|
106
|
-
* @returns {string} Unique session identifier
|
|
107
|
-
*/
|
|
108
|
-
function generateSessionId(userId) {
|
|
109
|
-
const timestamp = Date.now().toString(36);
|
|
110
|
-
const randomBytes = crypto.randomBytes(8).toString('hex');
|
|
111
|
-
const userHash = crypto.createHash('sha256').update(userId.toString()).digest('hex').substring(0, 8);
|
|
112
|
-
|
|
113
|
-
return `sess_${timestamp}_${userHash}_${randomBytes}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
module.exports = {
|
|
117
|
-
encryptToken,
|
|
118
|
-
decryptToken,
|
|
119
|
-
generateSessionId,
|
|
120
|
-
};
|
|
121
|
-
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extract real client IP address from request
|
|
3
|
-
* Handles proxies, load balancers, and various header formats
|
|
4
|
-
*
|
|
5
|
-
* Priority order:
|
|
6
|
-
* 1. CF-Connecting-IP (Cloudflare)
|
|
7
|
-
* 2. True-Client-IP (Akamai, Cloudflare)
|
|
8
|
-
* 3. X-Real-IP (nginx)
|
|
9
|
-
* 4. X-Forwarded-For (standard proxy header)
|
|
10
|
-
* 5. X-Client-IP
|
|
11
|
-
* 6. X-Cluster-Client-IP
|
|
12
|
-
* 7. ctx.request.ip (Koa default)
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const getClientIp = (ctx) => {
|
|
16
|
-
try {
|
|
17
|
-
const headers = ctx.request.headers || ctx.request.header || {};
|
|
18
|
-
|
|
19
|
-
// 1. Cloudflare
|
|
20
|
-
if (headers['cf-connecting-ip']) {
|
|
21
|
-
return cleanIp(headers['cf-connecting-ip']);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// 2. True-Client-IP (Akamai, Cloudflare Enterprise)
|
|
25
|
-
if (headers['true-client-ip']) {
|
|
26
|
-
return cleanIp(headers['true-client-ip']);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 3. X-Real-IP (nginx proxy_pass)
|
|
30
|
-
if (headers['x-real-ip']) {
|
|
31
|
-
return cleanIp(headers['x-real-ip']);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 4. X-Forwarded-For (most common)
|
|
35
|
-
// Format: "client, proxy1, proxy2"
|
|
36
|
-
// We want the FIRST IP (the actual client)
|
|
37
|
-
if (headers['x-forwarded-for']) {
|
|
38
|
-
const forwardedIps = headers['x-forwarded-for'].split(',');
|
|
39
|
-
const clientIp = forwardedIps[0].trim();
|
|
40
|
-
if (clientIp && !isPrivateIp(clientIp)) {
|
|
41
|
-
return cleanIp(clientIp);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 5. X-Client-IP
|
|
46
|
-
if (headers['x-client-ip']) {
|
|
47
|
-
return cleanIp(headers['x-client-ip']);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 6. X-Cluster-Client-IP (Rackspace, Riverbed)
|
|
51
|
-
if (headers['x-cluster-client-ip']) {
|
|
52
|
-
return cleanIp(headers['x-cluster-client-ip']);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 7. Forwarded (RFC 7239)
|
|
56
|
-
if (headers['forwarded']) {
|
|
57
|
-
const match = headers['forwarded'].match(/for=([^;,\s]+)/);
|
|
58
|
-
if (match && match[1]) {
|
|
59
|
-
return cleanIp(match[1].replace(/"/g, ''));
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// 8. Fallback to Koa's ctx.request.ip
|
|
64
|
-
if (ctx.request.ip) {
|
|
65
|
-
return cleanIp(ctx.request.ip);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 9. Last resort
|
|
69
|
-
return 'unknown';
|
|
70
|
-
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error('[getClientIp] Error extracting IP:', error);
|
|
73
|
-
return 'unknown';
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Clean IP address (remove IPv6 prefix, port, etc.)
|
|
79
|
-
*/
|
|
80
|
-
const cleanIp = (ip) => {
|
|
81
|
-
if (!ip) return 'unknown';
|
|
82
|
-
|
|
83
|
-
// Remove port if present
|
|
84
|
-
ip = ip.split(':')[0];
|
|
85
|
-
|
|
86
|
-
// Remove IPv6 prefix (::ffff:)
|
|
87
|
-
if (ip.startsWith('::ffff:')) {
|
|
88
|
-
ip = ip.substring(7);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Trim whitespace
|
|
92
|
-
ip = ip.trim();
|
|
93
|
-
|
|
94
|
-
return ip || 'unknown';
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Check if IP is private/local
|
|
99
|
-
*/
|
|
100
|
-
const isPrivateIp = (ip) => {
|
|
101
|
-
if (!ip) return true;
|
|
102
|
-
|
|
103
|
-
// Private IP ranges
|
|
104
|
-
if (ip === '127.0.0.1' || ip === 'localhost' || ip === '::1') return true;
|
|
105
|
-
if (ip.startsWith('192.168.')) return true;
|
|
106
|
-
if (ip.startsWith('10.')) return true;
|
|
107
|
-
if (ip.startsWith('172.')) {
|
|
108
|
-
const second = parseInt(ip.split('.')[1]);
|
|
109
|
-
if (second >= 16 && second <= 31) return true;
|
|
110
|
-
}
|
|
111
|
-
if (ip.startsWith('fc00:') || ip.startsWith('fd00:')) return true;
|
|
112
|
-
if (ip.startsWith('fe80:')) return true;
|
|
113
|
-
|
|
114
|
-
return false;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
module.exports = getClientIp;
|
|
118
|
-
|