@striae-org/striae 3.0.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/.env.example +100 -0
- package/LICENSE +190 -0
- package/NOTICE +18 -0
- package/README.md +133 -0
- package/app/components/actions/case-export/core-export.ts +328 -0
- package/app/components/actions/case-export/data-processing.ts +167 -0
- package/app/components/actions/case-export/download-handlers.ts +900 -0
- package/app/components/actions/case-export/index.ts +41 -0
- package/app/components/actions/case-export/metadata-helpers.ts +107 -0
- package/app/components/actions/case-export/types-constants.ts +56 -0
- package/app/components/actions/case-export/validation-utils.ts +25 -0
- package/app/components/actions/case-export.ts +4 -0
- package/app/components/actions/case-import/annotation-import.ts +35 -0
- package/app/components/actions/case-import/confirmation-import.ts +363 -0
- package/app/components/actions/case-import/image-operations.ts +61 -0
- package/app/components/actions/case-import/index.ts +39 -0
- package/app/components/actions/case-import/orchestrator.ts +420 -0
- package/app/components/actions/case-import/storage-operations.ts +270 -0
- package/app/components/actions/case-import/validation.ts +189 -0
- package/app/components/actions/case-import/zip-processing.ts +413 -0
- package/app/components/actions/case-manage.ts +524 -0
- package/app/components/actions/case-review.ts +4 -0
- package/app/components/actions/confirm-export.ts +351 -0
- package/app/components/actions/generate-pdf.ts +210 -0
- package/app/components/actions/image-manage.ts +385 -0
- package/app/components/actions/notes-manage.ts +33 -0
- package/app/components/actions/signout.module.css +15 -0
- package/app/components/actions/signout.tsx +50 -0
- package/app/components/audit/user-audit-viewer.tsx +975 -0
- package/app/components/audit/user-audit.module.css +568 -0
- package/app/components/auth/auth-provider.tsx +78 -0
- package/app/components/auth/mfa-enrollment.module.css +268 -0
- package/app/components/auth/mfa-enrollment.tsx +398 -0
- package/app/components/auth/mfa-verification.module.css +251 -0
- package/app/components/auth/mfa-verification.tsx +295 -0
- package/app/components/button/button.module.css +63 -0
- package/app/components/button/button.tsx +46 -0
- package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
- package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
- package/app/components/canvas/canvas.module.css +314 -0
- package/app/components/canvas/canvas.tsx +449 -0
- package/app/components/canvas/confirmation/confirmation.module.css +187 -0
- package/app/components/canvas/confirmation/confirmation.tsx +214 -0
- package/app/components/colors/colors.module.css +59 -0
- package/app/components/colors/colors.tsx +68 -0
- package/app/components/form/base-form.tsx +21 -0
- package/app/components/form/form-button.tsx +28 -0
- package/app/components/form/form-field.tsx +53 -0
- package/app/components/form/form-message.tsx +17 -0
- package/app/components/form/form-toggle.tsx +23 -0
- package/app/components/form/form.module.css +427 -0
- package/app/components/form/index.ts +6 -0
- package/app/components/icon/icon.module.css +3 -0
- package/app/components/icon/icon.tsx +27 -0
- package/app/components/icon/icons.svg +102 -0
- package/app/components/icon/manifest.json +110 -0
- package/app/components/sidebar/case-export/case-export.module.css +386 -0
- package/app/components/sidebar/case-export/case-export.tsx +317 -0
- package/app/components/sidebar/case-import/case-import.module.css +626 -0
- package/app/components/sidebar/case-import/case-import.tsx +404 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
- package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
- package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
- package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
- package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
- package/app/components/sidebar/case-import/index.ts +18 -0
- package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
- package/app/components/sidebar/cases/cases-modal.module.css +166 -0
- package/app/components/sidebar/cases/cases-modal.tsx +201 -0
- package/app/components/sidebar/cases/cases.module.css +713 -0
- package/app/components/sidebar/files/files-modal.module.css +209 -0
- package/app/components/sidebar/files/files-modal.tsx +239 -0
- package/app/components/sidebar/hash/hash-utility.module.css +366 -0
- package/app/components/sidebar/hash/hash-utility.tsx +982 -0
- package/app/components/sidebar/notes/notes-modal.tsx +51 -0
- package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
- package/app/components/sidebar/notes/notes.module.css +360 -0
- package/app/components/sidebar/sidebar-container.tsx +149 -0
- package/app/components/sidebar/sidebar.module.css +321 -0
- package/app/components/sidebar/sidebar.tsx +215 -0
- package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
- package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
- package/app/components/theme-provider/theme-provider.tsx +131 -0
- package/app/components/theme-provider/theme.ts +155 -0
- package/app/components/toast/toast.module.css +137 -0
- package/app/components/toast/toast.tsx +56 -0
- package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
- package/app/components/toolbar/toolbar.module.css +42 -0
- package/app/components/toolbar/toolbar.tsx +167 -0
- package/app/components/user/delete-account.module.css +274 -0
- package/app/components/user/delete-account.tsx +471 -0
- package/app/components/user/inactivity-warning.module.css +145 -0
- package/app/components/user/inactivity-warning.tsx +84 -0
- package/app/components/user/manage-profile.module.css +190 -0
- package/app/components/user/manage-profile.tsx +253 -0
- package/app/components/user/mfa-phone-update.tsx +739 -0
- package/app/config-example/admin-service.json +13 -0
- package/app/config-example/config.json +17 -0
- package/app/config-example/firebase.ts +21 -0
- package/app/config-example/inactivity.ts +13 -0
- package/app/config-example/meta-config.json +6 -0
- package/app/contexts/auth.context.ts +12 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +44 -0
- package/app/hooks/useInactivityTimeout.ts +110 -0
- package/app/root.tsx +170 -0
- package/app/routes/_index.tsx +16 -0
- package/app/routes/auth/emailActionHandler.module.css +232 -0
- package/app/routes/auth/emailActionHandler.tsx +405 -0
- package/app/routes/auth/emailVerification.tsx +120 -0
- package/app/routes/auth/login.module.css +523 -0
- package/app/routes/auth/login.tsx +654 -0
- package/app/routes/auth/passwordReset.module.css +274 -0
- package/app/routes/auth/passwordReset.tsx +154 -0
- package/app/routes/auth/route.ts +16 -0
- package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
- package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
- package/app/routes/mobile-prevented/route.ts +14 -0
- package/app/routes/striae/striae.module.css +30 -0
- package/app/routes/striae/striae.tsx +417 -0
- package/app/services/audit-export.service.ts +755 -0
- package/app/services/audit.service.ts +1454 -0
- package/app/services/firebase-errors.ts +106 -0
- package/app/services/firebase.ts +15 -0
- package/app/styles/legal-pages.module.css +113 -0
- package/app/styles/root.module.css +146 -0
- package/app/tailwind.css +225 -0
- package/app/types/annotations.ts +45 -0
- package/app/types/audit.ts +301 -0
- package/app/types/case.ts +90 -0
- package/app/types/export.ts +8 -0
- package/app/types/file.ts +30 -0
- package/app/types/import.ts +107 -0
- package/app/types/index.ts +24 -0
- package/app/types/user.ts +38 -0
- package/app/utils/SHA256.ts +461 -0
- package/app/utils/annotation-timestamp.ts +25 -0
- package/app/utils/audit-export-signature.ts +117 -0
- package/app/utils/auth-action-settings.ts +48 -0
- package/app/utils/auth.ts +34 -0
- package/app/utils/batch-operations.ts +135 -0
- package/app/utils/confirmation-signature.ts +193 -0
- package/app/utils/data-operations.ts +871 -0
- package/app/utils/device-detection.ts +5 -0
- package/app/utils/html-sanitizer.ts +80 -0
- package/app/utils/id-generator.ts +36 -0
- package/app/utils/meta.ts +48 -0
- package/app/utils/mfa-phone.ts +97 -0
- package/app/utils/mfa.ts +79 -0
- package/app/utils/password-policy.ts +28 -0
- package/app/utils/permissions.ts +562 -0
- package/app/utils/signature-utils.ts +160 -0
- package/app/utils/style.ts +83 -0
- package/app/utils/version.ts +5 -0
- package/firebase.json +11 -0
- package/functions/[[path]].ts +10 -0
- package/package.json +138 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/publickey.info@striae.org.asc +17 -0
- package/public/.well-known/security.txt +7 -0
- package/public/_headers +28 -0
- package/public/_routes.json +13 -0
- package/public/assets/striae.jpg +0 -0
- package/public/clear.jpg +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-dark.png +0 -0
- package/public/manifest.json +25 -0
- package/public/oin-badge.png +0 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/striae-ascii.txt +10 -0
- package/scripts/deploy-all.sh +100 -0
- package/scripts/deploy-config.sh +940 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-worker-secrets.sh +215 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/install-workers.sh +88 -0
- package/scripts/run-eslint.cjs +35 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/tailwind.config.ts +22 -0
- package/tsconfig.json +33 -0
- package/vite.config.ts +35 -0
- package/worker-configuration.d.ts +7490 -0
- package/workers/audit-worker/package.json +17 -0
- package/workers/audit-worker/src/audit-worker.example.ts +195 -0
- package/workers/audit-worker/worker-configuration.d.ts +7448 -0
- package/workers/audit-worker/wrangler.jsonc.example +29 -0
- package/workers/data-worker/package.json +17 -0
- package/workers/data-worker/src/data-worker.example.ts +267 -0
- package/workers/data-worker/src/signature-utils.ts +79 -0
- package/workers/data-worker/src/signing-payload-utils.ts +290 -0
- package/workers/data-worker/worker-configuration.d.ts +7448 -0
- package/workers/data-worker/wrangler.jsonc.example +30 -0
- package/workers/image-worker/package.json +17 -0
- package/workers/image-worker/src/image-worker.example.ts +180 -0
- package/workers/image-worker/worker-configuration.d.ts +7447 -0
- package/workers/image-worker/wrangler.jsonc.example +22 -0
- package/workers/keys-worker/package.json +17 -0
- package/workers/keys-worker/src/keys.example.ts +66 -0
- package/workers/keys-worker/src/keys.ts +66 -0
- package/workers/keys-worker/worker-configuration.d.ts +7447 -0
- package/workers/keys-worker/wrangler.jsonc.example +22 -0
- package/workers/pdf-worker/package.json +17 -0
- package/workers/pdf-worker/src/format-striae.ts +534 -0
- package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
- package/workers/pdf-worker/src/report-types.ts +69 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
- package/workers/pdf-worker/wrangler.jsonc.example +26 -0
- package/workers/user-worker/package.json +17 -0
- package/workers/user-worker/src/user-worker.example.ts +636 -0
- package/workers/user-worker/worker-configuration.d.ts +7448 -0
- package/workers/user-worker/wrangler.jsonc.example +29 -0
- package/wrangler.toml.example +8 -0
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
import { useState, useEffect, useContext } from 'react';
|
|
2
|
+
import { AuthContext } from '~/contexts/auth.context';
|
|
3
|
+
import { auditService } from '~/services/audit.service';
|
|
4
|
+
import { auditExportService } from '~/services/audit-export.service';
|
|
5
|
+
import { ValidationAuditEntry, AuditAction, AuditResult, AuditTrail, UserData } from '~/types';
|
|
6
|
+
import { getUserData } from '~/utils/permissions';
|
|
7
|
+
import styles from './user-audit.module.css';
|
|
8
|
+
|
|
9
|
+
interface UserAuditViewerProps {
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
caseNumber?: string; // Optional: filter by specific case
|
|
13
|
+
title?: string; // Optional: custom title
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAuditViewerProps) => {
|
|
17
|
+
const { user } = useContext(AuthContext);
|
|
18
|
+
const [auditEntries, setAuditEntries] = useState<ValidationAuditEntry[]>([]);
|
|
19
|
+
const [userData, setUserData] = useState<UserData | null>(null);
|
|
20
|
+
const [loading, setLoading] = useState(false);
|
|
21
|
+
const [error, setError] = useState<string>('');
|
|
22
|
+
const [filterAction, setFilterAction] = useState<AuditAction | 'all'>('all');
|
|
23
|
+
const [filterResult, setFilterResult] = useState<AuditResult | 'all'>('all');
|
|
24
|
+
const [filterCaseNumber, setFilterCaseNumber] = useState<string>('');
|
|
25
|
+
const [caseNumberInput, setCaseNumberInput] = useState<string>('');
|
|
26
|
+
const [dateRange, setDateRange] = useState<'1d' | '7d' | '30d' | '90d' | 'custom'>('1d');
|
|
27
|
+
const [customStartDate, setCustomStartDate] = useState<string>('');
|
|
28
|
+
const [customEndDate, setCustomEndDate] = useState<string>('');
|
|
29
|
+
const [customStartDateInput, setCustomStartDateInput] = useState<string>('');
|
|
30
|
+
const [customEndDateInput, setCustomEndDateInput] = useState<string>('');
|
|
31
|
+
const [auditTrail, setAuditTrail] = useState<AuditTrail | null>(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (isOpen && user) {
|
|
35
|
+
loadAuditData();
|
|
36
|
+
loadUserData();
|
|
37
|
+
}
|
|
38
|
+
}, [isOpen, user, dateRange, customStartDate, customEndDate, filterCaseNumber, caseNumber]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
42
|
+
if (event.key === 'Escape' && isOpen) {
|
|
43
|
+
onClose();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (isOpen) {
|
|
48
|
+
document.addEventListener('keydown', handleEscape);
|
|
49
|
+
return () => {
|
|
50
|
+
document.removeEventListener('keydown', handleEscape);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}, [isOpen, onClose]);
|
|
54
|
+
|
|
55
|
+
const loadUserData = async () => {
|
|
56
|
+
if (!user) return;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const data = await getUserData(user);
|
|
60
|
+
setUserData(data);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Failed to load user data:', error);
|
|
63
|
+
// Don't set error state for user data failure, just log it
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const loadAuditData = async () => {
|
|
68
|
+
if (!user?.uid) return;
|
|
69
|
+
|
|
70
|
+
setLoading(true);
|
|
71
|
+
setError('');
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Calculate date range
|
|
75
|
+
let startDate: string | undefined;
|
|
76
|
+
let endDate: string | undefined;
|
|
77
|
+
|
|
78
|
+
if (dateRange === 'custom') {
|
|
79
|
+
if (customStartDate) {
|
|
80
|
+
startDate = new Date(customStartDate + 'T00:00:00').toISOString();
|
|
81
|
+
}
|
|
82
|
+
if (customEndDate) {
|
|
83
|
+
endDate = new Date(customEndDate + 'T23:59:59').toISOString();
|
|
84
|
+
}
|
|
85
|
+
// If only one custom date is provided, handle it appropriately
|
|
86
|
+
if (customStartDate && !customEndDate) {
|
|
87
|
+
// If only start date, set end date to now
|
|
88
|
+
const endDateObj = new Date();
|
|
89
|
+
endDate = endDateObj.toISOString();
|
|
90
|
+
} else if (!customStartDate && customEndDate) {
|
|
91
|
+
// If only end date, set start date to 30 days before end date
|
|
92
|
+
const startDateObj = new Date(customEndDate + 'T23:59:59');
|
|
93
|
+
startDateObj.setDate(startDateObj.getDate() - 30);
|
|
94
|
+
startDate = startDateObj.toISOString();
|
|
95
|
+
}
|
|
96
|
+
} else if (dateRange === '90d') {
|
|
97
|
+
// For '90d' entries, get last 90 days to avoid loading too much data
|
|
98
|
+
const startDateObj = new Date();
|
|
99
|
+
startDateObj.setDate(startDateObj.getDate() - 90);
|
|
100
|
+
startDate = startDateObj.toISOString();
|
|
101
|
+
|
|
102
|
+
const endDateObj = new Date();
|
|
103
|
+
endDate = endDateObj.toISOString();
|
|
104
|
+
} else {
|
|
105
|
+
// Handle predefined ranges like '1d', '7d', '30d'
|
|
106
|
+
const days = parseInt(dateRange.replace('d', ''));
|
|
107
|
+
const startDateObj = new Date();
|
|
108
|
+
startDateObj.setDate(startDateObj.getDate() - days);
|
|
109
|
+
startDate = startDateObj.toISOString();
|
|
110
|
+
|
|
111
|
+
// Always set end date to now for proper range querying
|
|
112
|
+
const endDateObj = new Date();
|
|
113
|
+
endDate = endDateObj.toISOString();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get audit entries (filtered by case if specified)
|
|
117
|
+
const effectiveCaseNumber = caseNumber || (filterCaseNumber.trim() || undefined);
|
|
118
|
+
const entries = await auditService.getAuditEntriesForUser(user.uid, {
|
|
119
|
+
caseNumber: effectiveCaseNumber,
|
|
120
|
+
startDate,
|
|
121
|
+
endDate,
|
|
122
|
+
limit: effectiveCaseNumber ? 1000 : 500 // More entries for case-specific view
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
setAuditEntries(entries);
|
|
126
|
+
|
|
127
|
+
// If case-specific, create audit trail for enhanced export functionality
|
|
128
|
+
if (effectiveCaseNumber && entries.length > 0) {
|
|
129
|
+
const trail: AuditTrail = {
|
|
130
|
+
caseNumber: effectiveCaseNumber,
|
|
131
|
+
workflowId: `workflow-${effectiveCaseNumber}-${user.uid}`,
|
|
132
|
+
entries,
|
|
133
|
+
summary: {
|
|
134
|
+
totalEvents: entries.length,
|
|
135
|
+
successfulEvents: entries.filter(e => e.result === 'success').length,
|
|
136
|
+
failedEvents: entries.filter(e => e.result === 'failure').length,
|
|
137
|
+
warningEvents: entries.filter(e => e.result === 'warning').length,
|
|
138
|
+
workflowPhases: [...new Set(entries
|
|
139
|
+
.map(e => e.details.workflowPhase)
|
|
140
|
+
.filter(Boolean))] as any[],
|
|
141
|
+
participatingUsers: [...new Set(entries.map(e => e.userId))],
|
|
142
|
+
startTimestamp: entries[entries.length - 1]?.timestamp || new Date().toISOString(),
|
|
143
|
+
endTimestamp: entries[0]?.timestamp || new Date().toISOString(),
|
|
144
|
+
complianceStatus: entries.some(e => e.result === 'failure') ? 'non-compliant' : 'compliant',
|
|
145
|
+
securityIncidents: entries.filter(e => e.action === 'security-violation').length
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
setAuditTrail(trail);
|
|
149
|
+
} else {
|
|
150
|
+
setAuditTrail(null);
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
setError(err instanceof Error ? err.message : 'Failed to load audit data');
|
|
154
|
+
} finally {
|
|
155
|
+
setLoading(false);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleApplyCaseFilter = () => {
|
|
160
|
+
setFilterCaseNumber(caseNumberInput.trim());
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleClearCaseFilter = () => {
|
|
164
|
+
setCaseNumberInput('');
|
|
165
|
+
setFilterCaseNumber('');
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleApplyCustomDateRange = () => {
|
|
169
|
+
setCustomStartDate(customStartDateInput);
|
|
170
|
+
setCustomEndDate(customEndDateInput);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleClearCustomDateRange = () => {
|
|
174
|
+
setCustomStartDateInput('');
|
|
175
|
+
setCustomEndDateInput('');
|
|
176
|
+
setCustomStartDate('');
|
|
177
|
+
setCustomEndDate('');
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const getFilteredEntries = (): ValidationAuditEntry[] => {
|
|
181
|
+
return auditEntries.filter(entry => {
|
|
182
|
+
// Handle consolidation and mapping of actions
|
|
183
|
+
let actionMatch: boolean;
|
|
184
|
+
if (filterAction === 'all') {
|
|
185
|
+
actionMatch = true;
|
|
186
|
+
} else if (filterAction === 'confirmation-create') {
|
|
187
|
+
// Accept both 'confirm' and 'confirmation-create' for this filter
|
|
188
|
+
actionMatch = entry.action === 'confirm' || entry.action === 'confirmation-create';
|
|
189
|
+
} else if (filterAction === 'case-export') {
|
|
190
|
+
// Case exports use legacy 'export' action with 'case-export' workflowPhase
|
|
191
|
+
actionMatch = entry.action === 'export' && entry.details.workflowPhase === 'case-export';
|
|
192
|
+
} else if (filterAction === 'case-import') {
|
|
193
|
+
// Case imports use legacy 'import' action with 'case-import' workflowPhase
|
|
194
|
+
actionMatch = entry.action === 'import' && entry.details.workflowPhase === 'case-import';
|
|
195
|
+
} else if (filterAction === 'confirmation-export') {
|
|
196
|
+
// Confirmation exports use legacy 'export' action with 'confirmation' workflowPhase
|
|
197
|
+
actionMatch = entry.action === 'export' && entry.details.workflowPhase === 'confirmation';
|
|
198
|
+
} else if (filterAction === 'confirmation-import') {
|
|
199
|
+
// Confirmation imports use legacy 'import' action with 'confirmation' workflowPhase
|
|
200
|
+
actionMatch = entry.action === 'import' && entry.details.workflowPhase === 'confirmation';
|
|
201
|
+
} else {
|
|
202
|
+
// Direct action match for all other cases
|
|
203
|
+
actionMatch = entry.action === filterAction;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const resultMatch = filterResult === 'all' || entry.result === filterResult;
|
|
207
|
+
return actionMatch && resultMatch;
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Export functions
|
|
212
|
+
const handleExportCSV = async () => {
|
|
213
|
+
if (!user) return;
|
|
214
|
+
|
|
215
|
+
const filteredEntries = getFilteredEntries();
|
|
216
|
+
const effectiveCaseNumber = caseNumber || filterCaseNumber.trim();
|
|
217
|
+
const identifier = effectiveCaseNumber || user.uid;
|
|
218
|
+
const type = effectiveCaseNumber ? 'case' : 'user';
|
|
219
|
+
const filename = auditExportService.generateFilename(type, identifier, 'csv');
|
|
220
|
+
const exportContext = {
|
|
221
|
+
user,
|
|
222
|
+
scopeType: type,
|
|
223
|
+
scopeIdentifier: identifier,
|
|
224
|
+
caseNumber: effectiveCaseNumber || undefined
|
|
225
|
+
} as const;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
if (auditTrail && effectiveCaseNumber) {
|
|
229
|
+
// Use full audit trail export for case-specific data
|
|
230
|
+
await auditExportService.exportAuditTrailToCSV(auditTrail, filename, exportContext);
|
|
231
|
+
} else {
|
|
232
|
+
// Use regular entry export for user data
|
|
233
|
+
await auditExportService.exportToCSV(filteredEntries, filename, exportContext);
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('Export failed:', error);
|
|
237
|
+
setError('Failed to export audit trail to CSV');
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const handleExportJSON = async () => {
|
|
242
|
+
if (!user) return;
|
|
243
|
+
|
|
244
|
+
const filteredEntries = getFilteredEntries();
|
|
245
|
+
const effectiveCaseNumber = caseNumber || filterCaseNumber.trim();
|
|
246
|
+
const identifier = effectiveCaseNumber || user.uid;
|
|
247
|
+
const type = effectiveCaseNumber ? 'case' : 'user';
|
|
248
|
+
const filename = auditExportService.generateFilename(type, identifier, 'csv'); // Will be converted to .json
|
|
249
|
+
const exportContext = {
|
|
250
|
+
user,
|
|
251
|
+
scopeType: type,
|
|
252
|
+
scopeIdentifier: identifier,
|
|
253
|
+
caseNumber: effectiveCaseNumber || undefined
|
|
254
|
+
} as const;
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
if (auditTrail && effectiveCaseNumber) {
|
|
258
|
+
// Use full audit trail export for case-specific data
|
|
259
|
+
await auditExportService.exportAuditTrailToJSON(auditTrail, filename, exportContext);
|
|
260
|
+
} else {
|
|
261
|
+
// Use regular entry export for user data
|
|
262
|
+
await auditExportService.exportToJSON(filteredEntries, filename, exportContext);
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error('Export failed:', error);
|
|
266
|
+
setError('Failed to export audit trail to JSON');
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const handleGenerateReport = async () => {
|
|
271
|
+
if (!user) return;
|
|
272
|
+
|
|
273
|
+
const filteredEntries = getFilteredEntries();
|
|
274
|
+
const effectiveCaseNumber = caseNumber || filterCaseNumber.trim();
|
|
275
|
+
const identifier = effectiveCaseNumber || user.uid;
|
|
276
|
+
const type = effectiveCaseNumber ? 'case' : 'user';
|
|
277
|
+
const filename = `${type}-audit-report-${identifier}-${new Date().toISOString().split('T')[0]}.txt`;
|
|
278
|
+
const exportContext = {
|
|
279
|
+
user,
|
|
280
|
+
scopeType: type,
|
|
281
|
+
scopeIdentifier: identifier,
|
|
282
|
+
caseNumber: effectiveCaseNumber || undefined
|
|
283
|
+
} as const;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
let reportContent: string;
|
|
287
|
+
|
|
288
|
+
if (auditTrail && effectiveCaseNumber) {
|
|
289
|
+
// Use audit trail report for case-specific data
|
|
290
|
+
reportContent = await auditExportService.generateReportSummary(auditTrail, exportContext);
|
|
291
|
+
} else {
|
|
292
|
+
// Generate user-specific report
|
|
293
|
+
const totalEntries = filteredEntries.length;
|
|
294
|
+
const successfulActions = filteredEntries.filter(e => e.result === 'success').length;
|
|
295
|
+
const failedActions = filteredEntries.filter(e => e.result === 'failure').length;
|
|
296
|
+
|
|
297
|
+
const actionCounts = filteredEntries.reduce((acc, entry) => {
|
|
298
|
+
acc[entry.action] = (acc[entry.action] || 0) + 1;
|
|
299
|
+
return acc;
|
|
300
|
+
}, {} as Record<string, number>);
|
|
301
|
+
|
|
302
|
+
const dateRange = filteredEntries.length > 0 ? {
|
|
303
|
+
earliest: new Date(Math.min(...filteredEntries.map(e => new Date(e.timestamp).getTime()))),
|
|
304
|
+
latest: new Date(Math.max(...filteredEntries.map(e => new Date(e.timestamp).getTime())))
|
|
305
|
+
} : null;
|
|
306
|
+
|
|
307
|
+
reportContent = `${caseNumber ? 'CASE' : 'USER'} AUDIT REPORT
|
|
308
|
+
Generated: ${new Date().toISOString()}
|
|
309
|
+
${caseNumber ? `Case: ${caseNumber}` : `User: ${user.email}`}
|
|
310
|
+
${caseNumber ? '' : `User ID: ${user.uid}`}
|
|
311
|
+
|
|
312
|
+
=== SUMMARY ===
|
|
313
|
+
Total Actions: ${totalEntries}
|
|
314
|
+
Successful: ${successfulActions}
|
|
315
|
+
Failed: ${failedActions}
|
|
316
|
+
Success Rate: ${totalEntries > 0 ? ((successfulActions / totalEntries) * 100).toFixed(1) : 0}%
|
|
317
|
+
|
|
318
|
+
${dateRange ? `Date Range: ${dateRange.earliest.toLocaleDateString()} - ${dateRange.latest.toLocaleDateString()}` : 'No entries found'}
|
|
319
|
+
|
|
320
|
+
=== ACTION BREAKDOWN ===
|
|
321
|
+
${Object.entries(actionCounts)
|
|
322
|
+
.sort(([,a], [,b]) => b - a)
|
|
323
|
+
.map(([action, count]) => `${action}: ${count}`)
|
|
324
|
+
.join('\n')}
|
|
325
|
+
|
|
326
|
+
=== RECENT ACTIVITIES ===
|
|
327
|
+
${filteredEntries.slice(0, 10).map(entry =>
|
|
328
|
+
`${new Date(entry.timestamp).toLocaleString()} | ${entry.action} | ${entry.result}${entry.details.caseNumber ? ` | Case: ${entry.details.caseNumber}` : ''}`
|
|
329
|
+
).join('\n')}
|
|
330
|
+
|
|
331
|
+
Generated by Striae
|
|
332
|
+
`;
|
|
333
|
+
|
|
334
|
+
reportContent = await auditExportService.appendSignedReportIntegrity(
|
|
335
|
+
reportContent,
|
|
336
|
+
exportContext,
|
|
337
|
+
totalEntries
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Create and download the report file
|
|
342
|
+
const blob = new Blob([reportContent], { type: 'text/plain' });
|
|
343
|
+
const url = URL.createObjectURL(blob);
|
|
344
|
+
const a = document.createElement('a');
|
|
345
|
+
a.href = url;
|
|
346
|
+
a.download = filename;
|
|
347
|
+
document.body.appendChild(a);
|
|
348
|
+
a.click();
|
|
349
|
+
document.body.removeChild(a);
|
|
350
|
+
URL.revokeObjectURL(url);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error('Report generation failed:', error);
|
|
353
|
+
setError('Failed to generate audit report');
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const getActionIcon = (action: AuditAction): string => {
|
|
358
|
+
switch (action) {
|
|
359
|
+
// User & Session Management
|
|
360
|
+
case 'user-login': return '🔑';
|
|
361
|
+
case 'user-logout': return '🚪';
|
|
362
|
+
case 'user-profile-update': return '👤';
|
|
363
|
+
case 'user-password-reset': return '🔒';
|
|
364
|
+
// NEW: User Registration & Authentication
|
|
365
|
+
case 'user-registration': return '📝';
|
|
366
|
+
case 'email-verification': return '📧';
|
|
367
|
+
case 'mfa-enrollment': return '🔐';
|
|
368
|
+
case 'mfa-authentication': return '📱';
|
|
369
|
+
|
|
370
|
+
// Case Management
|
|
371
|
+
case 'case-create': return '📂';
|
|
372
|
+
case 'case-rename': return '✏️';
|
|
373
|
+
case 'case-delete': return '🗑️';
|
|
374
|
+
|
|
375
|
+
// Confirmation Workflow
|
|
376
|
+
case 'case-export': return '📤';
|
|
377
|
+
case 'case-import': return '📥';
|
|
378
|
+
case 'confirmation-create': return '✅';
|
|
379
|
+
case 'confirmation-export': return '📤';
|
|
380
|
+
case 'confirmation-import': return '📥';
|
|
381
|
+
|
|
382
|
+
// File Operations
|
|
383
|
+
case 'file-upload': return '⬆️';
|
|
384
|
+
case 'file-delete': return '🗑️';
|
|
385
|
+
case 'file-access': return '👁️';
|
|
386
|
+
|
|
387
|
+
// Annotation Operations
|
|
388
|
+
case 'annotation-create': return '✨';
|
|
389
|
+
case 'annotation-edit': return '✏️';
|
|
390
|
+
case 'annotation-delete': return '❌';
|
|
391
|
+
|
|
392
|
+
// Document Generation
|
|
393
|
+
case 'pdf-generate': return '📄';
|
|
394
|
+
|
|
395
|
+
// Security & Monitoring
|
|
396
|
+
case 'security-violation': return '🚨';
|
|
397
|
+
|
|
398
|
+
// Legacy Actions
|
|
399
|
+
case 'export': return '📤';
|
|
400
|
+
case 'import': return '📥';
|
|
401
|
+
case 'confirm': return '✓';
|
|
402
|
+
|
|
403
|
+
default: return '📄';
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const getStatusIcon = (result: AuditResult): string => {
|
|
408
|
+
switch (result) {
|
|
409
|
+
case 'success': return '✅';
|
|
410
|
+
case 'failure': return '❌';
|
|
411
|
+
case 'warning': return '⚠️';
|
|
412
|
+
case 'blocked': return '🛑';
|
|
413
|
+
case 'pending': return '⏳';
|
|
414
|
+
default: return '❓';
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const formatTimestamp = (timestamp: string): string => {
|
|
419
|
+
return new Date(timestamp).toLocaleString();
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const getDateRangeDisplay = (): string => {
|
|
423
|
+
switch (dateRange) {
|
|
424
|
+
case '90d':
|
|
425
|
+
return 'Last 90 Days';
|
|
426
|
+
case 'custom':
|
|
427
|
+
if (customStartDate && customEndDate) {
|
|
428
|
+
const startFormatted = new Date(customStartDate).toLocaleDateString();
|
|
429
|
+
const endFormatted = new Date(customEndDate).toLocaleDateString();
|
|
430
|
+
return `${startFormatted} - ${endFormatted}`;
|
|
431
|
+
} else if (customStartDate) {
|
|
432
|
+
return `From ${new Date(customStartDate).toLocaleDateString()}`;
|
|
433
|
+
} else if (customEndDate) {
|
|
434
|
+
return `Until ${new Date(customEndDate).toLocaleDateString()}`;
|
|
435
|
+
} else {
|
|
436
|
+
return 'Custom Range';
|
|
437
|
+
}
|
|
438
|
+
default:
|
|
439
|
+
return `Last ${dateRange}`;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Get summary statistics
|
|
444
|
+
const totalEntries = auditEntries.length;
|
|
445
|
+
const successfulEntries = auditEntries.filter(e => e.result === 'success').length;
|
|
446
|
+
const failedEntries = auditEntries.filter(e => e.result === 'failure').length;
|
|
447
|
+
const securityIncidents = auditEntries.filter(e =>
|
|
448
|
+
e.action === 'security-violation'
|
|
449
|
+
).length;
|
|
450
|
+
const loginSessions = auditEntries.filter(e => e.action === 'user-login').length;
|
|
451
|
+
|
|
452
|
+
if (!isOpen) return null;
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<div className={styles.overlay} onClick={(e) => e.target === e.currentTarget && onClose()}>
|
|
456
|
+
<div className={styles.modal}>
|
|
457
|
+
<div className={styles.header}>
|
|
458
|
+
<h2 className={styles.title}>
|
|
459
|
+
{title || (caseNumber ? `Audit Trail - Case ${caseNumber}` : 'My Audit Trail')}
|
|
460
|
+
</h2>
|
|
461
|
+
<div className={styles.headerActions}>
|
|
462
|
+
{auditEntries.length > 0 && (
|
|
463
|
+
<div className={styles.exportButtons}>
|
|
464
|
+
<button
|
|
465
|
+
onClick={handleExportCSV}
|
|
466
|
+
className={styles.exportButton}
|
|
467
|
+
title="CSV - Individual entry log with summary data"
|
|
468
|
+
>
|
|
469
|
+
📊 CSV
|
|
470
|
+
</button>
|
|
471
|
+
<button
|
|
472
|
+
onClick={handleExportJSON}
|
|
473
|
+
className={styles.exportButton}
|
|
474
|
+
title="JSON - Complete log data for version capture and auditing"
|
|
475
|
+
>
|
|
476
|
+
📄 JSON
|
|
477
|
+
</button>
|
|
478
|
+
<button
|
|
479
|
+
onClick={handleGenerateReport}
|
|
480
|
+
className={styles.exportButton}
|
|
481
|
+
title="Summary report only"
|
|
482
|
+
>
|
|
483
|
+
📋 Report
|
|
484
|
+
</button>
|
|
485
|
+
</div>
|
|
486
|
+
)}
|
|
487
|
+
<button className={styles.closeButton} onClick={onClose}>×</button>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<div className={styles.content}>
|
|
492
|
+
{loading && (
|
|
493
|
+
<div className={styles.loading}>
|
|
494
|
+
<div className={styles.spinner}></div>
|
|
495
|
+
<p>Loading your audit trail...this may take a while for longer time ranges</p>
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
498
|
+
|
|
499
|
+
{error && (
|
|
500
|
+
<div className={styles.error}>
|
|
501
|
+
<p>Error: {error}</p>
|
|
502
|
+
<button onClick={loadAuditData} className={styles.retryButton}>
|
|
503
|
+
Retry
|
|
504
|
+
</button>
|
|
505
|
+
</div>
|
|
506
|
+
)}
|
|
507
|
+
|
|
508
|
+
{!loading && !error && (
|
|
509
|
+
<>
|
|
510
|
+
{/* User Information Section */}
|
|
511
|
+
{user && (
|
|
512
|
+
<div className={styles.summary}>
|
|
513
|
+
<h3>User Information</h3>
|
|
514
|
+
<div className={styles.userInfoContent}>
|
|
515
|
+
<div className={styles.userInfoItem}>
|
|
516
|
+
Name: <strong>
|
|
517
|
+
{userData ? `${userData.firstName} ${userData.lastName}` : user.displayName || 'Not provided'}
|
|
518
|
+
</strong>
|
|
519
|
+
</div>
|
|
520
|
+
<div className={styles.userInfoItem}>
|
|
521
|
+
Email: <strong>{user.email || 'Not provided'}</strong>
|
|
522
|
+
</div>
|
|
523
|
+
<div className={styles.userInfoItem}>
|
|
524
|
+
Lab/Company: <strong>{userData?.company || 'Not provided'}</strong>
|
|
525
|
+
</div>
|
|
526
|
+
<div className={styles.userInfoItem}>
|
|
527
|
+
User ID: <strong>{user.uid}</strong>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
)}
|
|
532
|
+
|
|
533
|
+
{/* Summary Section */}
|
|
534
|
+
<div className={styles.summary}>
|
|
535
|
+
<h3>
|
|
536
|
+
{(caseNumber || filterCaseNumber.trim())
|
|
537
|
+
? `Case Activity Summary - ${caseNumber || filterCaseNumber.trim()} (${getDateRangeDisplay()})`
|
|
538
|
+
: `Activity Summary (${getDateRangeDisplay()})`
|
|
539
|
+
}
|
|
540
|
+
</h3>
|
|
541
|
+
<div className={styles.summaryGrid}>
|
|
542
|
+
<div className={styles.summaryItem}>
|
|
543
|
+
<span className={styles.label}>Total Activities:</span>
|
|
544
|
+
<span className={styles.value}>{totalEntries}</span>
|
|
545
|
+
</div>
|
|
546
|
+
<div className={styles.summaryItem}>
|
|
547
|
+
<span className={styles.label}>Successful:</span>
|
|
548
|
+
<span className={styles.value}>{successfulEntries}</span>
|
|
549
|
+
</div>
|
|
550
|
+
<div className={styles.summaryItem}>
|
|
551
|
+
<span className={styles.label}>Failed:</span>
|
|
552
|
+
<span className={styles.value}>{failedEntries}</span>
|
|
553
|
+
</div>
|
|
554
|
+
<div className={styles.summaryItem}>
|
|
555
|
+
<span className={styles.label}>Login Sessions:</span>
|
|
556
|
+
<span className={styles.value}>{loginSessions}</span>
|
|
557
|
+
</div>
|
|
558
|
+
<div className={styles.summaryItem}>
|
|
559
|
+
<span className={styles.label}>Security Incidents:</span>
|
|
560
|
+
<span className={`${styles.value} ${securityIncidents > 0 ? styles.warning : ''}`}>
|
|
561
|
+
{securityIncidents}
|
|
562
|
+
</span>
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
|
|
567
|
+
{/* Filters */}
|
|
568
|
+
<div className={styles.filters}>
|
|
569
|
+
<div className={styles.filterGroup}>
|
|
570
|
+
<label htmlFor="dateRange">Time Period:</label>
|
|
571
|
+
<select
|
|
572
|
+
id="dateRange"
|
|
573
|
+
value={dateRange}
|
|
574
|
+
onChange={(e) => {
|
|
575
|
+
const newRange = e.target.value as '1d' | '7d' | '30d' | '90d' | 'custom';
|
|
576
|
+
setDateRange(newRange);
|
|
577
|
+
// When switching to custom, populate inputs with current applied values
|
|
578
|
+
if (newRange === 'custom') {
|
|
579
|
+
setCustomStartDateInput(customStartDate);
|
|
580
|
+
setCustomEndDateInput(customEndDate);
|
|
581
|
+
}
|
|
582
|
+
}}
|
|
583
|
+
className={styles.filterSelect}
|
|
584
|
+
>
|
|
585
|
+
<option value="1d">Last 24 Hours</option>
|
|
586
|
+
<option value="7d">Last 7 Days</option>
|
|
587
|
+
<option value="30d">Last 30 Days</option>
|
|
588
|
+
<option value="90d">Last 90 Days</option>
|
|
589
|
+
<option value="custom">Custom Range</option>
|
|
590
|
+
</select>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
{/* Custom Date Range Inputs */}
|
|
594
|
+
{dateRange === 'custom' && (
|
|
595
|
+
<div className={styles.customDateRange}>
|
|
596
|
+
<div className={styles.customDateInputs}>
|
|
597
|
+
<div className={styles.filterGroup}>
|
|
598
|
+
<label htmlFor="startDate">Start Date:</label>
|
|
599
|
+
<input
|
|
600
|
+
type="date"
|
|
601
|
+
id="startDate"
|
|
602
|
+
value={customStartDateInput}
|
|
603
|
+
onChange={(e) => setCustomStartDateInput(e.target.value)}
|
|
604
|
+
className={styles.filterInput}
|
|
605
|
+
max={customEndDateInput || new Date().toISOString().split('T')[0]}
|
|
606
|
+
/>
|
|
607
|
+
</div>
|
|
608
|
+
<div className={styles.filterGroup}>
|
|
609
|
+
<label htmlFor="endDate">End Date:</label>
|
|
610
|
+
<input
|
|
611
|
+
type="date"
|
|
612
|
+
id="endDate"
|
|
613
|
+
value={customEndDateInput}
|
|
614
|
+
onChange={(e) => setCustomEndDateInput(e.target.value)}
|
|
615
|
+
className={styles.filterInput}
|
|
616
|
+
min={customStartDateInput}
|
|
617
|
+
max={new Date().toISOString().split('T')[0]}
|
|
618
|
+
/>
|
|
619
|
+
</div>
|
|
620
|
+
<div className={styles.dateRangeButtons}>
|
|
621
|
+
{(customStartDateInput || customEndDateInput) && (
|
|
622
|
+
<button
|
|
623
|
+
type="button"
|
|
624
|
+
onClick={handleApplyCustomDateRange}
|
|
625
|
+
className={styles.filterButton}
|
|
626
|
+
title="Apply custom date range"
|
|
627
|
+
>
|
|
628
|
+
Apply Dates
|
|
629
|
+
</button>
|
|
630
|
+
)}
|
|
631
|
+
{(customStartDate || customEndDate) && (
|
|
632
|
+
<button
|
|
633
|
+
type="button"
|
|
634
|
+
onClick={handleClearCustomDateRange}
|
|
635
|
+
className={styles.clearButton}
|
|
636
|
+
title="Clear custom date range"
|
|
637
|
+
>
|
|
638
|
+
Clear Dates
|
|
639
|
+
</button>
|
|
640
|
+
)}
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
{(customStartDate || customEndDate) && (
|
|
644
|
+
<div className={styles.activeFilter}>
|
|
645
|
+
<small>
|
|
646
|
+
Custom range:
|
|
647
|
+
{customStartDate && <strong> from {new Date(customStartDate).toLocaleDateString()}</strong>}
|
|
648
|
+
{customEndDate && <strong> to {new Date(customEndDate).toLocaleDateString()}</strong>}
|
|
649
|
+
</small>
|
|
650
|
+
</div>
|
|
651
|
+
)}
|
|
652
|
+
</div>
|
|
653
|
+
)}
|
|
654
|
+
|
|
655
|
+
<div className={styles.filterGroup}>
|
|
656
|
+
<label htmlFor="caseFilter">Case Number:</label>
|
|
657
|
+
<div className={styles.inputWithButton}>
|
|
658
|
+
<input
|
|
659
|
+
type="text"
|
|
660
|
+
id="caseFilter"
|
|
661
|
+
value={caseNumberInput}
|
|
662
|
+
onChange={(e) => setCaseNumberInput(e.target.value)}
|
|
663
|
+
className={styles.filterInput}
|
|
664
|
+
placeholder="Enter case number..."
|
|
665
|
+
disabled={!!caseNumber} // Disable if already viewing a specific case
|
|
666
|
+
title={caseNumber ? "Case filter disabled - viewing specific case" : "Enter complete case number and click Filter"}
|
|
667
|
+
onKeyDown={(e) => {
|
|
668
|
+
if (e.key === 'Enter' && caseNumberInput.trim() && !caseNumber) {
|
|
669
|
+
handleApplyCaseFilter();
|
|
670
|
+
}
|
|
671
|
+
}}
|
|
672
|
+
/>
|
|
673
|
+
{!caseNumber && (
|
|
674
|
+
<div className={styles.caseFilterButtons}>
|
|
675
|
+
{caseNumberInput.trim() && (
|
|
676
|
+
<button
|
|
677
|
+
type="button"
|
|
678
|
+
onClick={handleApplyCaseFilter}
|
|
679
|
+
className={styles.filterButton}
|
|
680
|
+
title="Apply case filter"
|
|
681
|
+
>
|
|
682
|
+
Filter
|
|
683
|
+
</button>
|
|
684
|
+
)}
|
|
685
|
+
{filterCaseNumber && (
|
|
686
|
+
<button
|
|
687
|
+
type="button"
|
|
688
|
+
onClick={handleClearCaseFilter}
|
|
689
|
+
className={styles.clearButton}
|
|
690
|
+
title="Clear case filter"
|
|
691
|
+
>
|
|
692
|
+
Clear
|
|
693
|
+
</button>
|
|
694
|
+
)}
|
|
695
|
+
</div>
|
|
696
|
+
)}
|
|
697
|
+
</div>
|
|
698
|
+
{filterCaseNumber && !caseNumber && (
|
|
699
|
+
<div className={styles.activeFilter}>
|
|
700
|
+
<small>Filtering by case: <strong>{filterCaseNumber}</strong></small>
|
|
701
|
+
</div>
|
|
702
|
+
)}
|
|
703
|
+
</div>
|
|
704
|
+
|
|
705
|
+
<div className={styles.filterGroup}>
|
|
706
|
+
<label htmlFor="actionFilter">Activity Type:</label>
|
|
707
|
+
<select
|
|
708
|
+
id="actionFilter"
|
|
709
|
+
value={filterAction}
|
|
710
|
+
onChange={(e) => setFilterAction(e.target.value as AuditAction | 'all')}
|
|
711
|
+
className={styles.filterSelect}
|
|
712
|
+
>
|
|
713
|
+
<option value="all">All Activities</option>
|
|
714
|
+
<optgroup label="User Sessions">
|
|
715
|
+
<option value="user-login">Login</option>
|
|
716
|
+
<option value="user-logout">Logout</option>
|
|
717
|
+
</optgroup>
|
|
718
|
+
<optgroup label="Case Management">
|
|
719
|
+
<option value="case-create">Case Create</option>
|
|
720
|
+
<option value="case-rename">Case Rename</option>
|
|
721
|
+
<option value="case-delete">Case Delete</option>
|
|
722
|
+
<option value="case-export">Case Export</option>
|
|
723
|
+
<option value="case-import">Case Import</option>
|
|
724
|
+
</optgroup>
|
|
725
|
+
<optgroup label="File Operations">
|
|
726
|
+
<option value="file-upload">File Upload</option>
|
|
727
|
+
<option value="file-access">File Access</option>
|
|
728
|
+
<option value="file-delete">File Delete</option>
|
|
729
|
+
</optgroup>
|
|
730
|
+
<optgroup label="Annotations">
|
|
731
|
+
<option value="annotation-create">Annotation Create</option>
|
|
732
|
+
<option value="annotation-edit">Annotation Edit</option>
|
|
733
|
+
<option value="annotation-delete">Annotation Delete</option>
|
|
734
|
+
</optgroup>
|
|
735
|
+
<optgroup label="Confirmation Activity">
|
|
736
|
+
<option value="confirmation-create">Confirmation Create</option>
|
|
737
|
+
<option value="confirmation-export">Confirmation Export</option>
|
|
738
|
+
<option value="confirmation-import">Confirmation Import</option>
|
|
739
|
+
</optgroup>
|
|
740
|
+
<optgroup label="Documents">
|
|
741
|
+
<option value="pdf-generate">PDF Generate</option>
|
|
742
|
+
</optgroup>
|
|
743
|
+
<optgroup label="Security">
|
|
744
|
+
<option value="security-violation">Security Violation</option>
|
|
745
|
+
</optgroup>
|
|
746
|
+
</select>
|
|
747
|
+
</div>
|
|
748
|
+
|
|
749
|
+
<div className={styles.filterGroup}>
|
|
750
|
+
<label htmlFor="resultFilter">Result:</label>
|
|
751
|
+
<select
|
|
752
|
+
id="resultFilter"
|
|
753
|
+
value={filterResult}
|
|
754
|
+
onChange={(e) => setFilterResult(e.target.value as AuditResult | 'all')}
|
|
755
|
+
className={styles.filterSelect}
|
|
756
|
+
>
|
|
757
|
+
<option value="all">All Results</option>
|
|
758
|
+
<option value="success">Success</option>
|
|
759
|
+
<option value="failure">Failure</option>
|
|
760
|
+
<option value="warning">Warning</option>
|
|
761
|
+
<option value="blocked">Blocked</option>
|
|
762
|
+
</select>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
|
|
766
|
+
{/* Entries List */}
|
|
767
|
+
<div className={styles.entriesList}>
|
|
768
|
+
<h3>Activity Log ({getFilteredEntries().length} entries)</h3>
|
|
769
|
+
{getFilteredEntries().length === 0 ? (
|
|
770
|
+
<div className={styles.noEntries}>
|
|
771
|
+
<p>No activities match the current filters.</p>
|
|
772
|
+
</div>
|
|
773
|
+
) : (
|
|
774
|
+
getFilteredEntries().map((entry, index) => (
|
|
775
|
+
<div key={index} className={`${styles.entry} ${styles[entry.result]}`}>
|
|
776
|
+
<div className={styles.entryHeader}>
|
|
777
|
+
<div className={styles.entryIcons}>
|
|
778
|
+
<span className={styles.actionIcon}>{getActionIcon(entry.action)}</span>
|
|
779
|
+
<span className={styles.statusIcon}>{getStatusIcon(entry.result)}</span>
|
|
780
|
+
</div>
|
|
781
|
+
<div className={styles.entryTitle}>
|
|
782
|
+
<span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
|
|
783
|
+
<span className={styles.fileName}>{entry.details.fileName}</span>
|
|
784
|
+
</div>
|
|
785
|
+
<div className={styles.entryTimestamp}>
|
|
786
|
+
{formatTimestamp(entry.timestamp)}
|
|
787
|
+
</div>
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
{/* Basic Details */}
|
|
791
|
+
<div className={styles.entryDetails}>
|
|
792
|
+
{entry.details.caseNumber && (
|
|
793
|
+
<div className={styles.detailRow}>
|
|
794
|
+
<span className={styles.detailLabel}>Case:</span>
|
|
795
|
+
<span className={styles.detailValue}>{entry.details.caseNumber}</span>
|
|
796
|
+
</div>
|
|
797
|
+
)}
|
|
798
|
+
|
|
799
|
+
{entry.result === 'failure' && entry.details.validationErrors.length > 0 && (
|
|
800
|
+
<div className={styles.detailRow}>
|
|
801
|
+
<span className={styles.detailLabel}>Error:</span>
|
|
802
|
+
<span className={styles.detailValue}>{entry.details.validationErrors[0]}</span>
|
|
803
|
+
</div>
|
|
804
|
+
)}
|
|
805
|
+
|
|
806
|
+
{/* Session Details for Login/Logout */}
|
|
807
|
+
{(entry.action === 'user-login' || entry.action === 'user-logout') && entry.details.sessionDetails && (
|
|
808
|
+
<>
|
|
809
|
+
{entry.details.sessionDetails.userAgent && (
|
|
810
|
+
<div className={styles.detailRow}>
|
|
811
|
+
<span className={styles.detailLabel}>User Agent:</span>
|
|
812
|
+
<span className={styles.detailValue}>{entry.details.sessionDetails.userAgent}</span>
|
|
813
|
+
</div>
|
|
814
|
+
)}
|
|
815
|
+
</>
|
|
816
|
+
)}
|
|
817
|
+
|
|
818
|
+
{/* Security Details */}
|
|
819
|
+
{entry.action === 'security-violation' && entry.details.securityDetails && (
|
|
820
|
+
<>
|
|
821
|
+
<div className={styles.detailRow}>
|
|
822
|
+
<span className={styles.detailLabel}>Severity:</span>
|
|
823
|
+
<span className={`${styles.detailValue} ${styles.severity} ${styles[entry.details.securityDetails.severity || 'low']}`}>
|
|
824
|
+
{(entry.details.securityDetails.severity || 'low').toUpperCase()}
|
|
825
|
+
</span>
|
|
826
|
+
</div>
|
|
827
|
+
{entry.details.securityDetails.incidentType && (
|
|
828
|
+
<div className={styles.detailRow}>
|
|
829
|
+
<span className={styles.detailLabel}>Type:</span>
|
|
830
|
+
<span className={styles.detailValue}>{entry.details.securityDetails.incidentType}</span>
|
|
831
|
+
</div>
|
|
832
|
+
)}
|
|
833
|
+
</>
|
|
834
|
+
)}
|
|
835
|
+
|
|
836
|
+
{/* File Operation Details */}
|
|
837
|
+
{(entry.action === 'file-upload' || entry.action === 'file-delete' || entry.action === 'file-access') && entry.details.fileDetails && (
|
|
838
|
+
<>
|
|
839
|
+
{/* File ID */}
|
|
840
|
+
{entry.details.fileDetails.fileId && (
|
|
841
|
+
<div className={styles.detailRow}>
|
|
842
|
+
<span className={styles.detailLabel}>File ID:</span>
|
|
843
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
844
|
+
</div>
|
|
845
|
+
)}
|
|
846
|
+
|
|
847
|
+
{/* Original Filename */}
|
|
848
|
+
{entry.details.fileDetails.originalFileName && (
|
|
849
|
+
<div className={styles.detailRow}>
|
|
850
|
+
<span className={styles.detailLabel}>Original Filename:</span>
|
|
851
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
852
|
+
</div>
|
|
853
|
+
)}
|
|
854
|
+
|
|
855
|
+
{/* File Size */}
|
|
856
|
+
{entry.details.fileDetails.fileSize > 0 && (
|
|
857
|
+
<div className={styles.detailRow}>
|
|
858
|
+
<span className={styles.detailLabel}>File Size:</span>
|
|
859
|
+
<span className={styles.detailValue}>
|
|
860
|
+
{(entry.details.fileDetails.fileSize / 1024 / 1024).toFixed(2)} MB
|
|
861
|
+
</span>
|
|
862
|
+
</div>
|
|
863
|
+
)}
|
|
864
|
+
|
|
865
|
+
{/* Access Method/Upload Method */}
|
|
866
|
+
{entry.details.fileDetails.uploadMethod && (
|
|
867
|
+
<div className={styles.detailRow}>
|
|
868
|
+
<span className={styles.detailLabel}>
|
|
869
|
+
{entry.action === 'file-access' ? 'Access Method' : 'Upload Method'}:
|
|
870
|
+
</span>
|
|
871
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.uploadMethod}</span>
|
|
872
|
+
</div>
|
|
873
|
+
)}
|
|
874
|
+
|
|
875
|
+
{/* Delete Reason */}
|
|
876
|
+
{entry.details.fileDetails.deleteReason && (
|
|
877
|
+
<div className={styles.detailRow}>
|
|
878
|
+
<span className={styles.detailLabel}>Reason:</span>
|
|
879
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.deleteReason}</span>
|
|
880
|
+
</div>
|
|
881
|
+
)}
|
|
882
|
+
|
|
883
|
+
{/* Access Source */}
|
|
884
|
+
{entry.details.fileDetails.sourceLocation && entry.action === 'file-access' && (
|
|
885
|
+
<div className={styles.detailRow}>
|
|
886
|
+
<span className={styles.detailLabel}>Access Source:</span>
|
|
887
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.sourceLocation}</span>
|
|
888
|
+
</div>
|
|
889
|
+
)}
|
|
890
|
+
</>
|
|
891
|
+
)}
|
|
892
|
+
|
|
893
|
+
{/* Annotation Details */}
|
|
894
|
+
{(entry.action === 'annotation-create' || entry.action === 'annotation-edit' || entry.action === 'annotation-delete') && entry.details.fileDetails && (
|
|
895
|
+
<>
|
|
896
|
+
{/* File ID */}
|
|
897
|
+
{entry.details.fileDetails.fileId && (
|
|
898
|
+
<div className={styles.detailRow}>
|
|
899
|
+
<span className={styles.detailLabel}>File ID:</span>
|
|
900
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
901
|
+
</div>
|
|
902
|
+
)}
|
|
903
|
+
|
|
904
|
+
{/* Original Filename */}
|
|
905
|
+
{entry.details.fileDetails.originalFileName && (
|
|
906
|
+
<div className={styles.detailRow}>
|
|
907
|
+
<span className={styles.detailLabel}>Original Filename:</span>
|
|
908
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
909
|
+
</div>
|
|
910
|
+
)}
|
|
911
|
+
|
|
912
|
+
{/* Annotation Type */}
|
|
913
|
+
{entry.details.annotationDetails?.annotationType && (
|
|
914
|
+
<div className={styles.detailRow}>
|
|
915
|
+
<span className={styles.detailLabel}>Annotation Type:</span>
|
|
916
|
+
<span className={styles.detailValue}>{entry.details.annotationDetails.annotationType}</span>
|
|
917
|
+
</div>
|
|
918
|
+
)}
|
|
919
|
+
|
|
920
|
+
{/* Tool Used */}
|
|
921
|
+
{entry.details.annotationDetails?.tool && (
|
|
922
|
+
<div className={styles.detailRow}>
|
|
923
|
+
<span className={styles.detailLabel}>Tool:</span>
|
|
924
|
+
<span className={styles.detailValue}>{entry.details.annotationDetails.tool}</span>
|
|
925
|
+
</div>
|
|
926
|
+
)}
|
|
927
|
+
</>
|
|
928
|
+
)}
|
|
929
|
+
|
|
930
|
+
{/* PDF Generation and Confirmation Details */}
|
|
931
|
+
{(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
|
|
932
|
+
<>
|
|
933
|
+
{/* Source File ID */}
|
|
934
|
+
{entry.details.fileDetails.fileId && (
|
|
935
|
+
<div className={styles.detailRow}>
|
|
936
|
+
<span className={styles.detailLabel}>{entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}</span>
|
|
937
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
938
|
+
</div>
|
|
939
|
+
)}
|
|
940
|
+
|
|
941
|
+
{/* Source Original Filename */}
|
|
942
|
+
{entry.details.fileDetails.originalFileName && (
|
|
943
|
+
<div className={styles.detailRow}>
|
|
944
|
+
<span className={styles.detailLabel}>{entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}</span>
|
|
945
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
946
|
+
</div>
|
|
947
|
+
)}
|
|
948
|
+
|
|
949
|
+
{/* Confirmation ID (for confirm actions) */}
|
|
950
|
+
{entry.action === 'confirm' && entry.details.confirmationId && (
|
|
951
|
+
<div className={styles.detailRow}>
|
|
952
|
+
<span className={styles.detailLabel}>Confirmation ID:</span>
|
|
953
|
+
<span className={styles.detailValue}>{entry.details.confirmationId}</span>
|
|
954
|
+
</div>
|
|
955
|
+
)}
|
|
956
|
+
</>
|
|
957
|
+
)}
|
|
958
|
+
</div>
|
|
959
|
+
</div>
|
|
960
|
+
))
|
|
961
|
+
)}
|
|
962
|
+
</div>
|
|
963
|
+
</>
|
|
964
|
+
)}
|
|
965
|
+
|
|
966
|
+
{auditEntries.length === 0 && !loading && !error && (
|
|
967
|
+
<div className={styles.noData}>
|
|
968
|
+
<p>No audit trail available. Your activities will appear here as you use Striae.</p>
|
|
969
|
+
</div>
|
|
970
|
+
)}
|
|
971
|
+
</div>
|
|
972
|
+
</div>
|
|
973
|
+
</div>
|
|
974
|
+
);
|
|
975
|
+
};
|