@striae-org/striae 4.0.3 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/app/components/actions/case-export/core-export.ts +14 -8
- package/app/components/actions/case-export/data-processing.ts +1 -0
- package/app/components/actions/case-export/download-handlers.ts +7 -0
- package/app/components/actions/case-export/metadata-helpers.ts +2 -1
- package/app/components/actions/case-import/confirmation-import.ts +12 -2
- package/app/components/actions/case-import/orchestrator.ts +78 -32
- package/app/components/actions/case-import/storage-operations.ts +97 -8
- package/app/components/actions/case-import/zip-processing.ts +159 -86
- package/app/components/actions/case-manage.ts +430 -8
- package/app/components/actions/confirm-export.ts +13 -4
- package/app/components/actions/generate-pdf.ts +10 -2
- package/app/components/actions/image-manage.ts +77 -44
- package/app/components/audit/user-audit-viewer.tsx +137 -945
- package/app/components/audit/user-audit.module.css +41 -0
- package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +207 -0
- package/app/components/audit/viewer/audit-filters-panel.tsx +307 -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 +123 -0
- package/app/components/audit/viewer/types.ts +1 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +186 -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/box-annotations/box-annotations.module.css +22 -18
- package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
- package/app/components/canvas/canvas.module.css +64 -54
- package/app/components/canvas/canvas.tsx +17 -16
- package/app/components/canvas/confirmation/confirmation.module.css +1 -0
- package/app/components/canvas/confirmation/confirmation.tsx +17 -47
- package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
- package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
- package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
- package/app/components/navbar/navbar.module.css +447 -0
- package/app/components/navbar/navbar.tsx +377 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +2 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +21 -51
- package/app/components/sidebar/case-export/case-export.module.css +1 -0
- package/app/components/sidebar/case-export/case-export.tsx +14 -77
- package/app/components/sidebar/case-import/case-import.module.css +25 -0
- package/app/components/sidebar/case-import/case-import.tsx +64 -40
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
- package/app/components/sidebar/cases/cases-modal.module.css +45 -9
- package/app/components/sidebar/cases/cases-modal.tsx +16 -16
- package/app/components/sidebar/cases/cases.module.css +62 -21
- package/app/components/sidebar/files/files-modal.module.css +46 -10
- package/app/components/sidebar/files/files-modal.tsx +22 -23
- package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
- package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
- package/app/components/sidebar/notes/notes-modal.tsx +18 -17
- package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
- package/app/components/sidebar/notes/notes.module.css +155 -0
- package/app/components/sidebar/sidebar-container.tsx +15 -28
- package/app/components/sidebar/sidebar.module.css +7 -71
- package/app/components/sidebar/sidebar.tsx +24 -125
- package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
- 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 +9 -6
- package/app/components/user/inactivity-warning.tsx +15 -2
- package/app/components/user/manage-profile.module.css +2 -0
- package/app/components/user/manage-profile.tsx +108 -40
- package/app/hooks/useOverlayDismiss.ts +116 -0
- package/app/routes/auth/login.example.tsx +19 -8
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/auth/passwordReset.module.css +23 -13
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +477 -31
- package/app/routes.ts +7 -0
- package/app/services/audit/audit-export-csv.ts +2 -0
- package/app/services/audit/audit.service.ts +202 -32
- package/app/services/audit/builders/audit-entry-builder.ts +2 -1
- package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
- package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
- package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -0
- package/app/services/audit/builders/index.ts +1 -0
- package/app/types/audit.ts +5 -2
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -0
- package/app/types/user.ts +1 -0
- package/app/utils/data/permissions.ts +17 -1
- package/app/utils/forensics/audit-export-signature.ts +5 -1
- package/app/utils/forensics/confirmation-signature.ts +3 -0
- package/app/utils/forensics/export-verification.ts +497 -22
- package/functions/api/pdf/[[path]].ts +32 -1
- package/load-context.ts +9 -0
- package/package.json +6 -2
- package/primershear.emails.example +6 -0
- package/scripts/deploy-pages-secrets.sh +6 -0
- package/scripts/deploy-primershear-emails.sh +167 -0
- package/worker-configuration.d.ts +7493 -7491
- package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/worker-configuration.d.ts +7448 -11323
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +7447 -11322
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
- package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
- package/workers/pdf-worker/src/report-types.ts +3 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/user-worker.example.ts +6 -1
- package/workers/user-worker/worker-configuration.d.ts +7448 -11323
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/public/.well-known/keybase.txt +0 -56
|
@@ -101,6 +101,27 @@
|
|
|
101
101
|
padding: 20px;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
.archivedNotice,
|
|
105
|
+
.archivedWarning {
|
|
106
|
+
margin-bottom: 16px;
|
|
107
|
+
padding: 12px 14px;
|
|
108
|
+
border-radius: 6px;
|
|
109
|
+
border: 1px solid;
|
|
110
|
+
font-size: 0.9rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.archivedNotice {
|
|
114
|
+
background: color-mix(in lab, var(--primary) 8%, transparent);
|
|
115
|
+
border-color: color-mix(in lab, var(--primary) 35%, transparent);
|
|
116
|
+
color: var(--textBody);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.archivedWarning {
|
|
120
|
+
background: color-mix(in lab, var(--warning) 12%, transparent);
|
|
121
|
+
border-color: color-mix(in lab, var(--warning) 40%, transparent);
|
|
122
|
+
color: var(--text);
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
/* Loading & Error States */
|
|
105
126
|
.loading,
|
|
106
127
|
.error {
|
|
@@ -531,6 +552,26 @@
|
|
|
531
552
|
word-break: break-word;
|
|
532
553
|
}
|
|
533
554
|
|
|
555
|
+
.badgeTag {
|
|
556
|
+
display: inline-flex;
|
|
557
|
+
align-items: center;
|
|
558
|
+
padding: 2px 10px;
|
|
559
|
+
border-radius: 999px;
|
|
560
|
+
border: 1px solid color-mix(in lab, var(--primary) 35%, transparent);
|
|
561
|
+
background: color-mix(in lab, var(--primary) 12%, transparent);
|
|
562
|
+
color: color-mix(in lab, var(--primary) 70%, var(--text));
|
|
563
|
+
font-size: 0.78rem;
|
|
564
|
+
font-weight: var(--fontWeightMedium);
|
|
565
|
+
letter-spacing: 0.2px;
|
|
566
|
+
line-height: 1.4;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.badgeTagMuted {
|
|
570
|
+
border-color: color-mix(in lab, var(--textLight) 25%, transparent);
|
|
571
|
+
background: color-mix(in lab, var(--backgroundLight) 70%, transparent);
|
|
572
|
+
color: var(--textLight);
|
|
573
|
+
}
|
|
574
|
+
|
|
534
575
|
.severity {
|
|
535
576
|
font-weight: var(--fontWeightMedium);
|
|
536
577
|
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,207 @@
|
|
|
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.action === 'confirmation-import' && entry.details.reviewerBadgeId && (
|
|
48
|
+
<div className={styles.detailRow}>
|
|
49
|
+
<span className={styles.detailLabel}>Reviewer Badge/ID:</span>
|
|
50
|
+
<span className={styles.badgeTag}>{entry.details.reviewerBadgeId}</span>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{entry.result === 'failure' && entry.details.validationErrors.length > 0 && (
|
|
55
|
+
<div className={styles.detailRow}>
|
|
56
|
+
<span className={styles.detailLabel}>Error:</span>
|
|
57
|
+
<span className={styles.detailValue}>{entry.details.validationErrors[0]}</span>
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{(entry.action === 'user-login' || entry.action === 'user-logout') && entry.details.sessionDetails && (
|
|
62
|
+
<>
|
|
63
|
+
{entry.details.sessionDetails.userAgent && (
|
|
64
|
+
<div className={styles.detailRow}>
|
|
65
|
+
<span className={styles.detailLabel}>User Agent:</span>
|
|
66
|
+
<span className={styles.detailValue}>{entry.details.sessionDetails.userAgent}</span>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{entry.action === 'security-violation' && entry.details.securityDetails && (
|
|
73
|
+
<>
|
|
74
|
+
<div className={styles.detailRow}>
|
|
75
|
+
<span className={styles.detailLabel}>Severity:</span>
|
|
76
|
+
<span
|
|
77
|
+
className={`${styles.detailValue} ${styles.severity} ${styles[entry.details.securityDetails.severity || 'low']}`}
|
|
78
|
+
>
|
|
79
|
+
{(entry.details.securityDetails.severity || 'low').toUpperCase()}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
{entry.details.securityDetails.incidentType && (
|
|
83
|
+
<div className={styles.detailRow}>
|
|
84
|
+
<span className={styles.detailLabel}>Type:</span>
|
|
85
|
+
<span className={styles.detailValue}>{entry.details.securityDetails.incidentType}</span>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{(entry.action === 'file-upload' || entry.action === 'file-delete' || entry.action === 'file-access') && entry.details.fileDetails && (
|
|
92
|
+
<>
|
|
93
|
+
{entry.details.fileDetails.fileId && (
|
|
94
|
+
<div className={styles.detailRow}>
|
|
95
|
+
<span className={styles.detailLabel}>File ID:</span>
|
|
96
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{entry.details.fileDetails.originalFileName && (
|
|
101
|
+
<div className={styles.detailRow}>
|
|
102
|
+
<span className={styles.detailLabel}>Original Filename:</span>
|
|
103
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{entry.details.fileDetails.fileSize > 0 && (
|
|
108
|
+
<div className={styles.detailRow}>
|
|
109
|
+
<span className={styles.detailLabel}>File Size:</span>
|
|
110
|
+
<span className={styles.detailValue}>
|
|
111
|
+
{(entry.details.fileDetails.fileSize / 1024 / 1024).toFixed(2)} MB
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{entry.details.fileDetails.uploadMethod && (
|
|
117
|
+
<div className={styles.detailRow}>
|
|
118
|
+
<span className={styles.detailLabel}>
|
|
119
|
+
{entry.action === 'file-access' ? 'Access Method' : 'Upload Method'}:
|
|
120
|
+
</span>
|
|
121
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.uploadMethod}</span>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{entry.details.fileDetails.deleteReason && (
|
|
126
|
+
<div className={styles.detailRow}>
|
|
127
|
+
<span className={styles.detailLabel}>Reason:</span>
|
|
128
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.deleteReason}</span>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{entry.details.fileDetails.sourceLocation && entry.action === 'file-access' && (
|
|
133
|
+
<div className={styles.detailRow}>
|
|
134
|
+
<span className={styles.detailLabel}>Access Source:</span>
|
|
135
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.sourceLocation}</span>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{(entry.action === 'annotation-create' || entry.action === 'annotation-edit' || entry.action === 'annotation-delete') && entry.details.fileDetails && (
|
|
142
|
+
<>
|
|
143
|
+
{entry.details.fileDetails.fileId && (
|
|
144
|
+
<div className={styles.detailRow}>
|
|
145
|
+
<span className={styles.detailLabel}>File ID:</span>
|
|
146
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{entry.details.fileDetails.originalFileName && (
|
|
151
|
+
<div className={styles.detailRow}>
|
|
152
|
+
<span className={styles.detailLabel}>Original Filename:</span>
|
|
153
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{entry.details.annotationDetails?.annotationType && (
|
|
158
|
+
<div className={styles.detailRow}>
|
|
159
|
+
<span className={styles.detailLabel}>Annotation Type:</span>
|
|
160
|
+
<span className={styles.detailValue}>{entry.details.annotationDetails.annotationType}</span>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{entry.details.annotationDetails?.tool && (
|
|
165
|
+
<div className={styles.detailRow}>
|
|
166
|
+
<span className={styles.detailLabel}>Tool:</span>
|
|
167
|
+
<span className={styles.detailValue}>{entry.details.annotationDetails.tool}</span>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
|
|
174
|
+
<>
|
|
175
|
+
{entry.details.fileDetails.fileId && (
|
|
176
|
+
<div className={styles.detailRow}>
|
|
177
|
+
<span className={styles.detailLabel}>
|
|
178
|
+
{entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}
|
|
179
|
+
</span>
|
|
180
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{entry.details.fileDetails.originalFileName && (
|
|
185
|
+
<div className={styles.detailRow}>
|
|
186
|
+
<span className={styles.detailLabel}>
|
|
187
|
+
{entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}
|
|
188
|
+
</span>
|
|
189
|
+
<span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{entry.action === 'confirm' && entry.details.confirmationId && (
|
|
194
|
+
<div className={styles.detailRow}>
|
|
195
|
+
<span className={styles.detailLabel}>Confirmation ID:</span>
|
|
196
|
+
<span className={styles.detailValue}>{entry.details.confirmationId}</span>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
))
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
@@ -0,0 +1,307 @@
|
|
|
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-archive">Case Archive</option>
|
|
263
|
+
<option value="case-export">Case Export</option>
|
|
264
|
+
<option value="case-import">Case Import</option>
|
|
265
|
+
</optgroup>
|
|
266
|
+
<optgroup label="File Operations">
|
|
267
|
+
<option value="file-upload">File Upload</option>
|
|
268
|
+
<option value="file-access">File Access</option>
|
|
269
|
+
<option value="file-delete">File Delete</option>
|
|
270
|
+
</optgroup>
|
|
271
|
+
<optgroup label="Annotations">
|
|
272
|
+
<option value="annotation-create">Annotation Create</option>
|
|
273
|
+
<option value="annotation-edit">Annotation Edit</option>
|
|
274
|
+
<option value="annotation-delete">Annotation Delete</option>
|
|
275
|
+
</optgroup>
|
|
276
|
+
<optgroup label="Confirmation Activity">
|
|
277
|
+
<option value="confirmation-create">Confirmation Create</option>
|
|
278
|
+
<option value="confirmation-export">Confirmation Export</option>
|
|
279
|
+
<option value="confirmation-import">Confirmation Import</option>
|
|
280
|
+
</optgroup>
|
|
281
|
+
<optgroup label="Documents">
|
|
282
|
+
<option value="pdf-generate">PDF Generate</option>
|
|
283
|
+
</optgroup>
|
|
284
|
+
<optgroup label="Security">
|
|
285
|
+
<option value="security-violation">Security Violation</option>
|
|
286
|
+
</optgroup>
|
|
287
|
+
</select>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div className={styles.filterGroup}>
|
|
291
|
+
<label htmlFor="resultFilter">Result:</label>
|
|
292
|
+
<select
|
|
293
|
+
id="resultFilter"
|
|
294
|
+
value={filterResult}
|
|
295
|
+
onChange={(e) => onFilterResultChange(e.target.value as AuditResult | 'all')}
|
|
296
|
+
className={styles.filterSelect}
|
|
297
|
+
>
|
|
298
|
+
<option value="all">All Results</option>
|
|
299
|
+
<option value="success">Success</option>
|
|
300
|
+
<option value="failure">Failure</option>
|
|
301
|
+
<option value="warning">Warning</option>
|
|
302
|
+
<option value="blocked">Blocked</option>
|
|
303
|
+
</select>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
};
|
|
@@ -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
|
+
};
|