tango-app-api-audit 1.0.37 → 1.0.39
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/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { download, getUTC, insertOpenSearchData, logger, sendMessageToQueue } from 'tango-app-api-middleware';
|
|
1
|
+
import { chunkArray, download, getUTC, insertOpenSearchData, logger, sendMessageToQueue } from 'tango-app-api-middleware';
|
|
2
2
|
import { aggregateAuditClientData } from '../service/auditClientData.service.js';
|
|
3
3
|
import { aggregateUserAudit, updateOneUserAudit } from '../service/userAudit.service.js';
|
|
4
4
|
import mongoose from 'mongoose';
|
|
@@ -21,7 +21,8 @@ export async function userAuditHistory( req, res ) {
|
|
|
21
21
|
const dateRange = await getUTC( new Date( inputData.fromDate ), new Date( inputData.toDate ) );
|
|
22
22
|
let filter = [
|
|
23
23
|
|
|
24
|
-
{ userId: { $eq:
|
|
24
|
+
{ userId: { $eq: userId } },
|
|
25
|
+
{ moduleType: 'traffic' },
|
|
25
26
|
|
|
26
27
|
];
|
|
27
28
|
if ( inputData.fileType == 'fileDate' ) {
|
|
@@ -36,6 +37,18 @@ export async function userAuditHistory( req, res ) {
|
|
|
36
37
|
);
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
if ( inputData.filterByStoreId?.length > 0 ) {
|
|
41
|
+
filter.push( {
|
|
42
|
+
storeId: { $in: inputData.filterByStoreId },
|
|
43
|
+
} );
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ( inputData.filterByStatus?.length > 0 ) {
|
|
47
|
+
filter.push( {
|
|
48
|
+
auditStatus: { $in: inputData.filterByStatus },
|
|
49
|
+
} );
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
const query = [
|
|
40
53
|
{
|
|
41
54
|
$match: {
|
|
@@ -63,10 +76,39 @@ export async function userAuditHistory( req, res ) {
|
|
|
63
76
|
);
|
|
64
77
|
}
|
|
65
78
|
|
|
79
|
+
query.push( {
|
|
80
|
+
$lookup: {
|
|
81
|
+
from: 'stores',
|
|
82
|
+
let: { storeId: '$storeId' },
|
|
83
|
+
pipeline: [
|
|
84
|
+
{
|
|
85
|
+
$match: {
|
|
86
|
+
$expr: {
|
|
87
|
+
$eq: [ '$storeId', '$$storeId' ],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
$project: {
|
|
93
|
+
storeName: 1,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
], as: 'store',
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
} );
|
|
100
|
+
|
|
101
|
+
query.push( {
|
|
102
|
+
$unwind: {
|
|
103
|
+
path: '$store', preserveNullAndEmptyArrays: true,
|
|
104
|
+
},
|
|
105
|
+
} );
|
|
106
|
+
|
|
66
107
|
query.push( {
|
|
67
108
|
$project: {
|
|
68
109
|
storeId: 1,
|
|
69
|
-
|
|
110
|
+
userId: 1,
|
|
111
|
+
storeName: '$store.storeName',
|
|
70
112
|
auditType: 1,
|
|
71
113
|
beforeCount: 1,
|
|
72
114
|
afterCount: 1,
|
|
@@ -91,6 +133,35 @@ export async function userAuditHistory( req, res ) {
|
|
|
91
133
|
return res.sendError( 'No Data Found', 204 );
|
|
92
134
|
}
|
|
93
135
|
|
|
136
|
+
if ( inputData.isExport ) {
|
|
137
|
+
const chunkedMappingData = await chunkArray( count, 10 );
|
|
138
|
+
const promises = chunkedMappingData.map( async ( chunk ) => {
|
|
139
|
+
const exportData = [];
|
|
140
|
+
chunk.forEach( ( element ) => {
|
|
141
|
+
exportData.push( {
|
|
142
|
+
'storeId': element.storeId,
|
|
143
|
+
'storeName': element.storeName,
|
|
144
|
+
'auditType': element.auditType,
|
|
145
|
+
'beforeCount': element.beforeCount,
|
|
146
|
+
'afterCount': element.afterCount,
|
|
147
|
+
'accuracy': element.accuracy,
|
|
148
|
+
'startTime': element.startTime,
|
|
149
|
+
'endTime': element.endTime,
|
|
150
|
+
'timeSpent': element.timeSpent,
|
|
151
|
+
'auditStatus': element.auditStatus,
|
|
152
|
+
'createdAt': element.createdAt,
|
|
153
|
+
|
|
154
|
+
} );
|
|
155
|
+
} );
|
|
156
|
+
return exportData;
|
|
157
|
+
} );
|
|
158
|
+
const mappedArrays = await Promise.all( promises );
|
|
159
|
+
mappedArrays.flat();
|
|
160
|
+
|
|
161
|
+
await download( mappedArrays, res );
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
94
165
|
query.push( {
|
|
95
166
|
$skip: offset,
|
|
96
167
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { download, getJsonFileData, getUTC, insertOpenSearchData, listFileByPath, sendMessageToQueue, signedUrl, sqsReceive } from 'tango-app-api-middleware';
|
|
1
|
+
import { chunkArray, download, fileUpload, getJsonFileData, getUTC, insertOpenSearchData, listFileByPath, sendMessageToQueue, signedUrl, sqsReceive } from 'tango-app-api-middleware';
|
|
2
2
|
import { aggregateZoneUserAudit, createZoneUserAudit, findOneZoneUserAudit } from '../service/zoneUserAudit.service.js';
|
|
3
3
|
import { logger } from 'tango-app-api-middleware';
|
|
4
4
|
import { aggregateAssignZoneAudit } from '../service/assignZoneAudit.service.js';
|
|
@@ -10,7 +10,7 @@ import { aggregateUserAudit, findOneUserAudit, updateOneUserAudit } from '../ser
|
|
|
10
10
|
import { aggregateAssignAudit } from '../service/assignAudit.service.js';
|
|
11
11
|
import { createAuditLog, findOneAuditLog } from '../service/auditLog.service.js';
|
|
12
12
|
import { mapCustomer, mapEmployee, mapJunk, splitB20000, splitE0, splitG20000 } from '../validation/audit.validation.js';
|
|
13
|
-
import { aggregateClient } from '../service/client.service.js';
|
|
13
|
+
import { aggregateClient, findClient } from '../service/client.service.js';
|
|
14
14
|
import { aggregateAuditClientData } from '../service/auditClientData.service.js';
|
|
15
15
|
import _ from 'lodash';
|
|
16
16
|
import { listQueue } from 'tango-app-api-middleware/src/utils/aws/sqs.js';
|
|
@@ -426,14 +426,14 @@ export async function save( req, res ) {
|
|
|
426
426
|
|
|
427
427
|
if ( mappingUpload?.statusCode ) {
|
|
428
428
|
logger.error( {
|
|
429
|
-
error: `ERROR IN UPLOAD PATH: ${inputData.storeId}/${inputData.fileDate}/${bucket.masterJsonFile}`,
|
|
429
|
+
error: `ERROR IN UPLOAD PATH: ${inputData.storeId}/${inputData.zoneName}/${inputData.fileDate}/${bucket.masterJsonFile}`,
|
|
430
430
|
type: 'UPLOAD_ERROR',
|
|
431
431
|
} );
|
|
432
432
|
return res.sendError( mappingUpload, 500 );
|
|
433
433
|
}
|
|
434
434
|
|
|
435
435
|
let getUserAuditData = await findOneUserAudit( {
|
|
436
|
-
_id:
|
|
436
|
+
_id: inputData.auditId,
|
|
437
437
|
} );
|
|
438
438
|
const isoDate = getUserAuditData.startTime;
|
|
439
439
|
const currentTime = dayjs();
|
|
@@ -441,7 +441,7 @@ export async function save( req, res ) {
|
|
|
441
441
|
const timeDifferenceInMinutes = currentTime.diff( isoDateTime, 'seconds' );
|
|
442
442
|
|
|
443
443
|
await updateOneUserAudit(
|
|
444
|
-
{ _id:
|
|
444
|
+
{ _id: inputData.auditId },
|
|
445
445
|
{
|
|
446
446
|
auditStatus: 'completed',
|
|
447
447
|
endTime: Date.now(),
|
|
@@ -481,7 +481,7 @@ export async function save( req, res ) {
|
|
|
481
481
|
storeConfig.auditConfigs.ratio
|
|
482
482
|
) && inputData.auditType == 'Audit' ) {
|
|
483
483
|
logger.info(
|
|
484
|
-
`Hit in Reaudit pushing queue Store Id=${inputData.storeId},File Date=${inputData.fileDate}, /n \nQueue Name = ${clientData[0].auditConfigs.queueName}`,
|
|
484
|
+
`Hit in Reaudit pushing queue Store Id=${inputData.storeId},zone Name = ${inputData.zoneName}File Date=${inputData.fileDate}, /n \nQueue Name = ${clientData[0].auditConfigs.queueName}`,
|
|
485
485
|
'Reaudit',
|
|
486
486
|
);
|
|
487
487
|
|
|
@@ -501,7 +501,7 @@ export async function save( req, res ) {
|
|
|
501
501
|
|
|
502
502
|
if ( sqsQueue.statusCode ) {
|
|
503
503
|
logger.error( {
|
|
504
|
-
error: `ERROR IN UPLOAD PATH: ${inputData.storeId}/${inputData.fileDate}/${bucket.masterJsonFile}`,
|
|
504
|
+
error: `ERROR IN UPLOAD PATH: ${inputData.storeId}/${inputData.zoneName}/${inputData.fileDate}/${bucket.masterJsonFile}`,
|
|
505
505
|
type: 'UPLOAD_ERROR',
|
|
506
506
|
} );
|
|
507
507
|
return res.sendError( mappingUpload, 500 );
|
|
@@ -543,7 +543,7 @@ export async function save( req, res ) {
|
|
|
543
543
|
} catch ( error ) {
|
|
544
544
|
const err = error.message || 'Internal Server Error';
|
|
545
545
|
logger.error( { error: error, message: req.body, function: 'save' } );
|
|
546
|
-
return res.sendError( err );
|
|
546
|
+
return res.sendError( err, 500 );
|
|
547
547
|
}
|
|
548
548
|
}
|
|
549
549
|
|
|
@@ -898,3 +898,172 @@ export async function workSpace( req, res ) {
|
|
|
898
898
|
return res.sendError( err, 500 );
|
|
899
899
|
}
|
|
900
900
|
}
|
|
901
|
+
|
|
902
|
+
export async function userZoneAuditHistory( req, res ) {
|
|
903
|
+
try {
|
|
904
|
+
const inputData = req.body;
|
|
905
|
+
const userId = inputData.userId || req.user._id;
|
|
906
|
+
const limit = inputData.limit || 10;
|
|
907
|
+
const offset = inputData.offset ? ( inputData.offset - 1 ) * limit : 0;
|
|
908
|
+
const dateRange = await getUTC( new Date( inputData.fromDate ), new Date( inputData.toDate ) );
|
|
909
|
+
let filter = [
|
|
910
|
+
|
|
911
|
+
{ userId: { $eq: userId } },
|
|
912
|
+
{ moduleType: 'zone' },
|
|
913
|
+
|
|
914
|
+
];
|
|
915
|
+
if ( inputData.fileType == 'fileDate' ) {
|
|
916
|
+
filter.push(
|
|
917
|
+
{ fileDateISO: { $gte: dateRange.start } },
|
|
918
|
+
{ fileDateISO: { $lte: dateRange.end } },
|
|
919
|
+
);
|
|
920
|
+
} else {
|
|
921
|
+
filter.push(
|
|
922
|
+
{ createdAt: { $gte: dateRange.start } },
|
|
923
|
+
{ createdAt: { $lte: dateRange.end } },
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
if ( inputData.filterByStoreId?.length > 0 ) {
|
|
927
|
+
filter.push( {
|
|
928
|
+
storeId: { $in: inputData.filterByStoreId },
|
|
929
|
+
} );
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if ( inputData.filterByStatus?.length > 0 ) {
|
|
933
|
+
filter.push( {
|
|
934
|
+
auditStatus: { $in: inputData.filterByStatus },
|
|
935
|
+
} );
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const query = [
|
|
939
|
+
{
|
|
940
|
+
$match: {
|
|
941
|
+
$and: filter,
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
];
|
|
945
|
+
|
|
946
|
+
if ( inputData.searchValue && inputData.searchValue!== '' ) {
|
|
947
|
+
query.push( {
|
|
948
|
+
$match: {
|
|
949
|
+
$or: [
|
|
950
|
+
{ clientId: { $regex: inputData.searchValue, $options: 'i' } },
|
|
951
|
+
{ clientName: { $regex: inputData.searchValue, $options: 'i' } },
|
|
952
|
+
],
|
|
953
|
+
|
|
954
|
+
},
|
|
955
|
+
} );
|
|
956
|
+
}
|
|
957
|
+
if ( inputData.sortColumnName ) {
|
|
958
|
+
const sortBy = inputData.sortBy || -1;
|
|
959
|
+
query.push( {
|
|
960
|
+
$sort: { [inputData.sortColumName]: sortBy },
|
|
961
|
+
},
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
query.push( {
|
|
966
|
+
$lookup: {
|
|
967
|
+
from: 'stores',
|
|
968
|
+
let: { storeId: '$storeId' },
|
|
969
|
+
pipeline: [
|
|
970
|
+
{
|
|
971
|
+
$match: {
|
|
972
|
+
$expr: {
|
|
973
|
+
$eq: [ '$storeId', '$$storeId' ],
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
$project: {
|
|
979
|
+
storeName: 1,
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
], as: 'store',
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
} );
|
|
986
|
+
|
|
987
|
+
query.push( {
|
|
988
|
+
$unwind: {
|
|
989
|
+
path: '$store', preserveNullAndEmptyArrays: true,
|
|
990
|
+
},
|
|
991
|
+
} );
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
query.push( {
|
|
995
|
+
|
|
996
|
+
$project: {
|
|
997
|
+
storeId: 1,
|
|
998
|
+
userId: 1,
|
|
999
|
+
storeName: '$store.storeName',
|
|
1000
|
+
zoneName: 1,
|
|
1001
|
+
auditType: 1,
|
|
1002
|
+
beforeCount: 1,
|
|
1003
|
+
afterCount: 1,
|
|
1004
|
+
accuracy: { $round: [
|
|
1005
|
+
{
|
|
1006
|
+
$divide: [ { $multiply: [ '$beforeCount', { $ifNull: [ '$afterCount', 0 ] } ] }, 100 ],
|
|
1007
|
+
}, 2,
|
|
1008
|
+
],
|
|
1009
|
+
},
|
|
1010
|
+
startTime: 1,
|
|
1011
|
+
endTime: 1,
|
|
1012
|
+
timeSpent: { $round: [
|
|
1013
|
+
{ $divide: [ '$timeSpent', 60 ] }, 2,
|
|
1014
|
+
] },
|
|
1015
|
+
auditStatus: 1,
|
|
1016
|
+
createdAt: 1,
|
|
1017
|
+
},
|
|
1018
|
+
|
|
1019
|
+
} );
|
|
1020
|
+
|
|
1021
|
+
const count = await aggregateUserAudit( query );
|
|
1022
|
+
if ( count.length == 0 ) {
|
|
1023
|
+
return res.sendError( 'No Data Found', 204 );
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if ( inputData.isExport ) {
|
|
1027
|
+
const chunkedMappingData = await chunkArray( count, 10 );
|
|
1028
|
+
const promises = chunkedMappingData.map( async ( chunk ) => {
|
|
1029
|
+
const exportData = [];
|
|
1030
|
+
chunk.forEach( ( element ) => {
|
|
1031
|
+
exportData.push( {
|
|
1032
|
+
'storeId': element.storeId,
|
|
1033
|
+
'storeName': element.storeName,
|
|
1034
|
+
'auditType': element.auditType,
|
|
1035
|
+
'beforeCount': element.beforeCount,
|
|
1036
|
+
'afterCount': element.afterCount,
|
|
1037
|
+
'accuracy': element.accuracy,
|
|
1038
|
+
'startTime': element.startTime,
|
|
1039
|
+
'endTime': element.endTime,
|
|
1040
|
+
'timeSpent': element.timeSpent,
|
|
1041
|
+
'auditStatus': element.auditStatus,
|
|
1042
|
+
'createdAt': element.createdAt,
|
|
1043
|
+
|
|
1044
|
+
} );
|
|
1045
|
+
} );
|
|
1046
|
+
return exportData;
|
|
1047
|
+
} );
|
|
1048
|
+
const mappedArrays = await Promise.all( promises );
|
|
1049
|
+
mappedArrays.flat();
|
|
1050
|
+
|
|
1051
|
+
await download( mappedArrays, res );
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
query.push( {
|
|
1056
|
+
$skip: offset,
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
$limit: limit,
|
|
1060
|
+
} );
|
|
1061
|
+
const result = await aggregateUserAudit( query );
|
|
1062
|
+
|
|
1063
|
+
return res.sendSuccess( { result: result } );
|
|
1064
|
+
} catch ( error ) {
|
|
1065
|
+
const err = error.message || 'Internal Server Error';
|
|
1066
|
+
logger.info( { error: error, message: req.body, function: 'userZoneAuditHistory' } );
|
|
1067
|
+
return res.sendError( err, 500 );
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import j2s from 'joi-to-swagger';
|
|
2
|
-
import { getDraftedDataSchema, getFileSchema, saveDraftSchema, saveSchema, workSpaceSchema } from '../dtos/zoneAudit.dtos.js';
|
|
2
|
+
import { getDraftedDataSchema, getFileSchema, saveDraftSchema, saveSchema, userZoneAuditHistorySchema, workSpaceSchema } from '../dtos/zoneAudit.dtos.js';
|
|
3
3
|
|
|
4
4
|
export const zoneAuditDocs = {
|
|
5
5
|
'/v3/zone-audit/get-file': {
|
|
@@ -155,4 +155,27 @@ export const zoneAuditDocs = {
|
|
|
155
155
|
},
|
|
156
156
|
},
|
|
157
157
|
|
|
158
|
+
// metrics
|
|
159
|
+
'/v3/zone-audit/metrics/user-zone-audit-history': {
|
|
160
|
+
post: {
|
|
161
|
+
tags: [ 'Zone Audit Metrics' ],
|
|
162
|
+
description: `get zone audit work history of a user`,
|
|
163
|
+
operationId: '/metrics/user-zone-audit-history',
|
|
164
|
+
parameters: {},
|
|
165
|
+
requestBody: {
|
|
166
|
+
content: {
|
|
167
|
+
'application/json': {
|
|
168
|
+
schema: j2s( userZoneAuditHistorySchema ).swagger,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
responses: {
|
|
173
|
+
200: { description: 'The Audited file has been submited successfully ' },
|
|
174
|
+
401: { description: 'Unauthorized User' },
|
|
175
|
+
422: { description: 'Field Error' },
|
|
176
|
+
500: { description: 'Server Error' },
|
|
177
|
+
204: { description: 'Not Found' },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
158
181
|
};
|
|
@@ -86,3 +86,27 @@ export const workSpaceSchema = joi.object(
|
|
|
86
86
|
export const workSpaceValid = {
|
|
87
87
|
query: workSpaceSchema,
|
|
88
88
|
};
|
|
89
|
+
|
|
90
|
+
export const userZoneAuditHistorySchema = joi.object(
|
|
91
|
+
{
|
|
92
|
+
userId: joi.string().optional(),
|
|
93
|
+
fromDate: joi.string().required(),
|
|
94
|
+
toDate: joi.string().required(),
|
|
95
|
+
fileType: joi.string().required(),
|
|
96
|
+
filterByStoreId: joi.array().required(),
|
|
97
|
+
filterByType: joi.array().optional(),
|
|
98
|
+
filterByAuditType: joi.array().optional(),
|
|
99
|
+
filterByStatus: joi.array().optional(),
|
|
100
|
+
searchValue: joi.string().optional().allow( '' ),
|
|
101
|
+
limit: joi.number().optional(),
|
|
102
|
+
offset: joi.number().optional(),
|
|
103
|
+
sortColumnName: joi.string().optional(),
|
|
104
|
+
sortBy: joi.number().optional(),
|
|
105
|
+
isExport: joi.boolean().optional(),
|
|
106
|
+
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
export const userZoneAuditHistoryValid = {
|
|
111
|
+
body: userZoneAuditHistorySchema,
|
|
112
|
+
};
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { getDraftedDataValid, getFileValid, saveDraftValid, saveValid, workSpaceValid } from '../dtos/zoneAudit.dtos.js';
|
|
2
|
+
import { getDraftedDataValid, getFileValid, saveDraftValid, saveValid, userZoneAuditHistoryValid, workSpaceValid } from '../dtos/zoneAudit.dtos.js';
|
|
3
3
|
import { isExistsQueue, validateUserAudit } from '../validation/audit.validation.js';
|
|
4
4
|
import { isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
|
|
5
|
-
import { getDraftedData, getZoneAuditFile, save, saveDraft, workSpace } from '../controllers/zoneAudit.controller.js';
|
|
5
|
+
import { getDraftedData, getZoneAuditFile, save, saveDraft, userZoneAuditHistory, workSpace } from '../controllers/zoneAudit.controller.js';
|
|
6
6
|
|
|
7
7
|
export const zoneAuditRouter=Router();
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// zone Audit
|
|
10
10
|
zoneAuditRouter.get( '/get-file', isAllowedSessionHandler, validate( getFileValid ), isExistsQueue, getZoneAuditFile );
|
|
11
11
|
zoneAuditRouter.post( '/save-draft', isAllowedSessionHandler, validate( saveDraftValid ), saveDraft );
|
|
12
12
|
zoneAuditRouter.get( '/get-drafted-data', isAllowedSessionHandler, validate( getDraftedDataValid ), getDraftedData );
|
|
13
13
|
zoneAuditRouter.post( '/save', isAllowedSessionHandler, validate( saveValid ), validateUserAudit, save );
|
|
14
14
|
zoneAuditRouter.get( '/work-space', isAllowedSessionHandler, validate( workSpaceValid ), workSpace );
|
|
15
15
|
|
|
16
|
+
// metrics
|
|
17
|
+
zoneAuditRouter.post( '/metrics/user-zone-audit-history', isAllowedSessionHandler, validate( userZoneAuditHistoryValid ), userZoneAuditHistory );
|