@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,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
|
+
};
|