@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,270 @@
|
|
|
1
|
+
import { User } from 'firebase/auth';
|
|
2
|
+
import paths from '~/config/config.json';
|
|
3
|
+
import {
|
|
4
|
+
getDataApiKey,
|
|
5
|
+
getUserApiKey
|
|
6
|
+
} from '~/utils/auth';
|
|
7
|
+
import {
|
|
8
|
+
getUserReadOnlyCases,
|
|
9
|
+
updateUserData,
|
|
10
|
+
validateUserSession
|
|
11
|
+
} from '~/utils/permissions';
|
|
12
|
+
import {
|
|
13
|
+
CaseExportData,
|
|
14
|
+
ExtendedUserData,
|
|
15
|
+
FileData,
|
|
16
|
+
CaseData,
|
|
17
|
+
ReadOnlyCaseMetadata
|
|
18
|
+
} from '~/types';
|
|
19
|
+
import { deleteFile } from '../image-manage';
|
|
20
|
+
import { SignedForensicManifest } from '~/utils/SHA256';
|
|
21
|
+
|
|
22
|
+
const USER_WORKER_URL = paths.user_worker_url;
|
|
23
|
+
const DATA_WORKER_URL = paths.data_worker_url;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if user already has a read-only case with the same number
|
|
27
|
+
*/
|
|
28
|
+
export async function checkReadOnlyCaseExists(
|
|
29
|
+
user: User,
|
|
30
|
+
caseNumber: string
|
|
31
|
+
): Promise<ReadOnlyCaseMetadata | null> {
|
|
32
|
+
try {
|
|
33
|
+
// Use centralized function to get read-only cases
|
|
34
|
+
const readOnlyCases = await getUserReadOnlyCases(user);
|
|
35
|
+
return readOnlyCases.find(c => c.caseNumber === caseNumber) || null;
|
|
36
|
+
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error checking read-only case existence:', error);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create read-only case entry in user database
|
|
45
|
+
* Note: Only one read-only case is allowed at a time. This function will clear any existing
|
|
46
|
+
* read-only cases before adding the new one to prevent accumulation of multiple read-only cases.
|
|
47
|
+
*/
|
|
48
|
+
export async function addReadOnlyCaseToUser(
|
|
49
|
+
user: User,
|
|
50
|
+
caseMetadata: ReadOnlyCaseMetadata
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
try {
|
|
53
|
+
// Validate user session
|
|
54
|
+
const sessionValidation = await validateUserSession(user);
|
|
55
|
+
if (!sessionValidation.valid) {
|
|
56
|
+
throw new Error(`Session validation failed: ${sessionValidation.reason}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Get current read-only cases
|
|
60
|
+
const currentReadOnlyCases = await getUserReadOnlyCases(user);
|
|
61
|
+
|
|
62
|
+
// IMPORTANT: Only allow one read-only case at a time
|
|
63
|
+
// Clear any existing read-only cases before adding the new one
|
|
64
|
+
if (currentReadOnlyCases.length > 0) {
|
|
65
|
+
const existingCaseNumbers = currentReadOnlyCases.map(c => c.caseNumber).join(', ');
|
|
66
|
+
console.log(`Clearing ${currentReadOnlyCases.length} existing read-only case(s) (${existingCaseNumbers}) before importing new case: ${caseMetadata.caseNumber}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Update user data with the new read-only case (replacing any existing ones)
|
|
70
|
+
await updateUserData(user, {
|
|
71
|
+
readOnlyCases: [caseMetadata] // Only the new case
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(`Added new read-only case to user profile: ${caseMetadata.caseNumber}`);
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Error adding read-only case to user:', error);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Store case data in R2 storage
|
|
84
|
+
*/
|
|
85
|
+
export async function storeCaseDataInR2(
|
|
86
|
+
user: User,
|
|
87
|
+
caseNumber: string,
|
|
88
|
+
caseData: CaseExportData,
|
|
89
|
+
importedFiles: FileData[],
|
|
90
|
+
originalImageIdMapping?: Map<string, string>,
|
|
91
|
+
forensicManifest?: SignedForensicManifest
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
const apiKey = await getDataApiKey();
|
|
95
|
+
|
|
96
|
+
// Convert the mapping to a plain object for JSON serialization
|
|
97
|
+
const originalImageIds = originalImageIdMapping ?
|
|
98
|
+
Object.fromEntries(originalImageIdMapping) : undefined;
|
|
99
|
+
|
|
100
|
+
const forensicManifestMetadata = forensicManifest ? {
|
|
101
|
+
manifestVersion: forensicManifest.manifestVersion,
|
|
102
|
+
createdAt: forensicManifest.createdAt,
|
|
103
|
+
dataHash: forensicManifest.dataHash,
|
|
104
|
+
manifestHash: forensicManifest.manifestHash,
|
|
105
|
+
signature: forensicManifest.signature
|
|
106
|
+
} : undefined;
|
|
107
|
+
|
|
108
|
+
// Create the case data structure that matches normal cases
|
|
109
|
+
const r2CaseData = {
|
|
110
|
+
createdAt: new Date().toISOString(),
|
|
111
|
+
caseNumber: caseNumber,
|
|
112
|
+
files: importedFiles,
|
|
113
|
+
// Add read-only metadata
|
|
114
|
+
isReadOnly: true,
|
|
115
|
+
importedAt: new Date().toISOString(),
|
|
116
|
+
// Add original image ID mapping for confirmation linking
|
|
117
|
+
originalImageIds: originalImageIds,
|
|
118
|
+
// Add forensic manifest timestamp if available for confirmation exports
|
|
119
|
+
...(forensicManifest?.createdAt && { forensicManifestCreatedAt: forensicManifest.createdAt }),
|
|
120
|
+
// Store full forensic manifest metadata for chain-of-custody validation
|
|
121
|
+
...(forensicManifestMetadata && { forensicManifest: forensicManifestMetadata })
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Store in R2
|
|
125
|
+
const response = await fetch(`${DATA_WORKER_URL}/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`, {
|
|
126
|
+
method: 'PUT',
|
|
127
|
+
headers: {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
'X-Custom-Auth-Key': apiKey
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify(r2CaseData)
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw new Error(`Failed to store case data: ${response.status}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('Error storing case data in R2:', error);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* List all read-only cases for a user
|
|
146
|
+
*/
|
|
147
|
+
export async function listReadOnlyCases(user: User): Promise<ReadOnlyCaseMetadata[]> {
|
|
148
|
+
try {
|
|
149
|
+
const apiKey = await getUserApiKey();
|
|
150
|
+
|
|
151
|
+
const response = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}`, {
|
|
152
|
+
method: 'GET',
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
'X-Custom-Auth-Key': apiKey
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
console.error('Failed to fetch user data:', response.status);
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const userData: ExtendedUserData = await response.json();
|
|
165
|
+
|
|
166
|
+
return userData.readOnlyCases || [];
|
|
167
|
+
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('Error listing read-only cases:', error);
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Remove a read-only case (does not delete the actual case data, just removes from user's read-only list)
|
|
176
|
+
*/
|
|
177
|
+
export async function removeReadOnlyCase(user: User, caseNumber: string): Promise<boolean> {
|
|
178
|
+
try {
|
|
179
|
+
const apiKey = await getUserApiKey();
|
|
180
|
+
|
|
181
|
+
// Get current user data
|
|
182
|
+
const response = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}`, {
|
|
183
|
+
method: 'GET',
|
|
184
|
+
headers: {
|
|
185
|
+
'Content-Type': 'application/json',
|
|
186
|
+
'X-Custom-Auth-Key': apiKey
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw new Error(`Failed to fetch user data: ${response.status}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const userData: ExtendedUserData = await response.json();
|
|
195
|
+
|
|
196
|
+
if (!userData.readOnlyCases) {
|
|
197
|
+
return false; // Nothing to remove
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Remove the case from the list
|
|
201
|
+
const initialLength = userData.readOnlyCases.length;
|
|
202
|
+
userData.readOnlyCases = userData.readOnlyCases.filter(c => c.caseNumber !== caseNumber);
|
|
203
|
+
|
|
204
|
+
if (userData.readOnlyCases.length === initialLength) {
|
|
205
|
+
return false; // Case wasn't found
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Update user data
|
|
209
|
+
const updateResponse = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}`, {
|
|
210
|
+
method: 'PUT',
|
|
211
|
+
headers: {
|
|
212
|
+
'Content-Type': 'application/json',
|
|
213
|
+
'X-Custom-Auth-Key': apiKey
|
|
214
|
+
},
|
|
215
|
+
body: JSON.stringify(userData)
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (!updateResponse.ok) {
|
|
219
|
+
throw new Error(`Failed to update user data: ${updateResponse.status}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return true;
|
|
223
|
+
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error('Error removing read-only case:', error);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Completely delete a read-only case including all associated data (R2, Images, user references)
|
|
232
|
+
*/
|
|
233
|
+
export async function deleteReadOnlyCase(user: User, caseNumber: string): Promise<boolean> {
|
|
234
|
+
try {
|
|
235
|
+
const dataApiKey = await getDataApiKey();
|
|
236
|
+
|
|
237
|
+
// Get case data first to get file IDs for deletion
|
|
238
|
+
const caseResponse = await fetch(`${DATA_WORKER_URL}/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`, {
|
|
239
|
+
headers: { 'X-Custom-Auth-Key': dataApiKey }
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (caseResponse.ok) {
|
|
243
|
+
const caseData = await caseResponse.json() as CaseData;
|
|
244
|
+
|
|
245
|
+
// Delete all files using data worker
|
|
246
|
+
if (caseData.files && caseData.files.length > 0) {
|
|
247
|
+
await Promise.all(
|
|
248
|
+
caseData.files.map((file: FileData) =>
|
|
249
|
+
deleteFile(user, caseNumber, file.id, 'Read-only case clearing - API operation')
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Delete case file using data worker
|
|
255
|
+
await fetch(`${DATA_WORKER_URL}/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`, {
|
|
256
|
+
method: 'DELETE',
|
|
257
|
+
headers: { 'X-Custom-Auth-Key': dataApiKey }
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Remove from user's read-only case list (separate from regular cases)
|
|
262
|
+
await removeReadOnlyCase(user, caseNumber);
|
|
263
|
+
|
|
264
|
+
return true;
|
|
265
|
+
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error('Error deleting read-only case:', error);
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { User } from 'firebase/auth';
|
|
2
|
+
import paths from '~/config/config.json';
|
|
3
|
+
import { getUserApiKey } from '~/utils/auth';
|
|
4
|
+
import { CaseExportData, ConfirmationImportData } from '~/types';
|
|
5
|
+
import { calculateSHA256Secure, ManifestSignatureVerificationResult } from '~/utils/SHA256';
|
|
6
|
+
import { verifyConfirmationSignature } from '~/utils/confirmation-signature';
|
|
7
|
+
|
|
8
|
+
const USER_WORKER_URL = paths.user_worker_url;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Remove forensic warning from content for hash validation (supports both JSON and CSV formats)
|
|
12
|
+
* This function ensures exact match with the content used during export hash generation
|
|
13
|
+
*/
|
|
14
|
+
export function removeForensicWarning(content: string): string {
|
|
15
|
+
// Handle JSON forensic warnings (block comment format)
|
|
16
|
+
// /* CASE DATA WARNING
|
|
17
|
+
// * This file contains evidence data for forensic examination.
|
|
18
|
+
// * Any modification may compromise the integrity of the evidence.
|
|
19
|
+
// * Handle according to your organization's chain of custody procedures.
|
|
20
|
+
// *
|
|
21
|
+
// * File generated: YYYY-MM-DDTHH:mm:ss.sssZ
|
|
22
|
+
// */
|
|
23
|
+
const jsonForensicWarningRegex = /^\/\*\s*CASE\s+DATA\s+WARNING[\s\S]*?\*\/\s*\r?\n*/;
|
|
24
|
+
|
|
25
|
+
// Handle CSV forensic warnings (quoted string format at the beginning of file)
|
|
26
|
+
// CRITICAL: The CSV forensic warning is ONLY the first quoted line, followed by two newlines
|
|
27
|
+
// Format: "CASE DATA WARNING: This file contains evidence data for forensic examination. Any modification may compromise the integrity of the evidence. Handle according to your organization's chain of custody procedures."\n\n
|
|
28
|
+
//
|
|
29
|
+
// After removal, what remains should be the csvWithHash content:
|
|
30
|
+
// # Striae Case Export - Generated: ...
|
|
31
|
+
// # Case: ...
|
|
32
|
+
// # Total Files: ...
|
|
33
|
+
// # SHA256 Hash: ...
|
|
34
|
+
// # Verification: ...
|
|
35
|
+
//
|
|
36
|
+
// [actual CSV data]
|
|
37
|
+
// More robust regex to handle various line endings and exact format from generation
|
|
38
|
+
const csvForensicWarningRegex = /^"CASE DATA WARNING: This file contains evidence data for forensic examination\. Any modification may compromise the integrity of the evidence\. Handle according to your organization's chain of custody procedures\."(?:\r?\n){2}/;
|
|
39
|
+
|
|
40
|
+
let cleaned = content;
|
|
41
|
+
|
|
42
|
+
// Try JSON format first
|
|
43
|
+
if (jsonForensicWarningRegex.test(content)) {
|
|
44
|
+
cleaned = content.replace(jsonForensicWarningRegex, '');
|
|
45
|
+
}
|
|
46
|
+
// Try CSV format with exact pattern match
|
|
47
|
+
else if (csvForensicWarningRegex.test(content)) {
|
|
48
|
+
cleaned = content.replace(csvForensicWarningRegex, '');
|
|
49
|
+
}
|
|
50
|
+
// Fallback: try broader CSV pattern in case of slight format differences
|
|
51
|
+
else if (content.startsWith('"CASE DATA WARNING:')) {
|
|
52
|
+
// Find the end of the first quoted string followed by newlines
|
|
53
|
+
const match = content.match(/^"[^"]*"(?:\r?\n)+/);
|
|
54
|
+
if (match) {
|
|
55
|
+
cleaned = content.substring(match[0].length);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Additional cleanup: remove any leading whitespace that might remain
|
|
60
|
+
// This ensures we match exactly what the generation functions produce with protectForensicData: false
|
|
61
|
+
cleaned = cleaned.replace(/^\s+/, '');
|
|
62
|
+
|
|
63
|
+
return cleaned;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate that a user exists in the database by UID and is not the current user
|
|
68
|
+
*/
|
|
69
|
+
export async function validateExporterUid(exporterUid: string, currentUser: User): Promise<{ exists: boolean; isSelf: boolean }> {
|
|
70
|
+
try {
|
|
71
|
+
const apiKey = await getUserApiKey();
|
|
72
|
+
const response = await fetch(`${USER_WORKER_URL}/${exporterUid}`, {
|
|
73
|
+
method: 'GET',
|
|
74
|
+
headers: {
|
|
75
|
+
'X-Custom-Auth-Key': apiKey
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const exists = response.status === 200;
|
|
80
|
+
const isSelf = exporterUid === currentUser.uid;
|
|
81
|
+
|
|
82
|
+
return { exists, isSelf };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error validating exporter UID:', error);
|
|
85
|
+
return { exists: false, isSelf: false };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if file is a confirmation data import
|
|
91
|
+
*/
|
|
92
|
+
export function isConfirmationDataFile(filename: string): boolean {
|
|
93
|
+
return filename.startsWith('confirmation-data') && filename.endsWith('.json');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate confirmation data file hash
|
|
98
|
+
*/
|
|
99
|
+
export async function validateConfirmationHash(jsonContent: string, expectedHash: string): Promise<boolean> {
|
|
100
|
+
try {
|
|
101
|
+
// Validate input parameters
|
|
102
|
+
if (!expectedHash || typeof expectedHash !== 'string') {
|
|
103
|
+
console.error('validateConfirmationHash: expectedHash is invalid:', expectedHash);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create data without hash for validation
|
|
108
|
+
const data = JSON.parse(jsonContent);
|
|
109
|
+
const dataWithoutHash = {
|
|
110
|
+
...data,
|
|
111
|
+
metadata: {
|
|
112
|
+
...data.metadata,
|
|
113
|
+
hash: undefined
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
delete dataWithoutHash.metadata.hash;
|
|
117
|
+
delete dataWithoutHash.metadata.signature;
|
|
118
|
+
delete dataWithoutHash.metadata.signatureVersion;
|
|
119
|
+
|
|
120
|
+
const contentForHash = JSON.stringify(dataWithoutHash, null, 2);
|
|
121
|
+
const actualHash = await calculateSHA256Secure(contentForHash);
|
|
122
|
+
|
|
123
|
+
if (!actualHash) {
|
|
124
|
+
console.error('validateConfirmationHash: failed to calculate hash');
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return actualHash.toUpperCase() === expectedHash.toUpperCase();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('validateConfirmationHash: validation failed:', error);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate imported case data integrity (optional verification)
|
|
137
|
+
*/
|
|
138
|
+
export function validateCaseIntegrity(
|
|
139
|
+
caseData: CaseExportData,
|
|
140
|
+
imageFiles: { [filename: string]: Blob }
|
|
141
|
+
): { isValid: boolean; issues: string[] } {
|
|
142
|
+
const issues: string[] = [];
|
|
143
|
+
|
|
144
|
+
// Check if all referenced images exist
|
|
145
|
+
for (const fileEntry of caseData.files) {
|
|
146
|
+
const filename = fileEntry.fileData.originalFilename;
|
|
147
|
+
if (!imageFiles[filename]) {
|
|
148
|
+
issues.push(`Missing image file: ${filename}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if there are extra images not referenced in case data
|
|
153
|
+
const referencedFiles = new Set(caseData.files.map(f => f.fileData.originalFilename));
|
|
154
|
+
for (const filename of Object.keys(imageFiles)) {
|
|
155
|
+
if (!referencedFiles.has(filename)) {
|
|
156
|
+
issues.push(`Unreferenced image file: ${filename}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Validate metadata completeness
|
|
161
|
+
if (!caseData.metadata.caseNumber) {
|
|
162
|
+
issues.push('Missing case number in metadata');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!caseData.metadata.exportDate) {
|
|
166
|
+
issues.push('Missing export date in metadata');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate annotation data
|
|
170
|
+
for (const fileEntry of caseData.files) {
|
|
171
|
+
if (fileEntry.hasAnnotations && !fileEntry.annotations) {
|
|
172
|
+
issues.push(`File ${fileEntry.fileData.originalFilename} marked as having annotations but no annotation data found`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
isValid: issues.length === 0,
|
|
178
|
+
issues
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Validate confirmation data file signature.
|
|
184
|
+
*/
|
|
185
|
+
export async function validateConfirmationSignatureFile(
|
|
186
|
+
confirmationData: Partial<ConfirmationImportData>
|
|
187
|
+
): Promise<ManifestSignatureVerificationResult> {
|
|
188
|
+
return verifyConfirmationSignature(confirmationData);
|
|
189
|
+
}
|