@striae-org/striae 4.1.0 → 4.2.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 +8 -0
- package/app/components/actions/case-export/core-export.ts +14 -8
- package/app/components/actions/case-export/data-processing.ts +1 -0
- package/app/components/actions/case-export/download-handlers.ts +7 -0
- package/app/components/actions/case-export/metadata-helpers.ts +2 -1
- package/app/components/actions/case-import/confirmation-import.ts +12 -2
- package/app/components/actions/case-import/orchestrator.ts +78 -32
- package/app/components/actions/case-import/storage-operations.ts +97 -8
- package/app/components/actions/case-import/zip-processing.ts +159 -86
- package/app/components/actions/case-manage.ts +430 -8
- package/app/components/actions/confirm-export.ts +9 -2
- package/app/components/actions/image-manage.ts +77 -44
- package/app/components/audit/user-audit-viewer.tsx +19 -8
- package/app/components/audit/user-audit.module.css +21 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +7 -0
- package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +21 -1
- package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
- package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
- package/app/components/canvas/canvas.module.css +64 -54
- package/app/components/canvas/canvas.tsx +14 -16
- package/app/components/canvas/confirmation/confirmation.module.css +1 -0
- package/app/components/canvas/confirmation/confirmation.tsx +6 -12
- package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
- package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
- package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
- package/app/components/navbar/navbar.module.css +447 -0
- package/app/components/navbar/navbar.tsx +377 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
- package/app/components/sidebar/case-export/case-export.module.css +1 -0
- package/app/components/sidebar/case-export/case-export.tsx +8 -46
- package/app/components/sidebar/case-import/case-import.module.css +23 -0
- package/app/components/sidebar/case-import/case-import.tsx +64 -16
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
- package/app/components/sidebar/cases/cases-modal.module.css +1 -0
- package/app/components/sidebar/cases/cases-modal.tsx +6 -8
- package/app/components/sidebar/cases/cases.module.css +62 -21
- package/app/components/sidebar/files/files-modal.module.css +1 -0
- package/app/components/sidebar/files/files-modal.tsx +12 -13
- package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
- package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
- package/app/components/sidebar/notes/notes-modal.tsx +7 -8
- package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
- package/app/components/sidebar/notes/notes.module.css +153 -0
- package/app/components/sidebar/sidebar-container.tsx +15 -28
- package/app/components/sidebar/sidebar.module.css +5 -69
- package/app/components/sidebar/sidebar.tsx +24 -125
- package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
- package/app/components/user/inactivity-warning.module.css +1 -0
- package/app/components/user/inactivity-warning.tsx +15 -2
- package/app/components/user/manage-profile.tsx +23 -10
- package/app/hooks/useOverlayDismiss.ts +52 -4
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +469 -30
- package/app/services/audit/audit.service.ts +173 -27
- package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
- package/app/services/audit/builders/index.ts +1 -0
- package/app/types/audit.ts +3 -1
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -0
- package/app/utils/data/permissions.ts +16 -1
- package/app/utils/forensics/audit-export-signature.ts +5 -1
- package/app/utils/forensics/confirmation-signature.ts +3 -0
- package/app/utils/forensics/export-verification.ts +497 -22
- package/package.json +3 -3
- package/scripts/deploy-primershear-emails.sh +2 -1
- package/worker-configuration.d.ts +1 -1
- package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/worker-configuration.d.ts +7448 -11323
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +7447 -11322
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/worker-configuration.d.ts +7448 -11323
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/public/.well-known/keybase.txt +0 -56
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
AuditResult,
|
|
10
10
|
PerformanceMetrics
|
|
11
11
|
} from '~/types';
|
|
12
|
+
import { getCaseData, getUserData } from '~/utils/data';
|
|
12
13
|
import { generateWorkflowId } from '~/utils/common';
|
|
13
14
|
import {
|
|
14
15
|
fetchAuditEntriesForUser,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
buildAnnotationCreateAuditParams,
|
|
27
28
|
buildAnnotationDeleteAuditParams,
|
|
28
29
|
buildAnnotationEditAuditParams,
|
|
30
|
+
buildCaseArchiveAuditParams,
|
|
29
31
|
buildCaseCreationAuditParams,
|
|
30
32
|
buildCaseDeletionAuditParams,
|
|
31
33
|
buildCaseExportAuditParams,
|
|
@@ -137,6 +139,67 @@ export class AuditService {
|
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
private normalizeBadgeId(badgeId?: string): string | undefined {
|
|
143
|
+
const normalized = badgeId?.trim();
|
|
144
|
+
return normalized ? normalized : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private applyBadgeIdToParams(
|
|
148
|
+
params: CreateAuditEntryParams,
|
|
149
|
+
badgeId?: string
|
|
150
|
+
): CreateAuditEntryParams {
|
|
151
|
+
const normalizedBadgeId = this.normalizeBadgeId(badgeId);
|
|
152
|
+
if (!normalizedBadgeId) {
|
|
153
|
+
return params;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
...params,
|
|
158
|
+
userProfileDetails: {
|
|
159
|
+
...(params.userProfileDetails || {}),
|
|
160
|
+
badgeId: normalizedBadgeId
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private async resolveBadgeIdForUser(user: User): Promise<string | undefined> {
|
|
166
|
+
const cachedBadgeId = this.userBadgeIdByUserId.get(user.uid);
|
|
167
|
+
if (cachedBadgeId) {
|
|
168
|
+
return cachedBadgeId;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const userData = await getUserData(user);
|
|
173
|
+
const resolvedBadgeId = this.normalizeBadgeId(userData?.badgeId);
|
|
174
|
+
|
|
175
|
+
if (resolvedBadgeId) {
|
|
176
|
+
this.userBadgeIdByUserId.set(user.uid, resolvedBadgeId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return resolvedBadgeId;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('🚨 Audit: Failed to resolve badge ID for user:', error);
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async logEventForUser(user: User, params: CreateAuditEntryParams): Promise<void> {
|
|
187
|
+
const resolvedBadgeId = await this.resolveBadgeIdForUser(user);
|
|
188
|
+
await this.logEvent(this.applyBadgeIdToParams(params, resolvedBadgeId));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async logEventForOptionalUser(
|
|
192
|
+
user: User | null,
|
|
193
|
+
params: CreateAuditEntryParams
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
if (!user) {
|
|
196
|
+
await this.logEvent(params);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await this.logEventForUser(user, params);
|
|
201
|
+
}
|
|
202
|
+
|
|
140
203
|
/**
|
|
141
204
|
* Log case export event
|
|
142
205
|
*/
|
|
@@ -155,7 +218,7 @@ export class AuditService {
|
|
|
155
218
|
keyId?: string;
|
|
156
219
|
}
|
|
157
220
|
): Promise<void> {
|
|
158
|
-
await this.
|
|
221
|
+
await this.logEventForUser(user,
|
|
159
222
|
buildCaseExportAuditParams({
|
|
160
223
|
user,
|
|
161
224
|
caseNumber,
|
|
@@ -188,7 +251,7 @@ export class AuditService {
|
|
|
188
251
|
keyId?: string;
|
|
189
252
|
}
|
|
190
253
|
): Promise<void> {
|
|
191
|
-
await this.
|
|
254
|
+
await this.logEventForUser(user,
|
|
192
255
|
buildCaseImportAuditParams({
|
|
193
256
|
user,
|
|
194
257
|
caseNumber,
|
|
@@ -219,7 +282,7 @@ export class AuditService {
|
|
|
219
282
|
originalImageFileName?: string,
|
|
220
283
|
badgeId?: string
|
|
221
284
|
): Promise<void> {
|
|
222
|
-
await this.
|
|
285
|
+
await this.logEventForUser(user,
|
|
223
286
|
buildConfirmationCreationAuditParams({
|
|
224
287
|
user,
|
|
225
288
|
caseNumber,
|
|
@@ -253,7 +316,7 @@ export class AuditService {
|
|
|
253
316
|
keyId?: string;
|
|
254
317
|
}
|
|
255
318
|
): Promise<void> {
|
|
256
|
-
await this.
|
|
319
|
+
await this.logEventForUser(user,
|
|
257
320
|
buildConfirmationExportAuditParams({
|
|
258
321
|
user,
|
|
259
322
|
caseNumber,
|
|
@@ -286,9 +349,10 @@ export class AuditService {
|
|
|
286
349
|
present: boolean;
|
|
287
350
|
valid: boolean;
|
|
288
351
|
keyId?: string;
|
|
289
|
-
}
|
|
352
|
+
},
|
|
353
|
+
reviewerBadgeId?: string // Badge/ID number of the reviewing examiner who exported the file
|
|
290
354
|
): Promise<void> {
|
|
291
|
-
await this.
|
|
355
|
+
await this.logEventForUser(user,
|
|
292
356
|
buildConfirmationImportAuditParams({
|
|
293
357
|
user,
|
|
294
358
|
caseNumber,
|
|
@@ -298,6 +362,7 @@ export class AuditService {
|
|
|
298
362
|
confirmationsImported,
|
|
299
363
|
errors,
|
|
300
364
|
reviewingExaminerUid,
|
|
365
|
+
reviewerBadgeId,
|
|
301
366
|
performanceMetrics,
|
|
302
367
|
exporterUidValidated,
|
|
303
368
|
totalConfirmationsInFile,
|
|
@@ -318,7 +383,7 @@ export class AuditService {
|
|
|
318
383
|
caseNumber: string,
|
|
319
384
|
caseName: string
|
|
320
385
|
): Promise<void> {
|
|
321
|
-
await this.
|
|
386
|
+
await this.logEventForUser(user,
|
|
322
387
|
buildCaseCreationAuditParams({
|
|
323
388
|
user,
|
|
324
389
|
caseNumber,
|
|
@@ -336,7 +401,7 @@ export class AuditService {
|
|
|
336
401
|
oldName: string,
|
|
337
402
|
newName: string
|
|
338
403
|
): Promise<void> {
|
|
339
|
-
await this.
|
|
404
|
+
await this.logEventForUser(user,
|
|
340
405
|
buildCaseRenameAuditParams({
|
|
341
406
|
user,
|
|
342
407
|
caseNumber,
|
|
@@ -356,7 +421,7 @@ export class AuditService {
|
|
|
356
421
|
deleteReason: string,
|
|
357
422
|
backupCreated: boolean = false
|
|
358
423
|
): Promise<void> {
|
|
359
|
-
await this.
|
|
424
|
+
await this.logEventForUser(user,
|
|
360
425
|
buildCaseDeletionAuditParams({
|
|
361
426
|
user,
|
|
362
427
|
caseNumber,
|
|
@@ -367,6 +432,35 @@ export class AuditService {
|
|
|
367
432
|
);
|
|
368
433
|
}
|
|
369
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Log case archive event
|
|
437
|
+
*/
|
|
438
|
+
public async logCaseArchive(
|
|
439
|
+
user: User,
|
|
440
|
+
caseNumber: string,
|
|
441
|
+
caseName: string,
|
|
442
|
+
archiveReason: string,
|
|
443
|
+
result: AuditResult = 'success',
|
|
444
|
+
errors: string[] = [],
|
|
445
|
+
totalFiles?: number,
|
|
446
|
+
archivedAt?: string,
|
|
447
|
+
processingTimeMs?: number
|
|
448
|
+
): Promise<void> {
|
|
449
|
+
await this.logEventForUser(user,
|
|
450
|
+
buildCaseArchiveAuditParams({
|
|
451
|
+
user,
|
|
452
|
+
caseNumber,
|
|
453
|
+
caseName,
|
|
454
|
+
archiveReason,
|
|
455
|
+
result,
|
|
456
|
+
errors,
|
|
457
|
+
totalFiles,
|
|
458
|
+
archivedAt,
|
|
459
|
+
processingTimeMs
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
370
464
|
/**
|
|
371
465
|
* Log file upload event
|
|
372
466
|
*/
|
|
@@ -381,7 +475,7 @@ export class AuditService {
|
|
|
381
475
|
processingTime?: number,
|
|
382
476
|
fileId?: string
|
|
383
477
|
): Promise<void> {
|
|
384
|
-
await this.
|
|
478
|
+
await this.logEventForUser(user,
|
|
385
479
|
buildFileUploadAuditParams({
|
|
386
480
|
user,
|
|
387
481
|
fileName,
|
|
@@ -408,7 +502,7 @@ export class AuditService {
|
|
|
408
502
|
fileId?: string,
|
|
409
503
|
originalFileName?: string
|
|
410
504
|
): Promise<void> {
|
|
411
|
-
await this.
|
|
505
|
+
await this.logEventForUser(user,
|
|
412
506
|
buildFileDeletionAuditParams({
|
|
413
507
|
user,
|
|
414
508
|
fileName,
|
|
@@ -435,7 +529,7 @@ export class AuditService {
|
|
|
435
529
|
accessReason?: string,
|
|
436
530
|
originalFileName?: string
|
|
437
531
|
): Promise<void> {
|
|
438
|
-
await this.
|
|
532
|
+
await this.logEventForUser(user,
|
|
439
533
|
buildFileAccessAuditParams({
|
|
440
534
|
user,
|
|
441
535
|
fileName,
|
|
@@ -463,7 +557,7 @@ export class AuditService {
|
|
|
463
557
|
imageFileId?: string,
|
|
464
558
|
originalImageFileName?: string
|
|
465
559
|
): Promise<void> {
|
|
466
|
-
await this.
|
|
560
|
+
await this.logEventForUser(user,
|
|
467
561
|
buildAnnotationCreateAuditParams({
|
|
468
562
|
user,
|
|
469
563
|
annotationId,
|
|
@@ -490,7 +584,7 @@ export class AuditService {
|
|
|
490
584
|
imageFileId?: string,
|
|
491
585
|
originalImageFileName?: string
|
|
492
586
|
): Promise<void> {
|
|
493
|
-
await this.
|
|
587
|
+
await this.logEventForUser(user,
|
|
494
588
|
buildAnnotationEditAuditParams({
|
|
495
589
|
user,
|
|
496
590
|
annotationId,
|
|
@@ -516,7 +610,7 @@ export class AuditService {
|
|
|
516
610
|
imageFileId?: string,
|
|
517
611
|
originalImageFileName?: string
|
|
518
612
|
): Promise<void> {
|
|
519
|
-
await this.
|
|
613
|
+
await this.logEventForUser(user,
|
|
520
614
|
buildAnnotationDeleteAuditParams({
|
|
521
615
|
user,
|
|
522
616
|
annotationId,
|
|
@@ -538,7 +632,7 @@ export class AuditService {
|
|
|
538
632
|
loginMethod: 'firebase' | 'sso' | 'api-key' | 'manual',
|
|
539
633
|
userAgent?: string
|
|
540
634
|
): Promise<void> {
|
|
541
|
-
await this.
|
|
635
|
+
await this.logEventForUser(user,
|
|
542
636
|
buildUserLoginAuditParams({
|
|
543
637
|
user,
|
|
544
638
|
sessionId,
|
|
@@ -557,7 +651,7 @@ export class AuditService {
|
|
|
557
651
|
sessionDuration: number,
|
|
558
652
|
logoutReason: 'user-initiated' | 'timeout' | 'security' | 'error'
|
|
559
653
|
): Promise<void> {
|
|
560
|
-
await this.
|
|
654
|
+
await this.logEventForUser(user,
|
|
561
655
|
buildUserLogoutAuditParams({
|
|
562
656
|
user,
|
|
563
657
|
sessionId,
|
|
@@ -580,7 +674,7 @@ export class AuditService {
|
|
|
580
674
|
errors: string[] = [],
|
|
581
675
|
badgeId?: string
|
|
582
676
|
): Promise<void> {
|
|
583
|
-
await this.
|
|
677
|
+
await this.logEventForUser(user,
|
|
584
678
|
buildUserProfileUpdateAuditParams({
|
|
585
679
|
user,
|
|
586
680
|
profileField,
|
|
@@ -701,7 +795,7 @@ export class AuditService {
|
|
|
701
795
|
userAgent?: string,
|
|
702
796
|
sessionId?: string
|
|
703
797
|
): Promise<void> {
|
|
704
|
-
await this.
|
|
798
|
+
await this.logEventForUser(user,
|
|
705
799
|
buildUserRegistrationAuditParams({
|
|
706
800
|
user,
|
|
707
801
|
firstName,
|
|
@@ -727,7 +821,7 @@ export class AuditService {
|
|
|
727
821
|
userAgent?: string,
|
|
728
822
|
errors: string[] = []
|
|
729
823
|
): Promise<void> {
|
|
730
|
-
await this.
|
|
824
|
+
await this.logEventForUser(user,
|
|
731
825
|
buildMfaEnrollmentAuditParams({
|
|
732
826
|
user,
|
|
733
827
|
phoneNumber,
|
|
@@ -753,7 +847,7 @@ export class AuditService {
|
|
|
753
847
|
userAgent?: string,
|
|
754
848
|
errors: string[] = []
|
|
755
849
|
): Promise<void> {
|
|
756
|
-
await this.
|
|
850
|
+
await this.logEventForUser(user,
|
|
757
851
|
buildMfaAuthenticationAuditParams({
|
|
758
852
|
user,
|
|
759
853
|
mfaMethod,
|
|
@@ -778,7 +872,7 @@ export class AuditService {
|
|
|
778
872
|
userAgent?: string,
|
|
779
873
|
errors: string[] = []
|
|
780
874
|
): Promise<void> {
|
|
781
|
-
await this.
|
|
875
|
+
await this.logEventForUser(user,
|
|
782
876
|
buildEmailVerificationAuditParams({
|
|
783
877
|
user,
|
|
784
878
|
result,
|
|
@@ -828,7 +922,7 @@ export class AuditService {
|
|
|
828
922
|
sessionId?: string,
|
|
829
923
|
userAgent?: string
|
|
830
924
|
): Promise<void> {
|
|
831
|
-
await this.
|
|
925
|
+
await this.logEventForUser(user,
|
|
832
926
|
buildMarkEmailVerificationSuccessfulAuditParams({
|
|
833
927
|
user,
|
|
834
928
|
reason,
|
|
@@ -852,7 +946,7 @@ export class AuditService {
|
|
|
852
946
|
sourceFileId?: string,
|
|
853
947
|
sourceFileName?: string
|
|
854
948
|
): Promise<void> {
|
|
855
|
-
await this.
|
|
949
|
+
await this.logEventForUser(user,
|
|
856
950
|
buildPDFGenerationAuditParams({
|
|
857
951
|
user,
|
|
858
952
|
fileName,
|
|
@@ -878,7 +972,7 @@ export class AuditService {
|
|
|
878
972
|
targetResource?: string,
|
|
879
973
|
blockedBySystem: boolean = true
|
|
880
974
|
): Promise<void> {
|
|
881
|
-
await this.
|
|
975
|
+
await this.logEventForOptionalUser(user,
|
|
882
976
|
buildSecurityViolationAuditParams({
|
|
883
977
|
user,
|
|
884
978
|
incidentType,
|
|
@@ -898,6 +992,7 @@ export class AuditService {
|
|
|
898
992
|
* Get audit entries for display (public method for components)
|
|
899
993
|
*/
|
|
900
994
|
public async getAuditEntriesForUser(userId: string, params?: {
|
|
995
|
+
requestingUser?: User;
|
|
901
996
|
startDate?: string;
|
|
902
997
|
endDate?: string;
|
|
903
998
|
caseNumber?: string;
|
|
@@ -911,7 +1006,7 @@ export class AuditService {
|
|
|
911
1006
|
userId,
|
|
912
1007
|
...params
|
|
913
1008
|
};
|
|
914
|
-
return await this.getAuditEntries(queryParams);
|
|
1009
|
+
return await this.getAuditEntries(queryParams, params?.requestingUser);
|
|
915
1010
|
}
|
|
916
1011
|
|
|
917
1012
|
/**
|
|
@@ -943,8 +1038,59 @@ export class AuditService {
|
|
|
943
1038
|
/**
|
|
944
1039
|
* Get audit entries based on query parameters
|
|
945
1040
|
*/
|
|
946
|
-
private
|
|
1041
|
+
private applyDateRangeFilter(
|
|
1042
|
+
entries: ValidationAuditEntry[],
|
|
1043
|
+
startDate?: string,
|
|
1044
|
+
endDate?: string
|
|
1045
|
+
): ValidationAuditEntry[] {
|
|
1046
|
+
const startMs = startDate ? new Date(startDate).getTime() : Number.NEGATIVE_INFINITY;
|
|
1047
|
+
const endMs = endDate ? new Date(endDate).getTime() : Number.POSITIVE_INFINITY;
|
|
1048
|
+
|
|
1049
|
+
return entries.filter((entry) => {
|
|
1050
|
+
const timestampMs = new Date(entry.timestamp).getTime();
|
|
1051
|
+
return timestampMs >= startMs && timestampMs <= endMs;
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private async getBundledArchivedCaseAuditEntries(
|
|
1056
|
+
params: AuditQueryParams,
|
|
1057
|
+
requestingUser?: User
|
|
1058
|
+
): Promise<ValidationAuditEntry[] | null> {
|
|
1059
|
+
if (!requestingUser || !params.caseNumber) {
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const caseData = await getCaseData(requestingUser, params.caseNumber);
|
|
1064
|
+
if (!caseData?.isReadOnly || caseData.archived !== true) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const bundledEntries = caseData.bundledAuditTrail?.entries;
|
|
1069
|
+
if (!Array.isArray(bundledEntries)) {
|
|
1070
|
+
return [];
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const sortedEntries = sortAuditEntriesNewestFirst(bundledEntries);
|
|
1074
|
+
const dateFilteredEntries = this.applyDateRangeFilter(
|
|
1075
|
+
sortedEntries,
|
|
1076
|
+
params.startDate,
|
|
1077
|
+
params.endDate
|
|
1078
|
+
);
|
|
1079
|
+
const filteredEntries = applyAuditEntryFilters(dateFilteredEntries, {
|
|
1080
|
+
...params,
|
|
1081
|
+
userId: undefined
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
return applyAuditPagination(filteredEntries, params);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
private async getAuditEntries(params: AuditQueryParams, requestingUser?: User): Promise<ValidationAuditEntry[]> {
|
|
947
1088
|
try {
|
|
1089
|
+
const bundledArchivedEntries = await this.getBundledArchivedCaseAuditEntries(params, requestingUser);
|
|
1090
|
+
if (bundledArchivedEntries) {
|
|
1091
|
+
return bundledArchivedEntries;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
948
1094
|
// If userId is provided, fetch from server
|
|
949
1095
|
if (params.userId) {
|
|
950
1096
|
const serverEntries = await fetchAuditEntriesForUser({
|
|
@@ -88,6 +88,49 @@ export const buildCaseDeletionAuditParams = (
|
|
|
88
88
|
};
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
interface BuildCaseArchiveAuditParamsInput {
|
|
92
|
+
user: User;
|
|
93
|
+
caseNumber: string;
|
|
94
|
+
caseName: string;
|
|
95
|
+
archiveReason: string;
|
|
96
|
+
result?: AuditResult;
|
|
97
|
+
errors?: string[];
|
|
98
|
+
totalFiles?: number;
|
|
99
|
+
archivedAt?: string;
|
|
100
|
+
processingTimeMs?: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const buildCaseArchiveAuditParams = (
|
|
104
|
+
input: BuildCaseArchiveAuditParamsInput
|
|
105
|
+
): CreateAuditEntryParams => {
|
|
106
|
+
const result = input.result || 'success';
|
|
107
|
+
const archivedAt = input.archivedAt || new Date().toISOString();
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
userId: input.user.uid,
|
|
111
|
+
userEmail: input.user.email || '',
|
|
112
|
+
action: 'case-archive',
|
|
113
|
+
result,
|
|
114
|
+
fileName: `${input.caseNumber}.case`,
|
|
115
|
+
fileType: 'case-package',
|
|
116
|
+
validationErrors: input.errors || [],
|
|
117
|
+
caseNumber: input.caseNumber,
|
|
118
|
+
workflowPhase: 'casework',
|
|
119
|
+
caseDetails: {
|
|
120
|
+
newCaseName: input.caseName,
|
|
121
|
+
deleteReason: input.archiveReason,
|
|
122
|
+
totalFiles: input.totalFiles,
|
|
123
|
+
lastModified: archivedAt,
|
|
124
|
+
},
|
|
125
|
+
performanceMetrics: input.processingTimeMs
|
|
126
|
+
? {
|
|
127
|
+
processingTimeMs: input.processingTimeMs,
|
|
128
|
+
fileSizeBytes: 0,
|
|
129
|
+
}
|
|
130
|
+
: undefined,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
91
134
|
interface BuildFileUploadAuditParamsInput {
|
|
92
135
|
user: User;
|
|
93
136
|
fileName: string;
|
|
@@ -220,6 +220,7 @@ interface BuildConfirmationImportAuditParamsInput {
|
|
|
220
220
|
confirmationsImported: number;
|
|
221
221
|
errors?: string[];
|
|
222
222
|
reviewingExaminerUid?: string;
|
|
223
|
+
reviewerBadgeId?: string;
|
|
223
224
|
performanceMetrics?: PerformanceMetrics;
|
|
224
225
|
exporterUidValidated?: boolean;
|
|
225
226
|
totalConfirmationsInFile?: number;
|
|
@@ -269,6 +270,7 @@ export const buildConfirmationImportAuditParams = (
|
|
|
269
270
|
},
|
|
270
271
|
originalExaminerUid: input.user.uid,
|
|
271
272
|
reviewingExaminerUid: input.reviewingExaminerUid,
|
|
273
|
+
reviewerBadgeId: input.reviewerBadgeId,
|
|
272
274
|
caseDetails: input.totalConfirmationsInFile !== undefined
|
|
273
275
|
? {
|
|
274
276
|
totalAnnotations: input.totalConfirmationsInFile
|
package/app/types/audit.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
export type AuditAction =
|
|
5
5
|
// Case Management Actions
|
|
6
|
-
| 'case-create' | 'case-rename' | 'case-delete'
|
|
6
|
+
| 'case-create' | 'case-rename' | 'case-delete' | 'case-archive'
|
|
7
7
|
// Confirmation Workflow Actions
|
|
8
8
|
| 'case-export' | 'case-import' | 'confirmation-create' | 'confirmation-export' | 'confirmation-import'
|
|
9
9
|
// File Operations
|
|
@@ -59,6 +59,7 @@ export interface AuditDetails {
|
|
|
59
59
|
// Context & Workflow
|
|
60
60
|
originalExaminerUid?: string;
|
|
61
61
|
reviewingExaminerUid?: string;
|
|
62
|
+
reviewerBadgeId?: string;
|
|
62
63
|
workflowPhase?: WorkflowPhase;
|
|
63
64
|
|
|
64
65
|
// Performance & Metrics
|
|
@@ -160,6 +161,7 @@ export interface CreateAuditEntryParams {
|
|
|
160
161
|
performanceMetrics?: PerformanceMetrics;
|
|
161
162
|
originalExaminerUid?: string;
|
|
162
163
|
reviewingExaminerUid?: string;
|
|
164
|
+
reviewerBadgeId?: string;
|
|
163
165
|
// Extended detail fields
|
|
164
166
|
caseDetails?: CaseAuditDetails;
|
|
165
167
|
fileDetails?: FileAuditDetails;
|
package/app/types/case.ts
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import { type FileData } from './file';
|
|
2
2
|
import { type AnnotationData, type ConfirmationData } from './annotations';
|
|
3
|
+
import { type ValidationAuditEntry } from './audit';
|
|
3
4
|
|
|
4
5
|
// Case-related types and interfaces
|
|
5
6
|
|
|
6
7
|
export type CaseActionType = 'loaded' | 'created' | 'deleted' | null;
|
|
7
8
|
|
|
9
|
+
export interface BundledAuditTrailData {
|
|
10
|
+
source: 'archive-bundle';
|
|
11
|
+
importedAt: string;
|
|
12
|
+
exportTimestamp?: string;
|
|
13
|
+
totalEntries?: number;
|
|
14
|
+
entries: ValidationAuditEntry[];
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
export interface CaseData {
|
|
9
18
|
createdAt: string;
|
|
10
19
|
caseNumber: string;
|
|
11
20
|
files: FileData[];
|
|
21
|
+
isReadOnly?: boolean;
|
|
22
|
+
archived?: boolean;
|
|
23
|
+
archivedAt?: string;
|
|
24
|
+
archivedBy?: string;
|
|
25
|
+
archivedByDisplay?: string;
|
|
26
|
+
archiveReason?: string;
|
|
27
|
+
bundledAuditTrail?: BundledAuditTrailData;
|
|
12
28
|
}
|
|
13
29
|
|
|
14
30
|
export interface ReadOnlyCaseData extends CaseData {
|
|
@@ -23,11 +39,17 @@ export interface CaseExportData {
|
|
|
23
39
|
metadata: {
|
|
24
40
|
caseNumber: string;
|
|
25
41
|
caseCreatedDate: string;
|
|
42
|
+
archived?: boolean;
|
|
43
|
+
archivedAt?: string;
|
|
44
|
+
archivedBy?: string;
|
|
45
|
+
archivedByDisplay?: string;
|
|
46
|
+
archiveReason?: string;
|
|
26
47
|
exportDate: string;
|
|
27
48
|
exportedBy: string | null;
|
|
28
49
|
exportedByUid: string;
|
|
29
50
|
exportedByName: string;
|
|
30
51
|
exportedByCompany: string;
|
|
52
|
+
exportedByBadgeId?: string;
|
|
31
53
|
striaeExportSchemaVersion: string;
|
|
32
54
|
totalFiles: number;
|
|
33
55
|
};
|
|
@@ -56,6 +78,7 @@ export interface AllCasesExportData {
|
|
|
56
78
|
exportedByUid: string;
|
|
57
79
|
exportedByName: string;
|
|
58
80
|
exportedByCompany: string;
|
|
81
|
+
exportedByBadgeId?: string;
|
|
59
82
|
striaeExportSchemaVersion: string;
|
|
60
83
|
totalCases: number;
|
|
61
84
|
totalFiles: number;
|
|
@@ -84,7 +107,13 @@ export interface CaseDataWithConfirmations {
|
|
|
84
107
|
caseNumber: string;
|
|
85
108
|
files: FileData[];
|
|
86
109
|
isReadOnly?: boolean;
|
|
110
|
+
archived?: boolean;
|
|
111
|
+
archivedAt?: string;
|
|
112
|
+
archivedBy?: string;
|
|
113
|
+
archivedByDisplay?: string;
|
|
114
|
+
archiveReason?: string;
|
|
87
115
|
importedAt?: string;
|
|
88
116
|
originalImageIds?: { [originalId: string]: string };
|
|
89
117
|
confirmations?: CaseConfirmations;
|
|
118
|
+
bundledAuditTrail?: BundledAuditTrailData;
|
|
90
119
|
}
|
package/app/types/import.ts
CHANGED
|
@@ -51,6 +51,7 @@ export interface ConfirmationImportData {
|
|
|
51
51
|
exportedByUid: string;
|
|
52
52
|
exportedByName: string;
|
|
53
53
|
exportedByCompany: string;
|
|
54
|
+
exportedByBadgeId?: string;
|
|
54
55
|
totalConfirmations: number;
|
|
55
56
|
version: string;
|
|
56
57
|
hash: string;
|
|
@@ -79,9 +80,11 @@ export interface ConfirmationImportData {
|
|
|
79
80
|
|
|
80
81
|
export interface CaseImportPreview {
|
|
81
82
|
caseNumber: string;
|
|
83
|
+
archived?: boolean;
|
|
82
84
|
exportedBy: string | null;
|
|
83
85
|
exportedByName: string | null;
|
|
84
86
|
exportedByCompany: string | null;
|
|
87
|
+
exportedByBadgeId?: string | null;
|
|
85
88
|
exportDate: string;
|
|
86
89
|
totalFiles: number;
|
|
87
90
|
caseCreatedDate?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
2
|
import type { UserData, ExtendedUserData, UserLimits, ReadOnlyCaseMetadata } from '~/types';
|
|
3
3
|
import paths from '~/config/config.json';
|
|
4
|
-
import { fetchUserApi } from '../api';
|
|
4
|
+
import { fetchDataApi, fetchUserApi } from '../api';
|
|
5
5
|
|
|
6
6
|
const MAX_CASES_REVIEW = paths.max_cases_review;
|
|
7
7
|
const MAX_FILES_PER_CASE_REVIEW = paths.max_files_per_case_review;
|
|
@@ -403,6 +403,21 @@ export const canModifyCase = async (user: User, caseNumber: string): Promise<Per
|
|
|
403
403
|
return { allowed: false, reason: 'User data not found' };
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
const archiveCheckResponse = await fetchDataApi(
|
|
407
|
+
user,
|
|
408
|
+
`/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`,
|
|
409
|
+
{
|
|
410
|
+
method: 'GET'
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (archiveCheckResponse.ok) {
|
|
415
|
+
const caseData = await archiveCheckResponse.json() as { archived?: boolean };
|
|
416
|
+
if (caseData.archived) {
|
|
417
|
+
return { allowed: false, reason: 'Archived cases are immutable and read-only' };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
406
421
|
// Check if user owns the case (regular cases)
|
|
407
422
|
if (userData.cases && userData.cases.some(c => c.caseNumber === caseNumber)) {
|
|
408
423
|
// For owned cases, user must be permitted
|
|
@@ -83,7 +83,8 @@ export function createAuditExportSigningPayload(payload: AuditExportSigningPaylo
|
|
|
83
83
|
|
|
84
84
|
export async function verifyAuditExportSignature(
|
|
85
85
|
payload: Partial<AuditExportSigningPayload>,
|
|
86
|
-
signature?: ForensicManifestSignature
|
|
86
|
+
signature?: ForensicManifestSignature,
|
|
87
|
+
verificationPublicKeyPem?: string
|
|
87
88
|
): Promise<ManifestSignatureVerificationResult> {
|
|
88
89
|
if (!signature) {
|
|
89
90
|
return {
|
|
@@ -112,6 +113,9 @@ export async function verifyAuditExportSignature(
|
|
|
112
113
|
noVerificationKeyPrefix: 'No verification key configured for key ID',
|
|
113
114
|
invalidPublicKeyError: 'Audit export signature verification failed: invalid public key',
|
|
114
115
|
verificationFailedError: 'Audit export signature verification failed'
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
verificationPublicKeyPem
|
|
115
119
|
}
|
|
116
120
|
);
|
|
117
121
|
}
|
|
@@ -134,6 +134,9 @@ export function createConfirmationSigningPayload(
|
|
|
134
134
|
exportedByUid: confirmationData.metadata.exportedByUid,
|
|
135
135
|
exportedByName: confirmationData.metadata.exportedByName,
|
|
136
136
|
exportedByCompany: confirmationData.metadata.exportedByCompany,
|
|
137
|
+
...(confirmationData.metadata.exportedByBadgeId
|
|
138
|
+
? { exportedByBadgeId: confirmationData.metadata.exportedByBadgeId }
|
|
139
|
+
: {}),
|
|
137
140
|
totalConfirmations: confirmationData.metadata.totalConfirmations,
|
|
138
141
|
version: confirmationData.metadata.version,
|
|
139
142
|
hash: confirmationData.metadata.hash.toUpperCase(),
|