@striae-org/striae 4.3.3 → 5.0.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 (53) hide show
  1. package/.env.example +4 -0
  2. package/app/components/actions/case-export/download-handlers.ts +60 -4
  3. package/app/components/actions/case-import/confirmation-import.ts +50 -7
  4. package/app/components/actions/case-import/confirmation-package.ts +99 -22
  5. package/app/components/actions/case-import/orchestrator.ts +116 -13
  6. package/app/components/actions/case-import/validation.ts +171 -7
  7. package/app/components/actions/case-import/zip-processing.ts +224 -127
  8. package/app/components/actions/case-manage.ts +110 -10
  9. package/app/components/actions/confirm-export.ts +32 -3
  10. package/app/components/audit/user-audit.module.css +49 -0
  11. package/app/components/audit/viewer/audit-entries-list.tsx +130 -48
  12. package/app/components/navbar/navbar.module.css +0 -10
  13. package/app/components/navbar/navbar.tsx +0 -22
  14. package/app/components/sidebar/case-import/case-import.module.css +7 -131
  15. package/app/components/sidebar/case-import/case-import.tsx +7 -14
  16. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +17 -60
  17. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +23 -39
  18. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +5 -45
  19. package/app/components/sidebar/case-import/components/FileSelector.tsx +5 -6
  20. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +2 -48
  21. package/app/components/sidebar/case-import/utils/file-validation.ts +9 -21
  22. package/app/config-example/config.json +5 -0
  23. package/app/routes/auth/login.tsx +1 -1
  24. package/app/services/audit/audit-console-logger.ts +1 -1
  25. package/app/services/audit/audit-export-csv.ts +1 -1
  26. package/app/services/audit/audit-export-signing.ts +2 -2
  27. package/app/services/audit/audit-export.service.ts +1 -1
  28. package/app/services/audit/audit-worker-client.ts +1 -1
  29. package/app/services/audit/audit.service.ts +5 -75
  30. package/app/services/audit/builders/audit-event-builders-case-file.ts +3 -0
  31. package/app/services/audit/index.ts +2 -2
  32. package/app/types/audit.ts +8 -7
  33. package/app/utils/data/operations/signing-operations.ts +93 -0
  34. package/app/utils/data/operations/types.ts +6 -0
  35. package/app/utils/forensics/export-encryption.ts +316 -0
  36. package/app/utils/forensics/export-verification.ts +1 -409
  37. package/app/utils/forensics/index.ts +1 -0
  38. package/app/utils/ui/case-messages.ts +5 -2
  39. package/package.json +1 -1
  40. package/scripts/deploy-config.sh +97 -3
  41. package/scripts/deploy-worker-secrets.sh +1 -1
  42. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  43. package/workers/data-worker/src/data-worker.example.ts +130 -0
  44. package/workers/data-worker/src/encryption-utils.ts +125 -0
  45. package/workers/data-worker/worker-configuration.d.ts +1 -1
  46. package/workers/data-worker/wrangler.jsonc.example +2 -2
  47. package/workers/image-worker/wrangler.jsonc.example +1 -1
  48. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  49. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  50. package/workers/user-worker/wrangler.jsonc.example +1 -1
  51. package/wrangler.toml.example +1 -1
  52. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +0 -287
  53. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +0 -470
@@ -14,7 +14,7 @@ import {
14
14
  signForensicManifest,
15
15
  removeCaseConfirmationSummary
16
16
  } from '~/utils/data';
17
- import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData } from '~/types';
17
+ import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData, type ValidationAuditEntry } from '~/types';
18
18
  import { auditService } from '~/services/audit';
19
19
  import { fetchImageApi } from '~/utils/api';
20
20
  import { exportCaseData, formatDateForFilename } from '~/components/actions/case-export';
@@ -22,12 +22,14 @@ import { getImageUrl } from './image-manage';
22
22
  import {
23
23
  calculateSHA256Secure,
24
24
  createPublicSigningKeyFileName,
25
+ encryptExportDataWithAllImages,
25
26
  generateForensicManifestSecure,
27
+ getCurrentEncryptionPublicKeyDetails,
26
28
  getCurrentPublicSigningKeyDetails,
27
29
  getVerificationPublicKey,
28
30
  } from '~/utils/forensics';
29
31
  import { signAuditExport } from '~/services/audit/audit-export-signing';
30
- import { generateAuditSummary } from '~/services/audit/audit-query-helpers';
32
+ import { generateAuditSummary, sortAuditEntriesNewestFirst } from '~/services/audit/audit-query-helpers';
31
33
 
32
34
  /**
33
35
  * Delete a file without individual audit logging (for bulk operations)
@@ -393,15 +395,23 @@ export const renameCase = async (
393
395
  // 5) Delete old case number in user's KV entry
394
396
  await removeUserCase(user, oldCaseNumber);
395
397
 
396
- // Log successful case rename
398
+ // Log successful case rename under the original case number context
397
399
  const endTime = Date.now();
398
400
  await auditService.logCaseRename(
399
401
  user,
400
- newCaseNumber, // Use new case number as the current context
402
+ oldCaseNumber,
401
403
  oldCaseNumber,
402
404
  newCaseNumber
403
405
  );
404
406
 
407
+ // Log creation of the new case number as a rename-derived case
408
+ await auditService.logCaseCreation(
409
+ user,
410
+ newCaseNumber,
411
+ newCaseNumber,
412
+ oldCaseNumber
413
+ );
414
+
405
415
  console.log(`✅ Case renamed: ${oldCaseNumber} → ${newCaseNumber} (${endTime - startTime}ms)`);
406
416
 
407
417
  } catch (error) {
@@ -808,11 +818,43 @@ export const archiveCase = async (
808
818
  startDate: caseData.createdAt,
809
819
  endDate: archivedAt,
810
820
  });
821
+
822
+ // Ensure the bundled archive trail includes the archival event itself.
823
+ const archiveAuditEntry: ValidationAuditEntry = {
824
+ timestamp: archivedAt,
825
+ userId: user.uid,
826
+ userEmail: user.email || '',
827
+ action: 'case-archive',
828
+ result: 'success',
829
+ details: {
830
+ fileName: `${caseNumber}.case`,
831
+ fileType: 'case-package',
832
+ validationErrors: [],
833
+ caseNumber,
834
+ workflowPhase: 'casework',
835
+ caseDetails: {
836
+ newCaseName: caseNumber,
837
+ archiveReason: archiveReason?.trim() || 'No reason provided',
838
+ totalFiles: archiveData.files?.length || 0,
839
+ lastModified: archivedAt,
840
+ },
841
+ performanceMetrics: {
842
+ processingTimeMs: Date.now() - startTime,
843
+ fileSizeBytes: 0,
844
+ },
845
+ },
846
+ };
847
+
848
+ const auditEntriesWithArchive = sortAuditEntriesNewestFirst([
849
+ ...auditEntries,
850
+ archiveAuditEntry,
851
+ ]);
852
+
811
853
  const auditTrail: AuditTrail = {
812
854
  caseNumber,
813
855
  workflowId: `${caseNumber}-archive-${Date.now()}`,
814
- entries: auditEntries,
815
- summary: generateAuditSummary(auditEntries),
856
+ entries: auditEntriesWithArchive,
857
+ summary: generateAuditSummary(auditEntriesWithArchive),
816
858
  };
817
859
 
818
860
  const auditTrailPayload = {
@@ -857,8 +899,64 @@ export const archiveCase = async (
857
899
  auditTrail,
858
900
  };
859
901
 
860
- zip.file('audit/case-audit-trail.json', JSON.stringify(signedAuditTrail, null, 2));
861
- zip.file('audit/case-audit-signature.json', JSON.stringify(signedAuditExportPayload, null, 2));
902
+ const auditTrailJson = JSON.stringify(signedAuditTrail, null, 2);
903
+ const auditSignatureJson = JSON.stringify(signedAuditExportPayload, null, 2);
904
+ zip.file('audit/case-audit-trail.json', auditTrailJson);
905
+ zip.file('audit/case-audit-signature.json', auditSignatureJson);
906
+
907
+ const encryptionKeyDetails = getCurrentEncryptionPublicKeyDetails();
908
+
909
+ if (!encryptionKeyDetails.publicKeyPem || !encryptionKeyDetails.keyId) {
910
+ throw new Error(
911
+ 'Archive encryption is mandatory. Your Striae instance does not have a configured encryption public key. ' +
912
+ 'Please contact your administrator to set up export encryption.'
913
+ );
914
+ }
915
+
916
+ try {
917
+ const filesToEncrypt: Array<{ filename: string; blob: Blob }> = [
918
+ ...Object.entries(imageBlobs).map(([filename, blob]) => ({
919
+ filename,
920
+ blob
921
+ })),
922
+ {
923
+ filename: 'audit/case-audit-trail.json',
924
+ blob: new Blob([auditTrailJson], { type: 'application/json' })
925
+ },
926
+ {
927
+ filename: 'audit/case-audit-signature.json',
928
+ blob: new Blob([auditSignatureJson], { type: 'application/json' })
929
+ }
930
+ ];
931
+
932
+ const encryptionResult = await encryptExportDataWithAllImages(
933
+ caseJsonContent,
934
+ filesToEncrypt,
935
+ encryptionKeyDetails.publicKeyPem,
936
+ encryptionKeyDetails.keyId
937
+ );
938
+
939
+ zip.file(`${caseNumber}_data.json`, encryptionResult.ciphertext);
940
+
941
+ for (let index = 0; index < filesToEncrypt.length; index += 1) {
942
+ const originalFilename = filesToEncrypt[index].filename;
943
+ const encryptedContent = encryptionResult.encryptedImages[index];
944
+
945
+ if (originalFilename.startsWith('audit/')) {
946
+ zip.file(originalFilename, encryptedContent);
947
+ continue;
948
+ }
949
+
950
+ if (imageFolder) {
951
+ imageFolder.file(originalFilename, encryptedContent);
952
+ }
953
+ }
954
+
955
+ zip.file('ENCRYPTION_MANIFEST.json', JSON.stringify(encryptionResult.encryptionManifest, null, 2));
956
+ } catch (error) {
957
+ console.error('Archive encryption failed:', error);
958
+ throw new Error(`Failed to encrypt archive package: ${error instanceof Error ? error.message : 'Unknown error'}`);
959
+ }
862
960
 
863
961
  zip.file(
864
962
  'README.txt',
@@ -873,12 +971,14 @@ export const archiveCase = async (
873
971
  '',
874
972
  'Package Contents',
875
973
  '- Case data JSON export with all image references',
876
- '- images/ folder with exported image files',
974
+ '- images/ folder with exported image files (encrypted)',
877
975
  '- Full case audit trail export and signed audit metadata',
878
976
  '- Forensic manifest with server-side signature',
977
+ '- ENCRYPTION_MANIFEST.json with encryption metadata and encrypted image hashes',
879
978
  `- ${publicKeyFileName} for verification`,
880
979
  '',
881
980
  'This package is intended for read-only review and verification workflows.',
981
+ 'This package is encrypted. Only Striae can decrypt and re-import it.',
882
982
  ].join('\n')
883
983
  );
884
984
 
@@ -910,7 +1010,7 @@ export const archiveCase = async (
910
1010
  );
911
1011
 
912
1012
  const downloadUrl = URL.createObjectURL(zipBlob);
913
- const archiveFileName = `striae-case-${caseNumber}-archive-${formatDateForFilename(new Date())}.zip`;
1013
+ const archiveFileName = `striae-case-${caseNumber}-archive-${formatDateForFilename(new Date())}-encrypted.zip`;
914
1014
  const anchor = document.createElement('a');
915
1015
  anchor.href = downloadUrl;
916
1016
  anchor.download = archiveFileName;
@@ -3,7 +3,9 @@ import {
3
3
  calculateSHA256Secure,
4
4
  createPublicSigningKeyFileName,
5
5
  getCurrentPublicSigningKeyDetails,
6
- getVerificationPublicKey
6
+ getVerificationPublicKey,
7
+ getCurrentEncryptionPublicKeyDetails,
8
+ encryptExportDataWithAllImages
7
9
  } from '~/utils/forensics';
8
10
  import { getUserData, getCaseData, updateCaseData, signConfirmationData, upsertFileConfirmationSummary } from '~/utils/data';
9
11
  import { type AnnotationData, type ConfirmationData, type CaseConfirmations, type CaseDataWithConfirmations, type ConfirmationImportData } from '~/types';
@@ -318,8 +320,35 @@ export async function exportConfirmationData(
318
320
  const zip = new JSZip();
319
321
  const normalizedPem = publicKeyPem.endsWith('\n') ? publicKeyPem : `${publicKeyPem}\n`;
320
322
 
321
- zip.file(confirmationFileName, finalJsonString);
323
+ const encKeyDetails = getCurrentEncryptionPublicKeyDetails();
324
+ if (!encKeyDetails.publicKeyPem || !encKeyDetails.keyId) {
325
+ throw new Error(
326
+ 'Confirmation export encryption is mandatory. Your Striae instance does not have a configured encryption public key. ' +
327
+ 'Please contact your administrator to set up export encryption.'
328
+ );
329
+ }
330
+
331
+ let encryptedConfirmationContent: string | Uint8Array;
332
+ let encryptionManifestJson: string;
333
+
334
+ try {
335
+ const encryptionResult = await encryptExportDataWithAllImages(
336
+ finalJsonString,
337
+ [],
338
+ encKeyDetails.publicKeyPem,
339
+ encKeyDetails.keyId
340
+ );
341
+
342
+ encryptedConfirmationContent = encryptionResult.ciphertext;
343
+ encryptionManifestJson = JSON.stringify(encryptionResult.encryptionManifest, null, 2);
344
+ } catch (error) {
345
+ console.error('Confirmation export encryption failed:', error);
346
+ throw new Error(`Failed to encrypt confirmation export: ${error instanceof Error ? error.message : 'Unknown error'}`);
347
+ }
348
+
349
+ zip.file(confirmationFileName, encryptedConfirmationContent);
322
350
  zip.file(publicKeyFileName, normalizedPem);
351
+ zip.file('ENCRYPTION_MANIFEST.json', encryptionManifestJson);
323
352
 
324
353
  const zipBlob = await zip.generateAsync({
325
354
  type: 'blob',
@@ -327,7 +356,7 @@ export async function exportConfirmationData(
327
356
  compressionOptions: { level: 6 }
328
357
  });
329
358
 
330
- const exportFileName = `confirmation-export-${caseNumber}-${timestampString}.zip`;
359
+ const exportFileName = `confirmation-export-${caseNumber}-${timestampString}-encrypted.zip`;
331
360
 
332
361
  // Create download
333
362
  const url = URL.createObjectURL(zipBlob);
@@ -522,11 +522,60 @@
522
522
  white-space: nowrap;
523
523
  }
524
524
 
525
+ .entryHeaderActions {
526
+ display: flex;
527
+ align-items: center;
528
+ gap: 8px;
529
+ margin-left: auto;
530
+ }
531
+
532
+ .entryDetailsToggle {
533
+ background: color-mix(in lab, var(--primary) 10%, transparent);
534
+ color: color-mix(in lab, var(--primary) 65%, var(--text));
535
+ border: 1px solid color-mix(in lab, var(--primary) 30%, transparent);
536
+ padding: 4px 8px;
537
+ border-radius: 999px;
538
+ font-size: 0.75rem;
539
+ font-weight: var(--fontWeightMedium);
540
+ cursor: pointer;
541
+ transition: background-color var(--durationS) var(--bezierFastoutSlowin);
542
+ }
543
+
544
+ .entryDetailsToggle:hover {
545
+ background: color-mix(in lab, var(--primary) 16%, transparent);
546
+ }
547
+
548
+ .entryDetailsToggle:focus-visible {
549
+ outline: 2px solid color-mix(in lab, var(--primary) 45%, transparent);
550
+ outline-offset: 2px;
551
+ }
552
+
525
553
  /* Entry Details */
526
554
  .entryDetails {
527
555
  padding: 12px 14px;
528
556
  }
529
557
 
558
+ .expandedDetails {
559
+ margin-top: 10px;
560
+ padding-top: 10px;
561
+ border-top: 1px dashed color-mix(in lab, var(--textLight) 25%, transparent);
562
+ }
563
+
564
+ .expandedDetailsCode {
565
+ margin: 4px 0 0;
566
+ padding: 10px;
567
+ border-radius: 6px;
568
+ border: 1px solid color-mix(in lab, var(--textLight) 20%, transparent);
569
+ background: color-mix(in lab, var(--backgroundLight) 75%, transparent);
570
+ color: var(--text);
571
+ font-size: 0.78rem;
572
+ line-height: 1.4;
573
+ white-space: pre-wrap;
574
+ word-break: break-word;
575
+ max-height: 280px;
576
+ overflow: auto;
577
+ }
578
+
530
579
  .detailRow {
531
580
  display: flex;
532
581
  align-items: center;
@@ -1,3 +1,4 @@
1
+ import { useMemo, useState, type MouseEvent } from 'react';
1
2
  import { type ValidationAuditEntry } from '~/types';
2
3
  import { formatAuditTimestamp, getAuditActionIcon, getAuditStatusIcon } from './audit-viewer-utils';
3
4
  import styles from '../user-audit.module.css';
@@ -13,7 +14,57 @@ const isConfirmationImportEntry = (entry: ValidationAuditEntry): boolean => {
13
14
  );
14
15
  };
15
16
 
17
+ const isConfirmationEvent = (entry: ValidationAuditEntry): boolean => {
18
+ return (
19
+ entry.action === 'confirmation-create' ||
20
+ entry.action === 'confirmation-export' ||
21
+ entry.action === 'confirmation-import' ||
22
+ entry.action === 'confirm' ||
23
+ (entry.action === 'import' && entry.details.workflowPhase === 'confirmation') ||
24
+ (entry.action === 'export' && entry.details.workflowPhase === 'confirmation')
25
+ );
26
+ };
27
+
28
+ const supportsFullDetailsToggle = (entry: ValidationAuditEntry): boolean => {
29
+ return (
30
+ entry.action === 'annotation-create' ||
31
+ entry.action === 'annotation-edit' ||
32
+ entry.action === 'annotation-delete' ||
33
+ isConfirmationEvent(entry)
34
+ );
35
+ };
36
+
37
+ const getEntryKey = (entry: ValidationAuditEntry): string => {
38
+ return `${entry.timestamp}-${entry.userId}-${entry.action}-${entry.details.fileName || ''}-${entry.details.confirmationId || ''}`;
39
+ };
40
+
16
41
  export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
42
+ const [expandedEntryKeys, setExpandedEntryKeys] = useState<Set<string>>(new Set());
43
+
44
+ const expandableEntries = useMemo(() => {
45
+ return new Set(entries.filter(supportsFullDetailsToggle).map(getEntryKey));
46
+ }, [entries]);
47
+
48
+ const toggleExpanded = (entryKey: string) => {
49
+ setExpandedEntryKeys((current) => {
50
+ const next = new Set(current);
51
+
52
+ if (next.has(entryKey)) {
53
+ next.delete(entryKey);
54
+ } else {
55
+ next.add(entryKey);
56
+ }
57
+
58
+ return next;
59
+ });
60
+ };
61
+
62
+ const handleToggleClick = (event: MouseEvent<HTMLButtonElement>, entryKey: string) => {
63
+ event.preventDefault();
64
+ event.stopPropagation();
65
+ toggleExpanded(entryKey);
66
+ };
67
+
17
68
  return (
18
69
  <div className={styles.entriesList}>
19
70
  <h3>Activity Log ({entries.length} entries)</h3>
@@ -22,30 +73,49 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
22
73
  <p>No activities match the current filters.</p>
23
74
  </div>
24
75
  ) : (
25
- entries.map((entry) => (
26
- <div
27
- key={`${entry.timestamp}-${entry.userId}-${entry.action}-${entry.details.fileName || ''}`}
28
- className={`${styles.entry} ${styles[entry.result]}`}
29
- >
30
- <div className={styles.entryHeader}>
31
- <div className={styles.entryIcons}>
32
- <span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
33
- <span className={styles.statusIcon}>{getAuditStatusIcon(entry.result)}</span>
34
- </div>
35
- <div className={styles.entryTitle}>
36
- <span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
37
- <span className={styles.fileName}>{entry.details.fileName}</span>
38
- </div>
39
- <div className={styles.entryTimestamp}>{formatAuditTimestamp(entry.timestamp)}</div>
40
- </div>
76
+ entries.map((entry) => {
77
+ const entryKey = getEntryKey(entry);
78
+ const isExpandable = expandableEntries.has(entryKey);
79
+ const isExpanded = expandedEntryKeys.has(entryKey);
41
80
 
42
- <div className={styles.entryDetails}>
43
- {entry.details.caseNumber && (
44
- <div className={styles.detailRow}>
45
- <span className={styles.detailLabel}>Case:</span>
46
- <span className={styles.detailValue}>{entry.details.caseNumber}</span>
81
+ return (
82
+ <div
83
+ key={entryKey}
84
+ className={`${styles.entry} ${styles[entry.result]}`}
85
+ >
86
+ <div className={styles.entryHeader}>
87
+ <div className={styles.entryIcons}>
88
+ <span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
89
+ <span className={styles.statusIcon}>{getAuditStatusIcon(entry.result)}</span>
47
90
  </div>
48
- )}
91
+ <div className={styles.entryTitle}>
92
+ <span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
93
+ <span className={styles.fileName}>{entry.details.fileName}</span>
94
+ </div>
95
+
96
+ <div className={styles.entryHeaderActions}>
97
+ <div className={styles.entryTimestamp}>{formatAuditTimestamp(entry.timestamp)}</div>
98
+ {isExpandable && (
99
+ <button
100
+ type="button"
101
+ className={styles.entryDetailsToggle}
102
+ aria-expanded={isExpanded}
103
+ aria-label={isExpanded ? 'Hide full entry details' : 'Show full entry details'}
104
+ onClick={(event) => handleToggleClick(event, entryKey)}
105
+ >
106
+ {isExpanded ? 'Hide details' : 'Show details'}
107
+ </button>
108
+ )}
109
+ </div>
110
+ </div>
111
+
112
+ <div className={styles.entryDetails}>
113
+ {entry.details.caseNumber && (
114
+ <div className={styles.detailRow}>
115
+ <span className={styles.detailLabel}>Case:</span>
116
+ <span className={styles.detailValue}>{entry.details.caseNumber}</span>
117
+ </div>
118
+ )}
49
119
 
50
120
  {entry.details.userProfileDetails?.badgeId && (
51
121
  <div className={styles.detailRow}>
@@ -191,37 +261,49 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
191
261
  </>
192
262
  )}
193
263
 
194
- {(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
195
- <>
196
- {entry.details.fileDetails.fileId && (
197
- <div className={styles.detailRow}>
198
- <span className={styles.detailLabel}>
199
- {entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}
200
- </span>
201
- <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
202
- </div>
203
- )}
264
+ {(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
265
+ <>
266
+ {entry.details.fileDetails.fileId && (
267
+ <div className={styles.detailRow}>
268
+ <span className={styles.detailLabel}>
269
+ {entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}
270
+ </span>
271
+ <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
272
+ </div>
273
+ )}
204
274
 
205
- {entry.details.fileDetails.originalFileName && (
206
- <div className={styles.detailRow}>
207
- <span className={styles.detailLabel}>
208
- {entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}
209
- </span>
210
- <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
211
- </div>
212
- )}
275
+ {entry.details.fileDetails.originalFileName && (
276
+ <div className={styles.detailRow}>
277
+ <span className={styles.detailLabel}>
278
+ {entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}
279
+ </span>
280
+ <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
281
+ </div>
282
+ )}
283
+
284
+ {entry.action === 'confirm' && entry.details.confirmationId && (
285
+ <div className={styles.detailRow}>
286
+ <span className={styles.detailLabel}>Confirmation ID:</span>
287
+ <span className={styles.detailValue}>{entry.details.confirmationId}</span>
288
+ </div>
289
+ )}
290
+ </>
291
+ )}
213
292
 
214
- {entry.action === 'confirm' && entry.details.confirmationId && (
293
+ {isExpandable && isExpanded && (
294
+ <div className={styles.expandedDetails}>
215
295
  <div className={styles.detailRow}>
216
- <span className={styles.detailLabel}>Confirmation ID:</span>
217
- <span className={styles.detailValue}>{entry.details.confirmationId}</span>
296
+ <span className={styles.detailLabel}>Full Entry Details:</span>
218
297
  </div>
219
- )}
220
- </>
221
- )}
298
+ <pre className={styles.expandedDetailsCode}>
299
+ {JSON.stringify(entry, null, 2)}
300
+ </pre>
301
+ </div>
302
+ )}
303
+ </div>
222
304
  </div>
223
- </div>
224
- ))
305
+ );
306
+ })
225
307
  )}
226
308
  </div>
227
309
  );
@@ -374,16 +374,6 @@
374
374
  background: color-mix(in lab, #6c757d 21%, #ffffff);
375
375
  }
376
376
 
377
- .caseMenuItemKey {
378
- background: color-mix(in lab, #0d9488 14%, #ffffff);
379
- color: #0f766e;
380
- border-color: color-mix(in lab, #0d9488 28%, transparent);
381
- }
382
-
383
- .caseMenuItemKey:hover {
384
- background: color-mix(in lab, #0d9488 20%, #ffffff);
385
- }
386
-
387
377
  .caseMenuItemClearRO {
388
378
  background: color-mix(in lab, #fd7e14 16%, #ffffff);
389
379
  color: #7c3f00;
@@ -3,8 +3,6 @@ import styles from './navbar.module.css';
3
3
  import { SignOut } from '../actions/signout';
4
4
  import { ManageProfile } from '../user/manage-profile';
5
5
  import { CaseImport } from '../sidebar/case-import/case-import';
6
- import { PublicSigningKeyModal } from '~/components/public-signing-key-modal/public-signing-key-modal';
7
- import { getCurrentPublicSigningKeyDetails } from '~/utils/forensics';
8
6
  import { AuthContext } from '~/contexts/auth.context';
9
7
  import { getUserData } from '~/utils/data';
10
8
  import { type ImportResult, type ConfirmationImportResult } from '~/types';
@@ -68,10 +66,8 @@ export const Navbar = ({
68
66
  const [userBadgeId, setUserBadgeId] = useState<string>('');
69
67
  const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
70
68
  const [isImportModalOpen, setIsImportModalOpen] = useState(false);
71
- const [isPublicKeyModalOpen, setIsPublicKeyModalOpen] = useState(false);
72
69
  const [isCaseMenuOpen, setIsCaseMenuOpen] = useState(false);
73
70
  const [isFileMenuOpen, setIsFileMenuOpen] = useState(false);
74
- const { keyId: publicSigningKeyId, publicKeyPem } = getCurrentPublicSigningKeyDetails();
75
71
  const caseMenuRef = useRef<HTMLDivElement>(null);
76
72
  const fileMenuRef = useRef<HTMLDivElement>(null);
77
73
 
@@ -292,18 +288,6 @@ export const Navbar = ({
292
288
  Archive Case
293
289
  </button>
294
290
  )}
295
- <div className={styles.caseMenuSectionLabel}>Verification</div>
296
- <button
297
- type="button"
298
- role="menuitem"
299
- className={`${styles.caseMenuItem} ${styles.caseMenuItemKey}`}
300
- onClick={() => {
301
- setIsPublicKeyModalOpen(true);
302
- setIsCaseMenuOpen(false);
303
- }}
304
- >
305
- Verify Exports
306
- </button>
307
291
  {currentCase && (
308
292
  <div className={styles.caseMenuCaption}>Case: {currentCase}</div>
309
293
  )}
@@ -423,12 +407,6 @@ export const Navbar = ({
423
407
  isOpen={isProfileModalOpen}
424
408
  onClose={() => setIsProfileModalOpen(false)}
425
409
  />
426
- <PublicSigningKeyModal
427
- isOpen={isPublicKeyModalOpen}
428
- onClose={() => setIsPublicKeyModalOpen(false)}
429
- publicSigningKeyId={publicSigningKeyId}
430
- publicKeyPem={publicKeyPem}
431
- />
432
410
  </>
433
411
  );
434
412
  };