@striae-org/striae 4.3.3 → 5.0.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 +4 -0
- package/app/components/actions/case-export/download-handlers.ts +60 -4
- package/app/components/actions/case-import/confirmation-import.ts +50 -7
- package/app/components/actions/case-import/confirmation-package.ts +99 -22
- package/app/components/actions/case-import/orchestrator.ts +116 -13
- package/app/components/actions/case-import/validation.ts +171 -7
- package/app/components/actions/case-import/zip-processing.ts +224 -127
- package/app/components/actions/case-manage.ts +110 -10
- package/app/components/actions/confirm-export.ts +32 -3
- package/app/components/audit/user-audit.module.css +49 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +130 -48
- package/app/components/navbar/navbar.module.css +0 -10
- package/app/components/navbar/navbar.tsx +0 -22
- package/app/components/sidebar/case-import/case-import.module.css +7 -131
- package/app/components/sidebar/case-import/case-import.tsx +7 -14
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +17 -60
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +23 -39
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +5 -45
- package/app/components/sidebar/case-import/components/FileSelector.tsx +5 -6
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +2 -48
- package/app/components/sidebar/case-import/utils/file-validation.ts +9 -21
- package/app/config-example/config.json +5 -0
- package/app/routes/auth/login.tsx +1 -1
- package/app/services/audit/audit-console-logger.ts +1 -1
- package/app/services/audit/audit-export-csv.ts +1 -1
- package/app/services/audit/audit-export-signing.ts +2 -2
- package/app/services/audit/audit-export.service.ts +1 -1
- package/app/services/audit/audit-worker-client.ts +1 -1
- package/app/services/audit/audit.service.ts +5 -75
- package/app/services/audit/builders/audit-event-builders-case-file.ts +3 -0
- package/app/services/audit/index.ts +2 -2
- package/app/types/audit.ts +8 -7
- package/app/utils/data/operations/signing-operations.ts +93 -0
- package/app/utils/data/operations/types.ts +6 -0
- package/app/utils/forensics/export-encryption.ts +316 -0
- package/app/utils/forensics/export-verification.ts +1 -409
- package/app/utils/forensics/index.ts +1 -0
- package/app/utils/ui/case-messages.ts +5 -2
- package/package.json +1 -1
- package/scripts/deploy-config.sh +97 -3
- package/scripts/deploy-worker-secrets.sh +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/data-worker.example.ts +130 -0
- package/workers/data-worker/src/encryption-utils.ts +125 -0
- package/workers/data-worker/worker-configuration.d.ts +1 -1
- package/workers/data-worker/wrangler.jsonc.example +2 -2
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +0 -287
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +0 -470
|
@@ -425,6 +425,13 @@
|
|
|
425
425
|
color: var(--textTitle);
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
.previewMessage {
|
|
429
|
+
margin: 0;
|
|
430
|
+
color: var(--textBody);
|
|
431
|
+
font-size: var(--fontSizeBodyS);
|
|
432
|
+
line-height: 1.5;
|
|
433
|
+
}
|
|
434
|
+
|
|
428
435
|
.archivedImportNote {
|
|
429
436
|
margin-bottom: var(--spaceM);
|
|
430
437
|
padding: var(--spaceS) var(--spaceM);
|
|
@@ -447,62 +454,6 @@
|
|
|
447
454
|
font-weight: var(--fontWeightMedium);
|
|
448
455
|
}
|
|
449
456
|
|
|
450
|
-
/* Validation Section - Green/Red Based on Status */
|
|
451
|
-
.validationSection {
|
|
452
|
-
border-radius: var(--spaceXS);
|
|
453
|
-
padding: var(--spaceM);
|
|
454
|
-
margin: var(--spaceM) 0;
|
|
455
|
-
background: color-mix(in lab, var(--primary) 5%, transparent);
|
|
456
|
-
border: 1px solid color-mix(in lab, var(--primary) 15%, transparent);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
.validationSectionValid {
|
|
460
|
-
background: color-mix(in lab, var(--success) 8%, transparent) !important;
|
|
461
|
-
border: 2px solid color-mix(in lab, var(--success) 25%, transparent) !important;
|
|
462
|
-
box-shadow: 0 2px 6px color-mix(in lab, var(--success) 12%, transparent) !important;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
.validationSectionInvalid {
|
|
466
|
-
background: color-mix(in lab, var(--error) 8%, transparent) !important;
|
|
467
|
-
border: 2px solid color-mix(in lab, var(--error) 25%, transparent) !important;
|
|
468
|
-
box-shadow: 0 2px 6px color-mix(in lab, var(--error) 12%, transparent) !important;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
.validationTitle {
|
|
472
|
-
margin: 0 0 var(--spaceM) 0;
|
|
473
|
-
font-size: var(--fontSizeBodyM);
|
|
474
|
-
font-weight: var(--fontWeightMedium);
|
|
475
|
-
color: var(--textTitle);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
.validationItem {
|
|
479
|
-
display: flex;
|
|
480
|
-
justify-content: space-between;
|
|
481
|
-
align-items: center;
|
|
482
|
-
padding: var(--spaceXS) 0;
|
|
483
|
-
font-size: var(--fontSizeBodyS);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
.validationLabel {
|
|
487
|
-
font-weight: var(--fontWeightMedium);
|
|
488
|
-
color: var(--textTitle);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
.validationValue {
|
|
492
|
-
font-family: var(--fontMono);
|
|
493
|
-
font-size: var(--fontSizeBodyXS);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
.validationSuccess {
|
|
497
|
-
color: var(--success);
|
|
498
|
-
font-weight: var(--fontWeightMedium);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
.validationError {
|
|
502
|
-
color: var(--error);
|
|
503
|
-
font-weight: var(--fontWeightMedium);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
457
|
.previewLoading {
|
|
507
458
|
text-align: center;
|
|
508
459
|
color: var(--textLight);
|
|
@@ -510,31 +461,6 @@
|
|
|
510
461
|
padding: var(--spaceM);
|
|
511
462
|
}
|
|
512
463
|
|
|
513
|
-
.previewGrid {
|
|
514
|
-
display: grid;
|
|
515
|
-
gap: var(--spaceS);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
.previewItem {
|
|
519
|
-
display: flex;
|
|
520
|
-
justify-content: space-between;
|
|
521
|
-
align-items: center;
|
|
522
|
-
padding: var(--spaceXS) 0;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
.previewLabel {
|
|
526
|
-
font-weight: var(--fontWeightMedium);
|
|
527
|
-
color: var(--textBody);
|
|
528
|
-
font-size: var(--fontSizeBodyS);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
.previewValue {
|
|
532
|
-
color: var(--textTitle);
|
|
533
|
-
font-size: var(--fontSizeBodyS);
|
|
534
|
-
text-align: right;
|
|
535
|
-
font-weight: var(--fontWeightMedium);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
464
|
/* Confirmation Dialog */
|
|
539
465
|
.confirmationOverlay {
|
|
540
466
|
position: fixed;
|
|
@@ -582,45 +508,6 @@
|
|
|
582
508
|
margin: 0 0 var(--spaceL) 0;
|
|
583
509
|
}
|
|
584
510
|
|
|
585
|
-
.confirmationItem {
|
|
586
|
-
display: flex;
|
|
587
|
-
justify-content: space-between;
|
|
588
|
-
align-items: center;
|
|
589
|
-
padding: var(--spaceXS) 0;
|
|
590
|
-
font-size: var(--fontSizeBodyS);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
.confirmationItem:not(:last-child) {
|
|
594
|
-
border-bottom: 1px solid color-mix(in lab, var(--text) 5%, transparent);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
.confirmationItemValid {
|
|
598
|
-
background: color-mix(in lab, var(--success) 15%, transparent);
|
|
599
|
-
border-radius: var(--spaceXS);
|
|
600
|
-
padding: var(--spaceS) var(--spaceM);
|
|
601
|
-
margin: var(--spaceXS) calc(-1 * var(--spaceM));
|
|
602
|
-
border: 2px solid color-mix(in lab, var(--success) 35%, transparent);
|
|
603
|
-
box-shadow: 0 2px 4px color-mix(in lab, var(--success) 10%, transparent);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
.confirmationItemInvalid {
|
|
607
|
-
background: color-mix(in lab, var(--error) 8%, transparent);
|
|
608
|
-
border-radius: var(--spaceXS);
|
|
609
|
-
padding: var(--spaceS) var(--spaceM);
|
|
610
|
-
margin: var(--spaceXS) calc(-1 * var(--spaceM));
|
|
611
|
-
border: 1px solid color-mix(in lab, var(--error) 20%, transparent);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
.confirmationSuccess {
|
|
615
|
-
color: var(--success);
|
|
616
|
-
font-weight: var(--fontWeightMedium);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
.confirmationError {
|
|
620
|
-
color: var(--error);
|
|
621
|
-
font-weight: var(--fontWeightMedium);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
511
|
.confirmationButtons {
|
|
625
512
|
display: flex;
|
|
626
513
|
gap: var(--spaceM);
|
|
@@ -642,14 +529,3 @@
|
|
|
642
529
|
.confirmButton:hover {
|
|
643
530
|
box-shadow: 0 2px 6px color-mix(in lab, var(--primary) 30%, transparent);
|
|
644
531
|
}
|
|
645
|
-
|
|
646
|
-
.hashWarning {
|
|
647
|
-
background: color-mix(in lab, var(--error) 10%, transparent);
|
|
648
|
-
border: 1px solid color-mix(in lab, var(--error) 30%, transparent);
|
|
649
|
-
color: var(--error);
|
|
650
|
-
padding: var(--spaceM);
|
|
651
|
-
border-radius: var(--spaceXS);
|
|
652
|
-
margin-top: var(--spaceM);
|
|
653
|
-
font-size: var(--fontSizeBodyS);
|
|
654
|
-
font-weight: var(--fontWeightMedium);
|
|
655
|
-
}
|
|
@@ -4,8 +4,7 @@ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
|
4
4
|
import {
|
|
5
5
|
ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE,
|
|
6
6
|
IMPORT_FILE_TYPE_NOT_ALLOWED,
|
|
7
|
-
IMPORT_FILE_TYPE_NOT_SUPPORTED
|
|
8
|
-
DATA_INTEGRITY_BLOCKED_TAMPERING
|
|
7
|
+
IMPORT_FILE_TYPE_NOT_SUPPORTED
|
|
9
8
|
} from '~/utils/ui';
|
|
10
9
|
import {
|
|
11
10
|
listReadOnlyCases,
|
|
@@ -177,14 +176,14 @@ export const CaseImport = ({
|
|
|
177
176
|
clearMessages();
|
|
178
177
|
|
|
179
178
|
if (!isValidImportFile(file)) {
|
|
180
|
-
setError(
|
|
179
|
+
setError(IMPORT_FILE_TYPE_NOT_ALLOWED);
|
|
181
180
|
clearImportData();
|
|
182
181
|
return;
|
|
183
182
|
}
|
|
184
183
|
|
|
185
184
|
const importType = await resolveImportType(file);
|
|
186
185
|
if (!importType) {
|
|
187
|
-
setError(
|
|
186
|
+
setError(IMPORT_FILE_TYPE_NOT_SUPPORTED);
|
|
188
187
|
clearImportData();
|
|
189
188
|
return;
|
|
190
189
|
}
|
|
@@ -415,13 +414,6 @@ export const CaseImport = ({
|
|
|
415
414
|
{/* Import progress */}
|
|
416
415
|
<ProgressSection importProgress={importProgress} />
|
|
417
416
|
|
|
418
|
-
{/* Hash validation warning */}
|
|
419
|
-
{casePreview?.hashValid === false && (
|
|
420
|
-
<div className={styles.hashWarning}>
|
|
421
|
-
<strong>⚠️ Import Blocked:</strong> {DATA_INTEGRITY_BLOCKED_TAMPERING}
|
|
422
|
-
</div>
|
|
423
|
-
)}
|
|
424
|
-
|
|
425
417
|
{isArchivedRegularCaseImportBlocked && (
|
|
426
418
|
<div className={styles.error}>{ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE}</div>
|
|
427
419
|
)}
|
|
@@ -452,7 +444,7 @@ export const CaseImport = ({
|
|
|
452
444
|
importState.isClearing ||
|
|
453
445
|
importState.isLoadingPreview ||
|
|
454
446
|
(importState.importType === 'case' && isArchivedRegularCaseImportBlocked) ||
|
|
455
|
-
(importState.importType === 'case' && (!casePreview || casePreview.hashValid
|
|
447
|
+
(importState.importType === 'case' && (!casePreview || casePreview.hashValid === false))
|
|
456
448
|
}
|
|
457
449
|
>
|
|
458
450
|
{importState.isImporting ? 'Importing...' :
|
|
@@ -472,15 +464,16 @@ export const CaseImport = ({
|
|
|
472
464
|
<div className={styles.instructions}>
|
|
473
465
|
<h3 className={styles.instructionsTitle}>Case Review Instructions:</h3>
|
|
474
466
|
<ul className={styles.instructionsList}>
|
|
475
|
-
<li>Only ZIP
|
|
467
|
+
<li>Only case ZIP packages exported from Striae are accepted</li>
|
|
476
468
|
<li>Only one case can be reviewed at a time</li>
|
|
477
469
|
<li>Imported cases are read-only and cannot be modified</li>
|
|
470
|
+
<li>Integrity and signature validation are enforced during import</li>
|
|
478
471
|
<li>Importing will automatically replace any existing review case</li>
|
|
479
472
|
</ul>
|
|
480
473
|
<br />
|
|
481
474
|
<h3 className={styles.instructionsTitle}>Confirmation Import Instructions:</h3>
|
|
482
475
|
<ul className={styles.instructionsList}>
|
|
483
|
-
<li>Confirmation imports accept
|
|
476
|
+
<li>Confirmation imports accept only encrypted confirmation ZIP packages exported from Striae</li>
|
|
484
477
|
<li>Only one confirmation file can be imported at a time</li>
|
|
485
478
|
<li>Confirmed images will become read-only and cannot be modified</li>
|
|
486
479
|
<li>If an image has a pre-existing confirmation, it will be skipped</li>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type CaseImportPreview } from '~/types';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE,
|
|
4
|
+
ARCHIVED_SELF_IMPORT_NOTE
|
|
5
|
+
} from '~/utils/ui';
|
|
3
6
|
import styles from '../case-import.module.css';
|
|
4
7
|
|
|
5
8
|
interface CasePreviewSectionProps {
|
|
@@ -26,67 +29,21 @@ export const CasePreviewSection = ({
|
|
|
26
29
|
if (!casePreview) return null;
|
|
27
30
|
|
|
28
31
|
return (
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)}
|
|
38
|
-
{isArchivedRegularCaseImportBlocked && (
|
|
39
|
-
<div className={styles.archivedRegularCaseRiskNote}>
|
|
40
|
-
{ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE}
|
|
41
|
-
</div>
|
|
42
|
-
)}
|
|
43
|
-
<div className={styles.previewGrid}>
|
|
44
|
-
<div className={styles.previewItem}>
|
|
45
|
-
<span className={styles.previewLabel}>Case Number:</span>
|
|
46
|
-
<span className={styles.previewValue}>{casePreview.caseNumber}</span>
|
|
47
|
-
</div>
|
|
48
|
-
<div className={styles.previewItem}>
|
|
49
|
-
<span className={styles.previewLabel}>Exported by:</span>
|
|
50
|
-
<span className={styles.previewValue}>
|
|
51
|
-
{casePreview.exportedByName || casePreview.exportedBy || 'N/A'}
|
|
52
|
-
</span>
|
|
53
|
-
</div>
|
|
54
|
-
<div className={styles.previewItem}>
|
|
55
|
-
<span className={styles.previewLabel}>Lab/Company:</span>
|
|
56
|
-
<span className={styles.previewValue}>{casePreview.exportedByCompany || 'N/A'}</span>
|
|
57
|
-
</div>
|
|
58
|
-
<div className={styles.previewItem}>
|
|
59
|
-
<span className={styles.previewLabel}>Export Date:</span>
|
|
60
|
-
<span className={styles.previewValue}>
|
|
61
|
-
{new Date(casePreview.exportDate).toLocaleDateString()}
|
|
62
|
-
</span>
|
|
63
|
-
</div>
|
|
64
|
-
<div className={styles.previewItem}>
|
|
65
|
-
<span className={styles.previewLabel}>Total Images:</span>
|
|
66
|
-
<span className={styles.previewValue}>{casePreview.totalFiles}</span>
|
|
67
|
-
</div>
|
|
68
|
-
<div className={styles.previewItem}>
|
|
69
|
-
<span className={styles.previewLabel}>Archived Export:</span>
|
|
70
|
-
<span className={styles.previewValue}>{casePreview.archived ? 'Yes' : 'No'}</span>
|
|
71
|
-
</div>
|
|
32
|
+
<div className={styles.previewSection}>
|
|
33
|
+
<h3 className={styles.previewTitle}>Case Import Preview</h3>
|
|
34
|
+
<p className={styles.previewMessage}>
|
|
35
|
+
Case package detected. Details are hidden until import verification completes.
|
|
36
|
+
</p>
|
|
37
|
+
{casePreview.archived && (
|
|
38
|
+
<div className={styles.archivedImportNote}>
|
|
39
|
+
{ARCHIVED_SELF_IMPORT_NOTE}
|
|
72
40
|
</div>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<div className={`${styles.validationSection} ${casePreview.hashValid ? styles.validationSectionValid : styles.validationSectionInvalid}`}>
|
|
78
|
-
<h3 className={styles.validationTitle}>Data Integrity Validation</h3>
|
|
79
|
-
<div className={styles.validationItem}>
|
|
80
|
-
<span className={`${styles.validationValue} ${casePreview.hashValid ? styles.validationSuccess : styles.validationError}`}>
|
|
81
|
-
{casePreview.hashValid ? (
|
|
82
|
-
<>{DATA_INTEGRITY_VALIDATION_PASSED}</>
|
|
83
|
-
) : (
|
|
84
|
-
<>{DATA_INTEGRITY_VALIDATION_FAILED}</>
|
|
85
|
-
)}
|
|
86
|
-
</span>
|
|
87
|
-
</div>
|
|
41
|
+
)}
|
|
42
|
+
{isArchivedRegularCaseImportBlocked && (
|
|
43
|
+
<div className={styles.archivedRegularCaseRiskNote}>
|
|
44
|
+
{ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE}
|
|
88
45
|
</div>
|
|
89
46
|
)}
|
|
90
|
-
|
|
47
|
+
</div>
|
|
91
48
|
);
|
|
92
49
|
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type CaseImportPreview } from '~/types';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE,
|
|
4
|
+
ARCHIVED_SELF_IMPORT_NOTE
|
|
5
|
+
} from '~/utils/ui';
|
|
3
6
|
import styles from '../case-import.module.css';
|
|
4
7
|
|
|
5
8
|
interface ConfirmationDialogProps {
|
|
@@ -21,6 +24,8 @@ export const ConfirmationDialog = ({
|
|
|
21
24
|
}: ConfirmationDialogProps) => {
|
|
22
25
|
if (!showConfirmation || !casePreview) return null;
|
|
23
26
|
|
|
27
|
+
const hasDetails = casePreview.archived || isArchivedRegularCaseImportBlocked;
|
|
28
|
+
|
|
24
29
|
return (
|
|
25
30
|
<div className={styles.confirmationOverlay}>
|
|
26
31
|
<div className={styles.confirmationModal}>
|
|
@@ -29,45 +34,24 @@ export const ConfirmationDialog = ({
|
|
|
29
34
|
<p className={styles.confirmationText}>
|
|
30
35
|
Are you sure you want to import this case for review?
|
|
31
36
|
</p>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<div className={styles.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
</div>
|
|
49
|
-
<div className={styles.confirmationItem}>
|
|
50
|
-
<strong>Archived Export:</strong> {casePreview.archived ? 'Yes' : 'No'}
|
|
37
|
+
<p className={styles.confirmationText}>
|
|
38
|
+
Package details stay hidden until verification completes.
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
{hasDetails && (
|
|
42
|
+
<div className={styles.confirmationDetails}>
|
|
43
|
+
{casePreview.archived && (
|
|
44
|
+
<div className={styles.archivedImportNote}>
|
|
45
|
+
{ARCHIVED_SELF_IMPORT_NOTE}
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
{isArchivedRegularCaseImportBlocked && (
|
|
49
|
+
<div className={styles.archivedRegularCaseRiskNote}>
|
|
50
|
+
{archivedRegularCaseBlockMessage}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
51
53
|
</div>
|
|
52
|
-
|
|
53
|
-
<div className={styles.archivedImportNote}>
|
|
54
|
-
Archived export detected. Original exporter imports are allowed for archived cases.
|
|
55
|
-
</div>
|
|
56
|
-
)}
|
|
57
|
-
{isArchivedRegularCaseImportBlocked && (
|
|
58
|
-
<div className={styles.archivedRegularCaseRiskNote}>
|
|
59
|
-
{archivedRegularCaseBlockMessage}
|
|
60
|
-
</div>
|
|
61
|
-
)}
|
|
62
|
-
{casePreview.hashValid !== undefined && (
|
|
63
|
-
<div className={`${styles.confirmationItem} ${casePreview.hashValid ? styles.confirmationItemValid : styles.confirmationItemInvalid}`}>
|
|
64
|
-
<strong>Data Integrity:</strong>
|
|
65
|
-
<span className={casePreview.hashValid ? styles.confirmationSuccess : styles.confirmationError}>
|
|
66
|
-
{casePreview.hashValid ? DATA_INTEGRITY_VALIDATION_PASSED : DATA_INTEGRITY_VALIDATION_FAILED}
|
|
67
|
-
</span>
|
|
68
|
-
</div>
|
|
69
|
-
)}
|
|
70
|
-
</div>
|
|
54
|
+
)}
|
|
71
55
|
|
|
72
56
|
<div className={styles.confirmationButtons}>
|
|
73
57
|
<button
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import styles from '../case-import.module.css';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
export interface ConfirmationPreview {
|
|
5
|
-
caseNumber: string;
|
|
6
|
-
fullName: string;
|
|
7
|
-
exportDate: string;
|
|
8
|
-
totalConfirmations: number;
|
|
9
|
-
confirmationIds: string[];
|
|
10
|
-
}
|
|
3
|
+
export type ConfirmationPreview = Record<string, never>;
|
|
11
4
|
|
|
12
5
|
interface ConfirmationPreviewSectionProps {
|
|
13
6
|
confirmationPreview: ConfirmationPreview | null;
|
|
@@ -29,43 +22,10 @@ export const ConfirmationPreviewSection = ({ confirmationPreview, isLoadingPrevi
|
|
|
29
22
|
|
|
30
23
|
return (
|
|
31
24
|
<div className={styles.previewSection}>
|
|
32
|
-
<h3 className={styles.previewTitle}>Confirmation
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<span className={styles.previewValue}>{confirmationPreview.caseNumber}</span>
|
|
37
|
-
</div>
|
|
38
|
-
<div className={styles.previewItem}>
|
|
39
|
-
<span className={styles.previewLabel}>Exported by:</span>
|
|
40
|
-
<span className={styles.previewValue}>{confirmationPreview.fullName}</span>
|
|
41
|
-
</div>
|
|
42
|
-
<div className={styles.previewItem}>
|
|
43
|
-
<span className={styles.previewLabel}>Export Date:</span>
|
|
44
|
-
<span className={styles.previewValue}>
|
|
45
|
-
{new Date(confirmationPreview.exportDate).toLocaleDateString(undefined, {
|
|
46
|
-
year: 'numeric',
|
|
47
|
-
month: 'long',
|
|
48
|
-
day: 'numeric',
|
|
49
|
-
hour: '2-digit',
|
|
50
|
-
minute: '2-digit',
|
|
51
|
-
timeZoneName: 'short'
|
|
52
|
-
})}
|
|
53
|
-
</span>
|
|
54
|
-
</div>
|
|
55
|
-
<div className={styles.previewItem}>
|
|
56
|
-
<span className={styles.previewLabel}>Total Confirmations:</span>
|
|
57
|
-
<span className={styles.previewValue}>{confirmationPreview.totalConfirmations}</span>
|
|
58
|
-
</div>
|
|
59
|
-
<div className={styles.previewItem}>
|
|
60
|
-
<span className={styles.previewLabel}>Confirmation IDs:</span>
|
|
61
|
-
<span className={styles.previewValue}>
|
|
62
|
-
{confirmationPreview.confirmationIds.length > 0
|
|
63
|
-
? confirmationPreview.confirmationIds.join(', ')
|
|
64
|
-
: 'None'
|
|
65
|
-
}
|
|
66
|
-
</span>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
25
|
+
<h3 className={styles.previewTitle}>Confirmation Import Preview</h3>
|
|
26
|
+
<p className={styles.previewMessage}>
|
|
27
|
+
Confirmation package detected. Details are hidden until import verification completes.
|
|
28
|
+
</p>
|
|
69
29
|
</div>
|
|
70
30
|
);
|
|
71
31
|
};
|
|
@@ -61,8 +61,7 @@ export const FileSelector = ({
|
|
|
61
61
|
const file = files[0];
|
|
62
62
|
|
|
63
63
|
// Check file type (same as input accept attribute)
|
|
64
|
-
const isValidType = file.name.toLowerCase().endsWith('.zip')
|
|
65
|
-
file.name.toLowerCase().endsWith('.json');
|
|
64
|
+
const isValidType = file.name.toLowerCase().endsWith('.zip');
|
|
66
65
|
|
|
67
66
|
if (isValidType) {
|
|
68
67
|
if (onFileSelectDirect) {
|
|
@@ -92,11 +91,11 @@ export const FileSelector = ({
|
|
|
92
91
|
ref={fileInputRef}
|
|
93
92
|
type="file"
|
|
94
93
|
id="zipFile"
|
|
95
|
-
accept=".zip
|
|
94
|
+
accept=".zip"
|
|
96
95
|
onChange={onFileSelect}
|
|
97
96
|
disabled={isDisabled}
|
|
98
97
|
className={styles.fileInput}
|
|
99
|
-
aria-label="File picker for ZIP
|
|
98
|
+
aria-label="File picker for ZIP packages"
|
|
100
99
|
/>
|
|
101
100
|
<div
|
|
102
101
|
className={`${styles.fileLabel} ${isDragOver ? styles.fileLabelDragOver : ''}`}
|
|
@@ -111,7 +110,7 @@ export const FileSelector = ({
|
|
|
111
110
|
role="button"
|
|
112
111
|
tabIndex={isDisabled ? -1 : 0}
|
|
113
112
|
aria-disabled={isDisabled}
|
|
114
|
-
aria-label="File selection area. Drag and drop a ZIP
|
|
113
|
+
aria-label="File selection area. Drag and drop a case ZIP or encrypted confirmation ZIP package for import."
|
|
115
114
|
onKeyDown={(e) => {
|
|
116
115
|
if ((e.key === 'Enter' || e.key === ' ') && !isDisabled) {
|
|
117
116
|
if (e.key === ' ') {
|
|
@@ -128,7 +127,7 @@ export const FileSelector = ({
|
|
|
128
127
|
? selectedFile.name
|
|
129
128
|
: isDragOver
|
|
130
129
|
? 'Drop file here...'
|
|
131
|
-
: 'Select ZIP
|
|
130
|
+
: 'Select ZIP package... or drag & drop'
|
|
132
131
|
}
|
|
133
132
|
</span>
|
|
134
133
|
</div>
|
|
@@ -4,11 +4,6 @@ import { previewCaseImport, extractConfirmationImportPackage } from '~/component
|
|
|
4
4
|
import { type CaseImportPreview } from '~/types';
|
|
5
5
|
import { type ConfirmationPreview } from '../components/ConfirmationPreviewSection';
|
|
6
6
|
|
|
7
|
-
type UnknownRecord = Record<string, unknown>;
|
|
8
|
-
|
|
9
|
-
const isRecord = (value: unknown): value is UnknownRecord =>
|
|
10
|
-
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
-
|
|
12
7
|
interface UseFilePreviewReturn {
|
|
13
8
|
casePreview: CaseImportPreview | null;
|
|
14
9
|
confirmationPreview: ConfirmationPreview | null;
|
|
@@ -56,50 +51,9 @@ export const useFilePreview = (
|
|
|
56
51
|
|
|
57
52
|
setIsLoadingPreview(true);
|
|
58
53
|
try {
|
|
59
|
-
|
|
60
|
-
const parsed = confirmationData as unknown;
|
|
61
|
-
|
|
62
|
-
if (!isRecord(parsed)) {
|
|
63
|
-
throw new Error('Invalid confirmation data format');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const metadata = isRecord(parsed.metadata) ? parsed.metadata : undefined;
|
|
67
|
-
const confirmations = isRecord(parsed.confirmations) ? parsed.confirmations : undefined;
|
|
68
|
-
|
|
69
|
-
// Extract confirmation IDs from the confirmations object
|
|
70
|
-
const confirmationIds: string[] = [];
|
|
71
|
-
if (confirmations) {
|
|
72
|
-
Object.values(confirmations).forEach((imageConfirmations) => {
|
|
73
|
-
if (Array.isArray(imageConfirmations)) {
|
|
74
|
-
imageConfirmations.forEach((confirmation) => {
|
|
75
|
-
if (isRecord(confirmation) && typeof confirmation.confirmationId === 'string') {
|
|
76
|
-
confirmationIds.push(confirmation.confirmationId);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const caseNumber =
|
|
84
|
-
metadata && typeof metadata.caseNumber === 'string' ? metadata.caseNumber : 'Unknown';
|
|
85
|
-
const fullName =
|
|
86
|
-
metadata && typeof metadata.exportedByName === 'string' ? metadata.exportedByName : 'Unknown';
|
|
87
|
-
const exportDate =
|
|
88
|
-
metadata && typeof metadata.exportDate === 'string'
|
|
89
|
-
? metadata.exportDate
|
|
90
|
-
: new Date().toISOString();
|
|
91
|
-
const totalConfirmations =
|
|
92
|
-
metadata && typeof metadata.totalConfirmations === 'number'
|
|
93
|
-
? metadata.totalConfirmations
|
|
94
|
-
: confirmationIds.length;
|
|
54
|
+
await extractConfirmationImportPackage(file);
|
|
95
55
|
|
|
96
|
-
const preview: ConfirmationPreview = {
|
|
97
|
-
caseNumber,
|
|
98
|
-
fullName,
|
|
99
|
-
exportDate,
|
|
100
|
-
totalConfirmations,
|
|
101
|
-
confirmationIds
|
|
102
|
-
};
|
|
56
|
+
const preview: ConfirmationPreview = {};
|
|
103
57
|
|
|
104
58
|
setConfirmationPreview(preview);
|
|
105
59
|
} catch (error) {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { isConfirmationDataFile } from '~/components/actions/case-review';
|
|
2
|
-
|
|
3
1
|
const CASE_EXPORT_DATA_FILE_REGEX = /_data\.(json|csv)$/i;
|
|
4
2
|
const CONFIRMATION_EXPORT_FILE_REGEX = /^confirmation-data-.*\.json$/i;
|
|
5
3
|
const FORENSIC_MANIFEST_FILE_NAME = 'forensic_manifest.json';
|
|
4
|
+
const ENCRYPTION_MANIFEST_FILE_NAME = 'encryption_manifest.json';
|
|
6
5
|
|
|
7
6
|
function getLeafFileName(path: string): string {
|
|
8
7
|
const segments = path.split('/').filter(Boolean);
|
|
@@ -19,20 +18,10 @@ export const isValidZipFile = (file: File): boolean => {
|
|
|
19
18
|
};
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
|
-
* Check if a file is
|
|
23
|
-
*/
|
|
24
|
-
export const isValidConfirmationFile = (file: File): boolean => {
|
|
25
|
-
const lowerName = file.name.toLowerCase();
|
|
26
|
-
const jsonType = file.type === 'application/json' || file.type === '';
|
|
27
|
-
|
|
28
|
-
return lowerName.endsWith('.json') && jsonType && isConfirmationDataFile(file.name);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Check if a file is valid for import (either ZIP or confirmation JSON)
|
|
21
|
+
* Check if a file is valid for import (ZIP packages only)
|
|
33
22
|
*/
|
|
34
23
|
export const isValidImportFile = (file: File): boolean => {
|
|
35
|
-
return isValidZipFile(file)
|
|
24
|
+
return isValidZipFile(file);
|
|
36
25
|
};
|
|
37
26
|
|
|
38
27
|
/**
|
|
@@ -40,20 +29,15 @@ export const isValidImportFile = (file: File): boolean => {
|
|
|
40
29
|
*/
|
|
41
30
|
export const getImportType = (file: File): 'case' | 'confirmation' | null => {
|
|
42
31
|
if (isValidZipFile(file)) return 'case';
|
|
43
|
-
if (isValidConfirmationFile(file)) return 'confirmation';
|
|
44
32
|
return null;
|
|
45
33
|
};
|
|
46
34
|
|
|
47
35
|
/**
|
|
48
36
|
* Resolve import type, including ZIP package inspection.
|
|
49
37
|
* Case ZIPs are identified by case data files or FORENSIC_MANIFEST.json.
|
|
50
|
-
* Confirmation ZIPs are identified by confirmation-data-*.json.
|
|
38
|
+
* Confirmation ZIPs are identified by confirmation-data-*.json plus ENCRYPTION_MANIFEST.json.
|
|
51
39
|
*/
|
|
52
40
|
export const resolveImportType = async (file: File): Promise<'case' | 'confirmation' | null> => {
|
|
53
|
-
if (isValidConfirmationFile(file)) {
|
|
54
|
-
return 'confirmation';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
41
|
if (!isValidZipFile(file)) {
|
|
58
42
|
return null;
|
|
59
43
|
}
|
|
@@ -78,7 +62,11 @@ export const resolveImportType = async (file: File): Promise<'case' | 'confirmat
|
|
|
78
62
|
CONFIRMATION_EXPORT_FILE_REGEX.test(getLeafFileName(path))
|
|
79
63
|
);
|
|
80
64
|
|
|
81
|
-
|
|
65
|
+
const hasEncryptionManifest = fileEntries.some(
|
|
66
|
+
(path) => getLeafFileName(path).toLowerCase() === ENCRYPTION_MANIFEST_FILE_NAME
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (hasConfirmationData && hasEncryptionManifest) {
|
|
82
70
|
return 'confirmation';
|
|
83
71
|
}
|
|
84
72
|
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
"manifest_signing_public_keys": {
|
|
7
7
|
"MANIFEST_SIGNING_KEY_ID": "MANIFEST_SIGNING_PUBLIC_KEY"
|
|
8
8
|
},
|
|
9
|
+
"export_encryption_key_id": "EXPORT_ENCRYPTION_KEY_ID",
|
|
10
|
+
"export_encryption_public_key": "EXPORT_ENCRYPTION_PUBLIC_KEY",
|
|
11
|
+
"export_encryption_public_keys": {
|
|
12
|
+
"EXPORT_ENCRYPTION_KEY_ID": "EXPORT_ENCRYPTION_PUBLIC_KEY"
|
|
13
|
+
},
|
|
9
14
|
"max_cases_review": 0,
|
|
10
15
|
"max_files_per_case_review": 0
|
|
11
16
|
}
|
|
@@ -28,7 +28,7 @@ import { generateUniqueId } from '~/utils/common';
|
|
|
28
28
|
import { evaluatePasswordPolicy, buildActionCodeSettings, userHasMFA } from '~/utils/auth';
|
|
29
29
|
import type { UserData } from '~/types';
|
|
30
30
|
|
|
31
|
-
const APP_CANONICAL_ORIGIN = '
|
|
31
|
+
const APP_CANONICAL_ORIGIN = 'https://striae.app';
|
|
32
32
|
const SOCIAL_IMAGE_PATH = '/social-image.png';
|
|
33
33
|
const SOCIAL_IMAGE_ALT = 'Striae forensic annotation and comparison workspace';
|
|
34
34
|
const LOGIN_PATH_ALIASES = new Set(['/auth', '/auth/', '/auth/login', '/auth/login/']);
|