@striae-org/striae 3.0.5 → 3.1.0

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 (78) hide show
  1. package/app/components/actions/case-export/core-export.ts +1 -1
  2. package/app/components/actions/case-export/download-handlers.ts +10 -12
  3. package/app/components/actions/case-export/metadata-helpers.ts +1 -1
  4. package/app/components/actions/case-import/confirmation-import.ts +24 -9
  5. package/app/components/actions/case-import/orchestrator.ts +3 -4
  6. package/app/components/actions/case-import/validation.ts +3 -3
  7. package/app/components/actions/case-import/zip-processing.ts +12 -48
  8. package/app/components/actions/case-manage.ts +0 -1
  9. package/app/components/actions/confirm-export.ts +2 -2
  10. package/app/components/audit/user-audit-viewer.tsx +53 -15
  11. package/app/components/audit/user-audit.module.css +11 -4
  12. package/app/components/canvas/box-annotations/box-annotations.tsx +36 -7
  13. package/app/components/canvas/canvas.tsx +35 -24
  14. package/app/components/canvas/confirmation/confirmation.module.css +5 -2
  15. package/app/components/canvas/confirmation/confirmation.tsx +25 -8
  16. package/app/components/sidebar/case-export/case-export.module.css +194 -5
  17. package/app/components/sidebar/case-export/case-export.tsx +291 -11
  18. package/app/components/sidebar/case-import/case-import.module.css +9 -5
  19. package/app/components/sidebar/case-import/case-import.tsx +30 -7
  20. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +2 -2
  21. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
  22. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +1 -1
  23. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +34 -9
  24. package/app/components/sidebar/cases/case-sidebar.tsx +13 -13
  25. package/app/components/sidebar/cases/cases-modal.tsx +12 -2
  26. package/app/components/sidebar/files/files-modal.tsx +28 -8
  27. package/app/components/sidebar/sidebar.module.css +2 -3
  28. package/app/components/sidebar/sidebar.tsx +1 -16
  29. package/app/components/sidebar/upload/image-upload-zone.tsx +4 -4
  30. package/app/components/toolbar/toolbar-color-selector.tsx +3 -3
  31. package/app/components/toolbar/toolbar.tsx +19 -9
  32. package/app/components/user/delete-account.module.css +4 -1
  33. package/app/components/user/delete-account.tsx +22 -3
  34. package/app/components/user/manage-profile.tsx +0 -2
  35. package/app/entry.server.tsx +2 -3
  36. package/app/hooks/useInactivityTimeout.ts +5 -1
  37. package/app/routes/_index.tsx +1 -16
  38. package/app/routes/auth/emailVerification.tsx +1 -1
  39. package/app/routes/auth/route.ts +3 -12
  40. package/app/routes/striae/striae.tsx +1 -1
  41. package/app/services/audit.service.ts +29 -9
  42. package/app/tailwind.css +16 -1
  43. package/app/types/audit.ts +3 -3
  44. package/app/types/case.ts +1 -1
  45. package/app/types/import.ts +0 -2
  46. package/app/utils/SHA256.ts +3 -3
  47. package/app/utils/batch-operations.ts +6 -6
  48. package/app/utils/data-operations.ts +14 -7
  49. package/app/utils/permissions.ts +0 -2
  50. package/functions/[[path]].ts +0 -1
  51. package/package.json +1 -2
  52. package/public/assets/striae.jpg +0 -0
  53. package/scripts/run-eslint.cjs +14 -6
  54. package/worker-configuration.d.ts +2 -2
  55. package/workers/audit-worker/src/audit-worker.example.ts +9 -7
  56. package/workers/audit-worker/worker-configuration.d.ts +2 -2
  57. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  58. package/workers/data-worker/src/data-worker.example.ts +1 -1
  59. package/workers/data-worker/worker-configuration.d.ts +2 -2
  60. package/workers/data-worker/wrangler.jsonc.example +1 -1
  61. package/workers/image-worker/worker-configuration.d.ts +2 -2
  62. package/workers/image-worker/wrangler.jsonc.example +1 -1
  63. package/workers/keys-worker/worker-configuration.d.ts +2 -2
  64. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  65. package/workers/pdf-worker/src/pdf-worker.example.ts +3 -3
  66. package/workers/pdf-worker/worker-configuration.d.ts +2 -2
  67. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  68. package/workers/user-worker/src/user-worker.example.ts +10 -10
  69. package/workers/user-worker/worker-configuration.d.ts +2 -2
  70. package/workers/user-worker/wrangler.jsonc.example +1 -1
  71. package/wrangler.toml.example +1 -1
  72. package/app/components/sidebar/hash/hash-utility.module.css +0 -366
  73. package/app/components/sidebar/hash/hash-utility.tsx +0 -982
  74. package/app/routes/mobile-prevented/mobilePrevented.module.css +0 -47
  75. package/app/routes/mobile-prevented/mobilePrevented.tsx +0 -28
  76. package/app/routes/mobile-prevented/route.ts +0 -14
  77. package/app/utils/device-detection.ts +0 -5
  78. package/app/utils/html-sanitizer.ts +0 -80
@@ -278,7 +278,7 @@ export async function exportCaseData(
278
278
  latestAnnotationDate = annotations.updatedAt;
279
279
  }
280
280
  }
281
- } catch (error) {
281
+ } catch {
282
282
  // Continue without annotations for this file
283
283
  }
284
284
 
@@ -36,7 +36,7 @@ export async function downloadAllCasesAsJSON(user: User, exportData: AllCasesExp
36
36
 
37
37
  try {
38
38
  // Start audit workflow
39
- const workflowId = auditService.startWorkflow('all-cases');
39
+ auditService.startWorkflow('all-cases');
40
40
 
41
41
  const dataStr = JSON.stringify(exportData, null, 2);
42
42
 
@@ -120,7 +120,7 @@ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExpo
120
120
 
121
121
  try {
122
122
  // Start audit workflow
123
- const workflowId = auditService.startWorkflow('all-cases');
123
+ auditService.startWorkflow('all-cases');
124
124
 
125
125
  // Dynamic import of XLSX to avoid bundle size issues
126
126
  const XLSX = await import('xlsx');
@@ -207,7 +207,7 @@ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExpo
207
207
  XLSX.utils.book_append_sheet(workbook, summaryWorksheet, 'Summary');
208
208
 
209
209
  // Create a worksheet for each case
210
- exportData.cases.forEach((caseData, index) => {
210
+ exportData.cases.forEach((caseData) => {
211
211
  if (caseData.summary?.exportError) {
212
212
  // For failed cases, create a simple error sheet
213
213
  const errorData = [
@@ -231,7 +231,7 @@ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExpo
231
231
  const metadataRows = generateMetadataRows(caseData);
232
232
 
233
233
  // Create case details with headers
234
- const caseDetailsData = [
234
+ const caseDetailsData: Array<Array<string | number | boolean | null | undefined>> = [
235
235
  protectForensicData
236
236
  ? [`CASE DATA - ${caseData.metadata.caseNumber} - PROTECTED`]
237
237
  : [`Case ${caseData.metadata.caseNumber} - Detailed Export`],
@@ -245,7 +245,7 @@ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExpo
245
245
 
246
246
  // Add file data if available
247
247
  if (caseData.files && caseData.files.length > 0) {
248
- const fileRows: any[][] = [];
248
+ const fileRows: Array<Array<string | number | boolean | null | undefined>> = [];
249
249
 
250
250
  caseData.files.forEach(fileEntry => {
251
251
  const processedRows = processFileDataForTabular(fileEntry);
@@ -265,7 +265,7 @@ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExpo
265
265
  }
266
266
 
267
267
  // Clean sheet name for Excel compatibility
268
- const sheetName = `Case_${caseData.metadata.caseNumber}`.replace(/[\\\/\?\*\[\]]/g, '_').substring(0, 31);
268
+ const sheetName = `Case_${caseData.metadata.caseNumber}`.replace(/[\\/?*\x5B\x5D]/g, '_').substring(0, 31);
269
269
  XLSX.utils.book_append_sheet(workbook, caseWorksheet, sheetName);
270
270
  });
271
271
 
@@ -303,8 +303,6 @@ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExpo
303
303
  // Clean up
304
304
  window.URL.revokeObjectURL(url);
305
305
 
306
- const passwordInfo = protectForensicData && exportPassword ? ` (Password: ${exportPassword})` : '';
307
-
308
306
  // Log successful export audit event
309
307
  const endTime = Date.now();
310
308
  await auditService.logCaseExport(
@@ -366,7 +364,7 @@ export async function downloadCaseAsJSON(
366
364
 
367
365
  try {
368
366
  // Start audit workflow
369
- const workflowId = auditService.startWorkflow(exportData.metadata.caseNumber);
367
+ auditService.startWorkflow(exportData.metadata.caseNumber);
370
368
 
371
369
  const jsonContent = await generateJSONContent(exportData, options.includeUserInfo, options.protectForensicData);
372
370
  const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(jsonContent);
@@ -445,7 +443,7 @@ export async function downloadCaseAsCSV(
445
443
 
446
444
  try {
447
445
  // Start audit workflow
448
- const workflowId = auditService.startWorkflow(exportData.metadata.caseNumber);
446
+ auditService.startWorkflow(exportData.metadata.caseNumber);
449
447
 
450
448
  const csvContent = await generateCSVContent(exportData, options.protectForensicData);
451
449
  const dataUri = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent);
@@ -528,7 +526,7 @@ export async function downloadCaseAsZip(
528
526
 
529
527
  try {
530
528
  // Start audit workflow
531
- const workflowId = auditService.startWorkflow(caseNumber);
529
+ auditService.startWorkflow(caseNumber);
532
530
 
533
531
  onProgress?.(10);
534
532
 
@@ -856,7 +854,7 @@ async function generateJSONContent(
856
854
  includeUserInfo: boolean = true,
857
855
  protectForensicData: boolean = true
858
856
  ): Promise<string> {
859
- let jsonData = { ...exportData };
857
+ const jsonData = { ...exportData };
860
858
 
861
859
  // Remove sensitive user info if not included
862
860
  if (!includeUserInfo) {
@@ -71,7 +71,7 @@ export function generateRandomPassword(): string {
71
71
  /**
72
72
  * Protect Excel worksheet from editing
73
73
  */
74
- export function protectExcelWorksheet(worksheet: any, sheetPassword?: string): string {
74
+ export function protectExcelWorksheet(worksheet: Record<string, unknown>, sheetPassword?: string): string {
75
75
  // Generate random password if none provided
76
76
  const password = sheetPassword || generateRandomPassword();
77
77
 
@@ -8,6 +8,21 @@ import { auditService } from '~/services/audit.service';
8
8
 
9
9
  const DATA_WORKER_URL = paths.data_worker_url;
10
10
 
11
+ interface CaseDataFile {
12
+ id: string;
13
+ originalFilename?: string;
14
+ }
15
+
16
+ interface CaseDataResponse {
17
+ files?: CaseDataFile[];
18
+ originalImageIds?: Record<string, string>;
19
+ }
20
+
21
+ type AnnotationImportData = Record<string, unknown> & {
22
+ confirmationData?: unknown;
23
+ updatedAt?: string;
24
+ };
25
+
11
26
  /**
12
27
  * Import confirmation data from JSON file
13
28
  */
@@ -98,7 +113,7 @@ export async function importConfirmationData(
98
113
  throw new Error(`Failed to fetch case data: ${caseResponse.status}`);
99
114
  }
100
115
 
101
- const caseData = await caseResponse.json() as any; // Using any for flexibility with originalImageIds
116
+ const caseData = await caseResponse.json() as CaseDataResponse;
102
117
 
103
118
  // Build mapping from original image IDs to current image IDs
104
119
  const imageIdMapping = new Map<string, string>();
@@ -110,7 +125,7 @@ export async function importConfirmationData(
110
125
  }
111
126
  } else {
112
127
  // For regular cases, assume original IDs match current IDs
113
- for (const file of caseData.files) {
128
+ for (const file of caseData.files || []) {
114
129
  imageIdMapping.set(file.id, file.id);
115
130
  }
116
131
  }
@@ -128,7 +143,7 @@ export async function importConfirmationData(
128
143
  }
129
144
 
130
145
  // Get the original filename for user-friendly messages
131
- const currentFile = caseData.files.find((file: any) => file.id === currentImageId);
146
+ const currentFile = (caseData.files || []).find((file) => file.id === currentImageId);
132
147
  const displayFilename = currentFile?.originalFilename || currentImageId;
133
148
 
134
149
  // Get current annotation data for this image
@@ -139,22 +154,22 @@ export async function importConfirmationData(
139
154
  }
140
155
  });
141
156
 
142
- let annotationData = {};
157
+ let annotationData: AnnotationImportData = {};
143
158
  if (annotationResponse.ok) {
144
- annotationData = await annotationResponse.json();
159
+ annotationData = await annotationResponse.json() as AnnotationImportData;
145
160
  }
146
161
 
147
162
  // Check if confirmation data already exists
148
- if ((annotationData as any).confirmationData) {
163
+ if (annotationData.confirmationData) {
149
164
  result.warnings?.push(`Image ${displayFilename} already has confirmation data - skipping`);
150
165
  continue;
151
166
  }
152
167
 
153
168
  // Validate that annotations haven't been modified after original export
154
169
  const importedConfirmationData = confirmations.length > 0 ? confirmations[0] : null;
155
- if (importedConfirmationData && confirmationData.metadata.originalExportCreatedAt && (annotationData as any).updatedAt) {
170
+ if (importedConfirmationData && confirmationData.metadata.originalExportCreatedAt && annotationData.updatedAt) {
156
171
  const originalExportDate = new Date(confirmationData.metadata.originalExportCreatedAt);
157
- const annotationUpdatedAt = new Date((annotationData as any).updatedAt);
172
+ const annotationUpdatedAt = new Date(annotationData.updatedAt);
158
173
 
159
174
  if (annotationUpdatedAt > originalExportDate) {
160
175
  // Format timestamps in user's timezone
@@ -305,7 +320,7 @@ export async function importConfirmationData(
305
320
 
306
321
  // First, try to extract basic metadata for audit purposes (if file is parseable)
307
322
  try {
308
- const confirmationData: any = JSON.parse(await confirmationFile.text());
323
+ const confirmationData = JSON.parse(await confirmationFile.text()) as ConfirmationImportData;
309
324
  reviewingExaminerUidForAudit = confirmationData.metadata?.exportedByUid;
310
325
  totalConfirmationsForAudit = confirmationData.metadata?.totalConfirmations || 0;
311
326
  if (confirmationData.metadata?.signature) {
@@ -187,7 +187,7 @@ export async function importCaseForReview(
187
187
 
188
188
  if (!signatureResult.isValid) {
189
189
  throw new Error(
190
- `Manifest signature validation failed: ${signatureResult.error || 'Unknown signature error'}. Import cannot proceed.`
190
+ 'Manifest signature validation failed. Import cannot proceed.'
191
191
  );
192
192
  }
193
193
 
@@ -206,8 +206,7 @@ export async function importCaseForReview(
206
206
 
207
207
  if (!validation.isValid) {
208
208
  throw new Error(
209
- `Comprehensive integrity validation failed: ${validation.summary}. ` +
210
- `Errors: ${validation.errors.join(', ')}. Import cannot proceed.`
209
+ 'Comprehensive integrity validation failed. Import cannot proceed.'
211
210
  );
212
211
  }
213
212
 
@@ -215,7 +214,7 @@ export async function importCaseForReview(
215
214
  onProgress?.(
216
215
  'Complete integrity verified',
217
216
  18,
218
- `${validation.summary}. Signature verified${signatureKeyId ? ` (${signatureKeyId})` : ''}`
217
+ `Integrity validation passed. Signature verified${signatureKeyId ? ` (${signatureKeyId})` : ''}`
219
218
  );
220
219
 
221
220
  } else {
@@ -100,7 +100,7 @@ export async function validateConfirmationHash(jsonContent: string, expectedHash
100
100
  try {
101
101
  // Validate input parameters
102
102
  if (!expectedHash || typeof expectedHash !== 'string') {
103
- console.error('validateConfirmationHash: expectedHash is invalid:', expectedHash);
103
+ console.error('validateConfirmationHash: expected hash input is invalid');
104
104
  return false;
105
105
  }
106
106
 
@@ -126,8 +126,8 @@ export async function validateConfirmationHash(jsonContent: string, expectedHash
126
126
  }
127
127
 
128
128
  return actualHash.toUpperCase() === expectedHash.toUpperCase();
129
- } catch (error) {
130
- console.error('validateConfirmationHash: validation failed:', error);
129
+ } catch {
130
+ console.error('validateConfirmationHash: validation failed');
131
131
  return false;
132
132
  }
133
133
  }
@@ -43,35 +43,6 @@ function extractImageIdFromFilename(exportFilename: string): string | null {
43
43
  return filenameWithoutExt.substring(lastHyphenIndex + 1);
44
44
  }
45
45
 
46
- /**
47
- * Reconstruct original filename from export filename
48
- * Format: {originalFilename}-{id}.{extension} → {originalFilename}.{extension}
49
- * Example: "evidence-2b365c5e-0559-4d6a-564f-d40bf1770101.jpg" returns "evidence.jpg"
50
- */
51
- function reconstructOriginalFilename(exportFilename: string): string {
52
- const lastDotIndex = exportFilename.lastIndexOf('.');
53
- const extension = lastDotIndex === -1 ? '' : exportFilename.substring(lastDotIndex);
54
- const filenameWithoutExt = lastDotIndex === -1 ? exportFilename : exportFilename.substring(0, lastDotIndex);
55
-
56
- // UUID pattern: 8-4-4-4-12 (36 chars including hyphens)
57
- const uuidPattern = /^(.+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
58
- const match = filenameWithoutExt.match(uuidPattern);
59
-
60
- if (match) {
61
- return match[1] + extension; // Return the original filename part + extension
62
- }
63
-
64
- // Fallback: remove everything after the last hyphen
65
- const lastHyphenIndex = filenameWithoutExt.lastIndexOf('-');
66
-
67
- if (lastHyphenIndex === -1) {
68
- return exportFilename; // No hyphen found, return as-is (backward compatibility)
69
- }
70
-
71
- const originalBasename = filenameWithoutExt.substring(0, lastHyphenIndex);
72
- return originalBasename + extension;
73
- }
74
-
75
46
  /**
76
47
  * Preview case information from ZIP file without importing
77
48
  */
@@ -84,8 +55,6 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
84
55
  // First, validate hash if forensic metadata exists
85
56
  let hashValid: boolean | undefined = undefined;
86
57
  let hashError: string | undefined = undefined;
87
- let expectedHash: string | undefined = undefined;
88
- let actualHash: string | undefined = undefined;
89
58
  let validationDetails: CaseImportPreview['validationDetails'];
90
59
 
91
60
  // Find the main data file (JSON or CSV)
@@ -132,7 +101,7 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
132
101
  const manifestForValidation = extractForensicManifestData(forensicManifest);
133
102
  if (!manifestForValidation) {
134
103
  hashValid = false;
135
- hashError = 'Forensic manifest format is invalid or incomplete.';
104
+ hashError = 'Validation failed.';
136
105
 
137
106
  validationDetails = {
138
107
  hasForensicManifest: true,
@@ -143,8 +112,6 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
143
112
  integrityErrors: [hashError]
144
113
  };
145
114
  } else {
146
- expectedHash = manifestForValidation.manifestHash;
147
-
148
115
  // Extract image files for comprehensive validation
149
116
  const imageFiles: { [filename: string]: Blob } = {};
150
117
  const imagesFolder = zip.folder('images');
@@ -171,17 +138,16 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
171
138
  );
172
139
 
173
140
  hashValid = validation.isValid && signatureResult.isValid;
174
- actualHash = validation.manifestValid ? expectedHash : 'validation_failed';
175
-
141
+
176
142
  if (!hashValid) {
177
143
  const errorParts: string[] = [];
178
144
  if (!signatureResult.isValid) {
179
- errorParts.push(`Signature validation failed: ${signatureResult.error}`);
145
+ errorParts.push('Signature validation failed.');
180
146
  }
181
147
  if (!validation.isValid) {
182
- errorParts.push(`Comprehensive validation failed: ${validation.summary}. Errors: ${validation.errors.join(', ')}`);
148
+ errorParts.push('Integrity validation failed.');
183
149
  }
184
- hashError = errorParts.join(' ');
150
+ hashError = errorParts.length > 0 ? errorParts.join(' ') : 'Validation failed.';
185
151
  }
186
152
 
187
153
  // Capture detailed validation information
@@ -206,7 +172,7 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
206
172
  } else {
207
173
  // No forensic manifest found - cannot validate
208
174
  hashValid = false;
209
- hashError = 'No forensic manifest found. This case export does not support comprehensive integrity validation.';
175
+ hashError = 'Validation failed.';
210
176
 
211
177
  validationDetails = {
212
178
  hasForensicManifest: false,
@@ -215,8 +181,8 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
215
181
  integrityErrors: ['Export does not contain forensic manifest required for validation']
216
182
  };
217
183
  }
218
- } catch (error) {
219
- hashError = `Failed to validate forensic metadata: ${error instanceof Error ? error.message : 'Unknown error'}`;
184
+ } catch {
185
+ hashError = 'Validation failed.';
220
186
  hashValid = false;
221
187
 
222
188
  validationDetails = {
@@ -280,11 +246,9 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
280
246
  totalFiles,
281
247
  caseCreatedDate: caseData.metadata.caseCreatedDate,
282
248
  hasAnnotations: false, // We'll need to determine this during parsing if needed
283
- validationSummary: hashValid ? 'Validation successful' : (hashError || 'Validation failed'),
249
+ validationSummary: hashValid ? 'Validation passed' : 'Validation failed',
284
250
  hashValid,
285
251
  hashError,
286
- expectedHash,
287
- actualHash,
288
252
  validationDetails
289
253
  };
290
254
 
@@ -301,7 +265,7 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
301
265
  caseData: CaseExportData;
302
266
  imageFiles: { [filename: string]: Blob };
303
267
  imageIdMapping: { [exportFilename: string]: string }; // exportFilename -> originalImageId
304
- metadata?: any;
268
+ metadata?: Record<string, unknown>;
305
269
  cleanedContent?: string; // Add cleaned content for hash validation
306
270
  }> {
307
271
  // Dynamic import of JSZip to avoid bundle size issues
@@ -390,12 +354,12 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
390
354
  }
391
355
 
392
356
  // Extract forensic manifest if present
393
- let metadata: any = undefined;
357
+ let metadata: Record<string, unknown> | undefined;
394
358
  const manifestFile = zip.file('FORENSIC_MANIFEST.json');
395
359
 
396
360
  if (manifestFile) {
397
361
  const manifestContent = await manifestFile.async('text');
398
- metadata = { forensicManifest: JSON.parse(manifestContent) };
362
+ metadata = { forensicManifest: JSON.parse(manifestContent) as unknown };
399
363
  }
400
364
 
401
365
  return {
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
1
  import { User } from 'firebase/auth';
3
2
  import {
4
3
  canCreateCase,
@@ -218,7 +218,7 @@ export async function exportConfirmationData(
218
218
  }
219
219
 
220
220
  // Try to get the forensic manifest createdAt timestamp from the original case export
221
- let originalExportCreatedAt: string | undefined = forensicManifestCreatedAt;
221
+ const originalExportCreatedAt: string | undefined = forensicManifestCreatedAt;
222
222
 
223
223
  if (!originalExportCreatedAt) {
224
224
  console.warn(`No forensic manifest timestamp found for case ${caseNumber}. This case may have been imported before forensic linking was implemented, or the original export did not include a forensic manifest.`);
@@ -292,7 +292,7 @@ export async function exportConfirmationData(
292
292
  document.body.removeChild(a);
293
293
  URL.revokeObjectURL(url);
294
294
 
295
- console.log(`Confirmation data exported for case ${caseNumber} with hash ${hash.toUpperCase()}`);
295
+ console.log(`Confirmation data exported for case ${caseNumber}`);
296
296
 
297
297
  // Log successful confirmation export
298
298
  const endTime = Date.now();
@@ -1,11 +1,18 @@
1
- import { useState, useEffect, useContext } from 'react';
1
+ import { useState, useEffect, useContext, useCallback } from 'react';
2
2
  import { AuthContext } from '~/contexts/auth.context';
3
3
  import { auditService } from '~/services/audit.service';
4
4
  import { auditExportService } from '~/services/audit-export.service';
5
- import { ValidationAuditEntry, AuditAction, AuditResult, AuditTrail, UserData } from '~/types';
5
+ import { ValidationAuditEntry, AuditAction, AuditResult, AuditTrail, UserData, WorkflowPhase } from '~/types';
6
6
  import { getUserData } from '~/utils/permissions';
7
7
  import styles from './user-audit.module.css';
8
8
 
9
+ const isWorkflowPhase = (phase: unknown): phase is WorkflowPhase =>
10
+ phase === 'casework' ||
11
+ phase === 'case-export' ||
12
+ phase === 'case-import' ||
13
+ phase === 'confirmation' ||
14
+ phase === 'user-management';
15
+
9
16
  interface UserAuditViewerProps {
10
17
  isOpen: boolean;
11
18
  onClose: () => void;
@@ -30,13 +37,6 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
30
37
  const [customEndDateInput, setCustomEndDateInput] = useState<string>('');
31
38
  const [auditTrail, setAuditTrail] = useState<AuditTrail | null>(null);
32
39
 
33
- useEffect(() => {
34
- if (isOpen && user) {
35
- loadAuditData();
36
- loadUserData();
37
- }
38
- }, [isOpen, user, dateRange, customStartDate, customEndDate, filterCaseNumber, caseNumber]);
39
-
40
40
  useEffect(() => {
41
41
  const handleEscape = (event: KeyboardEvent) => {
42
42
  if (event.key === 'Escape' && isOpen) {
@@ -52,7 +52,7 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
52
52
  }
53
53
  }, [isOpen, onClose]);
54
54
 
55
- const loadUserData = async () => {
55
+ const loadUserData = useCallback(async () => {
56
56
  if (!user) return;
57
57
 
58
58
  try {
@@ -62,9 +62,9 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
62
62
  console.error('Failed to load user data:', error);
63
63
  // Don't set error state for user data failure, just log it
64
64
  }
65
- };
65
+ }, [user]);
66
66
 
67
- const loadAuditData = async () => {
67
+ const loadAuditData = useCallback(async () => {
68
68
  if (!user?.uid) return;
69
69
 
70
70
  setLoading(true);
@@ -137,7 +137,7 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
137
137
  warningEvents: entries.filter(e => e.result === 'warning').length,
138
138
  workflowPhases: [...new Set(entries
139
139
  .map(e => e.details.workflowPhase)
140
- .filter(Boolean))] as any[],
140
+ .filter(isWorkflowPhase))],
141
141
  participatingUsers: [...new Set(entries.map(e => e.userId))],
142
142
  startTimestamp: entries[entries.length - 1]?.timestamp || new Date().toISOString(),
143
143
  endTimestamp: entries[0]?.timestamp || new Date().toISOString(),
@@ -154,7 +154,21 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
154
154
  } finally {
155
155
  setLoading(false);
156
156
  }
157
- };
157
+ }, [
158
+ user,
159
+ dateRange,
160
+ customStartDate,
161
+ customEndDate,
162
+ caseNumber,
163
+ filterCaseNumber
164
+ ]);
165
+
166
+ useEffect(() => {
167
+ if (isOpen && user) {
168
+ loadAuditData();
169
+ loadUserData();
170
+ }
171
+ }, [isOpen, user, loadAuditData, loadUserData]);
158
172
 
159
173
  const handleApplyCaseFilter = () => {
160
174
  setFilterCaseNumber(caseNumberInput.trim());
@@ -449,10 +463,34 @@ Generated by Striae
449
463
  ).length;
450
464
  const loginSessions = auditEntries.filter(e => e.action === 'user-login').length;
451
465
 
466
+ const handleOverlayMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
467
+ if (event.target === event.currentTarget) {
468
+ onClose();
469
+ }
470
+ };
471
+
472
+ const handleOverlayKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
473
+ if (event.target !== event.currentTarget) {
474
+ return;
475
+ }
476
+
477
+ if (event.key === 'Enter' || event.key === ' ') {
478
+ event.preventDefault();
479
+ onClose();
480
+ }
481
+ };
482
+
452
483
  if (!isOpen) return null;
453
484
 
454
485
  return (
455
- <div className={styles.overlay} onClick={(e) => e.target === e.currentTarget && onClose()}>
486
+ <div
487
+ className={styles.overlay}
488
+ onMouseDown={handleOverlayMouseDown}
489
+ onKeyDown={handleOverlayKeyDown}
490
+ role="button"
491
+ tabIndex={0}
492
+ aria-label="Close audit trail dialog"
493
+ >
456
494
  <div className={styles.modal}>
457
495
  <div className={styles.header}>
458
496
  <h2 className={styles.title}>
@@ -12,6 +12,7 @@
12
12
  justify-content: center;
13
13
  z-index: 1000;
14
14
  padding: 20px;
15
+ cursor: default;
15
16
  }
16
17
 
17
18
  .modal {
@@ -24,6 +25,7 @@
24
25
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
25
26
  display: flex;
26
27
  flex-direction: column;
28
+ cursor: default;
27
29
  }
28
30
 
29
31
  .header {
@@ -100,7 +102,8 @@
100
102
  }
101
103
 
102
104
  /* Loading & Error States */
103
- .loading, .error {
105
+ .loading,
106
+ .error {
104
107
  text-align: center;
105
108
  padding: 40px 20px;
106
109
  color: var(--textBody);
@@ -117,8 +120,12 @@
117
120
  }
118
121
 
119
122
  @keyframes spin {
120
- 0% { transform: rotate(0deg); }
121
- 100% { transform: rotate(360deg); }
123
+ 0% {
124
+ transform: rotate(0deg);
125
+ }
126
+ 100% {
127
+ transform: rotate(360deg);
128
+ }
122
129
  }
123
130
 
124
131
  .error {
@@ -565,4 +572,4 @@
565
572
  padding: 32px 16px;
566
573
  color: var(--textLight);
567
574
  font-style: italic;
568
- }
575
+ }