@striae-org/striae 5.4.2 → 5.4.4
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/app/components/actions/case-export/download-handlers.ts +1 -1
- package/app/components/actions/case-export/metadata-helpers.ts +2 -4
- package/app/components/actions/case-import/zip-processing.ts +3 -3
- package/app/components/mobile-warning/mobile-warning.module.css +80 -0
- package/app/components/mobile-warning/mobile-warning.tsx +108 -0
- package/app/components/navbar/case-import/utils/file-validation.ts +1 -1
- package/app/config-example/config.json +2 -2
- package/app/root.tsx +2 -0
- package/app/services/audit/audit-file-type.ts +0 -1
- package/app/services/audit/audit.service.ts +1 -1
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -6
- package/app/services/audit/index.ts +0 -1
- package/app/types/audit.ts +1 -1
- package/app/utils/auth/auth-action-settings.ts +1 -1
- package/app/utils/data/permissions.ts +17 -15
- package/app/utils/forensics/audit-export-signature.ts +4 -4
- package/app/utils/forensics/export-verification.ts +3 -11
- package/package.json +2 -2
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/src/audit-worker.example.ts +1 -1
- package/workers/audit-worker/src/handlers/audit-routes.ts +1 -30
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +17 -17
- package/workers/data-worker/src/encryption-utils.ts +1 -1
- package/workers/data-worker/src/signing-payload-utils.ts +4 -4
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/auth.ts +7 -0
- package/workers/image-worker/src/handlers/delete-image.ts +26 -0
- package/workers/image-worker/src/handlers/mint-signed-url.ts +83 -0
- package/workers/image-worker/src/handlers/serve-image.ts +65 -0
- package/workers/image-worker/src/handlers/upload-image.ts +62 -0
- package/workers/image-worker/src/image-worker.example.ts +3 -707
- package/workers/image-worker/src/router.ts +53 -0
- package/workers/image-worker/src/security/key-registry.ts +193 -0
- package/workers/image-worker/src/security/signed-url.ts +163 -0
- package/workers/image-worker/src/types.ts +68 -0
- package/workers/image-worker/src/utils/content-disposition.ts +33 -0
- package/workers/image-worker/src/utils/path-utils.ts +50 -0
- package/workers/image-worker/src/utils/storage-metadata.ts +27 -0
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/src/handlers/user-routes.ts +23 -34
- package/workers/user-worker/src/user-worker.example.ts +17 -23
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/audit/viewer/use-audit-viewer-export.ts +0 -176
- package/app/services/audit/audit-export-csv.ts +0 -130
- package/app/services/audit/audit-export-report.ts +0 -205
- package/app/services/audit/audit-export.service.ts +0 -333
|
@@ -13,7 +13,20 @@ import type {
|
|
|
13
13
|
function createJsonResponse(data: unknown, headers: ResponseHeaders, status: number = 200): Response {
|
|
14
14
|
return new Response(JSON.stringify(data), {
|
|
15
15
|
status,
|
|
16
|
-
headers
|
|
16
|
+
headers: {
|
|
17
|
+
...headers,
|
|
18
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createTextResponse(message: string, headers: ResponseHeaders, status: number): Response {
|
|
24
|
+
return new Response(message, {
|
|
25
|
+
status,
|
|
26
|
+
headers: {
|
|
27
|
+
...headers,
|
|
28
|
+
'Content-Type': 'text/plain; charset=utf-8'
|
|
29
|
+
}
|
|
17
30
|
});
|
|
18
31
|
}
|
|
19
32
|
|
|
@@ -25,10 +38,7 @@ export async function handleGetUser(
|
|
|
25
38
|
try {
|
|
26
39
|
const userData = await readUserRecord(env, userUid);
|
|
27
40
|
if (userData === null) {
|
|
28
|
-
return
|
|
29
|
-
status: 404,
|
|
30
|
-
headers: corsHeaders
|
|
31
|
-
});
|
|
41
|
+
return createTextResponse('User not found', corsHeaders, 404);
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
return createJsonResponse(userData, corsHeaders);
|
|
@@ -36,10 +46,7 @@ export async function handleGetUser(
|
|
|
36
46
|
const errorMessage = error instanceof Error ? error.message : 'Unknown user data read error';
|
|
37
47
|
console.error('Failed to get user data:', { uid: userUid, reason: errorMessage });
|
|
38
48
|
|
|
39
|
-
return
|
|
40
|
-
status: 500,
|
|
41
|
-
headers: corsHeaders
|
|
42
|
-
});
|
|
49
|
+
return createTextResponse('Failed to get user data', corsHeaders, 500);
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -91,10 +98,7 @@ export async function handleAddUser(
|
|
|
91
98
|
|
|
92
99
|
return createJsonResponse(userData, corsHeaders, existingUser !== null ? 200 : 201);
|
|
93
100
|
} catch {
|
|
94
|
-
return
|
|
95
|
-
status: 500,
|
|
96
|
-
headers: corsHeaders
|
|
97
|
-
});
|
|
101
|
+
return createTextResponse('Failed to save user data', corsHeaders, 500);
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -115,10 +119,7 @@ export async function handleDeleteUser(
|
|
|
115
119
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
116
120
|
|
|
117
121
|
if (errorMessage === 'User not found') {
|
|
118
|
-
return
|
|
119
|
-
status: 404,
|
|
120
|
-
headers: corsHeaders
|
|
121
|
-
});
|
|
122
|
+
return createTextResponse('User not found', corsHeaders, 404);
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
return createJsonResponse({
|
|
@@ -135,7 +136,7 @@ export function handleDeleteUserWithProgress(
|
|
|
135
136
|
): Response {
|
|
136
137
|
const sseHeaders: ResponseHeaders = {
|
|
137
138
|
...corsHeaders,
|
|
138
|
-
'Content-Type': 'text/event-stream',
|
|
139
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
139
140
|
'Cache-Control': 'no-cache, no-transform',
|
|
140
141
|
Connection: 'keep-alive'
|
|
141
142
|
};
|
|
@@ -188,10 +189,7 @@ export async function handleAddCases(
|
|
|
188
189
|
const { cases = [] }: AddCasesRequest = await request.json();
|
|
189
190
|
const userData = await readUserRecord(env, userUid);
|
|
190
191
|
if (!userData) {
|
|
191
|
-
return
|
|
192
|
-
status: 404,
|
|
193
|
-
headers: corsHeaders
|
|
194
|
-
});
|
|
192
|
+
return createTextResponse('User not found', corsHeaders, 404);
|
|
195
193
|
}
|
|
196
194
|
|
|
197
195
|
const existingCases = userData.cases || [];
|
|
@@ -205,10 +203,7 @@ export async function handleAddCases(
|
|
|
205
203
|
|
|
206
204
|
return createJsonResponse(userData, corsHeaders);
|
|
207
205
|
} catch {
|
|
208
|
-
return
|
|
209
|
-
status: 500,
|
|
210
|
-
headers: corsHeaders
|
|
211
|
-
});
|
|
206
|
+
return createTextResponse('Failed to add cases', corsHeaders, 500);
|
|
212
207
|
}
|
|
213
208
|
}
|
|
214
209
|
|
|
@@ -222,10 +217,7 @@ export async function handleDeleteCases(
|
|
|
222
217
|
const { casesToDelete }: DeleteCasesRequest = await request.json();
|
|
223
218
|
const userData = await readUserRecord(env, userUid);
|
|
224
219
|
if (!userData) {
|
|
225
|
-
return
|
|
226
|
-
status: 404,
|
|
227
|
-
headers: corsHeaders
|
|
228
|
-
});
|
|
220
|
+
return createTextResponse('User not found', corsHeaders, 404);
|
|
229
221
|
}
|
|
230
222
|
|
|
231
223
|
userData.cases = userData.cases.filter((caseItem) => !casesToDelete.includes(caseItem.caseNumber));
|
|
@@ -234,9 +226,6 @@ export async function handleDeleteCases(
|
|
|
234
226
|
|
|
235
227
|
return createJsonResponse(userData, corsHeaders);
|
|
236
228
|
} catch {
|
|
237
|
-
return
|
|
238
|
-
status: 500,
|
|
239
|
-
headers: corsHeaders
|
|
240
|
-
});
|
|
229
|
+
return createTextResponse('Failed to delete cases', corsHeaders, 500);
|
|
241
230
|
}
|
|
242
231
|
}
|
|
@@ -13,10 +13,19 @@ import type { Env } from './types';
|
|
|
13
13
|
const corsHeaders: Record<string, string> = {
|
|
14
14
|
'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
|
|
15
15
|
'Access-Control-Allow-Methods': 'GET, PUT, DELETE, OPTIONS',
|
|
16
|
-
'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key'
|
|
17
|
-
'Content-Type': 'application/json'
|
|
16
|
+
'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key'
|
|
18
17
|
};
|
|
19
18
|
|
|
19
|
+
function createTextResponse(message: string, status: number, headers: Record<string, string>): Response {
|
|
20
|
+
return new Response(message, {
|
|
21
|
+
status,
|
|
22
|
+
headers: {
|
|
23
|
+
...headers,
|
|
24
|
+
'Content-Type': 'text/plain; charset=utf-8'
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
export default {
|
|
21
30
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
22
31
|
if (request.method === 'OPTIONS') {
|
|
@@ -39,7 +48,7 @@ export default {
|
|
|
39
48
|
const isCasesEndpoint = parts[2] === USER_CASES_SEGMENT;
|
|
40
49
|
|
|
41
50
|
if (!userUid) {
|
|
42
|
-
return
|
|
51
|
+
return createTextResponse('Not Found', 404, corsHeaders);
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
// Handle regular cases endpoint
|
|
@@ -47,10 +56,7 @@ export default {
|
|
|
47
56
|
switch (request.method) {
|
|
48
57
|
case 'PUT': return handleAddCases(request, env, userUid, corsHeaders);
|
|
49
58
|
case 'DELETE': return handleDeleteCases(request, env, userUid, corsHeaders);
|
|
50
|
-
default: return
|
|
51
|
-
status: 405,
|
|
52
|
-
headers: corsHeaders
|
|
53
|
-
});
|
|
59
|
+
default: return createTextResponse('Method not allowed', 405, corsHeaders);
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
|
|
@@ -64,31 +70,19 @@ export default {
|
|
|
64
70
|
case 'DELETE': return streamProgress
|
|
65
71
|
? handleDeleteUserWithProgress(env, userUid, corsHeaders)
|
|
66
72
|
: handleDeleteUser(env, userUid, corsHeaders);
|
|
67
|
-
default: return
|
|
68
|
-
status: 405,
|
|
69
|
-
headers: corsHeaders
|
|
70
|
-
});
|
|
73
|
+
default: return createTextResponse('Method not allowed', 405, corsHeaders);
|
|
71
74
|
}
|
|
72
75
|
} catch (error) {
|
|
73
76
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
74
77
|
if (errorMessage === 'Unauthorized') {
|
|
75
|
-
return
|
|
76
|
-
status: 403,
|
|
77
|
-
headers: corsHeaders
|
|
78
|
-
});
|
|
78
|
+
return createTextResponse('Forbidden', 403, corsHeaders);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
if (errorMessage === 'User KV encryption is not fully configured') {
|
|
82
|
-
return
|
|
83
|
-
status: 500,
|
|
84
|
-
headers: corsHeaders
|
|
85
|
-
});
|
|
82
|
+
return createTextResponse(errorMessage, 500, corsHeaders);
|
|
86
83
|
}
|
|
87
84
|
|
|
88
|
-
return
|
|
89
|
-
status: 500,
|
|
90
|
-
headers: corsHeaders
|
|
91
|
-
});
|
|
85
|
+
return createTextResponse('Internal Server Error', 500, corsHeaders);
|
|
92
86
|
}
|
|
93
87
|
}
|
|
94
88
|
};
|
package/wrangler.toml.example
CHANGED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
2
|
-
import type { Dispatch, SetStateAction } from 'react';
|
|
3
|
-
import type { User } from 'firebase/auth';
|
|
4
|
-
import { auditExportService } from '~/services/audit';
|
|
5
|
-
import type { AuditTrail, ValidationAuditEntry } from '~/types';
|
|
6
|
-
|
|
7
|
-
interface UseAuditViewerExportParams {
|
|
8
|
-
user: User | null;
|
|
9
|
-
effectiveCaseNumber?: string;
|
|
10
|
-
filteredEntries: ValidationAuditEntry[];
|
|
11
|
-
auditTrail: AuditTrail | null;
|
|
12
|
-
setError: Dispatch<SetStateAction<string>>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const useAuditViewerExport = ({
|
|
16
|
-
user,
|
|
17
|
-
effectiveCaseNumber,
|
|
18
|
-
filteredEntries,
|
|
19
|
-
auditTrail,
|
|
20
|
-
setError
|
|
21
|
-
}: UseAuditViewerExportParams) => {
|
|
22
|
-
const resolveExportContext = useCallback(() => {
|
|
23
|
-
if (!user) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const identifier = effectiveCaseNumber || user.uid;
|
|
28
|
-
const scopeType: 'case' | 'user' = effectiveCaseNumber ? 'case' : 'user';
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
identifier,
|
|
32
|
-
scopeType,
|
|
33
|
-
context: {
|
|
34
|
-
user,
|
|
35
|
-
scopeType,
|
|
36
|
-
scopeIdentifier: identifier,
|
|
37
|
-
caseNumber: effectiveCaseNumber || undefined
|
|
38
|
-
} as const
|
|
39
|
-
};
|
|
40
|
-
}, [user, effectiveCaseNumber]);
|
|
41
|
-
|
|
42
|
-
const handleExportCSV = useCallback(async () => {
|
|
43
|
-
const exportContextData = resolveExportContext();
|
|
44
|
-
if (!exportContextData) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const filename = auditExportService.generateFilename(
|
|
49
|
-
exportContextData.scopeType,
|
|
50
|
-
exportContextData.identifier,
|
|
51
|
-
'csv'
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
if (auditTrail && effectiveCaseNumber) {
|
|
56
|
-
await auditExportService.exportAuditTrailToCSV(auditTrail, filename, exportContextData.context);
|
|
57
|
-
} else {
|
|
58
|
-
await auditExportService.exportToCSV(filteredEntries, filename, exportContextData.context);
|
|
59
|
-
}
|
|
60
|
-
} catch (exportError) {
|
|
61
|
-
console.error('Export failed:', exportError);
|
|
62
|
-
setError('Failed to export audit trail to CSV');
|
|
63
|
-
}
|
|
64
|
-
}, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
|
|
65
|
-
|
|
66
|
-
const handleExportJSON = useCallback(async () => {
|
|
67
|
-
const exportContextData = resolveExportContext();
|
|
68
|
-
if (!exportContextData) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const filename = auditExportService.generateFilename(
|
|
73
|
-
exportContextData.scopeType,
|
|
74
|
-
exportContextData.identifier,
|
|
75
|
-
'json'
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
if (auditTrail && effectiveCaseNumber) {
|
|
80
|
-
await auditExportService.exportAuditTrailToJSON(auditTrail, filename, exportContextData.context);
|
|
81
|
-
} else {
|
|
82
|
-
await auditExportService.exportToJSON(filteredEntries, filename, exportContextData.context);
|
|
83
|
-
}
|
|
84
|
-
} catch (exportError) {
|
|
85
|
-
console.error('Export failed:', exportError);
|
|
86
|
-
setError('Failed to export audit trail to JSON');
|
|
87
|
-
}
|
|
88
|
-
}, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
|
|
89
|
-
|
|
90
|
-
const handleGenerateReport = useCallback(async () => {
|
|
91
|
-
const exportContextData = resolveExportContext();
|
|
92
|
-
if (!exportContextData) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const resolvedUser = exportContextData.context.user;
|
|
97
|
-
|
|
98
|
-
const filename = `${exportContextData.scopeType}-audit-report-${exportContextData.identifier}-${new Date().toISOString().split('T')[0]}.txt`;
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
let reportContent: string;
|
|
102
|
-
|
|
103
|
-
if (auditTrail && effectiveCaseNumber) {
|
|
104
|
-
reportContent = await auditExportService.generateReportSummary(auditTrail, exportContextData.context);
|
|
105
|
-
} else {
|
|
106
|
-
const totalEntries = filteredEntries.length;
|
|
107
|
-
const successfulActions = filteredEntries.filter(entry => entry.result === 'success').length;
|
|
108
|
-
const failedActions = filteredEntries.filter(entry => entry.result === 'failure').length;
|
|
109
|
-
|
|
110
|
-
const actionCounts = filteredEntries.reduce((accumulator, entry) => {
|
|
111
|
-
accumulator[entry.action] = (accumulator[entry.action] || 0) + 1;
|
|
112
|
-
return accumulator;
|
|
113
|
-
}, {} as Record<string, number>);
|
|
114
|
-
|
|
115
|
-
const detectedDateRange = filteredEntries.length > 0
|
|
116
|
-
? {
|
|
117
|
-
earliest: new Date(Math.min(...filteredEntries.map(entry => new Date(entry.timestamp).getTime()))),
|
|
118
|
-
latest: new Date(Math.max(...filteredEntries.map(entry => new Date(entry.timestamp).getTime())))
|
|
119
|
-
}
|
|
120
|
-
: null;
|
|
121
|
-
|
|
122
|
-
reportContent = `${effectiveCaseNumber ? 'CASE' : 'USER'} AUDIT REPORT
|
|
123
|
-
Generated: ${new Date().toISOString()}
|
|
124
|
-
${effectiveCaseNumber ? `Case: ${effectiveCaseNumber}` : `User: ${resolvedUser.email}`}
|
|
125
|
-
${effectiveCaseNumber ? '' : `User ID: ${resolvedUser.uid}`}
|
|
126
|
-
|
|
127
|
-
=== SUMMARY ===
|
|
128
|
-
Total Actions: ${totalEntries}
|
|
129
|
-
Successful: ${successfulActions}
|
|
130
|
-
Failed: ${failedActions}
|
|
131
|
-
Success Rate: ${totalEntries > 0 ? ((successfulActions / totalEntries) * 100).toFixed(1) : 0}%
|
|
132
|
-
|
|
133
|
-
${detectedDateRange ? `Date Range: ${detectedDateRange.earliest.toLocaleDateString()} - ${detectedDateRange.latest.toLocaleDateString()}` : 'No entries found'}
|
|
134
|
-
|
|
135
|
-
=== ACTION BREAKDOWN ===
|
|
136
|
-
${Object.entries(actionCounts)
|
|
137
|
-
.sort(([, actionCountA], [, actionCountB]) => actionCountB - actionCountA)
|
|
138
|
-
.map(([action, count]) => `${action}: ${count}`)
|
|
139
|
-
.join('\n')}
|
|
140
|
-
|
|
141
|
-
=== RECENT ACTIVITIES ===
|
|
142
|
-
${filteredEntries.slice(0, 10).map(entry =>
|
|
143
|
-
`${new Date(entry.timestamp).toLocaleString()} | ${entry.action} | ${entry.result}${entry.details.caseNumber ? ` | Case: ${entry.details.caseNumber}` : ''}`
|
|
144
|
-
).join('\n')}
|
|
145
|
-
|
|
146
|
-
Generated by Striae
|
|
147
|
-
`;
|
|
148
|
-
|
|
149
|
-
reportContent = await auditExportService.appendSignedReportIntegrity(
|
|
150
|
-
reportContent,
|
|
151
|
-
exportContextData.context,
|
|
152
|
-
totalEntries
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const blob = new Blob([reportContent], { type: 'text/plain' });
|
|
157
|
-
const url = URL.createObjectURL(blob);
|
|
158
|
-
const anchor = document.createElement('a');
|
|
159
|
-
anchor.href = url;
|
|
160
|
-
anchor.download = filename;
|
|
161
|
-
document.body.appendChild(anchor);
|
|
162
|
-
anchor.click();
|
|
163
|
-
document.body.removeChild(anchor);
|
|
164
|
-
URL.revokeObjectURL(url);
|
|
165
|
-
} catch (reportError) {
|
|
166
|
-
console.error('Report generation failed:', reportError);
|
|
167
|
-
setError('Failed to generate audit report');
|
|
168
|
-
}
|
|
169
|
-
}, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
handleExportCSV,
|
|
173
|
-
handleExportJSON,
|
|
174
|
-
handleGenerateReport
|
|
175
|
-
};
|
|
176
|
-
};
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { type ValidationAuditEntry } from '~/types';
|
|
2
|
-
|
|
3
|
-
export const AUDIT_CSV_ENTRY_HEADERS = [
|
|
4
|
-
'Timestamp',
|
|
5
|
-
'User Email',
|
|
6
|
-
'Action',
|
|
7
|
-
'Result',
|
|
8
|
-
'File Name',
|
|
9
|
-
'File Type',
|
|
10
|
-
'Case Number',
|
|
11
|
-
'Confirmation ID',
|
|
12
|
-
'Original Examiner UID',
|
|
13
|
-
'Reviewing Examiner UID',
|
|
14
|
-
'File ID',
|
|
15
|
-
'Original Filename',
|
|
16
|
-
'File Size (MB)',
|
|
17
|
-
'MIME Type',
|
|
18
|
-
'Upload Method',
|
|
19
|
-
'Delete Reason',
|
|
20
|
-
'Annotation ID',
|
|
21
|
-
'Annotation Type',
|
|
22
|
-
'Annotation Tool',
|
|
23
|
-
'Session ID',
|
|
24
|
-
'User Agent',
|
|
25
|
-
'Processing Time (ms)',
|
|
26
|
-
'Hash Valid',
|
|
27
|
-
'Validation Errors',
|
|
28
|
-
'Security Issues',
|
|
29
|
-
'Workflow Phase',
|
|
30
|
-
'Profile Field',
|
|
31
|
-
'Old Value',
|
|
32
|
-
'New Value',
|
|
33
|
-
'Badge/ID',
|
|
34
|
-
'Total Confirmations In File',
|
|
35
|
-
'Confirmations Successfully Imported',
|
|
36
|
-
'Validation Steps Failed',
|
|
37
|
-
'Case Name',
|
|
38
|
-
'Total Files',
|
|
39
|
-
'MFA Method',
|
|
40
|
-
'Security Incident Type',
|
|
41
|
-
'Security Severity',
|
|
42
|
-
'Confirmed Files'
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const formatForCSV = (value?: string | number | null): string => {
|
|
46
|
-
if (value === undefined || value === null) return '';
|
|
47
|
-
const str = String(value);
|
|
48
|
-
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
49
|
-
return `"${str.replace(/"/g, '""')}"`;
|
|
50
|
-
}
|
|
51
|
-
return str;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const getSecurityIssues = (entry: ValidationAuditEntry): string => {
|
|
55
|
-
const securityChecks = entry.details.securityChecks;
|
|
56
|
-
if (!securityChecks) {
|
|
57
|
-
return '';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const issues = [];
|
|
61
|
-
|
|
62
|
-
if (securityChecks.selfConfirmationPrevented === true) {
|
|
63
|
-
issues.push('selfConfirmationPrevented');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (securityChecks.fileIntegrityValid === false) {
|
|
67
|
-
issues.push('fileIntegrityValid');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (securityChecks.exporterUidValidated === false) {
|
|
71
|
-
issues.push('exporterUidValidated');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return issues.join('; ');
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const entryToCSVRow = (entry: ValidationAuditEntry): string => {
|
|
78
|
-
const fileDetails = entry.details.fileDetails;
|
|
79
|
-
const annotationDetails = entry.details.annotationDetails;
|
|
80
|
-
const sessionDetails = entry.details.sessionDetails;
|
|
81
|
-
const userProfileDetails = entry.details.userProfileDetails;
|
|
82
|
-
const caseDetails = entry.details.caseDetails;
|
|
83
|
-
const performanceMetrics = entry.details.performanceMetrics;
|
|
84
|
-
const securityDetails = entry.details.securityDetails;
|
|
85
|
-
const securityIssues = getSecurityIssues(entry);
|
|
86
|
-
|
|
87
|
-
const values = [
|
|
88
|
-
formatForCSV(entry.timestamp),
|
|
89
|
-
formatForCSV(entry.userEmail),
|
|
90
|
-
formatForCSV(entry.action),
|
|
91
|
-
formatForCSV(entry.result),
|
|
92
|
-
formatForCSV(entry.details.fileName),
|
|
93
|
-
formatForCSV(entry.details.fileType),
|
|
94
|
-
formatForCSV(entry.details.caseNumber),
|
|
95
|
-
formatForCSV(entry.details.confirmationId),
|
|
96
|
-
formatForCSV(entry.details.originalExaminerUid),
|
|
97
|
-
formatForCSV(entry.details.reviewingExaminerUid),
|
|
98
|
-
formatForCSV(fileDetails?.fileId),
|
|
99
|
-
formatForCSV(fileDetails?.originalFileName),
|
|
100
|
-
fileDetails?.fileSize ? (fileDetails.fileSize / 1024 / 1024).toFixed(2) : '',
|
|
101
|
-
formatForCSV(fileDetails?.mimeType),
|
|
102
|
-
formatForCSV(fileDetails?.uploadMethod),
|
|
103
|
-
formatForCSV(fileDetails?.deleteReason),
|
|
104
|
-
formatForCSV(annotationDetails?.annotationId),
|
|
105
|
-
formatForCSV(annotationDetails?.annotationType),
|
|
106
|
-
formatForCSV(annotationDetails?.tool),
|
|
107
|
-
formatForCSV(sessionDetails?.sessionId),
|
|
108
|
-
formatForCSV(sessionDetails?.userAgent),
|
|
109
|
-
performanceMetrics?.processingTimeMs || '',
|
|
110
|
-
entry.details.hashValid !== undefined ? (entry.details.hashValid ? 'Yes' : 'No') : '',
|
|
111
|
-
formatForCSV(entry.details.validationErrors?.join('; ')),
|
|
112
|
-
formatForCSV(securityIssues),
|
|
113
|
-
formatForCSV(entry.details.workflowPhase),
|
|
114
|
-
formatForCSV(userProfileDetails?.profileField),
|
|
115
|
-
formatForCSV(userProfileDetails?.oldValue),
|
|
116
|
-
formatForCSV(userProfileDetails?.newValue),
|
|
117
|
-
formatForCSV(userProfileDetails?.badgeId),
|
|
118
|
-
caseDetails?.totalAnnotations?.toString() || '',
|
|
119
|
-
performanceMetrics?.validationStepsCompleted?.toString() || '',
|
|
120
|
-
performanceMetrics?.validationStepsFailed?.toString() || '',
|
|
121
|
-
formatForCSV(caseDetails?.newCaseName || caseDetails?.oldCaseName),
|
|
122
|
-
caseDetails?.totalFiles?.toString() || '',
|
|
123
|
-
formatForCSV(securityDetails?.mfaMethod),
|
|
124
|
-
formatForCSV(securityDetails?.incidentType),
|
|
125
|
-
formatForCSV(securityDetails?.severity),
|
|
126
|
-
formatForCSV(caseDetails?.confirmedFileNames?.join('; '))
|
|
127
|
-
];
|
|
128
|
-
|
|
129
|
-
return values.join(',');
|
|
130
|
-
};
|