@striae-org/striae 4.0.3 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/actions/confirm-export.ts +4 -2
- package/app/components/actions/generate-pdf.ts +10 -2
- package/app/components/audit/user-audit-viewer.tsx +121 -940
- package/app/components/audit/user-audit.module.css +20 -0
- package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +200 -0
- package/app/components/audit/viewer/audit-filters-panel.tsx +306 -0
- package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
- package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +121 -0
- package/app/components/audit/viewer/types.ts +1 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +166 -0
- package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
- package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
- package/app/components/auth/mfa-enrollment.module.css +13 -5
- package/app/components/auth/mfa-verification.module.css +13 -5
- package/app/components/canvas/canvas.tsx +3 -0
- package/app/components/canvas/confirmation/confirmation.tsx +13 -37
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +8 -37
- package/app/components/sidebar/case-export/case-export.tsx +9 -34
- package/app/components/sidebar/case-import/case-import.module.css +2 -0
- package/app/components/sidebar/case-import/case-import.tsx +10 -34
- package/app/components/sidebar/cases/cases-modal.module.css +44 -9
- package/app/components/sidebar/cases/cases-modal.tsx +16 -14
- package/app/components/sidebar/files/files-modal.module.css +45 -10
- package/app/components/sidebar/files/files-modal.tsx +16 -16
- package/app/components/sidebar/notes/notes-modal.tsx +17 -15
- package/app/components/sidebar/notes/notes.module.css +2 -0
- package/app/components/sidebar/sidebar.module.css +2 -2
- package/app/components/toast/toast.module.css +2 -1
- package/app/components/toast/toast.tsx +16 -11
- package/app/components/user/delete-account.tsx +10 -31
- package/app/components/user/inactivity-warning.module.css +8 -6
- package/app/components/user/manage-profile.module.css +2 -0
- package/app/components/user/manage-profile.tsx +85 -30
- package/app/hooks/useOverlayDismiss.ts +68 -0
- package/app/routes/auth/login.example.tsx +19 -8
- package/app/routes/auth/passwordReset.module.css +23 -13
- package/app/routes/striae/striae.tsx +8 -1
- package/app/routes.ts +7 -0
- package/app/services/audit/audit-export-csv.ts +2 -0
- package/app/services/audit/audit.service.ts +29 -5
- package/app/services/audit/builders/audit-entry-builder.ts +2 -1
- package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
- package/app/services/audit/builders/audit-event-builders-workflow.ts +6 -0
- package/app/types/audit.ts +2 -1
- package/app/types/user.ts +1 -0
- package/app/utils/data/permissions.ts +1 -0
- package/functions/api/pdf/[[path]].ts +32 -1
- package/load-context.ts +9 -0
- package/package.json +5 -1
- package/primershear.emails.example +6 -0
- package/scripts/deploy-pages-secrets.sh +6 -0
- package/scripts/deploy-primershear-emails.sh +166 -0
- package/worker-configuration.d.ts +7493 -7491
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
- package/workers/pdf-worker/src/report-types.ts +3 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/user-worker.example.ts +6 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
|
@@ -531,6 +531,26 @@
|
|
|
531
531
|
word-break: break-word;
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
+
.badgeTag {
|
|
535
|
+
display: inline-flex;
|
|
536
|
+
align-items: center;
|
|
537
|
+
padding: 2px 10px;
|
|
538
|
+
border-radius: 999px;
|
|
539
|
+
border: 1px solid color-mix(in lab, var(--primary) 35%, transparent);
|
|
540
|
+
background: color-mix(in lab, var(--primary) 12%, transparent);
|
|
541
|
+
color: color-mix(in lab, var(--primary) 70%, var(--text));
|
|
542
|
+
font-size: 0.78rem;
|
|
543
|
+
font-weight: var(--fontWeightMedium);
|
|
544
|
+
letter-spacing: 0.2px;
|
|
545
|
+
line-height: 1.4;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.badgeTagMuted {
|
|
549
|
+
border-color: color-mix(in lab, var(--textLight) 25%, transparent);
|
|
550
|
+
background: color-mix(in lab, var(--backgroundLight) 70%, transparent);
|
|
551
|
+
color: var(--textLight);
|
|
552
|
+
}
|
|
553
|
+
|
|
534
554
|
.severity {
|
|
535
555
|
font-weight: var(--fontWeightMedium);
|
|
536
556
|
padding: 2px 6px;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AuditViewerSummaryStats } from './audit-viewer-utils';
|
|
2
|
+
import styles from '../user-audit.module.css';
|
|
3
|
+
|
|
4
|
+
interface AuditActivitySummaryProps {
|
|
5
|
+
caseNumber?: string;
|
|
6
|
+
filterCaseNumber: string;
|
|
7
|
+
dateRangeDisplay: string;
|
|
8
|
+
summary: AuditViewerSummaryStats;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const AuditActivitySummary = ({
|
|
12
|
+
caseNumber,
|
|
13
|
+
filterCaseNumber,
|
|
14
|
+
dateRangeDisplay,
|
|
15
|
+
summary,
|
|
16
|
+
}: AuditActivitySummaryProps) => {
|
|
17
|
+
const activeCaseNumber = caseNumber || filterCaseNumber.trim();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={styles.summary}>
|
|
21
|
+
<h3>
|
|
22
|
+
{activeCaseNumber
|
|
23
|
+
? `Case Activity Summary - ${activeCaseNumber} (${dateRangeDisplay})`
|
|
24
|
+
: `Activity Summary (${dateRangeDisplay})`}
|
|
25
|
+
</h3>
|
|
26
|
+
<div className={styles.summaryGrid}>
|
|
27
|
+
<div className={styles.summaryItem}>
|
|
28
|
+
<span className={styles.label}>Total Activities:</span>
|
|
29
|
+
<span className={styles.value}>{summary.totalEntries}</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div className={styles.summaryItem}>
|
|
32
|
+
<span className={styles.label}>Successful:</span>
|
|
33
|
+
<span className={styles.value}>{summary.successfulEntries}</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div className={styles.summaryItem}>
|
|
36
|
+
<span className={styles.label}>Failed:</span>
|
|
37
|
+
<span className={styles.value}>{summary.failedEntries}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div className={styles.summaryItem}>
|
|
40
|
+
<span className={styles.label}>Login Sessions:</span>
|
|
41
|
+
<span className={styles.value}>{summary.loginSessions}</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div className={styles.summaryItem}>
|
|
44
|
+
<span className={styles.label}>Security Incidents:</span>
|
|
45
|
+
<span className={`${styles.value} ${summary.securityIncidents > 0 ? styles.warning : ''}`}>
|
|
46
|
+
{summary.securityIncidents}
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { type ValidationAuditEntry } from '~/types';
|
|
2
|
+
import { formatAuditTimestamp, getAuditActionIcon, getAuditStatusIcon } from './audit-viewer-utils';
|
|
3
|
+
import styles from '../user-audit.module.css';
|
|
4
|
+
|
|
5
|
+
interface AuditEntriesListProps {
|
|
6
|
+
entries: ValidationAuditEntry[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className={styles.entriesList}>
|
|
12
|
+
<h3>Activity Log ({entries.length} entries)</h3>
|
|
13
|
+
{entries.length === 0 ? (
|
|
14
|
+
<div className={styles.noEntries}>
|
|
15
|
+
<p>No activities match the current filters.</p>
|
|
16
|
+
</div>
|
|
17
|
+
) : (
|
|
18
|
+
entries.map((entry, index) => (
|
|
19
|
+
<div key={index} className={`${styles.entry} ${styles[entry.result]}`}>
|
|
20
|
+
<div className={styles.entryHeader}>
|
|
21
|
+
<div className={styles.entryIcons}>
|
|
22
|
+
<span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
|
|
23
|
+
<span className={styles.statusIcon}>{getAuditStatusIcon(entry.result)}</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div className={styles.entryTitle}>
|
|
26
|
+
<span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
|
|
27
|
+
<span className={styles.fileName}>{entry.details.fileName}</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div className={styles.entryTimestamp}>{formatAuditTimestamp(entry.timestamp)}</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div className={styles.entryDetails}>
|
|
33
|
+
{entry.details.caseNumber && (
|
|
34
|
+
<div className={styles.detailRow}>
|
|
35
|
+
<span className={styles.detailLabel}>Case:</span>
|
|
36
|
+
<span className={styles.detailValue}>{entry.details.caseNumber}</span>
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
{entry.details.userProfileDetails?.badgeId && (
|
|
41
|
+
<div className={styles.detailRow}>
|
|
42
|
+
<span className={styles.detailLabel}>Badge/ID:</span>
|
|
43
|
+
<span className={styles.badgeTag}>{entry.details.userProfileDetails.badgeId}</span>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{entry.result === 'failure' && entry.details.validationErrors.length > 0 && (
|
|
48
|
+
<div className={styles.detailRow}>
|
|
49
|
+
<span className={styles.detailLabel}>Error:</span>
|
|
50
|
+
<span className={styles.detailValue}>{entry.details.validationErrors[0]}</span>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{(entry.action === 'user-login' || entry.action === 'user-logout') && entry.details.sessionDetails && (
|
|
55
|
+
<>
|
|
56
|
+
{entry.details.sessionDetails.userAgent && (
|
|
57
|
+
<div className={styles.detailRow}>
|
|
58
|
+
<span className={styles.detailLabel}>User Agent:</span>
|
|
59
|
+
<span className={styles.detailValue}>{entry.details.sessionDetails.userAgent}</span>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{entry.action === 'security-violation' && entry.details.securityDetails && (
|
|
66
|
+
<>
|
|
67
|
+
<div className={styles.detailRow}>
|
|
68
|
+
<span className={styles.detailLabel}>Severity:</span>
|
|
69
|
+
<span
|
|
70
|
+
className={`${styles.detailValue} ${styles.severity} ${styles[entry.details.securityDetails.severity || 'low']}`}
|
|
71
|
+
>
|
|
72
|
+
{(entry.details.securityDetails.severity || 'low').toUpperCase()}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
{entry.details.securityDetails.incidentType && (
|
|
76
|
+
<div className={styles.detailRow}>
|
|
77
|
+
<span className={styles.detailLabel}>Type:</span>
|
|
78
|
+
<span className={styles.detailValue}>{entry.details.securityDetails.incidentType}</span>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{(entry.action === 'file-upload' || entry.action === 'file-delete' || entry.action === 'file-access') && entry.details.fileDetails && (
|
|
85
|
+
<>
|
|
86
|
+
{entry.details.fileDetails.fileId && (
|
|
87
|
+
<div className={styles.detailRow}>
|
|
88
|
+
<span className={styles.detailLabel}>File ID:</span>
|
|
89
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{entry.details.fileDetails.originalFileName && (
|
|
94
|
+
<div className={styles.detailRow}>
|
|
95
|
+
<span className={styles.detailLabel}>Original Filename:</span>
|
|
96
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{entry.details.fileDetails.fileSize > 0 && (
|
|
101
|
+
<div className={styles.detailRow}>
|
|
102
|
+
<span className={styles.detailLabel}>File Size:</span>
|
|
103
|
+
<span className={styles.detailValue}>
|
|
104
|
+
{(entry.details.fileDetails.fileSize / 1024 / 1024).toFixed(2)} MB
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{entry.details.fileDetails.uploadMethod && (
|
|
110
|
+
<div className={styles.detailRow}>
|
|
111
|
+
<span className={styles.detailLabel}>
|
|
112
|
+
{entry.action === 'file-access' ? 'Access Method' : 'Upload Method'}:
|
|
113
|
+
</span>
|
|
114
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.uploadMethod}</span>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{entry.details.fileDetails.deleteReason && (
|
|
119
|
+
<div className={styles.detailRow}>
|
|
120
|
+
<span className={styles.detailLabel}>Reason:</span>
|
|
121
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.deleteReason}</span>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{entry.details.fileDetails.sourceLocation && entry.action === 'file-access' && (
|
|
126
|
+
<div className={styles.detailRow}>
|
|
127
|
+
<span className={styles.detailLabel}>Access Source:</span>
|
|
128
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.sourceLocation}</span>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{(entry.action === 'annotation-create' || entry.action === 'annotation-edit' || entry.action === 'annotation-delete') && entry.details.fileDetails && (
|
|
135
|
+
<>
|
|
136
|
+
{entry.details.fileDetails.fileId && (
|
|
137
|
+
<div className={styles.detailRow}>
|
|
138
|
+
<span className={styles.detailLabel}>File ID:</span>
|
|
139
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{entry.details.fileDetails.originalFileName && (
|
|
144
|
+
<div className={styles.detailRow}>
|
|
145
|
+
<span className={styles.detailLabel}>Original Filename:</span>
|
|
146
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{entry.details.annotationDetails?.annotationType && (
|
|
151
|
+
<div className={styles.detailRow}>
|
|
152
|
+
<span className={styles.detailLabel}>Annotation Type:</span>
|
|
153
|
+
<span className={styles.detailValue}>{entry.details.annotationDetails.annotationType}</span>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{entry.details.annotationDetails?.tool && (
|
|
158
|
+
<div className={styles.detailRow}>
|
|
159
|
+
<span className={styles.detailLabel}>Tool:</span>
|
|
160
|
+
<span className={styles.detailValue}>{entry.details.annotationDetails.tool}</span>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
|
|
167
|
+
<>
|
|
168
|
+
{entry.details.fileDetails.fileId && (
|
|
169
|
+
<div className={styles.detailRow}>
|
|
170
|
+
<span className={styles.detailLabel}>
|
|
171
|
+
{entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}
|
|
172
|
+
</span>
|
|
173
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{entry.details.fileDetails.originalFileName && (
|
|
178
|
+
<div className={styles.detailRow}>
|
|
179
|
+
<span className={styles.detailLabel}>
|
|
180
|
+
{entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}
|
|
181
|
+
</span>
|
|
182
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{entry.action === 'confirm' && entry.details.confirmationId && (
|
|
187
|
+
<div className={styles.detailRow}>
|
|
188
|
+
<span className={styles.detailLabel}>Confirmation ID:</span>
|
|
189
|
+
<span className={styles.detailValue}>{entry.details.confirmationId}</span>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
))
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { AuditAction, AuditResult } from '~/types';
|
|
2
|
+
import type { DateRangeFilter } from './types';
|
|
3
|
+
import styles from '../user-audit.module.css';
|
|
4
|
+
|
|
5
|
+
interface AuditFiltersPanelProps {
|
|
6
|
+
dateRange: DateRangeFilter;
|
|
7
|
+
customStartDate: string;
|
|
8
|
+
customEndDate: string;
|
|
9
|
+
customStartDateInput: string;
|
|
10
|
+
customEndDateInput: string;
|
|
11
|
+
caseNumber?: string;
|
|
12
|
+
filterCaseNumber: string;
|
|
13
|
+
caseNumberInput: string;
|
|
14
|
+
filterBadgeId: string;
|
|
15
|
+
badgeIdInput: string;
|
|
16
|
+
filterAction: AuditAction | 'all';
|
|
17
|
+
filterResult: AuditResult | 'all';
|
|
18
|
+
onDateRangeChange: (value: DateRangeFilter) => void;
|
|
19
|
+
onCustomStartDateInputChange: (value: string) => void;
|
|
20
|
+
onCustomEndDateInputChange: (value: string) => void;
|
|
21
|
+
onApplyCustomDateRange: () => void;
|
|
22
|
+
onClearCustomDateRange: () => void;
|
|
23
|
+
onCaseNumberInputChange: (value: string) => void;
|
|
24
|
+
onApplyCaseFilter: () => void;
|
|
25
|
+
onClearCaseFilter: () => void;
|
|
26
|
+
onBadgeIdInputChange: (value: string) => void;
|
|
27
|
+
onApplyBadgeFilter: () => void;
|
|
28
|
+
onClearBadgeFilter: () => void;
|
|
29
|
+
onFilterActionChange: (value: AuditAction | 'all') => void;
|
|
30
|
+
onFilterResultChange: (value: AuditResult | 'all') => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const AuditFiltersPanel = ({
|
|
34
|
+
dateRange,
|
|
35
|
+
customStartDate,
|
|
36
|
+
customEndDate,
|
|
37
|
+
customStartDateInput,
|
|
38
|
+
customEndDateInput,
|
|
39
|
+
caseNumber,
|
|
40
|
+
filterCaseNumber,
|
|
41
|
+
caseNumberInput,
|
|
42
|
+
filterBadgeId,
|
|
43
|
+
badgeIdInput,
|
|
44
|
+
filterAction,
|
|
45
|
+
filterResult,
|
|
46
|
+
onDateRangeChange,
|
|
47
|
+
onCustomStartDateInputChange,
|
|
48
|
+
onCustomEndDateInputChange,
|
|
49
|
+
onApplyCustomDateRange,
|
|
50
|
+
onClearCustomDateRange,
|
|
51
|
+
onCaseNumberInputChange,
|
|
52
|
+
onApplyCaseFilter,
|
|
53
|
+
onClearCaseFilter,
|
|
54
|
+
onBadgeIdInputChange,
|
|
55
|
+
onApplyBadgeFilter,
|
|
56
|
+
onClearBadgeFilter,
|
|
57
|
+
onFilterActionChange,
|
|
58
|
+
onFilterResultChange,
|
|
59
|
+
}: AuditFiltersPanelProps) => {
|
|
60
|
+
return (
|
|
61
|
+
<div className={styles.filters}>
|
|
62
|
+
<div className={styles.filterGroup}>
|
|
63
|
+
<label htmlFor="dateRange">Time Period:</label>
|
|
64
|
+
<select
|
|
65
|
+
id="dateRange"
|
|
66
|
+
value={dateRange}
|
|
67
|
+
onChange={(e) => {
|
|
68
|
+
onDateRangeChange(e.target.value as DateRangeFilter);
|
|
69
|
+
}}
|
|
70
|
+
className={styles.filterSelect}
|
|
71
|
+
>
|
|
72
|
+
<option value="1d">Last 24 Hours</option>
|
|
73
|
+
<option value="7d">Last 7 Days</option>
|
|
74
|
+
<option value="30d">Last 30 Days</option>
|
|
75
|
+
<option value="90d">Last 90 Days</option>
|
|
76
|
+
<option value="custom">Custom Range</option>
|
|
77
|
+
</select>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{dateRange === 'custom' && (
|
|
81
|
+
<div className={styles.customDateRange}>
|
|
82
|
+
<div className={styles.customDateInputs}>
|
|
83
|
+
<div className={styles.filterGroup}>
|
|
84
|
+
<label htmlFor="startDate">Start Date:</label>
|
|
85
|
+
<input
|
|
86
|
+
type="date"
|
|
87
|
+
id="startDate"
|
|
88
|
+
value={customStartDateInput}
|
|
89
|
+
onChange={(e) => onCustomStartDateInputChange(e.target.value)}
|
|
90
|
+
className={styles.filterInput}
|
|
91
|
+
max={customEndDateInput || new Date().toISOString().split('T')[0]}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
<div className={styles.filterGroup}>
|
|
95
|
+
<label htmlFor="endDate">End Date:</label>
|
|
96
|
+
<input
|
|
97
|
+
type="date"
|
|
98
|
+
id="endDate"
|
|
99
|
+
value={customEndDateInput}
|
|
100
|
+
onChange={(e) => onCustomEndDateInputChange(e.target.value)}
|
|
101
|
+
className={styles.filterInput}
|
|
102
|
+
min={customStartDateInput}
|
|
103
|
+
max={new Date().toISOString().split('T')[0]}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
<div className={styles.dateRangeButtons}>
|
|
107
|
+
{(customStartDateInput || customEndDateInput) && (
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={onApplyCustomDateRange}
|
|
111
|
+
className={styles.filterButton}
|
|
112
|
+
title="Apply custom date range"
|
|
113
|
+
>
|
|
114
|
+
Apply Dates
|
|
115
|
+
</button>
|
|
116
|
+
)}
|
|
117
|
+
{(customStartDate || customEndDate) && (
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
onClick={onClearCustomDateRange}
|
|
121
|
+
className={styles.clearButton}
|
|
122
|
+
title="Clear custom date range"
|
|
123
|
+
>
|
|
124
|
+
Clear Dates
|
|
125
|
+
</button>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
{(customStartDate || customEndDate) && (
|
|
130
|
+
<div className={styles.activeFilter}>
|
|
131
|
+
<small>
|
|
132
|
+
Custom range:
|
|
133
|
+
{customStartDate && <strong> from {new Date(customStartDate).toLocaleDateString()}</strong>}
|
|
134
|
+
{customEndDate && <strong> to {new Date(customEndDate).toLocaleDateString()}</strong>}
|
|
135
|
+
</small>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
<div className={styles.filterGroup}>
|
|
142
|
+
<label htmlFor="caseFilter">Case Number:</label>
|
|
143
|
+
<div className={styles.inputWithButton}>
|
|
144
|
+
<input
|
|
145
|
+
type="text"
|
|
146
|
+
id="caseFilter"
|
|
147
|
+
value={caseNumberInput}
|
|
148
|
+
onChange={(e) => onCaseNumberInputChange(e.target.value)}
|
|
149
|
+
className={styles.filterInput}
|
|
150
|
+
placeholder="Enter case number..."
|
|
151
|
+
disabled={!!caseNumber}
|
|
152
|
+
title={
|
|
153
|
+
caseNumber
|
|
154
|
+
? 'Case filter disabled - viewing specific case'
|
|
155
|
+
: 'Enter complete case number and click Filter'
|
|
156
|
+
}
|
|
157
|
+
onKeyDown={(e) => {
|
|
158
|
+
if (e.key === 'Enter' && caseNumberInput.trim() && !caseNumber) {
|
|
159
|
+
onApplyCaseFilter();
|
|
160
|
+
}
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
{!caseNumber && (
|
|
164
|
+
<div className={styles.caseFilterButtons}>
|
|
165
|
+
{caseNumberInput.trim() && (
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
onClick={onApplyCaseFilter}
|
|
169
|
+
className={styles.filterButton}
|
|
170
|
+
title="Apply case filter"
|
|
171
|
+
>
|
|
172
|
+
Filter
|
|
173
|
+
</button>
|
|
174
|
+
)}
|
|
175
|
+
{filterCaseNumber && (
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onClick={onClearCaseFilter}
|
|
179
|
+
className={styles.clearButton}
|
|
180
|
+
title="Clear case filter"
|
|
181
|
+
>
|
|
182
|
+
Clear
|
|
183
|
+
</button>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
{filterCaseNumber && !caseNumber && (
|
|
189
|
+
<div className={styles.activeFilter}>
|
|
190
|
+
<small>
|
|
191
|
+
Filtering by case: <strong>{filterCaseNumber}</strong>
|
|
192
|
+
</small>
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className={styles.filterGroup}>
|
|
198
|
+
<label htmlFor="badgeFilter">Badge/ID #:</label>
|
|
199
|
+
<div className={styles.inputWithButton}>
|
|
200
|
+
<input
|
|
201
|
+
type="text"
|
|
202
|
+
id="badgeFilter"
|
|
203
|
+
value={badgeIdInput}
|
|
204
|
+
onChange={(e) => onBadgeIdInputChange(e.target.value)}
|
|
205
|
+
className={styles.filterInput}
|
|
206
|
+
placeholder="Enter badge/id #..."
|
|
207
|
+
onKeyDown={(e) => {
|
|
208
|
+
if (e.key === 'Enter' && badgeIdInput.trim()) {
|
|
209
|
+
onApplyBadgeFilter();
|
|
210
|
+
}
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
<div className={styles.caseFilterButtons}>
|
|
214
|
+
{badgeIdInput.trim() && (
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
onClick={onApplyBadgeFilter}
|
|
218
|
+
className={styles.filterButton}
|
|
219
|
+
title="Apply badge filter"
|
|
220
|
+
>
|
|
221
|
+
Filter
|
|
222
|
+
</button>
|
|
223
|
+
)}
|
|
224
|
+
{filterBadgeId && (
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
onClick={onClearBadgeFilter}
|
|
228
|
+
className={styles.clearButton}
|
|
229
|
+
title="Clear badge filter"
|
|
230
|
+
>
|
|
231
|
+
Clear
|
|
232
|
+
</button>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
{filterBadgeId && (
|
|
237
|
+
<div className={styles.activeFilter}>
|
|
238
|
+
<small>
|
|
239
|
+
Filtering by Badge/ID: <strong>{filterBadgeId}</strong>
|
|
240
|
+
</small>
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<div className={styles.filterGroup}>
|
|
246
|
+
<label htmlFor="actionFilter">Activity Type:</label>
|
|
247
|
+
<select
|
|
248
|
+
id="actionFilter"
|
|
249
|
+
value={filterAction}
|
|
250
|
+
onChange={(e) => onFilterActionChange(e.target.value as AuditAction | 'all')}
|
|
251
|
+
className={styles.filterSelect}
|
|
252
|
+
>
|
|
253
|
+
<option value="all">All Activities</option>
|
|
254
|
+
<optgroup label="User Sessions">
|
|
255
|
+
<option value="user-login">Login</option>
|
|
256
|
+
<option value="user-logout">Logout</option>
|
|
257
|
+
</optgroup>
|
|
258
|
+
<optgroup label="Case Management">
|
|
259
|
+
<option value="case-create">Case Create</option>
|
|
260
|
+
<option value="case-rename">Case Rename</option>
|
|
261
|
+
<option value="case-delete">Case Delete</option>
|
|
262
|
+
<option value="case-export">Case Export</option>
|
|
263
|
+
<option value="case-import">Case Import</option>
|
|
264
|
+
</optgroup>
|
|
265
|
+
<optgroup label="File Operations">
|
|
266
|
+
<option value="file-upload">File Upload</option>
|
|
267
|
+
<option value="file-access">File Access</option>
|
|
268
|
+
<option value="file-delete">File Delete</option>
|
|
269
|
+
</optgroup>
|
|
270
|
+
<optgroup label="Annotations">
|
|
271
|
+
<option value="annotation-create">Annotation Create</option>
|
|
272
|
+
<option value="annotation-edit">Annotation Edit</option>
|
|
273
|
+
<option value="annotation-delete">Annotation Delete</option>
|
|
274
|
+
</optgroup>
|
|
275
|
+
<optgroup label="Confirmation Activity">
|
|
276
|
+
<option value="confirmation-create">Confirmation Create</option>
|
|
277
|
+
<option value="confirmation-export">Confirmation Export</option>
|
|
278
|
+
<option value="confirmation-import">Confirmation Import</option>
|
|
279
|
+
</optgroup>
|
|
280
|
+
<optgroup label="Documents">
|
|
281
|
+
<option value="pdf-generate">PDF Generate</option>
|
|
282
|
+
</optgroup>
|
|
283
|
+
<optgroup label="Security">
|
|
284
|
+
<option value="security-violation">Security Violation</option>
|
|
285
|
+
</optgroup>
|
|
286
|
+
</select>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<div className={styles.filterGroup}>
|
|
290
|
+
<label htmlFor="resultFilter">Result:</label>
|
|
291
|
+
<select
|
|
292
|
+
id="resultFilter"
|
|
293
|
+
value={filterResult}
|
|
294
|
+
onChange={(e) => onFilterResultChange(e.target.value as AuditResult | 'all')}
|
|
295
|
+
className={styles.filterSelect}
|
|
296
|
+
>
|
|
297
|
+
<option value="all">All Results</option>
|
|
298
|
+
<option value="success">Success</option>
|
|
299
|
+
<option value="failure">Failure</option>
|
|
300
|
+
<option value="warning">Warning</option>
|
|
301
|
+
<option value="blocked">Blocked</option>
|
|
302
|
+
</select>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
import type { UserData } from '~/types';
|
|
3
|
+
import styles from '../user-audit.module.css';
|
|
4
|
+
|
|
5
|
+
interface AuditUserInfoCardProps {
|
|
6
|
+
user: User;
|
|
7
|
+
userData: UserData | null;
|
|
8
|
+
userBadgeId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const AuditUserInfoCard = ({
|
|
12
|
+
user,
|
|
13
|
+
userData,
|
|
14
|
+
userBadgeId,
|
|
15
|
+
}: AuditUserInfoCardProps) => {
|
|
16
|
+
return (
|
|
17
|
+
<div className={styles.summary}>
|
|
18
|
+
<h3>User Information</h3>
|
|
19
|
+
<div className={styles.userInfoContent}>
|
|
20
|
+
<div className={styles.userInfoItem}>
|
|
21
|
+
Name:{' '}
|
|
22
|
+
<strong>
|
|
23
|
+
{userData ? `${userData.firstName} ${userData.lastName}` : user.displayName || 'Not provided'}
|
|
24
|
+
</strong>
|
|
25
|
+
</div>
|
|
26
|
+
<div className={styles.userInfoItem}>
|
|
27
|
+
Email: <strong>{user.email || 'Not provided'}</strong>
|
|
28
|
+
</div>
|
|
29
|
+
<div className={styles.userInfoItem}>
|
|
30
|
+
Lab/Company: <strong>{userData?.company || 'Not provided'}</strong>
|
|
31
|
+
</div>
|
|
32
|
+
<div className={styles.userInfoItem}>
|
|
33
|
+
Badge/ID #:{' '}
|
|
34
|
+
<span className={`${styles.badgeTag} ${!userBadgeId ? styles.badgeTagMuted : ''}`}>
|
|
35
|
+
{userBadgeId || 'Not provided'}
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div className={styles.userInfoItem}>
|
|
39
|
+
User ID: <strong>{user.uid}</strong>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import styles from '../user-audit.module.css';
|
|
2
|
+
|
|
3
|
+
interface AuditViewerHeaderProps {
|
|
4
|
+
title: string;
|
|
5
|
+
hasEntries: boolean;
|
|
6
|
+
onExportCSV: () => void;
|
|
7
|
+
onExportJSON: () => void;
|
|
8
|
+
onGenerateReport: () => void;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const AuditViewerHeader = ({
|
|
13
|
+
title,
|
|
14
|
+
hasEntries,
|
|
15
|
+
onExportCSV,
|
|
16
|
+
onExportJSON,
|
|
17
|
+
onGenerateReport,
|
|
18
|
+
onClose,
|
|
19
|
+
}: AuditViewerHeaderProps) => {
|
|
20
|
+
return (
|
|
21
|
+
<div className={styles.header}>
|
|
22
|
+
<h2 className={styles.title}>{title}</h2>
|
|
23
|
+
<div className={styles.headerActions}>
|
|
24
|
+
{hasEntries && (
|
|
25
|
+
<div className={styles.exportButtons}>
|
|
26
|
+
<button
|
|
27
|
+
onClick={onExportCSV}
|
|
28
|
+
className={styles.exportButton}
|
|
29
|
+
title="CSV - Individual entry log with summary data"
|
|
30
|
+
>
|
|
31
|
+
📊 CSV
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
onClick={onExportJSON}
|
|
35
|
+
className={styles.exportButton}
|
|
36
|
+
title="JSON - Complete log data for version capture and auditing"
|
|
37
|
+
>
|
|
38
|
+
📄 JSON
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
onClick={onGenerateReport}
|
|
42
|
+
className={styles.exportButton}
|
|
43
|
+
title="Summary report only"
|
|
44
|
+
>
|
|
45
|
+
📋 Report
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
<button className={styles.closeButton} onClick={onClose}>
|
|
50
|
+
×
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|