@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
|
@@ -1,14 +1,63 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
2
|
import { type CaseExportData, type CaseImportPreview } from '~/types';
|
|
3
|
+
import { getCaseData } from '~/utils/data';
|
|
3
4
|
import { validateCaseNumber } from '../case-manage';
|
|
4
5
|
import {
|
|
5
|
-
extractForensicManifestData,
|
|
6
6
|
type SignedForensicManifest,
|
|
7
|
-
|
|
8
|
-
verifyForensicManifestSignature
|
|
7
|
+
verifyCasePackageIntegrity
|
|
9
8
|
} from '~/utils/forensics';
|
|
10
9
|
import { validateExporterUid, removeForensicWarning } from './validation';
|
|
11
10
|
|
|
11
|
+
function isArchivedExportData(parsedData: unknown): boolean {
|
|
12
|
+
if (!parsedData || typeof parsedData !== 'object') {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const root = parsedData as Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
if (root.archived === true) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof root.archivedAt === 'string' && root.archivedAt.trim().length > 0) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const metadata = root.metadata;
|
|
27
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const metadataRecord = metadata as Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
if (metadataRecord.archived === true) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof metadataRecord.archivedAt === 'string' && metadataRecord.archivedAt.trim().length > 0) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function allowSelfImportForArchivedCase(
|
|
45
|
+
currentUser: User,
|
|
46
|
+
caseNumber: string,
|
|
47
|
+
parsedData: unknown
|
|
48
|
+
): Promise<boolean> {
|
|
49
|
+
if (isArchivedExportData(parsedData)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const existingCase = await getCaseData(currentUser, caseNumber);
|
|
55
|
+
return existingCase?.archived === true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
12
61
|
function getLeafFileName(path: string): string {
|
|
13
62
|
const segments = path.split('/').filter(Boolean);
|
|
14
63
|
return segments.length > 0 ? segments[segments.length - 1] : path;
|
|
@@ -54,9 +103,11 @@ async function extractVerificationPublicKeyFromZip(
|
|
|
54
103
|
* a reasonable portion from the end.
|
|
55
104
|
*/
|
|
56
105
|
function extractImageIdFromFilename(exportFilename: string): string | null {
|
|
106
|
+
const leafFilename = getLeafFileName(exportFilename);
|
|
107
|
+
|
|
57
108
|
// Remove extension first
|
|
58
|
-
const lastDotIndex =
|
|
59
|
-
const filenameWithoutExt = lastDotIndex === -1 ?
|
|
109
|
+
const lastDotIndex = leafFilename.lastIndexOf('.');
|
|
110
|
+
const filenameWithoutExt = lastDotIndex === -1 ? leafFilename : leafFilename.substring(0, lastDotIndex);
|
|
60
111
|
|
|
61
112
|
// UUID pattern: 8-4-4-4-12 (36 chars including hyphens)
|
|
62
113
|
// Look for a pattern that matches this at the end
|
|
@@ -134,79 +185,72 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
|
|
|
134
185
|
forensicManifest = JSON.parse(manifestContent) as SignedForensicManifest;
|
|
135
186
|
|
|
136
187
|
if (forensicManifest) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
integrityErrors: [hashError]
|
|
149
|
-
};
|
|
150
|
-
} else {
|
|
151
|
-
// Extract image files for comprehensive validation
|
|
152
|
-
const imageFiles: { [filename: string]: Blob } = {};
|
|
153
|
-
const imagesFolder = zip.folder('images');
|
|
154
|
-
if (imagesFolder) {
|
|
155
|
-
await Promise.all(Object.keys(imagesFolder.files).map(async (path) => {
|
|
156
|
-
if (path.startsWith('images/') && !path.endsWith('/')) {
|
|
157
|
-
const filename = path.replace('images/', '');
|
|
158
|
-
const file = zip.file(path);
|
|
159
|
-
if (file) {
|
|
160
|
-
const blob = await file.async('blob');
|
|
161
|
-
imageFiles[filename] = blob;
|
|
162
|
-
}
|
|
188
|
+
// Extract image files for comprehensive validation
|
|
189
|
+
const imageFiles: { [filename: string]: Blob } = {};
|
|
190
|
+
const imagesFolder = zip.folder('images');
|
|
191
|
+
if (imagesFolder) {
|
|
192
|
+
await Promise.all(Object.keys(imagesFolder.files).map(async (path) => {
|
|
193
|
+
if (path.startsWith('images/') && !path.endsWith('/')) {
|
|
194
|
+
const filename = path.replace('images/', '');
|
|
195
|
+
const file = zip.file(path);
|
|
196
|
+
if (file) {
|
|
197
|
+
const blob = await file.async('blob');
|
|
198
|
+
imageFiles[filename] = blob;
|
|
163
199
|
}
|
|
164
|
-
}
|
|
200
|
+
}
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const casePackageResult = await verifyCasePackageIntegrity({
|
|
205
|
+
cleanedContent,
|
|
206
|
+
imageFiles,
|
|
207
|
+
forensicManifest,
|
|
208
|
+
verificationPublicKeyPem,
|
|
209
|
+
bundledAuditFiles: {
|
|
210
|
+
auditTrailContent: await zip.file('audit/case-audit-trail.json')?.async('text'),
|
|
211
|
+
auditSignatureContent: await zip.file('audit/case-audit-signature.json')?.async('text')
|
|
165
212
|
}
|
|
213
|
+
});
|
|
166
214
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
// Perform comprehensive validation
|
|
173
|
-
const validation = await validateForensicIntegrity(
|
|
174
|
-
cleanedContent,
|
|
175
|
-
imageFiles,
|
|
176
|
-
manifestForValidation
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
hashValid = validation.isValid && signatureResult.isValid;
|
|
215
|
+
const signatureResult = casePackageResult.signatureResult;
|
|
216
|
+
const validation = casePackageResult.integrityResult;
|
|
217
|
+
const bundledAuditVerification = casePackageResult.bundledAuditVerification;
|
|
180
218
|
|
|
181
|
-
|
|
182
|
-
const errorParts: string[] = [];
|
|
183
|
-
if (!signatureResult.isValid) {
|
|
184
|
-
errorParts.push('Signature validation failed.');
|
|
185
|
-
}
|
|
186
|
-
if (!validation.isValid) {
|
|
187
|
-
errorParts.push('Integrity validation failed.');
|
|
188
|
-
}
|
|
189
|
-
hashError = errorParts.length > 0 ? errorParts.join(' ') : 'Validation failed.';
|
|
190
|
-
}
|
|
219
|
+
hashValid = casePackageResult.isValid;
|
|
191
220
|
|
|
192
|
-
|
|
193
|
-
const
|
|
221
|
+
if (!hashValid) {
|
|
222
|
+
const errorParts: string[] = [];
|
|
194
223
|
if (!signatureResult.isValid) {
|
|
195
|
-
|
|
224
|
+
errorParts.push('Signature validation failed.');
|
|
196
225
|
}
|
|
226
|
+
if (!validation.isValid) {
|
|
227
|
+
errorParts.push('Integrity validation failed.');
|
|
228
|
+
}
|
|
229
|
+
if (bundledAuditVerification) {
|
|
230
|
+
errorParts.push(bundledAuditVerification.message);
|
|
231
|
+
}
|
|
232
|
+
hashError = errorParts.length > 0 ? errorParts.join(' ') : 'Validation failed.';
|
|
233
|
+
}
|
|
197
234
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
imageValidation: validation.imageValidation,
|
|
202
|
-
manifestValid: validation.manifestValid,
|
|
203
|
-
signatureValid: signatureResult.isValid,
|
|
204
|
-
signatureKeyId: signatureResult.keyId,
|
|
205
|
-
signatureError: signatureResult.error,
|
|
206
|
-
validationSummary: validation.summary,
|
|
207
|
-
integrityErrors
|
|
208
|
-
};
|
|
235
|
+
const integrityErrors = [...validation.errors];
|
|
236
|
+
if (!signatureResult.isValid) {
|
|
237
|
+
integrityErrors.push(`Signature validation failed: ${signatureResult.error || 'Unknown signature error'}`);
|
|
209
238
|
}
|
|
239
|
+
if (bundledAuditVerification) {
|
|
240
|
+
integrityErrors.push(bundledAuditVerification.message);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
validationDetails = {
|
|
244
|
+
hasForensicManifest: true,
|
|
245
|
+
dataValid: validation.dataValid,
|
|
246
|
+
imageValidation: validation.imageValidation,
|
|
247
|
+
manifestValid: validation.manifestValid,
|
|
248
|
+
signatureValid: signatureResult.isValid,
|
|
249
|
+
signatureKeyId: signatureResult.keyId,
|
|
250
|
+
signatureError: signatureResult.error,
|
|
251
|
+
validationSummary: validation.summary,
|
|
252
|
+
integrityErrors
|
|
253
|
+
};
|
|
210
254
|
|
|
211
255
|
} else {
|
|
212
256
|
// No forensic manifest found - cannot validate
|
|
@@ -239,7 +283,8 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
|
|
|
239
283
|
};
|
|
240
284
|
}
|
|
241
285
|
|
|
242
|
-
const
|
|
286
|
+
const parsedCaseData = JSON.parse(cleanedContent) as unknown;
|
|
287
|
+
const caseData: CaseExportData = parsedCaseData as CaseExportData;
|
|
243
288
|
|
|
244
289
|
// Validate case data structure
|
|
245
290
|
if (!caseData.metadata?.caseNumber) {
|
|
@@ -250,6 +295,12 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
|
|
|
250
295
|
throw new Error(`Invalid case number format: ${caseData.metadata.caseNumber}`);
|
|
251
296
|
}
|
|
252
297
|
|
|
298
|
+
const isArchivedExport = await allowSelfImportForArchivedCase(
|
|
299
|
+
currentUser,
|
|
300
|
+
caseData.metadata.caseNumber,
|
|
301
|
+
parsedCaseData
|
|
302
|
+
);
|
|
303
|
+
|
|
253
304
|
// Validate exporter UID exists in user database and is not current user
|
|
254
305
|
if (caseData.metadata.exportedByUid) {
|
|
255
306
|
const validation = await validateExporterUid(caseData.metadata.exportedByUid, currentUser);
|
|
@@ -258,7 +309,7 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
|
|
|
258
309
|
throw new Error(`The original exporter is not a valid Striae user. This case cannot be imported.`);
|
|
259
310
|
}
|
|
260
311
|
|
|
261
|
-
if (validation.isSelf) {
|
|
312
|
+
if (validation.isSelf && !isArchivedExport) {
|
|
262
313
|
throw new Error(`You cannot import a case that you originally exported. Original analysts cannot review their own cases.`);
|
|
263
314
|
}
|
|
264
315
|
} else {
|
|
@@ -278,9 +329,11 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
|
|
|
278
329
|
|
|
279
330
|
return {
|
|
280
331
|
caseNumber: caseData.metadata.caseNumber,
|
|
332
|
+
archived: isArchivedExport,
|
|
281
333
|
exportedBy: caseData.metadata.exportedBy || null,
|
|
282
334
|
exportedByName: caseData.metadata.exportedByName || null,
|
|
283
335
|
exportedByCompany: caseData.metadata.exportedByCompany || null,
|
|
336
|
+
exportedByBadgeId: caseData.metadata.exportedByBadgeId || null,
|
|
284
337
|
exportDate: caseData.metadata.exportDate,
|
|
285
338
|
totalFiles,
|
|
286
339
|
caseCreatedDate: caseData.metadata.caseCreatedDate,
|
|
@@ -304,6 +357,11 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
|
|
|
304
357
|
caseData: CaseExportData;
|
|
305
358
|
imageFiles: { [filename: string]: Blob };
|
|
306
359
|
imageIdMapping: { [exportFilename: string]: string }; // exportFilename -> originalImageId
|
|
360
|
+
isArchivedExport: boolean;
|
|
361
|
+
bundledAuditFiles?: {
|
|
362
|
+
auditTrailContent?: string;
|
|
363
|
+
auditSignatureContent?: string;
|
|
364
|
+
};
|
|
307
365
|
metadata?: Record<string, unknown>;
|
|
308
366
|
cleanedContent?: string; // Add cleaned content for hash validation
|
|
309
367
|
verificationPublicKeyPem?: string;
|
|
@@ -333,6 +391,7 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
|
|
|
333
391
|
|
|
334
392
|
// Extract and parse case data
|
|
335
393
|
let caseData: CaseExportData;
|
|
394
|
+
let parsedCaseData: unknown;
|
|
336
395
|
let cleanedContent: string = '';
|
|
337
396
|
if (isJsonFormat) {
|
|
338
397
|
const dataContent = await zip.file(dataFileName)?.async('text');
|
|
@@ -342,7 +401,8 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
|
|
|
342
401
|
|
|
343
402
|
// Handle forensic protection warnings in JSON
|
|
344
403
|
cleanedContent = removeForensicWarning(dataContent);
|
|
345
|
-
|
|
404
|
+
parsedCaseData = JSON.parse(cleanedContent) as unknown;
|
|
405
|
+
caseData = parsedCaseData as CaseExportData;
|
|
346
406
|
} else {
|
|
347
407
|
throw new Error('CSV import not yet supported. Please use JSON format.');
|
|
348
408
|
}
|
|
@@ -356,6 +416,12 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
|
|
|
356
416
|
throw new Error(`Invalid case number format: ${caseData.metadata.caseNumber}`);
|
|
357
417
|
}
|
|
358
418
|
|
|
419
|
+
const isArchivedExport = await allowSelfImportForArchivedCase(
|
|
420
|
+
currentUser,
|
|
421
|
+
caseData.metadata.caseNumber,
|
|
422
|
+
parsedCaseData
|
|
423
|
+
);
|
|
424
|
+
|
|
359
425
|
// Validate exporter UID exists in user database and is not current user
|
|
360
426
|
if (caseData.metadata.exportedByUid) {
|
|
361
427
|
const validation = await validateExporterUid(caseData.metadata.exportedByUid, currentUser);
|
|
@@ -364,7 +430,7 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
|
|
|
364
430
|
throw new Error(`The original exporter is not a valid Striae user. This case cannot be imported.`);
|
|
365
431
|
}
|
|
366
432
|
|
|
367
|
-
if (validation.isSelf) {
|
|
433
|
+
if (validation.isSelf && !isArchivedExport) {
|
|
368
434
|
throw new Error(`You cannot import a case that you originally exported. Original analysts cannot review their own cases.`);
|
|
369
435
|
}
|
|
370
436
|
} else {
|
|
@@ -377,19 +443,19 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
|
|
|
377
443
|
const imagesFolder = zip.folder('images');
|
|
378
444
|
|
|
379
445
|
if (imagesFolder) {
|
|
380
|
-
for (const [, file] of Object.entries(imagesFolder.files)) {
|
|
381
|
-
if (!
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
446
|
+
for (const [path, file] of Object.entries(imagesFolder.files)) {
|
|
447
|
+
if (!path.startsWith('images/') || path.endsWith('/') || file.dir) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const exportFilename = path.replace('images/', '');
|
|
452
|
+
const blob = await file.async('blob');
|
|
453
|
+
imageFiles[exportFilename] = blob;
|
|
454
|
+
|
|
455
|
+
// Extract original image ID from filename
|
|
456
|
+
const originalImageId = extractImageIdFromFilename(exportFilename);
|
|
457
|
+
if (originalImageId) {
|
|
458
|
+
imageIdMapping[exportFilename] = originalImageId;
|
|
393
459
|
}
|
|
394
460
|
}
|
|
395
461
|
}
|
|
@@ -397,6 +463,8 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
|
|
|
397
463
|
// Extract forensic manifest if present
|
|
398
464
|
let metadata: Record<string, unknown> | undefined;
|
|
399
465
|
const manifestFile = zip.file('FORENSIC_MANIFEST.json');
|
|
466
|
+
const auditTrailContent = await zip.file('audit/case-audit-trail.json')?.async('text');
|
|
467
|
+
const auditSignatureContent = await zip.file('audit/case-audit-signature.json')?.async('text');
|
|
400
468
|
|
|
401
469
|
if (manifestFile) {
|
|
402
470
|
const manifestContent = await manifestFile.async('text');
|
|
@@ -407,6 +475,11 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
|
|
|
407
475
|
caseData,
|
|
408
476
|
imageFiles,
|
|
409
477
|
imageIdMapping,
|
|
478
|
+
isArchivedExport,
|
|
479
|
+
bundledAuditFiles: {
|
|
480
|
+
auditTrailContent,
|
|
481
|
+
auditSignatureContent
|
|
482
|
+
},
|
|
410
483
|
metadata,
|
|
411
484
|
cleanedContent,
|
|
412
485
|
verificationPublicKeyPem
|