@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.
Files changed (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. 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
+ }