@striae-org/striae 5.2.0 → 5.3.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 +36 -33
- package/README.md +5 -46
- package/app/components/actions/case-export/core-export.ts +2 -174
- package/app/components/actions/case-export/download-handlers.ts +83 -750
- package/app/components/actions/case-export/index.ts +6 -30
- package/app/components/actions/case-export/metadata-helpers.ts +0 -78
- package/app/components/actions/case-export/types-constants.ts +0 -43
- package/app/components/actions/case-import/confirmation-import.ts +13 -14
- package/app/components/actions/case-import/zip-processing.ts +92 -12
- package/app/components/actions/generate-pdf.ts +3 -2
- package/app/components/audit/user-audit-viewer.tsx +0 -19
- package/app/components/audit/viewer/audit-viewer-header.tsx +0 -33
- package/app/components/navbar/case-modals/archive-case-modal.tsx +1 -1
- package/app/components/navbar/navbar.tsx +1 -1
- package/app/components/sidebar/case-import/case-import.module.css +35 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +59 -3
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +1 -1
- package/app/components/sidebar/notes/class-details-shared.ts +2 -2
- package/app/components/toast/toast.module.css +36 -0
- package/app/components/toast/toast.tsx +6 -2
- package/app/components/user/manage-profile.tsx +4 -3
- package/app/config-example/config.json +1 -2
- package/app/root.tsx +0 -7
- package/app/routes/_index.tsx +1 -1
- package/app/routes/auth/login.example.tsx +22 -103
- package/app/routes/auth/route.ts +1 -1
- package/app/routes/striae/striae.tsx +53 -59
- package/app/services/firebase/index.ts +0 -3
- package/app/types/export.ts +1 -2
- package/app/utils/auth/index.ts +0 -1
- package/app/utils/data/permissions.ts +3 -2
- package/package.json +10 -17
- package/public/_headers +0 -4
- package/public/_routes.json +0 -1
- package/worker-configuration.d.ts +20 -17
- package/workers/audit-worker/src/audit-worker.example.ts +9 -806
- package/workers/audit-worker/src/config.ts +7 -0
- package/workers/audit-worker/src/crypto/data-at-rest.ts +410 -0
- package/workers/audit-worker/src/handlers/audit-routes.ts +125 -0
- package/workers/audit-worker/src/storage/audit-storage.ts +99 -0
- package/workers/audit-worker/src/types.ts +56 -0
- package/workers/audit-worker/worker-configuration.d.ts +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/config.ts +11 -0
- package/workers/data-worker/src/data-worker.example.ts +21 -942
- package/workers/data-worker/src/handlers/decrypt-export.ts +118 -0
- package/workers/data-worker/src/handlers/signing.ts +174 -0
- package/workers/data-worker/src/handlers/storage-routes.ts +129 -0
- package/workers/data-worker/src/registry/key-registry.ts +368 -0
- package/workers/data-worker/src/types.ts +46 -0
- package/workers/data-worker/worker-configuration.d.ts +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/worker-configuration.d.ts +2 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/auth.ts +30 -0
- package/workers/user-worker/src/cleanup/account-deletion.ts +337 -0
- package/workers/user-worker/src/config.ts +4 -0
- package/workers/user-worker/src/encryption-utils.ts +25 -0
- package/workers/user-worker/src/firebase/admin.ts +152 -0
- package/workers/user-worker/src/handlers/user-routes.ts +242 -0
- package/workers/user-worker/src/registry/user-kv.ts +172 -0
- package/workers/user-worker/src/storage/user-records.ts +34 -0
- package/workers/user-worker/src/types.ts +106 -0
- package/workers/user-worker/src/user-worker.example.ts +18 -964
- package/workers/user-worker/worker-configuration.d.ts +4 -2
- package/workers/user-worker/wrangler.jsonc.example +12 -1
- package/wrangler.toml.example +1 -1
- package/app/components/actions/case-export/data-processing.ts +0 -223
- package/app/components/sidebar/case-export/case-export.module.css +0 -418
- package/app/components/sidebar/case-export/case-export.tsx +0 -310
- package/app/types/exceljs-bare.d.ts +0 -9
- package/app/utils/auth/auth.ts +0 -11
- package/public/.well-known/security.txt +0 -6
- package/public/favicon.ico +0 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +0 -39
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/vendor/exceljs.LICENSE +0 -22
- package/public/vendor/exceljs.bare.min.js +0 -45
- package/scripts/deploy-all.sh +0 -166
- package/scripts/deploy-config/modules/env-utils.sh +0 -322
- package/scripts/deploy-config/modules/keys.sh +0 -404
- package/scripts/deploy-config/modules/prompt.sh +0 -372
- package/scripts/deploy-config/modules/scaffolding.sh +0 -336
- package/scripts/deploy-config/modules/validation.sh +0 -365
- package/scripts/deploy-config.sh +0 -236
- package/scripts/deploy-pages-secrets.sh +0 -231
- package/scripts/deploy-pages.sh +0 -34
- package/scripts/deploy-primershear-emails.sh +0 -167
- package/scripts/deploy-worker-secrets.sh +0 -374
- package/scripts/dev.cjs +0 -23
- package/scripts/install-workers.sh +0 -88
- package/scripts/run-eslint.cjs +0 -43
- package/scripts/update-compatibility-dates.cjs +0 -124
- package/scripts/update-markdown-versions.cjs +0 -43
- package/workers/keys-worker/package.json +0 -18
- package/workers/keys-worker/src/keys.example.ts +0 -67
- package/workers/keys-worker/src/keys.ts +0 -67
- package/workers/keys-worker/worker-configuration.d.ts +0 -7447
- package/workers/keys-worker/wrangler.jsonc.example +0 -15
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
|
-
import type
|
|
3
|
-
import { type FileData, type AllCasesExportData, type CaseExportData, type ExportOptions } from '~/types';
|
|
2
|
+
import { type FileData, type CaseExportData, type ExportOptions } from '~/types';
|
|
4
3
|
import { getImageUrl } from '../image-manage';
|
|
5
4
|
import {
|
|
6
5
|
generateForensicManifestSecure,
|
|
@@ -12,98 +11,11 @@ import {
|
|
|
12
11
|
encryptExportDataWithAllImages
|
|
13
12
|
} from '~/utils/forensics';
|
|
14
13
|
import { signForensicManifest } from '~/utils/data';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { generateMetadataRows, generateCSVContent, processFileDataForTabular, sanitizeTabularMatrix } from './data-processing';
|
|
14
|
+
import { formatDateForFilename } from './types-constants';
|
|
15
|
+
import { addForensicDataWarning } from './metadata-helpers';
|
|
18
16
|
import { exportCaseData } from './core-export';
|
|
19
17
|
import { auditService } from '~/services/audit';
|
|
20
18
|
|
|
21
|
-
type TabularRow = Array<string | number | boolean | null | undefined>;
|
|
22
|
-
type ExcelJsBrowserBundle = typeof ExcelJSModule;
|
|
23
|
-
|
|
24
|
-
const EXCELJS_BROWSER_BUNDLE_SRC = '/vendor/exceljs.bare.min.js';
|
|
25
|
-
let excelJsBundlePromise: Promise<ExcelJsBrowserBundle> | null = null;
|
|
26
|
-
|
|
27
|
-
async function loadExcelJsBrowserBundle(): Promise<ExcelJsBrowserBundle> {
|
|
28
|
-
if (typeof window === 'undefined') {
|
|
29
|
-
throw new Error('Excel export is only available in a browser context.');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (window.ExcelJS?.Workbook) {
|
|
33
|
-
return window.ExcelJS;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (!excelJsBundlePromise) {
|
|
37
|
-
excelJsBundlePromise = new Promise((resolve, reject) => {
|
|
38
|
-
const resolveFromWindow = () => {
|
|
39
|
-
if (window.ExcelJS?.Workbook) {
|
|
40
|
-
resolve(window.ExcelJS);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
excelJsBundlePromise = null;
|
|
45
|
-
reject(new Error('ExcelJS bundle loaded but Workbook API is unavailable.'));
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const failLoad = () => {
|
|
49
|
-
excelJsBundlePromise = null;
|
|
50
|
-
reject(new Error('Failed to load ExcelJS browser bundle.'));
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const existingScript = document.querySelector<HTMLScriptElement>('script[data-exceljs-bundle="true"]');
|
|
54
|
-
|
|
55
|
-
if (existingScript) {
|
|
56
|
-
if (existingScript.dataset.loaded === 'true') {
|
|
57
|
-
resolveFromWindow();
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
existingScript.addEventListener('load', resolveFromWindow, { once: true });
|
|
62
|
-
existingScript.addEventListener('error', failLoad, { once: true });
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const script = document.createElement('script');
|
|
67
|
-
script.src = EXCELJS_BROWSER_BUNDLE_SRC;
|
|
68
|
-
script.async = true;
|
|
69
|
-
script.dataset.exceljsBundle = 'true';
|
|
70
|
-
script.addEventListener(
|
|
71
|
-
'load',
|
|
72
|
-
() => {
|
|
73
|
-
script.dataset.loaded = 'true';
|
|
74
|
-
resolveFromWindow();
|
|
75
|
-
},
|
|
76
|
-
{ once: true }
|
|
77
|
-
);
|
|
78
|
-
script.addEventListener('error', failLoad, { once: true });
|
|
79
|
-
document.head.appendChild(script);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return excelJsBundlePromise;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function sanitizeWorksheetName(name: string): string {
|
|
87
|
-
const cleaned = name.replace(/[\\/?*:\x5B\x5D]/g, '_').trim();
|
|
88
|
-
const normalized = cleaned.length > 0 ? cleaned : 'Sheet';
|
|
89
|
-
return normalized.substring(0, 31);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function createUniqueWorksheetName(existingNames: Set<string>, desiredName: string): string {
|
|
93
|
-
const baseName = sanitizeWorksheetName(desiredName);
|
|
94
|
-
let candidate = baseName;
|
|
95
|
-
let suffix = 1;
|
|
96
|
-
|
|
97
|
-
while (existingNames.has(candidate)) {
|
|
98
|
-
const suffixText = `_${suffix}`;
|
|
99
|
-
candidate = `${baseName.substring(0, 31 - suffixText.length)}${suffixText}`;
|
|
100
|
-
suffix += 1;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
existingNames.add(candidate);
|
|
104
|
-
return candidate;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
19
|
/**
|
|
108
20
|
* Generate export filename with embedded ID to prevent collisions
|
|
109
21
|
* Format: {originalFilename}-{id}.{extension}
|
|
@@ -147,504 +59,20 @@ function addPublicSigningKeyPemToZip(
|
|
|
147
59
|
return publicKeyFileName;
|
|
148
60
|
}
|
|
149
61
|
|
|
150
|
-
/**
|
|
151
|
-
* Download all cases data as JSON file
|
|
152
|
-
*/
|
|
153
|
-
export async function downloadAllCasesAsJSON(user: User, exportData: AllCasesExportData): Promise<void> {
|
|
154
|
-
const startTime = Date.now();
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
// Start audit workflow
|
|
158
|
-
auditService.startWorkflow('all-cases');
|
|
159
|
-
|
|
160
|
-
const dataStr = JSON.stringify(exportData, null, 2);
|
|
161
|
-
|
|
162
|
-
// Calculate hash for integrity verification
|
|
163
|
-
const hash = await calculateSHA256Secure(dataStr);
|
|
164
|
-
|
|
165
|
-
// Create final export with hash included
|
|
166
|
-
const finalExportData = {
|
|
167
|
-
...exportData,
|
|
168
|
-
metadata: {
|
|
169
|
-
...exportData.metadata,
|
|
170
|
-
hash: hash.toUpperCase(),
|
|
171
|
-
integrityNote: 'Verify by recalculating SHA256 of this entire JSON content'
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const finalDataStr = JSON.stringify(finalExportData, null, 2);
|
|
176
|
-
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(finalDataStr);
|
|
177
|
-
|
|
178
|
-
const exportFileName = `striae-all-cases-export-${formatDateForFilename(new Date())}.json`;
|
|
179
|
-
|
|
180
|
-
const linkElement = document.createElement('a');
|
|
181
|
-
linkElement.setAttribute('href', dataUri);
|
|
182
|
-
linkElement.setAttribute('download', exportFileName);
|
|
183
|
-
linkElement.click();
|
|
184
|
-
|
|
185
|
-
// Log successful export audit event
|
|
186
|
-
const endTime = Date.now();
|
|
187
|
-
await auditService.logCaseExport(
|
|
188
|
-
user,
|
|
189
|
-
'all-cases',
|
|
190
|
-
exportFileName,
|
|
191
|
-
'success',
|
|
192
|
-
[],
|
|
193
|
-
{
|
|
194
|
-
processingTimeMs: endTime - startTime,
|
|
195
|
-
fileSizeBytes: finalDataStr.length,
|
|
196
|
-
validationStepsCompleted: exportData.cases.length,
|
|
197
|
-
validationStepsFailed: exportData.cases.filter(c => c.summary?.exportError).length
|
|
198
|
-
},
|
|
199
|
-
'json',
|
|
200
|
-
false // JSON format is not protected
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
// End audit workflow
|
|
204
|
-
auditService.endWorkflow();
|
|
205
|
-
|
|
206
|
-
} catch (error) {
|
|
207
|
-
console.error('Download failed:', error);
|
|
208
|
-
|
|
209
|
-
// Log failed export audit event
|
|
210
|
-
const endTime = Date.now();
|
|
211
|
-
await auditService.logCaseExport(
|
|
212
|
-
user,
|
|
213
|
-
'all-cases',
|
|
214
|
-
'striae-all-cases-export.json',
|
|
215
|
-
'failure',
|
|
216
|
-
[error instanceof Error ? error.message : 'Unknown error'],
|
|
217
|
-
{
|
|
218
|
-
processingTimeMs: endTime - startTime,
|
|
219
|
-
fileSizeBytes: 0,
|
|
220
|
-
validationStepsCompleted: 0,
|
|
221
|
-
validationStepsFailed: 1
|
|
222
|
-
},
|
|
223
|
-
'json',
|
|
224
|
-
false
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
// End audit workflow
|
|
228
|
-
auditService.endWorkflow();
|
|
229
|
-
|
|
230
|
-
throw new Error('Failed to download all cases export file');
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Download all cases data as Excel file with multiple worksheets
|
|
236
|
-
*/
|
|
237
|
-
export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExportData, protectForensicData: boolean = true): Promise<void> {
|
|
238
|
-
const startTime = Date.now();
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
// Start audit workflow
|
|
242
|
-
auditService.startWorkflow('all-cases');
|
|
243
|
-
|
|
244
|
-
const ExcelJS = await loadExcelJsBrowserBundle();
|
|
245
|
-
|
|
246
|
-
const workbook = new ExcelJS.Workbook();
|
|
247
|
-
workbook.creator = exportData.metadata.exportedBy || 'Striae';
|
|
248
|
-
workbook.lastModifiedBy = exportData.metadata.exportedBy || 'Striae';
|
|
249
|
-
workbook.created = new Date();
|
|
250
|
-
workbook.modified = new Date();
|
|
251
|
-
|
|
252
|
-
const existingWorksheetNames = new Set<string>();
|
|
253
|
-
|
|
254
|
-
const appendRowsToWorksheet = (worksheet: { addRow: (row: TabularRow) => unknown }, rows: TabularRow[]) => {
|
|
255
|
-
rows.forEach((row) => {
|
|
256
|
-
worksheet.addRow(row);
|
|
257
|
-
});
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
let exportPassword: string | undefined;
|
|
261
|
-
|
|
262
|
-
// Create summary worksheet
|
|
263
|
-
const summaryDataRows = [
|
|
264
|
-
['Export Date', new Date().toISOString()],
|
|
265
|
-
['Exported By (Email)', exportData.metadata.exportedBy || 'N/A'],
|
|
266
|
-
['Exported By (UID)', exportData.metadata.exportedByUid || 'N/A'],
|
|
267
|
-
['Exported By (Name)', exportData.metadata.exportedByName || 'N/A'],
|
|
268
|
-
['Exported By (Company)', exportData.metadata.exportedByCompany || 'N/A'],
|
|
269
|
-
['Exported By (Badge/ID)', exportData.metadata.exportedByBadgeId || 'N/A'],
|
|
270
|
-
['Striae Export Schema Version', '1.0'],
|
|
271
|
-
['Total Cases', exportData.cases.length],
|
|
272
|
-
['Successful Exports', exportData.cases.filter(c => !c.summary?.exportError).length],
|
|
273
|
-
['Failed Exports', exportData.cases.filter(c => c.summary?.exportError).length],
|
|
274
|
-
['Total Files (All Cases)', exportData.metadata.totalFiles],
|
|
275
|
-
['Total Annotations (All Cases)', exportData.metadata.totalAnnotations],
|
|
276
|
-
['Total Confirmations (All Cases)', exportData.metadata.totalConfirmations || 0],
|
|
277
|
-
['Total Confirmations Requested (All Cases)', exportData.metadata.totalConfirmationsRequested || 0]
|
|
278
|
-
];
|
|
279
|
-
|
|
280
|
-
// XLSX files are inherently protected, no hash validation needed
|
|
281
|
-
const summaryData = sanitizeTabularMatrix([
|
|
282
|
-
protectForensicData ? ['CASE DATA - PROTECTED EXPORT'] : ['Striae - All Cases Export Summary'],
|
|
283
|
-
protectForensicData ? ['WARNING: This workbook contains evidence data and is protected from editing.'] : [''],
|
|
284
|
-
[''],
|
|
285
|
-
...summaryDataRows,
|
|
286
|
-
[''],
|
|
287
|
-
['Case Details'],
|
|
288
|
-
[
|
|
289
|
-
'Case Number',
|
|
290
|
-
'Case Created Date',
|
|
291
|
-
'Export Status',
|
|
292
|
-
'Export Date',
|
|
293
|
-
'Exported By (Email)',
|
|
294
|
-
'Exported By (UID)',
|
|
295
|
-
'Exported By (Name)',
|
|
296
|
-
'Exported By (Company)',
|
|
297
|
-
'Exported By (Badge/ID)',
|
|
298
|
-
'Schema Version',
|
|
299
|
-
'Total Files',
|
|
300
|
-
'Files with Annotations',
|
|
301
|
-
'Files without Annotations',
|
|
302
|
-
'Total Box Annotations',
|
|
303
|
-
'Files with Confirmations',
|
|
304
|
-
'Files with Confirmations Requested',
|
|
305
|
-
'Last Modified',
|
|
306
|
-
'Earliest Annotation Date',
|
|
307
|
-
'Latest Annotation Date',
|
|
308
|
-
'Export Error'
|
|
309
|
-
],
|
|
310
|
-
...exportData.cases.map(caseData => [
|
|
311
|
-
caseData.metadata.caseNumber,
|
|
312
|
-
caseData.metadata.caseCreatedDate,
|
|
313
|
-
caseData.summary?.exportError ? 'Failed' : 'Success',
|
|
314
|
-
caseData.metadata.exportDate,
|
|
315
|
-
caseData.metadata.exportedBy || 'N/A',
|
|
316
|
-
caseData.metadata.exportedByUid || 'N/A',
|
|
317
|
-
caseData.metadata.exportedByName || 'N/A',
|
|
318
|
-
caseData.metadata.exportedByCompany || 'N/A',
|
|
319
|
-
caseData.metadata.exportedByBadgeId || 'N/A',
|
|
320
|
-
caseData.metadata.striaeExportSchemaVersion,
|
|
321
|
-
caseData.metadata.totalFiles,
|
|
322
|
-
caseData.summary?.filesWithAnnotations || 0,
|
|
323
|
-
caseData.summary?.filesWithoutAnnotations || 0,
|
|
324
|
-
caseData.summary?.totalBoxAnnotations || 0,
|
|
325
|
-
caseData.summary?.filesWithConfirmations || 0,
|
|
326
|
-
caseData.summary?.filesWithConfirmationsRequested || 0,
|
|
327
|
-
caseData.summary?.lastModified || '',
|
|
328
|
-
caseData.summary?.earliestAnnotationDate || '',
|
|
329
|
-
caseData.summary?.latestAnnotationDate || '',
|
|
330
|
-
caseData.summary?.exportError || ''
|
|
331
|
-
])
|
|
332
|
-
]);
|
|
333
|
-
|
|
334
|
-
const summaryWorksheetName = createUniqueWorksheetName(existingWorksheetNames, 'Summary');
|
|
335
|
-
const summaryWorksheet = workbook.addWorksheet(summaryWorksheetName);
|
|
336
|
-
appendRowsToWorksheet(summaryWorksheet, summaryData);
|
|
337
|
-
|
|
338
|
-
// Protect summary worksheet if forensic protection is enabled
|
|
339
|
-
if (protectForensicData) {
|
|
340
|
-
exportPassword = await protectExcelWorksheet(summaryWorksheet);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Create a worksheet for each case
|
|
344
|
-
for (const caseData of exportData.cases) {
|
|
345
|
-
if (caseData.summary?.exportError) {
|
|
346
|
-
// For failed cases, create a simple error sheet
|
|
347
|
-
const errorData = sanitizeTabularMatrix([
|
|
348
|
-
[`Case ${caseData.metadata.caseNumber} - Export Failed`],
|
|
349
|
-
[''],
|
|
350
|
-
['Error:', caseData.summary.exportError],
|
|
351
|
-
['Case Number:', caseData.metadata.caseNumber],
|
|
352
|
-
['Total Files:', caseData.metadata.totalFiles]
|
|
353
|
-
]);
|
|
354
|
-
const errorSheetName = createUniqueWorksheetName(existingWorksheetNames, `Case_${caseData.metadata.caseNumber}_Error`);
|
|
355
|
-
const errorWorksheet = workbook.addWorksheet(errorSheetName);
|
|
356
|
-
appendRowsToWorksheet(errorWorksheet, errorData);
|
|
357
|
-
|
|
358
|
-
if (protectForensicData && exportPassword) {
|
|
359
|
-
await protectExcelWorksheet(errorWorksheet, exportPassword);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// For successful cases, create detailed worksheets
|
|
366
|
-
const metadataRows = generateMetadataRows(caseData);
|
|
367
|
-
|
|
368
|
-
// Create case details with headers
|
|
369
|
-
const caseDetailsData: Array<Array<string | number | boolean | null | undefined>> = [
|
|
370
|
-
protectForensicData
|
|
371
|
-
? [`CASE DATA - ${caseData.metadata.caseNumber} - PROTECTED`]
|
|
372
|
-
: [`Case ${caseData.metadata.caseNumber} - Detailed Export`],
|
|
373
|
-
protectForensicData ? ['WARNING: This worksheet is protected to maintain data integrity.'] : [''],
|
|
374
|
-
[''],
|
|
375
|
-
...metadataRows.slice(2, -1), // Skip title and "File Details" header
|
|
376
|
-
[''],
|
|
377
|
-
['File Details'],
|
|
378
|
-
CSV_HEADERS
|
|
379
|
-
];
|
|
380
|
-
|
|
381
|
-
// Add file data if available
|
|
382
|
-
if (caseData.files && caseData.files.length > 0) {
|
|
383
|
-
const fileRows: Array<Array<string | number | boolean | null | undefined>> = [];
|
|
384
|
-
|
|
385
|
-
caseData.files.forEach(fileEntry => {
|
|
386
|
-
const processedRows = processFileDataForTabular(fileEntry);
|
|
387
|
-
fileRows.push(...processedRows);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
caseDetailsData.push(...fileRows);
|
|
391
|
-
} else {
|
|
392
|
-
caseDetailsData.push(['No detailed file data available for this case']);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const sanitizedCaseDetailsData = sanitizeTabularMatrix(caseDetailsData);
|
|
396
|
-
|
|
397
|
-
// Clean sheet name for Excel compatibility and uniqueness
|
|
398
|
-
const sheetName = createUniqueWorksheetName(existingWorksheetNames, `Case_${caseData.metadata.caseNumber}`);
|
|
399
|
-
const caseWorksheet = workbook.addWorksheet(sheetName);
|
|
400
|
-
appendRowsToWorksheet(caseWorksheet, sanitizedCaseDetailsData);
|
|
401
|
-
|
|
402
|
-
// Protect worksheet if forensic protection is enabled
|
|
403
|
-
if (protectForensicData && exportPassword) {
|
|
404
|
-
await protectExcelWorksheet(caseWorksheet, exportPassword);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Generate Excel file
|
|
410
|
-
const excelBuffer = await workbook.xlsx.writeBuffer();
|
|
411
|
-
|
|
412
|
-
// Create blob and download
|
|
413
|
-
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
414
|
-
const url = window.URL.createObjectURL(blob);
|
|
415
|
-
|
|
416
|
-
const protectionSuffix = protectForensicData ? '-protected' : '';
|
|
417
|
-
const exportFileName = `striae-all-cases-detailed${protectionSuffix}-${formatDateForFilename(new Date())}.xlsx`;
|
|
418
|
-
|
|
419
|
-
const linkElement = document.createElement('a');
|
|
420
|
-
linkElement.href = url;
|
|
421
|
-
linkElement.download = exportFileName;
|
|
422
|
-
linkElement.click();
|
|
423
|
-
|
|
424
|
-
// Clean up
|
|
425
|
-
window.URL.revokeObjectURL(url);
|
|
426
|
-
|
|
427
|
-
// Log successful export audit event
|
|
428
|
-
const endTime = Date.now();
|
|
429
|
-
await auditService.logCaseExport(
|
|
430
|
-
user,
|
|
431
|
-
'all-cases',
|
|
432
|
-
exportFileName,
|
|
433
|
-
'success',
|
|
434
|
-
[],
|
|
435
|
-
{
|
|
436
|
-
processingTimeMs: endTime - startTime,
|
|
437
|
-
fileSizeBytes: blob.size,
|
|
438
|
-
validationStepsCompleted: exportData.cases.length,
|
|
439
|
-
validationStepsFailed: exportData.cases.filter(c => c.summary?.exportError).length
|
|
440
|
-
},
|
|
441
|
-
'xlsx',
|
|
442
|
-
protectForensicData
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
// End audit workflow
|
|
446
|
-
auditService.endWorkflow();
|
|
447
|
-
|
|
448
|
-
} catch (error) {
|
|
449
|
-
console.error('Excel export failed:', error);
|
|
450
|
-
|
|
451
|
-
// Log failed export audit event
|
|
452
|
-
const endTime = Date.now();
|
|
453
|
-
await auditService.logCaseExport(
|
|
454
|
-
user,
|
|
455
|
-
'all-cases',
|
|
456
|
-
'striae-all-cases-detailed.xlsx',
|
|
457
|
-
'failure',
|
|
458
|
-
[error instanceof Error ? error.message : 'Unknown error'],
|
|
459
|
-
{
|
|
460
|
-
processingTimeMs: endTime - startTime,
|
|
461
|
-
fileSizeBytes: 0,
|
|
462
|
-
validationStepsCompleted: 0,
|
|
463
|
-
validationStepsFailed: 1
|
|
464
|
-
},
|
|
465
|
-
'xlsx',
|
|
466
|
-
protectForensicData
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
// End audit workflow
|
|
470
|
-
auditService.endWorkflow();
|
|
471
|
-
|
|
472
|
-
throw new Error('Failed to export Excel file');
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Download case data as JSON file with forensic protection options
|
|
478
|
-
*/
|
|
479
|
-
export async function downloadCaseAsJSON(
|
|
480
|
-
user: User,
|
|
481
|
-
exportData: CaseExportData,
|
|
482
|
-
options: ExportOptions = { protectForensicData: true }
|
|
483
|
-
): Promise<void> {
|
|
484
|
-
const startTime = Date.now();
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
// Start audit workflow
|
|
488
|
-
auditService.startWorkflow(exportData.metadata.caseNumber);
|
|
489
|
-
|
|
490
|
-
const jsonContent = await generateJSONContent(exportData, options.includeUserInfo, options.protectForensicData);
|
|
491
|
-
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(jsonContent);
|
|
492
|
-
|
|
493
|
-
const protectionSuffix = options.protectForensicData ? '-protected' : '';
|
|
494
|
-
const exportFileName = `striae-case-${exportData.metadata.caseNumber}-export${protectionSuffix}-${formatDateForFilename(new Date())}.json`;
|
|
495
|
-
|
|
496
|
-
const linkElement = document.createElement('a');
|
|
497
|
-
linkElement.setAttribute('href', dataUri);
|
|
498
|
-
linkElement.setAttribute('download', exportFileName);
|
|
499
|
-
|
|
500
|
-
if (options.protectForensicData) {
|
|
501
|
-
linkElement.setAttribute('data-forensic-protected', 'true');
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
linkElement.click();
|
|
505
|
-
|
|
506
|
-
// Log successful export audit event
|
|
507
|
-
const endTime = Date.now();
|
|
508
|
-
await auditService.logCaseExport(
|
|
509
|
-
user,
|
|
510
|
-
exportData.metadata.caseNumber,
|
|
511
|
-
exportFileName,
|
|
512
|
-
'success',
|
|
513
|
-
[],
|
|
514
|
-
{
|
|
515
|
-
processingTimeMs: endTime - startTime,
|
|
516
|
-
fileSizeBytes: jsonContent.length,
|
|
517
|
-
validationStepsCompleted: exportData.files?.length || 0,
|
|
518
|
-
validationStepsFailed: 0
|
|
519
|
-
},
|
|
520
|
-
'json',
|
|
521
|
-
options.protectForensicData || false
|
|
522
|
-
);
|
|
523
|
-
|
|
524
|
-
// End audit workflow
|
|
525
|
-
auditService.endWorkflow();
|
|
526
|
-
|
|
527
|
-
} catch (error) {
|
|
528
|
-
console.error('JSON export failed:', error);
|
|
529
|
-
|
|
530
|
-
// Log failed export audit event
|
|
531
|
-
const endTime = Date.now();
|
|
532
|
-
await auditService.logCaseExport(
|
|
533
|
-
user,
|
|
534
|
-
exportData.metadata.caseNumber,
|
|
535
|
-
`striae-case-${exportData.metadata.caseNumber}-export.json`,
|
|
536
|
-
'failure',
|
|
537
|
-
[error instanceof Error ? error.message : 'Unknown error'],
|
|
538
|
-
{
|
|
539
|
-
processingTimeMs: endTime - startTime,
|
|
540
|
-
fileSizeBytes: 0,
|
|
541
|
-
validationStepsCompleted: 0,
|
|
542
|
-
validationStepsFailed: 1
|
|
543
|
-
},
|
|
544
|
-
'json',
|
|
545
|
-
options.protectForensicData || false
|
|
546
|
-
);
|
|
547
|
-
|
|
548
|
-
// End audit workflow
|
|
549
|
-
auditService.endWorkflow();
|
|
550
|
-
|
|
551
|
-
throw new Error('Failed to download JSON export file');
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Download case data as comprehensive CSV file with forensic protection options
|
|
557
|
-
*/
|
|
558
|
-
export async function downloadCaseAsCSV(
|
|
559
|
-
user: User,
|
|
560
|
-
exportData: CaseExportData,
|
|
561
|
-
options: ExportOptions = { protectForensicData: true }
|
|
562
|
-
): Promise<void> {
|
|
563
|
-
const startTime = Date.now();
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
// Start audit workflow
|
|
567
|
-
auditService.startWorkflow(exportData.metadata.caseNumber);
|
|
568
|
-
|
|
569
|
-
const csvContent = await generateCSVContent(exportData, options.protectForensicData);
|
|
570
|
-
const dataUri = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent);
|
|
571
|
-
|
|
572
|
-
const protectionSuffix = options.protectForensicData ? '-protected' : '';
|
|
573
|
-
const exportFileName = `striae-case-${exportData.metadata.caseNumber}-detailed${protectionSuffix}-${formatDateForFilename(new Date())}.csv`;
|
|
574
|
-
|
|
575
|
-
const linkElement = document.createElement('a');
|
|
576
|
-
linkElement.setAttribute('href', dataUri);
|
|
577
|
-
linkElement.setAttribute('download', exportFileName);
|
|
578
|
-
|
|
579
|
-
if (options.protectForensicData) {
|
|
580
|
-
linkElement.setAttribute('data-forensic-protected', 'true');
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
linkElement.click();
|
|
584
|
-
|
|
585
|
-
// Log successful export audit event
|
|
586
|
-
const endTime = Date.now();
|
|
587
|
-
await auditService.logCaseExport(
|
|
588
|
-
user,
|
|
589
|
-
exportData.metadata.caseNumber,
|
|
590
|
-
exportFileName,
|
|
591
|
-
'success',
|
|
592
|
-
[],
|
|
593
|
-
{
|
|
594
|
-
processingTimeMs: endTime - startTime,
|
|
595
|
-
fileSizeBytes: csvContent.length,
|
|
596
|
-
validationStepsCompleted: exportData.files?.length || 0,
|
|
597
|
-
validationStepsFailed: 0
|
|
598
|
-
},
|
|
599
|
-
'csv',
|
|
600
|
-
options.protectForensicData || false
|
|
601
|
-
);
|
|
602
|
-
|
|
603
|
-
// End audit workflow
|
|
604
|
-
auditService.endWorkflow();
|
|
605
|
-
|
|
606
|
-
} catch (error) {
|
|
607
|
-
console.error('CSV export failed:', error);
|
|
608
|
-
|
|
609
|
-
// Log failed export audit event
|
|
610
|
-
const endTime = Date.now();
|
|
611
|
-
await auditService.logCaseExport(
|
|
612
|
-
user,
|
|
613
|
-
exportData.metadata.caseNumber,
|
|
614
|
-
`striae-case-${exportData.metadata.caseNumber}-detailed.csv`,
|
|
615
|
-
'failure',
|
|
616
|
-
[error instanceof Error ? error.message : 'Unknown error'],
|
|
617
|
-
{
|
|
618
|
-
processingTimeMs: endTime - startTime,
|
|
619
|
-
fileSizeBytes: 0,
|
|
620
|
-
validationStepsCompleted: 0,
|
|
621
|
-
validationStepsFailed: 1
|
|
622
|
-
},
|
|
623
|
-
'csv',
|
|
624
|
-
options.protectForensicData || false
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
// End audit workflow
|
|
628
|
-
auditService.endWorkflow();
|
|
629
|
-
|
|
630
|
-
throw new Error('Failed to export CSV file');
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
62
|
/**
|
|
635
63
|
* Download case data as ZIP file including images with forensic protection options
|
|
636
64
|
*/
|
|
637
65
|
export async function downloadCaseAsZip(
|
|
638
66
|
user: User,
|
|
639
67
|
caseNumber: string,
|
|
640
|
-
format: ExportFormat,
|
|
641
68
|
onProgress?: (progress: number) => void,
|
|
642
|
-
options: ExportOptions = {
|
|
69
|
+
options: ExportOptions = {}
|
|
643
70
|
): Promise<void> {
|
|
644
71
|
const startTime = Date.now();
|
|
645
72
|
let manifestSignatureKeyId: string | undefined;
|
|
646
73
|
let manifestSigned = false;
|
|
647
74
|
let publicKeyFileName: string | undefined;
|
|
75
|
+
const protectForensicData = true;
|
|
648
76
|
|
|
649
77
|
try {
|
|
650
78
|
// Start audit workflow
|
|
@@ -660,14 +88,8 @@ export async function downloadCaseAsZip(
|
|
|
660
88
|
const JSZip = (await import('jszip')).default;
|
|
661
89
|
const zip = new JSZip();
|
|
662
90
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const jsonContent = generateJSONContent(exportData, options.includeUserInfo, options.protectForensicData);
|
|
666
|
-
zip.file(`${caseNumber}_data.json`, jsonContent);
|
|
667
|
-
} else {
|
|
668
|
-
const csvContent = generateCSVContent(exportData, options.protectForensicData);
|
|
669
|
-
zip.file(`${caseNumber}_data.csv`, csvContent);
|
|
670
|
-
}
|
|
91
|
+
const jsonContent = await generateJSONContent(exportData, options.includeUserInfo, protectForensicData);
|
|
92
|
+
zip.file(`${caseNumber}_data.json`, jsonContent);
|
|
671
93
|
onProgress?.(50);
|
|
672
94
|
|
|
673
95
|
// Add images and collect them for manifest generation
|
|
@@ -691,86 +113,66 @@ export async function downloadCaseAsZip(
|
|
|
691
113
|
}
|
|
692
114
|
}
|
|
693
115
|
|
|
694
|
-
//
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
// This MUST match exactly what gets saved in the actual data file
|
|
698
|
-
// So we use the same includeUserInfo setting for both
|
|
699
|
-
const contentForHash = format === 'json'
|
|
700
|
-
? await generateJSONContent(exportData, options.includeUserInfo, false) // Raw content without warnings but same includeUserInfo
|
|
701
|
-
: await generateCSVContent(exportData, false); // Raw content without warnings
|
|
116
|
+
// CRITICAL: Get the content that will be used for hash calculation.
|
|
117
|
+
// This must match the exported package content before encryption.
|
|
118
|
+
const contentForHash = await generateJSONContent(exportData, options.includeUserInfo, false);
|
|
702
119
|
|
|
703
|
-
|
|
704
|
-
const forensicManifest = await generateForensicManifestSecure(contentForHash, imageFiles);
|
|
120
|
+
const forensicManifest = await generateForensicManifestSecure(contentForHash, imageFiles);
|
|
705
121
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
manifestSigned = true;
|
|
122
|
+
const signingResult = await signForensicManifest(user, caseNumber, forensicManifest);
|
|
123
|
+
manifestSignatureKeyId = signingResult.signature.keyId;
|
|
124
|
+
manifestSigned = true;
|
|
710
125
|
|
|
711
|
-
|
|
126
|
+
publicKeyFileName = addPublicSigningKeyPemToZip(zip, signingResult.signature.keyId);
|
|
712
127
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
// Add dedicated forensic manifest file for validation
|
|
720
|
-
zip.file('FORENSIC_MANIFEST.json', JSON.stringify(signedForensicManifest, null, 2));
|
|
721
|
-
|
|
722
|
-
// Export encryption is mandatory
|
|
723
|
-
const encKeyDetails = getCurrentEncryptionPublicKeyDetails();
|
|
724
|
-
|
|
725
|
-
if (!encKeyDetails.publicKeyPem || !encKeyDetails.keyId) {
|
|
726
|
-
throw new Error(
|
|
727
|
-
'Export encryption is mandatory. Your Striae instance does not have a configured encryption public key. ' +
|
|
728
|
-
'Please contact your administrator to set up export encryption.'
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
let encryptionManifestJson: string | null = null;
|
|
733
|
-
const isEncrypted = true;
|
|
734
|
-
|
|
735
|
-
try {
|
|
736
|
-
// Build image blobs array from the collected imageFiles
|
|
737
|
-
const imagesToEncrypt = Object.entries(imageFiles).map(([filename, blob]) => ({
|
|
738
|
-
filename,
|
|
739
|
-
blob
|
|
740
|
-
}));
|
|
128
|
+
const signedForensicManifest = {
|
|
129
|
+
...forensicManifest,
|
|
130
|
+
manifestVersion: signingResult.manifestVersion,
|
|
131
|
+
signature: signingResult.signature
|
|
132
|
+
};
|
|
741
133
|
|
|
742
|
-
|
|
743
|
-
const encryptionResult = await encryptExportDataWithAllImages(
|
|
744
|
-
contentForHash,
|
|
745
|
-
imagesToEncrypt,
|
|
746
|
-
encKeyDetails.publicKeyPem,
|
|
747
|
-
encKeyDetails.keyId
|
|
748
|
-
);
|
|
134
|
+
zip.file('FORENSIC_MANIFEST.json', JSON.stringify(signedForensicManifest, null, 2));
|
|
749
135
|
|
|
750
|
-
|
|
751
|
-
zip.file(`${caseNumber}_data.${format}`, encryptionResult.ciphertext);
|
|
136
|
+
const encKeyDetails = getCurrentEncryptionPublicKeyDetails();
|
|
752
137
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}
|
|
760
|
-
}
|
|
138
|
+
if (!encKeyDetails.publicKeyPem || !encKeyDetails.keyId) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
'Export encryption is mandatory. Your Striae instance does not have a configured encryption public key. ' +
|
|
141
|
+
'Please contact your administrator to set up export encryption.'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
761
144
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
145
|
+
try {
|
|
146
|
+
const imagesToEncrypt = Object.entries(imageFiles).map(([filename, blob]) => ({
|
|
147
|
+
filename,
|
|
148
|
+
blob
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
const encryptionResult = await encryptExportDataWithAllImages(
|
|
152
|
+
contentForHash,
|
|
153
|
+
imagesToEncrypt,
|
|
154
|
+
encKeyDetails.publicKeyPem,
|
|
155
|
+
encKeyDetails.keyId
|
|
156
|
+
);
|
|
765
157
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
158
|
+
zip.file(`${caseNumber}_data.json`, encryptionResult.ciphertext);
|
|
159
|
+
|
|
160
|
+
if (imageFolder && encryptionResult.encryptedImages.length > 0) {
|
|
161
|
+
for (let i = 0; i < imagesToEncrypt.length; i++) {
|
|
162
|
+
const originalFilename = imagesToEncrypt[i].filename;
|
|
163
|
+
imageFolder.file(originalFilename, encryptionResult.encryptedImages[i]);
|
|
164
|
+
}
|
|
770
165
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
166
|
+
|
|
167
|
+
zip.file('ENCRYPTION_MANIFEST.json', JSON.stringify(encryptionResult.encryptionManifest, null, 2));
|
|
168
|
+
|
|
169
|
+
onProgress?.(80);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('Export encryption failed:', error);
|
|
172
|
+
throw new Error(`Failed to encrypt export: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const instructionContent = `EVIDENCE ARCHIVE - READ ONLY
|
|
774
176
|
|
|
775
177
|
This ZIP archive contains evidence data exported from Striae.
|
|
776
178
|
|
|
@@ -779,13 +181,13 @@ IMPORTANT WARNINGS:
|
|
|
779
181
|
- Do not modify, rename, or delete any files in this archive
|
|
780
182
|
- Any modifications may compromise evidence integrity
|
|
781
183
|
- Maintain proper chain of custody procedures
|
|
782
|
-
|
|
184
|
+
- This archive is encrypted. Only Striae can decrypt and re-import it.
|
|
783
185
|
|
|
784
186
|
Archive Contents:
|
|
785
|
-
- ${caseNumber}_data
|
|
786
|
-
- images/: Image files with annotations
|
|
187
|
+
- ${caseNumber}_data.json: Complete case data manifest (encrypted)
|
|
188
|
+
- images/: Image files with annotations (encrypted)
|
|
787
189
|
- FORENSIC_MANIFEST.json: File integrity validation manifest
|
|
788
|
-
|
|
190
|
+
- ENCRYPTION_MANIFEST.json: Encryption metadata and encrypted file hashes
|
|
789
191
|
- ${publicKeyFileName}: Public signing key PEM for verification
|
|
790
192
|
- README.txt: General information about this export
|
|
791
193
|
|
|
@@ -797,107 +199,35 @@ Case Information:
|
|
|
797
199
|
- Total Annotations: ${(exportData.summary?.filesWithAnnotations || 0) + (exportData.summary?.totalBoxAnnotations || 0)}
|
|
798
200
|
- Total Confirmations: ${exportData.summary?.filesWithConfirmations || 0}
|
|
799
201
|
- Confirmations Requested: ${exportData.summary?.filesWithConfirmationsRequested || 0}
|
|
800
|
-
|
|
202
|
+
- Encryption Status: ENCRYPTED (key ID: ${encKeyDetails.keyId})
|
|
801
203
|
|
|
802
204
|
For questions about this export, contact your Striae system administrator.
|
|
803
205
|
`;
|
|
804
|
-
|
|
805
|
-
zip.file('READ_ONLY_INSTRUCTIONS.txt', instructionContent);
|
|
806
|
-
|
|
807
|
-
// Add README
|
|
808
|
-
const readme = generateZipReadme(
|
|
809
|
-
exportData,
|
|
810
|
-
options.protectForensicData,
|
|
811
|
-
publicKeyFileName
|
|
812
|
-
);
|
|
813
|
-
zip.file('README.txt', readme);
|
|
814
|
-
onProgress?.(85);
|
|
815
|
-
|
|
816
|
-
// Generate ZIP blob
|
|
817
|
-
const zipBlob = await zip.generateAsync({
|
|
818
|
-
type: 'blob',
|
|
819
|
-
compression: 'DEFLATE',
|
|
820
|
-
compressionOptions: { level: 6 }
|
|
821
|
-
});
|
|
822
|
-
onProgress?.(95);
|
|
823
|
-
|
|
824
|
-
// Download
|
|
825
|
-
const url = URL.createObjectURL(zipBlob);
|
|
826
|
-
const protectionSuffix = options.protectForensicData ? '-protected' : '';
|
|
827
|
-
const encryptedSuffix = isEncrypted ? '-encrypted' : '';
|
|
828
|
-
const exportFileName = `striae-case-${caseNumber}-export${protectionSuffix}${encryptedSuffix}-${formatDateForFilename(new Date())}.zip`;
|
|
829
|
-
|
|
830
|
-
const linkElement = document.createElement('a');
|
|
831
|
-
linkElement.href = url;
|
|
832
|
-
linkElement.setAttribute('download', exportFileName);
|
|
833
|
-
|
|
834
|
-
if (options.protectForensicData) {
|
|
835
|
-
linkElement.setAttribute('title', 'Evidence archive with forensic protection enabled');
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
linkElement.click();
|
|
839
|
-
|
|
840
|
-
URL.revokeObjectURL(url);
|
|
841
|
-
onProgress?.(100);
|
|
842
|
-
|
|
843
|
-
// Log successful export audit event (forensic protected case)
|
|
844
|
-
const endTime = Date.now();
|
|
845
|
-
await auditService.logCaseExport(
|
|
846
|
-
user,
|
|
847
|
-
caseNumber,
|
|
848
|
-
exportFileName,
|
|
849
|
-
'success',
|
|
850
|
-
[],
|
|
851
|
-
{
|
|
852
|
-
processingTimeMs: endTime - startTime,
|
|
853
|
-
fileSizeBytes: zipBlob.size,
|
|
854
|
-
validationStepsCompleted: exportData.files?.length || 0,
|
|
855
|
-
validationStepsFailed: 0
|
|
856
|
-
},
|
|
857
|
-
'zip',
|
|
858
|
-
options.protectForensicData || false,
|
|
859
|
-
{
|
|
860
|
-
present: true,
|
|
861
|
-
valid: true,
|
|
862
|
-
keyId: manifestSignatureKeyId
|
|
863
|
-
}
|
|
864
|
-
);
|
|
865
|
-
|
|
866
|
-
// End audit workflow
|
|
867
|
-
auditService.endWorkflow();
|
|
868
|
-
|
|
869
|
-
return; // Exit early as we've handled the forensic case
|
|
870
|
-
}
|
|
871
206
|
|
|
872
|
-
|
|
207
|
+
zip.file('READ_ONLY_INSTRUCTIONS.txt', instructionContent);
|
|
873
208
|
|
|
874
|
-
// Add README (standard or enhanced for forensic)
|
|
875
209
|
const readme = generateZipReadme(
|
|
876
210
|
exportData,
|
|
877
|
-
|
|
211
|
+
protectForensicData,
|
|
878
212
|
publicKeyFileName
|
|
879
213
|
);
|
|
880
214
|
zip.file('README.txt', readme);
|
|
881
215
|
onProgress?.(85);
|
|
882
216
|
|
|
883
|
-
// Generate ZIP blob for non-forensic case
|
|
884
217
|
const zipBlob = await zip.generateAsync({
|
|
885
218
|
type: 'blob',
|
|
886
219
|
compression: 'DEFLATE',
|
|
887
220
|
compressionOptions: { level: 6 }
|
|
888
221
|
});
|
|
889
|
-
onProgress?.(95);
|
|
222
|
+
onProgress?.(95);
|
|
223
|
+
|
|
890
224
|
const url = URL.createObjectURL(zipBlob);
|
|
891
|
-
const
|
|
892
|
-
const exportFileName = `striae-case-${caseNumber}-export${protectionSuffix}-${formatDateForFilename(new Date())}.zip`;
|
|
225
|
+
const exportFileName = `striae-case-${caseNumber}-encrypted-package-${formatDateForFilename(new Date())}.zip`;
|
|
893
226
|
|
|
894
227
|
const linkElement = document.createElement('a');
|
|
895
228
|
linkElement.href = url;
|
|
896
229
|
linkElement.setAttribute('download', exportFileName);
|
|
897
|
-
|
|
898
|
-
if (options.protectForensicData) {
|
|
899
|
-
linkElement.setAttribute('data-forensic-protected', 'true');
|
|
900
|
-
}
|
|
230
|
+
linkElement.setAttribute('title', 'Encrypted Striae case package');
|
|
901
231
|
|
|
902
232
|
linkElement.click();
|
|
903
233
|
|
|
@@ -919,7 +249,12 @@ For questions about this export, contact your Striae system administrator.
|
|
|
919
249
|
validationStepsFailed: 0
|
|
920
250
|
},
|
|
921
251
|
'zip',
|
|
922
|
-
|
|
252
|
+
protectForensicData,
|
|
253
|
+
{
|
|
254
|
+
present: true,
|
|
255
|
+
valid: true,
|
|
256
|
+
keyId: manifestSignatureKeyId
|
|
257
|
+
}
|
|
923
258
|
);
|
|
924
259
|
|
|
925
260
|
// End audit workflow
|
|
@@ -943,20 +278,18 @@ For questions about this export, contact your Striae system administrator.
|
|
|
943
278
|
validationStepsFailed: 1
|
|
944
279
|
},
|
|
945
280
|
'zip',
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
: undefined
|
|
281
|
+
protectForensicData,
|
|
282
|
+
{
|
|
283
|
+
present: manifestSigned,
|
|
284
|
+
valid: manifestSigned,
|
|
285
|
+
keyId: manifestSignatureKeyId
|
|
286
|
+
}
|
|
954
287
|
);
|
|
955
288
|
|
|
956
289
|
// End audit workflow
|
|
957
290
|
auditService.endWorkflow();
|
|
958
291
|
|
|
959
|
-
throw new Error('Failed to export
|
|
292
|
+
throw new Error('Failed to export encrypted case package');
|
|
960
293
|
}
|
|
961
294
|
}
|
|
962
295
|
|
|
@@ -1034,8 +367,8 @@ Summary:
|
|
|
1034
367
|
- Latest Annotation Date: ${exportData.summary?.latestAnnotationDate || 'N/A'}
|
|
1035
368
|
|
|
1036
369
|
Contents:
|
|
1037
|
-
- ${exportData.metadata.caseNumber}_data.json
|
|
1038
|
-
- images/:
|
|
370
|
+
- ${exportData.metadata.caseNumber}_data.json: Encrypted case data and annotations
|
|
371
|
+
- images/: Encrypted uploaded images
|
|
1039
372
|
- ${publicKeyFileName}: Public signing key PEM for verification
|
|
1040
373
|
- README.txt: This file`;
|
|
1041
374
|
|