@striae-org/striae 5.0.0 → 5.1.0
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 +5 -2
- package/app/components/actions/case-export/download-handlers.ts +6 -7
- package/app/components/actions/case-manage.ts +10 -11
- package/app/components/actions/generate-pdf.ts +43 -1
- package/app/components/actions/image-manage.ts +13 -45
- package/app/routes/striae/hooks/use-striae-reset-helpers.ts +4 -0
- package/app/routes/striae/striae.tsx +15 -4
- package/app/utils/data/operations/case-operations.ts +13 -1
- package/app/utils/data/operations/confirmation-summary-operations.ts +38 -1
- package/app/utils/data/operations/file-annotation-operations.ts +13 -1
- package/package.json +2 -2
- package/scripts/deploy-config.sh +149 -6
- package/scripts/deploy-pages-secrets.sh +0 -6
- package/scripts/deploy-worker-secrets.sh +66 -5
- package/scripts/encrypt-r2-backfill.mjs +376 -0
- package/worker-configuration.d.ts +13 -7
- package/workers/audit-worker/package.json +1 -4
- package/workers/audit-worker/src/audit-worker.example.ts +522 -61
- package/workers/audit-worker/wrangler.jsonc.example +5 -0
- package/workers/data-worker/package.json +1 -4
- package/workers/data-worker/src/data-worker.example.ts +280 -2
- package/workers/data-worker/src/encryption-utils.ts +145 -1
- package/workers/data-worker/wrangler.jsonc.example +4 -0
- package/workers/image-worker/package.json +1 -4
- package/workers/image-worker/src/encryption-utils.ts +217 -0
- package/workers/image-worker/src/image-worker.example.ts +196 -127
- package/workers/image-worker/wrangler.jsonc.example +7 -0
- package/workers/keys-worker/package.json +1 -4
- package/workers/pdf-worker/package.json +1 -4
- package/workers/user-worker/package.json +1 -4
package/.env.example
CHANGED
|
@@ -61,6 +61,7 @@ KV_STORE_ID=your_kv_store_id_here
|
|
|
61
61
|
# ================================
|
|
62
62
|
DATA_WORKER_NAME=your_data_worker_name_here
|
|
63
63
|
DATA_BUCKET_NAME=your_data_bucket_name_here
|
|
64
|
+
FILES_BUCKET_NAME=your_files_bucket_name_here
|
|
64
65
|
DATA_WORKER_DOMAIN=your_data_worker_domain_here
|
|
65
66
|
# Auto-generated by scripts/deploy-config.sh when placeholders are detected.
|
|
66
67
|
MANIFEST_SIGNING_PRIVATE_KEY=your_manifest_signing_private_key_here
|
|
@@ -69,6 +70,10 @@ MANIFEST_SIGNING_PUBLIC_KEY=your_manifest_signing_public_key_here
|
|
|
69
70
|
EXPORT_ENCRYPTION_PRIVATE_KEY=your_export_encryption_private_key_here
|
|
70
71
|
EXPORT_ENCRYPTION_KEY_ID=your_export_encryption_key_id_here
|
|
71
72
|
EXPORT_ENCRYPTION_PUBLIC_KEY=your_export_encryption_public_key_here
|
|
73
|
+
DATA_AT_REST_ENCRYPTION_ENABLED=true
|
|
74
|
+
DATA_AT_REST_ENCRYPTION_PRIVATE_KEY=your_data_at_rest_encryption_private_key_here
|
|
75
|
+
DATA_AT_REST_ENCRYPTION_KEY_ID=your_data_at_rest_encryption_key_id_here
|
|
76
|
+
DATA_AT_REST_ENCRYPTION_PUBLIC_KEY=your_data_at_rest_encryption_public_key_here
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
# ================================
|
|
@@ -83,8 +88,6 @@ AUDIT_WORKER_DOMAIN=your_audit_worker_domain_here
|
|
|
83
88
|
# ================================
|
|
84
89
|
IMAGES_WORKER_NAME=your_images_worker_name_here
|
|
85
90
|
IMAGES_WORKER_DOMAIN=your_images_worker_domain_here
|
|
86
|
-
API_TOKEN=your_cloudflare_images_api_token_here
|
|
87
|
-
HMAC_KEY=your_cloudflare_images_hmac_key_here
|
|
88
91
|
|
|
89
92
|
# ================================
|
|
90
93
|
# PDF WORKER ENVIRONMENT VARIABLES
|
|
@@ -965,13 +965,12 @@ For questions about this export, contact your Striae system administrator.
|
|
|
965
965
|
*/
|
|
966
966
|
async function fetchImageAsBlob(user: User, fileData: FileData, caseNumber: string): Promise<Blob | null> {
|
|
967
967
|
try {
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
return await response.blob();
|
|
968
|
+
const { blob, revoke } = await getImageUrl(user, fileData, caseNumber, 'Export Package');
|
|
969
|
+
try {
|
|
970
|
+
return blob;
|
|
971
|
+
} finally {
|
|
972
|
+
revoke();
|
|
973
|
+
}
|
|
975
974
|
} catch (error) {
|
|
976
975
|
console.error('Failed to fetch image blob:', error);
|
|
977
976
|
return null;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
duplicateCaseData,
|
|
13
13
|
deleteFileAnnotations,
|
|
14
14
|
signForensicManifest,
|
|
15
|
+
moveCaseConfirmationSummary,
|
|
15
16
|
removeCaseConfirmationSummary
|
|
16
17
|
} from '~/utils/data';
|
|
17
18
|
import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData, type ValidationAuditEntry } from '~/types';
|
|
@@ -392,7 +393,10 @@ export const renameCase = async (
|
|
|
392
393
|
// 4) Delete R2 case data with old case number
|
|
393
394
|
await deleteCaseData(user, oldCaseNumber);
|
|
394
395
|
|
|
395
|
-
// 5)
|
|
396
|
+
// 5) Move confirmation summary metadata to the new case number
|
|
397
|
+
await moveCaseConfirmationSummary(user, oldCaseNumber, newCaseNumber);
|
|
398
|
+
|
|
399
|
+
// 6) Delete old case number in user's KV entry
|
|
396
400
|
await removeUserCase(user, oldCaseNumber);
|
|
397
401
|
|
|
398
402
|
// Log successful case rename under the original case number context
|
|
@@ -681,18 +685,13 @@ const getVerificationPublicSigningKey = (preferredKeyId?: string): { keyId: stri
|
|
|
681
685
|
|
|
682
686
|
const fetchImageAsBlob = async (user: User, fileData: FileData, caseNumber: string): Promise<Blob | null> => {
|
|
683
687
|
try {
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
if (!imageUrl) {
|
|
687
|
-
return null;
|
|
688
|
-
}
|
|
688
|
+
const { blob, revoke } = await getImageUrl(user, fileData, caseNumber, 'Archive Package');
|
|
689
689
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
690
|
+
try {
|
|
691
|
+
return blob;
|
|
692
|
+
} finally {
|
|
693
|
+
revoke();
|
|
693
694
|
}
|
|
694
|
-
|
|
695
|
-
return await response.blob();
|
|
696
695
|
} catch (error) {
|
|
697
696
|
console.error('Failed to fetch image for archive package:', error);
|
|
698
697
|
return null;
|
|
@@ -21,6 +21,46 @@ interface GeneratePDFParams {
|
|
|
21
21
|
setToastDuration?: (duration: number) => void;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
const CLEAR_IMAGE_SENTINEL = '/clear.jpg';
|
|
25
|
+
|
|
26
|
+
const blobToDataUrl = async (blob: Blob): Promise<string> => {
|
|
27
|
+
return await new Promise<string>((resolve, reject) => {
|
|
28
|
+
const reader = new FileReader();
|
|
29
|
+
reader.onloadend = () => {
|
|
30
|
+
if (typeof reader.result === 'string') {
|
|
31
|
+
resolve(reader.result);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
reject(new Error('Failed to read image blob as data URL'));
|
|
36
|
+
};
|
|
37
|
+
reader.onerror = () => reject(new Error('Failed to convert image for PDF rendering'));
|
|
38
|
+
reader.readAsDataURL(blob);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const resolvePdfImageUrl = async (selectedImage: string | undefined): Promise<string | undefined> => {
|
|
43
|
+
if (!selectedImage || selectedImage === CLEAR_IMAGE_SENTINEL) {
|
|
44
|
+
return selectedImage;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (selectedImage.startsWith('data:')) {
|
|
48
|
+
return selectedImage;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (selectedImage.startsWith('blob:')) {
|
|
52
|
+
const imageResponse = await fetch(selectedImage);
|
|
53
|
+
if (!imageResponse.ok) {
|
|
54
|
+
throw new Error('Failed to load selected image for PDF generation');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const imageBlob = await imageResponse.blob();
|
|
58
|
+
return await blobToDataUrl(imageBlob);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return selectedImage;
|
|
62
|
+
};
|
|
63
|
+
|
|
24
64
|
export const generatePDF = async ({
|
|
25
65
|
user,
|
|
26
66
|
selectedImage,
|
|
@@ -61,8 +101,10 @@ export const generatePDF = async ({
|
|
|
61
101
|
notesUpdatedFormatted = `${(updatedDate.getMonth() + 1).toString().padStart(2, '0')}/${updatedDate.getDate().toString().padStart(2, '0')}/${updatedDate.getFullYear()}`;
|
|
62
102
|
}
|
|
63
103
|
|
|
104
|
+
const resolvedImageUrl = await resolvePdfImageUrl(selectedImage);
|
|
105
|
+
|
|
64
106
|
const pdfData = {
|
|
65
|
-
imageUrl:
|
|
107
|
+
imageUrl: resolvedImageUrl,
|
|
66
108
|
filename: selectedFilename,
|
|
67
109
|
userCompany: userCompany,
|
|
68
110
|
firstName: userFirstName,
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
|
-
import {
|
|
3
|
-
getAccountHash
|
|
4
|
-
} from '~/utils/auth';
|
|
5
2
|
import { fetchImageApi, uploadImageApi } from '~/utils/api';
|
|
6
3
|
import { canUploadFile, getCaseData, updateCaseData, deleteFileAnnotations } from '~/utils/data';
|
|
7
4
|
import type { CaseData, FileData, ImageUploadResponse } from '~/types';
|
|
@@ -258,30 +255,15 @@ export const deleteFile = async (
|
|
|
258
255
|
}
|
|
259
256
|
};
|
|
260
257
|
|
|
261
|
-
const
|
|
262
|
-
interface ImageDeliveryConfig {
|
|
263
|
-
accountHash: string;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const getImageConfig = async (): Promise<ImageDeliveryConfig> => {
|
|
267
|
-
const accountHash = await getAccountHash();
|
|
268
|
-
return { accountHash };
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
export const getImageUrl = async (user: User, fileData: FileData, caseNumber: string, accessReason?: string): Promise<string> => {
|
|
258
|
+
export const getImageUrl = async (user: User, fileData: FileData, caseNumber: string, accessReason?: string): Promise<{ blob: Blob; url: string; revoke: () => void }> => {
|
|
273
259
|
const startTime = Date.now();
|
|
274
260
|
const defaultAccessReason = accessReason || 'Image viewer access';
|
|
275
261
|
|
|
276
262
|
try {
|
|
277
|
-
const
|
|
278
|
-
const imageDeliveryUrl = `https://imagedelivery.net/${accountHash}/${fileData.id}/${DEFAULT_VARIANT}`;
|
|
279
|
-
const encodedImageDeliveryUrl = encodeURIComponent(imageDeliveryUrl);
|
|
280
|
-
|
|
281
|
-
const workerResponse = await fetchImageApi(user, `/${encodedImageDeliveryUrl}`, {
|
|
263
|
+
const workerResponse = await fetchImageApi(user, `/${encodeURIComponent(fileData.id)}`, {
|
|
282
264
|
method: 'GET',
|
|
283
265
|
headers: {
|
|
284
|
-
'Accept': '
|
|
266
|
+
'Accept': 'application/octet-stream,image/*'
|
|
285
267
|
}
|
|
286
268
|
});
|
|
287
269
|
|
|
@@ -291,39 +273,25 @@ export const getImageUrl = async (user: User, fileData: FileData, caseNumber: st
|
|
|
291
273
|
user,
|
|
292
274
|
fileData.originalFilename || fileData.id,
|
|
293
275
|
fileData.id,
|
|
294
|
-
'
|
|
295
|
-
caseNumber,
|
|
296
|
-
'failure',
|
|
297
|
-
Date.now() - startTime,
|
|
298
|
-
'Image URL generation failed',
|
|
299
|
-
fileData.originalFilename
|
|
300
|
-
);
|
|
301
|
-
throw new Error('Failed to get signed image URL');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const signedUrl = await workerResponse.text();
|
|
305
|
-
if (!signedUrl.includes('sig=') || !signedUrl.includes('exp=')) {
|
|
306
|
-
// Log invalid URL response
|
|
307
|
-
await auditService.logFileAccess(
|
|
308
|
-
user,
|
|
309
|
-
fileData.originalFilename || fileData.id,
|
|
310
|
-
fileData.id,
|
|
311
|
-
'signed-url',
|
|
276
|
+
'direct-url',
|
|
312
277
|
caseNumber,
|
|
313
278
|
'failure',
|
|
314
279
|
Date.now() - startTime,
|
|
315
|
-
'
|
|
280
|
+
'Image retrieval failed',
|
|
316
281
|
fileData.originalFilename
|
|
317
282
|
);
|
|
318
|
-
throw new Error('
|
|
283
|
+
throw new Error('Failed to retrieve image');
|
|
319
284
|
}
|
|
285
|
+
|
|
286
|
+
const blob = await workerResponse.blob();
|
|
287
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
320
288
|
|
|
321
289
|
// Log successful image access
|
|
322
290
|
await auditService.logFileAccess(
|
|
323
291
|
user,
|
|
324
292
|
fileData.originalFilename || fileData.id,
|
|
325
293
|
fileData.id,
|
|
326
|
-
'
|
|
294
|
+
'direct-url',
|
|
327
295
|
caseNumber,
|
|
328
296
|
'success',
|
|
329
297
|
Date.now() - startTime,
|
|
@@ -331,15 +299,15 @@ export const getImageUrl = async (user: User, fileData: FileData, caseNumber: st
|
|
|
331
299
|
fileData.originalFilename
|
|
332
300
|
);
|
|
333
301
|
|
|
334
|
-
return
|
|
302
|
+
return { blob, url: objectUrl, revoke: () => URL.revokeObjectURL(objectUrl) };
|
|
335
303
|
} catch (error) {
|
|
336
304
|
// Log any unexpected errors if not already logged
|
|
337
|
-
if (!(error instanceof Error && error.message.includes('Failed to
|
|
305
|
+
if (!(error instanceof Error && error.message.includes('Failed to retrieve image'))) {
|
|
338
306
|
await auditService.logFileAccess(
|
|
339
307
|
user,
|
|
340
308
|
fileData.originalFilename || fileData.id,
|
|
341
309
|
fileData.id,
|
|
342
|
-
'
|
|
310
|
+
'direct-url',
|
|
343
311
|
caseNumber,
|
|
344
312
|
'failure',
|
|
345
313
|
Date.now() - startTime,
|
|
@@ -26,6 +26,7 @@ interface UseStriaeResetHelpersProps {
|
|
|
26
26
|
setShowNotes: Dispatch<SetStateAction<boolean>>;
|
|
27
27
|
setIsAuditTrailOpen: Dispatch<SetStateAction<boolean>>;
|
|
28
28
|
setIsRenameCaseModalOpen: Dispatch<SetStateAction<boolean>>;
|
|
29
|
+
onRevokeImage?: () => void;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export const useStriaeResetHelpers = ({
|
|
@@ -45,8 +46,10 @@ export const useStriaeResetHelpers = ({
|
|
|
45
46
|
setShowNotes,
|
|
46
47
|
setIsAuditTrailOpen,
|
|
47
48
|
setIsRenameCaseModalOpen,
|
|
49
|
+
onRevokeImage,
|
|
48
50
|
}: UseStriaeResetHelpersProps) => {
|
|
49
51
|
const clearSelectedImageState = useCallback(() => {
|
|
52
|
+
onRevokeImage?.();
|
|
50
53
|
setSelectedImage('/clear.jpg');
|
|
51
54
|
setSelectedFilename(undefined);
|
|
52
55
|
setImageId(undefined);
|
|
@@ -54,6 +57,7 @@ export const useStriaeResetHelpers = ({
|
|
|
54
57
|
setError(undefined);
|
|
55
58
|
setImageLoaded(false);
|
|
56
59
|
}, [
|
|
60
|
+
onRevokeImage,
|
|
57
61
|
setSelectedImage,
|
|
58
62
|
setSelectedFilename,
|
|
59
63
|
setImageId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
3
3
|
import { SidebarContainer } from '~/components/sidebar/sidebar-container';
|
|
4
4
|
import { Navbar } from '~/components/navbar/navbar';
|
|
5
5
|
import { RenameCaseModal } from '~/components/navbar/case-modals/rename-case-modal';
|
|
@@ -47,6 +47,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
47
47
|
const [imageId, setImageId] = useState<string>();
|
|
48
48
|
const [error, setError] = useState<string>();
|
|
49
49
|
const [imageLoaded, setImageLoaded] = useState(false);
|
|
50
|
+
const currentRevokeRef = useRef<(() => void) | null>(null);
|
|
50
51
|
|
|
51
52
|
// User states
|
|
52
53
|
const [userCompany, setUserCompany] = useState<string>('');
|
|
@@ -98,6 +99,11 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
98
99
|
archiveReason?: string;
|
|
99
100
|
}>({ archived: false });
|
|
100
101
|
|
|
102
|
+
const handleRevokeImage = useCallback(() => {
|
|
103
|
+
currentRevokeRef.current?.();
|
|
104
|
+
currentRevokeRef.current = null;
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
101
107
|
const {
|
|
102
108
|
clearSelectedImageState,
|
|
103
109
|
clearCaseContextState,
|
|
@@ -119,6 +125,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
119
125
|
setShowNotes,
|
|
120
126
|
setIsAuditTrailOpen,
|
|
121
127
|
setIsRenameCaseModalOpen,
|
|
128
|
+
onRevokeImage: handleRevokeImage,
|
|
122
129
|
});
|
|
123
130
|
|
|
124
131
|
|
|
@@ -574,6 +581,8 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
574
581
|
useEffect(() => {
|
|
575
582
|
// Cleanup function to clear image when component unmounts
|
|
576
583
|
return () => {
|
|
584
|
+
currentRevokeRef.current?.();
|
|
585
|
+
currentRevokeRef.current = null;
|
|
577
586
|
setSelectedImage(undefined);
|
|
578
587
|
setSelectedFilename(undefined);
|
|
579
588
|
setError(undefined);
|
|
@@ -645,14 +654,16 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
645
654
|
|
|
646
655
|
try {
|
|
647
656
|
setError(undefined);
|
|
657
|
+
currentRevokeRef.current?.();
|
|
658
|
+
currentRevokeRef.current = null;
|
|
648
659
|
setSelectedImage(undefined);
|
|
649
660
|
setSelectedFilename(undefined);
|
|
650
661
|
setImageLoaded(false);
|
|
651
662
|
|
|
652
|
-
const
|
|
653
|
-
|
|
663
|
+
const { url, revoke } = await getImageUrl(user, file, currentCase);
|
|
664
|
+
currentRevokeRef.current = revoke;
|
|
654
665
|
|
|
655
|
-
setSelectedImage(
|
|
666
|
+
setSelectedImage(url);
|
|
656
667
|
setSelectedFilename(file.originalFilename);
|
|
657
668
|
setImageId(file.id);
|
|
658
669
|
setImageLoaded(true);
|
|
@@ -43,7 +43,19 @@ export const getCaseData = async (
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (!response.ok) {
|
|
46
|
-
|
|
46
|
+
let errorDetails = '';
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const errorPayload = await response.json() as { error?: unknown };
|
|
50
|
+
if (typeof errorPayload?.error === 'string' && errorPayload.error.trim().length > 0) {
|
|
51
|
+
errorDetails = errorPayload.error.trim();
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Ignore parse errors and fall back to status text only.
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const baseMessage = `Failed to fetch case data: ${response.status} ${response.statusText}`;
|
|
58
|
+
throw new Error(errorDetails ? `${baseMessage} - ${errorDetails}` : baseMessage);
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
const caseData = await response.json() as CaseData;
|
|
@@ -86,7 +86,19 @@ export const getConfirmationSummaryDocument = async (
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
if (!response.ok) {
|
|
89
|
-
|
|
89
|
+
let errorDetails = '';
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const errorPayload = await response.json() as { error?: unknown };
|
|
93
|
+
if (typeof errorPayload?.error === 'string' && errorPayload.error.trim().length > 0) {
|
|
94
|
+
errorDetails = errorPayload.error.trim();
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Ignore parse errors and fall back to status text only.
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const baseMessage = `Failed to fetch confirmation summary: ${response.status} ${response.statusText}`;
|
|
101
|
+
throw new Error(errorDetails ? `${baseMessage} - ${errorDetails}` : baseMessage);
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
const payload = await response.json().catch(() => null) as unknown;
|
|
@@ -299,3 +311,28 @@ export const removeCaseConfirmationSummary = async (
|
|
|
299
311
|
|
|
300
312
|
await saveConfirmationSummaryDocument(user, summary);
|
|
301
313
|
};
|
|
314
|
+
|
|
315
|
+
export const moveCaseConfirmationSummary = async (
|
|
316
|
+
user: User,
|
|
317
|
+
fromCaseNumber: string,
|
|
318
|
+
toCaseNumber: string
|
|
319
|
+
): Promise<void> => {
|
|
320
|
+
if (fromCaseNumber === toCaseNumber) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const summary = await getConfirmationSummaryDocument(user);
|
|
325
|
+
const existingCaseSummary = summary.cases[fromCaseNumber];
|
|
326
|
+
if (!existingCaseSummary) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
delete summary.cases[fromCaseNumber];
|
|
331
|
+
summary.cases[toCaseNumber] = {
|
|
332
|
+
...existingCaseSummary,
|
|
333
|
+
updatedAt: getIsoNow(),
|
|
334
|
+
};
|
|
335
|
+
summary.updatedAt = getIsoNow();
|
|
336
|
+
|
|
337
|
+
await saveConfirmationSummaryDocument(user, summary);
|
|
338
|
+
};
|
|
@@ -42,7 +42,19 @@ export const getFileAnnotations = async (
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if (!response.ok) {
|
|
45
|
-
|
|
45
|
+
let errorDetails = '';
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const errorPayload = await response.json() as { error?: unknown };
|
|
49
|
+
if (typeof errorPayload?.error === 'string' && errorPayload.error.trim().length > 0) {
|
|
50
|
+
errorDetails = errorPayload.error.trim();
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Ignore parse errors and fall back to status text only.
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const baseMessage = `Failed to fetch file annotations: ${response.status} ${response.statusText}`;
|
|
57
|
+
throw new Error(errorDetails ? `${baseMessage} - ${errorDetails}` : baseMessage);
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
return await response.json() as AnnotationData;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"deploy-workers:image": "cd workers/image-worker && npm run deploy",
|
|
107
107
|
"deploy-workers:keys": "cd workers/keys-worker && npm run deploy",
|
|
108
108
|
"deploy-workers:pdf": "cd workers/pdf-worker && npm run deploy",
|
|
109
|
-
"deploy-workers:user": "cd workers/user-worker && npm run deploy"
|
|
109
|
+
"deploy-workers:user": "cd workers/user-worker && npm run deploy"
|
|
110
110
|
},
|
|
111
111
|
"dependencies": {
|
|
112
112
|
"@react-router/cloudflare": "^7.13.2",
|