tango-app-api-trax 3.9.32 → 3.9.33

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 (53) hide show
  1. package/index.js +1 -2
  2. package/package.json +2 -2
  3. package/src/controllers/gallery.controller.js +0 -2
  4. package/src/controllers/internalTrax.controller.js +78 -117
  5. package/src/controllers/mobileTrax.controller.js +29 -69
  6. package/src/controllers/teaxFlag.controller.js +5 -5
  7. package/src/controllers/trax.controller.js +3 -4
  8. package/src/controllers/traxDashboard.controllers.js +54 -63
  9. package/src/hbs/flag.hbs +1 -1
  10. package/src/hbs/login-otp.hbs +943 -943
  11. package/src/hbs/template.hbs +0 -7
  12. package/src/hbs/visit-checklist.hbs +1 -51
  13. package/src/services/app.service.js +14 -20
  14. package/src/services/approver.service.js +20 -28
  15. package/src/services/authentication.service.js +6 -12
  16. package/src/services/camera.service.js +18 -24
  17. package/src/services/checklist.service.js +31 -36
  18. package/src/services/checklistAssign.service.js +39 -44
  19. package/src/services/checklistQuestion.service.js +35 -40
  20. package/src/services/checklistlog.service.js +35 -40
  21. package/src/services/clientRequest.service.js +5 -12
  22. package/src/services/clients.services.js +18 -23
  23. package/src/services/cluster.service.js +28 -36
  24. package/src/services/domain.service.js +28 -33
  25. package/src/services/download.services.js +38 -48
  26. package/src/services/group.service.js +22 -28
  27. package/src/services/lenskartEmployeeMapping.service.js +15 -20
  28. package/src/services/locus.service.js +34 -41
  29. package/src/services/notification.service.js +31 -40
  30. package/src/services/otp.service.js +17 -24
  31. package/src/services/planogram.service.js +5 -12
  32. package/src/services/processedTaskConfig.service.js +32 -40
  33. package/src/services/processedTaskList.service.js +30 -36
  34. package/src/services/processedchecklist.services.js +48 -56
  35. package/src/services/processedchecklistconfig.services.js +35 -40
  36. package/src/services/recurringFlagTracker.service.js +33 -40
  37. package/src/services/runAIFeatures.services.js +31 -36
  38. package/src/services/runAIRequest.services.js +39 -44
  39. package/src/services/store.service.js +31 -36
  40. package/src/services/tagging.service.js +5 -12
  41. package/src/services/taskConfig.service.js +32 -40
  42. package/src/services/teams.service.js +30 -41
  43. package/src/services/ticket.service.js +15 -20
  44. package/src/services/user.service.js +25 -32
  45. package/src/services/userAssignedstores.service.js +10 -17
  46. package/src/utils/visitChecklistPdf.utils.js +11 -114
  47. package/src/logging/activityLogFlusher.js +0 -59
  48. package/src/logging/activityLogMiddleware.js +0 -45
  49. package/src/logging/activityLogStore.js +0 -91
  50. package/src/logging/compressBatches.js +0 -83
  51. package/src/logging/config.js +0 -24
  52. package/src/logging/createLoggableService.js +0 -46
  53. package/src/logging/logExternalCall.js +0 -37
@@ -1,41 +1,30 @@
1
- import teamsModel from 'tango-api-schema/schema/teams.model.js';
2
- import { createLoggableService } from '../logging/createLoggableService.js';
3
-
4
-
5
- const _methods = {
6
- async createTeamsModel( data ) {
7
- return await teamsModel.create( data );
8
- },
9
- async updateOneTeams( query, record ) {
10
- return await teamsModel.updateOne( query, { $set: record }, { upsert: true } );
11
- },
12
- async aggregateTeams( query ) {
13
- return await teamsModel.aggregate( query );
14
- },
15
- async deleteTeams( query ={} ) {
16
- return await teamsModel.deleteOne( query );
17
- },
18
- async findOneTeams( query ={}, field={} ) {
19
- return await teamsModel.findOne( query, field );
20
- },
21
- async findteams( query ={}, field={} ) {
22
- return await teamsModel.find( query, field );
23
- },
24
- async updateOneTeamModel( query, record ) {
25
- return await teamsModel.updateOne( query, record );
26
- },
27
- countDocumentsTeams( query ) {
28
- return teamsModel.countDocuments( query );
29
- },
30
- };
31
-
32
- const _svc = createLoggableService( _methods, 'teams' );
33
-
34
- export const createTeamsModel = ( ...args ) => _svc.createTeamsModel( ...args );
35
- export const updateOneTeams = ( ...args ) => _svc.updateOneTeams( ...args );
36
- export const aggregateTeams = ( ...args ) => _svc.aggregateTeams( ...args );
37
- export const deleteTeams = ( ...args ) => _svc.deleteTeams( ...args );
38
- export const findOneTeams = ( ...args ) => _svc.findOneTeams( ...args );
39
- export const findteams = ( ...args ) => _svc.findteams( ...args );
40
- export const updateOneTeamModel = ( ...args ) => _svc.updateOneTeamModel( ...args );
41
- export const countDocumentsTeams = ( ...args ) => _svc.countDocumentsTeams( ...args );
1
+ import teamsModel from 'tango-api-schema/schema/teams.model.js';
2
+
3
+
4
+ export async function createTeamsModel( data ) {
5
+ return await teamsModel.create( data );
6
+ };
7
+ export async function updateOneTeams( query, record ) {
8
+ return await teamsModel.updateOne( query, { $set: record }, { upsert: true } );
9
+ };
10
+
11
+ export async function aggregateTeams( query ) {
12
+ return await teamsModel.aggregate( query );
13
+ };
14
+ export async function deleteTeams( query ={} ) {
15
+ return await teamsModel.deleteOne( query );
16
+ };
17
+
18
+ export async function findOneTeams( query ={}, field={} ) {
19
+ return await teamsModel.findOne( query, field );
20
+ };
21
+ export async function findteams( query ={}, field={} ) {
22
+ return await teamsModel.find( query, field );
23
+ };
24
+
25
+ export async function updateOneTeamModel( query, record ) {
26
+ return await teamsModel.updateOne( query, record );
27
+ }
28
+ export function countDocumentsTeams( query ) {
29
+ return teamsModel.countDocuments( query );
30
+ }
@@ -1,20 +1,15 @@
1
- import model from 'tango-api-schema';
2
- import { createLoggableService } from '../logging/createLoggableService.js';
3
-
4
- const _methods = {
5
- async findOne( query={}, field={} ) {
6
- return model.aiTicketConfigModel.findOne( query, field );
7
- },
8
- async create( document = {} ) {
9
- return model.aiTicketConfigModel.create( document );
10
- },
11
- async deleteOne( query = {} ) {
12
- return model.aiTicketConfigModel.deleteOne( query );
13
- },
14
- };
15
-
16
- const _svc = createLoggableService( _methods, 'tickets' );
17
-
18
- export const findOne = ( ...args ) => _svc.findOne( ...args );
19
- export const create = ( ...args ) => _svc.create( ...args );
20
- export const deleteOne = ( ...args ) => _svc.deleteOne( ...args );
1
+ import model from 'tango-api-schema';
2
+
3
+ export const findOne = async ( query={}, field={} ) => {
4
+ return model.aiTicketConfigModel.findOne( query, field );
5
+ };
6
+
7
+ export const create = async ( document = {} ) => {
8
+ return model.aiTicketConfigModel.create( document );
9
+ };
10
+
11
+ export const deleteOne = async ( query = {} ) => {
12
+ return model.aiTicketConfigModel.deleteOne( query );
13
+ };
14
+
15
+
@@ -1,32 +1,25 @@
1
- import model from 'tango-api-schema';
2
- import { createLoggableService } from '../logging/createLoggableService.js';
3
-
4
- const _methods = {
5
- async findOne( query={}, field={} ) {
6
- return model.userModel.findOne( query, field ).sort( { userName: 1 } );
7
- },
8
- async find( query={}, field={} ) {
9
- return model.userModel.find( query, field ).sort( { userName: 1 } );
10
- },
11
- async create( document = {} ) {
12
- return model.userModel.create( document );
13
- },
14
- async deleteOne( query = {} ) {
15
- return model.userModel.deleteOne( query );
16
- },
17
- async updateOne( query = {}, record={} ) {
18
- return model.userModel.updateOne( query, { $set: record } );
19
- },
20
- async aggregate( query = {} ) {
21
- return model.userModel.aggregate( query );
22
- },
23
- };
24
-
25
- const _svc = createLoggableService( _methods, 'users' );
26
-
27
- export const findOne = ( ...args ) => _svc.findOne( ...args );
28
- export const find = ( ...args ) => _svc.find( ...args );
29
- export const create = ( ...args ) => _svc.create( ...args );
30
- export const deleteOne = ( ...args ) => _svc.deleteOne( ...args );
31
- export const updateOne = ( ...args ) => _svc.updateOne( ...args );
32
- export const aggregate = ( ...args ) => _svc.aggregate( ...args );
1
+ import model from 'tango-api-schema';
2
+
3
+ export const findOne = async ( query={}, field={} ) => {
4
+ return model.userModel.findOne( query, field ).sort( { userName: 1 } );
5
+ };
6
+
7
+ export const find = async ( query={}, field={} ) => {
8
+ return model.userModel.find( query, field ).sort( { userName: 1 } );
9
+ };
10
+
11
+ export const create = async ( document = {} ) => {
12
+ return model.userModel.create( document );
13
+ };
14
+
15
+ export const deleteOne = async ( query = {} ) => {
16
+ return model.userModel.deleteOne( query );
17
+ };
18
+
19
+ export const updateOne = async ( query = {}, record={} ) => {
20
+ return model.userModel.updateOne( query, { $set: record } );
21
+ };
22
+
23
+ export const aggregate = async ( query = {} ) => {
24
+ return model.userModel.aggregate( query );
25
+ };
@@ -1,17 +1,10 @@
1
- import userAssignedStoreModel from 'tango-api-schema/schema/userAssignedStore.model.js';
2
- import { createLoggableService } from '../logging/createLoggableService.js';
3
-
4
-
5
- const _methods = {
6
- async aggregateUserAssignedStore( query ) {
7
- return await userAssignedStoreModel.aggregate( query );
8
- },
9
- async findUserAssignedStore( query ) {
10
- return await userAssignedStoreModel.find( query );
11
- },
12
- };
13
-
14
- const _svc = createLoggableService( _methods, 'userassignedstores' );
15
-
16
- export const aggregateUserAssignedStore = ( ...args ) => _svc.aggregateUserAssignedStore( ...args );
17
- export const findUserAssignedStore = ( ...args ) => _svc.findUserAssignedStore( ...args );
1
+ import userAssignedStoreModel from 'tango-api-schema/schema/userAssignedStore.model.js';
2
+
3
+
4
+ export async function aggregateUserAssignedStore( query ) {
5
+ return await userAssignedStoreModel.aggregate( query );
6
+ };
7
+
8
+ export async function findUserAssignedStore( query ) {
9
+ return await userAssignedStoreModel.find( query );
10
+ };
@@ -180,21 +180,11 @@ function buildQuestionAnswerEntries( question ) {
180
180
  }
181
181
  }
182
182
  const validationImage = flattenImageRefs( userAnswer?.validationImage );
183
- // Videos captured alongside 'Capture Multiple Image with description' validation (no runAI; shown as links).
184
- const validationVideo = flattenImageRefs( userAnswer?.validationVideo );
185
-
186
- const referenceImage = userAnswer?.referenceImage || matchedAnswer?.referenceImage || '';
187
- // Left column shows a reference image only for non-image/video, non-multipleImage answer types.
188
- // When there is no reference image, uploaded/validation media is rendered on the left instead of the right.
189
- const hasReferenceImage = question?.answerType !== 'image/video' &&
190
- question?.answerType !== 'multipleImage' &&
191
- Boolean( referenceImage || multiReferenceImage.length );
192
183
 
193
184
  return {
194
185
  answer: userAnswer?.answer || '',
195
186
  answerType: getMediaDisplayType( question?.answerType, userAnswer ),
196
- referenceImage,
197
- hasReferenceImage,
187
+ referenceImage: userAnswer?.referenceImage || matchedAnswer?.referenceImage || '',
198
188
  multiReferenceImage,
199
189
  remarks: userAnswer?.remarks || '',
200
190
  sopFlag: userAnswer?.sopFlag ?? matchedAnswer?.sopFlag ?? false,
@@ -202,40 +192,12 @@ function buildQuestionAnswerEntries( question ) {
202
192
  validationType,
203
193
  validationAnswer,
204
194
  validationImage,
205
- validationVideo,
206
195
  validationDisplayType: getValidationDisplayType( validationType ),
207
196
  };
208
197
  } );
209
198
  }
210
199
 
211
200
 
212
- /**
213
- * Extracts every 'Matched/Not Matched' value from a userAnswer's runAIData.
214
- * Handles both shapes:
215
- * - [ { answerImage, results: [ { featureName, value } ] } ] (per-image nested)
216
- * - [ { featureName, value } ] (flat)
217
- * @param {Array} runAIData
218
- * @return {string[]} list of 'True' / 'False' values (in source order)
219
- */
220
- function getRunAIMatchValues( runAIData ) {
221
- if ( !Array.isArray( runAIData ) ) return [];
222
-
223
- const values = [];
224
-
225
- runAIData.forEach( ( item ) => {
226
- if ( Array.isArray( item?.results ) ) {
227
- item.results.forEach( ( res ) => {
228
- if ( res?.featureName === 'Matched/Not Matched' ) values.push( res.value );
229
- } );
230
- } else if ( item?.featureName === 'Matched/Not Matched' ) {
231
- values.push( item.value );
232
- }
233
- } );
234
-
235
- return values;
236
- }
237
-
238
-
239
201
  /**
240
202
 
241
203
  * Map section array { sectionName, questions[] } (view API or processed checklist) to scores / template sections.
@@ -285,18 +247,14 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
285
247
  let score = q.compliance ? Math.max( ...q?.userAnswer?.map( ( o ) => o?.complianceScore ?? 0 ) ) : 0;
286
248
 
287
249
 
288
- if ( q.compliance && ( q.answerType === 'image' || q.answerType === 'image/video' ) ) {
289
- // image/video questions mix image + video entries in userAnswer, so only score the images.
290
- const imageAnswers = q.answerType === 'image/video' ?
291
- ( q.userAnswer || [] ).filter( ( ua ) => ua?.answerType === 'image' ) :
292
- ( q.userAnswer || [] );
293
-
294
- const matchValues = imageAnswers.flatMap( ( ua ) => getRunAIMatchValues( ua?.runAIData ) );
295
-
296
- if ( matchValues.length ) {
297
- // All images must be matched ('True'); if any one is not, treat as not matched.
298
- const allMatched = matchValues.every( ( value ) => value === 'True' );
299
- score = allMatched ? ( q?.answers?.[0]?.matchedCount ?? 0 ) : ( q?.answers?.[0]?.notMatchedCount ?? 0 );
250
+ if ( q.answerType == 'image' && q.compliance && q.userAnswer?.[0]?.runAIData ) {
251
+ let find = q.userAnswer?.[0]?.runAIData?.find( ( run ) => run?.featureName == 'Matched/Not Matched' );
252
+ if ( find ) {
253
+ if ( find?.value == 'True' ) {
254
+ score = q?.answers?.[0]?.matchedCount;
255
+ } else {
256
+ score = q?.answers?.[0]?.notMatchedCount;
257
+ }
300
258
  }
301
259
  }
302
260
 
@@ -523,9 +481,6 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
523
481
 
524
482
  let referenceId = doc.coverage == 'store' ? doc?.storeName : doc?.userName;
525
483
 
526
- const userImage = doc.userImage || '';
527
- const userSignature = doc.userSignature || '';
528
-
529
484
  return {
530
485
 
531
486
 
@@ -557,12 +512,6 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
557
512
 
558
513
  country: doc.country || '--',
559
514
 
560
- userImage,
561
-
562
- userSignature,
563
-
564
- showUserVerification: Boolean( userImage || userSignature ),
565
-
566
515
  hasCompliancePage,
567
516
 
568
517
  detailPageStart,
@@ -673,9 +622,6 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
673
622
  } );
674
623
  }
675
624
 
676
- const userImage = checklistAnswer?.userImage || checklistInfo?.userImage || '';
677
- const userSignature = checklistAnswer?.userSignature || checklistInfo?.userSignature || '';
678
-
679
625
  return {
680
626
 
681
627
  brandLogo: brandInfo.brandLogo || '',
@@ -710,12 +656,6 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
710
656
 
711
657
  country: storeProfile?.Country || '--',
712
658
 
713
- userImage,
714
-
715
- userSignature,
716
-
717
- showUserVerification: Boolean( userImage || userSignature ),
718
-
719
659
  hasCompliancePage,
720
660
 
721
661
  detailPageStart,
@@ -842,16 +782,6 @@ export function createImageCache() {
842
782
  }
843
783
 
844
784
 
845
- if ( resolvedData.userImage && resolvedData.userImage.startsWith( 'http' ) ) {
846
- urls.add( resolvedData.userImage );
847
- }
848
-
849
-
850
- if ( resolvedData.userSignature && resolvedData.userSignature.startsWith( 'http' ) ) {
851
- urls.add( resolvedData.userSignature );
852
- }
853
-
854
-
855
785
  const collectFromSection = ( section ) => {
856
786
  section.questions?.forEach( ( q ) => {
857
787
  if ( q.questionReferenceImage && q.questionReferenceImage.startsWith( 'http' ) ) urls.add( q.questionReferenceImage );
@@ -912,16 +842,6 @@ export function createImageCache() {
912
842
  }
913
843
 
914
844
 
915
- if ( resolvedData.userImage ) {
916
- resolvedData.userImage = cache.get( resolvedData.userImage ) || resolvedData.userImage;
917
- }
918
-
919
-
920
- if ( resolvedData.userSignature ) {
921
- resolvedData.userSignature = cache.get( resolvedData.userSignature ) || resolvedData.userSignature;
922
- }
923
-
924
-
925
845
  const replaceInSection = ( section ) => {
926
846
  section.questions?.forEach( ( q ) => {
927
847
  if ( q.questionReferenceImage && cache.has( q.questionReferenceImage ) ) q.questionReferenceImage = cache.get( q.questionReferenceImage );
@@ -1016,10 +936,6 @@ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2ssk
1016
936
  ua.validationImage = ua.validationImage.map( ( ele ) => resolveUrl( ele ) );
1017
937
  }
1018
938
 
1019
- if ( ua?.validationVideo?.length ) {
1020
- ua.validationVideo = ua.validationVideo.map( ( ele ) => resolveUrl( ele ) );
1021
- }
1022
-
1023
939
 
1024
940
  if ( ua.answer && ua.answerType !== 'text' ) {
1025
941
  ua.answer = resolveUrl( ua.answer );
@@ -1038,16 +954,9 @@ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2ssk
1038
954
 
1039
955
  resolvedData.sections?.forEach( resolveQuestionMedia );
1040
956
 
1041
- if ( resolvedData?.brandLogo && !resolvedData?.brandLogo?.startsWith( 'http' ) ) {
1042
- resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );
1043
- }
1044
-
1045
- if ( resolvedData?.userImage && !resolvedData.userImage.startsWith( 'http' ) && !resolvedData.userImage.startsWith( 'data:' ) ) {
1046
- resolvedData.userImage = resolveUrl( resolvedData.userImage );
1047
- }
1048
957
 
1049
- if ( resolvedData?.userSignature && !resolvedData.userSignature.startsWith( 'http' ) && !resolvedData.userSignature.startsWith( 'data:' ) ) {
1050
- resolvedData.userSignature = resolveUrl( resolvedData.userSignature );
958
+ if ( resolvedData.brandLogo && !resolvedData.brandLogo.startsWith( 'http' ) ) {
959
+ resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );
1051
960
  }
1052
961
 
1053
962
 
@@ -1161,10 +1070,6 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
1161
1070
  ua.validationImage = ua.validationImage.map( ( ele ) => resolveUrl( ele ) );
1162
1071
  }
1163
1072
 
1164
- if ( ua.validationVideo?.length ) {
1165
- ua.validationVideo = ua.validationVideo.map( ( ele ) => resolveUrl( ele ) );
1166
- }
1167
-
1168
1073
  if ( ua.answer && ua.answerType !== 'text' ) {
1169
1074
  ua.answer = resolveUrl( ua.answer );
1170
1075
  }
@@ -1182,14 +1087,6 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
1182
1087
  resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );
1183
1088
  }
1184
1089
 
1185
- if ( resolvedData.userImage && !resolvedData.userImage.startsWith( 'http' ) && !resolvedData.userImage.startsWith( 'data:' ) ) {
1186
- resolvedData.userImage = resolveUrl( resolvedData.userImage );
1187
- }
1188
-
1189
- if ( resolvedData.userSignature && !resolvedData.userSignature.startsWith( 'http' ) && !resolvedData.userSignature.startsWith( 'data:' ) ) {
1190
- resolvedData.userSignature = resolveUrl( resolvedData.userSignature );
1191
- }
1192
-
1193
1090
 
1194
1091
  const html = template( resolvedData );
1195
1092
 
@@ -1,59 +0,0 @@
1
- import { insertOpenSearchData, logger } from 'tango-app-api-middleware';
2
- import { loggingConfig } from './config.js';
3
- import { compressBatches } from './compressBatches.js';
4
-
5
- export async function flushActivityLog( ctx, status ) {
6
- if ( !loggingConfig.enabled ) return;
7
- if ( !ctx ) return;
8
-
9
- if ( ctx.steps.length === 0 && !ctx.error ) return;
10
-
11
- const compressedSteps = safeCompress( ctx.steps );
12
-
13
- const doc = {
14
- version: loggingConfig.version,
15
- requestId: ctx.requestId,
16
- timestamp: new Date( ctx.startTime ).toISOString(),
17
- duration: Date.now() - ctx.startTime,
18
- user: ctx.user,
19
- api: {
20
- method: ctx.api.method,
21
- path: ctx.api.path,
22
- action: ctx.api.action,
23
- body: stringifyBody( ctx.api.body ),
24
- },
25
- response: {
26
- code: ctx.response?.code ?? null,
27
- body: stringifyBody( ctx.response?.body ),
28
- },
29
- status,
30
- steps: compressedSteps,
31
- error: ctx.error,
32
- };
33
-
34
- try {
35
- await insertOpenSearchData( loggingConfig.index, doc );
36
- } catch ( e ) {
37
- logger.error( { functionName: 'flushActivityLog', error: e } );
38
- }
39
- }
40
-
41
- function safeCompress( steps ) {
42
- try {
43
- return compressBatches( steps );
44
- } catch ( _e ) {
45
- // Never let compression failure block log delivery — fall back to raw steps.
46
- return steps;
47
- }
48
- }
49
-
50
- function stringifyBody( body ) {
51
- if ( body === null || body === undefined ) return null;
52
- if ( typeof body === 'string' ) return body;
53
- if ( body instanceof Error ) return body.message || String( body );
54
- try {
55
- return JSON.stringify( body );
56
- } catch ( _e ) {
57
- return '[unserializable]';
58
- }
59
- }
@@ -1,45 +0,0 @@
1
- import { activityLogStore, createLogContext } from './activityLogStore.js';
2
- import { flushActivityLog } from './activityLogFlusher.js';
3
- import { loggingConfig } from './config.js';
4
-
5
- function refreshUser( ctx, req ) {
6
- if ( req?.user ) {
7
- ctx.user = {
8
- _id: req.user._id,
9
- userName: req.user.userName,
10
- email: req.user.email,
11
- };
12
- }
13
- }
14
-
15
- export default function traxActivityLogMiddleware( req, res, next ) {
16
- if ( !loggingConfig.enabled ) return next();
17
-
18
- const WRITE_METHODS = [ 'POST', 'PUT', 'PATCH', 'DELETE' ];
19
- if ( !WRITE_METHODS.includes( req.method ) ) return next();
20
-
21
- const ctx = createLogContext( req );
22
-
23
- activityLogStore.run( ctx, () => {
24
- const originalSendSuccess = res.sendSuccess;
25
- const originalSendError = res.sendError;
26
-
27
- res.sendSuccess = ( data ) => {
28
- refreshUser( ctx, req );
29
- ctx.response.code = 200;
30
- ctx.response.body = data;
31
- originalSendSuccess( data );
32
- setImmediate( () => flushActivityLog( ctx, 'success' ).catch( () => {} ) );
33
- };
34
-
35
- res.sendError = ( message, code ) => {
36
- refreshUser( ctx, req );
37
- ctx.response.code = code ?? 500;
38
- ctx.response.body = message;
39
- originalSendError( message, code );
40
- setImmediate( () => flushActivityLog( ctx, 'failed' ).catch( () => {} ) );
41
- };
42
-
43
- next();
44
- } );
45
- }
@@ -1,91 +0,0 @@
1
- import { AsyncLocalStorage } from 'async_hooks';
2
- import { randomUUID } from 'crypto';
3
-
4
- export const activityLogStore = new AsyncLocalStorage();
5
-
6
- const SENSITIVE_KEYS = [ 'password', 'token', 'secret', 'authorization' ];
7
-
8
- export function createLogContext( req ) {
9
- return {
10
- requestId: randomUUID(),
11
- startTime: Date.now(),
12
- user: req.user ? {
13
- _id: req.user._id,
14
- userName: req.user.userName,
15
- email: req.user.email,
16
- } : null,
17
- api: {
18
- method: req.method,
19
- path: req.path,
20
- action: `${ req.method } ${ req.path }`,
21
- body: sanitizeBody( req.body ),
22
- },
23
- response: {
24
- code: null,
25
- body: null,
26
- },
27
- steps: [],
28
- stepCounter: 0,
29
- error: null,
30
- };
31
- }
32
-
33
- export function getLogContext() {
34
- return activityLogStore.getStore();
35
- }
36
-
37
- export function pushStep( entry ) {
38
- const ctx = getLogContext();
39
- if ( !ctx ) return;
40
-
41
- const last = ctx.steps[ctx.steps.length - 1];
42
- if (
43
- last &&
44
- last.name === entry.name &&
45
- last.type === entry.type &&
46
- last.status === 'success' &&
47
- entry.status === 'success'
48
- ) {
49
- last.count = ( last.count || 1 ) + 1;
50
- last.ms += entry.ms;
51
- return;
52
- }
53
-
54
- ctx.stepCounter++;
55
- ctx.steps.push( {
56
- stepIndex: ctx.stepCounter,
57
- name: entry.name,
58
- type: entry.type,
59
- status: entry.status,
60
- ms: entry.ms,
61
- } );
62
- }
63
-
64
- export function pushStepFailure( entry, error ) {
65
- const ctx = getLogContext();
66
- if ( !ctx ) return;
67
- ctx.stepCounter++;
68
- const message = error?.message || String( error );
69
- ctx.steps.push( {
70
- stepIndex: ctx.stepCounter,
71
- name: entry.name,
72
- type: entry.type,
73
- status: 'failed',
74
- ms: entry.ms,
75
- error: message,
76
- } );
77
- ctx.error = {
78
- message,
79
- failedAtStepIndex: ctx.stepCounter,
80
- failedStepName: entry.name,
81
- };
82
- }
83
-
84
- function sanitizeBody( body ) {
85
- if ( !body || typeof body !== 'object' ) return body;
86
- const cleaned = { ...body };
87
- for ( const key of SENSITIVE_KEYS ) {
88
- if ( cleaned[key] !== undefined ) cleaned[key] = '[REDACTED]';
89
- }
90
- return cleaned;
91
- }