@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
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
align-items: center;
|
|
11
11
|
z-index: 9999;
|
|
12
12
|
backdrop-filter: blur(2px);
|
|
13
|
+
cursor: default;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
.modal {
|
|
17
|
+
position: relative;
|
|
16
18
|
background: #ffffff;
|
|
17
19
|
border-radius: 12px;
|
|
18
20
|
padding: 2rem;
|
|
@@ -21,6 +23,7 @@
|
|
|
21
23
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
|
22
24
|
border: 1px solid #e0e0e0;
|
|
23
25
|
animation: slideIn 0.3s ease-out;
|
|
26
|
+
cursor: default;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
@keyframes slideIn {
|
|
@@ -62,7 +65,7 @@
|
|
|
62
65
|
font-size: 2rem;
|
|
63
66
|
font-weight: bold;
|
|
64
67
|
color: #dc3545;
|
|
65
|
-
font-family:
|
|
68
|
+
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
|
66
69
|
background: rgba(220, 53, 69, 0.1);
|
|
67
70
|
padding: 1rem;
|
|
68
71
|
border-radius: 8px;
|
|
@@ -118,25 +121,25 @@
|
|
|
118
121
|
.overlay {
|
|
119
122
|
background-color: rgba(0, 0, 0, 0.9);
|
|
120
123
|
}
|
|
121
|
-
|
|
124
|
+
|
|
122
125
|
.modal {
|
|
123
126
|
background: #2d2d2d;
|
|
124
127
|
border-color: #404040;
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
|
|
127
130
|
.header h3 {
|
|
128
131
|
color: #ffffff;
|
|
129
132
|
}
|
|
130
|
-
|
|
133
|
+
|
|
131
134
|
.content p {
|
|
132
135
|
color: #cccccc;
|
|
133
136
|
}
|
|
134
|
-
|
|
137
|
+
|
|
135
138
|
.signOutButton {
|
|
136
139
|
color: #adb5bd;
|
|
137
140
|
border-color: #6c757d;
|
|
138
141
|
}
|
|
139
|
-
|
|
142
|
+
|
|
140
143
|
.signOutButton:hover {
|
|
141
144
|
background: #404040;
|
|
142
145
|
color: #ffffff;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
2
3
|
import styles from './inactivity-warning.module.css';
|
|
3
4
|
|
|
4
5
|
interface InactivityWarningProps {
|
|
@@ -15,6 +16,15 @@ export const InactivityWarning = ({
|
|
|
15
16
|
onSignOut
|
|
16
17
|
}: InactivityWarningProps) => {
|
|
17
18
|
const [countdown, setCountdown] = useState(remainingSeconds);
|
|
19
|
+
const {
|
|
20
|
+
requestClose,
|
|
21
|
+
overlayProps,
|
|
22
|
+
getCloseButtonProps,
|
|
23
|
+
} = useOverlayDismiss({
|
|
24
|
+
isOpen,
|
|
25
|
+
onClose: onExtendSession,
|
|
26
|
+
closeOnBackdrop: false,
|
|
27
|
+
});
|
|
18
28
|
|
|
19
29
|
useEffect(() => {
|
|
20
30
|
setCountdown(remainingSeconds);
|
|
@@ -46,8 +56,11 @@ export const InactivityWarning = ({
|
|
|
46
56
|
const seconds = countdown % 60;
|
|
47
57
|
|
|
48
58
|
return (
|
|
49
|
-
<div className={styles.overlay}>
|
|
59
|
+
<div className={styles.overlay} aria-label="Close inactivity warning" {...overlayProps}>
|
|
50
60
|
<div className={styles.modal}>
|
|
61
|
+
<button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close inactivity warning' })}>
|
|
62
|
+
×
|
|
63
|
+
</button>
|
|
51
64
|
<div className={styles.header}>
|
|
52
65
|
<h3>Session Timeout Warning</h3>
|
|
53
66
|
</div>
|
|
@@ -66,7 +79,7 @@ export const InactivityWarning = ({
|
|
|
66
79
|
|
|
67
80
|
<div className={styles.actions}>
|
|
68
81
|
<button
|
|
69
|
-
onClick={
|
|
82
|
+
onClick={requestClose}
|
|
70
83
|
className={styles.extendButton}
|
|
71
84
|
>
|
|
72
85
|
Extend Session
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
justify-content: center;
|
|
8
8
|
align-items: center;
|
|
9
9
|
z-index: var(--zIndex5);
|
|
10
|
+
cursor: default;
|
|
10
11
|
transition: background-color var(--durationM) var(--bezierFastoutSlowin);
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
flex-direction: column;
|
|
23
24
|
transition: background-color var(--durationM) var(--bezierFastoutSlowin);
|
|
24
25
|
overflow: hidden;
|
|
26
|
+
cursor: default;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/* Modal Header */
|
|
@@ -4,10 +4,12 @@ import { PasswordReset } from '~/routes/auth/passwordReset';
|
|
|
4
4
|
import { DeleteAccount } from './delete-account';
|
|
5
5
|
import { UserAuditViewer } from '../audit/user-audit-viewer';
|
|
6
6
|
import { AuthContext } from '~/contexts/auth.context';
|
|
7
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
7
8
|
import { getUserData, updateUserData } from '~/utils/data';
|
|
8
9
|
import { auditService } from '~/services/audit';
|
|
9
10
|
import { handleAuthError, ERROR_MESSAGES } from '~/services/firebase/errors';
|
|
10
|
-
import { FormField, FormButton
|
|
11
|
+
import { FormField, FormButton } from '../form';
|
|
12
|
+
import { Toast } from '~/components/toast/toast';
|
|
11
13
|
import { MfaPhoneUpdateSection } from './mfa-phone-update';
|
|
12
14
|
import styles from './manage-profile.module.css';
|
|
13
15
|
|
|
@@ -19,29 +21,33 @@ interface ManageProfileProps {
|
|
|
19
21
|
export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
20
22
|
const { user } = useContext(AuthContext);
|
|
21
23
|
const [displayName, setDisplayName] = useState(user?.displayName || '');
|
|
24
|
+
const [badgeId, setBadgeId] = useState('');
|
|
25
|
+
const [initialBadgeId, setInitialBadgeId] = useState('');
|
|
22
26
|
const [company, setCompany] = useState('');
|
|
23
27
|
const [email, setEmail] = useState('');
|
|
24
28
|
const [isLoading, setIsLoading] = useState(false);
|
|
25
29
|
const [isMfaBusy, setIsMfaBusy] = useState(false);
|
|
26
|
-
const [
|
|
27
|
-
const [
|
|
30
|
+
const [showToast, setShowToast] = useState(false);
|
|
31
|
+
const [toastMessage, setToastMessage] = useState('');
|
|
32
|
+
const [toastType, setToastType] = useState<'success' | 'error'>('success');
|
|
28
33
|
const [showResetForm, setShowResetForm] = useState(false);
|
|
29
34
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
30
35
|
const [showAuditViewer, setShowAuditViewer] = useState(false);
|
|
31
36
|
const isCloseBlocked = isMfaBusy || isLoading;
|
|
37
|
+
const {
|
|
38
|
+
requestClose,
|
|
39
|
+
handleOverlayMouseDown,
|
|
40
|
+
handleOverlayKeyDown
|
|
41
|
+
} = useOverlayDismiss({
|
|
42
|
+
isOpen,
|
|
43
|
+
onClose,
|
|
44
|
+
canDismiss: !isCloseBlocked
|
|
45
|
+
});
|
|
32
46
|
|
|
33
47
|
const handleMfaBusyChange = useCallback((isBusy: boolean) => {
|
|
34
48
|
setIsMfaBusy(isBusy);
|
|
35
49
|
}, []);
|
|
36
50
|
|
|
37
|
-
const handleCloseRequest = () => {
|
|
38
|
-
if (isCloseBlocked) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
onClose();
|
|
43
|
-
};
|
|
44
|
-
|
|
45
51
|
useEffect(() => {
|
|
46
52
|
if (isOpen && user) {
|
|
47
53
|
const loadUserData = async () => {
|
|
@@ -51,6 +57,18 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
51
57
|
if (userData) {
|
|
52
58
|
setCompany(userData.company || '');
|
|
53
59
|
setEmail(userData.email || '');
|
|
60
|
+
const storedBadgeId = userData.badgeId || '';
|
|
61
|
+
setBadgeId(storedBadgeId);
|
|
62
|
+
setInitialBadgeId(storedBadgeId);
|
|
63
|
+
|
|
64
|
+
if (userData.badgeId === undefined) {
|
|
65
|
+
try {
|
|
66
|
+
await updateUserData(user, { badgeId: '' });
|
|
67
|
+
setInitialBadgeId('');
|
|
68
|
+
} catch (badgeInitError) {
|
|
69
|
+
console.error('Failed to initialize badge ID field:', badgeInitError);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
54
72
|
}
|
|
55
73
|
} catch (err) {
|
|
56
74
|
console.error('Failed to load user data:', err);
|
|
@@ -61,29 +79,14 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
61
79
|
}
|
|
62
80
|
}, [isOpen, user]);
|
|
63
81
|
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const handleEscape = (e: KeyboardEvent) => {
|
|
66
|
-
if (e.key === 'Escape' && isOpen && !isCloseBlocked) {
|
|
67
|
-
onClose();
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (isOpen) {
|
|
72
|
-
document.addEventListener('keydown', handleEscape);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return () => {
|
|
76
|
-
document.removeEventListener('keydown', handleEscape);
|
|
77
|
-
};
|
|
78
|
-
}, [isOpen, isCloseBlocked, onClose]);
|
|
79
|
-
|
|
80
82
|
const handleUpdateProfile = async (e: React.FormEvent) => {
|
|
81
83
|
e.preventDefault();
|
|
82
84
|
setIsLoading(true);
|
|
83
|
-
|
|
84
|
-
setSuccess('');
|
|
85
|
+
setShowToast(false);
|
|
85
86
|
|
|
86
87
|
const oldDisplayName = user?.displayName || '';
|
|
88
|
+
const oldBadgeId = initialBadgeId;
|
|
89
|
+
const normalizedBadgeId = badgeId.trim();
|
|
87
90
|
|
|
88
91
|
try {
|
|
89
92
|
if (!user) throw new Error(ERROR_MESSAGES.NO_USER);
|
|
@@ -98,6 +101,7 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
98
101
|
email: user.email,
|
|
99
102
|
firstName: firstName || '',
|
|
100
103
|
lastName: lastName || '',
|
|
104
|
+
badgeId: normalizedBadgeId,
|
|
101
105
|
});
|
|
102
106
|
|
|
103
107
|
await auditService.logUserProfileUpdate(
|
|
@@ -105,10 +109,33 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
105
109
|
'displayName',
|
|
106
110
|
oldDisplayName,
|
|
107
111
|
displayName,
|
|
108
|
-
'success'
|
|
112
|
+
'success',
|
|
113
|
+
undefined,
|
|
114
|
+
[],
|
|
115
|
+
normalizedBadgeId
|
|
109
116
|
);
|
|
110
117
|
|
|
111
|
-
|
|
118
|
+
if (oldBadgeId !== normalizedBadgeId) {
|
|
119
|
+
await auditService.logUserProfileUpdate(
|
|
120
|
+
user,
|
|
121
|
+
'badgeId',
|
|
122
|
+
oldBadgeId,
|
|
123
|
+
normalizedBadgeId,
|
|
124
|
+
'success',
|
|
125
|
+
undefined,
|
|
126
|
+
[],
|
|
127
|
+
normalizedBadgeId
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setInitialBadgeId(normalizedBadgeId);
|
|
132
|
+
|
|
133
|
+
setToastType('success');
|
|
134
|
+
setToastMessage(ERROR_MESSAGES.PROFILE_UPDATED);
|
|
135
|
+
setShowToast(true);
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
window.location.reload();
|
|
138
|
+
}, 1500);
|
|
112
139
|
} catch (err) {
|
|
113
140
|
const { message } = handleAuthError(err);
|
|
114
141
|
|
|
@@ -119,10 +146,26 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
119
146
|
displayName,
|
|
120
147
|
'failure',
|
|
121
148
|
undefined,
|
|
122
|
-
[message]
|
|
149
|
+
[message],
|
|
150
|
+
normalizedBadgeId
|
|
123
151
|
);
|
|
124
152
|
|
|
125
|
-
|
|
153
|
+
if (oldBadgeId !== normalizedBadgeId) {
|
|
154
|
+
await auditService.logUserProfileUpdate(
|
|
155
|
+
user!,
|
|
156
|
+
'badgeId',
|
|
157
|
+
oldBadgeId,
|
|
158
|
+
normalizedBadgeId,
|
|
159
|
+
'failure',
|
|
160
|
+
undefined,
|
|
161
|
+
[message],
|
|
162
|
+
normalizedBadgeId
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setToastType('error');
|
|
167
|
+
setToastMessage(message);
|
|
168
|
+
setShowToast(true);
|
|
126
169
|
} finally {
|
|
127
170
|
setIsLoading(false);
|
|
128
171
|
}
|
|
@@ -158,19 +201,31 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
158
201
|
}
|
|
159
202
|
|
|
160
203
|
return (
|
|
161
|
-
|
|
162
|
-
|
|
204
|
+
<>
|
|
205
|
+
<Toast
|
|
206
|
+
message={toastMessage}
|
|
207
|
+
type={toastType}
|
|
208
|
+
isVisible={showToast}
|
|
209
|
+
onClose={() => setShowToast(false)}
|
|
210
|
+
/>
|
|
211
|
+
<div
|
|
212
|
+
className={styles.modalOverlay}
|
|
213
|
+
onMouseDown={handleOverlayMouseDown}
|
|
214
|
+
onKeyDown={handleOverlayKeyDown}
|
|
215
|
+
role="button"
|
|
216
|
+
tabIndex={0}
|
|
217
|
+
aria-label="Close manage profile dialog"
|
|
218
|
+
>
|
|
163
219
|
<div
|
|
164
220
|
className={styles.modal}
|
|
165
221
|
role="dialog"
|
|
166
222
|
aria-modal="true"
|
|
167
223
|
aria-labelledby="modal-title"
|
|
168
|
-
onClick={(e) => e.stopPropagation()}
|
|
169
224
|
>
|
|
170
225
|
<header className={styles.modalHeader}>
|
|
171
226
|
<h1 id="modal-title">Manage Profile</h1>
|
|
172
227
|
<button
|
|
173
|
-
onClick={
|
|
228
|
+
onClick={requestClose}
|
|
174
229
|
className={styles.closeButton}
|
|
175
230
|
aria-label="Close modal"
|
|
176
231
|
disabled={isCloseBlocked}
|
|
@@ -192,6 +247,21 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
192
247
|
required
|
|
193
248
|
/>
|
|
194
249
|
|
|
250
|
+
<div className={styles.formGroup}>
|
|
251
|
+
<label htmlFor="badgeId">Badge/ID #</label>
|
|
252
|
+
<input
|
|
253
|
+
id="badgeId"
|
|
254
|
+
type="text"
|
|
255
|
+
value={badgeId}
|
|
256
|
+
onChange={(e) => setBadgeId(e.target.value)}
|
|
257
|
+
className={styles.input}
|
|
258
|
+
autoComplete="off"
|
|
259
|
+
/>
|
|
260
|
+
<p className={styles.helpText}>
|
|
261
|
+
Enter your Badge/ID number for confirmations and reports. This can be updated as needed.
|
|
262
|
+
</p>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
195
265
|
<div className={styles.formGroup}>
|
|
196
266
|
<label htmlFor="company">Lab/Company Name</label>
|
|
197
267
|
<input
|
|
@@ -227,9 +297,6 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
227
297
|
|
|
228
298
|
<MfaPhoneUpdateSection user={user} isOpen={isOpen} onBusyChange={handleMfaBusyChange} />
|
|
229
299
|
|
|
230
|
-
{error && <FormMessage type="error" message={error} />}
|
|
231
|
-
{success && <FormMessage type="success" message={success} />}
|
|
232
|
-
|
|
233
300
|
<div className={styles.buttonGroup}>
|
|
234
301
|
<FormButton variant="primary" type="submit" isLoading={isLoading} loadingText="Updating...">
|
|
235
302
|
Update Profile
|
|
@@ -247,5 +314,6 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
247
314
|
</form>
|
|
248
315
|
</div>
|
|
249
316
|
</div>
|
|
317
|
+
</>
|
|
250
318
|
);
|
|
251
319
|
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useCallback, useEffect, type CSSProperties, type KeyboardEventHandler, type MouseEventHandler } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UseOverlayDismissOptions {
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
canDismiss?: boolean;
|
|
7
|
+
closeOnEscape?: boolean;
|
|
8
|
+
closeOnBackdrop?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface CloseButtonOptions {
|
|
12
|
+
ariaLabel?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sharedCloseButtonStyle: CSSProperties = {
|
|
17
|
+
position: 'absolute',
|
|
18
|
+
top: '0.6rem',
|
|
19
|
+
right: '0.6rem',
|
|
20
|
+
width: '1.9rem',
|
|
21
|
+
height: '1.9rem',
|
|
22
|
+
borderRadius: '999px',
|
|
23
|
+
border: '1px solid #d6dce2',
|
|
24
|
+
background: '#f8f9fa',
|
|
25
|
+
color: '#495057',
|
|
26
|
+
fontSize: '1.2rem',
|
|
27
|
+
lineHeight: 1,
|
|
28
|
+
display: 'inline-flex',
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
justifyContent: 'center',
|
|
31
|
+
cursor: 'pointer',
|
|
32
|
+
zIndex: 1,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const useOverlayDismiss = ({
|
|
36
|
+
isOpen,
|
|
37
|
+
onClose,
|
|
38
|
+
canDismiss = true,
|
|
39
|
+
closeOnEscape = true,
|
|
40
|
+
closeOnBackdrop = true
|
|
41
|
+
}: UseOverlayDismissOptions) => {
|
|
42
|
+
const requestClose = useCallback(() => {
|
|
43
|
+
if (!canDismiss) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onClose();
|
|
48
|
+
}, [canDismiss, onClose]);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!isOpen || !closeOnEscape || !canDismiss) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
56
|
+
if (event.key === 'Escape') {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
requestClose();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
document.addEventListener('keydown', handleEscape);
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
document.removeEventListener('keydown', handleEscape);
|
|
66
|
+
};
|
|
67
|
+
}, [isOpen, closeOnEscape, canDismiss, requestClose]);
|
|
68
|
+
|
|
69
|
+
const handleOverlayMouseDown = useCallback<MouseEventHandler<HTMLDivElement>>((event) => {
|
|
70
|
+
if (!closeOnBackdrop || event.target !== event.currentTarget) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
requestClose();
|
|
75
|
+
}, [closeOnBackdrop, requestClose]);
|
|
76
|
+
|
|
77
|
+
const handleOverlayKeyDown = useCallback<KeyboardEventHandler<HTMLDivElement>>((event) => {
|
|
78
|
+
if (!closeOnBackdrop || event.target !== event.currentTarget) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
requestClose();
|
|
85
|
+
}
|
|
86
|
+
}, [closeOnBackdrop, requestClose]);
|
|
87
|
+
|
|
88
|
+
const overlayProps = {
|
|
89
|
+
role: 'button' as const,
|
|
90
|
+
tabIndex: 0,
|
|
91
|
+
onMouseDown: handleOverlayMouseDown,
|
|
92
|
+
onKeyDown: handleOverlayKeyDown,
|
|
93
|
+
style: { cursor: 'default' as const },
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const getCloseButtonProps = useCallback((options?: CloseButtonOptions) => {
|
|
97
|
+
const ariaLabel = options?.ariaLabel || 'Close modal';
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
type: 'button' as const,
|
|
101
|
+
onClick: requestClose,
|
|
102
|
+
disabled: !canDismiss,
|
|
103
|
+
'aria-label': ariaLabel,
|
|
104
|
+
title: options?.title || ariaLabel,
|
|
105
|
+
style: sharedCloseButtonStyle,
|
|
106
|
+
};
|
|
107
|
+
}, [requestClose, canDismiss]);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
requestClose,
|
|
111
|
+
handleOverlayMouseDown,
|
|
112
|
+
handleOverlayKeyDown,
|
|
113
|
+
overlayProps,
|
|
114
|
+
getCloseButtonProps,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
@@ -26,6 +26,7 @@ import { getUserData, createUser } from '~/utils/data';
|
|
|
26
26
|
import { auditService } from '~/services/audit';
|
|
27
27
|
import { generateUniqueId } from '~/utils/common';
|
|
28
28
|
import { evaluatePasswordPolicy, buildActionCodeSettings, userHasMFA } from '~/utils/auth';
|
|
29
|
+
import type { UserData } from '~/types';
|
|
29
30
|
|
|
30
31
|
const APP_CANONICAL_ORIGIN = 'PAGES_CUSTOM_DOMAIN';
|
|
31
32
|
const SOCIAL_IMAGE_PATH = '/social-image.png';
|
|
@@ -143,6 +144,7 @@ export const Login = () => {
|
|
|
143
144
|
const [error, setError] = useState('');
|
|
144
145
|
const [success, setSuccess] = useState('');
|
|
145
146
|
const [welcomeToastMessage, setWelcomeToastMessage] = useState('');
|
|
147
|
+
const [welcomeToastType, setWelcomeToastType] = useState<'success' | 'warning'>('success');
|
|
146
148
|
const [isWelcomeToastVisible, setIsWelcomeToastVisible] = useState(false);
|
|
147
149
|
const [isLogin, setIsLogin] = useState(true);
|
|
148
150
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -212,11 +214,9 @@ export const Login = () => {
|
|
|
212
214
|
};
|
|
213
215
|
|
|
214
216
|
// Check if user exists in the USER_DB using centralized function
|
|
215
|
-
const checkUserExists = async (currentUser: User): Promise<
|
|
217
|
+
const checkUserExists = async (currentUser: User): Promise<UserData | null> => {
|
|
216
218
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return userData !== null;
|
|
219
|
+
return await getUserData(currentUser);
|
|
220
220
|
} catch (error) {
|
|
221
221
|
console.error('Error checking user existence:', error);
|
|
222
222
|
// On network/API errors, throw error to prevent login
|
|
@@ -251,16 +251,19 @@ export const Login = () => {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// Check if user exists in the USER_DB
|
|
254
|
+
let hasBadgeId = true;
|
|
254
255
|
setIsCheckingUser(true);
|
|
255
256
|
try {
|
|
256
|
-
const
|
|
257
|
+
const userData = await checkUserExists(currentUser);
|
|
257
258
|
setIsCheckingUser(false);
|
|
258
259
|
|
|
259
|
-
if (!
|
|
260
|
+
if (!userData) {
|
|
260
261
|
handleSignOut();
|
|
261
262
|
setError('This account does not exist or has been deleted');
|
|
262
263
|
return;
|
|
263
264
|
}
|
|
265
|
+
|
|
266
|
+
hasBadgeId = Boolean(userData.badgeId?.trim());
|
|
264
267
|
} catch (error) {
|
|
265
268
|
setIsCheckingUser(false);
|
|
266
269
|
handleSignOut();
|
|
@@ -279,7 +282,13 @@ export const Login = () => {
|
|
|
279
282
|
setShowMfaEnrollment(false);
|
|
280
283
|
|
|
281
284
|
if (shouldShowWelcomeToastRef.current) {
|
|
282
|
-
|
|
285
|
+
if (hasBadgeId) {
|
|
286
|
+
setWelcomeToastType('success');
|
|
287
|
+
setWelcomeToastMessage(`Welcome to Striae, ${getUserFirstName(currentUser)}!`);
|
|
288
|
+
} else {
|
|
289
|
+
setWelcomeToastType('warning');
|
|
290
|
+
setWelcomeToastMessage('Your badge or ID number is not set. You can set one in Manage Profile.');
|
|
291
|
+
}
|
|
283
292
|
setIsWelcomeToastVisible(true);
|
|
284
293
|
shouldShowWelcomeToastRef.current = false;
|
|
285
294
|
}
|
|
@@ -302,6 +311,7 @@ export const Login = () => {
|
|
|
302
311
|
setShowMfaEnrollment(false);
|
|
303
312
|
setIsCheckingUser(false);
|
|
304
313
|
setIsWelcomeToastVisible(false);
|
|
314
|
+
setWelcomeToastType('success');
|
|
305
315
|
shouldShowWelcomeToastRef.current = false;
|
|
306
316
|
}
|
|
307
317
|
});
|
|
@@ -517,6 +527,7 @@ export const Login = () => {
|
|
|
517
527
|
setShowMfaVerification(false);
|
|
518
528
|
setMfaResolver(null);
|
|
519
529
|
setIsWelcomeToastVisible(false);
|
|
530
|
+
setWelcomeToastType('success');
|
|
520
531
|
shouldShowWelcomeToastRef.current = false;
|
|
521
532
|
} catch (err) {
|
|
522
533
|
console.error('Sign out error:', err);
|
|
@@ -762,7 +773,7 @@ export const Login = () => {
|
|
|
762
773
|
{!shouldHandleEmailAction && (
|
|
763
774
|
<Toast
|
|
764
775
|
message={welcomeToastMessage}
|
|
765
|
-
type=
|
|
776
|
+
type={welcomeToastType}
|
|
766
777
|
isVisible={isWelcomeToastVisible}
|
|
767
778
|
onClose={() => setIsWelcomeToastVisible(false)}
|
|
768
779
|
/>
|