@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
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
signForensicManifest,
|
|
15
15
|
removeCaseConfirmationSummary
|
|
16
16
|
} from '~/utils/data';
|
|
17
|
-
import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData } from '~/types';
|
|
17
|
+
import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData, type ValidationAuditEntry } from '~/types';
|
|
18
18
|
import { auditService } from '~/services/audit';
|
|
19
19
|
import { fetchImageApi } from '~/utils/api';
|
|
20
20
|
import { exportCaseData, formatDateForFilename } from '~/components/actions/case-export';
|
|
@@ -22,12 +22,14 @@ import { getImageUrl } from './image-manage';
|
|
|
22
22
|
import {
|
|
23
23
|
calculateSHA256Secure,
|
|
24
24
|
createPublicSigningKeyFileName,
|
|
25
|
+
encryptExportDataWithAllImages,
|
|
25
26
|
generateForensicManifestSecure,
|
|
27
|
+
getCurrentEncryptionPublicKeyDetails,
|
|
26
28
|
getCurrentPublicSigningKeyDetails,
|
|
27
29
|
getVerificationPublicKey,
|
|
28
30
|
} from '~/utils/forensics';
|
|
29
31
|
import { signAuditExport } from '~/services/audit/audit-export-signing';
|
|
30
|
-
import { generateAuditSummary } from '~/services/audit/audit-query-helpers';
|
|
32
|
+
import { generateAuditSummary, sortAuditEntriesNewestFirst } from '~/services/audit/audit-query-helpers';
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* Delete a file without individual audit logging (for bulk operations)
|
|
@@ -393,15 +395,23 @@ export const renameCase = async (
|
|
|
393
395
|
// 5) Delete old case number in user's KV entry
|
|
394
396
|
await removeUserCase(user, oldCaseNumber);
|
|
395
397
|
|
|
396
|
-
// Log successful case rename
|
|
398
|
+
// Log successful case rename under the original case number context
|
|
397
399
|
const endTime = Date.now();
|
|
398
400
|
await auditService.logCaseRename(
|
|
399
401
|
user,
|
|
400
|
-
|
|
402
|
+
oldCaseNumber,
|
|
401
403
|
oldCaseNumber,
|
|
402
404
|
newCaseNumber
|
|
403
405
|
);
|
|
404
406
|
|
|
407
|
+
// Log creation of the new case number as a rename-derived case
|
|
408
|
+
await auditService.logCaseCreation(
|
|
409
|
+
user,
|
|
410
|
+
newCaseNumber,
|
|
411
|
+
newCaseNumber,
|
|
412
|
+
oldCaseNumber
|
|
413
|
+
);
|
|
414
|
+
|
|
405
415
|
console.log(`✅ Case renamed: ${oldCaseNumber} → ${newCaseNumber} (${endTime - startTime}ms)`);
|
|
406
416
|
|
|
407
417
|
} catch (error) {
|
|
@@ -808,11 +818,43 @@ export const archiveCase = async (
|
|
|
808
818
|
startDate: caseData.createdAt,
|
|
809
819
|
endDate: archivedAt,
|
|
810
820
|
});
|
|
821
|
+
|
|
822
|
+
// Ensure the bundled archive trail includes the archival event itself.
|
|
823
|
+
const archiveAuditEntry: ValidationAuditEntry = {
|
|
824
|
+
timestamp: archivedAt,
|
|
825
|
+
userId: user.uid,
|
|
826
|
+
userEmail: user.email || '',
|
|
827
|
+
action: 'case-archive',
|
|
828
|
+
result: 'success',
|
|
829
|
+
details: {
|
|
830
|
+
fileName: `${caseNumber}.case`,
|
|
831
|
+
fileType: 'case-package',
|
|
832
|
+
validationErrors: [],
|
|
833
|
+
caseNumber,
|
|
834
|
+
workflowPhase: 'casework',
|
|
835
|
+
caseDetails: {
|
|
836
|
+
newCaseName: caseNumber,
|
|
837
|
+
archiveReason: archiveReason?.trim() || 'No reason provided',
|
|
838
|
+
totalFiles: archiveData.files?.length || 0,
|
|
839
|
+
lastModified: archivedAt,
|
|
840
|
+
},
|
|
841
|
+
performanceMetrics: {
|
|
842
|
+
processingTimeMs: Date.now() - startTime,
|
|
843
|
+
fileSizeBytes: 0,
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const auditEntriesWithArchive = sortAuditEntriesNewestFirst([
|
|
849
|
+
...auditEntries,
|
|
850
|
+
archiveAuditEntry,
|
|
851
|
+
]);
|
|
852
|
+
|
|
811
853
|
const auditTrail: AuditTrail = {
|
|
812
854
|
caseNumber,
|
|
813
855
|
workflowId: `${caseNumber}-archive-${Date.now()}`,
|
|
814
|
-
entries:
|
|
815
|
-
summary: generateAuditSummary(
|
|
856
|
+
entries: auditEntriesWithArchive,
|
|
857
|
+
summary: generateAuditSummary(auditEntriesWithArchive),
|
|
816
858
|
};
|
|
817
859
|
|
|
818
860
|
const auditTrailPayload = {
|
|
@@ -857,8 +899,64 @@ export const archiveCase = async (
|
|
|
857
899
|
auditTrail,
|
|
858
900
|
};
|
|
859
901
|
|
|
860
|
-
|
|
861
|
-
|
|
902
|
+
const auditTrailJson = JSON.stringify(signedAuditTrail, null, 2);
|
|
903
|
+
const auditSignatureJson = JSON.stringify(signedAuditExportPayload, null, 2);
|
|
904
|
+
zip.file('audit/case-audit-trail.json', auditTrailJson);
|
|
905
|
+
zip.file('audit/case-audit-signature.json', auditSignatureJson);
|
|
906
|
+
|
|
907
|
+
const encryptionKeyDetails = getCurrentEncryptionPublicKeyDetails();
|
|
908
|
+
|
|
909
|
+
if (!encryptionKeyDetails.publicKeyPem || !encryptionKeyDetails.keyId) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
'Archive encryption is mandatory. Your Striae instance does not have a configured encryption public key. ' +
|
|
912
|
+
'Please contact your administrator to set up export encryption.'
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
const filesToEncrypt: Array<{ filename: string; blob: Blob }> = [
|
|
918
|
+
...Object.entries(imageBlobs).map(([filename, blob]) => ({
|
|
919
|
+
filename,
|
|
920
|
+
blob
|
|
921
|
+
})),
|
|
922
|
+
{
|
|
923
|
+
filename: 'audit/case-audit-trail.json',
|
|
924
|
+
blob: new Blob([auditTrailJson], { type: 'application/json' })
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
filename: 'audit/case-audit-signature.json',
|
|
928
|
+
blob: new Blob([auditSignatureJson], { type: 'application/json' })
|
|
929
|
+
}
|
|
930
|
+
];
|
|
931
|
+
|
|
932
|
+
const encryptionResult = await encryptExportDataWithAllImages(
|
|
933
|
+
caseJsonContent,
|
|
934
|
+
filesToEncrypt,
|
|
935
|
+
encryptionKeyDetails.publicKeyPem,
|
|
936
|
+
encryptionKeyDetails.keyId
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
zip.file(`${caseNumber}_data.json`, encryptionResult.ciphertext);
|
|
940
|
+
|
|
941
|
+
for (let index = 0; index < filesToEncrypt.length; index += 1) {
|
|
942
|
+
const originalFilename = filesToEncrypt[index].filename;
|
|
943
|
+
const encryptedContent = encryptionResult.encryptedImages[index];
|
|
944
|
+
|
|
945
|
+
if (originalFilename.startsWith('audit/')) {
|
|
946
|
+
zip.file(originalFilename, encryptedContent);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (imageFolder) {
|
|
951
|
+
imageFolder.file(originalFilename, encryptedContent);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
zip.file('ENCRYPTION_MANIFEST.json', JSON.stringify(encryptionResult.encryptionManifest, null, 2));
|
|
956
|
+
} catch (error) {
|
|
957
|
+
console.error('Archive encryption failed:', error);
|
|
958
|
+
throw new Error(`Failed to encrypt archive package: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
959
|
+
}
|
|
862
960
|
|
|
863
961
|
zip.file(
|
|
864
962
|
'README.txt',
|
|
@@ -873,12 +971,14 @@ export const archiveCase = async (
|
|
|
873
971
|
'',
|
|
874
972
|
'Package Contents',
|
|
875
973
|
'- Case data JSON export with all image references',
|
|
876
|
-
'- images/ folder with exported image files',
|
|
974
|
+
'- images/ folder with exported image files (encrypted)',
|
|
877
975
|
'- Full case audit trail export and signed audit metadata',
|
|
878
976
|
'- Forensic manifest with server-side signature',
|
|
977
|
+
'- ENCRYPTION_MANIFEST.json with encryption metadata and encrypted image hashes',
|
|
879
978
|
`- ${publicKeyFileName} for verification`,
|
|
880
979
|
'',
|
|
881
980
|
'This package is intended for read-only review and verification workflows.',
|
|
981
|
+
'This package is encrypted. Only Striae can decrypt and re-import it.',
|
|
882
982
|
].join('\n')
|
|
883
983
|
);
|
|
884
984
|
|
|
@@ -910,7 +1010,7 @@ export const archiveCase = async (
|
|
|
910
1010
|
);
|
|
911
1011
|
|
|
912
1012
|
const downloadUrl = URL.createObjectURL(zipBlob);
|
|
913
|
-
const archiveFileName = `striae-case-${caseNumber}-archive-${formatDateForFilename(new Date())}.zip`;
|
|
1013
|
+
const archiveFileName = `striae-case-${caseNumber}-archive-${formatDateForFilename(new Date())}-encrypted.zip`;
|
|
914
1014
|
const anchor = document.createElement('a');
|
|
915
1015
|
anchor.href = downloadUrl;
|
|
916
1016
|
anchor.download = archiveFileName;
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
calculateSHA256Secure,
|
|
4
4
|
createPublicSigningKeyFileName,
|
|
5
5
|
getCurrentPublicSigningKeyDetails,
|
|
6
|
-
getVerificationPublicKey
|
|
6
|
+
getVerificationPublicKey,
|
|
7
|
+
getCurrentEncryptionPublicKeyDetails,
|
|
8
|
+
encryptExportDataWithAllImages
|
|
7
9
|
} from '~/utils/forensics';
|
|
8
10
|
import { getUserData, getCaseData, updateCaseData, signConfirmationData, upsertFileConfirmationSummary } from '~/utils/data';
|
|
9
11
|
import { type AnnotationData, type ConfirmationData, type CaseConfirmations, type CaseDataWithConfirmations, type ConfirmationImportData } from '~/types';
|
|
@@ -318,8 +320,35 @@ export async function exportConfirmationData(
|
|
|
318
320
|
const zip = new JSZip();
|
|
319
321
|
const normalizedPem = publicKeyPem.endsWith('\n') ? publicKeyPem : `${publicKeyPem}\n`;
|
|
320
322
|
|
|
321
|
-
|
|
323
|
+
const encKeyDetails = getCurrentEncryptionPublicKeyDetails();
|
|
324
|
+
if (!encKeyDetails.publicKeyPem || !encKeyDetails.keyId) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
'Confirmation export encryption is mandatory. Your Striae instance does not have a configured encryption public key. ' +
|
|
327
|
+
'Please contact your administrator to set up export encryption.'
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let encryptedConfirmationContent: string | Uint8Array;
|
|
332
|
+
let encryptionManifestJson: string;
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const encryptionResult = await encryptExportDataWithAllImages(
|
|
336
|
+
finalJsonString,
|
|
337
|
+
[],
|
|
338
|
+
encKeyDetails.publicKeyPem,
|
|
339
|
+
encKeyDetails.keyId
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
encryptedConfirmationContent = encryptionResult.ciphertext;
|
|
343
|
+
encryptionManifestJson = JSON.stringify(encryptionResult.encryptionManifest, null, 2);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error('Confirmation export encryption failed:', error);
|
|
346
|
+
throw new Error(`Failed to encrypt confirmation export: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
zip.file(confirmationFileName, encryptedConfirmationContent);
|
|
322
350
|
zip.file(publicKeyFileName, normalizedPem);
|
|
351
|
+
zip.file('ENCRYPTION_MANIFEST.json', encryptionManifestJson);
|
|
323
352
|
|
|
324
353
|
const zipBlob = await zip.generateAsync({
|
|
325
354
|
type: 'blob',
|
|
@@ -327,7 +356,7 @@ export async function exportConfirmationData(
|
|
|
327
356
|
compressionOptions: { level: 6 }
|
|
328
357
|
});
|
|
329
358
|
|
|
330
|
-
const exportFileName = `confirmation-export-${caseNumber}-${timestampString}.zip`;
|
|
359
|
+
const exportFileName = `confirmation-export-${caseNumber}-${timestampString}-encrypted.zip`;
|
|
331
360
|
|
|
332
361
|
// Create download
|
|
333
362
|
const url = URL.createObjectURL(zipBlob);
|
|
@@ -522,11 +522,60 @@
|
|
|
522
522
|
white-space: nowrap;
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
.entryHeaderActions {
|
|
526
|
+
display: flex;
|
|
527
|
+
align-items: center;
|
|
528
|
+
gap: 8px;
|
|
529
|
+
margin-left: auto;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.entryDetailsToggle {
|
|
533
|
+
background: color-mix(in lab, var(--primary) 10%, transparent);
|
|
534
|
+
color: color-mix(in lab, var(--primary) 65%, var(--text));
|
|
535
|
+
border: 1px solid color-mix(in lab, var(--primary) 30%, transparent);
|
|
536
|
+
padding: 4px 8px;
|
|
537
|
+
border-radius: 999px;
|
|
538
|
+
font-size: 0.75rem;
|
|
539
|
+
font-weight: var(--fontWeightMedium);
|
|
540
|
+
cursor: pointer;
|
|
541
|
+
transition: background-color var(--durationS) var(--bezierFastoutSlowin);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.entryDetailsToggle:hover {
|
|
545
|
+
background: color-mix(in lab, var(--primary) 16%, transparent);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.entryDetailsToggle:focus-visible {
|
|
549
|
+
outline: 2px solid color-mix(in lab, var(--primary) 45%, transparent);
|
|
550
|
+
outline-offset: 2px;
|
|
551
|
+
}
|
|
552
|
+
|
|
525
553
|
/* Entry Details */
|
|
526
554
|
.entryDetails {
|
|
527
555
|
padding: 12px 14px;
|
|
528
556
|
}
|
|
529
557
|
|
|
558
|
+
.expandedDetails {
|
|
559
|
+
margin-top: 10px;
|
|
560
|
+
padding-top: 10px;
|
|
561
|
+
border-top: 1px dashed color-mix(in lab, var(--textLight) 25%, transparent);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.expandedDetailsCode {
|
|
565
|
+
margin: 4px 0 0;
|
|
566
|
+
padding: 10px;
|
|
567
|
+
border-radius: 6px;
|
|
568
|
+
border: 1px solid color-mix(in lab, var(--textLight) 20%, transparent);
|
|
569
|
+
background: color-mix(in lab, var(--backgroundLight) 75%, transparent);
|
|
570
|
+
color: var(--text);
|
|
571
|
+
font-size: 0.78rem;
|
|
572
|
+
line-height: 1.4;
|
|
573
|
+
white-space: pre-wrap;
|
|
574
|
+
word-break: break-word;
|
|
575
|
+
max-height: 280px;
|
|
576
|
+
overflow: auto;
|
|
577
|
+
}
|
|
578
|
+
|
|
530
579
|
.detailRow {
|
|
531
580
|
display: flex;
|
|
532
581
|
align-items: center;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useMemo, useState, type MouseEvent } from 'react';
|
|
1
2
|
import { type ValidationAuditEntry } from '~/types';
|
|
2
3
|
import { formatAuditTimestamp, getAuditActionIcon, getAuditStatusIcon } from './audit-viewer-utils';
|
|
3
4
|
import styles from '../user-audit.module.css';
|
|
@@ -13,7 +14,57 @@ const isConfirmationImportEntry = (entry: ValidationAuditEntry): boolean => {
|
|
|
13
14
|
);
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
const isConfirmationEvent = (entry: ValidationAuditEntry): boolean => {
|
|
18
|
+
return (
|
|
19
|
+
entry.action === 'confirmation-create' ||
|
|
20
|
+
entry.action === 'confirmation-export' ||
|
|
21
|
+
entry.action === 'confirmation-import' ||
|
|
22
|
+
entry.action === 'confirm' ||
|
|
23
|
+
(entry.action === 'import' && entry.details.workflowPhase === 'confirmation') ||
|
|
24
|
+
(entry.action === 'export' && entry.details.workflowPhase === 'confirmation')
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const supportsFullDetailsToggle = (entry: ValidationAuditEntry): boolean => {
|
|
29
|
+
return (
|
|
30
|
+
entry.action === 'annotation-create' ||
|
|
31
|
+
entry.action === 'annotation-edit' ||
|
|
32
|
+
entry.action === 'annotation-delete' ||
|
|
33
|
+
isConfirmationEvent(entry)
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getEntryKey = (entry: ValidationAuditEntry): string => {
|
|
38
|
+
return `${entry.timestamp}-${entry.userId}-${entry.action}-${entry.details.fileName || ''}-${entry.details.confirmationId || ''}`;
|
|
39
|
+
};
|
|
40
|
+
|
|
16
41
|
export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
42
|
+
const [expandedEntryKeys, setExpandedEntryKeys] = useState<Set<string>>(new Set());
|
|
43
|
+
|
|
44
|
+
const expandableEntries = useMemo(() => {
|
|
45
|
+
return new Set(entries.filter(supportsFullDetailsToggle).map(getEntryKey));
|
|
46
|
+
}, [entries]);
|
|
47
|
+
|
|
48
|
+
const toggleExpanded = (entryKey: string) => {
|
|
49
|
+
setExpandedEntryKeys((current) => {
|
|
50
|
+
const next = new Set(current);
|
|
51
|
+
|
|
52
|
+
if (next.has(entryKey)) {
|
|
53
|
+
next.delete(entryKey);
|
|
54
|
+
} else {
|
|
55
|
+
next.add(entryKey);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return next;
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleToggleClick = (event: MouseEvent<HTMLButtonElement>, entryKey: string) => {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
toggleExpanded(entryKey);
|
|
66
|
+
};
|
|
67
|
+
|
|
17
68
|
return (
|
|
18
69
|
<div className={styles.entriesList}>
|
|
19
70
|
<h3>Activity Log ({entries.length} entries)</h3>
|
|
@@ -22,30 +73,49 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
|
22
73
|
<p>No activities match the current filters.</p>
|
|
23
74
|
</div>
|
|
24
75
|
) : (
|
|
25
|
-
entries.map((entry) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
>
|
|
30
|
-
<div className={styles.entryHeader}>
|
|
31
|
-
<div className={styles.entryIcons}>
|
|
32
|
-
<span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
|
|
33
|
-
<span className={styles.statusIcon}>{getAuditStatusIcon(entry.result)}</span>
|
|
34
|
-
</div>
|
|
35
|
-
<div className={styles.entryTitle}>
|
|
36
|
-
<span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
|
|
37
|
-
<span className={styles.fileName}>{entry.details.fileName}</span>
|
|
38
|
-
</div>
|
|
39
|
-
<div className={styles.entryTimestamp}>{formatAuditTimestamp(entry.timestamp)}</div>
|
|
40
|
-
</div>
|
|
76
|
+
entries.map((entry) => {
|
|
77
|
+
const entryKey = getEntryKey(entry);
|
|
78
|
+
const isExpandable = expandableEntries.has(entryKey);
|
|
79
|
+
const isExpanded = expandedEntryKeys.has(entryKey);
|
|
41
80
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
key={entryKey}
|
|
84
|
+
className={`${styles.entry} ${styles[entry.result]}`}
|
|
85
|
+
>
|
|
86
|
+
<div className={styles.entryHeader}>
|
|
87
|
+
<div className={styles.entryIcons}>
|
|
88
|
+
<span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
|
|
89
|
+
<span className={styles.statusIcon}>{getAuditStatusIcon(entry.result)}</span>
|
|
47
90
|
</div>
|
|
48
|
-
|
|
91
|
+
<div className={styles.entryTitle}>
|
|
92
|
+
<span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
|
|
93
|
+
<span className={styles.fileName}>{entry.details.fileName}</span>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className={styles.entryHeaderActions}>
|
|
97
|
+
<div className={styles.entryTimestamp}>{formatAuditTimestamp(entry.timestamp)}</div>
|
|
98
|
+
{isExpandable && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className={styles.entryDetailsToggle}
|
|
102
|
+
aria-expanded={isExpanded}
|
|
103
|
+
aria-label={isExpanded ? 'Hide full entry details' : 'Show full entry details'}
|
|
104
|
+
onClick={(event) => handleToggleClick(event, entryKey)}
|
|
105
|
+
>
|
|
106
|
+
{isExpanded ? 'Hide details' : 'Show details'}
|
|
107
|
+
</button>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className={styles.entryDetails}>
|
|
113
|
+
{entry.details.caseNumber && (
|
|
114
|
+
<div className={styles.detailRow}>
|
|
115
|
+
<span className={styles.detailLabel}>Case:</span>
|
|
116
|
+
<span className={styles.detailValue}>{entry.details.caseNumber}</span>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
49
119
|
|
|
50
120
|
{entry.details.userProfileDetails?.badgeId && (
|
|
51
121
|
<div className={styles.detailRow}>
|
|
@@ -191,37 +261,49 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
|
191
261
|
</>
|
|
192
262
|
)}
|
|
193
263
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
264
|
+
{(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
|
|
265
|
+
<>
|
|
266
|
+
{entry.details.fileDetails.fileId && (
|
|
267
|
+
<div className={styles.detailRow}>
|
|
268
|
+
<span className={styles.detailLabel}>
|
|
269
|
+
{entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}
|
|
270
|
+
</span>
|
|
271
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
204
274
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
275
|
+
{entry.details.fileDetails.originalFileName && (
|
|
276
|
+
<div className={styles.detailRow}>
|
|
277
|
+
<span className={styles.detailLabel}>
|
|
278
|
+
{entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}
|
|
279
|
+
</span>
|
|
280
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{entry.action === 'confirm' && entry.details.confirmationId && (
|
|
285
|
+
<div className={styles.detailRow}>
|
|
286
|
+
<span className={styles.detailLabel}>Confirmation ID:</span>
|
|
287
|
+
<span className={styles.detailValue}>{entry.details.confirmationId}</span>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
</>
|
|
291
|
+
)}
|
|
213
292
|
|
|
214
|
-
|
|
293
|
+
{isExpandable && isExpanded && (
|
|
294
|
+
<div className={styles.expandedDetails}>
|
|
215
295
|
<div className={styles.detailRow}>
|
|
216
|
-
<span className={styles.detailLabel}>
|
|
217
|
-
<span className={styles.detailValue}>{entry.details.confirmationId}</span>
|
|
296
|
+
<span className={styles.detailLabel}>Full Entry Details:</span>
|
|
218
297
|
</div>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
298
|
+
<pre className={styles.expandedDetailsCode}>
|
|
299
|
+
{JSON.stringify(entry, null, 2)}
|
|
300
|
+
</pre>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
222
304
|
</div>
|
|
223
|
-
|
|
224
|
-
)
|
|
305
|
+
);
|
|
306
|
+
})
|
|
225
307
|
)}
|
|
226
308
|
</div>
|
|
227
309
|
);
|
|
@@ -374,16 +374,6 @@
|
|
|
374
374
|
background: color-mix(in lab, #6c757d 21%, #ffffff);
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
.caseMenuItemKey {
|
|
378
|
-
background: color-mix(in lab, #0d9488 14%, #ffffff);
|
|
379
|
-
color: #0f766e;
|
|
380
|
-
border-color: color-mix(in lab, #0d9488 28%, transparent);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
.caseMenuItemKey:hover {
|
|
384
|
-
background: color-mix(in lab, #0d9488 20%, #ffffff);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
377
|
.caseMenuItemClearRO {
|
|
388
378
|
background: color-mix(in lab, #fd7e14 16%, #ffffff);
|
|
389
379
|
color: #7c3f00;
|
|
@@ -3,8 +3,6 @@ import styles from './navbar.module.css';
|
|
|
3
3
|
import { SignOut } from '../actions/signout';
|
|
4
4
|
import { ManageProfile } from '../user/manage-profile';
|
|
5
5
|
import { CaseImport } from '../sidebar/case-import/case-import';
|
|
6
|
-
import { PublicSigningKeyModal } from '~/components/public-signing-key-modal/public-signing-key-modal';
|
|
7
|
-
import { getCurrentPublicSigningKeyDetails } from '~/utils/forensics';
|
|
8
6
|
import { AuthContext } from '~/contexts/auth.context';
|
|
9
7
|
import { getUserData } from '~/utils/data';
|
|
10
8
|
import { type ImportResult, type ConfirmationImportResult } from '~/types';
|
|
@@ -68,10 +66,8 @@ export const Navbar = ({
|
|
|
68
66
|
const [userBadgeId, setUserBadgeId] = useState<string>('');
|
|
69
67
|
const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
|
|
70
68
|
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
|
71
|
-
const [isPublicKeyModalOpen, setIsPublicKeyModalOpen] = useState(false);
|
|
72
69
|
const [isCaseMenuOpen, setIsCaseMenuOpen] = useState(false);
|
|
73
70
|
const [isFileMenuOpen, setIsFileMenuOpen] = useState(false);
|
|
74
|
-
const { keyId: publicSigningKeyId, publicKeyPem } = getCurrentPublicSigningKeyDetails();
|
|
75
71
|
const caseMenuRef = useRef<HTMLDivElement>(null);
|
|
76
72
|
const fileMenuRef = useRef<HTMLDivElement>(null);
|
|
77
73
|
|
|
@@ -292,18 +288,6 @@ export const Navbar = ({
|
|
|
292
288
|
Archive Case
|
|
293
289
|
</button>
|
|
294
290
|
)}
|
|
295
|
-
<div className={styles.caseMenuSectionLabel}>Verification</div>
|
|
296
|
-
<button
|
|
297
|
-
type="button"
|
|
298
|
-
role="menuitem"
|
|
299
|
-
className={`${styles.caseMenuItem} ${styles.caseMenuItemKey}`}
|
|
300
|
-
onClick={() => {
|
|
301
|
-
setIsPublicKeyModalOpen(true);
|
|
302
|
-
setIsCaseMenuOpen(false);
|
|
303
|
-
}}
|
|
304
|
-
>
|
|
305
|
-
Verify Exports
|
|
306
|
-
</button>
|
|
307
291
|
{currentCase && (
|
|
308
292
|
<div className={styles.caseMenuCaption}>Case: {currentCase}</div>
|
|
309
293
|
)}
|
|
@@ -423,12 +407,6 @@ export const Navbar = ({
|
|
|
423
407
|
isOpen={isProfileModalOpen}
|
|
424
408
|
onClose={() => setIsProfileModalOpen(false)}
|
|
425
409
|
/>
|
|
426
|
-
<PublicSigningKeyModal
|
|
427
|
-
isOpen={isPublicKeyModalOpen}
|
|
428
|
-
onClose={() => setIsPublicKeyModalOpen(false)}
|
|
429
|
-
publicSigningKeyId={publicSigningKeyId}
|
|
430
|
-
publicKeyPem={publicKeyPem}
|
|
431
|
-
/>
|
|
432
410
|
</>
|
|
433
411
|
);
|
|
434
412
|
};
|