@striae-org/striae 3.0.4
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 +100 -0
- package/LICENSE +190 -0
- package/NOTICE +18 -0
- package/README.md +133 -0
- package/app/components/actions/case-export/core-export.ts +328 -0
- package/app/components/actions/case-export/data-processing.ts +167 -0
- package/app/components/actions/case-export/download-handlers.ts +900 -0
- package/app/components/actions/case-export/index.ts +41 -0
- package/app/components/actions/case-export/metadata-helpers.ts +107 -0
- package/app/components/actions/case-export/types-constants.ts +56 -0
- package/app/components/actions/case-export/validation-utils.ts +25 -0
- package/app/components/actions/case-export.ts +4 -0
- package/app/components/actions/case-import/annotation-import.ts +35 -0
- package/app/components/actions/case-import/confirmation-import.ts +363 -0
- package/app/components/actions/case-import/image-operations.ts +61 -0
- package/app/components/actions/case-import/index.ts +39 -0
- package/app/components/actions/case-import/orchestrator.ts +420 -0
- package/app/components/actions/case-import/storage-operations.ts +270 -0
- package/app/components/actions/case-import/validation.ts +189 -0
- package/app/components/actions/case-import/zip-processing.ts +413 -0
- package/app/components/actions/case-manage.ts +524 -0
- package/app/components/actions/case-review.ts +4 -0
- package/app/components/actions/confirm-export.ts +351 -0
- package/app/components/actions/generate-pdf.ts +210 -0
- package/app/components/actions/image-manage.ts +385 -0
- package/app/components/actions/notes-manage.ts +33 -0
- package/app/components/actions/signout.module.css +15 -0
- package/app/components/actions/signout.tsx +50 -0
- package/app/components/audit/user-audit-viewer.tsx +975 -0
- package/app/components/audit/user-audit.module.css +568 -0
- package/app/components/auth/auth-provider.tsx +78 -0
- package/app/components/auth/mfa-enrollment.module.css +268 -0
- package/app/components/auth/mfa-enrollment.tsx +398 -0
- package/app/components/auth/mfa-verification.module.css +251 -0
- package/app/components/auth/mfa-verification.tsx +295 -0
- package/app/components/button/button.module.css +63 -0
- package/app/components/button/button.tsx +46 -0
- package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
- package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
- package/app/components/canvas/canvas.module.css +314 -0
- package/app/components/canvas/canvas.tsx +449 -0
- package/app/components/canvas/confirmation/confirmation.module.css +187 -0
- package/app/components/canvas/confirmation/confirmation.tsx +214 -0
- package/app/components/colors/colors.module.css +59 -0
- package/app/components/colors/colors.tsx +68 -0
- package/app/components/form/base-form.tsx +21 -0
- package/app/components/form/form-button.tsx +28 -0
- package/app/components/form/form-field.tsx +53 -0
- package/app/components/form/form-message.tsx +17 -0
- package/app/components/form/form-toggle.tsx +23 -0
- package/app/components/form/form.module.css +427 -0
- package/app/components/form/index.ts +6 -0
- package/app/components/icon/icon.module.css +3 -0
- package/app/components/icon/icon.tsx +27 -0
- package/app/components/icon/icons.svg +102 -0
- package/app/components/icon/manifest.json +110 -0
- package/app/components/sidebar/case-export/case-export.module.css +386 -0
- package/app/components/sidebar/case-export/case-export.tsx +317 -0
- package/app/components/sidebar/case-import/case-import.module.css +626 -0
- package/app/components/sidebar/case-import/case-import.tsx +404 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
- package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
- package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
- package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
- package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
- package/app/components/sidebar/case-import/index.ts +18 -0
- package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
- package/app/components/sidebar/cases/cases-modal.module.css +166 -0
- package/app/components/sidebar/cases/cases-modal.tsx +201 -0
- package/app/components/sidebar/cases/cases.module.css +713 -0
- package/app/components/sidebar/files/files-modal.module.css +209 -0
- package/app/components/sidebar/files/files-modal.tsx +239 -0
- package/app/components/sidebar/hash/hash-utility.module.css +366 -0
- package/app/components/sidebar/hash/hash-utility.tsx +982 -0
- package/app/components/sidebar/notes/notes-modal.tsx +51 -0
- package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
- package/app/components/sidebar/notes/notes.module.css +360 -0
- package/app/components/sidebar/sidebar-container.tsx +149 -0
- package/app/components/sidebar/sidebar.module.css +321 -0
- package/app/components/sidebar/sidebar.tsx +215 -0
- package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
- package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
- package/app/components/theme-provider/theme-provider.tsx +131 -0
- package/app/components/theme-provider/theme.ts +155 -0
- package/app/components/toast/toast.module.css +137 -0
- package/app/components/toast/toast.tsx +56 -0
- package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
- package/app/components/toolbar/toolbar.module.css +42 -0
- package/app/components/toolbar/toolbar.tsx +167 -0
- package/app/components/user/delete-account.module.css +274 -0
- package/app/components/user/delete-account.tsx +471 -0
- package/app/components/user/inactivity-warning.module.css +145 -0
- package/app/components/user/inactivity-warning.tsx +84 -0
- package/app/components/user/manage-profile.module.css +190 -0
- package/app/components/user/manage-profile.tsx +253 -0
- package/app/components/user/mfa-phone-update.tsx +739 -0
- package/app/config-example/admin-service.json +13 -0
- package/app/config-example/config.json +17 -0
- package/app/config-example/firebase.ts +21 -0
- package/app/config-example/inactivity.ts +13 -0
- package/app/config-example/meta-config.json +6 -0
- package/app/contexts/auth.context.ts +12 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +44 -0
- package/app/hooks/useInactivityTimeout.ts +110 -0
- package/app/root.tsx +170 -0
- package/app/routes/_index.tsx +16 -0
- package/app/routes/auth/emailActionHandler.module.css +232 -0
- package/app/routes/auth/emailActionHandler.tsx +405 -0
- package/app/routes/auth/emailVerification.tsx +120 -0
- package/app/routes/auth/login.module.css +523 -0
- package/app/routes/auth/login.tsx +654 -0
- package/app/routes/auth/passwordReset.module.css +274 -0
- package/app/routes/auth/passwordReset.tsx +154 -0
- package/app/routes/auth/route.ts +16 -0
- package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
- package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
- package/app/routes/mobile-prevented/route.ts +14 -0
- package/app/routes/striae/striae.module.css +30 -0
- package/app/routes/striae/striae.tsx +417 -0
- package/app/services/audit-export.service.ts +755 -0
- package/app/services/audit.service.ts +1454 -0
- package/app/services/firebase-errors.ts +106 -0
- package/app/services/firebase.ts +15 -0
- package/app/styles/legal-pages.module.css +113 -0
- package/app/styles/root.module.css +146 -0
- package/app/tailwind.css +225 -0
- package/app/types/annotations.ts +45 -0
- package/app/types/audit.ts +301 -0
- package/app/types/case.ts +90 -0
- package/app/types/export.ts +8 -0
- package/app/types/file.ts +30 -0
- package/app/types/import.ts +107 -0
- package/app/types/index.ts +24 -0
- package/app/types/user.ts +38 -0
- package/app/utils/SHA256.ts +461 -0
- package/app/utils/annotation-timestamp.ts +25 -0
- package/app/utils/audit-export-signature.ts +117 -0
- package/app/utils/auth-action-settings.ts +48 -0
- package/app/utils/auth.ts +34 -0
- package/app/utils/batch-operations.ts +135 -0
- package/app/utils/confirmation-signature.ts +193 -0
- package/app/utils/data-operations.ts +871 -0
- package/app/utils/device-detection.ts +5 -0
- package/app/utils/html-sanitizer.ts +80 -0
- package/app/utils/id-generator.ts +36 -0
- package/app/utils/meta.ts +48 -0
- package/app/utils/mfa-phone.ts +97 -0
- package/app/utils/mfa.ts +79 -0
- package/app/utils/password-policy.ts +28 -0
- package/app/utils/permissions.ts +562 -0
- package/app/utils/signature-utils.ts +160 -0
- package/app/utils/style.ts +83 -0
- package/app/utils/version.ts +5 -0
- package/firebase.json +11 -0
- package/functions/[[path]].ts +10 -0
- package/package.json +138 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/publickey.info@striae.org.asc +17 -0
- package/public/.well-known/security.txt +7 -0
- package/public/_headers +28 -0
- package/public/_routes.json +13 -0
- package/public/assets/striae.jpg +0 -0
- package/public/clear.jpg +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-dark.png +0 -0
- package/public/manifest.json +25 -0
- package/public/oin-badge.png +0 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/striae-ascii.txt +10 -0
- package/scripts/deploy-all.sh +100 -0
- package/scripts/deploy-config.sh +940 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-worker-secrets.sh +215 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/install-workers.sh +88 -0
- package/scripts/run-eslint.cjs +35 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/tailwind.config.ts +22 -0
- package/tsconfig.json +33 -0
- package/vite.config.ts +35 -0
- package/worker-configuration.d.ts +7490 -0
- package/workers/audit-worker/package.json +17 -0
- package/workers/audit-worker/src/audit-worker.example.ts +195 -0
- package/workers/audit-worker/worker-configuration.d.ts +7448 -0
- package/workers/audit-worker/wrangler.jsonc.example +29 -0
- package/workers/data-worker/package.json +17 -0
- package/workers/data-worker/src/data-worker.example.ts +267 -0
- package/workers/data-worker/src/signature-utils.ts +79 -0
- package/workers/data-worker/src/signing-payload-utils.ts +290 -0
- package/workers/data-worker/worker-configuration.d.ts +7448 -0
- package/workers/data-worker/wrangler.jsonc.example +30 -0
- package/workers/image-worker/package.json +17 -0
- package/workers/image-worker/src/image-worker.example.ts +180 -0
- package/workers/image-worker/worker-configuration.d.ts +7447 -0
- package/workers/image-worker/wrangler.jsonc.example +22 -0
- package/workers/keys-worker/package.json +17 -0
- package/workers/keys-worker/src/keys.example.ts +66 -0
- package/workers/keys-worker/src/keys.ts +66 -0
- package/workers/keys-worker/worker-configuration.d.ts +7447 -0
- package/workers/keys-worker/wrangler.jsonc.example +22 -0
- package/workers/pdf-worker/package.json +17 -0
- package/workers/pdf-worker/src/format-striae.ts +534 -0
- package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
- package/workers/pdf-worker/src/report-types.ts +69 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
- package/workers/pdf-worker/wrangler.jsonc.example +26 -0
- package/workers/user-worker/package.json +17 -0
- package/workers/user-worker/src/user-worker.example.ts +636 -0
- package/workers/user-worker/worker-configuration.d.ts +7448 -0
- package/workers/user-worker/wrangler.jsonc.example +29 -0
- package/wrangler.toml.example +8 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { User } from 'firebase/auth';
|
|
2
|
+
import { AnnotationData, CaseExportData, AllCasesExportData, ExportOptions } from '~/types';
|
|
3
|
+
import { fetchFiles } from '../image-manage';
|
|
4
|
+
import { getNotes } from '../notes-manage';
|
|
5
|
+
import { checkExistingCase, validateCaseNumber, listCases } from '../case-manage';
|
|
6
|
+
import { getUserExportMetadata } from './metadata-helpers';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Export all cases for a user
|
|
10
|
+
*/
|
|
11
|
+
export async function exportAllCases(
|
|
12
|
+
user: User,
|
|
13
|
+
options: ExportOptions = {},
|
|
14
|
+
onProgress?: (current: number, total: number, caseName: string) => void
|
|
15
|
+
): Promise<AllCasesExportData> {
|
|
16
|
+
// NOTE: startTime tracking moved to download handlers
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// NOTE: Audit workflow management moved to download handlers
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
includeMetadata = true
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
// Get user export metadata
|
|
26
|
+
const userMetadata = await getUserExportMetadata(user);
|
|
27
|
+
|
|
28
|
+
// Get list of all cases for the user
|
|
29
|
+
const caseNumbers = await listCases(user);
|
|
30
|
+
|
|
31
|
+
if (!caseNumbers || caseNumbers.length === 0) {
|
|
32
|
+
throw new Error('No cases found for user');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const exportedCases: CaseExportData[] = [];
|
|
36
|
+
let totalFiles = 0;
|
|
37
|
+
let totalAnnotations = 0;
|
|
38
|
+
let totalConfirmations = 0;
|
|
39
|
+
let totalConfirmationsRequested = 0;
|
|
40
|
+
let casesWithFiles = 0;
|
|
41
|
+
let casesWithAnnotations = 0;
|
|
42
|
+
let casesWithoutFiles = 0;
|
|
43
|
+
let lastModified: string | undefined;
|
|
44
|
+
let earliestAnnotationDate: string | undefined;
|
|
45
|
+
let latestAnnotationDate: string | undefined;
|
|
46
|
+
|
|
47
|
+
// Export each case
|
|
48
|
+
for (let i = 0; i < caseNumbers.length; i++) {
|
|
49
|
+
const caseNumber = caseNumbers[i];
|
|
50
|
+
|
|
51
|
+
// Report progress
|
|
52
|
+
if (onProgress) {
|
|
53
|
+
onProgress(i + 1, caseNumbers.length, caseNumber);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const caseExport = await exportCaseData(user, caseNumber, options);
|
|
58
|
+
exportedCases.push(caseExport);
|
|
59
|
+
|
|
60
|
+
// Update totals
|
|
61
|
+
totalFiles += caseExport.metadata.totalFiles;
|
|
62
|
+
|
|
63
|
+
if (caseExport.metadata.totalFiles > 0) {
|
|
64
|
+
casesWithFiles++;
|
|
65
|
+
} else {
|
|
66
|
+
casesWithoutFiles++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Count annotations and confirmations
|
|
70
|
+
const caseAnnotations = caseExport.files.filter(f => f.hasAnnotations).length;
|
|
71
|
+
if (caseAnnotations > 0) {
|
|
72
|
+
casesWithAnnotations++;
|
|
73
|
+
totalAnnotations += caseAnnotations;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Count confirmations
|
|
77
|
+
if (caseExport.summary?.filesWithConfirmations) {
|
|
78
|
+
totalConfirmations += caseExport.summary.filesWithConfirmations;
|
|
79
|
+
}
|
|
80
|
+
if (caseExport.summary?.filesWithConfirmationsRequested) {
|
|
81
|
+
totalConfirmationsRequested += caseExport.summary.filesWithConfirmationsRequested;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Track latest modification
|
|
85
|
+
if (caseExport.summary?.lastModified) {
|
|
86
|
+
if (!lastModified || caseExport.summary.lastModified > lastModified) {
|
|
87
|
+
lastModified = caseExport.summary.lastModified;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Track annotation date range across all cases
|
|
92
|
+
if (caseExport.summary?.earliestAnnotationDate) {
|
|
93
|
+
if (!earliestAnnotationDate || caseExport.summary.earliestAnnotationDate < earliestAnnotationDate) {
|
|
94
|
+
earliestAnnotationDate = caseExport.summary.earliestAnnotationDate;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (caseExport.summary?.latestAnnotationDate) {
|
|
98
|
+
if (!latestAnnotationDate || caseExport.summary.latestAnnotationDate > latestAnnotationDate) {
|
|
99
|
+
latestAnnotationDate = caseExport.summary.latestAnnotationDate;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// Get case creation date even for failed exports
|
|
105
|
+
let caseCreatedDate = new Date().toISOString(); // fallback
|
|
106
|
+
try {
|
|
107
|
+
const existingCase = await checkExistingCase(user, caseNumber);
|
|
108
|
+
if (existingCase?.createdAt) {
|
|
109
|
+
caseCreatedDate = existingCase.createdAt;
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Use fallback date if case lookup fails
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Create a placeholder entry for failed exports
|
|
116
|
+
exportedCases.push({
|
|
117
|
+
metadata: {
|
|
118
|
+
caseNumber,
|
|
119
|
+
caseCreatedDate,
|
|
120
|
+
exportDate: new Date().toISOString(),
|
|
121
|
+
...userMetadata,
|
|
122
|
+
striaeExportSchemaVersion: '1.0',
|
|
123
|
+
totalFiles: 0
|
|
124
|
+
},
|
|
125
|
+
files: [],
|
|
126
|
+
summary: {
|
|
127
|
+
filesWithAnnotations: 0,
|
|
128
|
+
filesWithoutAnnotations: 0,
|
|
129
|
+
totalBoxAnnotations: 0,
|
|
130
|
+
exportError: error instanceof Error ? error.message : 'Unknown error'
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
casesWithoutFiles++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const allCasesExport: AllCasesExportData = {
|
|
138
|
+
metadata: {
|
|
139
|
+
exportDate: new Date().toISOString(),
|
|
140
|
+
...userMetadata,
|
|
141
|
+
striaeExportSchemaVersion: '1.0',
|
|
142
|
+
totalCases: caseNumbers.length,
|
|
143
|
+
totalFiles,
|
|
144
|
+
totalAnnotations,
|
|
145
|
+
totalConfirmations,
|
|
146
|
+
totalConfirmationsRequested
|
|
147
|
+
},
|
|
148
|
+
cases: exportedCases
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (includeMetadata) {
|
|
152
|
+
allCasesExport.summary = {
|
|
153
|
+
casesWithFiles,
|
|
154
|
+
casesWithAnnotations,
|
|
155
|
+
casesWithoutFiles,
|
|
156
|
+
lastModified,
|
|
157
|
+
earliestAnnotationDate,
|
|
158
|
+
latestAnnotationDate
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Report completion
|
|
163
|
+
if (onProgress) {
|
|
164
|
+
onProgress(caseNumbers.length, caseNumbers.length, 'Export completed!');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// NOTE: Audit logging moved to download handlers where actual filename and format are known
|
|
168
|
+
|
|
169
|
+
return allCasesExport;
|
|
170
|
+
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('Export all cases failed:', error);
|
|
173
|
+
|
|
174
|
+
// NOTE: Audit logging for failures moved to download handlers
|
|
175
|
+
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Export case data with files and annotations
|
|
182
|
+
*/
|
|
183
|
+
export async function exportCaseData(
|
|
184
|
+
user: User,
|
|
185
|
+
caseNumber: string,
|
|
186
|
+
options: ExportOptions = {}
|
|
187
|
+
): Promise<CaseExportData> {
|
|
188
|
+
// NOTE: startTime and fileName tracking moved to download handlers
|
|
189
|
+
|
|
190
|
+
const {
|
|
191
|
+
includeMetadata = true
|
|
192
|
+
} = options;
|
|
193
|
+
|
|
194
|
+
// Get user export metadata
|
|
195
|
+
const userMetadata = await getUserExportMetadata(user);
|
|
196
|
+
|
|
197
|
+
// Validate case number format
|
|
198
|
+
if (!validateCaseNumber(caseNumber)) {
|
|
199
|
+
throw new Error('Invalid case number format');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check if case exists
|
|
203
|
+
const existingCase = await checkExistingCase(user, caseNumber);
|
|
204
|
+
if (!existingCase) {
|
|
205
|
+
throw new Error(`Case "${caseNumber}" does not exist`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// NOTE: Audit workflow management moved to download handlers
|
|
210
|
+
|
|
211
|
+
// Fetch all files for the case
|
|
212
|
+
const files = await fetchFiles(user, caseNumber);
|
|
213
|
+
|
|
214
|
+
if (!files || files.length === 0) {
|
|
215
|
+
throw new Error(`No files found for case: ${caseNumber}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Collect file data with annotations
|
|
219
|
+
const filesWithAnnotations: CaseExportData['files'] = [];
|
|
220
|
+
let filesWithAnnotationsCount = 0;
|
|
221
|
+
let totalBoxAnnotations = 0;
|
|
222
|
+
let filesWithConfirmationsCount = 0;
|
|
223
|
+
let filesWithConfirmationsRequestedCount = 0;
|
|
224
|
+
let lastModified: string | undefined;
|
|
225
|
+
let earliestAnnotationDate: string | undefined;
|
|
226
|
+
let latestAnnotationDate: string | undefined;
|
|
227
|
+
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
let annotations: AnnotationData | undefined;
|
|
230
|
+
let hasAnnotations = false;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
annotations = await getNotes(user, caseNumber, file.id) || undefined;
|
|
234
|
+
|
|
235
|
+
// Check if file has any annotation data beyond just defaults
|
|
236
|
+
hasAnnotations = !!(annotations && (
|
|
237
|
+
annotations.additionalNotes ||
|
|
238
|
+
annotations.classNote ||
|
|
239
|
+
annotations.customClass ||
|
|
240
|
+
annotations.leftCase ||
|
|
241
|
+
annotations.rightCase ||
|
|
242
|
+
annotations.leftItem ||
|
|
243
|
+
annotations.rightItem ||
|
|
244
|
+
annotations.supportLevel ||
|
|
245
|
+
annotations.classType ||
|
|
246
|
+
(annotations.boxAnnotations && annotations.boxAnnotations.length > 0)
|
|
247
|
+
));
|
|
248
|
+
|
|
249
|
+
if (hasAnnotations) {
|
|
250
|
+
filesWithAnnotationsCount++;
|
|
251
|
+
if (annotations?.boxAnnotations) {
|
|
252
|
+
totalBoxAnnotations += annotations.boxAnnotations.length;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Track confirmation data
|
|
256
|
+
if (annotations?.confirmationData) {
|
|
257
|
+
filesWithConfirmationsCount++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Track confirmation requests separately (regardless of other annotations)
|
|
262
|
+
if (annotations?.includeConfirmation) {
|
|
263
|
+
filesWithConfirmationsRequestedCount++;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Track last modified (only for files with annotations)
|
|
267
|
+
if (hasAnnotations && annotations?.updatedAt) {
|
|
268
|
+
if (!lastModified || annotations.updatedAt > lastModified) {
|
|
269
|
+
lastModified = annotations.updatedAt;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Track annotation date range using earliest timestamp for first annotation
|
|
273
|
+
const annotationDateToCheck = annotations.earliestAnnotationTimestamp || annotations.updatedAt;
|
|
274
|
+
if (!earliestAnnotationDate || annotationDateToCheck < earliestAnnotationDate) {
|
|
275
|
+
earliestAnnotationDate = annotationDateToCheck;
|
|
276
|
+
}
|
|
277
|
+
if (!latestAnnotationDate || annotations.updatedAt > latestAnnotationDate) {
|
|
278
|
+
latestAnnotationDate = annotations.updatedAt;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
// Continue without annotations for this file
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
filesWithAnnotations.push({
|
|
286
|
+
fileData: file,
|
|
287
|
+
annotations,
|
|
288
|
+
hasAnnotations
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Build export data
|
|
293
|
+
const exportData: CaseExportData = {
|
|
294
|
+
metadata: {
|
|
295
|
+
caseNumber,
|
|
296
|
+
caseCreatedDate: existingCase.createdAt,
|
|
297
|
+
exportDate: new Date().toISOString(),
|
|
298
|
+
...userMetadata,
|
|
299
|
+
striaeExportSchemaVersion: '1.0',
|
|
300
|
+
totalFiles: files.length
|
|
301
|
+
},
|
|
302
|
+
files: filesWithAnnotations,
|
|
303
|
+
...(includeMetadata && {
|
|
304
|
+
summary: {
|
|
305
|
+
filesWithAnnotations: filesWithAnnotationsCount,
|
|
306
|
+
filesWithoutAnnotations: files.length - filesWithAnnotationsCount,
|
|
307
|
+
totalBoxAnnotations,
|
|
308
|
+
filesWithConfirmations: filesWithConfirmationsCount,
|
|
309
|
+
filesWithConfirmationsRequested: filesWithConfirmationsRequestedCount,
|
|
310
|
+
lastModified,
|
|
311
|
+
earliestAnnotationDate,
|
|
312
|
+
latestAnnotationDate
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// NOTE: Audit logging moved to download handlers where actual filename and format are known
|
|
318
|
+
|
|
319
|
+
return exportData;
|
|
320
|
+
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('Case export failed:', error);
|
|
323
|
+
|
|
324
|
+
// NOTE: Audit logging for failures moved to download handlers
|
|
325
|
+
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { CaseExportData } from '~/types';
|
|
2
|
+
import { calculateSHA256Secure } from '~/utils/SHA256';
|
|
3
|
+
import { CSV_HEADERS } from './types-constants';
|
|
4
|
+
import { addForensicDataWarning } from './metadata-helpers';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate metadata rows for tabular format
|
|
8
|
+
*/
|
|
9
|
+
export function generateMetadataRows(exportData: CaseExportData): string[][] {
|
|
10
|
+
return [
|
|
11
|
+
['Case Export Report'],
|
|
12
|
+
[''],
|
|
13
|
+
['Case Number', exportData.metadata.caseNumber],
|
|
14
|
+
['Case Created Date', exportData.metadata.caseCreatedDate],
|
|
15
|
+
['Export Date', exportData.metadata.exportDate],
|
|
16
|
+
['Exported By (Email)', exportData.metadata.exportedBy || 'N/A'],
|
|
17
|
+
['Exported By (UID)', exportData.metadata.exportedByUid || 'N/A'],
|
|
18
|
+
['Exported By (Name)', exportData.metadata.exportedByName || 'N/A'],
|
|
19
|
+
['Exported By (Company)', exportData.metadata.exportedByCompany || 'N/A'],
|
|
20
|
+
['Striae Export Schema Version', exportData.metadata.striaeExportSchemaVersion],
|
|
21
|
+
['Total Files', exportData.metadata.totalFiles.toString()],
|
|
22
|
+
[''],
|
|
23
|
+
['Summary'],
|
|
24
|
+
['Files with Annotations', (exportData.summary?.filesWithAnnotations || 0).toString()],
|
|
25
|
+
['Files without Annotations', (exportData.summary?.filesWithoutAnnotations || 0).toString()],
|
|
26
|
+
['Total Box Annotations', (exportData.summary?.totalBoxAnnotations || 0).toString()],
|
|
27
|
+
['Files with Confirmations', (exportData.summary?.filesWithConfirmations || 0).toString()],
|
|
28
|
+
['Files with Confirmations Requested', (exportData.summary?.filesWithConfirmationsRequested || 0).toString()],
|
|
29
|
+
['Last Modified', exportData.summary?.lastModified || 'N/A'],
|
|
30
|
+
['Earliest Annotation Date', exportData.summary?.earliestAnnotationDate || 'N/A'],
|
|
31
|
+
['Latest Annotation Date', exportData.summary?.latestAnnotationDate || 'N/A'],
|
|
32
|
+
[''],
|
|
33
|
+
['File Details']
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Process file data for tabular format (CSV/Excel)
|
|
39
|
+
*/
|
|
40
|
+
export function processFileDataForTabular(fileEntry: CaseExportData['files'][0]): string[][] {
|
|
41
|
+
// Full file data for the first row (excluding Additional Notes and Last Updated)
|
|
42
|
+
const fullFileData = [
|
|
43
|
+
fileEntry.fileData.id,
|
|
44
|
+
fileEntry.fileData.originalFilename,
|
|
45
|
+
fileEntry.fileData.uploadedAt,
|
|
46
|
+
fileEntry.hasAnnotations ? 'Yes' : 'No',
|
|
47
|
+
fileEntry.annotations?.leftCase || '',
|
|
48
|
+
fileEntry.annotations?.rightCase || '',
|
|
49
|
+
fileEntry.annotations?.leftItem || '',
|
|
50
|
+
fileEntry.annotations?.rightItem || '',
|
|
51
|
+
fileEntry.annotations?.caseFontColor || '',
|
|
52
|
+
fileEntry.annotations?.classType || '',
|
|
53
|
+
fileEntry.annotations?.customClass || '',
|
|
54
|
+
fileEntry.annotations?.classNote || '',
|
|
55
|
+
fileEntry.annotations?.indexType || '',
|
|
56
|
+
fileEntry.annotations?.indexNumber || '',
|
|
57
|
+
fileEntry.annotations?.indexColor || '',
|
|
58
|
+
fileEntry.annotations?.supportLevel || '',
|
|
59
|
+
fileEntry.annotations?.hasSubclass ? 'Yes' : 'No',
|
|
60
|
+
fileEntry.annotations?.includeConfirmation ? 'Yes' : 'No',
|
|
61
|
+
fileEntry.annotations?.confirmationData ? 'Confirmed' : (fileEntry.annotations?.includeConfirmation ? 'Requested' : 'Not Requested'),
|
|
62
|
+
fileEntry.annotations?.confirmationData?.fullName || '',
|
|
63
|
+
fileEntry.annotations?.confirmationData?.badgeId || '',
|
|
64
|
+
fileEntry.annotations?.confirmationData?.confirmedByEmail || '',
|
|
65
|
+
fileEntry.annotations?.confirmationData?.confirmedByCompany || '',
|
|
66
|
+
fileEntry.annotations?.confirmationData?.confirmationId || '',
|
|
67
|
+
fileEntry.annotations?.confirmationData?.timestamp || '',
|
|
68
|
+
fileEntry.annotations?.confirmationData?.confirmedAt || '',
|
|
69
|
+
(fileEntry.annotations?.boxAnnotations?.length || 0).toString()
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// Additional Notes and Last Updated (at the end)
|
|
73
|
+
const additionalFileData = [
|
|
74
|
+
fileEntry.annotations?.additionalNotes || '',
|
|
75
|
+
fileEntry.annotations?.updatedAt || ''
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// Calculate array sizes programmatically from CSV_HEADERS
|
|
79
|
+
const fileDataColumnCount = fullFileData.length; // Dynamic count based on actual data
|
|
80
|
+
const additionalDataColumnCount = additionalFileData.length; // Dynamic count based on actual data
|
|
81
|
+
|
|
82
|
+
// Empty row template for subsequent box annotations (file info columns empty)
|
|
83
|
+
const emptyFileData = Array(fileDataColumnCount).fill('');
|
|
84
|
+
const emptyAdditionalData = Array(additionalDataColumnCount).fill('');
|
|
85
|
+
|
|
86
|
+
const rows: string[][] = [];
|
|
87
|
+
|
|
88
|
+
// If there are box annotations, create a row for each one
|
|
89
|
+
if (fileEntry.annotations?.boxAnnotations && fileEntry.annotations.boxAnnotations.length > 0) {
|
|
90
|
+
fileEntry.annotations.boxAnnotations.forEach((box, index) => {
|
|
91
|
+
const rowData = index === 0 ? fullFileData : emptyFileData;
|
|
92
|
+
const additionalData = index === 0 ? additionalFileData : emptyAdditionalData;
|
|
93
|
+
|
|
94
|
+
rows.push([
|
|
95
|
+
...rowData,
|
|
96
|
+
box.id,
|
|
97
|
+
box.x.toString(),
|
|
98
|
+
box.y.toString(),
|
|
99
|
+
box.width.toString(),
|
|
100
|
+
box.height.toString(),
|
|
101
|
+
box.color || '',
|
|
102
|
+
box.label || '',
|
|
103
|
+
box.timestamp || '',
|
|
104
|
+
...additionalData
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
// If no box annotations, still include one row with empty box data
|
|
109
|
+
rows.push([
|
|
110
|
+
...fullFileData,
|
|
111
|
+
'', // Box ID
|
|
112
|
+
'', // Box X
|
|
113
|
+
'', // Box Y
|
|
114
|
+
'', // Box Width
|
|
115
|
+
'', // Box Height
|
|
116
|
+
'', // Box Color
|
|
117
|
+
'', // Box Label
|
|
118
|
+
'', // Box Timestamp
|
|
119
|
+
...additionalFileData
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return rows;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate CSV content from export data
|
|
128
|
+
*/
|
|
129
|
+
export async function generateCSVContent(exportData: CaseExportData, protectForensicData: boolean = true): Promise<string> {
|
|
130
|
+
// Case metadata section
|
|
131
|
+
const metadataRows = generateMetadataRows(exportData);
|
|
132
|
+
|
|
133
|
+
// File data rows
|
|
134
|
+
const fileRows: string[][] = [];
|
|
135
|
+
exportData.files.forEach(fileEntry => {
|
|
136
|
+
const processedRows = processFileDataForTabular(fileEntry);
|
|
137
|
+
fileRows.push(...processedRows);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Combine data rows for hash calculation (excluding header comments)
|
|
141
|
+
const dataRows = [
|
|
142
|
+
...metadataRows,
|
|
143
|
+
CSV_HEADERS,
|
|
144
|
+
...fileRows
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const csvDataContent = dataRows.map(row =>
|
|
148
|
+
row.map(field => `"${String(field).replace(/"/g, '""')}"`).join(',')
|
|
149
|
+
).join('\n');
|
|
150
|
+
|
|
151
|
+
// Calculate hash for integrity verification
|
|
152
|
+
const hash = await calculateSHA256Secure(csvDataContent);
|
|
153
|
+
|
|
154
|
+
// Create final CSV with hash header
|
|
155
|
+
const csvWithHash = [
|
|
156
|
+
`# Striae Case Export - Generated: ${new Date().toISOString()}`,
|
|
157
|
+
`# Case: ${exportData.metadata.caseNumber}`,
|
|
158
|
+
`# Total Files: ${exportData.metadata.totalFiles}`,
|
|
159
|
+
`# SHA-256 Hash: ${hash.toUpperCase()}`,
|
|
160
|
+
'# Verification: Recalculate SHA-256 of data rows only (excluding these comment lines)',
|
|
161
|
+
'',
|
|
162
|
+
csvDataContent
|
|
163
|
+
].join('\n');
|
|
164
|
+
|
|
165
|
+
// Add forensic protection warning if enabled
|
|
166
|
+
return protectForensicData ? addForensicDataWarning(csvWithHash, 'csv') : csvWithHash;
|
|
167
|
+
}
|