@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.
Files changed (45) hide show
  1. package/app/components/actions/case-export/download-handlers.ts +1 -1
  2. package/app/components/actions/case-export/metadata-helpers.ts +2 -4
  3. package/app/components/actions/case-import/confirmation-import.ts +11 -0
  4. package/app/components/actions/case-import/orchestrator.ts +1 -0
  5. package/app/components/actions/case-import/storage-operations.ts +2 -0
  6. package/app/components/actions/case-import/zip-processing.ts +3 -3
  7. package/app/components/actions/confirm-export.ts +6 -4
  8. package/app/components/canvas/confirmation/confirmation.tsx +4 -18
  9. package/app/components/mobile-warning/mobile-warning.module.css +80 -0
  10. package/app/components/mobile-warning/mobile-warning.tsx +108 -0
  11. package/app/components/navbar/case-import/utils/file-validation.ts +1 -1
  12. package/app/config-example/config.json +2 -2
  13. package/app/root.tsx +2 -0
  14. package/app/services/audit/audit-file-type.ts +0 -1
  15. package/app/services/audit/audit.service.ts +1 -1
  16. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -6
  17. package/app/services/audit/index.ts +0 -1
  18. package/app/types/audit.ts +1 -1
  19. package/app/types/case.ts +1 -0
  20. package/app/types/import.ts +2 -0
  21. package/app/utils/data/permissions.ts +17 -15
  22. package/app/utils/forensics/audit-export-signature.ts +4 -4
  23. package/app/utils/forensics/confirmation-signature.ts +10 -0
  24. package/app/utils/forensics/export-verification.ts +3 -11
  25. package/package.json +2 -2
  26. package/worker-configuration.d.ts +4 -3
  27. package/workers/audit-worker/package.json +1 -1
  28. package/workers/audit-worker/src/audit-worker.example.ts +1 -1
  29. package/workers/audit-worker/src/handlers/audit-routes.ts +1 -30
  30. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  31. package/workers/data-worker/package.json +17 -17
  32. package/workers/data-worker/src/encryption-utils.ts +1 -1
  33. package/workers/data-worker/src/signing-payload-utils.ts +15 -4
  34. package/workers/data-worker/wrangler.jsonc.example +1 -1
  35. package/workers/image-worker/package.json +1 -1
  36. package/workers/image-worker/wrangler.jsonc.example +1 -1
  37. package/workers/pdf-worker/package.json +1 -1
  38. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  39. package/workers/user-worker/package.json +1 -1
  40. package/workers/user-worker/wrangler.jsonc.example +1 -1
  41. package/wrangler.toml.example +1 -1
  42. package/app/components/audit/viewer/use-audit-viewer-export.ts +0 -176
  43. package/app/services/audit/audit-export-csv.ts +0 -130
  44. package/app/services/audit/audit-export-report.ts +0 -205
  45. 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.1",
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.79.0"
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: d8f8f87d89a635e81e94aa31fb52008f)
3
- // Runtime types generated with workerd@1.20250823.0 2026-03-26 nodejs_compat
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
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "@cloudflare/puppeteer": "^1.0.6",
12
- "wrangler": "^4.78.0"
12
+ "wrangler": "^4.80.0"
13
13
  },
14
14
  "overrides": {
15
15
  "undici": "7.24.1",
@@ -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, DELETE, OPTIONS',
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
- if (request.method === 'DELETE') {
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
  }
@@ -7,7 +7,7 @@
7
7
  "name": "AUDIT_WORKER_NAME",
8
8
  "account_id": "ACCOUNT_ID",
9
9
  "main": "src/audit-worker.ts",
10
- "compatibility_date": "2026-03-31",
10
+ "compatibility_date": "2026-04-02",
11
11
  "compatibility_flags": [
12
12
  "nodejs_compat"
13
13
  ],
@@ -1,18 +1,18 @@
1
1
  {
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.78.0"
13
- },
14
- "overrides": {
15
- "undici": "7.24.1",
16
- "yauzl": "3.2.1"
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
+ }
@@ -224,7 +224,7 @@ export async function decryptJsonFromStorage(
224
224
  }
225
225
 
226
226
  /**
227
- * Decrypt data file (plaintext JSON/CSV)
227
+ * Decrypt data file (plaintext JSON)
228
228
  */
229
229
  export async function decryptExportData(
230
230
  encryptedDataBase64: string,
@@ -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 = 'csv' | 'json' | 'txt';
40
- export type AuditExportType = 'entries' | 'trail' | 'report';
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 !== 'csv' && candidate.exportFormat !== 'json' && candidate.exportFormat !== 'txt') {
190
+ if (candidate.exportFormat !== 'json') {
183
191
  return false;
184
192
  }
185
193
 
186
- if (candidate.exportType !== 'entries' && candidate.exportType !== 'trail' && candidate.exportType !== 'report') {
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)
@@ -5,7 +5,7 @@
5
5
  "name": "DATA_WORKER_NAME",
6
6
  "account_id": "ACCOUNT_ID",
7
7
  "main": "src/data-worker.ts",
8
- "compatibility_date": "2026-03-31",
8
+ "compatibility_date": "2026-04-02",
9
9
  "compatibility_flags": [
10
10
  "nodejs_compat"
11
11
  ],
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "@cloudflare/puppeteer": "^1.0.6",
12
- "wrangler": "^4.78.0"
12
+ "wrangler": "^4.80.0"
13
13
  },
14
14
  "overrides": {
15
15
  "undici": "7.24.1",
@@ -2,7 +2,7 @@
2
2
  "name": "IMAGES_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/image-worker.ts",
5
- "compatibility_date": "2026-03-31",
5
+ "compatibility_date": "2026-04-02",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "devDependencies": {
12
12
  "@cloudflare/puppeteer": "^1.0.6",
13
- "wrangler": "^4.78.0"
13
+ "wrangler": "^4.80.0"
14
14
  },
15
15
  "overrides": {
16
16
  "undici": "7.24.1",
@@ -2,7 +2,7 @@
2
2
  "name": "PDF_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
- "compatibility_date": "2026-03-31",
5
+ "compatibility_date": "2026-04-02",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "@cloudflare/puppeteer": "^1.0.6",
12
- "wrangler": "^4.78.0"
12
+ "wrangler": "^4.80.0"
13
13
  },
14
14
  "overrides": {
15
15
  "undici": "7.24.1",
@@ -2,7 +2,7 @@
2
2
  "name": "USER_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/user-worker.ts",
5
- "compatibility_date": "2026-03-31",
5
+ "compatibility_date": "2026-04-02",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  #:schema node_modules/wrangler/config-schema.json
2
2
  name = "PAGES_PROJECT_NAME"
3
- compatibility_date = "2026-03-31"
3
+ compatibility_date = "2026-04-02"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
  pages_build_output_dir = "./build/client"
6
6
 
@@ -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
- };