@striae-org/striae 3.2.0 → 3.2.2
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/README.md +3 -32
- package/app/components/actions/case-export/core-export.ts +2 -2
- package/app/components/actions/case-export/data-processing.ts +65 -10
- package/app/components/actions/case-export/download-handlers.ts +130 -44
- package/app/components/actions/case-export/metadata-helpers.ts +32 -14
- package/app/components/actions/case-import/annotation-import.ts +2 -2
- package/app/components/actions/case-import/confirmation-import.ts +3 -3
- package/app/components/actions/case-import/image-operations.ts +1 -1
- package/app/components/actions/case-import/orchestrator.ts +4 -4
- package/app/components/actions/case-import/storage-operations.ts +7 -7
- package/app/components/actions/case-import/validation.ts +3 -3
- package/app/components/actions/case-import/zip-processing.ts +3 -3
- package/app/components/actions/case-manage.ts +3 -3
- package/app/components/actions/confirm-export.ts +3 -3
- package/app/components/actions/generate-pdf.ts +3 -3
- package/app/components/actions/image-manage.ts +3 -3
- package/app/components/actions/notes-manage.ts +3 -3
- package/app/components/actions/signout.tsx +1 -1
- package/app/components/audit/user-audit-viewer.tsx +2 -3
- package/app/components/auth/auth-provider.tsx +2 -2
- package/app/components/auth/mfa-enrollment.tsx +3 -3
- package/app/components/auth/mfa-verification.tsx +4 -4
- package/app/components/canvas/box-annotations/box-annotations.tsx +2 -2
- package/app/components/canvas/canvas.tsx +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +1 -1
- package/app/components/form/base-form.tsx +1 -1
- package/app/components/sidebar/case-export/case-export.tsx +15 -15
- package/app/components/sidebar/case-import/case-import.tsx +2 -2
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +1 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +3 -3
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -2
- package/app/components/sidebar/cases/case-sidebar.tsx +27 -19
- package/app/components/sidebar/cases/cases-modal.tsx +1 -1
- package/app/components/sidebar/files/files-modal.tsx +3 -2
- package/app/components/sidebar/notes/notes-sidebar.tsx +3 -3
- package/app/components/sidebar/sidebar-container.tsx +5 -4
- package/app/components/sidebar/sidebar.tsx +2 -2
- package/app/components/sidebar/upload/image-upload-zone.tsx +2 -2
- package/app/components/theme-provider/theme-provider.tsx +1 -1
- package/app/components/user/delete-account.tsx +1 -1
- package/app/components/user/manage-profile.tsx +2 -2
- package/app/components/user/mfa-phone-update.tsx +2 -2
- package/app/contexts/auth.context.ts +1 -1
- package/app/entry.client.tsx +12 -12
- package/app/entry.server.tsx +4 -4
- package/app/hooks/useInactivityTimeout.ts +1 -1
- package/app/root.tsx +3 -3
- package/app/routes/auth/emailActionHandler.tsx +3 -3
- package/app/routes/auth/emailVerification.tsx +3 -3
- package/app/routes/auth/login.tsx +6 -6
- package/app/routes/auth/passwordReset.tsx +3 -3
- package/app/routes/auth/route.ts +1 -1
- package/app/routes/striae/striae.tsx +2 -2
- package/app/services/audit/audit-console-logger.ts +46 -0
- package/app/services/audit/audit-export-csv.ts +126 -0
- package/app/services/audit/audit-export-report.ts +174 -0
- package/app/services/audit/audit-export-signing.ts +85 -0
- package/app/services/audit/audit-export.service.ts +334 -0
- package/app/services/audit/audit-file-type.ts +13 -0
- package/app/services/audit/audit-query-helpers.ts +88 -0
- package/app/services/audit/audit-worker-client.ts +95 -0
- package/app/services/audit/audit.service.ts +990 -0
- package/app/services/audit/builders/audit-entry-builder.ts +32 -0
- package/app/services/audit/builders/audit-event-builders-annotation.ts +150 -0
- package/app/services/audit/builders/audit-event-builders-case-file.ts +249 -0
- package/app/services/audit/builders/audit-event-builders-user-security.ts +449 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +272 -0
- package/app/services/audit/builders/index.ts +40 -0
- package/app/services/audit/index.ts +2 -0
- package/app/types/case.ts +2 -2
- package/app/types/exceljs-bare.d.ts +9 -0
- package/app/types/user.ts +1 -1
- package/app/utils/audit-export-signature.ts +2 -2
- package/app/utils/confirmation-signature.ts +3 -3
- package/app/utils/data-operations.ts +5 -5
- package/app/utils/mfa-phone.ts +1 -1
- package/app/utils/mfa.ts +1 -1
- package/app/utils/permissions.ts +2 -2
- package/functions/[[path]].ts +2 -2
- package/package.json +34 -20
- package/public/vendor/exceljs.LICENSE +22 -0
- package/public/vendor/exceljs.bare.min.js +45 -0
- package/scripts/deploy-all.sh +52 -0
- package/scripts/deploy-config.sh +282 -1
- package/tsconfig.json +18 -8
- package/vite.config.ts +6 -22
- package/worker-configuration.d.ts +4435 -562
- package/workers/audit-worker/package.json +8 -4
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +8 -4
- package/workers/data-worker/src/data-worker.example.ts +3 -3
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +8 -4
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +8 -4
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +8 -4
- package/workers/pdf-worker/src/{generated-assets.ts → assets/generated-assets.ts} +117 -117
- package/workers/pdf-worker/src/{format-striae.ts → formats/format-striae.ts} +535 -535
- package/workers/pdf-worker/src/pdf-worker.example.ts +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +8 -4
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/services/audit-export.service.ts +0 -755
- package/app/services/audit.service.ts +0 -1474
- /package/app/services/{firebase-errors.ts → firebase/errors.ts} +0 -0
- /package/app/services/{firebase.ts → firebase/index.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @striae-org/striae
|
|
2
2
|
|
|
3
|
-
Striae is a cloud-native forensic annotation application for firearms identification, built with
|
|
3
|
+
Striae is a cloud-native forensic annotation application for firearms identification, built with React Router and Cloudflare Workers.
|
|
4
4
|
|
|
5
5
|
This npm package publishes the Striae application source and deployment scaffolding for teams that run their own Striae environment.
|
|
6
6
|
|
|
@@ -40,7 +40,7 @@ npm i @striae-org/striae
|
|
|
40
40
|
cp -R node_modules/@striae-org/striae/. .
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
3) Reinstall using Striae's own package.json (includes dev deps like wrangler/
|
|
43
|
+
3) Reinstall using Striae's own package.json (includes dev deps like wrangler/react-router)
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
rm -rf node_modules package-lock.json
|
|
@@ -62,41 +62,12 @@ cp -f app/config-example/admin-service.json app/config/admin-service.json
|
|
|
62
62
|
npx wrangler login
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
7) Run guided config + full deployment
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
npm run deploy:all
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
## Publish To npmjs And GitHub Packages
|
|
72
|
-
|
|
73
|
-
1) Verify auth for both registries
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
npm whoami --registry=https://registry.npmjs.org/
|
|
77
|
-
npm whoami --registry=https://npm.pkg.github.com/
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
2) Publish current version to npmjs (public)
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
npm run publish:npm
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
3) Publish the same version to GitHub Packages
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
npm run publish:github
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
4) Optional dry-runs before a real release
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
npm run publish:npm:dry-run
|
|
96
|
-
npm run publish:github:dry-run
|
|
97
|
-
npm run publish:all:dry-run
|
|
98
|
-
```
|
|
99
|
-
|
|
100
71
|
## NPM Package Content Policy
|
|
101
72
|
|
|
102
73
|
This package intentionally includes only non-sensitive defaults and runtime source needed for setup.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
2
|
-
import { AnnotationData, CaseExportData, AllCasesExportData, ExportOptions } from '~/types';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
import { type AnnotationData, type CaseExportData, type AllCasesExportData, type ExportOptions } from '~/types';
|
|
3
3
|
import { fetchFiles } from '../image-manage';
|
|
4
4
|
import { getNotes } from '../notes-manage';
|
|
5
5
|
import { checkExistingCase, validateCaseNumber, listCases } from '../case-manage';
|
|
@@ -1,13 +1,68 @@
|
|
|
1
|
-
import { CaseExportData } from '~/types';
|
|
1
|
+
import { type CaseExportData } from '~/types';
|
|
2
2
|
import { calculateSHA256Secure } from '~/utils/SHA256';
|
|
3
3
|
import { CSV_HEADERS } from './types-constants';
|
|
4
4
|
import { addForensicDataWarning } from './metadata-helpers';
|
|
5
5
|
|
|
6
|
+
export type TabularCell = string | number | boolean | null;
|
|
7
|
+
|
|
8
|
+
const MAX_SPREADSHEET_CELL_LENGTH = 32767;
|
|
9
|
+
const DANGEROUS_FORMULA_PREFIX_PATTERN = /^\s*[=+\-@]/;
|
|
10
|
+
|
|
11
|
+
function stripUnsafeControlChars(input: string): string {
|
|
12
|
+
let output = '';
|
|
13
|
+
|
|
14
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
15
|
+
const code = input.charCodeAt(index);
|
|
16
|
+
const isControlChar = code <= 0x1f || code === 0x7f;
|
|
17
|
+
const isAllowedWhitespace = code === 0x09 || code === 0x0a || code === 0x0d;
|
|
18
|
+
|
|
19
|
+
if (!isControlChar || isAllowedWhitespace) {
|
|
20
|
+
output += input[index];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return output;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sanitize cell values before CSV/XLSX export.
|
|
29
|
+
* - Strips non-printable control characters (except tab/newline/carriage return)
|
|
30
|
+
* - Prevents formula injection when files are opened in spreadsheet tools
|
|
31
|
+
* - Caps content length to Excel's per-cell limit
|
|
32
|
+
*/
|
|
33
|
+
export function sanitizeTabularCell(value: unknown): TabularCell {
|
|
34
|
+
if (value === null || value === undefined) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let normalized = stripUnsafeControlChars(String(value));
|
|
43
|
+
|
|
44
|
+
if (normalized.length > MAX_SPREADSHEET_CELL_LENGTH) {
|
|
45
|
+
normalized = normalized.slice(0, MAX_SPREADSHEET_CELL_LENGTH);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (DANGEROUS_FORMULA_PREFIX_PATTERN.test(normalized)) {
|
|
49
|
+
return `'${normalized}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function sanitizeTabularMatrix(
|
|
56
|
+
rows: Array<Array<string | number | boolean | null | undefined>>
|
|
57
|
+
): TabularCell[][] {
|
|
58
|
+
return rows.map((row) => row.map((cell) => sanitizeTabularCell(cell)));
|
|
59
|
+
}
|
|
60
|
+
|
|
6
61
|
/**
|
|
7
62
|
* Generate metadata rows for tabular format
|
|
8
63
|
*/
|
|
9
|
-
export function generateMetadataRows(exportData: CaseExportData):
|
|
10
|
-
return [
|
|
64
|
+
export function generateMetadataRows(exportData: CaseExportData): TabularCell[][] {
|
|
65
|
+
return sanitizeTabularMatrix([
|
|
11
66
|
['Case Export Report'],
|
|
12
67
|
[''],
|
|
13
68
|
['Case Number', exportData.metadata.caseNumber],
|
|
@@ -31,13 +86,13 @@ export function generateMetadataRows(exportData: CaseExportData): string[][] {
|
|
|
31
86
|
['Latest Annotation Date', exportData.summary?.latestAnnotationDate || 'N/A'],
|
|
32
87
|
[''],
|
|
33
88
|
['File Details']
|
|
34
|
-
];
|
|
89
|
+
]);
|
|
35
90
|
}
|
|
36
91
|
|
|
37
92
|
/**
|
|
38
93
|
* Process file data for tabular format (CSV/Excel)
|
|
39
94
|
*/
|
|
40
|
-
export function processFileDataForTabular(fileEntry: CaseExportData['files'][0]):
|
|
95
|
+
export function processFileDataForTabular(fileEntry: CaseExportData['files'][0]): TabularCell[][] {
|
|
41
96
|
// Full file data for the first row (excluding Additional Notes and Last Updated)
|
|
42
97
|
const fullFileData = [
|
|
43
98
|
fileEntry.fileData.id,
|
|
@@ -83,7 +138,7 @@ export function processFileDataForTabular(fileEntry: CaseExportData['files'][0])
|
|
|
83
138
|
const emptyFileData = Array(fileDataColumnCount).fill('');
|
|
84
139
|
const emptyAdditionalData = Array(additionalDataColumnCount).fill('');
|
|
85
140
|
|
|
86
|
-
const rows: string
|
|
141
|
+
const rows: Array<Array<string | number | boolean | null | undefined>> = [];
|
|
87
142
|
|
|
88
143
|
// If there are box annotations, create a row for each one
|
|
89
144
|
if (fileEntry.annotations?.boxAnnotations && fileEntry.annotations.boxAnnotations.length > 0) {
|
|
@@ -120,7 +175,7 @@ export function processFileDataForTabular(fileEntry: CaseExportData['files'][0])
|
|
|
120
175
|
]);
|
|
121
176
|
}
|
|
122
177
|
|
|
123
|
-
return rows;
|
|
178
|
+
return sanitizeTabularMatrix(rows);
|
|
124
179
|
}
|
|
125
180
|
|
|
126
181
|
/**
|
|
@@ -131,16 +186,16 @@ export async function generateCSVContent(exportData: CaseExportData, protectFore
|
|
|
131
186
|
const metadataRows = generateMetadataRows(exportData);
|
|
132
187
|
|
|
133
188
|
// File data rows
|
|
134
|
-
const fileRows:
|
|
189
|
+
const fileRows: TabularCell[][] = [];
|
|
135
190
|
exportData.files.forEach(fileEntry => {
|
|
136
191
|
const processedRows = processFileDataForTabular(fileEntry);
|
|
137
192
|
fileRows.push(...processedRows);
|
|
138
193
|
});
|
|
139
194
|
|
|
140
195
|
// Combine data rows for hash calculation (excluding header comments)
|
|
141
|
-
const dataRows = [
|
|
196
|
+
const dataRows: TabularCell[][] = [
|
|
142
197
|
...metadataRows,
|
|
143
|
-
CSV_HEADERS,
|
|
198
|
+
...sanitizeTabularMatrix([CSV_HEADERS]),
|
|
144
199
|
...fileRows
|
|
145
200
|
];
|
|
146
201
|
|
|
@@ -1,13 +1,100 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
2
|
-
import
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
import type * as ExcelJSModule from 'exceljs';
|
|
3
|
+
import { type FileData, type AllCasesExportData, type CaseExportData, type ExportOptions } from '~/types';
|
|
3
4
|
import { getImageUrl } from '../image-manage';
|
|
4
5
|
import { generateForensicManifestSecure, calculateSHA256Secure } from '~/utils/SHA256';
|
|
5
6
|
import { signForensicManifest } from '~/utils/data-operations';
|
|
6
|
-
import { ExportFormat, formatDateForFilename, CSV_HEADERS } from './types-constants';
|
|
7
|
+
import { type ExportFormat, formatDateForFilename, CSV_HEADERS } from './types-constants';
|
|
7
8
|
import { protectExcelWorksheet, addForensicDataWarning } from './metadata-helpers';
|
|
8
|
-
import { generateMetadataRows, generateCSVContent, processFileDataForTabular } from './data-processing';
|
|
9
|
+
import { generateMetadataRows, generateCSVContent, processFileDataForTabular, sanitizeTabularMatrix } from './data-processing';
|
|
9
10
|
import { exportCaseData } from './core-export';
|
|
10
|
-
import { auditService } from '~/services/audit
|
|
11
|
+
import { auditService } from '~/services/audit';
|
|
12
|
+
|
|
13
|
+
type TabularRow = Array<string | number | boolean | null | undefined>;
|
|
14
|
+
type ExcelJsBrowserBundle = typeof ExcelJSModule;
|
|
15
|
+
|
|
16
|
+
const EXCELJS_BROWSER_BUNDLE_SRC = '/vendor/exceljs.bare.min.js';
|
|
17
|
+
let excelJsBundlePromise: Promise<ExcelJsBrowserBundle> | null = null;
|
|
18
|
+
|
|
19
|
+
async function loadExcelJsBrowserBundle(): Promise<ExcelJsBrowserBundle> {
|
|
20
|
+
if (typeof window === 'undefined') {
|
|
21
|
+
throw new Error('Excel export is only available in a browser context.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (window.ExcelJS?.Workbook) {
|
|
25
|
+
return window.ExcelJS;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!excelJsBundlePromise) {
|
|
29
|
+
excelJsBundlePromise = new Promise((resolve, reject) => {
|
|
30
|
+
const resolveFromWindow = () => {
|
|
31
|
+
if (window.ExcelJS?.Workbook) {
|
|
32
|
+
resolve(window.ExcelJS);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
excelJsBundlePromise = null;
|
|
37
|
+
reject(new Error('ExcelJS bundle loaded but Workbook API is unavailable.'));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const failLoad = () => {
|
|
41
|
+
excelJsBundlePromise = null;
|
|
42
|
+
reject(new Error('Failed to load ExcelJS browser bundle.'));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const existingScript = document.querySelector<HTMLScriptElement>('script[data-exceljs-bundle="true"]');
|
|
46
|
+
|
|
47
|
+
if (existingScript) {
|
|
48
|
+
if (existingScript.dataset.loaded === 'true') {
|
|
49
|
+
resolveFromWindow();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
existingScript.addEventListener('load', resolveFromWindow, { once: true });
|
|
54
|
+
existingScript.addEventListener('error', failLoad, { once: true });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const script = document.createElement('script');
|
|
59
|
+
script.src = EXCELJS_BROWSER_BUNDLE_SRC;
|
|
60
|
+
script.async = true;
|
|
61
|
+
script.dataset.exceljsBundle = 'true';
|
|
62
|
+
script.addEventListener(
|
|
63
|
+
'load',
|
|
64
|
+
() => {
|
|
65
|
+
script.dataset.loaded = 'true';
|
|
66
|
+
resolveFromWindow();
|
|
67
|
+
},
|
|
68
|
+
{ once: true }
|
|
69
|
+
);
|
|
70
|
+
script.addEventListener('error', failLoad, { once: true });
|
|
71
|
+
document.head.appendChild(script);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return excelJsBundlePromise;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sanitizeWorksheetName(name: string): string {
|
|
79
|
+
const cleaned = name.replace(/[\\/?*:\x5B\x5D]/g, '_').trim();
|
|
80
|
+
const normalized = cleaned.length > 0 ? cleaned : 'Sheet';
|
|
81
|
+
return normalized.substring(0, 31);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createUniqueWorksheetName(existingNames: Set<string>, desiredName: string): string {
|
|
85
|
+
const baseName = sanitizeWorksheetName(desiredName);
|
|
86
|
+
let candidate = baseName;
|
|
87
|
+
let suffix = 1;
|
|
88
|
+
|
|
89
|
+
while (existingNames.has(candidate)) {
|
|
90
|
+
const suffixText = `_${suffix}`;
|
|
91
|
+
candidate = `${baseName.substring(0, 31 - suffixText.length)}${suffixText}`;
|
|
92
|
+
suffix += 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
existingNames.add(candidate);
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
11
98
|
|
|
12
99
|
/**
|
|
13
100
|
* Generate export filename with embedded ID to prevent collisions
|
|
@@ -122,10 +209,22 @@ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExpo
|
|
|
122
209
|
// Start audit workflow
|
|
123
210
|
auditService.startWorkflow('all-cases');
|
|
124
211
|
|
|
125
|
-
|
|
126
|
-
const XLSX = await import('xlsx');
|
|
212
|
+
const ExcelJS = await loadExcelJsBrowserBundle();
|
|
127
213
|
|
|
128
|
-
const workbook =
|
|
214
|
+
const workbook = new ExcelJS.Workbook();
|
|
215
|
+
workbook.creator = exportData.metadata.exportedBy || 'Striae';
|
|
216
|
+
workbook.lastModifiedBy = exportData.metadata.exportedBy || 'Striae';
|
|
217
|
+
workbook.created = new Date();
|
|
218
|
+
workbook.modified = new Date();
|
|
219
|
+
|
|
220
|
+
const existingWorksheetNames = new Set<string>();
|
|
221
|
+
|
|
222
|
+
const appendRowsToWorksheet = (worksheet: { addRow: (row: TabularRow) => unknown }, rows: TabularRow[]) => {
|
|
223
|
+
rows.forEach((row) => {
|
|
224
|
+
worksheet.addRow(row);
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
129
228
|
let exportPassword: string | undefined;
|
|
130
229
|
|
|
131
230
|
// Create summary worksheet
|
|
@@ -146,7 +245,7 @@ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExpo
|
|
|
146
245
|
];
|
|
147
246
|
|
|
148
247
|
// XLSX files are inherently protected, no hash validation needed
|
|
149
|
-
const summaryData = [
|
|
248
|
+
const summaryData = sanitizeTabularMatrix([
|
|
150
249
|
protectForensicData ? ['CASE DATA - PROTECTED EXPORT'] : ['Striae - All Cases Export Summary'],
|
|
151
250
|
protectForensicData ? ['WARNING: This workbook contains evidence data and is protected from editing.'] : [''],
|
|
152
251
|
[''],
|
|
@@ -195,36 +294,37 @@ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExpo
|
|
|
195
294
|
caseData.summary?.latestAnnotationDate || '',
|
|
196
295
|
caseData.summary?.exportError || ''
|
|
197
296
|
])
|
|
198
|
-
];
|
|
297
|
+
]);
|
|
199
298
|
|
|
200
|
-
const
|
|
299
|
+
const summaryWorksheetName = createUniqueWorksheetName(existingWorksheetNames, 'Summary');
|
|
300
|
+
const summaryWorksheet = workbook.addWorksheet(summaryWorksheetName);
|
|
301
|
+
appendRowsToWorksheet(summaryWorksheet, summaryData);
|
|
201
302
|
|
|
202
303
|
// Protect summary worksheet if forensic protection is enabled
|
|
203
304
|
if (protectForensicData) {
|
|
204
|
-
exportPassword = protectExcelWorksheet(summaryWorksheet);
|
|
305
|
+
exportPassword = await protectExcelWorksheet(summaryWorksheet);
|
|
205
306
|
}
|
|
206
|
-
|
|
207
|
-
XLSX.utils.book_append_sheet(workbook, summaryWorksheet, 'Summary');
|
|
208
307
|
|
|
209
308
|
// Create a worksheet for each case
|
|
210
|
-
exportData.cases
|
|
309
|
+
for (const caseData of exportData.cases) {
|
|
211
310
|
if (caseData.summary?.exportError) {
|
|
212
311
|
// For failed cases, create a simple error sheet
|
|
213
|
-
const errorData = [
|
|
312
|
+
const errorData = sanitizeTabularMatrix([
|
|
214
313
|
[`Case ${caseData.metadata.caseNumber} - Export Failed`],
|
|
215
314
|
[''],
|
|
216
315
|
['Error:', caseData.summary.exportError],
|
|
217
316
|
['Case Number:', caseData.metadata.caseNumber],
|
|
218
317
|
['Total Files:', caseData.metadata.totalFiles]
|
|
219
|
-
];
|
|
220
|
-
const
|
|
318
|
+
]);
|
|
319
|
+
const errorSheetName = createUniqueWorksheetName(existingWorksheetNames, `Case_${caseData.metadata.caseNumber}_Error`);
|
|
320
|
+
const errorWorksheet = workbook.addWorksheet(errorSheetName);
|
|
321
|
+
appendRowsToWorksheet(errorWorksheet, errorData);
|
|
221
322
|
|
|
222
323
|
if (protectForensicData && exportPassword) {
|
|
223
|
-
protectExcelWorksheet(errorWorksheet, exportPassword);
|
|
324
|
+
await protectExcelWorksheet(errorWorksheet, exportPassword);
|
|
224
325
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return;
|
|
326
|
+
|
|
327
|
+
continue;
|
|
228
328
|
}
|
|
229
329
|
|
|
230
330
|
// For successful cases, create detailed worksheets
|
|
@@ -257,36 +357,22 @@ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExpo
|
|
|
257
357
|
caseDetailsData.push(['No detailed file data available for this case']);
|
|
258
358
|
}
|
|
259
359
|
|
|
260
|
-
const
|
|
360
|
+
const sanitizedCaseDetailsData = sanitizeTabularMatrix(caseDetailsData);
|
|
361
|
+
|
|
362
|
+
// Clean sheet name for Excel compatibility and uniqueness
|
|
363
|
+
const sheetName = createUniqueWorksheetName(existingWorksheetNames, `Case_${caseData.metadata.caseNumber}`);
|
|
364
|
+
const caseWorksheet = workbook.addWorksheet(sheetName);
|
|
365
|
+
appendRowsToWorksheet(caseWorksheet, sanitizedCaseDetailsData);
|
|
261
366
|
|
|
262
367
|
// Protect worksheet if forensic protection is enabled
|
|
263
368
|
if (protectForensicData && exportPassword) {
|
|
264
|
-
protectExcelWorksheet(caseWorksheet, exportPassword);
|
|
369
|
+
await protectExcelWorksheet(caseWorksheet, exportPassword);
|
|
265
370
|
}
|
|
266
|
-
|
|
267
|
-
// Clean sheet name for Excel compatibility
|
|
268
|
-
const sheetName = `Case_${caseData.metadata.caseNumber}`.replace(/[\\/?*\x5B\x5D]/g, '_').substring(0, 31);
|
|
269
|
-
XLSX.utils.book_append_sheet(workbook, caseWorksheet, sheetName);
|
|
270
|
-
});
|
|
271
371
|
|
|
272
|
-
// Set workbook protection if forensic protection is enabled
|
|
273
|
-
if (protectForensicData && exportPassword) {
|
|
274
|
-
workbook.Props = {
|
|
275
|
-
Title: 'Striae Case Export - Protected',
|
|
276
|
-
Subject: 'Case Data Export',
|
|
277
|
-
Author: exportData.metadata.exportedBy || 'Striae',
|
|
278
|
-
Comments: `This workbook contains protected case data. Modification may compromise evidence integrity. Worksheets are password protected.`,
|
|
279
|
-
Company: 'Striae'
|
|
280
|
-
};
|
|
281
372
|
}
|
|
282
373
|
|
|
283
374
|
// Generate Excel file
|
|
284
|
-
const excelBuffer =
|
|
285
|
-
bookType: 'xlsx',
|
|
286
|
-
type: 'array',
|
|
287
|
-
bookSST: true, // Shared string table for better compression
|
|
288
|
-
cellStyles: true
|
|
289
|
-
});
|
|
375
|
+
const excelBuffer = await workbook.xlsx.writeBuffer();
|
|
290
376
|
|
|
291
377
|
// Create blob and download
|
|
292
378
|
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import { getUserData } from '~/utils/permissions';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -68,16 +68,38 @@ export function generateRandomPassword(): string {
|
|
|
68
68
|
return password.split('').sort(() => Math.random() - 0.5).join('');
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
type WorksheetProtectionOptions = {
|
|
72
|
+
selectLockedCells: boolean;
|
|
73
|
+
selectUnlockedCells: boolean;
|
|
74
|
+
formatCells: boolean;
|
|
75
|
+
formatColumns: boolean;
|
|
76
|
+
formatRows: boolean;
|
|
77
|
+
insertColumns: boolean;
|
|
78
|
+
insertRows: boolean;
|
|
79
|
+
insertHyperlinks: boolean;
|
|
80
|
+
deleteColumns: boolean;
|
|
81
|
+
deleteRows: boolean;
|
|
82
|
+
sort: boolean;
|
|
83
|
+
autoFilter: boolean;
|
|
84
|
+
pivotTables: boolean;
|
|
85
|
+
objects: boolean;
|
|
86
|
+
scenarios: boolean;
|
|
87
|
+
spinCount: number;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type ProtectableWorksheet = {
|
|
91
|
+
protect: (password: string, options: Record<string, unknown>) => Promise<unknown> | unknown;
|
|
92
|
+
};
|
|
93
|
+
|
|
71
94
|
/**
|
|
72
95
|
* Protect Excel worksheet from editing
|
|
73
96
|
*/
|
|
74
|
-
export function protectExcelWorksheet(worksheet:
|
|
97
|
+
export async function protectExcelWorksheet(worksheet: ProtectableWorksheet, sheetPassword?: string): Promise<string> {
|
|
75
98
|
// Generate random password if none provided
|
|
76
99
|
const password = sheetPassword || generateRandomPassword();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
password: password,
|
|
100
|
+
|
|
101
|
+
const protectionOptions: WorksheetProtectionOptions = {
|
|
102
|
+
// Keep read-only defaults and prevent structural edits.
|
|
81
103
|
selectLockedCells: true,
|
|
82
104
|
selectUnlockedCells: true,
|
|
83
105
|
formatCells: false,
|
|
@@ -92,15 +114,11 @@ export function protectExcelWorksheet(worksheet: Record<string, unknown>, sheetP
|
|
|
92
114
|
autoFilter: false,
|
|
93
115
|
pivotTables: false,
|
|
94
116
|
objects: false,
|
|
95
|
-
scenarios: false
|
|
117
|
+
scenarios: false,
|
|
118
|
+
spinCount: 100000
|
|
96
119
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!worksheet['!cols']) worksheet['!cols'] = [];
|
|
100
|
-
if (!worksheet['!rows']) worksheet['!rows'] = [];
|
|
101
|
-
|
|
102
|
-
// Add protection metadata
|
|
103
|
-
worksheet['!margins'] = { left: 0.7, right: 0.7, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3 };
|
|
120
|
+
|
|
121
|
+
await Promise.resolve(worksheet.protect(password, protectionOptions as Record<string, unknown>));
|
|
104
122
|
|
|
105
123
|
// Return the password for inclusion in metadata
|
|
106
124
|
return password;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import paths from '~/config/config.json';
|
|
3
3
|
import { getDataApiKey } from '~/utils/auth';
|
|
4
|
-
import { ConfirmationImportResult, ConfirmationImportData } from '~/types';
|
|
4
|
+
import { type ConfirmationImportResult, type ConfirmationImportData } from '~/types';
|
|
5
5
|
import { checkExistingCase } from '../case-manage';
|
|
6
6
|
import { validateExporterUid, validateConfirmationHash, validateConfirmationSignatureFile } from './validation';
|
|
7
|
-
import { auditService } from '~/services/audit
|
|
7
|
+
import { auditService } from '~/services/audit';
|
|
8
8
|
|
|
9
9
|
const DATA_WORKER_URL = paths.data_worker_url;
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import paths from '~/config/config.json';
|
|
2
2
|
import { getImageApiKey } from '~/utils/auth';
|
|
3
|
-
import { FileData, ImageUploadResponse } from '~/types';
|
|
3
|
+
import { type FileData, type ImageUploadResponse } from '~/types';
|
|
4
4
|
|
|
5
5
|
const IMAGE_WORKER_URL = paths.image_worker_url;
|
|
6
6
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
2
|
-
import { ImportOptions, ImportResult, ReadOnlyCaseMetadata, FileData } from '~/types';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
import { type ImportOptions, type ImportResult, type ReadOnlyCaseMetadata, type FileData } from '~/types';
|
|
3
3
|
import { checkExistingCase } from '../case-manage';
|
|
4
4
|
import {
|
|
5
5
|
extractForensicManifestData,
|
|
6
|
-
SignedForensicManifest,
|
|
6
|
+
type SignedForensicManifest,
|
|
7
7
|
validateCaseIntegritySecure as validateForensicIntegrity,
|
|
8
8
|
verifyForensicManifestSignature
|
|
9
9
|
} from '~/utils/SHA256';
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from './storage-operations';
|
|
20
20
|
import { uploadImageBlob } from './image-operations';
|
|
21
21
|
import { importAnnotations } from './annotation-import';
|
|
22
|
-
import { auditService } from '~/services/audit
|
|
22
|
+
import { auditService } from '~/services/audit';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Track the state of an import operation for cleanup purposes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import paths from '~/config/config.json';
|
|
3
3
|
import {
|
|
4
4
|
getDataApiKey,
|
|
@@ -10,14 +10,14 @@ import {
|
|
|
10
10
|
validateUserSession
|
|
11
11
|
} from '~/utils/permissions';
|
|
12
12
|
import {
|
|
13
|
-
CaseExportData,
|
|
14
|
-
ExtendedUserData,
|
|
15
|
-
FileData,
|
|
16
|
-
CaseData,
|
|
17
|
-
ReadOnlyCaseMetadata
|
|
13
|
+
type CaseExportData,
|
|
14
|
+
type ExtendedUserData,
|
|
15
|
+
type FileData,
|
|
16
|
+
type CaseData,
|
|
17
|
+
type ReadOnlyCaseMetadata
|
|
18
18
|
} from '~/types';
|
|
19
19
|
import { deleteFile } from '../image-manage';
|
|
20
|
-
import { SignedForensicManifest } from '~/utils/SHA256';
|
|
20
|
+
import { type SignedForensicManifest } from '~/utils/SHA256';
|
|
21
21
|
|
|
22
22
|
const USER_WORKER_URL = paths.user_worker_url;
|
|
23
23
|
const DATA_WORKER_URL = paths.data_worker_url;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import paths from '~/config/config.json';
|
|
3
3
|
import { getUserApiKey } from '~/utils/auth';
|
|
4
|
-
import { CaseExportData, ConfirmationImportData } from '~/types';
|
|
5
|
-
import { calculateSHA256Secure, ManifestSignatureVerificationResult } from '~/utils/SHA256';
|
|
4
|
+
import { type CaseExportData, type ConfirmationImportData } from '~/types';
|
|
5
|
+
import { calculateSHA256Secure, type ManifestSignatureVerificationResult } from '~/utils/SHA256';
|
|
6
6
|
import { verifyConfirmationSignature } from '~/utils/confirmation-signature';
|
|
7
7
|
|
|
8
8
|
const USER_WORKER_URL = paths.user_worker_url;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
2
|
-
import { CaseExportData, CaseImportPreview } from '~/types';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
import { type CaseExportData, type CaseImportPreview } from '~/types';
|
|
3
3
|
import { validateCaseNumber } from '../case-manage';
|
|
4
4
|
import {
|
|
5
5
|
extractForensicManifestData,
|
|
6
|
-
SignedForensicManifest,
|
|
6
|
+
type SignedForensicManifest,
|
|
7
7
|
validateCaseIntegritySecure as validateForensicIntegrity,
|
|
8
8
|
verifyForensicManifestSignature
|
|
9
9
|
} from '~/utils/SHA256';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import {
|
|
3
3
|
canCreateCase,
|
|
4
4
|
getUserCases,
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
duplicateCaseData,
|
|
14
14
|
deleteFileAnnotations
|
|
15
15
|
} from '~/utils/data-operations';
|
|
16
|
-
import { CaseData, ReadOnlyCaseData, FileData } from '~/types';
|
|
17
|
-
import { auditService } from '~/services/audit
|
|
16
|
+
import { type CaseData, type ReadOnlyCaseData, type FileData } from '~/types';
|
|
17
|
+
import { auditService } from '~/services/audit';
|
|
18
18
|
import { getImageApiKey } from '~/utils/auth';
|
|
19
19
|
import paths from '~/config/config.json';
|
|
20
20
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import { calculateSHA256Secure } from '~/utils/SHA256';
|
|
3
3
|
import { getUserData } from '~/utils/permissions';
|
|
4
4
|
import { getCaseData, updateCaseData, signConfirmationData } from '~/utils/data-operations';
|
|
5
|
-
import { ConfirmationData, CaseConfirmations, CaseDataWithConfirmations, ConfirmationImportData } from '~/types';
|
|
6
|
-
import { auditService } from '~/services/audit
|
|
5
|
+
import { type ConfirmationData, type CaseConfirmations, type CaseDataWithConfirmations, type ConfirmationImportData } from '~/types';
|
|
6
|
+
import { auditService } from '~/services/audit';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Store a confirmation for a specific image, linked to the original image ID
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import paths from '~/config/config.json';
|
|
2
|
-
import { AnnotationData } from '~/types/annotations';
|
|
3
|
-
import { auditService } from '~/services/audit
|
|
2
|
+
import { type AnnotationData } from '~/types/annotations';
|
|
3
|
+
import { auditService } from '~/services/audit';
|
|
4
4
|
import { getPdfApiKey } from '~/utils/auth';
|
|
5
|
-
import { User } from 'firebase/auth';
|
|
5
|
+
import type { User } from 'firebase/auth';
|
|
6
6
|
|
|
7
7
|
interface GeneratePDFParams {
|
|
8
8
|
user: User;
|