@striae-org/striae 3.3.0 → 4.0.1
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 +1 -1
- package/README.md +1 -1
- package/app/components/actions/case-export/core-export.ts +5 -2
- package/app/components/actions/case-export/download-handlers.ts +1 -1
- package/app/components/actions/case-import/confirmation-import.ts +24 -23
- package/app/components/actions/case-import/image-operations.ts +20 -49
- package/app/components/actions/case-import/orchestrator.ts +1 -1
- package/app/components/actions/case-import/storage-operations.ts +54 -89
- package/app/components/actions/case-import/validation.ts +2 -13
- package/app/components/actions/case-manage.ts +15 -27
- package/app/components/actions/generate-pdf.ts +3 -7
- package/app/components/actions/image-manage.ts +64 -129
- package/app/components/button/button.module.css +12 -8
- package/app/components/sidebar/case-export/case-export.tsx +11 -6
- package/app/components/sidebar/cases/case-sidebar.tsx +21 -6
- package/app/components/sidebar/sidebar.module.css +0 -2
- package/app/components/user/delete-account.tsx +7 -7
- package/app/config-example/config.json +2 -8
- package/app/hooks/useInactivityTimeout.ts +2 -5
- package/app/root.tsx +94 -63
- package/app/routes/auth/emailActionHandler.tsx +1 -1
- package/app/routes/auth/emailVerification.tsx +1 -1
- package/app/routes/auth/login.tsx +7 -9
- package/app/routes/auth/passwordReset.tsx +1 -1
- package/app/routes/auth/route.ts +4 -3
- package/app/routes/striae/striae.tsx +4 -8
- package/app/services/audit/audit-api-client.ts +40 -0
- package/app/services/audit/audit-worker-client.ts +14 -17
- package/app/styles/root.module.css +13 -101
- package/app/tailwind.css +9 -2
- package/app/utils/auth.ts +5 -32
- package/app/utils/data-api-client.ts +43 -0
- package/app/utils/data-operations.ts +59 -75
- package/app/utils/image-api-client.ts +130 -0
- package/app/utils/pdf-api-client.ts +43 -0
- package/app/utils/permissions.ts +10 -23
- package/app/utils/user-api-client.ts +90 -0
- package/functions/api/_shared/firebase-auth.ts +255 -0
- package/functions/api/audit/[[path]].ts +150 -0
- package/functions/api/data/[[path]].ts +141 -0
- package/functions/api/image/[[path]].ts +143 -0
- package/functions/api/pdf/[[path]].ts +110 -0
- package/functions/api/user/[[path]].ts +196 -0
- package/package.json +3 -2
- package/public/.well-known/security.txt +3 -4
- package/scripts/deploy-all.sh +22 -8
- package/scripts/deploy-config.sh +194 -165
- package/scripts/deploy-pages-secrets.sh +231 -0
- package/scripts/deploy-worker-secrets.sh +1 -1
- package/worker-configuration.d.ts +7491 -11363
- package/workers/audit-worker/worker-configuration.d.ts +11323 -7448
- package/workers/audit-worker/wrangler.jsonc.example +1 -8
- package/workers/data-worker/worker-configuration.d.ts +11323 -7448
- package/workers/data-worker/wrangler.jsonc.example +1 -8
- package/workers/image-worker/src/image-worker.example.ts +10 -2
- package/workers/image-worker/worker-configuration.d.ts +11322 -7447
- package/workers/image-worker/wrangler.jsonc.example +1 -8
- package/workers/keys-worker/src/keys.ts +2 -1
- package/workers/keys-worker/worker-configuration.d.ts +11322 -7447
- package/workers/keys-worker/wrangler.jsonc.example +2 -9
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- package/workers/pdf-worker/worker-configuration.d.ts +11323 -7448
- package/workers/pdf-worker/wrangler.jsonc.example +1 -8
- package/workers/user-worker/src/user-worker.example.ts +121 -41
- package/workers/user-worker/worker-configuration.d.ts +11323 -7448
- package/workers/user-worker/wrangler.jsonc.example +1 -8
- package/wrangler.toml.example +1 -1
- package/app/styles/legal-pages.module.css +0 -113
package/.env.example
CHANGED
|
@@ -64,7 +64,7 @@ DATA_BUCKET_NAME=your_data_bucket_name_here
|
|
|
64
64
|
DATA_WORKER_DOMAIN=your_data_worker_domain_here
|
|
65
65
|
# Auto-generated by scripts/deploy-config.sh when placeholders are detected.
|
|
66
66
|
MANIFEST_SIGNING_PRIVATE_KEY=your_manifest_signing_private_key_here
|
|
67
|
-
MANIFEST_SIGNING_KEY_ID=
|
|
67
|
+
MANIFEST_SIGNING_KEY_ID=your_manifest_signing_key_id_here
|
|
68
68
|
MANIFEST_SIGNING_PUBLIC_KEY=your_manifest_signing_public_key_here
|
|
69
69
|
|
|
70
70
|
# ================================
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ This npm package publishes the Striae application source and deployment scaffold
|
|
|
6
6
|
|
|
7
7
|
## Live Project
|
|
8
8
|
|
|
9
|
-
- Application: [https://
|
|
9
|
+
- Application: [https://striae.app](https://striae.app)
|
|
10
10
|
- Source repository: [https://github.com/striae-org/striae](https://github.com/striae-org/striae)
|
|
11
11
|
- Releases: [https://github.com/striae-org/striae/releases](https://github.com/striae-org/striae/releases)
|
|
12
12
|
- Security policy: [https://github.com/striae-org/striae/security/policy](https://github.com/striae-org/striae/security/policy)
|
|
@@ -183,7 +183,8 @@ export async function exportAllCases(
|
|
|
183
183
|
export async function exportCaseData(
|
|
184
184
|
user: User,
|
|
185
185
|
caseNumber: string,
|
|
186
|
-
options: ExportOptions = {}
|
|
186
|
+
options: ExportOptions = {},
|
|
187
|
+
onProgress?: (current: number, total: number, label: string) => void
|
|
187
188
|
): Promise<CaseExportData> {
|
|
188
189
|
// NOTE: startTime and fileName tracking moved to download handlers
|
|
189
190
|
|
|
@@ -225,7 +226,8 @@ export async function exportCaseData(
|
|
|
225
226
|
let earliestAnnotationDate: string | undefined;
|
|
226
227
|
let latestAnnotationDate: string | undefined;
|
|
227
228
|
|
|
228
|
-
for (
|
|
229
|
+
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
230
|
+
const file = files[fileIndex];
|
|
229
231
|
let annotations: AnnotationData | undefined;
|
|
230
232
|
let hasAnnotations = false;
|
|
231
233
|
|
|
@@ -287,6 +289,7 @@ export async function exportCaseData(
|
|
|
287
289
|
annotations,
|
|
288
290
|
hasAnnotations
|
|
289
291
|
});
|
|
292
|
+
onProgress?.(fileIndex + 1, files.length, `Loading file ${fileIndex + 1} of ${files.length}`);
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
// Build export data
|
|
@@ -975,7 +975,7 @@ forensic procedures and maintain proper documentation.`;
|
|
|
975
975
|
const footer = `
|
|
976
976
|
|
|
977
977
|
Generated by Striae - A Firearms Examiner's Comparison Companion
|
|
978
|
-
https://
|
|
978
|
+
https://striae.app`;
|
|
979
979
|
|
|
980
980
|
return protectForensicData ? baseContent + forensicAddition + footer : baseContent + footer;
|
|
981
981
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
|
-
import
|
|
3
|
-
import { getDataApiKey } from '~/utils/auth';
|
|
2
|
+
import { fetchDataApi } from '~/utils/data-api-client';
|
|
4
3
|
import { type ConfirmationImportResult, type ConfirmationImportData } from '~/types';
|
|
5
4
|
import { checkExistingCase } from '../case-manage';
|
|
6
5
|
import { extractConfirmationImportPackage } from './confirmation-package';
|
|
7
6
|
import { validateExporterUid, validateConfirmationHash, validateConfirmationSignatureFile } from './validation';
|
|
8
7
|
import { auditService } from '~/services/audit';
|
|
9
8
|
|
|
10
|
-
const DATA_WORKER_URL = paths.data_worker_url;
|
|
11
|
-
|
|
12
9
|
interface CaseDataFile {
|
|
13
10
|
id: string;
|
|
14
11
|
originalFilename?: string;
|
|
@@ -113,13 +110,13 @@ export async function importConfirmationData(
|
|
|
113
110
|
onProgress?.('Processing confirmations', 60, 'Validating timestamps and updating annotations...');
|
|
114
111
|
|
|
115
112
|
// Get case data to find image IDs
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
'
|
|
113
|
+
const caseResponse = await fetchDataApi(
|
|
114
|
+
user,
|
|
115
|
+
`/${encodeURIComponent(user.uid)}/${encodeURIComponent(result.caseNumber)}/data.json`,
|
|
116
|
+
{
|
|
117
|
+
method: 'GET'
|
|
121
118
|
}
|
|
122
|
-
|
|
119
|
+
);
|
|
123
120
|
|
|
124
121
|
if (!caseResponse.ok) {
|
|
125
122
|
throw new Error(`Failed to fetch case data: ${caseResponse.status}`);
|
|
@@ -159,12 +156,13 @@ export async function importConfirmationData(
|
|
|
159
156
|
const displayFilename = currentFile?.originalFilename || currentImageId;
|
|
160
157
|
|
|
161
158
|
// Get current annotation data for this image
|
|
162
|
-
const annotationResponse = await
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
const annotationResponse = await fetchDataApi(
|
|
160
|
+
user,
|
|
161
|
+
`/${encodeURIComponent(user.uid)}/${encodeURIComponent(result.caseNumber)}/${encodeURIComponent(currentImageId)}/data.json`,
|
|
162
|
+
{
|
|
163
|
+
method: 'GET'
|
|
166
164
|
}
|
|
167
|
-
|
|
165
|
+
);
|
|
168
166
|
|
|
169
167
|
let annotationData: AnnotationImportData = {};
|
|
170
168
|
if (annotationResponse.ok) {
|
|
@@ -216,14 +214,17 @@ export async function importConfirmationData(
|
|
|
216
214
|
};
|
|
217
215
|
|
|
218
216
|
// Save updated annotation data
|
|
219
|
-
const saveResponse = await
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
'
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
217
|
+
const saveResponse = await fetchDataApi(
|
|
218
|
+
user,
|
|
219
|
+
`/${encodeURIComponent(user.uid)}/${encodeURIComponent(result.caseNumber)}/${encodeURIComponent(currentImageId)}/data.json`,
|
|
220
|
+
{
|
|
221
|
+
method: 'PUT',
|
|
222
|
+
headers: {
|
|
223
|
+
'Content-Type': 'application/json'
|
|
224
|
+
},
|
|
225
|
+
body: JSON.stringify(updatedAnnotationData)
|
|
226
|
+
}
|
|
227
|
+
);
|
|
227
228
|
|
|
228
229
|
if (saveResponse.ok) {
|
|
229
230
|
result.imagesUpdated++;
|
|
@@ -1,61 +1,32 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { type FileData
|
|
4
|
-
|
|
5
|
-
const IMAGE_WORKER_URL = paths.image_worker_url;
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
import { uploadImageApi } from '~/utils/image-api-client';
|
|
3
|
+
import { type FileData } from '~/types';
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* Upload image blob to image worker and get file data
|
|
9
7
|
*/
|
|
10
8
|
export async function uploadImageBlob(
|
|
9
|
+
user: User,
|
|
11
10
|
imageBlob: Blob,
|
|
12
11
|
originalFilename: string,
|
|
13
12
|
onProgress?: (filename: string, progress: number) => void
|
|
14
13
|
): Promise<FileData> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const file = new File([imageBlob], originalFilename, { type: imageBlob.type });
|
|
23
|
-
formData.append('file', file);
|
|
24
|
-
|
|
25
|
-
xhr.upload.addEventListener('progress', (event) => {
|
|
26
|
-
if (event.lengthComputable && onProgress) {
|
|
27
|
-
const progress = Math.round((event.loaded / event.total) * 100);
|
|
28
|
-
onProgress(originalFilename, progress);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
xhr.addEventListener('load', async () => {
|
|
33
|
-
if (xhr.status === 200) {
|
|
34
|
-
try {
|
|
35
|
-
const imageData = JSON.parse(xhr.responseText) as ImageUploadResponse;
|
|
36
|
-
if (!imageData.success) {
|
|
37
|
-
throw new Error(`Upload failed: ${imageData.errors?.join(', ') || 'Unknown error'}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const fileData: FileData = {
|
|
41
|
-
id: imageData.result.id,
|
|
42
|
-
originalFilename: originalFilename,
|
|
43
|
-
uploadedAt: new Date().toISOString()
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
resolve(fileData);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
reject(error);
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
52
|
-
}
|
|
53
|
-
});
|
|
14
|
+
// Create a File object from the blob to preserve the filename
|
|
15
|
+
const file = new File([imageBlob], originalFilename, { type: imageBlob.type });
|
|
16
|
+
const imageData = await uploadImageApi(user, file, (progress) => {
|
|
17
|
+
if (onProgress) {
|
|
18
|
+
onProgress(originalFilename, progress);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
54
21
|
|
|
55
|
-
|
|
22
|
+
const uploadedImageId = imageData.result?.id;
|
|
23
|
+
if (!uploadedImageId) {
|
|
24
|
+
throw new Error('Upload failed: missing image identifier');
|
|
25
|
+
}
|
|
56
26
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
27
|
+
return {
|
|
28
|
+
id: uploadedImageId,
|
|
29
|
+
originalFilename,
|
|
30
|
+
uploadedAt: new Date().toISOString()
|
|
31
|
+
};
|
|
61
32
|
}
|
|
@@ -283,7 +283,7 @@ export async function importCaseForReview(
|
|
|
283
283
|
const originalFileEntry = caseData.files.find(f => f.fileData.id === originalImageId);
|
|
284
284
|
const originalFilename = originalFileEntry?.fileData.originalFilename || exportFilename;
|
|
285
285
|
|
|
286
|
-
const fileData = await uploadImageBlob(blob, originalFilename, (fname, progress) => {
|
|
286
|
+
const fileData = await uploadImageBlob(user, blob, originalFilename, (fname, progress) => {
|
|
287
287
|
const overallProgress = 30 + (uploadedCount / totalImages) * 40 + (progress / totalImages) * 0.4;
|
|
288
288
|
onProgress?.('Uploading images', overallProgress, `Uploading ${fname}...`);
|
|
289
289
|
});
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
getDataApiKey,
|
|
5
|
-
getUserApiKey
|
|
6
|
-
} from '~/utils/auth';
|
|
2
|
+
import { fetchDataApi } from '~/utils/data-api-client';
|
|
7
3
|
import {
|
|
8
4
|
getUserReadOnlyCases,
|
|
9
5
|
updateUserData,
|
|
@@ -11,7 +7,6 @@ import {
|
|
|
11
7
|
} from '~/utils/permissions';
|
|
12
8
|
import {
|
|
13
9
|
type CaseExportData,
|
|
14
|
-
type ExtendedUserData,
|
|
15
10
|
type FileData,
|
|
16
11
|
type CaseData,
|
|
17
12
|
type ReadOnlyCaseMetadata
|
|
@@ -19,9 +14,6 @@ import {
|
|
|
19
14
|
import { deleteFile } from '../image-manage';
|
|
20
15
|
import { type SignedForensicManifest } from '~/utils/SHA256';
|
|
21
16
|
|
|
22
|
-
const USER_WORKER_URL = paths.user_worker_url;
|
|
23
|
-
const DATA_WORKER_URL = paths.data_worker_url;
|
|
24
|
-
|
|
25
17
|
/**
|
|
26
18
|
* Check if user already has a read-only case with the same number
|
|
27
19
|
*/
|
|
@@ -91,8 +83,6 @@ export async function storeCaseDataInR2(
|
|
|
91
83
|
forensicManifest?: SignedForensicManifest
|
|
92
84
|
): Promise<void> {
|
|
93
85
|
try {
|
|
94
|
-
const apiKey = await getDataApiKey();
|
|
95
|
-
|
|
96
86
|
// Convert the mapping to a plain object for JSON serialization
|
|
97
87
|
const originalImageIds = originalImageIdMapping ?
|
|
98
88
|
Object.fromEntries(originalImageIdMapping) : undefined;
|
|
@@ -122,14 +112,17 @@ export async function storeCaseDataInR2(
|
|
|
122
112
|
};
|
|
123
113
|
|
|
124
114
|
// Store in R2
|
|
125
|
-
const response = await
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
'
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
115
|
+
const response = await fetchDataApi(
|
|
116
|
+
user,
|
|
117
|
+
`/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`,
|
|
118
|
+
{
|
|
119
|
+
method: 'PUT',
|
|
120
|
+
headers: {
|
|
121
|
+
'Content-Type': 'application/json'
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify(r2CaseData)
|
|
124
|
+
}
|
|
125
|
+
);
|
|
133
126
|
|
|
134
127
|
if (!response.ok) {
|
|
135
128
|
throw new Error(`Failed to store case data: ${response.status}`);
|
|
@@ -146,24 +139,7 @@ export async function storeCaseDataInR2(
|
|
|
146
139
|
*/
|
|
147
140
|
export async function listReadOnlyCases(user: User): Promise<ReadOnlyCaseMetadata[]> {
|
|
148
141
|
try {
|
|
149
|
-
|
|
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 || [];
|
|
142
|
+
return await getUserReadOnlyCases(user);
|
|
167
143
|
|
|
168
144
|
} catch (error) {
|
|
169
145
|
console.error('Error listing read-only cases:', error);
|
|
@@ -176,48 +152,20 @@ export async function listReadOnlyCases(user: User): Promise<ReadOnlyCaseMetadat
|
|
|
176
152
|
*/
|
|
177
153
|
export async function removeReadOnlyCase(user: User, caseNumber: string): Promise<boolean> {
|
|
178
154
|
try {
|
|
179
|
-
const
|
|
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) {
|
|
155
|
+
const currentReadOnlyCases = await getUserReadOnlyCases(user);
|
|
156
|
+
if (!currentReadOnlyCases.length) {
|
|
197
157
|
return false; // Nothing to remove
|
|
198
158
|
}
|
|
199
159
|
|
|
200
160
|
// Remove the case from the list
|
|
201
|
-
const
|
|
202
|
-
userData.readOnlyCases = userData.readOnlyCases.filter(c => c.caseNumber !== caseNumber);
|
|
161
|
+
const nextReadOnlyCases = currentReadOnlyCases.filter(c => c.caseNumber !== caseNumber);
|
|
203
162
|
|
|
204
|
-
if (
|
|
163
|
+
if (nextReadOnlyCases.length === currentReadOnlyCases.length) {
|
|
205
164
|
return false; // Case wasn't found
|
|
206
165
|
}
|
|
207
166
|
|
|
208
167
|
// Update user data
|
|
209
|
-
|
|
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
|
-
}
|
|
168
|
+
await updateUserData(user, { readOnlyCases: nextReadOnlyCases });
|
|
221
169
|
|
|
222
170
|
return true;
|
|
223
171
|
|
|
@@ -232,30 +180,47 @@ export async function removeReadOnlyCase(user: User, caseNumber: string): Promis
|
|
|
232
180
|
*/
|
|
233
181
|
export async function deleteReadOnlyCase(user: User, caseNumber: string): Promise<boolean> {
|
|
234
182
|
try {
|
|
235
|
-
const dataApiKey = await getDataApiKey();
|
|
236
|
-
|
|
237
183
|
// Get case data first to get file IDs for deletion
|
|
238
|
-
const caseResponse = await
|
|
239
|
-
|
|
240
|
-
|
|
184
|
+
const caseResponse = await fetchDataApi(
|
|
185
|
+
user,
|
|
186
|
+
`/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`,
|
|
187
|
+
{
|
|
188
|
+
method: 'GET'
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (caseResponse.status === 404) {
|
|
193
|
+
// No backing data object exists; only remove the case reference from user metadata.
|
|
194
|
+
await removeReadOnlyCase(user, caseNumber);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
241
197
|
|
|
242
|
-
if (caseResponse.ok) {
|
|
243
|
-
|
|
198
|
+
if (!caseResponse.ok) {
|
|
199
|
+
throw new Error(`Failed to fetch read-only case data for deletion: ${caseResponse.status}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const caseData = await caseResponse.json() as CaseData;
|
|
203
|
+
|
|
204
|
+
// Delete all files using data worker
|
|
205
|
+
if (caseData.files && caseData.files.length > 0) {
|
|
206
|
+
await Promise.all(
|
|
207
|
+
caseData.files.map((file: FileData) =>
|
|
208
|
+
deleteFile(user, caseNumber, file.id, 'Read-only case clearing - API operation')
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
244
212
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
);
|
|
213
|
+
// Delete case file using data worker
|
|
214
|
+
const deleteCaseResponse = await fetchDataApi(
|
|
215
|
+
user,
|
|
216
|
+
`/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`,
|
|
217
|
+
{
|
|
218
|
+
method: 'DELETE'
|
|
252
219
|
}
|
|
220
|
+
);
|
|
253
221
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
method: 'DELETE',
|
|
257
|
-
headers: { 'X-Custom-Auth-Key': dataApiKey }
|
|
258
|
-
});
|
|
222
|
+
if (!deleteCaseResponse.ok && deleteCaseResponse.status !== 404) {
|
|
223
|
+
throw new Error(`Failed to delete read-only case data: ${deleteCaseResponse.status}`);
|
|
259
224
|
}
|
|
260
225
|
|
|
261
226
|
// Remove from user's read-only case list (separate from regular cases)
|
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
|
-
import
|
|
3
|
-
import { getUserApiKey } from '~/utils/auth';
|
|
2
|
+
import { checkUserExistsApi } from '~/utils/user-api-client';
|
|
4
3
|
import { type CaseExportData, type ConfirmationImportData } from '~/types';
|
|
5
4
|
import { type ManifestSignatureVerificationResult } from '~/utils/SHA256';
|
|
6
5
|
import { verifyConfirmationSignature } from '~/utils/confirmation-signature';
|
|
7
6
|
export { removeForensicWarning, validateConfirmationHash } from '~/utils/export-verification';
|
|
8
7
|
|
|
9
|
-
const USER_WORKER_URL = paths.user_worker_url;
|
|
10
|
-
|
|
11
8
|
/**
|
|
12
9
|
* Validate that a user exists in the database by UID and is not the current user
|
|
13
10
|
*/
|
|
14
11
|
export async function validateExporterUid(exporterUid: string, currentUser: User): Promise<{ exists: boolean; isSelf: boolean }> {
|
|
15
12
|
try {
|
|
16
|
-
const
|
|
17
|
-
const response = await fetch(`${USER_WORKER_URL}/${exporterUid}`, {
|
|
18
|
-
method: 'GET',
|
|
19
|
-
headers: {
|
|
20
|
-
'X-Custom-Auth-Key': apiKey
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const exists = response.status === 200;
|
|
13
|
+
const exists = await checkUserExistsApi(currentUser, exporterUid);
|
|
25
14
|
const isSelf = exporterUid === currentUser.uid;
|
|
26
15
|
|
|
27
16
|
return { exists, isSelf };
|
|
@@ -15,8 +15,7 @@ import {
|
|
|
15
15
|
} from '~/utils/data-operations';
|
|
16
16
|
import { type CaseData, type ReadOnlyCaseData, type FileData } from '~/types';
|
|
17
17
|
import { auditService } from '~/services/audit';
|
|
18
|
-
import {
|
|
19
|
-
import paths from '~/config/config.json';
|
|
18
|
+
import { fetchImageApi } from '~/utils/image-api-client';
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
21
|
* Delete a file without individual audit logging (for bulk operations)
|
|
@@ -34,34 +33,17 @@ const deleteFileWithoutAudit = async (user: User, caseNumber: string, fileId: st
|
|
|
34
33
|
throw new Error('File not found in case');
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
// Delete
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const imagesApiToken = await getImageApiKey();
|
|
42
|
-
const imageResponse = await fetch(`${IMAGE_URL}/${fileId}`, {
|
|
43
|
-
method: 'DELETE',
|
|
44
|
-
headers: {
|
|
45
|
-
'Authorization': `Bearer ${imagesApiToken}`
|
|
46
|
-
}
|
|
47
|
-
});
|
|
36
|
+
// Delete image file and fail fast on non-404 failures so case deletion can be retried safely
|
|
37
|
+
const imageResponse = await fetchImageApi(user, `/${encodeURIComponent(fileId)}`, {
|
|
38
|
+
method: 'DELETE'
|
|
39
|
+
});
|
|
48
40
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
throw new Error(`Failed to delete image: ${imageResponse.statusText}`);
|
|
52
|
-
}
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.warn(`Image deletion warning for ${fileToDelete.originalFilename}:`, error);
|
|
55
|
-
// Continue with data cleanup even if image deletion fails
|
|
41
|
+
if (!imageResponse.ok && imageResponse.status !== 404) {
|
|
42
|
+
throw new Error(`Failed to delete image: ${imageResponse.status} ${imageResponse.statusText}`);
|
|
56
43
|
}
|
|
57
44
|
|
|
58
|
-
// Delete annotation data
|
|
59
|
-
|
|
60
|
-
await deleteFileAnnotations(user, caseNumber, fileId);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
// Annotation file might not exist, continue
|
|
63
|
-
console.warn(`Annotation deletion warning for ${fileToDelete.originalFilename}:`, error);
|
|
64
|
-
}
|
|
45
|
+
// Delete annotation data (404s are handled by deleteFileAnnotations)
|
|
46
|
+
await deleteFileAnnotations(user, caseNumber, fileId);
|
|
65
47
|
|
|
66
48
|
// Update case data to remove file reference
|
|
67
49
|
const updatedData: CaseData = {
|
|
@@ -466,6 +448,12 @@ export const deleteCase = async (user: User, caseNumber: string): Promise<void>
|
|
|
466
448
|
} catch (auditError) {
|
|
467
449
|
console.error('⚠️ Failed to log batch file deletion (continuing with case deletion):', auditError);
|
|
468
450
|
}
|
|
451
|
+
|
|
452
|
+
if (failedFiles.length > 0) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Case deletion aborted: failed to delete ${failedFiles.length} file(s): ${failedFiles.map(f => f.originalFilename).join(', ')}`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
469
457
|
}
|
|
470
458
|
|
|
471
459
|
// Remove case from user data first (so user loses access immediately)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import paths from '~/config/config.json';
|
|
2
1
|
import { type AnnotationData } from '~/types/annotations';
|
|
3
2
|
import { auditService } from '~/services/audit';
|
|
4
|
-
import { getPdfApiKey } from '~/utils/auth';
|
|
5
3
|
import type { User } from 'firebase/auth';
|
|
4
|
+
import { fetchPdfApi } from '~/utils/pdf-api-client';
|
|
6
5
|
|
|
7
6
|
interface GeneratePDFParams {
|
|
8
7
|
user: User;
|
|
@@ -75,13 +74,10 @@ export const generatePDF = async ({
|
|
|
75
74
|
data: pdfData,
|
|
76
75
|
};
|
|
77
76
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const response = await fetch(paths.pdf_worker_url, {
|
|
77
|
+
const response = await fetchPdfApi(user, '/', {
|
|
81
78
|
method: 'POST',
|
|
82
79
|
headers: {
|
|
83
|
-
'Content-Type': 'application/json'
|
|
84
|
-
'X-Custom-Auth-Key': pdfAuthKey,
|
|
80
|
+
'Content-Type': 'application/json'
|
|
85
81
|
},
|
|
86
82
|
body: JSON.stringify(pdfRequest)
|
|
87
83
|
});
|