strapi-plugin-magic-sessionmanager 4.2.3 → 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.
Files changed (61) hide show
  1. package/dist/server/index.js +1 -1
  2. package/dist/server/index.mjs +1 -1
  3. package/package.json +1 -3
  4. package/admin/jsconfig.json +0 -10
  5. package/admin/src/components/Initializer.jsx +0 -11
  6. package/admin/src/components/LicenseGuard.jsx +0 -591
  7. package/admin/src/components/OnlineUsersWidget.jsx +0 -212
  8. package/admin/src/components/PluginIcon.jsx +0 -8
  9. package/admin/src/components/SessionDetailModal.jsx +0 -449
  10. package/admin/src/components/SessionInfoCard.jsx +0 -151
  11. package/admin/src/components/SessionInfoPanel.jsx +0 -385
  12. package/admin/src/components/index.jsx +0 -5
  13. package/admin/src/hooks/useLicense.js +0 -103
  14. package/admin/src/index.js +0 -149
  15. package/admin/src/pages/ActiveSessions.jsx +0 -12
  16. package/admin/src/pages/Analytics.jsx +0 -735
  17. package/admin/src/pages/App.jsx +0 -12
  18. package/admin/src/pages/HomePage.jsx +0 -1212
  19. package/admin/src/pages/License.jsx +0 -603
  20. package/admin/src/pages/Settings.jsx +0 -1646
  21. package/admin/src/pages/SettingsNew.jsx +0 -1204
  22. package/admin/src/pages/UpgradePage.jsx +0 -448
  23. package/admin/src/pages/index.jsx +0 -3
  24. package/admin/src/pluginId.js +0 -4
  25. package/admin/src/translations/de.json +0 -299
  26. package/admin/src/translations/en.json +0 -299
  27. package/admin/src/translations/es.json +0 -287
  28. package/admin/src/translations/fr.json +0 -287
  29. package/admin/src/translations/pt.json +0 -287
  30. package/admin/src/utils/getTranslation.js +0 -5
  31. package/admin/src/utils/index.js +0 -2
  32. package/admin/src/utils/parseUserAgent.js +0 -79
  33. package/admin/src/utils/theme.js +0 -85
  34. package/server/jsconfig.json +0 -10
  35. package/server/src/bootstrap.js +0 -492
  36. package/server/src/config/index.js +0 -23
  37. package/server/src/content-types/index.js +0 -9
  38. package/server/src/content-types/session/schema.json +0 -84
  39. package/server/src/controllers/controller.js +0 -11
  40. package/server/src/controllers/index.js +0 -11
  41. package/server/src/controllers/license.js +0 -266
  42. package/server/src/controllers/session.js +0 -433
  43. package/server/src/controllers/settings.js +0 -122
  44. package/server/src/destroy.js +0 -22
  45. package/server/src/index.js +0 -23
  46. package/server/src/middlewares/index.js +0 -5
  47. package/server/src/middlewares/last-seen.js +0 -62
  48. package/server/src/policies/index.js +0 -3
  49. package/server/src/register.js +0 -36
  50. package/server/src/routes/admin.js +0 -149
  51. package/server/src/routes/content-api.js +0 -60
  52. package/server/src/routes/index.js +0 -9
  53. package/server/src/services/geolocation.js +0 -182
  54. package/server/src/services/index.js +0 -13
  55. package/server/src/services/license-guard.js +0 -316
  56. package/server/src/services/notifications.js +0 -319
  57. package/server/src/services/service.js +0 -7
  58. package/server/src/services/session.js +0 -393
  59. package/server/src/utils/encryption.js +0 -121
  60. package/server/src/utils/getClientIp.js +0 -118
  61. 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
-