@striae-org/striae 5.4.1 → 5.4.3
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/confirmation-import.ts +11 -0
- package/app/components/actions/case-import/orchestrator.ts +1 -0
- package/app/components/actions/case-import/storage-operations.ts +2 -0
- package/app/components/actions/case-import/zip-processing.ts +3 -3
- package/app/components/actions/confirm-export.ts +6 -4
- package/app/components/canvas/confirmation/confirmation.tsx +4 -18
- 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/types/case.ts +1 -0
- package/app/types/import.ts +2 -0
- package/app/utils/data/permissions.ts +17 -15
- package/app/utils/forensics/audit-export-signature.ts +4 -4
- package/app/utils/forensics/confirmation-signature.ts +10 -0
- package/app/utils/forensics/export-verification.ts +3 -11
- package/package.json +2 -2
- package/worker-configuration.d.ts +4 -3
- 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 +15 -4
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- 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/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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"typescript": "^5.9.3",
|
|
129
129
|
"vite": "^6.4.1",
|
|
130
130
|
"vite-tsconfig-paths": "^6.1.1",
|
|
131
|
-
"wrangler": "^4.
|
|
131
|
+
"wrangler": "^4.80.0"
|
|
132
132
|
},
|
|
133
133
|
"overrides": {
|
|
134
134
|
"@tootallnate/once": "3.0.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
// Generated by Wrangler by running `wrangler types` (hash:
|
|
3
|
-
// Runtime types generated with workerd@1.20250823.0 2026-03-
|
|
2
|
+
// Generated by Wrangler by running `wrangler types` (hash: df3e4db815fe1724ed2abbb4f6b2065f)
|
|
3
|
+
// Runtime types generated with workerd@1.20250823.0 2026-03-31 nodejs_compat
|
|
4
4
|
declare namespace Cloudflare {
|
|
5
5
|
interface Env {
|
|
6
6
|
ACCOUNT_ID: string;
|
|
@@ -52,6 +52,7 @@ declare namespace Cloudflare {
|
|
|
52
52
|
IMAGES_WORKER_DOMAIN: string;
|
|
53
53
|
IMAGE_SIGNED_URL_SECRET: string;
|
|
54
54
|
IMAGE_SIGNED_URL_TTL_SECONDS: string;
|
|
55
|
+
IMAGE_SIGNED_URL_BASE_URL: string;
|
|
55
56
|
PDF_WORKER_NAME: string;
|
|
56
57
|
PDF_WORKER_DOMAIN: string;
|
|
57
58
|
PDF_WORKER_AUTH: string;
|
|
@@ -64,7 +65,7 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
|
|
|
64
65
|
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
|
|
65
66
|
};
|
|
66
67
|
declare namespace NodeJS {
|
|
67
|
-
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ACCOUNT_ID" | "USER_DB_AUTH" | "R2_KEY_SECRET" | "IMAGES_API_TOKEN" | "API_KEY" | "AUTH_DOMAIN" | "PROJECT_ID" | "STORAGE_BUCKET" | "MESSAGING_SENDER_ID" | "APP_ID" | "MEASUREMENT_ID" | "FIREBASE_SERVICE_ACCOUNT_EMAIL" | "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" | "USER_KV_ENCRYPTION_PRIVATE_KEY" | "USER_KV_ENCRYPTION_KEY_ID" | "USER_KV_ENCRYPTION_PUBLIC_KEY" | "USER_KV_WRITE_ENDPOINTS_ENABLED" | "USER_KV_ENCRYPTION_KEYS_JSON" | "USER_KV_ENCRYPTION_ACTIVE_KEY_ID" | "MANIFEST_SIGNING_PRIVATE_KEY" | "MANIFEST_SIGNING_KEY_ID" | "MANIFEST_SIGNING_PUBLIC_KEY" | "EXPORT_ENCRYPTION_PRIVATE_KEY" | "EXPORT_ENCRYPTION_KEY_ID" | "EXPORT_ENCRYPTION_PUBLIC_KEY" | "EXPORT_ENCRYPTION_KEYS_JSON" | "EXPORT_ENCRYPTION_ACTIVE_KEY_ID" | "DATA_AT_REST_ENCRYPTION_ENABLED" | "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" | "DATA_AT_REST_ENCRYPTION_KEY_ID" | "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" | "DATA_AT_REST_ENCRYPTION_KEYS_JSON" | "DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID" | "PAGES_PROJECT_NAME" | "PAGES_CUSTOM_DOMAIN" | "USER_WORKER_NAME" | "USER_WORKER_DOMAIN" | "KV_STORE_ID" | "DATA_WORKER_NAME" | "DATA_BUCKET_NAME" | "FILES_BUCKET_NAME" | "DATA_WORKER_DOMAIN" | "AUDIT_WORKER_NAME" | "AUDIT_BUCKET_NAME" | "AUDIT_WORKER_DOMAIN" | "IMAGES_WORKER_NAME" | "IMAGES_WORKER_DOMAIN" | "IMAGE_SIGNED_URL_SECRET" | "IMAGE_SIGNED_URL_TTL_SECONDS" | "PDF_WORKER_NAME" | "PDF_WORKER_DOMAIN" | "PDF_WORKER_AUTH" | "BROWSER_API_TOKEN" | "PRIMERSHEAR_EMAILS">> {}
|
|
68
|
+
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ACCOUNT_ID" | "USER_DB_AUTH" | "R2_KEY_SECRET" | "IMAGES_API_TOKEN" | "API_KEY" | "AUTH_DOMAIN" | "PROJECT_ID" | "STORAGE_BUCKET" | "MESSAGING_SENDER_ID" | "APP_ID" | "MEASUREMENT_ID" | "FIREBASE_SERVICE_ACCOUNT_EMAIL" | "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" | "USER_KV_ENCRYPTION_PRIVATE_KEY" | "USER_KV_ENCRYPTION_KEY_ID" | "USER_KV_ENCRYPTION_PUBLIC_KEY" | "USER_KV_WRITE_ENDPOINTS_ENABLED" | "USER_KV_ENCRYPTION_KEYS_JSON" | "USER_KV_ENCRYPTION_ACTIVE_KEY_ID" | "MANIFEST_SIGNING_PRIVATE_KEY" | "MANIFEST_SIGNING_KEY_ID" | "MANIFEST_SIGNING_PUBLIC_KEY" | "EXPORT_ENCRYPTION_PRIVATE_KEY" | "EXPORT_ENCRYPTION_KEY_ID" | "EXPORT_ENCRYPTION_PUBLIC_KEY" | "EXPORT_ENCRYPTION_KEYS_JSON" | "EXPORT_ENCRYPTION_ACTIVE_KEY_ID" | "DATA_AT_REST_ENCRYPTION_ENABLED" | "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" | "DATA_AT_REST_ENCRYPTION_KEY_ID" | "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" | "DATA_AT_REST_ENCRYPTION_KEYS_JSON" | "DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID" | "PAGES_PROJECT_NAME" | "PAGES_CUSTOM_DOMAIN" | "USER_WORKER_NAME" | "USER_WORKER_DOMAIN" | "KV_STORE_ID" | "DATA_WORKER_NAME" | "DATA_BUCKET_NAME" | "FILES_BUCKET_NAME" | "DATA_WORKER_DOMAIN" | "AUDIT_WORKER_NAME" | "AUDIT_BUCKET_NAME" | "AUDIT_WORKER_DOMAIN" | "IMAGES_WORKER_NAME" | "IMAGES_WORKER_DOMAIN" | "IMAGE_SIGNED_URL_SECRET" | "IMAGE_SIGNED_URL_TTL_SECONDS" | "IMAGE_SIGNED_URL_BASE_URL" | "PDF_WORKER_NAME" | "PDF_WORKER_DOMAIN" | "PDF_WORKER_AUTH" | "BROWSER_API_TOKEN" | "PRIMERSHEAR_EMAILS">> {}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// Begin runtime types
|
|
@@ -4,7 +4,7 @@ import type { CreateResponse, Env } from './types';
|
|
|
4
4
|
|
|
5
5
|
const corsHeaders: Record<string, string> = {
|
|
6
6
|
'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
|
|
7
|
-
'Access-Control-Allow-Methods': 'GET, POST,
|
|
7
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
8
8
|
'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key',
|
|
9
9
|
'Content-Type': 'application/json'
|
|
10
10
|
};
|
|
@@ -92,34 +92,5 @@ export async function handleAuditRequest(
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
if (!userId) {
|
|
97
|
-
return respond({ error: 'userId parameter is required' }, 400);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const prefix = `audit-trails/${userId}/`;
|
|
102
|
-
let deletedCount = 0;
|
|
103
|
-
let cursor: string | undefined;
|
|
104
|
-
|
|
105
|
-
do {
|
|
106
|
-
const listed = await bucket.list({ prefix, cursor, limit: 1000 });
|
|
107
|
-
|
|
108
|
-
const keys = listed.objects.map((obj) => obj.key);
|
|
109
|
-
if (keys.length > 0) {
|
|
110
|
-
await bucket.delete(keys);
|
|
111
|
-
deletedCount += keys.length;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
cursor = listed.truncated ? listed.cursor : undefined;
|
|
115
|
-
} while (cursor !== undefined);
|
|
116
|
-
|
|
117
|
-
return respond({ success: true, deletedCount });
|
|
118
|
-
} catch (error) {
|
|
119
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
120
|
-
return respond({ error: `Failed to delete audit entries: ${errorMessage}` }, 500);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return respond({ error: 'Method not allowed for audit endpoints. Only GET, POST, and DELETE are supported.' }, 405);
|
|
95
|
+
return respond({ error: 'Method not allowed for audit endpoints. Only GET and POST are supported.' }, 405);
|
|
125
96
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
2
|
+
"name": "data-worker",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"deploy": "wrangler deploy",
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"start": "wrangler dev"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@cloudflare/puppeteer": "^1.0.6",
|
|
12
|
+
"wrangler": "^4.80.0"
|
|
13
|
+
},
|
|
14
|
+
"overrides": {
|
|
15
|
+
"undici": "7.24.1",
|
|
16
|
+
"yauzl": "3.2.1"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -18,6 +18,7 @@ export interface ConfirmationSignatureMetadata {
|
|
|
18
18
|
version: string;
|
|
19
19
|
hash: string;
|
|
20
20
|
originalExportCreatedAt?: string;
|
|
21
|
+
originalCaseOwnerUid?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export interface ConfirmationRecord {
|
|
@@ -36,8 +37,8 @@ export interface ConfirmationSigningPayload {
|
|
|
36
37
|
confirmations: Record<string, ConfirmationRecord[]>;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export type AuditExportFormat = '
|
|
40
|
-
export type AuditExportType = '
|
|
40
|
+
export type AuditExportFormat = 'json';
|
|
41
|
+
export type AuditExportType = 'trail';
|
|
41
42
|
export type AuditExportScopeType = 'case' | 'user';
|
|
42
43
|
|
|
43
44
|
export interface AuditExportSigningPayload {
|
|
@@ -153,6 +154,13 @@ export function isValidConfirmationPayload(
|
|
|
153
154
|
return false;
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
if (
|
|
158
|
+
typeof metadata.originalCaseOwnerUid !== 'undefined' &&
|
|
159
|
+
(typeof metadata.originalCaseOwnerUid !== 'string' || metadata.originalCaseOwnerUid.trim().length === 0)
|
|
160
|
+
) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
156
164
|
for (const [imageId, confirmationList] of Object.entries(candidate.confirmations)) {
|
|
157
165
|
if (!imageId || !Array.isArray(confirmationList)) {
|
|
158
166
|
return false;
|
|
@@ -179,11 +187,11 @@ export function isValidAuditExportPayload(
|
|
|
179
187
|
return false;
|
|
180
188
|
}
|
|
181
189
|
|
|
182
|
-
if (candidate.exportFormat !== '
|
|
190
|
+
if (candidate.exportFormat !== 'json') {
|
|
183
191
|
return false;
|
|
184
192
|
}
|
|
185
193
|
|
|
186
|
-
if (candidate.exportType !== '
|
|
194
|
+
if (candidate.exportType !== 'trail') {
|
|
187
195
|
return false;
|
|
188
196
|
}
|
|
189
197
|
|
|
@@ -271,6 +279,9 @@ export function createConfirmationSigningPayload(confirmationData: ConfirmationS
|
|
|
271
279
|
hash: confirmationData.metadata.hash.toUpperCase(),
|
|
272
280
|
...(confirmationData.metadata.originalExportCreatedAt
|
|
273
281
|
? { originalExportCreatedAt: confirmationData.metadata.originalExportCreatedAt }
|
|
282
|
+
: {}),
|
|
283
|
+
...(confirmationData.metadata.originalCaseOwnerUid
|
|
284
|
+
? { originalCaseOwnerUid: confirmationData.metadata.originalCaseOwnerUid }
|
|
274
285
|
: {})
|
|
275
286
|
},
|
|
276
287
|
confirmations: normalizeConfirmations(confirmationData.confirmations)
|
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
|
-
};
|