@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.
- 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 +13 -4
- package/app/components/actions/generate-pdf.ts +10 -2
- package/app/components/actions/image-manage.ts +77 -44
- package/app/components/audit/user-audit-viewer.tsx +137 -945
- package/app/components/audit/user-audit.module.css +41 -0
- package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +207 -0
- package/app/components/audit/viewer/audit-filters-panel.tsx +307 -0
- package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
- package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +123 -0
- package/app/components/audit/viewer/types.ts +1 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +186 -0
- package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
- package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
- package/app/components/auth/mfa-enrollment.module.css +13 -5
- package/app/components/auth/mfa-verification.module.css +13 -5
- 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 +17 -16
- package/app/components/canvas/confirmation/confirmation.module.css +1 -0
- package/app/components/canvas/confirmation/confirmation.tsx +17 -47
- 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 +2 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +21 -51
- package/app/components/sidebar/case-export/case-export.module.css +1 -0
- package/app/components/sidebar/case-export/case-export.tsx +14 -77
- package/app/components/sidebar/case-import/case-import.module.css +25 -0
- package/app/components/sidebar/case-import/case-import.tsx +64 -40
- 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 +45 -9
- package/app/components/sidebar/cases/cases-modal.tsx +16 -16
- package/app/components/sidebar/cases/cases.module.css +62 -21
- package/app/components/sidebar/files/files-modal.module.css +46 -10
- package/app/components/sidebar/files/files-modal.tsx +22 -23
- 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 +18 -17
- package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
- package/app/components/sidebar/notes/notes.module.css +155 -0
- package/app/components/sidebar/sidebar-container.tsx +15 -28
- package/app/components/sidebar/sidebar.module.css +7 -71
- package/app/components/sidebar/sidebar.tsx +24 -125
- package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
- package/app/components/toast/toast.module.css +2 -1
- package/app/components/toast/toast.tsx +16 -11
- package/app/components/user/delete-account.tsx +10 -31
- package/app/components/user/inactivity-warning.module.css +9 -6
- package/app/components/user/inactivity-warning.tsx +15 -2
- package/app/components/user/manage-profile.module.css +2 -0
- package/app/components/user/manage-profile.tsx +108 -40
- package/app/hooks/useOverlayDismiss.ts +116 -0
- package/app/routes/auth/login.example.tsx +19 -8
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/auth/passwordReset.module.css +23 -13
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +477 -31
- package/app/routes.ts +7 -0
- package/app/services/audit/audit-export-csv.ts +2 -0
- package/app/services/audit/audit.service.ts +202 -32
- package/app/services/audit/builders/audit-entry-builder.ts +2 -1
- package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
- package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
- package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -0
- package/app/services/audit/builders/index.ts +1 -0
- package/app/types/audit.ts +5 -2
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -0
- package/app/types/user.ts +1 -0
- package/app/utils/data/permissions.ts +17 -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/functions/api/pdf/[[path]].ts +32 -1
- package/load-context.ts +9 -0
- package/package.json +6 -2
- package/primershear.emails.example +6 -0
- package/scripts/deploy-pages-secrets.sh +6 -0
- package/scripts/deploy-primershear-emails.sh +167 -0
- package/worker-configuration.d.ts +7493 -7491
- 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/src/pdf-worker.example.ts +3 -0
- package/workers/pdf-worker/src/report-types.ts +3 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/user-worker.example.ts +6 -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
|
+
};
|
|
@@ -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
|
-
|
|
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
|
|
375
|
+
Striae Verification Utility
|
|
404
376
|
</h3>
|
|
405
377
|
<button
|
|
406
|
-
type="button"
|
|
407
378
|
className={styles.closeButton}
|
|
408
|
-
|
|
409
|
-
aria-label="Close public signing key dialog"
|
|
379
|
+
{...getCloseButtonProps({ ariaLabel: 'Close public signing key dialog' })}
|
|
410
380
|
>
|
|
411
381
|
×
|
|
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}
|