@striae-org/striae 4.1.0 → 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.
- package/.env.example +8 -0
- package/app/components/actions/case-export/core-export.ts +14 -8
- package/app/components/actions/case-export/data-processing.ts +1 -0
- package/app/components/actions/case-export/download-handlers.ts +7 -0
- package/app/components/actions/case-export/metadata-helpers.ts +2 -1
- package/app/components/actions/case-import/confirmation-import.ts +12 -2
- package/app/components/actions/case-import/orchestrator.ts +78 -32
- package/app/components/actions/case-import/storage-operations.ts +97 -8
- package/app/components/actions/case-import/zip-processing.ts +159 -86
- package/app/components/actions/case-manage.ts +430 -8
- package/app/components/actions/confirm-export.ts +9 -2
- package/app/components/actions/image-manage.ts +77 -44
- package/app/components/audit/user-audit-viewer.tsx +19 -8
- package/app/components/audit/user-audit.module.css +21 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +7 -0
- package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +21 -1
- package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
- package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
- package/app/components/canvas/canvas.module.css +64 -54
- package/app/components/canvas/canvas.tsx +14 -16
- package/app/components/canvas/confirmation/confirmation.module.css +1 -0
- package/app/components/canvas/confirmation/confirmation.tsx +6 -12
- package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
- package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
- package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
- package/app/components/navbar/navbar.module.css +447 -0
- package/app/components/navbar/navbar.tsx +377 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
- package/app/components/sidebar/case-export/case-export.module.css +1 -0
- package/app/components/sidebar/case-export/case-export.tsx +8 -46
- package/app/components/sidebar/case-import/case-import.module.css +23 -0
- package/app/components/sidebar/case-import/case-import.tsx +64 -16
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
- package/app/components/sidebar/cases/cases-modal.module.css +1 -0
- package/app/components/sidebar/cases/cases-modal.tsx +6 -8
- package/app/components/sidebar/cases/cases.module.css +62 -21
- package/app/components/sidebar/files/files-modal.module.css +1 -0
- package/app/components/sidebar/files/files-modal.tsx +12 -13
- package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
- package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
- package/app/components/sidebar/notes/notes-modal.tsx +7 -8
- package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
- package/app/components/sidebar/notes/notes.module.css +153 -0
- package/app/components/sidebar/sidebar-container.tsx +15 -28
- package/app/components/sidebar/sidebar.module.css +5 -69
- package/app/components/sidebar/sidebar.tsx +24 -125
- package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
- package/app/components/user/inactivity-warning.module.css +1 -0
- package/app/components/user/inactivity-warning.tsx +15 -2
- package/app/components/user/manage-profile.tsx +23 -10
- package/app/hooks/useOverlayDismiss.ts +52 -4
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +469 -30
- package/app/services/audit/audit.service.ts +173 -27
- package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
- package/app/services/audit/builders/index.ts +1 -0
- package/app/types/audit.ts +3 -1
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -0
- package/app/utils/data/permissions.ts +16 -1
- package/app/utils/forensics/audit-export-signature.ts +5 -1
- package/app/utils/forensics/confirmation-signature.ts +3 -0
- package/app/utils/forensics/export-verification.ts +497 -22
- package/package.json +3 -3
- package/scripts/deploy-primershear-emails.sh +2 -1
- package/worker-configuration.d.ts +1 -1
- package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/worker-configuration.d.ts +7448 -11323
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +7447 -11322
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/worker-configuration.d.ts +7448 -11323
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- 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
|
+
};
|
|
@@ -230,8 +230,8 @@ export const PublicSigningKeyModal = ({
|
|
|
230
230
|
const publicKeyInputId = useId();
|
|
231
231
|
const exportFileInputId = useId();
|
|
232
232
|
const {
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
overlayProps,
|
|
234
|
+
getCloseButtonProps
|
|
235
235
|
} = useOverlayDismiss({
|
|
236
236
|
isOpen,
|
|
237
237
|
onClose
|
|
@@ -303,7 +303,7 @@ export const PublicSigningKeyModal = ({
|
|
|
303
303
|
const lowerName = file.name.toLowerCase();
|
|
304
304
|
|
|
305
305
|
if (!lowerName.endsWith('.zip') && !lowerName.endsWith('.json')) {
|
|
306
|
-
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.');
|
|
307
307
|
return;
|
|
308
308
|
}
|
|
309
309
|
|
|
@@ -317,7 +317,7 @@ export const PublicSigningKeyModal = ({
|
|
|
317
317
|
const hasExportFile = !!selectedExportFile;
|
|
318
318
|
|
|
319
319
|
setKeyError(hasPublicKey ? '' : 'Select or download a public key PEM file first.');
|
|
320
|
-
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.');
|
|
321
321
|
|
|
322
322
|
if (!hasPublicKey || !hasExportFile || !selectedPublicKey || !selectedExportFile) {
|
|
323
323
|
return;
|
|
@@ -350,18 +350,19 @@ export const PublicSigningKeyModal = ({
|
|
|
350
350
|
return lowerName.includes('confirmation-data-') ? 'Confirmation ZIP' : 'Case export ZIP';
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
|
|
353
|
+
if (lowerName.includes('audit')) {
|
|
354
|
+
return 'Audit JSON';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return 'JSON export';
|
|
354
358
|
})()} • ${formatFileSize(selectedExportFile.size)}`
|
|
355
359
|
: undefined;
|
|
356
360
|
|
|
357
361
|
return (
|
|
358
362
|
<div
|
|
359
363
|
className={styles.overlay}
|
|
360
|
-
onMouseDown={handleOverlayMouseDown}
|
|
361
|
-
onKeyDown={handleOverlayKeyDown}
|
|
362
|
-
role="button"
|
|
363
|
-
tabIndex={0}
|
|
364
364
|
aria-label="Close public signing key dialog"
|
|
365
|
+
{...overlayProps}
|
|
365
366
|
>
|
|
366
367
|
<div
|
|
367
368
|
className={styles.modal}
|
|
@@ -371,13 +372,11 @@ export const PublicSigningKeyModal = ({
|
|
|
371
372
|
>
|
|
372
373
|
<div className={styles.header}>
|
|
373
374
|
<h3 id={publicSigningKeyTitleId} className={styles.title}>
|
|
374
|
-
Striae
|
|
375
|
+
Striae Verification Utility
|
|
375
376
|
</h3>
|
|
376
377
|
<button
|
|
377
|
-
type="button"
|
|
378
378
|
className={styles.closeButton}
|
|
379
|
-
|
|
380
|
-
aria-label="Close public signing key dialog"
|
|
379
|
+
{...getCloseButtonProps({ ariaLabel: 'Close public signing key dialog' })}
|
|
381
380
|
>
|
|
382
381
|
×
|
|
383
382
|
</button>
|
|
@@ -385,7 +384,7 @@ export const PublicSigningKeyModal = ({
|
|
|
385
384
|
|
|
386
385
|
<div className={styles.content}>
|
|
387
386
|
<p className={styles.description}>
|
|
388
|
-
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
|
|
389
388
|
verification directly in the browser.
|
|
390
389
|
</p>
|
|
391
390
|
|
|
@@ -422,8 +421,8 @@ export const PublicSigningKeyModal = ({
|
|
|
422
421
|
inputId={exportFileInputId}
|
|
423
422
|
label="2. Confirmation File or Export ZIP"
|
|
424
423
|
accept=".json,.zip"
|
|
425
|
-
emptyText="Drop a confirmation JSON/ZIP or case export ZIP here"
|
|
426
|
-
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."
|
|
427
426
|
selectedFileName={selectedExportFile?.name}
|
|
428
427
|
selectedDescription={selectedExportDescription}
|
|
429
428
|
errorMessage={exportFileError}
|
|
@@ -2,8 +2,6 @@ import { useState, useEffect, useContext } from 'react';
|
|
|
2
2
|
import styles from './case-export.module.css';
|
|
3
3
|
import { AuthContext } from '~/contexts/auth.context';
|
|
4
4
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
5
|
-
import { PublicSigningKeyModal } from '~/components/public-signing-key-modal/public-signing-key-modal';
|
|
6
|
-
import { getCurrentPublicSigningKeyDetails } from '~/utils/forensics';
|
|
7
5
|
import { getCaseConfirmations, exportConfirmationData } from '../../actions/confirm-export';
|
|
8
6
|
|
|
9
7
|
export type ExportFormat = 'json' | 'csv';
|
|
@@ -35,15 +33,13 @@ export const CaseExport = ({
|
|
|
35
33
|
const [selectedFormat, setSelectedFormat] = useState<ExportFormat>('json');
|
|
36
34
|
const [includeImages, setIncludeImages] = useState(false);
|
|
37
35
|
const [hasConfirmationData, setHasConfirmationData] = useState(false);
|
|
38
|
-
const [isPublicKeyModalOpen, setIsPublicKeyModalOpen] = useState(false);
|
|
39
|
-
const { keyId: publicSigningKeyId, publicKeyPem } = getCurrentPublicSigningKeyDetails();
|
|
40
36
|
const {
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
requestClose,
|
|
38
|
+
overlayProps,
|
|
39
|
+
getCloseButtonProps
|
|
43
40
|
} = useOverlayDismiss({
|
|
44
41
|
isOpen,
|
|
45
42
|
onClose,
|
|
46
|
-
canDismiss: !isPublicKeyModalOpen
|
|
47
43
|
});
|
|
48
44
|
|
|
49
45
|
// Update caseNumber when currentCaseNumber prop changes
|
|
@@ -103,12 +99,6 @@ export const CaseExport = ({
|
|
|
103
99
|
}
|
|
104
100
|
}, [isReadOnly]);
|
|
105
101
|
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (!isOpen) {
|
|
108
|
-
setIsPublicKeyModalOpen(false);
|
|
109
|
-
}
|
|
110
|
-
}, [isOpen]);
|
|
111
|
-
|
|
112
102
|
if (!isOpen) return null;
|
|
113
103
|
|
|
114
104
|
const handleExport = async () => {
|
|
@@ -125,7 +115,7 @@ export const CaseExport = ({
|
|
|
125
115
|
await onExport(caseNumber.trim(), selectedFormat, includeImages, (progress, label) => {
|
|
126
116
|
setExportProgress({ current: progress, total: 100, caseName: label, mode: 'single' });
|
|
127
117
|
});
|
|
128
|
-
|
|
118
|
+
requestClose();
|
|
129
119
|
} catch (error) {
|
|
130
120
|
console.error('Export failed:', error);
|
|
131
121
|
setError(error instanceof Error ? error.message : 'Export failed. Please try again.');
|
|
@@ -144,7 +134,7 @@ export const CaseExport = ({
|
|
|
144
134
|
await onExportAll((current: number, total: number, caseName: string) => {
|
|
145
135
|
setExportProgress({ current, total, caseName });
|
|
146
136
|
}, selectedFormat);
|
|
147
|
-
|
|
137
|
+
requestClose();
|
|
148
138
|
} catch (error) {
|
|
149
139
|
console.error('Export all failed:', error);
|
|
150
140
|
setError(error instanceof Error ? error.message : 'Export all cases failed. Please try again.');
|
|
@@ -165,7 +155,7 @@ export const CaseExport = ({
|
|
|
165
155
|
|
|
166
156
|
try {
|
|
167
157
|
await exportConfirmationData(user, caseNumber.trim());
|
|
168
|
-
|
|
158
|
+
requestClose();
|
|
169
159
|
} catch (error) {
|
|
170
160
|
console.error('Confirmation export failed:', error);
|
|
171
161
|
setError(error instanceof Error ? error.message : 'Confirmation export failed. Please try again.');
|
|
@@ -177,20 +167,13 @@ export const CaseExport = ({
|
|
|
177
167
|
return (
|
|
178
168
|
<div
|
|
179
169
|
className={styles.overlay}
|
|
180
|
-
onMouseDown={handleOverlayMouseDown}
|
|
181
|
-
onKeyDown={handleOverlayKeyDown}
|
|
182
|
-
role="button"
|
|
183
|
-
tabIndex={0}
|
|
184
170
|
aria-label="Close case export dialog"
|
|
171
|
+
{...overlayProps}
|
|
185
172
|
>
|
|
186
173
|
<div className={styles.modal}>
|
|
187
174
|
<div className={styles.header}>
|
|
188
175
|
<h2 className={styles.title}>Export Case Data</h2>
|
|
189
|
-
<button
|
|
190
|
-
className={styles.closeButton}
|
|
191
|
-
onClick={onClose}
|
|
192
|
-
aria-label="Close modal"
|
|
193
|
-
>
|
|
176
|
+
<button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close case export dialog' })}>
|
|
194
177
|
×
|
|
195
178
|
</button>
|
|
196
179
|
</div>
|
|
@@ -313,20 +296,6 @@ export const CaseExport = ({
|
|
|
313
296
|
</div>
|
|
314
297
|
</div>
|
|
315
298
|
)}
|
|
316
|
-
|
|
317
|
-
<div className={styles.divider}>
|
|
318
|
-
<span>Verification</span>
|
|
319
|
-
</div>
|
|
320
|
-
|
|
321
|
-
<div className={styles.publicKeySection}>
|
|
322
|
-
<button
|
|
323
|
-
type="button"
|
|
324
|
-
className={styles.publicKeyButton}
|
|
325
|
-
onClick={() => setIsPublicKeyModalOpen(true)}
|
|
326
|
-
>
|
|
327
|
-
View Public Signing Key
|
|
328
|
-
</button>
|
|
329
|
-
</div>
|
|
330
299
|
|
|
331
300
|
{error && (
|
|
332
301
|
<div className={styles.error}>
|
|
@@ -336,13 +305,6 @@ export const CaseExport = ({
|
|
|
336
305
|
</div>
|
|
337
306
|
</div>
|
|
338
307
|
</div>
|
|
339
|
-
|
|
340
|
-
<PublicSigningKeyModal
|
|
341
|
-
isOpen={isPublicKeyModalOpen}
|
|
342
|
-
onClose={() => setIsPublicKeyModalOpen(false)}
|
|
343
|
-
publicSigningKeyId={publicSigningKeyId}
|
|
344
|
-
publicKeyPem={publicKeyPem}
|
|
345
|
-
/>
|
|
346
308
|
</div>
|
|
347
309
|
);
|
|
348
310
|
};
|
|
@@ -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%;
|
|
@@ -424,6 +425,28 @@
|
|
|
424
425
|
color: var(--textTitle);
|
|
425
426
|
}
|
|
426
427
|
|
|
428
|
+
.archivedImportNote {
|
|
429
|
+
margin-bottom: var(--spaceM);
|
|
430
|
+
padding: var(--spaceS) var(--spaceM);
|
|
431
|
+
border-radius: var(--spaceXS);
|
|
432
|
+
background: color-mix(in lab, var(--success) 10%, transparent);
|
|
433
|
+
border: 1px solid color-mix(in lab, var(--success) 25%, transparent);
|
|
434
|
+
color: color-mix(in lab, var(--success) 80%, var(--black));
|
|
435
|
+
font-size: var(--fontSizeBodyXS);
|
|
436
|
+
font-weight: var(--fontWeightMedium);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.archivedRegularCaseRiskNote {
|
|
440
|
+
margin-bottom: var(--spaceM);
|
|
441
|
+
padding: var(--spaceS) var(--spaceM);
|
|
442
|
+
border-radius: var(--spaceXS);
|
|
443
|
+
background: color-mix(in lab, var(--warning) 12%, transparent);
|
|
444
|
+
border: 1px solid color-mix(in lab, var(--warning) 30%, transparent);
|
|
445
|
+
color: color-mix(in lab, var(--warning) 85%, var(--black));
|
|
446
|
+
font-size: var(--fontSizeBodyXS);
|
|
447
|
+
font-weight: var(--fontWeightMedium);
|
|
448
|
+
}
|
|
449
|
+
|
|
427
450
|
/* Validation Section - Green/Red Based on Status */
|
|
428
451
|
.validationSection {
|
|
429
452
|
border-radius: var(--spaceXS);
|