@striae-org/striae 4.3.3 → 4.3.4
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/app/components/actions/case-manage.ts +46 -6
- package/app/components/audit/user-audit.module.css +49 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +130 -48
- 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/package.json +1 -1
|
@@ -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';
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
getVerificationPublicKey,
|
|
28
28
|
} from '~/utils/forensics';
|
|
29
29
|
import { signAuditExport } from '~/services/audit/audit-export-signing';
|
|
30
|
-
import { generateAuditSummary } from '~/services/audit/audit-query-helpers';
|
|
30
|
+
import { generateAuditSummary, sortAuditEntriesNewestFirst } from '~/services/audit/audit-query-helpers';
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Delete a file without individual audit logging (for bulk operations)
|
|
@@ -393,15 +393,23 @@ export const renameCase = async (
|
|
|
393
393
|
// 5) Delete old case number in user's KV entry
|
|
394
394
|
await removeUserCase(user, oldCaseNumber);
|
|
395
395
|
|
|
396
|
-
// Log successful case rename
|
|
396
|
+
// Log successful case rename under the original case number context
|
|
397
397
|
const endTime = Date.now();
|
|
398
398
|
await auditService.logCaseRename(
|
|
399
399
|
user,
|
|
400
|
-
|
|
400
|
+
oldCaseNumber,
|
|
401
401
|
oldCaseNumber,
|
|
402
402
|
newCaseNumber
|
|
403
403
|
);
|
|
404
404
|
|
|
405
|
+
// Log creation of the new case number as a rename-derived case
|
|
406
|
+
await auditService.logCaseCreation(
|
|
407
|
+
user,
|
|
408
|
+
newCaseNumber,
|
|
409
|
+
newCaseNumber,
|
|
410
|
+
oldCaseNumber
|
|
411
|
+
);
|
|
412
|
+
|
|
405
413
|
console.log(`✅ Case renamed: ${oldCaseNumber} → ${newCaseNumber} (${endTime - startTime}ms)`);
|
|
406
414
|
|
|
407
415
|
} catch (error) {
|
|
@@ -808,11 +816,43 @@ export const archiveCase = async (
|
|
|
808
816
|
startDate: caseData.createdAt,
|
|
809
817
|
endDate: archivedAt,
|
|
810
818
|
});
|
|
819
|
+
|
|
820
|
+
// Ensure the bundled archive trail includes the archival event itself.
|
|
821
|
+
const archiveAuditEntry: ValidationAuditEntry = {
|
|
822
|
+
timestamp: archivedAt,
|
|
823
|
+
userId: user.uid,
|
|
824
|
+
userEmail: user.email || '',
|
|
825
|
+
action: 'case-archive',
|
|
826
|
+
result: 'success',
|
|
827
|
+
details: {
|
|
828
|
+
fileName: `${caseNumber}.case`,
|
|
829
|
+
fileType: 'case-package',
|
|
830
|
+
validationErrors: [],
|
|
831
|
+
caseNumber,
|
|
832
|
+
workflowPhase: 'casework',
|
|
833
|
+
caseDetails: {
|
|
834
|
+
newCaseName: caseNumber,
|
|
835
|
+
archiveReason: archiveReason?.trim() || 'No reason provided',
|
|
836
|
+
totalFiles: archiveData.files?.length || 0,
|
|
837
|
+
lastModified: archivedAt,
|
|
838
|
+
},
|
|
839
|
+
performanceMetrics: {
|
|
840
|
+
processingTimeMs: Date.now() - startTime,
|
|
841
|
+
fileSizeBytes: 0,
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const auditEntriesWithArchive = sortAuditEntriesNewestFirst([
|
|
847
|
+
...auditEntries,
|
|
848
|
+
archiveAuditEntry,
|
|
849
|
+
]);
|
|
850
|
+
|
|
811
851
|
const auditTrail: AuditTrail = {
|
|
812
852
|
caseNumber,
|
|
813
853
|
workflowId: `${caseNumber}-archive-${Date.now()}`,
|
|
814
|
-
entries:
|
|
815
|
-
summary: generateAuditSummary(
|
|
854
|
+
entries: auditEntriesWithArchive,
|
|
855
|
+
summary: generateAuditSummary(auditEntriesWithArchive),
|
|
816
856
|
};
|
|
817
857
|
|
|
818
858
|
const auditTrailPayload = {
|
|
@@ -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
|
);
|
|
@@ -42,7 +42,7 @@ export const AUDIT_CSV_ENTRY_HEADERS = [
|
|
|
42
42
|
'Confirmed Files'
|
|
43
43
|
];
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
const formatForCSV = (value?: string | number | null): string => {
|
|
46
46
|
if (value === undefined || value === null) return '';
|
|
47
47
|
const str = String(value);
|
|
48
48
|
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
@@ -22,14 +22,14 @@ interface SignAuditExportInput {
|
|
|
22
22
|
hash: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
interface AuditExportSignature {
|
|
26
26
|
algorithm: string;
|
|
27
27
|
keyId: string;
|
|
28
28
|
signedAt: string;
|
|
29
29
|
value: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
interface SignedAuditExportPayload {
|
|
33
33
|
signatureMetadata: AuditExportSigningPayload;
|
|
34
34
|
signature: AuditExportSignature;
|
|
35
35
|
}
|
|
@@ -8,7 +8,7 @@ import { type AuditExportContext, signAuditExport } from './audit-export-signing
|
|
|
8
8
|
* Audit Export Service
|
|
9
9
|
* Handles exporting audit trails to various formats for compliance and forensic analysis
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
class AuditExportService {
|
|
12
12
|
private static instance: AuditExportService;
|
|
13
13
|
|
|
14
14
|
private constructor() {}
|
|
@@ -2,7 +2,6 @@ import type { User } from 'firebase/auth';
|
|
|
2
2
|
import type {
|
|
3
3
|
ValidationAuditEntry,
|
|
4
4
|
CreateAuditEntryParams,
|
|
5
|
-
AuditTrail,
|
|
6
5
|
AuditQueryParams,
|
|
7
6
|
WorkflowPhase,
|
|
8
7
|
AuditAction,
|
|
@@ -18,7 +17,6 @@ import {
|
|
|
18
17
|
import {
|
|
19
18
|
applyAuditEntryFilters,
|
|
20
19
|
applyAuditPagination,
|
|
21
|
-
generateAuditSummary,
|
|
22
20
|
sortAuditEntriesNewestFirst
|
|
23
21
|
} from './audit-query-helpers';
|
|
24
22
|
import { logAuditEntryToConsole } from './audit-console-logger';
|
|
@@ -58,7 +56,7 @@ import {
|
|
|
58
56
|
* Audit Service for ValidationAuditEntry system
|
|
59
57
|
* Provides comprehensive audit logging throughout the confirmation workflow
|
|
60
58
|
*/
|
|
61
|
-
|
|
59
|
+
class AuditService {
|
|
62
60
|
private static instance: AuditService;
|
|
63
61
|
private auditBuffer: ValidationAuditEntry[] = [];
|
|
64
62
|
private workflowId: string | null = null;
|
|
@@ -383,13 +381,15 @@ export class AuditService {
|
|
|
383
381
|
public async logCaseCreation(
|
|
384
382
|
user: User,
|
|
385
383
|
caseNumber: string,
|
|
386
|
-
caseName: string
|
|
384
|
+
caseName: string,
|
|
385
|
+
renamedFromCaseNumber?: string
|
|
387
386
|
): Promise<void> {
|
|
388
387
|
await this.logEventForUser(user,
|
|
389
388
|
buildCaseCreationAuditParams({
|
|
390
389
|
user,
|
|
391
390
|
caseNumber,
|
|
392
|
-
caseName
|
|
391
|
+
caseName,
|
|
392
|
+
renamedFromCaseNumber
|
|
393
393
|
})
|
|
394
394
|
);
|
|
395
395
|
}
|
|
@@ -721,37 +721,6 @@ export class AuditService {
|
|
|
721
721
|
);
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
-
/**
|
|
725
|
-
* Log user account deletion event
|
|
726
|
-
*/
|
|
727
|
-
public async logAccountDeletion(
|
|
728
|
-
user: User,
|
|
729
|
-
result: AuditResult,
|
|
730
|
-
deletionReason: 'user-requested' | 'admin-initiated' | 'policy-violation' | 'inactive-account' = 'user-requested',
|
|
731
|
-
confirmationMethod: 'uid-email' | 'password' | 'admin-override' = 'uid-email',
|
|
732
|
-
casesCount?: number,
|
|
733
|
-
filesCount?: number,
|
|
734
|
-
dataRetentionPeriod?: number,
|
|
735
|
-
emailNotificationSent?: boolean,
|
|
736
|
-
sessionId?: string,
|
|
737
|
-
errors: string[] = []
|
|
738
|
-
): Promise<void> {
|
|
739
|
-
// Wrapper that extracts user data and calls the simplified version
|
|
740
|
-
return this.logAccountDeletionSimple(
|
|
741
|
-
user.uid,
|
|
742
|
-
user.email || '',
|
|
743
|
-
result,
|
|
744
|
-
deletionReason,
|
|
745
|
-
confirmationMethod,
|
|
746
|
-
casesCount,
|
|
747
|
-
filesCount,
|
|
748
|
-
dataRetentionPeriod,
|
|
749
|
-
emailNotificationSent,
|
|
750
|
-
sessionId,
|
|
751
|
-
errors
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
724
|
/**
|
|
756
725
|
* Log user account deletion event with simplified user data
|
|
757
726
|
*/
|
|
@@ -1011,32 +980,6 @@ export class AuditService {
|
|
|
1011
980
|
return await this.getAuditEntries(queryParams, params?.requestingUser);
|
|
1012
981
|
}
|
|
1013
982
|
|
|
1014
|
-
/**
|
|
1015
|
-
* Get audit trail for a case
|
|
1016
|
-
*/
|
|
1017
|
-
public async getAuditTrail(caseNumber: string): Promise<AuditTrail | null> {
|
|
1018
|
-
try {
|
|
1019
|
-
// Implement retrieval from storage
|
|
1020
|
-
const entries = await this.getAuditEntries({ caseNumber });
|
|
1021
|
-
if (!entries || entries.length === 0) {
|
|
1022
|
-
return null;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
const summary = generateAuditSummary(entries);
|
|
1026
|
-
const workflowId = this.workflowId || `${caseNumber}-archived`;
|
|
1027
|
-
|
|
1028
|
-
return {
|
|
1029
|
-
caseNumber,
|
|
1030
|
-
workflowId,
|
|
1031
|
-
entries,
|
|
1032
|
-
summary
|
|
1033
|
-
};
|
|
1034
|
-
} catch (error) {
|
|
1035
|
-
console.error('🚨 Audit: Failed to get audit trail:', error);
|
|
1036
|
-
return null;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
983
|
/**
|
|
1041
984
|
* Get audit entries based on query parameters
|
|
1042
985
|
*/
|
|
@@ -1143,19 +1086,6 @@ export class AuditService {
|
|
|
1143
1086
|
}
|
|
1144
1087
|
}
|
|
1145
1088
|
|
|
1146
|
-
/**
|
|
1147
|
-
* Clear audit buffer (for testing)
|
|
1148
|
-
*/
|
|
1149
|
-
public clearBuffer(): void {
|
|
1150
|
-
this.auditBuffer = [];
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
/**
|
|
1154
|
-
* Get current buffer size (for monitoring)
|
|
1155
|
-
*/
|
|
1156
|
-
public getBufferSize(): number {
|
|
1157
|
-
return this.auditBuffer.length;
|
|
1158
|
-
}
|
|
1159
1089
|
}
|
|
1160
1090
|
|
|
1161
1091
|
// Export singleton instance
|
|
@@ -6,6 +6,7 @@ interface BuildCaseCreationAuditParamsInput {
|
|
|
6
6
|
user: User;
|
|
7
7
|
caseNumber: string;
|
|
8
8
|
caseName: string;
|
|
9
|
+
renamedFromCaseNumber?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const buildCaseCreationAuditParams = (
|
|
@@ -22,7 +23,9 @@ export const buildCaseCreationAuditParams = (
|
|
|
22
23
|
caseNumber: input.caseNumber,
|
|
23
24
|
workflowPhase: 'casework',
|
|
24
25
|
caseDetails: {
|
|
26
|
+
oldCaseName: input.renamedFromCaseNumber,
|
|
25
27
|
newCaseName: input.caseName,
|
|
28
|
+
createdByRename: Boolean(input.renamedFromCaseNumber),
|
|
26
29
|
createdDate: new Date().toISOString(),
|
|
27
30
|
totalFiles: 0,
|
|
28
31
|
totalAnnotations: 0
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
1
|
+
export { auditService } from './audit.service';
|
|
2
|
+
export { auditExportService } from './audit-export.service';
|
package/app/types/audit.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface ValidationAuditEntry {
|
|
|
44
44
|
* Detailed information for each audit entry
|
|
45
45
|
* Contains action-specific data and metadata
|
|
46
46
|
*/
|
|
47
|
-
|
|
47
|
+
interface AuditDetails {
|
|
48
48
|
// Core identification
|
|
49
49
|
fileName?: string;
|
|
50
50
|
fileType?: AuditFileType;
|
|
@@ -194,9 +194,10 @@ export interface AuditQueryParams {
|
|
|
194
194
|
/**
|
|
195
195
|
* Case management specific audit details
|
|
196
196
|
*/
|
|
197
|
-
|
|
197
|
+
interface CaseAuditDetails {
|
|
198
198
|
oldCaseName?: string;
|
|
199
199
|
newCaseName?: string;
|
|
200
|
+
createdByRename?: boolean;
|
|
200
201
|
totalFiles?: number;
|
|
201
202
|
totalAnnotations?: number;
|
|
202
203
|
confirmedFileNames?: string[];
|
|
@@ -210,7 +211,7 @@ export interface CaseAuditDetails {
|
|
|
210
211
|
/**
|
|
211
212
|
* File operation specific audit details
|
|
212
213
|
*/
|
|
213
|
-
|
|
214
|
+
interface FileAuditDetails {
|
|
214
215
|
fileId?: string;
|
|
215
216
|
originalFileName?: string;
|
|
216
217
|
fileSize: number;
|
|
@@ -225,7 +226,7 @@ export interface FileAuditDetails {
|
|
|
225
226
|
/**
|
|
226
227
|
* Annotation operation specific audit details
|
|
227
228
|
*/
|
|
228
|
-
|
|
229
|
+
interface AnnotationAuditDetails {
|
|
229
230
|
annotationId?: string;
|
|
230
231
|
annotationType?: 'measurement' | 'identification' | 'comparison' | 'note' | 'region';
|
|
231
232
|
annotationData?: unknown; // The actual annotation data structure
|
|
@@ -238,7 +239,7 @@ export interface AnnotationAuditDetails {
|
|
|
238
239
|
/**
|
|
239
240
|
* User session specific audit details
|
|
240
241
|
*/
|
|
241
|
-
|
|
242
|
+
interface SessionAuditDetails {
|
|
242
243
|
sessionId?: string;
|
|
243
244
|
userAgent?: string;
|
|
244
245
|
sessionDuration?: number;
|
|
@@ -249,7 +250,7 @@ export interface SessionAuditDetails {
|
|
|
249
250
|
/**
|
|
250
251
|
* Security incident specific audit details
|
|
251
252
|
*/
|
|
252
|
-
|
|
253
|
+
interface SecurityAuditDetails {
|
|
253
254
|
incidentType?: 'unauthorized-access' | 'data-breach' | 'malware' | 'injection' | 'brute-force' | 'privilege-escalation';
|
|
254
255
|
severity?: 'low' | 'medium' | 'high' | 'critical';
|
|
255
256
|
targetResource?: string;
|
|
@@ -272,7 +273,7 @@ export interface SecurityAuditDetails {
|
|
|
272
273
|
/**
|
|
273
274
|
* User profile and authentication specific audit details
|
|
274
275
|
*/
|
|
275
|
-
|
|
276
|
+
interface UserProfileAuditDetails {
|
|
276
277
|
profileField?: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar' | 'badgeId';
|
|
277
278
|
oldValue?: string;
|
|
278
279
|
newValue?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.4",
|
|
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",
|