@striae-org/striae 4.0.3 → 4.2.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 (118) hide show
  1. package/.env.example +8 -0
  2. package/app/components/actions/case-export/core-export.ts +14 -8
  3. package/app/components/actions/case-export/data-processing.ts +1 -0
  4. package/app/components/actions/case-export/download-handlers.ts +7 -0
  5. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  6. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  7. package/app/components/actions/case-import/orchestrator.ts +78 -32
  8. package/app/components/actions/case-import/storage-operations.ts +97 -8
  9. package/app/components/actions/case-import/zip-processing.ts +159 -86
  10. package/app/components/actions/case-manage.ts +430 -8
  11. package/app/components/actions/confirm-export.ts +13 -4
  12. package/app/components/actions/generate-pdf.ts +10 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +137 -945
  15. package/app/components/audit/user-audit.module.css +41 -0
  16. package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
  17. package/app/components/audit/viewer/audit-entries-list.tsx +207 -0
  18. package/app/components/audit/viewer/audit-filters-panel.tsx +307 -0
  19. package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
  20. package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
  21. package/app/components/audit/viewer/audit-viewer-utils.ts +123 -0
  22. package/app/components/audit/viewer/types.ts +1 -0
  23. package/app/components/audit/viewer/use-audit-viewer-data.ts +186 -0
  24. package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
  25. package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
  26. package/app/components/auth/mfa-enrollment.module.css +13 -5
  27. package/app/components/auth/mfa-verification.module.css +13 -5
  28. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  29. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  30. package/app/components/canvas/canvas.module.css +64 -54
  31. package/app/components/canvas/canvas.tsx +17 -16
  32. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  33. package/app/components/canvas/confirmation/confirmation.tsx +17 -47
  34. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  35. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  36. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  37. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  38. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  39. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  40. package/app/components/navbar/navbar.module.css +447 -0
  41. package/app/components/navbar/navbar.tsx +377 -0
  42. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +2 -0
  43. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +21 -51
  44. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  45. package/app/components/sidebar/case-export/case-export.tsx +14 -77
  46. package/app/components/sidebar/case-import/case-import.module.css +25 -0
  47. package/app/components/sidebar/case-import/case-import.tsx +64 -40
  48. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  49. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  50. package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
  51. package/app/components/sidebar/cases/cases-modal.module.css +45 -9
  52. package/app/components/sidebar/cases/cases-modal.tsx +16 -16
  53. package/app/components/sidebar/cases/cases.module.css +62 -21
  54. package/app/components/sidebar/files/files-modal.module.css +46 -10
  55. package/app/components/sidebar/files/files-modal.tsx +22 -23
  56. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  57. package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
  58. package/app/components/sidebar/notes/notes-modal.tsx +18 -17
  59. package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
  60. package/app/components/sidebar/notes/notes.module.css +155 -0
  61. package/app/components/sidebar/sidebar-container.tsx +15 -28
  62. package/app/components/sidebar/sidebar.module.css +7 -71
  63. package/app/components/sidebar/sidebar.tsx +24 -125
  64. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  65. package/app/components/toast/toast.module.css +2 -1
  66. package/app/components/toast/toast.tsx +16 -11
  67. package/app/components/user/delete-account.tsx +10 -31
  68. package/app/components/user/inactivity-warning.module.css +9 -6
  69. package/app/components/user/inactivity-warning.tsx +15 -2
  70. package/app/components/user/manage-profile.module.css +2 -0
  71. package/app/components/user/manage-profile.tsx +108 -40
  72. package/app/hooks/useOverlayDismiss.ts +116 -0
  73. package/app/routes/auth/login.example.tsx +19 -8
  74. package/app/routes/auth/login.tsx +785 -774
  75. package/app/routes/auth/passwordReset.module.css +23 -13
  76. package/app/routes/striae/striae.module.css +10 -3
  77. package/app/routes/striae/striae.tsx +477 -31
  78. package/app/routes.ts +7 -0
  79. package/app/services/audit/audit-export-csv.ts +2 -0
  80. package/app/services/audit/audit.service.ts +202 -32
  81. package/app/services/audit/builders/audit-entry-builder.ts +2 -1
  82. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  83. package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
  84. package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -0
  85. package/app/services/audit/builders/index.ts +1 -0
  86. package/app/types/audit.ts +5 -2
  87. package/app/types/case.ts +29 -0
  88. package/app/types/import.ts +3 -0
  89. package/app/types/user.ts +1 -0
  90. package/app/utils/data/permissions.ts +17 -1
  91. package/app/utils/forensics/audit-export-signature.ts +5 -1
  92. package/app/utils/forensics/confirmation-signature.ts +3 -0
  93. package/app/utils/forensics/export-verification.ts +497 -22
  94. package/functions/api/pdf/[[path]].ts +32 -1
  95. package/load-context.ts +9 -0
  96. package/package.json +6 -2
  97. package/primershear.emails.example +6 -0
  98. package/scripts/deploy-pages-secrets.sh +6 -0
  99. package/scripts/deploy-primershear-emails.sh +167 -0
  100. package/worker-configuration.d.ts +7493 -7491
  101. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  102. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  103. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  104. package/workers/data-worker/wrangler.jsonc.example +1 -1
  105. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  106. package/workers/image-worker/wrangler.jsonc.example +1 -1
  107. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  108. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  109. package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
  110. package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
  111. package/workers/pdf-worker/src/report-types.ts +3 -0
  112. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  113. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  114. package/workers/user-worker/src/user-worker.example.ts +6 -1
  115. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  116. package/workers/user-worker/wrangler.jsonc.example +1 -1
  117. package/wrangler.toml.example +1 -1
  118. package/public/.well-known/keybase.txt +0 -56
@@ -0,0 +1,377 @@
1
+ import { useEffect, useRef, useState, useContext } from 'react';
2
+ import styles from './navbar.module.css';
3
+ import { SignOut } from '../actions/signout';
4
+ import { ManageProfile } from '../user/manage-profile';
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
+ import { AuthContext } from '~/contexts/auth.context';
9
+ import { getUserData } from '~/utils/data';
10
+ import { type ImportResult, type ConfirmationImportResult } from '~/types';
11
+
12
+ interface NavbarProps {
13
+ isUploading?: boolean;
14
+ company?: string;
15
+ isReadOnly?: boolean;
16
+ currentCase?: string;
17
+ currentFileName?: string;
18
+ isCurrentImageConfirmed?: boolean;
19
+ hasLoadedCase?: boolean;
20
+ hasLoadedImage?: boolean;
21
+ onImportComplete?: (result: ImportResult | ConfirmationImportResult) => void;
22
+ onOpenCase?: () => void;
23
+ onOpenListAllCases?: () => void;
24
+ onOpenCaseExport?: () => void;
25
+ onOpenAuditTrail?: () => void;
26
+ onOpenRenameCase?: () => void;
27
+ onDeleteCase?: () => void;
28
+ onArchiveCase?: () => void;
29
+ onOpenViewAllFiles?: () => void;
30
+ onDeleteCurrentFile?: () => void;
31
+ onOpenImageNotes?: () => void;
32
+ archiveDetails?: {
33
+ archived: boolean;
34
+ archivedAt?: string;
35
+ archivedBy?: string;
36
+ archivedByDisplay?: string;
37
+ archiveReason?: string;
38
+ };
39
+ }
40
+
41
+ export const Navbar = ({
42
+ isUploading = false,
43
+ company,
44
+ isReadOnly = false,
45
+ currentCase,
46
+ currentFileName,
47
+ isCurrentImageConfirmed = false,
48
+ hasLoadedCase = false,
49
+ hasLoadedImage = false,
50
+ onImportComplete,
51
+ onOpenCase,
52
+ onOpenListAllCases,
53
+ onOpenCaseExport,
54
+ onOpenAuditTrail,
55
+ onOpenRenameCase,
56
+ onDeleteCase,
57
+ onArchiveCase,
58
+ onOpenViewAllFiles,
59
+ onDeleteCurrentFile,
60
+ onOpenImageNotes,
61
+ archiveDetails,
62
+ }: NavbarProps) => {
63
+ const { user } = useContext(AuthContext);
64
+ const [userBadgeId, setUserBadgeId] = useState<string>('');
65
+ const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
66
+ const [isImportModalOpen, setIsImportModalOpen] = useState(false);
67
+ const [isPublicKeyModalOpen, setIsPublicKeyModalOpen] = useState(false);
68
+ const [isCaseMenuOpen, setIsCaseMenuOpen] = useState(false);
69
+ const [isFileMenuOpen, setIsFileMenuOpen] = useState(false);
70
+ const { keyId: publicSigningKeyId, publicKeyPem } = getCurrentPublicSigningKeyDetails();
71
+ const caseMenuRef = useRef<HTMLDivElement>(null);
72
+ const fileMenuRef = useRef<HTMLDivElement>(null);
73
+
74
+ useEffect(() => {
75
+ const loadUserBadgeId = async () => {
76
+ if (user) {
77
+ try {
78
+ const userData = await getUserData(user);
79
+ if (userData?.badgeId) {
80
+ setUserBadgeId(userData.badgeId);
81
+ }
82
+ } catch (err) {
83
+ console.error('Failed to load user badge ID:', err);
84
+ }
85
+ }
86
+ };
87
+
88
+ loadUserBadgeId();
89
+ }, [user]);
90
+
91
+ useEffect(() => {
92
+ if (!isCaseMenuOpen && !isFileMenuOpen) {
93
+ return;
94
+ }
95
+
96
+ const handlePointerDown = (event: MouseEvent) => {
97
+ const targetNode = event.target as Node;
98
+ const clickedOutsideCaseMenu = !caseMenuRef.current?.contains(targetNode);
99
+ const clickedOutsideFileMenu = !fileMenuRef.current?.contains(targetNode);
100
+
101
+ if (clickedOutsideCaseMenu) {
102
+ setIsCaseMenuOpen(false);
103
+ }
104
+
105
+ if (clickedOutsideFileMenu) {
106
+ setIsFileMenuOpen(false);
107
+ }
108
+ };
109
+
110
+ document.addEventListener('mousedown', handlePointerDown);
111
+ return () => {
112
+ document.removeEventListener('mousedown', handlePointerDown);
113
+ };
114
+ }, [isCaseMenuOpen, isFileMenuOpen]);
115
+
116
+ const caseActionsDisabled = false;
117
+ const isCaseManagementActive = true;
118
+ const isFileManagementActive = isFileMenuOpen || hasLoadedImage;
119
+ const canOpenImageNotes = hasLoadedImage && !isCurrentImageConfirmed;
120
+ const isImageNotesActive = canOpenImageNotes;
121
+ const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
122
+
123
+ return (
124
+ <>
125
+ <header className={styles.navbar} aria-label="Canvas top navigation">
126
+ <div className={styles.companyLabelContainer}>
127
+ <div className={styles.companyLabel}>
128
+ {isReadOnly ? 'CASE REVIEW ONLY' : `${company}${user?.displayName ? ` | ${user.displayName}` : ''}${userBadgeId ? `, ${userBadgeId}` : ''}`}
129
+ </div>
130
+ </div>
131
+ <div className={styles.navCenterTrack}>
132
+ <div className={styles.navCentral}>
133
+ <div className={styles.caseMenuContainer} ref={caseMenuRef}>
134
+ <button
135
+ type="button"
136
+ className={`${styles.navSectionButton} ${isCaseManagementActive ? styles.navSectionButtonActive : ''}`}
137
+ aria-pressed={isCaseManagementActive}
138
+ aria-expanded={isCaseMenuOpen}
139
+ aria-haspopup="menu"
140
+ disabled={caseActionsDisabled}
141
+ onClick={() => setIsCaseMenuOpen((prev) => !prev)}
142
+ title={isUploading ? 'Cannot access case actions while uploading' : undefined}
143
+ >
144
+ Case Management
145
+ </button>
146
+ {isCaseMenuOpen && (
147
+ <div className={styles.caseMenu} role="menu" aria-label="Case Management actions">
148
+ <div className={styles.caseMenuSectionLabel}>Case Access</div>
149
+ <button
150
+ type="button"
151
+ role="menuitem"
152
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemOpen}`}
153
+ onClick={() => {
154
+ onOpenCase?.();
155
+ setIsCaseMenuOpen(false);
156
+ }}
157
+ >
158
+ Open Case
159
+ </button>
160
+ <button
161
+ type="button"
162
+ role="menuitem"
163
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemList}`}
164
+ onClick={() => {
165
+ onOpenListAllCases?.();
166
+ setIsCaseMenuOpen(false);
167
+ }}
168
+ >
169
+ List All Cases
170
+ </button>
171
+ <div className={styles.caseMenuSectionLabel}>Case Operations</div>
172
+ <button
173
+ type="button"
174
+ role="menuitem"
175
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemExport}`}
176
+ disabled={!hasLoadedCase}
177
+ title={!hasLoadedCase ? 'Load a case to export case data' : undefined}
178
+ onClick={() => {
179
+ onOpenCaseExport?.();
180
+ setIsCaseMenuOpen(false);
181
+ }}
182
+ >
183
+ Export Case Data
184
+ </button>
185
+ <button
186
+ type="button"
187
+ role="menuitem"
188
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemAudit}`}
189
+ disabled={!hasLoadedCase}
190
+ title={!hasLoadedCase ? 'Load a case to view audit trail' : undefined}
191
+ onClick={() => {
192
+ onOpenAuditTrail?.();
193
+ setIsCaseMenuOpen(false);
194
+ }}
195
+ >
196
+ Case Audit Trail
197
+ </button>
198
+ {(!isReadOnly || archiveDetails?.archived) && (
199
+ <div className={styles.caseMenuSectionLabel}>Maintenance</div>
200
+ )}
201
+ {!isReadOnly && (
202
+ <button
203
+ type="button"
204
+ role="menuitem"
205
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemRename}`}
206
+ disabled={!hasLoadedCase}
207
+ title={!hasLoadedCase ? 'Load a case to rename it' : undefined}
208
+ onClick={() => {
209
+ onOpenRenameCase?.();
210
+ setIsCaseMenuOpen(false);
211
+ }}
212
+ >
213
+ Rename Case
214
+ </button>
215
+ )}
216
+ {(!isReadOnly || archiveDetails?.archived) && (
217
+ <button
218
+ type="button"
219
+ role="menuitem"
220
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemDelete}`}
221
+ disabled={!hasLoadedCase}
222
+ title={!hasLoadedCase ? 'Load a case to delete it' : undefined}
223
+ onClick={() => {
224
+ onDeleteCase?.();
225
+ setIsCaseMenuOpen(false);
226
+ }}
227
+ >
228
+ Delete Case
229
+ </button>
230
+ )}
231
+ {!isReadOnly && (
232
+ <button
233
+ type="button"
234
+ role="menuitem"
235
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemArchive}`}
236
+ disabled={!hasLoadedCase}
237
+ title={!hasLoadedCase ? 'Load a case to archive it' : undefined}
238
+ onClick={() => {
239
+ onArchiveCase?.();
240
+ setIsCaseMenuOpen(false);
241
+ }}
242
+ >
243
+ Archive Case
244
+ </button>
245
+ )}
246
+ <div className={styles.caseMenuSectionLabel}>Verification</div>
247
+ <button
248
+ type="button"
249
+ role="menuitem"
250
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemKey}`}
251
+ onClick={() => {
252
+ setIsPublicKeyModalOpen(true);
253
+ setIsCaseMenuOpen(false);
254
+ }}
255
+ >
256
+ Verify Exports
257
+ </button>
258
+ {currentCase && (
259
+ <div className={styles.caseMenuCaption}>Case: {currentCase}</div>
260
+ )}
261
+ {archiveDetails?.archived && (
262
+ <div className={styles.caseArchiveDetails}>
263
+ <strong>Archived Case</strong>
264
+ <span>Archived At: {archiveDetails.archivedAt ? new Date(archiveDetails.archivedAt).toLocaleString() : 'Unknown'}</span>
265
+ <span>
266
+ Archived By (Name, ID): {archiveDetails.archivedByDisplay || archiveDetails.archivedBy || 'Unknown'}
267
+ {archiveDetails.archivedByDisplay && archiveDetails.archivedBy ? ` (${archiveDetails.archivedBy})` : ''}
268
+ </span>
269
+ <span>Reason: {archiveDetails.archiveReason || 'Not provided'}</span>
270
+ </div>
271
+ )}
272
+ </div>
273
+ )}
274
+ </div>
275
+ <div className={styles.fileMenuContainer} ref={fileMenuRef}>
276
+ <button
277
+ type="button"
278
+ className={`${styles.navSectionButton} ${isFileManagementActive ? styles.navSectionButtonActive : ''}`}
279
+ disabled={!hasLoadedCase}
280
+ aria-pressed={isFileManagementActive}
281
+ aria-expanded={isFileMenuOpen}
282
+ aria-haspopup="menu"
283
+ onClick={() => setIsFileMenuOpen((prev) => !prev)}
284
+ title={!hasLoadedCase ? 'Load a case to enable file management' : undefined}
285
+ >
286
+ File Management
287
+ </button>
288
+ {isFileMenuOpen && (
289
+ <div className={styles.fileMenu} role="menu" aria-label="File Management actions">
290
+ <button
291
+ type="button"
292
+ role="menuitem"
293
+ className={`${styles.fileMenuItem} ${styles.fileMenuItemViewAll}`}
294
+ onClick={() => {
295
+ onOpenViewAllFiles?.();
296
+ setIsFileMenuOpen(false);
297
+ }}
298
+ >
299
+ View All Files
300
+ </button>
301
+ <div className={styles.fileMenuSectionLabel}>Selected File</div>
302
+ <button
303
+ type="button"
304
+ role="menuitem"
305
+ className={`${styles.fileMenuItem} ${styles.fileMenuItemDelete}`}
306
+ disabled={!canDeleteCurrentFile}
307
+ title={!hasLoadedImage ? 'Load an image to delete the selected file' : isReadOnly ? 'Cannot delete files for read-only cases' : undefined}
308
+ onClick={() => {
309
+ onDeleteCurrentFile?.();
310
+ setIsFileMenuOpen(false);
311
+ }}
312
+ >
313
+ Delete File
314
+ </button>
315
+ <div
316
+ className={styles.fileMenuCaption}
317
+ title={hasLoadedImage && currentFileName ? currentFileName : 'No file loaded'}
318
+ >
319
+ File: {hasLoadedImage && currentFileName ? currentFileName : 'No file loaded'}
320
+ </div>
321
+ </div>
322
+ )}
323
+ </div>
324
+ <button
325
+ type="button"
326
+ className={`${styles.navSectionButton} ${isImageNotesActive ? styles.navSectionButtonActive : ''}`}
327
+ disabled={!canOpenImageNotes}
328
+ aria-pressed={isImageNotesActive}
329
+ title={!hasLoadedImage ? 'Load an image to enable image notes' : isCurrentImageConfirmed ? 'Confirmed images are read-only and viewable via toolbar only' : undefined}
330
+ onClick={() => {
331
+ onOpenImageNotes?.();
332
+ }}
333
+ >
334
+ Image Notes
335
+ </button>
336
+ <button
337
+ type="button"
338
+ onClick={() => setIsImportModalOpen(true)}
339
+ className={`${styles.navSectionButton} ${styles.navPrimaryButton}`}
340
+ disabled={isUploading}
341
+ title={isUploading ? 'Cannot import while uploading files' : undefined}
342
+ >
343
+ Import Case/Confirmations
344
+ </button>
345
+ </div>
346
+ </div>
347
+ <div className={styles.navActions}>
348
+ <button
349
+ type="button"
350
+ onClick={() => setIsProfileModalOpen(true)}
351
+ className={styles.navTextButton}
352
+ disabled={isUploading}
353
+ title={isUploading ? 'Cannot manage profile while uploading files' : undefined}
354
+ >
355
+ Manage Profile
356
+ </button>
357
+ <SignOut disabled={isUploading} />
358
+ </div>
359
+ </header>
360
+ <CaseImport
361
+ isOpen={isImportModalOpen}
362
+ onClose={() => setIsImportModalOpen(false)}
363
+ onImportComplete={onImportComplete}
364
+ />
365
+ <ManageProfile
366
+ isOpen={isProfileModalOpen}
367
+ onClose={() => setIsProfileModalOpen(false)}
368
+ />
369
+ <PublicSigningKeyModal
370
+ isOpen={isPublicKeyModalOpen}
371
+ onClose={() => setIsPublicKeyModalOpen(false)}
372
+ publicSigningKeyId={publicSigningKeyId}
373
+ publicKeyPem={publicKeyPem}
374
+ />
375
+ </>
376
+ );
377
+ };
@@ -7,9 +7,11 @@
7
7
  align-items: center;
8
8
  z-index: var(--zIndex5);
9
9
  padding: var(--spaceL);
10
+ cursor: default;
10
11
  }
11
12
 
12
13
  .modal {
14
+ position: relative;
13
15
  width: 100%;
14
16
  max-width: 640px;
15
17
  max-height: 90vh;
@@ -5,10 +5,9 @@ import {
5
5
  useState,
6
6
  type ChangeEvent,
7
7
  type DragEvent,
8
- type KeyboardEvent,
9
- type MouseEvent
10
8
  } from 'react';
11
9
  import styles from './public-signing-key-modal.module.css';
10
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
12
11
  import { verifyExportFile } from '~/utils/forensics';
13
12
 
14
13
  const NO_PUBLIC_KEY_MESSAGE = 'No public signing key is configured for this environment.';
@@ -230,6 +229,13 @@ export const PublicSigningKeyModal = ({
230
229
  const publicSigningKeyTitleId = useId();
231
230
  const publicKeyInputId = useId();
232
231
  const exportFileInputId = useId();
232
+ const {
233
+ overlayProps,
234
+ getCloseButtonProps
235
+ } = useOverlayDismiss({
236
+ isOpen,
237
+ onClose
238
+ });
233
239
 
234
240
  useEffect(() => {
235
241
  if (!isOpen) {
@@ -242,45 +248,10 @@ export const PublicSigningKeyModal = ({
242
248
  }
243
249
  }, [isOpen]);
244
250
 
245
- useEffect(() => {
246
- if (!isOpen) {
247
- return;
248
- }
249
-
250
- const handleEscapeKey = (event: globalThis.KeyboardEvent) => {
251
- if (event.key === 'Escape') {
252
- onClose();
253
- }
254
- };
255
-
256
- document.addEventListener('keydown', handleEscapeKey);
257
-
258
- return () => {
259
- document.removeEventListener('keydown', handleEscapeKey);
260
- };
261
- }, [isOpen, onClose]);
262
-
263
251
  if (!isOpen) {
264
252
  return null;
265
253
  }
266
254
 
267
- const handleOverlayMouseDown = (event: MouseEvent<HTMLDivElement>) => {
268
- if (event.target === event.currentTarget) {
269
- onClose();
270
- }
271
- };
272
-
273
- const handleOverlayKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
274
- if (event.target !== event.currentTarget) {
275
- return;
276
- }
277
-
278
- if (event.key === 'Enter' || event.key === ' ') {
279
- event.preventDefault();
280
- onClose();
281
- }
282
- };
283
-
284
255
  const resetVerificationState = () => {
285
256
  setVerificationOutcome(null);
286
257
  };
@@ -332,7 +303,7 @@ export const PublicSigningKeyModal = ({
332
303
  const lowerName = file.name.toLowerCase();
333
304
 
334
305
  if (!lowerName.endsWith('.zip') && !lowerName.endsWith('.json')) {
335
- setExportFileError('Select a confirmation JSON/ZIP file or a case export ZIP file.');
306
+ setExportFileError('Select a confirmation JSON/ZIP file, standalone audit JSON export, or a case export ZIP file.');
336
307
  return;
337
308
  }
338
309
 
@@ -346,7 +317,7 @@ export const PublicSigningKeyModal = ({
346
317
  const hasExportFile = !!selectedExportFile;
347
318
 
348
319
  setKeyError(hasPublicKey ? '' : 'Select or download a public key PEM file first.');
349
- setExportFileError(hasExportFile ? '' : 'Select a confirmation JSON/ZIP file or a case export ZIP file.');
320
+ setExportFileError(hasExportFile ? '' : 'Select a confirmation JSON/ZIP file, standalone audit JSON export, or a case export ZIP file.');
350
321
 
351
322
  if (!hasPublicKey || !hasExportFile || !selectedPublicKey || !selectedExportFile) {
352
323
  return;
@@ -379,18 +350,19 @@ export const PublicSigningKeyModal = ({
379
350
  return lowerName.includes('confirmation-data-') ? 'Confirmation ZIP' : 'Case export ZIP';
380
351
  }
381
352
 
382
- return 'Confirmation JSON';
353
+ if (lowerName.includes('audit')) {
354
+ return 'Audit JSON';
355
+ }
356
+
357
+ return 'JSON export';
383
358
  })()} • ${formatFileSize(selectedExportFile.size)}`
384
359
  : undefined;
385
360
 
386
361
  return (
387
362
  <div
388
363
  className={styles.overlay}
389
- onMouseDown={handleOverlayMouseDown}
390
- onKeyDown={handleOverlayKeyDown}
391
- role="button"
392
- tabIndex={0}
393
364
  aria-label="Close public signing key dialog"
365
+ {...overlayProps}
394
366
  >
395
367
  <div
396
368
  className={styles.modal}
@@ -400,13 +372,11 @@ export const PublicSigningKeyModal = ({
400
372
  >
401
373
  <div className={styles.header}>
402
374
  <h3 id={publicSigningKeyTitleId} className={styles.title}>
403
- Striae Public Signing Key
375
+ Striae Verification Utility
404
376
  </h3>
405
377
  <button
406
- type="button"
407
378
  className={styles.closeButton}
408
- onClick={onClose}
409
- aria-label="Close public signing key dialog"
379
+ {...getCloseButtonProps({ ariaLabel: 'Close public signing key dialog' })}
410
380
  >
411
381
  &times;
412
382
  </button>
@@ -414,7 +384,7 @@ export const PublicSigningKeyModal = ({
414
384
 
415
385
  <div className={styles.content}>
416
386
  <p className={styles.description}>
417
- Drop a public key PEM file and a Striae confirmation JSON/ZIP or case export ZIP, then run
387
+ Drop a public key PEM file and a Striae confirmation JSON/ZIP, standalone audit JSON export, or case export ZIP, then run
418
388
  verification directly in the browser.
419
389
  </p>
420
390
 
@@ -451,8 +421,8 @@ export const PublicSigningKeyModal = ({
451
421
  inputId={exportFileInputId}
452
422
  label="2. Confirmation File or Export ZIP"
453
423
  accept=".json,.zip"
454
- emptyText="Drop a confirmation JSON/ZIP or case export ZIP here"
455
- helperText="Case exports use .zip. Confirmation exports can be .json or .zip."
424
+ emptyText="Drop a confirmation JSON/ZIP, audit JSON, or case export ZIP here"
425
+ helperText="Case exports use .zip. Confirmation exports can be .json or .zip. Audit exports are supported as standalone .json files."
456
426
  selectedFileName={selectedExportFile?.name}
457
427
  selectedDescription={selectedExportDescription}
458
428
  errorMessage={exportFileError}
@@ -11,6 +11,7 @@
11
11
  }
12
12
 
13
13
  .modal {
14
+ position: relative;
14
15
  background: var(--backgroundLight);
15
16
  border-radius: var(--spaceXS);
16
17
  width: 90%;