enterprise-logging-system 1.0.55 → 1.1.1
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/backend/controllers/LogController.d.ts +1 -3
- package/dist/backend/controllers/LogController.d.ts.map +1 -1
- package/dist/backend/controllers/LogController.js +25 -55
- package/dist/backend/controllers/LogController.js.map +1 -1
- package/dist/backend/repositories/AccessLogRepository.d.ts +15 -20
- package/dist/backend/repositories/AccessLogRepository.d.ts.map +1 -1
- package/dist/backend/repositories/AccessLogRepository.js +267 -288
- package/dist/backend/repositories/AccessLogRepository.js.map +1 -1
- package/dist/backend/repositories/BaseRepository.d.ts +2 -0
- package/dist/backend/repositories/BaseRepository.d.ts.map +1 -1
- package/dist/backend/repositories/BaseRepository.js +10 -5
- package/dist/backend/repositories/BaseRepository.js.map +1 -1
- package/dist/backend/routes.d.ts.map +1 -1
- package/dist/backend/routes.js +0 -22
- package/dist/backend/routes.js.map +1 -1
- package/dist/backend/services/LoggingService.d.ts +115 -12
- package/dist/backend/services/LoggingService.d.ts.map +1 -1
- package/dist/backend/services/LoggingService.js +126 -19
- package/dist/backend/services/LoggingService.js.map +1 -1
- package/package.json +1 -1
|
@@ -12,320 +12,182 @@ class AccessLogRepository extends BaseRepository_1.BaseRepository {
|
|
|
12
12
|
/**
|
|
13
13
|
* Ensure unique index exists for userId + tenantId + date
|
|
14
14
|
* This prevents race conditions from creating duplicate entries
|
|
15
|
-
* NOTE: This index is only for aggregated daily summaries, not individual activities
|
|
16
15
|
*/
|
|
17
16
|
async ensureUniqueIndex() {
|
|
18
17
|
try {
|
|
19
|
-
// We DON'T want a unique index on individual activities
|
|
20
|
-
// Individual activities should allow multiple entries per day
|
|
21
|
-
// Only aggregated summaries should be unique
|
|
22
|
-
// Drop the unique index if it exists
|
|
23
|
-
try {
|
|
24
|
-
await this.collection.dropIndex('userId_1_tenantId_1_indexes.session_date_1');
|
|
25
|
-
console.log('✅ Dropped unique index - allowing multiple activities per day');
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
// Index might not exist, that's fine
|
|
29
|
-
}
|
|
30
|
-
// Create a non-unique index for better query performance
|
|
31
18
|
await this.collection.createIndex({
|
|
32
19
|
userId: 1,
|
|
33
20
|
tenantId: 1,
|
|
34
|
-
|
|
21
|
+
'indexes.session_date': 1
|
|
35
22
|
}, {
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
unique: true,
|
|
24
|
+
partialFilterExpression: {
|
|
25
|
+
'indexes.session_date': { $exists: true }
|
|
26
|
+
},
|
|
27
|
+
background: true
|
|
38
28
|
});
|
|
39
|
-
console.log('✅ Created non-unique index for queries');
|
|
40
29
|
}
|
|
41
30
|
catch (error) {
|
|
42
|
-
|
|
31
|
+
// Index might already exist, ignore error
|
|
32
|
+
console.log('Unique index creation skipped (may already exist)');
|
|
43
33
|
}
|
|
44
34
|
}
|
|
45
35
|
/**
|
|
46
|
-
*
|
|
36
|
+
* Log a page view. Implements upsert logic: if an access log entry already exists
|
|
37
|
+
* for this userId on the same day, it updates that record instead of creating
|
|
38
|
+
* a new one. This ensures one access log entry per user per day.
|
|
39
|
+
* Uses atomic operations to prevent race conditions.
|
|
47
40
|
*/
|
|
48
|
-
async
|
|
49
|
-
|
|
50
|
-
const
|
|
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
|
-
date: {
|
|
86
|
-
$dateToString: {
|
|
87
|
-
format: '%Y-%m-%d',
|
|
88
|
-
date: '$timestamp'
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
// User info (take first occurrence)
|
|
93
|
-
username: { $first: '$username' },
|
|
94
|
-
employeeId: { $first: '$employeeId' },
|
|
95
|
-
userRole: { $first: '$userRole' },
|
|
96
|
-
tenantId: { $first: '$tenantId' },
|
|
97
|
-
// Session info
|
|
98
|
-
sessionId: { $first: '$sessionId' },
|
|
99
|
-
ipAddress: { $first: '$ipAddress' },
|
|
100
|
-
browser: { $first: '$browser' },
|
|
101
|
-
// Time tracking
|
|
102
|
-
startTime: { $min: '$timestamp' },
|
|
103
|
-
endTime: { $max: '$timestamp' },
|
|
104
|
-
// Activity counts
|
|
105
|
-
totalActivities: { $sum: 1 },
|
|
106
|
-
totalPageViews: {
|
|
107
|
-
$sum: { $cond: [{ $eq: ['$type', 'PAGE_OPEN'] }, 1, 0] }
|
|
108
|
-
},
|
|
109
|
-
totalActions: {
|
|
110
|
-
$sum: { $cond: [{ $eq: ['$type', 'ACTION_PERFORMED'] }, 1, 0] }
|
|
111
|
-
},
|
|
112
|
-
// Collect all activities for detailed view
|
|
113
|
-
activities: {
|
|
114
|
-
$push: {
|
|
115
|
-
_id: '$_id',
|
|
116
|
-
timestamp: '$timestamp',
|
|
117
|
-
type: '$type',
|
|
118
|
-
activityType: '$activityType',
|
|
119
|
-
activityName: '$activityName',
|
|
120
|
-
pageTitle: '$pageTitle',
|
|
121
|
-
pageUrl: '$pageUrl',
|
|
122
|
-
actionType: '$actionType',
|
|
123
|
-
actionTarget: '$actionTarget',
|
|
124
|
-
actionData: '$actionData',
|
|
125
|
-
activeDuration: '$activeDuration'
|
|
126
|
-
}
|
|
127
|
-
}
|
|
41
|
+
async logPageView(log) {
|
|
42
|
+
// Get start and end of the day for the current timestamp
|
|
43
|
+
const currentDate = new Date(log.timestamp);
|
|
44
|
+
// Create a unique daily key for this user (user_date format)
|
|
45
|
+
const dateKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}`;
|
|
46
|
+
const dailyKey = `${log.userId}_${dateKey}`;
|
|
47
|
+
// Use atomic findOneAndUpdate with upsert to prevent race conditions
|
|
48
|
+
// Filter by user_date index to ensure one entry per user per day
|
|
49
|
+
const filter = {
|
|
50
|
+
userId: log.userId,
|
|
51
|
+
tenantId: log.tenantId,
|
|
52
|
+
'indexes.user_date': dailyKey
|
|
53
|
+
};
|
|
54
|
+
const update = {
|
|
55
|
+
$set: {
|
|
56
|
+
// Always update these fields to latest values
|
|
57
|
+
timestamp: log.timestamp,
|
|
58
|
+
sessionId: log.sessionId,
|
|
59
|
+
pageId: log.pageId,
|
|
60
|
+
pageTitle: log.pageTitle,
|
|
61
|
+
pageUrl: log.pageUrl,
|
|
62
|
+
pageRoute: log.pageRoute,
|
|
63
|
+
previousPage: log.previousPage,
|
|
64
|
+
endTime: log.timestamp,
|
|
65
|
+
browser: log.browser,
|
|
66
|
+
geoLocation: log.geoLocation,
|
|
67
|
+
network: log.network,
|
|
68
|
+
ipAddress: log.ipAddress,
|
|
69
|
+
userAgent: log.userAgent,
|
|
70
|
+
activityType: log.activityType,
|
|
71
|
+
activityName: log.activityName,
|
|
72
|
+
...(log.metadata && { metadata: log.metadata }),
|
|
73
|
+
indexes: {
|
|
74
|
+
tenant_user_date: `${log.tenantId}_${log.userId}_${dateKey}`,
|
|
75
|
+
session_time: `${log.sessionId}_${log.activityType}`,
|
|
76
|
+
page_time: `${log.pageId}_${log.timestamp.getTime()}`,
|
|
77
|
+
session_date: dailyKey
|
|
128
78
|
}
|
|
129
79
|
},
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
},
|
|
146
|
-
totalActivities: 1,
|
|
147
|
-
totalPageViews: 1,
|
|
148
|
-
totalActions: 1,
|
|
149
|
-
activities: 1,
|
|
150
|
-
// For display compatibility
|
|
151
|
-
timestamp: '$startTime',
|
|
152
|
-
activityType: 'DAILY_SUMMARY',
|
|
153
|
-
activityName: {
|
|
154
|
-
$concat: [
|
|
155
|
-
{ $toString: '$totalActivities' },
|
|
156
|
-
' activities (',
|
|
157
|
-
{ $toString: '$totalPageViews' },
|
|
158
|
-
' page views, ',
|
|
159
|
-
{ $toString: '$totalActions' },
|
|
160
|
-
' actions)'
|
|
161
|
-
]
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
];
|
|
166
|
-
// Add sorting
|
|
167
|
-
const sortField = sortBy === 'date' ? 'startTime' : sortBy;
|
|
168
|
-
const sortDirection = sortOrder === 'asc' ? 1 : -1;
|
|
169
|
-
pipeline.push({ $sort: { [sortField]: sortDirection } });
|
|
170
|
-
console.log('🔧 Aggregation Pipeline:', JSON.stringify(pipeline, null, 2));
|
|
171
|
-
// Get total count
|
|
172
|
-
const countPipeline = [...pipeline, { $count: 'total' }];
|
|
173
|
-
const countResult = await this.collection.aggregate(countPipeline).toArray();
|
|
174
|
-
const total = countResult[0]?.total || 0;
|
|
175
|
-
console.log('📈 Total after aggregation:', total);
|
|
176
|
-
// Add pagination
|
|
177
|
-
pipeline.push({ $skip: skip }, { $limit: limit });
|
|
178
|
-
const data = await this.collection.aggregate(pipeline).toArray();
|
|
179
|
-
console.log('📋 Final result count:', data.length);
|
|
180
|
-
console.log('📋 Sample data:', data.slice(0, 2));
|
|
181
|
-
return {
|
|
182
|
-
data,
|
|
183
|
-
total,
|
|
184
|
-
page,
|
|
185
|
-
limit,
|
|
186
|
-
totalPages: Math.ceil(total / limit),
|
|
187
|
-
hasNext: page < Math.ceil(total / limit),
|
|
188
|
-
hasPrev: page > 1
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Get detailed activities for a specific user and date
|
|
193
|
-
*/
|
|
194
|
-
async getUserDayActivities(userId, date, tenantId) {
|
|
195
|
-
const startOfDay = new Date(date);
|
|
196
|
-
startOfDay.setHours(0, 0, 0, 0);
|
|
197
|
-
const endOfDay = new Date(date);
|
|
198
|
-
endOfDay.setHours(23, 59, 59, 999);
|
|
199
|
-
const filter = {
|
|
200
|
-
userId,
|
|
201
|
-
timestamp: {
|
|
202
|
-
$gte: startOfDay,
|
|
203
|
-
$lte: endOfDay
|
|
80
|
+
$setOnInsert: {
|
|
81
|
+
// Only set these on first insert
|
|
82
|
+
_id: this.generateId(),
|
|
83
|
+
type: 'PAGE_OPEN',
|
|
84
|
+
tenantId: log.tenantId,
|
|
85
|
+
userId: log.userId,
|
|
86
|
+
userRole: log.userRole,
|
|
87
|
+
username: log.username,
|
|
88
|
+
employeeId: log.employeeId,
|
|
89
|
+
eventTime: log.eventTime,
|
|
90
|
+
startTime: log.startTime || log.timestamp
|
|
91
|
+
},
|
|
92
|
+
$inc: {
|
|
93
|
+
// Increment activity count atomically (starts at 1 on first insert)
|
|
94
|
+
activityCount: 1
|
|
204
95
|
}
|
|
205
96
|
};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
* Get ALL individual activities for a user (every click, every page view)
|
|
216
|
-
* This returns detailed activity log for comprehensive user tracking
|
|
217
|
-
*/
|
|
218
|
-
async getUserAllActivities(userId, startDate, endDate, tenantId) {
|
|
219
|
-
const filter = { userId };
|
|
220
|
-
if (startDate || endDate) {
|
|
221
|
-
filter.timestamp = {};
|
|
222
|
-
if (startDate) {
|
|
223
|
-
const start = new Date(startDate);
|
|
224
|
-
start.setHours(0, 0, 0, 0);
|
|
225
|
-
filter.timestamp.$gte = start;
|
|
226
|
-
}
|
|
227
|
-
if (endDate) {
|
|
228
|
-
const end = new Date(endDate);
|
|
229
|
-
end.setHours(23, 59, 59, 999);
|
|
230
|
-
filter.timestamp.$lte = end;
|
|
231
|
-
}
|
|
97
|
+
const result = await this.collection.findOneAndUpdate(filter, update, {
|
|
98
|
+
upsert: true,
|
|
99
|
+
returnDocument: 'after'
|
|
100
|
+
});
|
|
101
|
+
if (result.value) {
|
|
102
|
+
// Calculate duration
|
|
103
|
+
const startTime = new Date(result.value.startTime || result.value.timestamp);
|
|
104
|
+
result.value.activeDuration = log.timestamp.getTime() - startTime.getTime();
|
|
105
|
+
return result.value;
|
|
232
106
|
}
|
|
233
|
-
if
|
|
234
|
-
|
|
107
|
+
// Fallback: if upsert somehow failed, try to find the existing record
|
|
108
|
+
const existingLog = await this.findOne(filter);
|
|
109
|
+
if (existingLog) {
|
|
110
|
+
return existingLog;
|
|
235
111
|
}
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
.find(filter)
|
|
239
|
-
.sort({ timestamp: -1 }) // Most recent first
|
|
240
|
-
.toArray();
|
|
112
|
+
// Last resort: This should never happen with upsert, but handle it
|
|
113
|
+
throw new Error('Failed to create or update access log entry');
|
|
241
114
|
}
|
|
242
115
|
/**
|
|
243
|
-
* Log a
|
|
244
|
-
*
|
|
116
|
+
* Log a user action. Implements upsert logic: if an access log entry already exists
|
|
117
|
+
* for this userId on the same day, it updates that record instead of creating
|
|
118
|
+
* a new one. This ensures one access log entry per user per day.
|
|
119
|
+
* Uses atomic operations to prevent race conditions.
|
|
245
120
|
*/
|
|
246
|
-
async
|
|
121
|
+
async logAction(log) {
|
|
122
|
+
// Get start and end of the day for the current timestamp
|
|
247
123
|
const currentDate = new Date(log.timestamp);
|
|
124
|
+
// Create a unique daily key for this user
|
|
248
125
|
const dateKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}`;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
type: 'PAGE_OPEN',
|
|
253
|
-
tenantId: log.tenantId,
|
|
126
|
+
const dailyKey = `${log.userId}_${dateKey}`;
|
|
127
|
+
// Use atomic findOneAndUpdate with upsert - same filter as logPageView
|
|
128
|
+
const filter = {
|
|
254
129
|
userId: log.userId,
|
|
255
|
-
userRole: log.userRole,
|
|
256
|
-
username: log.username,
|
|
257
|
-
employeeId: log.employeeId,
|
|
258
|
-
sessionId: log.sessionId,
|
|
259
|
-
ipAddress: log.ipAddress,
|
|
260
|
-
userAgent: log.userAgent,
|
|
261
|
-
timestamp: log.timestamp,
|
|
262
|
-
eventTime: log.eventTime,
|
|
263
|
-
startTime: log.startTime || log.timestamp,
|
|
264
|
-
activityType: log.activityType,
|
|
265
|
-
activityName: log.activityName,
|
|
266
|
-
pageId: log.pageId,
|
|
267
|
-
pageTitle: log.pageTitle,
|
|
268
|
-
pageUrl: log.pageUrl,
|
|
269
|
-
pageRoute: log.pageRoute,
|
|
270
|
-
previousPage: log.previousPage,
|
|
271
|
-
browser: log.browser,
|
|
272
|
-
geoLocation: log.geoLocation,
|
|
273
|
-
network: log.network,
|
|
274
|
-
metadata: log.metadata,
|
|
275
|
-
indexes: {
|
|
276
|
-
tenant_user_date: `${log.tenantId}_${log.userId}_${dateKey}`,
|
|
277
|
-
session_time: `${log.sessionId}_${log.activityType}`,
|
|
278
|
-
page_time: `${log.pageId}_${log.timestamp.getTime()}`,
|
|
279
|
-
session_date: `${log.userId}_${dateKey}`
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
await this.collection.insertOne(individualLog);
|
|
283
|
-
return individualLog;
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Log a user action as individual entry (not aggregated)
|
|
287
|
-
* This creates a separate log entry for each action to track detailed user activity
|
|
288
|
-
*/
|
|
289
|
-
async logIndividualAction(log) {
|
|
290
|
-
const currentDate = new Date(log.timestamp);
|
|
291
|
-
const dateKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}`;
|
|
292
|
-
// Create individual entry with unique ID - NO UPSERT, just insert
|
|
293
|
-
const individualLog = {
|
|
294
|
-
_id: this.generateId(),
|
|
295
|
-
type: 'ACTION_PERFORMED',
|
|
296
130
|
tenantId: log.tenantId,
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
131
|
+
'indexes.session_date': dailyKey
|
|
132
|
+
};
|
|
133
|
+
const update = {
|
|
134
|
+
$set: {
|
|
135
|
+
// Always update these fields to latest values
|
|
136
|
+
timestamp: log.timestamp,
|
|
137
|
+
sessionId: log.sessionId,
|
|
138
|
+
activityType: log.activityType,
|
|
139
|
+
activityName: log.activityName,
|
|
140
|
+
actionType: log.actionType,
|
|
141
|
+
actionTarget: log.actionTarget,
|
|
142
|
+
actionData: log.actionData,
|
|
143
|
+
endTime: log.timestamp,
|
|
144
|
+
browser: log.browser,
|
|
145
|
+
ipAddress: log.ipAddress,
|
|
146
|
+
userAgent: log.userAgent,
|
|
147
|
+
...(log.pageId && { pageId: log.pageId }),
|
|
148
|
+
...(log.metadata && { metadata: log.metadata }),
|
|
149
|
+
indexes: {
|
|
150
|
+
tenant_user_date: `${log.tenantId}_${log.userId}_${dateKey}`,
|
|
151
|
+
session_time: `${log.sessionId}_${log.activityType}`,
|
|
152
|
+
page_time: `${log.pageId}_${log.timestamp.getTime()}`,
|
|
153
|
+
session_date: dailyKey
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
$setOnInsert: {
|
|
157
|
+
// Only set these on first insert
|
|
158
|
+
_id: this.generateId(),
|
|
159
|
+
type: 'ACTION_PERFORMED',
|
|
160
|
+
tenantId: log.tenantId,
|
|
161
|
+
userId: log.userId,
|
|
162
|
+
userRole: log.userRole,
|
|
163
|
+
username: log.username,
|
|
164
|
+
employeeId: log.employeeId,
|
|
165
|
+
eventTime: log.eventTime,
|
|
166
|
+
startTime: log.startTime || log.timestamp
|
|
167
|
+
},
|
|
168
|
+
$inc: {
|
|
169
|
+
// Track how many actions this user performed today (starts at 1 on first insert)
|
|
170
|
+
actionCount: 1
|
|
320
171
|
}
|
|
321
172
|
};
|
|
322
|
-
await this.collection.
|
|
323
|
-
|
|
173
|
+
const result = await this.collection.findOneAndUpdate(filter, update, {
|
|
174
|
+
upsert: true,
|
|
175
|
+
returnDocument: 'after'
|
|
176
|
+
});
|
|
177
|
+
if (result.value) {
|
|
178
|
+
// Calculate duration for existing entries
|
|
179
|
+
const startTime = new Date(result.value.startTime || result.value.timestamp);
|
|
180
|
+
result.value.activeDuration = log.timestamp.getTime() - startTime.getTime();
|
|
181
|
+
return result.value;
|
|
182
|
+
}
|
|
183
|
+
// Fallback: if upsert failed, try to find the existing record
|
|
184
|
+
const existingLog = await this.findOne(filter);
|
|
185
|
+
if (existingLog) {
|
|
186
|
+
return existingLog;
|
|
187
|
+
}
|
|
188
|
+
// Last resort: This should never happen with upsert, but handle it
|
|
189
|
+
throw new Error('Failed to create or update access log entry');
|
|
324
190
|
}
|
|
325
|
-
// OLD METHODS REMOVED - Use logIndividualPageView and logIndividualAction instead
|
|
326
|
-
// These old methods used upsert logic with unique indexes which caused E11000 errors
|
|
327
|
-
// when users revisited pages on the same day. The new approach stores each activity
|
|
328
|
-
// as a separate document and uses aggregation to show grouped views.
|
|
329
191
|
async getUserTimeline(userId, dateRange) {
|
|
330
192
|
return this.find({
|
|
331
193
|
userId,
|
|
@@ -338,6 +200,7 @@ class AccessLogRepository extends BaseRepository_1.BaseRepository {
|
|
|
338
200
|
/**
|
|
339
201
|
* List access logs with filters, search, and pagination
|
|
340
202
|
* Supports grouping by user per day to show one entry per user per day
|
|
203
|
+
* Excludes heavy fields like images, large metadata, etc. from listing
|
|
341
204
|
*/
|
|
342
205
|
async list(query) {
|
|
343
206
|
const filter = {};
|
|
@@ -369,9 +232,9 @@ class AccessLogRepository extends BaseRepository_1.BaseRepository {
|
|
|
369
232
|
{ pageRoute: searchRegex }
|
|
370
233
|
];
|
|
371
234
|
}
|
|
372
|
-
// If groupByUser is true, use
|
|
235
|
+
// If groupByUser is true, use aggregation to get one entry per user
|
|
373
236
|
if (query.groupByUser) {
|
|
374
|
-
return this.listGroupedByUser(query);
|
|
237
|
+
return this.listGroupedByUser(filter, query);
|
|
375
238
|
}
|
|
376
239
|
// Default behavior: list all entries
|
|
377
240
|
const page = Math.max(1, query.page ?? 1);
|
|
@@ -381,7 +244,22 @@ class AccessLogRepository extends BaseRepository_1.BaseRepository {
|
|
|
381
244
|
: 'timestamp';
|
|
382
245
|
const sortOrder = query.sortOrder === 'asc' ? 1 : -1;
|
|
383
246
|
const sort = { [sortKey]: sortOrder };
|
|
384
|
-
|
|
247
|
+
// Projection to include only essential fields for listing (more efficient than exclusion)
|
|
248
|
+
const projection = {
|
|
249
|
+
_id: 1,
|
|
250
|
+
username: 1,
|
|
251
|
+
employeeId: 1,
|
|
252
|
+
ipAddress: 1,
|
|
253
|
+
browser: 1,
|
|
254
|
+
startTime: 1,
|
|
255
|
+
endTime: 1,
|
|
256
|
+
timestamp: 1,
|
|
257
|
+
activeDuration: 1,
|
|
258
|
+
type: 1,
|
|
259
|
+
activityType: 1,
|
|
260
|
+
activityName: 1
|
|
261
|
+
};
|
|
262
|
+
const result = await this.paginate(filter, page, limit, sort, projection);
|
|
385
263
|
return {
|
|
386
264
|
data: result.data,
|
|
387
265
|
total: result.total,
|
|
@@ -392,6 +270,107 @@ class AccessLogRepository extends BaseRepository_1.BaseRepository {
|
|
|
392
270
|
hasPrev: result.page > 1
|
|
393
271
|
};
|
|
394
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* List access logs grouped by user per day - shows one entry per user per day with aggregated data
|
|
275
|
+
*/
|
|
276
|
+
async listGroupedByUser(filter, query) {
|
|
277
|
+
const page = Math.max(1, query.page ?? 1);
|
|
278
|
+
const limit = Math.min(100, Math.max(1, query.limit ?? 20));
|
|
279
|
+
const skip = (page - 1) * limit;
|
|
280
|
+
// Build aggregation pipeline
|
|
281
|
+
const pipeline = [
|
|
282
|
+
{ $match: filter },
|
|
283
|
+
{
|
|
284
|
+
$addFields: {
|
|
285
|
+
// Extract date from timestamp for grouping
|
|
286
|
+
dateOnly: {
|
|
287
|
+
$dateToString: { format: '%Y-%m-%d', date: '$timestamp' }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
$sort: { timestamp: -1 } // Sort by latest timestamp first
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
$group: {
|
|
296
|
+
_id: {
|
|
297
|
+
userId: '$userId',
|
|
298
|
+
date: '$dateOnly' // Group by both userId AND date
|
|
299
|
+
},
|
|
300
|
+
// Take the latest entry for each user per day
|
|
301
|
+
latestLog: { $first: '$$ROOT' },
|
|
302
|
+
// Aggregate statistics for this user on this day
|
|
303
|
+
totalActivities: { $sum: '$activityCount' },
|
|
304
|
+
totalActions: { $sum: '$actionCount' },
|
|
305
|
+
firstActivity: { $min: '$startTime' },
|
|
306
|
+
lastActivity: { $max: '$timestamp' },
|
|
307
|
+
totalSessions: { $addToSet: '$sessionId' }
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
$replaceRoot: {
|
|
312
|
+
newRoot: {
|
|
313
|
+
$mergeObjects: [
|
|
314
|
+
'$latestLog',
|
|
315
|
+
{
|
|
316
|
+
// Add aggregated fields for this day
|
|
317
|
+
totalActivities: '$totalActivities',
|
|
318
|
+
totalActions: '$totalActions',
|
|
319
|
+
firstActivity: '$firstActivity',
|
|
320
|
+
lastActivity: '$lastActivity',
|
|
321
|
+
sessionCount: { $size: '$totalSessions' }
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
// Include only essential fields for listing
|
|
328
|
+
{
|
|
329
|
+
$project: {
|
|
330
|
+
_id: 1,
|
|
331
|
+
username: 1,
|
|
332
|
+
employeeId: 1,
|
|
333
|
+
ipAddress: 1,
|
|
334
|
+
browser: 1,
|
|
335
|
+
startTime: 1,
|
|
336
|
+
endTime: 1,
|
|
337
|
+
timestamp: 1,
|
|
338
|
+
activeDuration: 1,
|
|
339
|
+
type: 1,
|
|
340
|
+
activityType: 1,
|
|
341
|
+
activityName: 1,
|
|
342
|
+
// Include aggregated fields
|
|
343
|
+
totalActivities: 1,
|
|
344
|
+
totalActions: 1,
|
|
345
|
+
firstActivity: 1,
|
|
346
|
+
lastActivity: 1,
|
|
347
|
+
sessionCount: 1
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
$sort: { timestamp: -1 } // Sort final results by latest activity
|
|
352
|
+
}
|
|
353
|
+
];
|
|
354
|
+
// Get total count of unique user-day combinations (before projection)
|
|
355
|
+
const countPipeline = [...pipeline.slice(0, 4), { $count: 'total' }];
|
|
356
|
+
const countResult = await this.collection.aggregate(countPipeline).toArray();
|
|
357
|
+
const total = countResult.length > 0 ? countResult[0].total : 0;
|
|
358
|
+
// Add pagination
|
|
359
|
+
pipeline.push({ $skip: skip });
|
|
360
|
+
pipeline.push({ $limit: limit });
|
|
361
|
+
// Execute aggregation
|
|
362
|
+
const data = await this.collection.aggregate(pipeline).toArray();
|
|
363
|
+
const totalPages = Math.ceil(total / limit);
|
|
364
|
+
return {
|
|
365
|
+
data,
|
|
366
|
+
total,
|
|
367
|
+
page,
|
|
368
|
+
limit,
|
|
369
|
+
totalPages,
|
|
370
|
+
hasNext: page < totalPages,
|
|
371
|
+
hasPrev: page > 1
|
|
372
|
+
};
|
|
373
|
+
}
|
|
395
374
|
async updateActivityDuration(logId, endTime) {
|
|
396
375
|
const log = await this.findById(logId);
|
|
397
376
|
if (!log)
|
|
@@ -403,7 +382,7 @@ class AccessLogRepository extends BaseRepository_1.BaseRepository {
|
|
|
403
382
|
});
|
|
404
383
|
}
|
|
405
384
|
generateId() {
|
|
406
|
-
return `acc_${Date.now()}_${Math.random().toString(36).
|
|
385
|
+
return `acc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
407
386
|
}
|
|
408
387
|
}
|
|
409
388
|
exports.AccessLogRepository = AccessLogRepository;
|