@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,524 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { User } from 'firebase/auth';
3
+ import {
4
+ canCreateCase,
5
+ getUserCases,
6
+ validateUserSession,
7
+ addUserCase,
8
+ removeUserCase
9
+ } from '~/utils/permissions';
10
+ import {
11
+ getCaseData,
12
+ updateCaseData,
13
+ deleteCaseData,
14
+ duplicateCaseData,
15
+ deleteFileAnnotations
16
+ } from '~/utils/data-operations';
17
+ import { CaseData, ReadOnlyCaseData, FileData } from '~/types';
18
+ import { auditService } from '~/services/audit.service';
19
+ import { getImageApiKey } from '~/utils/auth';
20
+ import paths from '~/config/config.json';
21
+
22
+ /**
23
+ * Delete a file without individual audit logging (for bulk operations)
24
+ * This reduces API calls during bulk deletions
25
+ */
26
+ const deleteFileWithoutAudit = async (user: User, caseNumber: string, fileId: string): Promise<void> => {
27
+ // Get the case data to find file info
28
+ const caseData = await getCaseData(user, caseNumber);
29
+ if (!caseData) {
30
+ throw new Error('Case not found');
31
+ }
32
+
33
+ const fileToDelete = (caseData.files || []).find((f: FileData) => f.id === fileId);
34
+ if (!fileToDelete) {
35
+ throw new Error('File not found in case');
36
+ }
37
+
38
+ // Delete the image file from Cloudflare Images (but don't audit this individual operation)
39
+ try {
40
+ const IMAGE_URL = paths.image_worker_url;
41
+
42
+ const imagesApiToken = await getImageApiKey();
43
+ const imageResponse = await fetch(`${IMAGE_URL}/${fileId}`, {
44
+ method: 'DELETE',
45
+ headers: {
46
+ 'Authorization': `Bearer ${imagesApiToken}`
47
+ }
48
+ });
49
+
50
+ // Only fail if it's not a 404 (file might already be deleted)
51
+ if (!imageResponse.ok && imageResponse.status !== 404) {
52
+ throw new Error(`Failed to delete image: ${imageResponse.statusText}`);
53
+ }
54
+ } catch (error) {
55
+ console.warn(`Image deletion warning for ${fileToDelete.originalFilename}:`, error);
56
+ // Continue with data cleanup even if image deletion fails
57
+ }
58
+
59
+ // Delete annotation data
60
+ try {
61
+ await deleteFileAnnotations(user, caseNumber, fileId);
62
+ } catch (error) {
63
+ // Annotation file might not exist, continue
64
+ console.warn(`Annotation deletion warning for ${fileToDelete.originalFilename}:`, error);
65
+ }
66
+
67
+ // Update case data to remove file reference
68
+ const updatedData: CaseData = {
69
+ ...caseData,
70
+ files: (caseData.files || []).filter((f: FileData) => f.id !== fileId)
71
+ };
72
+
73
+ await updateCaseData(user, caseNumber, updatedData);
74
+ };
75
+
76
+ const CASE_NUMBER_REGEX = /^[A-Za-z0-9-]+$/;
77
+
78
+ /**
79
+ * Type guard to check if case data has isReadOnly property
80
+ */
81
+ const isReadOnlyCaseData = (caseData: CaseData): caseData is ReadOnlyCaseData => {
82
+ return 'isReadOnly' in caseData && typeof (caseData as ReadOnlyCaseData).isReadOnly === 'boolean';
83
+ };
84
+ const MAX_CASE_NUMBER_LENGTH = 25;
85
+
86
+ export const listCases = async (user: User): Promise<string[]> => {
87
+ try {
88
+ // Use centralized function to get user cases
89
+ const userCases = await getUserCases(user);
90
+ const caseNumbers = userCases.map(c => c.caseNumber);
91
+ return sortCaseNumbers(caseNumbers);
92
+
93
+ } catch (error) {
94
+ console.error('Error listing cases:', error);
95
+ return [];
96
+ }
97
+ };
98
+
99
+ const sortCaseNumbers = (cases: string[]): string[] => {
100
+ return cases.sort((a, b) => {
101
+ // Extract all numbers and letters
102
+ const getComponents = (str: string) => {
103
+ const numbers = str.match(/\d+/g)?.map(Number) || [];
104
+ const letters = str.match(/[A-Za-z]+/g)?.join('') || '';
105
+ return { numbers, letters };
106
+ };
107
+
108
+ const aComponents = getComponents(a);
109
+ const bComponents = getComponents(b);
110
+
111
+ // Compare numbers first
112
+ const maxLength = Math.max(aComponents.numbers.length, bComponents.numbers.length);
113
+ for (let i = 0; i < maxLength; i++) {
114
+ const aNum = aComponents.numbers[i] || 0;
115
+ const bNum = bComponents.numbers[i] || 0;
116
+ if (aNum !== bNum) return aNum - bNum;
117
+ }
118
+
119
+ // If all numbers match, compare letters
120
+ return aComponents.letters.localeCompare(bComponents.letters);
121
+ });
122
+ };
123
+
124
+ export const validateCaseNumber = (caseNumber: string): boolean => {
125
+ return CASE_NUMBER_REGEX.test(caseNumber) &&
126
+ caseNumber.length <= MAX_CASE_NUMBER_LENGTH;
127
+ };
128
+
129
+ export const checkExistingCase = async (user: User, caseNumber: string): Promise<CaseData | null> => {
130
+ try {
131
+ // Try to get case data - if user doesn't have access, it means case doesn't exist for them
132
+ const caseData = await getCaseData(user, caseNumber);
133
+
134
+ if (!caseData) {
135
+ return null;
136
+ }
137
+
138
+ // Check if this is a read-only case - if so, don't consider it as an existing regular case
139
+ if ('isReadOnly' in caseData && caseData.isReadOnly) {
140
+ return null;
141
+ }
142
+
143
+ // Verify the case number matches (extra safety check)
144
+ if (caseData.caseNumber === caseNumber) {
145
+ return caseData;
146
+ }
147
+
148
+ return null;
149
+
150
+ } catch (error) {
151
+ // If access denied, treat as case doesn't exist for this user
152
+ if (error instanceof Error && error.message.includes('Access denied')) {
153
+ return null;
154
+ }
155
+ console.error('Error checking existing case:', error);
156
+ return null;
157
+ }
158
+ };
159
+
160
+ export const checkCaseIsReadOnly = async (user: User, caseNumber: string): Promise<boolean> => {
161
+ try {
162
+ const caseData = await getCaseData(user, caseNumber);
163
+ if (!caseData) {
164
+ // Case doesn't exist, so it's not read-only
165
+ return false;
166
+ }
167
+
168
+ // Use type guard to check for isReadOnly property safely
169
+ return isReadOnlyCaseData(caseData) ? !!caseData.isReadOnly : false;
170
+
171
+ } catch (error) {
172
+ console.error('Error checking if case is read-only:', error);
173
+ return false;
174
+ }
175
+ };
176
+
177
+ export const createNewCase = async (user: User, caseNumber: string): Promise<CaseData> => {
178
+ const startTime = Date.now();
179
+
180
+ try {
181
+ // Validate user session first
182
+ const sessionValidation = await validateUserSession(user);
183
+ if (!sessionValidation.valid) {
184
+ throw new Error(`Session validation failed: ${sessionValidation.reason}`);
185
+ }
186
+
187
+ // Check if user can create a new case
188
+ const permission = await canCreateCase(user);
189
+ if (!permission.canCreate) {
190
+ throw new Error(permission.reason || 'You cannot create more cases.');
191
+ }
192
+
193
+ const newCase: CaseData = {
194
+ createdAt: new Date().toISOString(),
195
+ caseNumber,
196
+ files: []
197
+ };
198
+
199
+ const caseMetadata = {
200
+ createdAt: newCase.createdAt,
201
+ caseNumber: newCase.caseNumber
202
+ };
203
+
204
+ // Add case to user data first (so user has permission to create case data)
205
+ await addUserCase(user, caseMetadata);
206
+
207
+ // Create case file using centralized function
208
+ await updateCaseData(user, caseNumber, newCase);
209
+
210
+ // Log successful case creation
211
+ const endTime = Date.now();
212
+ await auditService.logCaseCreation(
213
+ user,
214
+ caseNumber,
215
+ caseNumber // Using case number as case name for now
216
+ );
217
+
218
+ console.log(`✅ Case created: ${caseNumber} (${endTime - startTime}ms)`);
219
+ return newCase;
220
+
221
+ } catch (error) {
222
+ // Log failed case creation
223
+ const endTime = Date.now();
224
+ try {
225
+ await auditService.logEvent({
226
+ userId: user.uid,
227
+ userEmail: user.email || '',
228
+ action: 'case-create',
229
+ result: 'failure',
230
+ fileName: `${caseNumber}.case`,
231
+ fileType: 'case-package',
232
+ validationErrors: [error instanceof Error ? error.message : 'Unknown error'],
233
+ caseNumber,
234
+ caseDetails: {
235
+ newCaseName: caseNumber
236
+ },
237
+ performanceMetrics: {
238
+ processingTimeMs: endTime - startTime,
239
+ fileSizeBytes: 0
240
+ }
241
+ });
242
+ } catch (auditError) {
243
+ console.error('Failed to log case creation failure:', auditError);
244
+ }
245
+
246
+ console.error('Error creating new case:', error);
247
+ throw error;
248
+ }
249
+ };
250
+
251
+ export const renameCase = async (
252
+ user: User,
253
+ oldCaseNumber: string,
254
+ newCaseNumber: string
255
+ ): Promise<void> => {
256
+ const startTime = Date.now();
257
+
258
+ try {
259
+ // Validate case numbers
260
+ if (!validateCaseNumber(oldCaseNumber) || !validateCaseNumber(newCaseNumber)) {
261
+ throw new Error('Invalid case number format');
262
+ }
263
+
264
+ // Check if new case exists
265
+ const existingCase = await checkExistingCase(user, newCaseNumber);
266
+ if (existingCase) {
267
+ throw new Error('New case number already exists');
268
+ }
269
+
270
+ // Get the old case data to find all files that need annotation cleanup
271
+ const oldCaseData = await getCaseData(user, oldCaseNumber);
272
+ if (!oldCaseData) {
273
+ throw new Error('Old case not found');
274
+ }
275
+
276
+ // 1) Create new case number in USER DB's entry (KV storage)
277
+ const newCaseMetadata = {
278
+ createdAt: new Date().toISOString(),
279
+ caseNumber: newCaseNumber
280
+ };
281
+ await addUserCase(user, newCaseMetadata);
282
+
283
+ // 2) Copy R2 case data from old case number to new case number in R2
284
+ await duplicateCaseData(user, oldCaseNumber, newCaseNumber);
285
+
286
+ // 3) Delete individual file annotations from the old case (before losing access)
287
+ if (oldCaseData.files && oldCaseData.files.length > 0) {
288
+ // Process annotation deletions in batches to avoid rate limiting
289
+ const BATCH_SIZE = 5;
290
+ const files = oldCaseData.files;
291
+
292
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
293
+ const batch = files.slice(i, i + BATCH_SIZE);
294
+
295
+ // Delete annotation files in this batch
296
+ await Promise.all(
297
+ batch.map(async file => {
298
+ try {
299
+ await deleteFileAnnotations(user, oldCaseNumber, file.id);
300
+ } catch (error) {
301
+ // Continue if annotation file doesn't exist or fails to delete
302
+ console.warn(`Failed to delete annotations for ${file.originalFilename}:`, error);
303
+ }
304
+ })
305
+ );
306
+
307
+ // Add delay between batches to reduce rate limiting
308
+ if (i + BATCH_SIZE < files.length) {
309
+ await new Promise(resolve => setTimeout(resolve, 150));
310
+ }
311
+ }
312
+ }
313
+
314
+ // 4) Delete R2 case data with old case number
315
+ await deleteCaseData(user, oldCaseNumber);
316
+
317
+ // 5) Delete old case number in user's KV entry
318
+ await removeUserCase(user, oldCaseNumber);
319
+
320
+ // Log successful case rename
321
+ const endTime = Date.now();
322
+ await auditService.logCaseRename(
323
+ user,
324
+ newCaseNumber, // Use new case number as the current context
325
+ oldCaseNumber,
326
+ newCaseNumber
327
+ );
328
+
329
+ console.log(`✅ Case renamed: ${oldCaseNumber} → ${newCaseNumber} (${endTime - startTime}ms)`);
330
+
331
+ } catch (error) {
332
+ // Log failed case rename
333
+ const endTime = Date.now();
334
+ try {
335
+ await auditService.logEvent({
336
+ userId: user.uid,
337
+ userEmail: user.email || '',
338
+ action: 'case-rename',
339
+ result: 'failure',
340
+ fileName: `${oldCaseNumber}.case`,
341
+ fileType: 'case-package',
342
+ validationErrors: [error instanceof Error ? error.message : 'Unknown error'],
343
+ caseNumber: oldCaseNumber,
344
+ caseDetails: {
345
+ oldCaseName: oldCaseNumber,
346
+ newCaseName: newCaseNumber,
347
+ lastModified: new Date().toISOString()
348
+ },
349
+ performanceMetrics: {
350
+ processingTimeMs: endTime - startTime,
351
+ fileSizeBytes: 0
352
+ }
353
+ });
354
+ } catch (auditError) {
355
+ console.error('Failed to log case rename failure:', auditError);
356
+ }
357
+
358
+ console.error('Error renaming case:', error);
359
+ throw error;
360
+ }
361
+ };
362
+
363
+ export const deleteCase = async (user: User, caseNumber: string): Promise<void> => {
364
+ const startTime = Date.now();
365
+
366
+ try {
367
+ if (!validateCaseNumber(caseNumber)) {
368
+ throw new Error('Invalid case number');
369
+ }
370
+
371
+ // Validate user session
372
+ const sessionValidation = await validateUserSession(user);
373
+ if (!sessionValidation.valid) {
374
+ throw new Error(`Session validation failed: ${sessionValidation.reason}`);
375
+ }
376
+
377
+ // Get case data using centralized function
378
+ const caseData = await getCaseData(user, caseNumber);
379
+ if (!caseData) {
380
+ throw new Error('Case not found');
381
+ }
382
+
383
+ // Store case info for audit logging
384
+ const fileCount = caseData.files?.length || 0;
385
+ const caseName = caseData.caseNumber || caseNumber;
386
+
387
+ // Process file deletions in batches to reduce audit rate limiting
388
+ if (caseData.files && caseData.files.length > 0) {
389
+ const BATCH_SIZE = 3; // Reduced batch size for better stability
390
+ const BATCH_DELAY = 300; // Increased delay between batches
391
+ const files = caseData.files;
392
+ const deletedFiles: Array<{id: string, originalFilename: string, fileSize: number}> = [];
393
+ const failedFiles: Array<{id: string, originalFilename: string, error: string}> = [];
394
+
395
+ console.log(`🗑️ Deleting ${files.length} files in batches of ${BATCH_SIZE}...`);
396
+
397
+ // Process files in batches
398
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
399
+ const batch = files.slice(i, i + BATCH_SIZE);
400
+ const batchNumber = Math.floor(i / BATCH_SIZE) + 1;
401
+ const totalBatches = Math.ceil(files.length / BATCH_SIZE);
402
+
403
+ console.log(`📦 Processing batch ${batchNumber}/${totalBatches} (${batch.length} files)...`);
404
+
405
+ // Delete files in this batch with individual error handling
406
+ await Promise.allSettled(
407
+ batch.map(async file => {
408
+ try {
409
+ // Delete file without individual audit logging to reduce API calls
410
+ // We'll do bulk audit logging at the end
411
+ await deleteFileWithoutAudit(user, caseNumber, file.id);
412
+ deletedFiles.push({
413
+ id: file.id,
414
+ originalFilename: file.originalFilename,
415
+ fileSize: 0 // We don't track file size, use 0
416
+ });
417
+ } catch (error) {
418
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
419
+ console.error(`❌ Failed to delete file ${file.originalFilename}:`, errorMessage);
420
+ failedFiles.push({
421
+ id: file.id,
422
+ originalFilename: file.originalFilename,
423
+ error: errorMessage
424
+ });
425
+ }
426
+ })
427
+ );
428
+
429
+ // Add delay between batches to reduce rate limiting
430
+ if (i + BATCH_SIZE < files.length) {
431
+ console.log(`⏱️ Waiting ${BATCH_DELAY}ms before next batch...`);
432
+ await new Promise(resolve => setTimeout(resolve, BATCH_DELAY));
433
+ }
434
+ }
435
+
436
+ // Single consolidated audit entry for all file operations
437
+ try {
438
+ const endTime = Date.now();
439
+ const successCount = deletedFiles.length;
440
+ const failureCount = failedFiles.length;
441
+
442
+ await auditService.logEvent({
443
+ userId: user.uid,
444
+ userEmail: user.email || '',
445
+ action: 'file-delete',
446
+ result: failureCount === 0 ? 'success' : 'failure',
447
+ fileName: `Bulk deletion: ${successCount} succeeded, ${failureCount} failed`,
448
+ fileType: 'case-package',
449
+ caseNumber,
450
+ caseDetails: {
451
+ newCaseName: `${caseNumber} - Bulk file deletion`,
452
+ deleteReason: `Case deletion: processed ${files.length} files (${successCount} deleted, ${failureCount} failed)`,
453
+ backupCreated: false,
454
+ lastModified: new Date().toISOString()
455
+ },
456
+ performanceMetrics: {
457
+ processingTimeMs: endTime - startTime,
458
+ fileSizeBytes: deletedFiles.reduce((total, file) => total + file.fileSize, 0)
459
+ },
460
+ // Include details of failed files if any
461
+ ...(failedFiles.length > 0 && {
462
+ validationErrors: failedFiles.map(f => `${f.originalFilename}: ${f.error}`)
463
+ })
464
+ });
465
+
466
+ console.log(`✅ Batch deletion complete: ${successCount} files deleted, ${failureCount} failed`);
467
+ } catch (auditError) {
468
+ console.error('⚠️ Failed to log batch file deletion (continuing with case deletion):', auditError);
469
+ }
470
+ }
471
+
472
+ // Remove case from user data first (so user loses access immediately)
473
+ await removeUserCase(user, caseNumber);
474
+
475
+ // Delete case data using centralized function (skip validation since user no longer has access)
476
+ await deleteCaseData(user, caseNumber, { skipValidation: true });
477
+
478
+ // Add a small delay before audit logging to reduce rate limiting
479
+ await new Promise(resolve => setTimeout(resolve, 100));
480
+
481
+ // Log successful case deletion with file details
482
+ const endTime = Date.now();
483
+ await auditService.logCaseDeletion(
484
+ user,
485
+ caseNumber,
486
+ caseName,
487
+ `User-requested deletion via case actions (${fileCount} files deleted)`,
488
+ false // No backup created for standard deletions
489
+ );
490
+
491
+ console.log(`✅ Case deleted: ${caseNumber} (${fileCount} files) (${endTime - startTime}ms)`);
492
+
493
+ } catch (error) {
494
+ // Log failed case deletion
495
+ const endTime = Date.now();
496
+ try {
497
+ await auditService.logEvent({
498
+ userId: user.uid,
499
+ userEmail: user.email || '',
500
+ action: 'case-delete',
501
+ result: 'failure',
502
+ fileName: `${caseNumber}.case`,
503
+ fileType: 'case-package',
504
+ validationErrors: [error instanceof Error ? error.message : 'Unknown error'],
505
+ caseNumber,
506
+ caseDetails: {
507
+ newCaseName: caseNumber,
508
+ deleteReason: 'Failed deletion attempt',
509
+ backupCreated: false,
510
+ lastModified: new Date().toISOString()
511
+ },
512
+ performanceMetrics: {
513
+ processingTimeMs: endTime - startTime,
514
+ fileSizeBytes: 0
515
+ }
516
+ });
517
+ } catch (auditError) {
518
+ console.error('Failed to log case deletion failure:', auditError);
519
+ }
520
+
521
+ console.error('Error deleting case:', error);
522
+ throw error;
523
+ }
524
+ };
@@ -0,0 +1,4 @@
1
+ // Re-export all case import functionality from the new modular structure
2
+ // This maintains backward compatibility while providing a cleaner, more maintainable codebase
3
+
4
+ export * from './case-import';