@striae-org/striae 3.2.0 → 3.2.2
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/README.md +3 -32
- package/app/components/actions/case-export/core-export.ts +2 -2
- package/app/components/actions/case-export/data-processing.ts +65 -10
- package/app/components/actions/case-export/download-handlers.ts +130 -44
- package/app/components/actions/case-export/metadata-helpers.ts +32 -14
- package/app/components/actions/case-import/annotation-import.ts +2 -2
- package/app/components/actions/case-import/confirmation-import.ts +3 -3
- package/app/components/actions/case-import/image-operations.ts +1 -1
- package/app/components/actions/case-import/orchestrator.ts +4 -4
- package/app/components/actions/case-import/storage-operations.ts +7 -7
- package/app/components/actions/case-import/validation.ts +3 -3
- package/app/components/actions/case-import/zip-processing.ts +3 -3
- package/app/components/actions/case-manage.ts +3 -3
- package/app/components/actions/confirm-export.ts +3 -3
- package/app/components/actions/generate-pdf.ts +3 -3
- package/app/components/actions/image-manage.ts +3 -3
- package/app/components/actions/notes-manage.ts +3 -3
- package/app/components/actions/signout.tsx +1 -1
- package/app/components/audit/user-audit-viewer.tsx +2 -3
- package/app/components/auth/auth-provider.tsx +2 -2
- package/app/components/auth/mfa-enrollment.tsx +3 -3
- package/app/components/auth/mfa-verification.tsx +4 -4
- package/app/components/canvas/box-annotations/box-annotations.tsx +2 -2
- package/app/components/canvas/canvas.tsx +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +1 -1
- package/app/components/form/base-form.tsx +1 -1
- package/app/components/sidebar/case-export/case-export.tsx +15 -15
- package/app/components/sidebar/case-import/case-import.tsx +2 -2
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +1 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +3 -3
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -2
- package/app/components/sidebar/cases/case-sidebar.tsx +27 -19
- package/app/components/sidebar/cases/cases-modal.tsx +1 -1
- package/app/components/sidebar/files/files-modal.tsx +3 -2
- package/app/components/sidebar/notes/notes-sidebar.tsx +3 -3
- package/app/components/sidebar/sidebar-container.tsx +5 -4
- package/app/components/sidebar/sidebar.tsx +2 -2
- package/app/components/sidebar/upload/image-upload-zone.tsx +2 -2
- package/app/components/theme-provider/theme-provider.tsx +1 -1
- package/app/components/user/delete-account.tsx +1 -1
- package/app/components/user/manage-profile.tsx +2 -2
- package/app/components/user/mfa-phone-update.tsx +2 -2
- package/app/contexts/auth.context.ts +1 -1
- package/app/entry.client.tsx +12 -12
- package/app/entry.server.tsx +4 -4
- package/app/hooks/useInactivityTimeout.ts +1 -1
- package/app/root.tsx +3 -3
- package/app/routes/auth/emailActionHandler.tsx +3 -3
- package/app/routes/auth/emailVerification.tsx +3 -3
- package/app/routes/auth/login.tsx +6 -6
- package/app/routes/auth/passwordReset.tsx +3 -3
- package/app/routes/auth/route.ts +1 -1
- package/app/routes/striae/striae.tsx +2 -2
- package/app/services/audit/audit-console-logger.ts +46 -0
- package/app/services/audit/audit-export-csv.ts +126 -0
- package/app/services/audit/audit-export-report.ts +174 -0
- package/app/services/audit/audit-export-signing.ts +85 -0
- package/app/services/audit/audit-export.service.ts +334 -0
- package/app/services/audit/audit-file-type.ts +13 -0
- package/app/services/audit/audit-query-helpers.ts +88 -0
- package/app/services/audit/audit-worker-client.ts +95 -0
- package/app/services/audit/audit.service.ts +990 -0
- package/app/services/audit/builders/audit-entry-builder.ts +32 -0
- package/app/services/audit/builders/audit-event-builders-annotation.ts +150 -0
- package/app/services/audit/builders/audit-event-builders-case-file.ts +249 -0
- package/app/services/audit/builders/audit-event-builders-user-security.ts +449 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +272 -0
- package/app/services/audit/builders/index.ts +40 -0
- package/app/services/audit/index.ts +2 -0
- package/app/types/case.ts +2 -2
- package/app/types/exceljs-bare.d.ts +9 -0
- package/app/types/user.ts +1 -1
- package/app/utils/audit-export-signature.ts +2 -2
- package/app/utils/confirmation-signature.ts +3 -3
- package/app/utils/data-operations.ts +5 -5
- package/app/utils/mfa-phone.ts +1 -1
- package/app/utils/mfa.ts +1 -1
- package/app/utils/permissions.ts +2 -2
- package/functions/[[path]].ts +2 -2
- package/package.json +34 -20
- package/public/vendor/exceljs.LICENSE +22 -0
- package/public/vendor/exceljs.bare.min.js +45 -0
- package/scripts/deploy-all.sh +52 -0
- package/scripts/deploy-config.sh +282 -1
- package/tsconfig.json +18 -8
- package/vite.config.ts +6 -22
- package/worker-configuration.d.ts +4435 -562
- package/workers/audit-worker/package.json +8 -4
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +8 -4
- package/workers/data-worker/src/data-worker.example.ts +3 -3
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +8 -4
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +8 -4
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +8 -4
- package/workers/pdf-worker/src/{generated-assets.ts → assets/generated-assets.ts} +117 -117
- package/workers/pdf-worker/src/{format-striae.ts → formats/format-striae.ts} +535 -535
- package/workers/pdf-worker/src/pdf-worker.example.ts +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +8 -4
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/services/audit-export.service.ts +0 -755
- package/app/services/audit.service.ts +0 -1474
- /package/app/services/{firebase-errors.ts → firebase/errors.ts} +0 -0
- /package/app/services/{firebase.ts → firebase/index.ts} +0 -0
|
@@ -1,1474 +0,0 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
2
|
-
import {
|
|
3
|
-
ValidationAuditEntry,
|
|
4
|
-
CreateAuditEntryParams,
|
|
5
|
-
AuditTrail,
|
|
6
|
-
AuditQueryParams,
|
|
7
|
-
AuditSummary,
|
|
8
|
-
WorkflowPhase,
|
|
9
|
-
AuditAction,
|
|
10
|
-
AuditResult,
|
|
11
|
-
AuditFileType,
|
|
12
|
-
SecurityCheckResults,
|
|
13
|
-
PerformanceMetrics
|
|
14
|
-
} from '~/types';
|
|
15
|
-
import paths from '~/config/config.json';
|
|
16
|
-
import { getDataApiKey } from '~/utils/auth';
|
|
17
|
-
import { generateWorkflowId } from '../utils/id-generator';
|
|
18
|
-
|
|
19
|
-
const AUDIT_WORKER_URL = paths.audit_worker_url;
|
|
20
|
-
|
|
21
|
-
type AnnotationSnapshot = Record<string, unknown> & {
|
|
22
|
-
type?: 'measurement' | 'identification' | 'comparison' | 'note' | 'region';
|
|
23
|
-
position?: { x: number; y: number };
|
|
24
|
-
size?: { width: number; height: number };
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const toAnnotationSnapshot = (value: unknown): AnnotationSnapshot | undefined => {
|
|
28
|
-
if (typeof value !== 'object' || value === null) {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return value as AnnotationSnapshot;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Audit Service for ValidationAuditEntry system
|
|
37
|
-
* Provides comprehensive audit logging throughout the confirmation workflow
|
|
38
|
-
*/
|
|
39
|
-
export class AuditService {
|
|
40
|
-
private static instance: AuditService;
|
|
41
|
-
private auditBuffer: ValidationAuditEntry[] = [];
|
|
42
|
-
private workflowId: string | null = null;
|
|
43
|
-
|
|
44
|
-
private constructor() {}
|
|
45
|
-
|
|
46
|
-
public static getInstance(): AuditService {
|
|
47
|
-
if (!AuditService.instance) {
|
|
48
|
-
AuditService.instance = new AuditService();
|
|
49
|
-
}
|
|
50
|
-
return AuditService.instance;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Initialize a new workflow session with unique ID
|
|
55
|
-
*/
|
|
56
|
-
public startWorkflow(caseNumber: string): string {
|
|
57
|
-
const workflowId = generateWorkflowId(caseNumber);
|
|
58
|
-
this.workflowId = workflowId;
|
|
59
|
-
console.log(`🔍 Audit: Started workflow ${this.workflowId}`);
|
|
60
|
-
return workflowId;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* End current workflow session
|
|
65
|
-
*/
|
|
66
|
-
public endWorkflow(): void {
|
|
67
|
-
if (this.workflowId) {
|
|
68
|
-
console.log(`🔍 Audit: Ended workflow ${this.workflowId}`);
|
|
69
|
-
this.workflowId = null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Create and log an audit entry
|
|
75
|
-
*/
|
|
76
|
-
public async logEvent(params: CreateAuditEntryParams): Promise<void> {
|
|
77
|
-
const startTime = Date.now();
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const auditEntry: ValidationAuditEntry = {
|
|
81
|
-
timestamp: new Date().toISOString(),
|
|
82
|
-
userId: params.userId,
|
|
83
|
-
userEmail: params.userEmail,
|
|
84
|
-
action: params.action,
|
|
85
|
-
result: params.result,
|
|
86
|
-
details: {
|
|
87
|
-
fileName: params.fileName,
|
|
88
|
-
fileType: params.fileType,
|
|
89
|
-
hashValid: params.hashValid,
|
|
90
|
-
validationErrors: params.validationErrors || [],
|
|
91
|
-
caseNumber: params.caseNumber,
|
|
92
|
-
confirmationId: params.confirmationId,
|
|
93
|
-
originalExaminerUid: params.originalExaminerUid,
|
|
94
|
-
reviewingExaminerUid: params.reviewingExaminerUid,
|
|
95
|
-
workflowPhase: params.workflowPhase,
|
|
96
|
-
securityChecks: params.securityChecks,
|
|
97
|
-
performanceMetrics: params.performanceMetrics,
|
|
98
|
-
// Extended detail fields
|
|
99
|
-
caseDetails: params.caseDetails,
|
|
100
|
-
fileDetails: params.fileDetails,
|
|
101
|
-
annotationDetails: params.annotationDetails,
|
|
102
|
-
sessionDetails: params.sessionDetails,
|
|
103
|
-
securityDetails: params.securityDetails
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// Add to buffer for batch processing
|
|
108
|
-
this.auditBuffer.push(auditEntry);
|
|
109
|
-
|
|
110
|
-
// Log to console for immediate feedback
|
|
111
|
-
this.logToConsole(auditEntry);
|
|
112
|
-
|
|
113
|
-
// Persist to storage asynchronously
|
|
114
|
-
await this.persistAuditEntry(auditEntry);
|
|
115
|
-
|
|
116
|
-
const endTime = Date.now();
|
|
117
|
-
console.log(`🔍 Audit: Event logged in ${endTime - startTime}ms`);
|
|
118
|
-
|
|
119
|
-
} catch (error) {
|
|
120
|
-
console.error('🚨 Audit: Failed to log event:', error);
|
|
121
|
-
// Don't throw - audit failures shouldn't break the main workflow
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Log case export event
|
|
127
|
-
*/
|
|
128
|
-
public async logCaseExport(
|
|
129
|
-
user: User,
|
|
130
|
-
caseNumber: string,
|
|
131
|
-
fileName: string,
|
|
132
|
-
result: AuditResult,
|
|
133
|
-
errors: string[] = [],
|
|
134
|
-
performanceMetrics?: PerformanceMetrics,
|
|
135
|
-
exportFormat?: 'json' | 'csv' | 'xlsx' | 'zip',
|
|
136
|
-
protectionEnabled?: boolean,
|
|
137
|
-
signatureDetails?: {
|
|
138
|
-
present?: boolean;
|
|
139
|
-
valid?: boolean;
|
|
140
|
-
keyId?: string;
|
|
141
|
-
}
|
|
142
|
-
): Promise<void> {
|
|
143
|
-
const securityChecks: SecurityCheckResults = {
|
|
144
|
-
selfConfirmationPrevented: false, // Not applicable for exports
|
|
145
|
-
fileIntegrityValid: result === 'success',
|
|
146
|
-
manifestSignaturePresent: signatureDetails?.present,
|
|
147
|
-
manifestSignatureValid: signatureDetails?.valid,
|
|
148
|
-
manifestSignatureKeyId: signatureDetails?.keyId
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
// Determine file type based on format or fallback to filename
|
|
152
|
-
let fileType: AuditFileType = 'case-package';
|
|
153
|
-
if (exportFormat) {
|
|
154
|
-
switch (exportFormat) {
|
|
155
|
-
case 'json':
|
|
156
|
-
fileType = 'json-data';
|
|
157
|
-
break;
|
|
158
|
-
case 'csv':
|
|
159
|
-
case 'xlsx':
|
|
160
|
-
fileType = 'csv-export';
|
|
161
|
-
break;
|
|
162
|
-
case 'zip':
|
|
163
|
-
fileType = 'case-package';
|
|
164
|
-
break;
|
|
165
|
-
default:
|
|
166
|
-
fileType = 'case-package';
|
|
167
|
-
}
|
|
168
|
-
} else {
|
|
169
|
-
// Fallback: extract from filename
|
|
170
|
-
if (fileName.includes('.json')) fileType = 'json-data';
|
|
171
|
-
else if (fileName.includes('.csv') || fileName.includes('.xlsx')) fileType = 'csv-export';
|
|
172
|
-
else fileType = 'case-package';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
await this.logEvent({
|
|
176
|
-
userId: user.uid,
|
|
177
|
-
userEmail: user.email || '',
|
|
178
|
-
action: 'export',
|
|
179
|
-
result,
|
|
180
|
-
fileName,
|
|
181
|
-
fileType,
|
|
182
|
-
validationErrors: errors,
|
|
183
|
-
caseNumber,
|
|
184
|
-
workflowPhase: 'case-export',
|
|
185
|
-
securityChecks,
|
|
186
|
-
performanceMetrics,
|
|
187
|
-
originalExaminerUid: user.uid
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Log case import event
|
|
193
|
-
*/
|
|
194
|
-
public async logCaseImport(
|
|
195
|
-
user: User,
|
|
196
|
-
caseNumber: string,
|
|
197
|
-
fileName: string,
|
|
198
|
-
result: AuditResult,
|
|
199
|
-
hashValid: boolean,
|
|
200
|
-
errors: string[] = [],
|
|
201
|
-
originalExaminerUid?: string,
|
|
202
|
-
performanceMetrics?: PerformanceMetrics,
|
|
203
|
-
exporterUidValidated?: boolean, // Separate flag for validation status
|
|
204
|
-
signatureDetails?: {
|
|
205
|
-
present?: boolean;
|
|
206
|
-
valid?: boolean;
|
|
207
|
-
keyId?: string;
|
|
208
|
-
}
|
|
209
|
-
): Promise<void> {
|
|
210
|
-
const securityChecks: SecurityCheckResults = {
|
|
211
|
-
selfConfirmationPrevented: originalExaminerUid ? originalExaminerUid !== user.uid : false,
|
|
212
|
-
fileIntegrityValid: hashValid,
|
|
213
|
-
exporterUidValidated: exporterUidValidated !== undefined ? exporterUidValidated : !!originalExaminerUid,
|
|
214
|
-
manifestSignaturePresent: signatureDetails?.present,
|
|
215
|
-
manifestSignatureValid: signatureDetails?.valid,
|
|
216
|
-
manifestSignatureKeyId: signatureDetails?.keyId
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
await this.logEvent({
|
|
220
|
-
userId: user.uid,
|
|
221
|
-
userEmail: user.email || '',
|
|
222
|
-
action: 'import',
|
|
223
|
-
result,
|
|
224
|
-
fileName,
|
|
225
|
-
fileType: 'case-package',
|
|
226
|
-
hashValid,
|
|
227
|
-
validationErrors: errors,
|
|
228
|
-
caseNumber,
|
|
229
|
-
workflowPhase: 'case-import',
|
|
230
|
-
securityChecks,
|
|
231
|
-
performanceMetrics,
|
|
232
|
-
originalExaminerUid,
|
|
233
|
-
reviewingExaminerUid: user.uid
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Log confirmation creation event
|
|
239
|
-
*/
|
|
240
|
-
public async logConfirmationCreation(
|
|
241
|
-
user: User,
|
|
242
|
-
caseNumber: string,
|
|
243
|
-
confirmationId: string,
|
|
244
|
-
result: AuditResult,
|
|
245
|
-
errors: string[] = [],
|
|
246
|
-
originalExaminerUid?: string,
|
|
247
|
-
performanceMetrics?: PerformanceMetrics,
|
|
248
|
-
imageFileId?: string,
|
|
249
|
-
originalImageFileName?: string
|
|
250
|
-
): Promise<void> {
|
|
251
|
-
const securityChecks: SecurityCheckResults = {
|
|
252
|
-
selfConfirmationPrevented: false, // Not applicable for confirmation creation
|
|
253
|
-
fileIntegrityValid: true // Confirmation creation doesn't involve file integrity validation
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
await this.logEvent({
|
|
257
|
-
userId: user.uid,
|
|
258
|
-
userEmail: user.email || '',
|
|
259
|
-
action: 'confirm',
|
|
260
|
-
result,
|
|
261
|
-
fileName: `confirmation-${confirmationId}`,
|
|
262
|
-
fileType: 'confirmation-data',
|
|
263
|
-
validationErrors: errors,
|
|
264
|
-
caseNumber,
|
|
265
|
-
confirmationId,
|
|
266
|
-
workflowPhase: 'confirmation',
|
|
267
|
-
securityChecks,
|
|
268
|
-
performanceMetrics,
|
|
269
|
-
originalExaminerUid,
|
|
270
|
-
reviewingExaminerUid: user.uid,
|
|
271
|
-
fileDetails: imageFileId && originalImageFileName ? {
|
|
272
|
-
fileId: imageFileId,
|
|
273
|
-
originalFileName: originalImageFileName,
|
|
274
|
-
fileSize: 0 // Not applicable for confirmation creation
|
|
275
|
-
} : undefined
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Log confirmation export event
|
|
281
|
-
*/
|
|
282
|
-
public async logConfirmationExport(
|
|
283
|
-
user: User,
|
|
284
|
-
caseNumber: string,
|
|
285
|
-
fileName: string,
|
|
286
|
-
confirmationCount: number,
|
|
287
|
-
result: AuditResult,
|
|
288
|
-
errors: string[] = [],
|
|
289
|
-
originalExaminerUid?: string,
|
|
290
|
-
performanceMetrics?: PerformanceMetrics,
|
|
291
|
-
signatureDetails?: {
|
|
292
|
-
present: boolean;
|
|
293
|
-
valid: boolean;
|
|
294
|
-
keyId?: string;
|
|
295
|
-
}
|
|
296
|
-
): Promise<void> {
|
|
297
|
-
const securityChecks: SecurityCheckResults = {
|
|
298
|
-
selfConfirmationPrevented: false, // Not applicable for exports
|
|
299
|
-
fileIntegrityValid: result === 'success',
|
|
300
|
-
manifestSignaturePresent: signatureDetails?.present,
|
|
301
|
-
manifestSignatureValid: signatureDetails?.valid,
|
|
302
|
-
manifestSignatureKeyId: signatureDetails?.keyId
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
await this.logEvent({
|
|
306
|
-
userId: user.uid,
|
|
307
|
-
userEmail: user.email || '',
|
|
308
|
-
action: 'export',
|
|
309
|
-
result,
|
|
310
|
-
fileName,
|
|
311
|
-
fileType: 'confirmation-data',
|
|
312
|
-
validationErrors: errors,
|
|
313
|
-
caseNumber,
|
|
314
|
-
workflowPhase: 'confirmation',
|
|
315
|
-
securityChecks,
|
|
316
|
-
performanceMetrics,
|
|
317
|
-
originalExaminerUid,
|
|
318
|
-
reviewingExaminerUid: user.uid
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Log confirmation import event
|
|
324
|
-
*/
|
|
325
|
-
public async logConfirmationImport(
|
|
326
|
-
user: User,
|
|
327
|
-
caseNumber: string,
|
|
328
|
-
fileName: string,
|
|
329
|
-
result: AuditResult,
|
|
330
|
-
hashValid: boolean,
|
|
331
|
-
confirmationsImported: number,
|
|
332
|
-
errors: string[] = [],
|
|
333
|
-
reviewingExaminerUid?: string,
|
|
334
|
-
performanceMetrics?: PerformanceMetrics,
|
|
335
|
-
exporterUidValidated?: boolean, // Separate flag for validation status
|
|
336
|
-
totalConfirmationsInFile?: number, // Total confirmations in the import file
|
|
337
|
-
signatureDetails?: {
|
|
338
|
-
present: boolean;
|
|
339
|
-
valid: boolean;
|
|
340
|
-
keyId?: string;
|
|
341
|
-
}
|
|
342
|
-
): Promise<void> {
|
|
343
|
-
const securityChecks: SecurityCheckResults = {
|
|
344
|
-
selfConfirmationPrevented: reviewingExaminerUid ? reviewingExaminerUid === user.uid : false,
|
|
345
|
-
fileIntegrityValid: hashValid,
|
|
346
|
-
exporterUidValidated: exporterUidValidated !== undefined ? exporterUidValidated : !!reviewingExaminerUid,
|
|
347
|
-
manifestSignaturePresent: signatureDetails?.present,
|
|
348
|
-
manifestSignatureValid: signatureDetails?.valid,
|
|
349
|
-
manifestSignatureKeyId: signatureDetails?.keyId
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
await this.logEvent({
|
|
353
|
-
userId: user.uid,
|
|
354
|
-
userEmail: user.email || '',
|
|
355
|
-
action: 'import',
|
|
356
|
-
result,
|
|
357
|
-
fileName,
|
|
358
|
-
fileType: 'confirmation-data',
|
|
359
|
-
hashValid,
|
|
360
|
-
validationErrors: errors,
|
|
361
|
-
caseNumber,
|
|
362
|
-
workflowPhase: 'confirmation',
|
|
363
|
-
securityChecks,
|
|
364
|
-
performanceMetrics: performanceMetrics ? {
|
|
365
|
-
...performanceMetrics,
|
|
366
|
-
validationStepsCompleted: confirmationsImported, // Successfully imported
|
|
367
|
-
validationStepsFailed: errors.length
|
|
368
|
-
} : {
|
|
369
|
-
processingTimeMs: 0,
|
|
370
|
-
fileSizeBytes: 0,
|
|
371
|
-
validationStepsCompleted: confirmationsImported, // Successfully imported
|
|
372
|
-
validationStepsFailed: errors.length
|
|
373
|
-
},
|
|
374
|
-
originalExaminerUid: user.uid,
|
|
375
|
-
reviewingExaminerUid: reviewingExaminerUid, // Pass through the reviewing examiner UID
|
|
376
|
-
// Store total confirmations in file using caseDetails
|
|
377
|
-
caseDetails: totalConfirmationsInFile !== undefined ? {
|
|
378
|
-
totalAnnotations: totalConfirmationsInFile // Total confirmations in the import file
|
|
379
|
-
} : undefined
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// =============================================================================
|
|
384
|
-
// COMPREHENSIVE AUDIT LOGGING METHODS
|
|
385
|
-
// =============================================================================
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Log case creation event
|
|
389
|
-
*/
|
|
390
|
-
public async logCaseCreation(
|
|
391
|
-
user: User,
|
|
392
|
-
caseNumber: string,
|
|
393
|
-
caseName: string
|
|
394
|
-
): Promise<void> {
|
|
395
|
-
await this.logEvent({
|
|
396
|
-
userId: user.uid,
|
|
397
|
-
userEmail: user.email || '',
|
|
398
|
-
action: 'case-create',
|
|
399
|
-
result: 'success',
|
|
400
|
-
fileName: `${caseNumber}.case`,
|
|
401
|
-
fileType: 'case-package',
|
|
402
|
-
validationErrors: [],
|
|
403
|
-
caseNumber,
|
|
404
|
-
workflowPhase: 'casework',
|
|
405
|
-
caseDetails: {
|
|
406
|
-
newCaseName: caseName,
|
|
407
|
-
createdDate: new Date().toISOString(),
|
|
408
|
-
totalFiles: 0,
|
|
409
|
-
totalAnnotations: 0
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Log case rename event
|
|
416
|
-
*/
|
|
417
|
-
public async logCaseRename(
|
|
418
|
-
user: User,
|
|
419
|
-
caseNumber: string,
|
|
420
|
-
oldName: string,
|
|
421
|
-
newName: string
|
|
422
|
-
): Promise<void> {
|
|
423
|
-
await this.logEvent({
|
|
424
|
-
userId: user.uid,
|
|
425
|
-
userEmail: user.email || '',
|
|
426
|
-
action: 'case-rename',
|
|
427
|
-
result: 'success',
|
|
428
|
-
fileName: `${caseNumber}.case`,
|
|
429
|
-
fileType: 'case-package',
|
|
430
|
-
validationErrors: [],
|
|
431
|
-
caseNumber,
|
|
432
|
-
workflowPhase: 'casework',
|
|
433
|
-
caseDetails: {
|
|
434
|
-
oldCaseName: oldName,
|
|
435
|
-
newCaseName: newName,
|
|
436
|
-
lastModified: new Date().toISOString()
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Log case deletion event
|
|
443
|
-
*/
|
|
444
|
-
public async logCaseDeletion(
|
|
445
|
-
user: User,
|
|
446
|
-
caseNumber: string,
|
|
447
|
-
caseName: string,
|
|
448
|
-
deleteReason: string,
|
|
449
|
-
backupCreated: boolean = false
|
|
450
|
-
): Promise<void> {
|
|
451
|
-
await this.logEvent({
|
|
452
|
-
userId: user.uid,
|
|
453
|
-
userEmail: user.email || '',
|
|
454
|
-
action: 'case-delete',
|
|
455
|
-
result: 'success',
|
|
456
|
-
fileName: `${caseNumber}.case`,
|
|
457
|
-
fileType: 'case-package',
|
|
458
|
-
validationErrors: [],
|
|
459
|
-
caseNumber,
|
|
460
|
-
workflowPhase: 'casework',
|
|
461
|
-
caseDetails: {
|
|
462
|
-
newCaseName: caseName,
|
|
463
|
-
deleteReason,
|
|
464
|
-
backupCreated,
|
|
465
|
-
lastModified: new Date().toISOString()
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Log file upload event
|
|
472
|
-
*/
|
|
473
|
-
public async logFileUpload(
|
|
474
|
-
user: User,
|
|
475
|
-
fileName: string,
|
|
476
|
-
fileSize: number,
|
|
477
|
-
mimeType: string,
|
|
478
|
-
uploadMethod: 'drag-drop' | 'file-picker' | 'api' | 'import',
|
|
479
|
-
caseNumber: string,
|
|
480
|
-
result: AuditResult = 'success',
|
|
481
|
-
processingTime?: number,
|
|
482
|
-
fileId?: string
|
|
483
|
-
): Promise<void> {
|
|
484
|
-
await this.logEvent({
|
|
485
|
-
userId: user.uid,
|
|
486
|
-
userEmail: user.email || '',
|
|
487
|
-
action: 'file-upload',
|
|
488
|
-
result,
|
|
489
|
-
fileName,
|
|
490
|
-
fileType: this.getFileTypeFromMime(mimeType),
|
|
491
|
-
validationErrors: [],
|
|
492
|
-
caseNumber,
|
|
493
|
-
workflowPhase: 'casework',
|
|
494
|
-
fileDetails: {
|
|
495
|
-
fileId: fileId || undefined,
|
|
496
|
-
originalFileName: fileName,
|
|
497
|
-
fileSize,
|
|
498
|
-
mimeType,
|
|
499
|
-
uploadMethod,
|
|
500
|
-
processingTime,
|
|
501
|
-
thumbnailGenerated: result === 'success' && this.isImageFile(mimeType)
|
|
502
|
-
},
|
|
503
|
-
performanceMetrics: processingTime ? {
|
|
504
|
-
processingTimeMs: processingTime,
|
|
505
|
-
fileSizeBytes: fileSize
|
|
506
|
-
} : undefined
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Log file deletion event
|
|
512
|
-
*/
|
|
513
|
-
public async logFileDeletion(
|
|
514
|
-
user: User,
|
|
515
|
-
fileName: string,
|
|
516
|
-
fileSize: number,
|
|
517
|
-
deleteReason: string,
|
|
518
|
-
caseNumber: string,
|
|
519
|
-
fileId?: string,
|
|
520
|
-
originalFileName?: string
|
|
521
|
-
): Promise<void> {
|
|
522
|
-
await this.logEvent({
|
|
523
|
-
userId: user.uid,
|
|
524
|
-
userEmail: user.email || '',
|
|
525
|
-
action: 'file-delete',
|
|
526
|
-
result: 'success',
|
|
527
|
-
fileName,
|
|
528
|
-
fileType: 'unknown',
|
|
529
|
-
validationErrors: [],
|
|
530
|
-
caseNumber,
|
|
531
|
-
workflowPhase: 'casework',
|
|
532
|
-
fileDetails: {
|
|
533
|
-
fileId: fileId || undefined,
|
|
534
|
-
originalFileName,
|
|
535
|
-
fileSize,
|
|
536
|
-
deleteReason
|
|
537
|
-
}
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Log file access event (e.g., viewing an image)
|
|
543
|
-
*/
|
|
544
|
-
public async logFileAccess(
|
|
545
|
-
user: User,
|
|
546
|
-
fileName: string,
|
|
547
|
-
fileId: string,
|
|
548
|
-
accessMethod: 'direct-url' | 'signed-url' | 'download',
|
|
549
|
-
caseNumber: string,
|
|
550
|
-
result: AuditResult = 'success',
|
|
551
|
-
processingTime?: number,
|
|
552
|
-
accessReason?: string,
|
|
553
|
-
originalFileName?: string
|
|
554
|
-
): Promise<void> {
|
|
555
|
-
await this.logEvent({
|
|
556
|
-
userId: user.uid,
|
|
557
|
-
userEmail: user.email || '',
|
|
558
|
-
action: 'file-access',
|
|
559
|
-
result,
|
|
560
|
-
fileName,
|
|
561
|
-
fileType: 'image-file', // Most file access in Striae is for images
|
|
562
|
-
validationErrors: result === 'failure' ? ['File access failed'] : [],
|
|
563
|
-
caseNumber,
|
|
564
|
-
workflowPhase: 'casework',
|
|
565
|
-
fileDetails: {
|
|
566
|
-
fileId,
|
|
567
|
-
originalFileName,
|
|
568
|
-
fileSize: 0, // File size not available for access events
|
|
569
|
-
uploadMethod: accessMethod,
|
|
570
|
-
processingTime,
|
|
571
|
-
sourceLocation: accessReason || 'Image viewer'
|
|
572
|
-
},
|
|
573
|
-
performanceMetrics: processingTime ? {
|
|
574
|
-
processingTimeMs: processingTime,
|
|
575
|
-
fileSizeBytes: 0
|
|
576
|
-
} : undefined
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Log annotation creation event
|
|
582
|
-
*/
|
|
583
|
-
public async logAnnotationCreate(
|
|
584
|
-
user: User,
|
|
585
|
-
annotationId: string,
|
|
586
|
-
annotationType: 'measurement' | 'identification' | 'comparison' | 'note' | 'region',
|
|
587
|
-
annotationData: unknown,
|
|
588
|
-
caseNumber: string,
|
|
589
|
-
tool?: string,
|
|
590
|
-
imageFileId?: string,
|
|
591
|
-
originalImageFileName?: string
|
|
592
|
-
): Promise<void> {
|
|
593
|
-
const annotationSnapshot = toAnnotationSnapshot(annotationData);
|
|
594
|
-
|
|
595
|
-
await this.logEvent({
|
|
596
|
-
userId: user.uid,
|
|
597
|
-
userEmail: user.email || '',
|
|
598
|
-
action: 'annotation-create',
|
|
599
|
-
result: 'success',
|
|
600
|
-
fileName: `annotation-${annotationId}.json`,
|
|
601
|
-
fileType: 'json-data',
|
|
602
|
-
validationErrors: [],
|
|
603
|
-
caseNumber,
|
|
604
|
-
workflowPhase: 'casework',
|
|
605
|
-
annotationDetails: {
|
|
606
|
-
annotationId,
|
|
607
|
-
annotationType,
|
|
608
|
-
annotationData,
|
|
609
|
-
tool,
|
|
610
|
-
canvasPosition: annotationSnapshot?.position,
|
|
611
|
-
annotationSize: annotationSnapshot?.size
|
|
612
|
-
},
|
|
613
|
-
fileDetails: imageFileId || originalImageFileName ? {
|
|
614
|
-
fileId: imageFileId,
|
|
615
|
-
originalFileName: originalImageFileName,
|
|
616
|
-
fileSize: 0, // Not available for image annotations
|
|
617
|
-
mimeType: 'image/*', // Generic image type
|
|
618
|
-
uploadMethod: 'api'
|
|
619
|
-
} : undefined
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Log annotation edit event
|
|
625
|
-
*/
|
|
626
|
-
public async logAnnotationEdit(
|
|
627
|
-
user: User,
|
|
628
|
-
annotationId: string,
|
|
629
|
-
previousValue: unknown,
|
|
630
|
-
newValue: unknown,
|
|
631
|
-
caseNumber: string,
|
|
632
|
-
tool?: string,
|
|
633
|
-
imageFileId?: string,
|
|
634
|
-
originalImageFileName?: string
|
|
635
|
-
): Promise<void> {
|
|
636
|
-
const newValueSnapshot = toAnnotationSnapshot(newValue);
|
|
637
|
-
|
|
638
|
-
await this.logEvent({
|
|
639
|
-
userId: user.uid,
|
|
640
|
-
userEmail: user.email || '',
|
|
641
|
-
action: 'annotation-edit',
|
|
642
|
-
result: 'success',
|
|
643
|
-
fileName: `annotation-${annotationId}.json`,
|
|
644
|
-
fileType: 'json-data',
|
|
645
|
-
validationErrors: [],
|
|
646
|
-
caseNumber,
|
|
647
|
-
workflowPhase: 'casework',
|
|
648
|
-
annotationDetails: {
|
|
649
|
-
annotationId,
|
|
650
|
-
annotationType: newValueSnapshot?.type,
|
|
651
|
-
annotationData: newValue,
|
|
652
|
-
previousValue,
|
|
653
|
-
tool
|
|
654
|
-
},
|
|
655
|
-
fileDetails: imageFileId || originalImageFileName ? {
|
|
656
|
-
fileId: imageFileId,
|
|
657
|
-
originalFileName: originalImageFileName,
|
|
658
|
-
fileSize: 0, // Not available for image annotations
|
|
659
|
-
mimeType: 'image/*', // Generic image type
|
|
660
|
-
uploadMethod: 'api'
|
|
661
|
-
} : undefined
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* Log annotation deletion event
|
|
667
|
-
*/
|
|
668
|
-
public async logAnnotationDelete(
|
|
669
|
-
user: User,
|
|
670
|
-
annotationId: string,
|
|
671
|
-
annotationData: unknown,
|
|
672
|
-
caseNumber: string,
|
|
673
|
-
deleteReason?: string,
|
|
674
|
-
imageFileId?: string,
|
|
675
|
-
originalImageFileName?: string
|
|
676
|
-
): Promise<void> {
|
|
677
|
-
const annotationSnapshot = toAnnotationSnapshot(annotationData);
|
|
678
|
-
|
|
679
|
-
await this.logEvent({
|
|
680
|
-
userId: user.uid,
|
|
681
|
-
userEmail: user.email || '',
|
|
682
|
-
action: 'annotation-delete',
|
|
683
|
-
result: 'success',
|
|
684
|
-
fileName: `annotation-${annotationId}.json`,
|
|
685
|
-
fileType: 'json-data',
|
|
686
|
-
validationErrors: [],
|
|
687
|
-
caseNumber,
|
|
688
|
-
workflowPhase: 'casework',
|
|
689
|
-
annotationDetails: {
|
|
690
|
-
annotationId,
|
|
691
|
-
annotationType: annotationSnapshot?.type,
|
|
692
|
-
annotationData,
|
|
693
|
-
tool: deleteReason
|
|
694
|
-
},
|
|
695
|
-
fileDetails: imageFileId || originalImageFileName ? {
|
|
696
|
-
fileId: imageFileId,
|
|
697
|
-
originalFileName: originalImageFileName,
|
|
698
|
-
fileSize: 0, // Not available for image annotations
|
|
699
|
-
mimeType: 'image/*', // Generic image type
|
|
700
|
-
uploadMethod: 'api'
|
|
701
|
-
} : undefined
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Log user login event
|
|
707
|
-
*/
|
|
708
|
-
public async logUserLogin(
|
|
709
|
-
user: User,
|
|
710
|
-
sessionId: string,
|
|
711
|
-
loginMethod: 'firebase' | 'sso' | 'api-key' | 'manual',
|
|
712
|
-
userAgent?: string
|
|
713
|
-
): Promise<void> {
|
|
714
|
-
await this.logEvent({
|
|
715
|
-
userId: user.uid,
|
|
716
|
-
userEmail: user.email || '',
|
|
717
|
-
action: 'user-login',
|
|
718
|
-
result: 'success',
|
|
719
|
-
fileName: `session-${sessionId}.log`,
|
|
720
|
-
fileType: 'log-file',
|
|
721
|
-
validationErrors: [],
|
|
722
|
-
workflowPhase: 'user-management',
|
|
723
|
-
sessionDetails: {
|
|
724
|
-
sessionId,
|
|
725
|
-
userAgent,
|
|
726
|
-
loginMethod
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* Log user logout event
|
|
733
|
-
*/
|
|
734
|
-
public async logUserLogout(
|
|
735
|
-
user: User,
|
|
736
|
-
sessionId: string,
|
|
737
|
-
sessionDuration: number,
|
|
738
|
-
logoutReason: 'user-initiated' | 'timeout' | 'security' | 'error'
|
|
739
|
-
): Promise<void> {
|
|
740
|
-
await this.logEvent({
|
|
741
|
-
userId: user.uid,
|
|
742
|
-
userEmail: user.email || '',
|
|
743
|
-
action: 'user-logout',
|
|
744
|
-
result: 'success',
|
|
745
|
-
fileName: `session-${sessionId}.log`,
|
|
746
|
-
fileType: 'log-file',
|
|
747
|
-
validationErrors: [],
|
|
748
|
-
workflowPhase: 'user-management',
|
|
749
|
-
sessionDetails: {
|
|
750
|
-
sessionId,
|
|
751
|
-
sessionDuration,
|
|
752
|
-
logoutReason
|
|
753
|
-
}
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Log user profile update event
|
|
759
|
-
*/
|
|
760
|
-
public async logUserProfileUpdate(
|
|
761
|
-
user: User,
|
|
762
|
-
profileField: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar',
|
|
763
|
-
oldValue: string,
|
|
764
|
-
newValue: string,
|
|
765
|
-
result: AuditResult,
|
|
766
|
-
sessionId?: string,
|
|
767
|
-
errors: string[] = []
|
|
768
|
-
): Promise<void> {
|
|
769
|
-
await this.logEvent({
|
|
770
|
-
userId: user.uid,
|
|
771
|
-
userEmail: user.email || '',
|
|
772
|
-
action: 'user-profile-update',
|
|
773
|
-
result,
|
|
774
|
-
fileName: `profile-update-${profileField}.log`,
|
|
775
|
-
fileType: 'log-file',
|
|
776
|
-
validationErrors: errors,
|
|
777
|
-
workflowPhase: 'user-management',
|
|
778
|
-
sessionDetails: sessionId ? {
|
|
779
|
-
sessionId
|
|
780
|
-
} : undefined,
|
|
781
|
-
userProfileDetails: {
|
|
782
|
-
profileField,
|
|
783
|
-
oldValue,
|
|
784
|
-
newValue
|
|
785
|
-
}
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* Log password reset event
|
|
791
|
-
*/
|
|
792
|
-
public async logPasswordReset(
|
|
793
|
-
userEmail: string,
|
|
794
|
-
resetMethod: 'email' | 'sms' | 'security-questions' | 'admin-reset',
|
|
795
|
-
result: AuditResult,
|
|
796
|
-
resetToken?: string,
|
|
797
|
-
verificationMethod?: 'email-link' | 'sms-code' | 'totp' | 'backup-codes',
|
|
798
|
-
verificationAttempts?: number,
|
|
799
|
-
passwordComplexityMet?: boolean,
|
|
800
|
-
previousPasswordReused?: boolean,
|
|
801
|
-
sessionId?: string,
|
|
802
|
-
errors: string[] = []
|
|
803
|
-
): Promise<void> {
|
|
804
|
-
// For password resets, we might not have the full user object yet
|
|
805
|
-
const userId = ''; // No user ID available during password reset
|
|
806
|
-
|
|
807
|
-
await this.logEvent({
|
|
808
|
-
userId,
|
|
809
|
-
userEmail,
|
|
810
|
-
action: 'user-password-reset',
|
|
811
|
-
result,
|
|
812
|
-
fileName: `password-reset-${resetMethod}.log`,
|
|
813
|
-
fileType: 'log-file',
|
|
814
|
-
validationErrors: errors,
|
|
815
|
-
workflowPhase: 'user-management',
|
|
816
|
-
sessionDetails: sessionId ? {
|
|
817
|
-
sessionId
|
|
818
|
-
} : undefined,
|
|
819
|
-
userProfileDetails: {
|
|
820
|
-
resetMethod,
|
|
821
|
-
resetToken: resetToken ? `***${resetToken.slice(-4)}` : undefined, // Only store last 4 chars
|
|
822
|
-
verificationMethod,
|
|
823
|
-
verificationAttempts,
|
|
824
|
-
passwordComplexityMet,
|
|
825
|
-
previousPasswordReused
|
|
826
|
-
}
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
* Log user account deletion event
|
|
832
|
-
*/
|
|
833
|
-
public async logAccountDeletion(
|
|
834
|
-
user: User,
|
|
835
|
-
result: AuditResult,
|
|
836
|
-
deletionReason: 'user-requested' | 'admin-initiated' | 'policy-violation' | 'inactive-account' = 'user-requested',
|
|
837
|
-
confirmationMethod: 'uid-email' | 'password' | 'admin-override' = 'uid-email',
|
|
838
|
-
casesCount?: number,
|
|
839
|
-
filesCount?: number,
|
|
840
|
-
dataRetentionPeriod?: number,
|
|
841
|
-
emailNotificationSent?: boolean,
|
|
842
|
-
sessionId?: string,
|
|
843
|
-
errors: string[] = []
|
|
844
|
-
): Promise<void> {
|
|
845
|
-
// Wrapper that extracts user data and calls the simplified version
|
|
846
|
-
return this.logAccountDeletionSimple(
|
|
847
|
-
user.uid,
|
|
848
|
-
user.email || '',
|
|
849
|
-
result,
|
|
850
|
-
deletionReason,
|
|
851
|
-
confirmationMethod,
|
|
852
|
-
casesCount,
|
|
853
|
-
filesCount,
|
|
854
|
-
dataRetentionPeriod,
|
|
855
|
-
emailNotificationSent,
|
|
856
|
-
sessionId,
|
|
857
|
-
errors
|
|
858
|
-
);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
/**
|
|
862
|
-
* Log user account deletion event with simplified user data
|
|
863
|
-
*/
|
|
864
|
-
public async logAccountDeletionSimple(
|
|
865
|
-
userId: string,
|
|
866
|
-
userEmail: string,
|
|
867
|
-
result: AuditResult,
|
|
868
|
-
deletionReason: 'user-requested' | 'admin-initiated' | 'policy-violation' | 'inactive-account' = 'user-requested',
|
|
869
|
-
confirmationMethod: 'uid-email' | 'password' | 'admin-override' = 'uid-email',
|
|
870
|
-
casesCount?: number,
|
|
871
|
-
filesCount?: number,
|
|
872
|
-
dataRetentionPeriod?: number,
|
|
873
|
-
emailNotificationSent?: boolean,
|
|
874
|
-
sessionId?: string,
|
|
875
|
-
errors: string[] = []
|
|
876
|
-
): Promise<void> {
|
|
877
|
-
await this.logEvent({
|
|
878
|
-
userId,
|
|
879
|
-
userEmail: userEmail || '',
|
|
880
|
-
action: 'user-account-delete',
|
|
881
|
-
result,
|
|
882
|
-
fileName: `account-deletion-${userId}.log`,
|
|
883
|
-
fileType: 'log-file',
|
|
884
|
-
validationErrors: errors,
|
|
885
|
-
workflowPhase: 'user-management',
|
|
886
|
-
sessionDetails: sessionId ? {
|
|
887
|
-
sessionId,
|
|
888
|
-
} : undefined,
|
|
889
|
-
userProfileDetails: {
|
|
890
|
-
deletionReason,
|
|
891
|
-
confirmationMethod,
|
|
892
|
-
casesCount,
|
|
893
|
-
filesCount,
|
|
894
|
-
dataRetentionPeriod,
|
|
895
|
-
emailNotificationSent
|
|
896
|
-
}
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
/**
|
|
901
|
-
* Log user registration/creation event
|
|
902
|
-
*/
|
|
903
|
-
public async logUserRegistration(
|
|
904
|
-
user: User,
|
|
905
|
-
firstName: string,
|
|
906
|
-
lastName: string,
|
|
907
|
-
company: string,
|
|
908
|
-
registrationMethod: 'email-password' | 'sso' | 'admin-created' | 'api',
|
|
909
|
-
userAgent?: string,
|
|
910
|
-
sessionId?: string
|
|
911
|
-
): Promise<void> {
|
|
912
|
-
await this.logEvent({
|
|
913
|
-
userId: user.uid,
|
|
914
|
-
userEmail: user.email || '',
|
|
915
|
-
action: 'user-registration',
|
|
916
|
-
result: 'success',
|
|
917
|
-
fileName: `registration-${user.uid}.log`,
|
|
918
|
-
fileType: 'log-file',
|
|
919
|
-
validationErrors: [],
|
|
920
|
-
workflowPhase: 'user-management',
|
|
921
|
-
sessionDetails: sessionId ? {
|
|
922
|
-
sessionId,
|
|
923
|
-
userAgent
|
|
924
|
-
} : { userAgent },
|
|
925
|
-
userProfileDetails: {
|
|
926
|
-
registrationMethod,
|
|
927
|
-
firstName,
|
|
928
|
-
lastName,
|
|
929
|
-
company,
|
|
930
|
-
emailVerificationRequired: true,
|
|
931
|
-
mfaEnrollmentRequired: true
|
|
932
|
-
}
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
/**
|
|
937
|
-
* Log successful MFA enrollment event
|
|
938
|
-
*/
|
|
939
|
-
public async logMfaEnrollment(
|
|
940
|
-
user: User,
|
|
941
|
-
phoneNumber: string,
|
|
942
|
-
mfaMethod: 'sms' | 'totp' | 'hardware-key',
|
|
943
|
-
result: AuditResult,
|
|
944
|
-
enrollmentAttempts?: number,
|
|
945
|
-
sessionId?: string,
|
|
946
|
-
userAgent?: string,
|
|
947
|
-
errors: string[] = []
|
|
948
|
-
): Promise<void> {
|
|
949
|
-
// Mask phone number for privacy (show only last 4 digits)
|
|
950
|
-
const maskedPhone = phoneNumber.length > 4
|
|
951
|
-
? `***-***-${phoneNumber.slice(-4)}`
|
|
952
|
-
: '***-***-****';
|
|
953
|
-
|
|
954
|
-
await this.logEvent({
|
|
955
|
-
userId: user.uid,
|
|
956
|
-
userEmail: user.email || '',
|
|
957
|
-
action: 'mfa-enrollment',
|
|
958
|
-
result,
|
|
959
|
-
fileName: `mfa-enrollment-${user.uid}.log`,
|
|
960
|
-
fileType: 'log-file',
|
|
961
|
-
validationErrors: errors,
|
|
962
|
-
workflowPhase: 'user-management',
|
|
963
|
-
sessionDetails: sessionId ? {
|
|
964
|
-
sessionId,
|
|
965
|
-
userAgent
|
|
966
|
-
} : { userAgent },
|
|
967
|
-
securityDetails: {
|
|
968
|
-
mfaMethod,
|
|
969
|
-
phoneNumber: maskedPhone,
|
|
970
|
-
enrollmentAttempts,
|
|
971
|
-
enrollmentDate: new Date().toISOString(),
|
|
972
|
-
mandatoryEnrollment: true,
|
|
973
|
-
backupCodesGenerated: false // SMS doesn't generate backup codes
|
|
974
|
-
}
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
/**
|
|
979
|
-
* Log MFA authentication/verification event
|
|
980
|
-
*/
|
|
981
|
-
public async logMfaAuthentication(
|
|
982
|
-
user: User,
|
|
983
|
-
mfaMethod: 'sms' | 'totp' | 'hardware-key',
|
|
984
|
-
result: AuditResult,
|
|
985
|
-
verificationAttempts?: number,
|
|
986
|
-
sessionId?: string,
|
|
987
|
-
userAgent?: string,
|
|
988
|
-
errors: string[] = []
|
|
989
|
-
): Promise<void> {
|
|
990
|
-
await this.logEvent({
|
|
991
|
-
userId: user.uid,
|
|
992
|
-
userEmail: user.email || '',
|
|
993
|
-
action: 'mfa-authentication',
|
|
994
|
-
result,
|
|
995
|
-
fileName: `mfa-auth-${sessionId || Date.now()}.log`,
|
|
996
|
-
fileType: 'log-file',
|
|
997
|
-
validationErrors: errors,
|
|
998
|
-
workflowPhase: 'user-management',
|
|
999
|
-
sessionDetails: sessionId ? {
|
|
1000
|
-
sessionId,
|
|
1001
|
-
userAgent
|
|
1002
|
-
} : { userAgent },
|
|
1003
|
-
securityDetails: {
|
|
1004
|
-
mfaMethod,
|
|
1005
|
-
verificationAttempts,
|
|
1006
|
-
authenticationDate: new Date().toISOString(),
|
|
1007
|
-
loginFlowStep: 'second-factor'
|
|
1008
|
-
}
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
/**
|
|
1013
|
-
* Log email verification event
|
|
1014
|
-
*/
|
|
1015
|
-
public async logEmailVerification(
|
|
1016
|
-
user: User,
|
|
1017
|
-
result: AuditResult,
|
|
1018
|
-
verificationMethod: 'email-link' | 'admin-verification',
|
|
1019
|
-
verificationAttempts?: number,
|
|
1020
|
-
sessionId?: string,
|
|
1021
|
-
userAgent?: string,
|
|
1022
|
-
errors: string[] = []
|
|
1023
|
-
): Promise<void> {
|
|
1024
|
-
await this.logEvent({
|
|
1025
|
-
userId: user.uid,
|
|
1026
|
-
userEmail: user.email || '',
|
|
1027
|
-
action: 'email-verification',
|
|
1028
|
-
result,
|
|
1029
|
-
fileName: `email-verification-${user.uid}.log`,
|
|
1030
|
-
fileType: 'log-file',
|
|
1031
|
-
validationErrors: errors,
|
|
1032
|
-
workflowPhase: 'user-management',
|
|
1033
|
-
sessionDetails: sessionId ? {
|
|
1034
|
-
sessionId,
|
|
1035
|
-
userAgent
|
|
1036
|
-
} : { userAgent },
|
|
1037
|
-
userProfileDetails: {
|
|
1038
|
-
verificationMethod,
|
|
1039
|
-
verificationAttempts,
|
|
1040
|
-
verificationDate: new Date().toISOString(),
|
|
1041
|
-
emailVerified: result === 'success'
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* Log email verification event when no authenticated User object is available.
|
|
1048
|
-
*/
|
|
1049
|
-
public async logEmailVerificationByEmail(
|
|
1050
|
-
userEmail: string,
|
|
1051
|
-
result: AuditResult,
|
|
1052
|
-
verificationMethod: 'email-link' | 'admin-verification',
|
|
1053
|
-
verificationAttempts?: number,
|
|
1054
|
-
sessionId?: string,
|
|
1055
|
-
userAgent?: string,
|
|
1056
|
-
errors: string[] = [],
|
|
1057
|
-
userId: string = ''
|
|
1058
|
-
): Promise<void> {
|
|
1059
|
-
await this.logEvent({
|
|
1060
|
-
userId,
|
|
1061
|
-
userEmail,
|
|
1062
|
-
action: 'email-verification',
|
|
1063
|
-
result,
|
|
1064
|
-
fileName: `email-verification-${userId || Date.now()}.log`,
|
|
1065
|
-
fileType: 'log-file',
|
|
1066
|
-
validationErrors: errors,
|
|
1067
|
-
workflowPhase: 'user-management',
|
|
1068
|
-
sessionDetails: sessionId ? {
|
|
1069
|
-
sessionId,
|
|
1070
|
-
userAgent
|
|
1071
|
-
} : { userAgent },
|
|
1072
|
-
userProfileDetails: {
|
|
1073
|
-
verificationMethod,
|
|
1074
|
-
verificationAttempts,
|
|
1075
|
-
verificationDate: new Date().toISOString(),
|
|
1076
|
-
emailVerified: result === 'success'
|
|
1077
|
-
}
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
/**
|
|
1082
|
-
* Mark pending email verification as successful (retroactive)
|
|
1083
|
-
* Called when user completes MFA enrollment, which implies email verification was successful
|
|
1084
|
-
*/
|
|
1085
|
-
public async markEmailVerificationSuccessful(
|
|
1086
|
-
user: User,
|
|
1087
|
-
reason: string = 'MFA enrollment completed',
|
|
1088
|
-
sessionId?: string,
|
|
1089
|
-
userAgent?: string
|
|
1090
|
-
): Promise<void> {
|
|
1091
|
-
await this.logEvent({
|
|
1092
|
-
userId: user.uid,
|
|
1093
|
-
userEmail: user.email || '',
|
|
1094
|
-
action: 'email-verification',
|
|
1095
|
-
result: 'success',
|
|
1096
|
-
fileName: `email-verification-${user.uid}.log`,
|
|
1097
|
-
fileType: 'log-file',
|
|
1098
|
-
validationErrors: [],
|
|
1099
|
-
workflowPhase: 'user-management',
|
|
1100
|
-
sessionDetails: sessionId ? {
|
|
1101
|
-
sessionId,
|
|
1102
|
-
userAgent
|
|
1103
|
-
} : { userAgent },
|
|
1104
|
-
userProfileDetails: {
|
|
1105
|
-
verificationMethod: 'email-link',
|
|
1106
|
-
verificationAttempts: 1,
|
|
1107
|
-
verificationDate: new Date().toISOString(),
|
|
1108
|
-
emailVerified: true,
|
|
1109
|
-
retroactiveVerification: true,
|
|
1110
|
-
retroactiveReason: reason
|
|
1111
|
-
}
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
/**
|
|
1116
|
-
* Log PDF generation event
|
|
1117
|
-
*/
|
|
1118
|
-
public async logPDFGeneration(
|
|
1119
|
-
user: User,
|
|
1120
|
-
fileName: string,
|
|
1121
|
-
caseNumber: string,
|
|
1122
|
-
result: AuditResult,
|
|
1123
|
-
processingTime: number,
|
|
1124
|
-
fileSize?: number,
|
|
1125
|
-
errors: string[] = [],
|
|
1126
|
-
sourceFileId?: string,
|
|
1127
|
-
sourceFileName?: string
|
|
1128
|
-
): Promise<void> {
|
|
1129
|
-
await this.logEvent({
|
|
1130
|
-
userId: user.uid,
|
|
1131
|
-
userEmail: user.email || '',
|
|
1132
|
-
action: 'pdf-generate',
|
|
1133
|
-
result,
|
|
1134
|
-
fileName,
|
|
1135
|
-
fileType: 'pdf-document',
|
|
1136
|
-
validationErrors: errors,
|
|
1137
|
-
caseNumber,
|
|
1138
|
-
workflowPhase: 'casework',
|
|
1139
|
-
performanceMetrics: {
|
|
1140
|
-
processingTimeMs: processingTime,
|
|
1141
|
-
fileSizeBytes: fileSize || 0
|
|
1142
|
-
},
|
|
1143
|
-
fileDetails: sourceFileId && sourceFileName ? {
|
|
1144
|
-
fileId: sourceFileId,
|
|
1145
|
-
originalFileName: sourceFileName,
|
|
1146
|
-
fileSize: 0 // PDF generation doesn't modify source file size
|
|
1147
|
-
} : undefined
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
/**
|
|
1152
|
-
* Log security violation event
|
|
1153
|
-
*/
|
|
1154
|
-
public async logSecurityViolation(
|
|
1155
|
-
user: User | null,
|
|
1156
|
-
incidentType: 'unauthorized-access' | 'data-breach' | 'malware' | 'injection' | 'brute-force' | 'privilege-escalation',
|
|
1157
|
-
severity: 'low' | 'medium' | 'high' | 'critical',
|
|
1158
|
-
description: string,
|
|
1159
|
-
targetResource?: string,
|
|
1160
|
-
blockedBySystem: boolean = true
|
|
1161
|
-
): Promise<void> {
|
|
1162
|
-
await this.logEvent({
|
|
1163
|
-
userId: user?.uid || 'unknown',
|
|
1164
|
-
userEmail: user?.email || 'unknown@system.com',
|
|
1165
|
-
action: 'security-violation',
|
|
1166
|
-
result: blockedBySystem ? 'blocked' : 'failure',
|
|
1167
|
-
fileName: `security-incident-${Date.now()}.log`,
|
|
1168
|
-
fileType: 'log-file',
|
|
1169
|
-
validationErrors: [description],
|
|
1170
|
-
securityDetails: {
|
|
1171
|
-
incidentType,
|
|
1172
|
-
severity,
|
|
1173
|
-
targetResource,
|
|
1174
|
-
blockedBySystem,
|
|
1175
|
-
investigationId: `INV-${Date.now()}`,
|
|
1176
|
-
reportedToAuthorities: severity === 'critical',
|
|
1177
|
-
mitigationSteps: [
|
|
1178
|
-
blockedBySystem ? 'Automatically blocked by system' : 'Manual intervention required'
|
|
1179
|
-
]
|
|
1180
|
-
}
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// =============================================================================
|
|
1185
|
-
// HELPER METHODS
|
|
1186
|
-
// =============================================================================
|
|
1187
|
-
|
|
1188
|
-
/**
|
|
1189
|
-
* Determine file type from MIME type
|
|
1190
|
-
*/
|
|
1191
|
-
private getFileTypeFromMime(mimeType: string): AuditFileType {
|
|
1192
|
-
if (mimeType.startsWith('image/')) return 'image-file';
|
|
1193
|
-
if (mimeType === 'application/pdf') return 'pdf-document';
|
|
1194
|
-
if (mimeType === 'application/json') return 'json-data';
|
|
1195
|
-
if (mimeType === 'text/csv') return 'csv-export';
|
|
1196
|
-
return 'unknown';
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
/**
|
|
1200
|
-
* Check if file is an image
|
|
1201
|
-
*/
|
|
1202
|
-
private isImageFile(mimeType: string): boolean {
|
|
1203
|
-
return mimeType.startsWith('image/');
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
/**
|
|
1207
|
-
* Get audit entries for display (public method for components)
|
|
1208
|
-
*/
|
|
1209
|
-
public async getAuditEntriesForUser(userId: string, params?: {
|
|
1210
|
-
startDate?: string;
|
|
1211
|
-
endDate?: string;
|
|
1212
|
-
caseNumber?: string;
|
|
1213
|
-
action?: AuditAction;
|
|
1214
|
-
result?: AuditResult;
|
|
1215
|
-
workflowPhase?: WorkflowPhase;
|
|
1216
|
-
offset?: number;
|
|
1217
|
-
limit?: number;
|
|
1218
|
-
}): Promise<ValidationAuditEntry[]> {
|
|
1219
|
-
const queryParams: AuditQueryParams = {
|
|
1220
|
-
userId,
|
|
1221
|
-
...params
|
|
1222
|
-
};
|
|
1223
|
-
return await this.getAuditEntries(queryParams);
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
/**
|
|
1227
|
-
* Get audit trail for a case
|
|
1228
|
-
*/
|
|
1229
|
-
public async getAuditTrail(caseNumber: string): Promise<AuditTrail | null> {
|
|
1230
|
-
try {
|
|
1231
|
-
// Implement retrieval from storage
|
|
1232
|
-
const entries = await this.getAuditEntries({ caseNumber });
|
|
1233
|
-
if (!entries || entries.length === 0) {
|
|
1234
|
-
return null;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
const summary = this.generateAuditSummary(entries);
|
|
1238
|
-
const workflowId = this.workflowId || `${caseNumber}-archived`;
|
|
1239
|
-
|
|
1240
|
-
return {
|
|
1241
|
-
caseNumber,
|
|
1242
|
-
workflowId,
|
|
1243
|
-
entries,
|
|
1244
|
-
summary
|
|
1245
|
-
};
|
|
1246
|
-
} catch (error) {
|
|
1247
|
-
console.error('🚨 Audit: Failed to get audit trail:', error);
|
|
1248
|
-
return null;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
/**
|
|
1253
|
-
* Generate audit summary from entries
|
|
1254
|
-
*/
|
|
1255
|
-
private generateAuditSummary(entries: ValidationAuditEntry[]): AuditSummary {
|
|
1256
|
-
const successCount = entries.filter(e => e.result === 'success').length;
|
|
1257
|
-
const failureCount = entries.filter(e => e.result === 'failure').length;
|
|
1258
|
-
const warningCount = entries.filter(e => e.result === 'warning').length;
|
|
1259
|
-
|
|
1260
|
-
const phases = [...new Set(entries
|
|
1261
|
-
.map(e => e.details.workflowPhase)
|
|
1262
|
-
.filter(Boolean))] as WorkflowPhase[];
|
|
1263
|
-
|
|
1264
|
-
const users = [...new Set(entries.map(e => e.userId))];
|
|
1265
|
-
|
|
1266
|
-
const timestamps = entries.map(e => e.timestamp).sort();
|
|
1267
|
-
const securityIncidents = entries.filter(e =>
|
|
1268
|
-
e.result === 'failure' &&
|
|
1269
|
-
(e.details.securityChecks?.selfConfirmationPrevented === true ||
|
|
1270
|
-
!e.details.securityChecks?.fileIntegrityValid)
|
|
1271
|
-
).length;
|
|
1272
|
-
|
|
1273
|
-
return {
|
|
1274
|
-
totalEvents: entries.length,
|
|
1275
|
-
successfulEvents: successCount,
|
|
1276
|
-
failedEvents: failureCount,
|
|
1277
|
-
warningEvents: warningCount,
|
|
1278
|
-
workflowPhases: phases,
|
|
1279
|
-
participatingUsers: users,
|
|
1280
|
-
startTimestamp: timestamps[0] || new Date().toISOString(),
|
|
1281
|
-
endTimestamp: timestamps[timestamps.length - 1] || new Date().toISOString(),
|
|
1282
|
-
complianceStatus: failureCount === 0 ? 'compliant' : 'non-compliant',
|
|
1283
|
-
securityIncidents
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
/**
|
|
1288
|
-
* Get audit entries based on query parameters
|
|
1289
|
-
*/
|
|
1290
|
-
private async getAuditEntries(params: AuditQueryParams): Promise<ValidationAuditEntry[]> {
|
|
1291
|
-
try {
|
|
1292
|
-
// If userId is provided, fetch from server
|
|
1293
|
-
if (params.userId) {
|
|
1294
|
-
const apiKey = await getDataApiKey();
|
|
1295
|
-
const url = new URL(`${AUDIT_WORKER_URL}/audit/`);
|
|
1296
|
-
url.searchParams.set('userId', params.userId);
|
|
1297
|
-
|
|
1298
|
-
if (params.startDate) {
|
|
1299
|
-
url.searchParams.set('startDate', params.startDate);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
if (params.endDate) {
|
|
1303
|
-
url.searchParams.set('endDate', params.endDate);
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
const response = await fetch(url.toString(), {
|
|
1307
|
-
method: 'GET',
|
|
1308
|
-
headers: {
|
|
1309
|
-
'X-Custom-Auth-Key': apiKey
|
|
1310
|
-
}
|
|
1311
|
-
});
|
|
1312
|
-
|
|
1313
|
-
if (response.ok) {
|
|
1314
|
-
const result = await response.json() as { entries: ValidationAuditEntry[]; total: number };
|
|
1315
|
-
let entries = result.entries;
|
|
1316
|
-
|
|
1317
|
-
// Apply client-side filters
|
|
1318
|
-
if (params.caseNumber) {
|
|
1319
|
-
entries = entries.filter(e => e.details.caseNumber === params.caseNumber);
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
if (params.action) {
|
|
1323
|
-
entries = entries.filter(e => e.action === params.action);
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
if (params.result) {
|
|
1327
|
-
entries = entries.filter(e => e.result === params.result);
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
if (params.workflowPhase) {
|
|
1331
|
-
entries = entries.filter(e => e.details.workflowPhase === params.workflowPhase);
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
// Apply pagination
|
|
1335
|
-
if (params.offset || params.limit) {
|
|
1336
|
-
const offset = params.offset || 0;
|
|
1337
|
-
const limit = params.limit || 100;
|
|
1338
|
-
entries = entries.slice(offset, offset + limit);
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
return entries;
|
|
1342
|
-
} else {
|
|
1343
|
-
console.error('🚨 Audit: Failed to fetch entries from server');
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
// Fallback to buffer for backward compatibility
|
|
1348
|
-
let entries = [...this.auditBuffer];
|
|
1349
|
-
|
|
1350
|
-
if (params.caseNumber) {
|
|
1351
|
-
entries = entries.filter(e => e.details.caseNumber === params.caseNumber);
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
if (params.userId) {
|
|
1355
|
-
entries = entries.filter(e => e.userId === params.userId);
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
if (params.action) {
|
|
1359
|
-
entries = entries.filter(e => e.action === params.action);
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
if (params.result) {
|
|
1363
|
-
entries = entries.filter(e => e.result === params.result);
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
if (params.workflowPhase) {
|
|
1367
|
-
entries = entries.filter(e => e.details.workflowPhase === params.workflowPhase);
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
// Sort by timestamp (newest first)
|
|
1371
|
-
entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
1372
|
-
|
|
1373
|
-
// Apply pagination
|
|
1374
|
-
if (params.offset || params.limit) {
|
|
1375
|
-
const offset = params.offset || 0;
|
|
1376
|
-
const limit = params.limit || 100;
|
|
1377
|
-
entries = entries.slice(offset, offset + limit);
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
return entries;
|
|
1381
|
-
} catch (error) {
|
|
1382
|
-
console.error('🚨 Audit: Failed to get audit entries:', error);
|
|
1383
|
-
return [];
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
/**
|
|
1388
|
-
* Persist audit entry to storage
|
|
1389
|
-
*/
|
|
1390
|
-
private async persistAuditEntry(entry: ValidationAuditEntry): Promise<void> {
|
|
1391
|
-
try {
|
|
1392
|
-
// Store to audit worker
|
|
1393
|
-
const apiKey = await getDataApiKey();
|
|
1394
|
-
const url = new URL(`${AUDIT_WORKER_URL}/audit/`);
|
|
1395
|
-
url.searchParams.set('userId', entry.userId);
|
|
1396
|
-
|
|
1397
|
-
const response = await fetch(url.toString(), {
|
|
1398
|
-
method: 'POST',
|
|
1399
|
-
headers: {
|
|
1400
|
-
'Content-Type': 'application/json',
|
|
1401
|
-
'X-Custom-Auth-Key': apiKey
|
|
1402
|
-
},
|
|
1403
|
-
body: JSON.stringify(entry)
|
|
1404
|
-
});
|
|
1405
|
-
|
|
1406
|
-
if (!response.ok) {
|
|
1407
|
-
const errorData = await response.json().catch(() => ({}));
|
|
1408
|
-
console.error('🚨 Audit: Failed to persist entry:', response.status, errorData);
|
|
1409
|
-
} else {
|
|
1410
|
-
const result = await response.json() as { success: boolean; entryCount: number; filename: string };
|
|
1411
|
-
console.log(`🔍 Audit: Entry persisted (${result.entryCount} total entries)`);
|
|
1412
|
-
}
|
|
1413
|
-
} catch (error) {
|
|
1414
|
-
console.error('🚨 Audit: Storage error:', error);
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
/**
|
|
1419
|
-
* Log audit entry to console for development
|
|
1420
|
-
*/
|
|
1421
|
-
private logToConsole(entry: ValidationAuditEntry): void {
|
|
1422
|
-
const icon = entry.result === 'success' ? '✅' :
|
|
1423
|
-
entry.result === 'failure' ? '❌' : '⚠️';
|
|
1424
|
-
|
|
1425
|
-
console.log(
|
|
1426
|
-
`${icon} Audit [${entry.action.toUpperCase()}]: ${entry.details.fileName} ` +
|
|
1427
|
-
`(Case: ${entry.details.caseNumber || 'N/A'}) - ${entry.result.toUpperCase()}`
|
|
1428
|
-
);
|
|
1429
|
-
|
|
1430
|
-
if (entry.details.validationErrors.length > 0) {
|
|
1431
|
-
console.log(' Errors:', entry.details.validationErrors);
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
if (entry.details.securityChecks) {
|
|
1435
|
-
const securityIssues = [];
|
|
1436
|
-
|
|
1437
|
-
// selfConfirmationPrevented: Only check for import actions when self-confirmation was actually prevented
|
|
1438
|
-
if (entry.action === 'import' && entry.details.securityChecks.selfConfirmationPrevented === true) {
|
|
1439
|
-
securityIssues.push('selfConfirmationPrevented');
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
// fileIntegrityValid: false means issue (integrity failed)
|
|
1443
|
-
if (entry.details.securityChecks.fileIntegrityValid === false) {
|
|
1444
|
-
securityIssues.push('fileIntegrityValid');
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// exporterUidValidated: false means issue (validation failed)
|
|
1448
|
-
if (entry.details.securityChecks.exporterUidValidated === false) {
|
|
1449
|
-
securityIssues.push('exporterUidValidated');
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
if (securityIssues.length > 0) {
|
|
1453
|
-
console.warn(' Security Issues:', securityIssues);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
/**
|
|
1459
|
-
* Clear audit buffer (for testing)
|
|
1460
|
-
*/
|
|
1461
|
-
public clearBuffer(): void {
|
|
1462
|
-
this.auditBuffer = [];
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
/**
|
|
1466
|
-
* Get current buffer size (for monitoring)
|
|
1467
|
-
*/
|
|
1468
|
-
public getBufferSize(): number {
|
|
1469
|
-
return this.auditBuffer.length;
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
// Export singleton instance
|
|
1474
|
-
export const auditService = AuditService.getInstance();
|