@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.
Files changed (118) hide show
  1. package/.env.example +8 -0
  2. package/app/components/actions/case-export/core-export.ts +14 -8
  3. package/app/components/actions/case-export/data-processing.ts +1 -0
  4. package/app/components/actions/case-export/download-handlers.ts +7 -0
  5. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  6. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  7. package/app/components/actions/case-import/orchestrator.ts +78 -32
  8. package/app/components/actions/case-import/storage-operations.ts +97 -8
  9. package/app/components/actions/case-import/zip-processing.ts +159 -86
  10. package/app/components/actions/case-manage.ts +430 -8
  11. package/app/components/actions/confirm-export.ts +13 -4
  12. package/app/components/actions/generate-pdf.ts +10 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +137 -945
  15. package/app/components/audit/user-audit.module.css +41 -0
  16. package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
  17. package/app/components/audit/viewer/audit-entries-list.tsx +207 -0
  18. package/app/components/audit/viewer/audit-filters-panel.tsx +307 -0
  19. package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
  20. package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
  21. package/app/components/audit/viewer/audit-viewer-utils.ts +123 -0
  22. package/app/components/audit/viewer/types.ts +1 -0
  23. package/app/components/audit/viewer/use-audit-viewer-data.ts +186 -0
  24. package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
  25. package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
  26. package/app/components/auth/mfa-enrollment.module.css +13 -5
  27. package/app/components/auth/mfa-verification.module.css +13 -5
  28. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  29. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  30. package/app/components/canvas/canvas.module.css +64 -54
  31. package/app/components/canvas/canvas.tsx +17 -16
  32. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  33. package/app/components/canvas/confirmation/confirmation.tsx +17 -47
  34. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  35. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  36. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  37. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  38. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  39. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  40. package/app/components/navbar/navbar.module.css +447 -0
  41. package/app/components/navbar/navbar.tsx +377 -0
  42. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +2 -0
  43. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +21 -51
  44. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  45. package/app/components/sidebar/case-export/case-export.tsx +14 -77
  46. package/app/components/sidebar/case-import/case-import.module.css +25 -0
  47. package/app/components/sidebar/case-import/case-import.tsx +64 -40
  48. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  49. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  50. package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
  51. package/app/components/sidebar/cases/cases-modal.module.css +45 -9
  52. package/app/components/sidebar/cases/cases-modal.tsx +16 -16
  53. package/app/components/sidebar/cases/cases.module.css +62 -21
  54. package/app/components/sidebar/files/files-modal.module.css +46 -10
  55. package/app/components/sidebar/files/files-modal.tsx +22 -23
  56. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  57. package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
  58. package/app/components/sidebar/notes/notes-modal.tsx +18 -17
  59. package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
  60. package/app/components/sidebar/notes/notes.module.css +155 -0
  61. package/app/components/sidebar/sidebar-container.tsx +15 -28
  62. package/app/components/sidebar/sidebar.module.css +7 -71
  63. package/app/components/sidebar/sidebar.tsx +24 -125
  64. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  65. package/app/components/toast/toast.module.css +2 -1
  66. package/app/components/toast/toast.tsx +16 -11
  67. package/app/components/user/delete-account.tsx +10 -31
  68. package/app/components/user/inactivity-warning.module.css +9 -6
  69. package/app/components/user/inactivity-warning.tsx +15 -2
  70. package/app/components/user/manage-profile.module.css +2 -0
  71. package/app/components/user/manage-profile.tsx +108 -40
  72. package/app/hooks/useOverlayDismiss.ts +116 -0
  73. package/app/routes/auth/login.example.tsx +19 -8
  74. package/app/routes/auth/login.tsx +785 -774
  75. package/app/routes/auth/passwordReset.module.css +23 -13
  76. package/app/routes/striae/striae.module.css +10 -3
  77. package/app/routes/striae/striae.tsx +477 -31
  78. package/app/routes.ts +7 -0
  79. package/app/services/audit/audit-export-csv.ts +2 -0
  80. package/app/services/audit/audit.service.ts +202 -32
  81. package/app/services/audit/builders/audit-entry-builder.ts +2 -1
  82. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  83. package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
  84. package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -0
  85. package/app/services/audit/builders/index.ts +1 -0
  86. package/app/types/audit.ts +5 -2
  87. package/app/types/case.ts +29 -0
  88. package/app/types/import.ts +3 -0
  89. package/app/types/user.ts +1 -0
  90. package/app/utils/data/permissions.ts +17 -1
  91. package/app/utils/forensics/audit-export-signature.ts +5 -1
  92. package/app/utils/forensics/confirmation-signature.ts +3 -0
  93. package/app/utils/forensics/export-verification.ts +497 -22
  94. package/functions/api/pdf/[[path]].ts +32 -1
  95. package/load-context.ts +9 -0
  96. package/package.json +6 -2
  97. package/primershear.emails.example +6 -0
  98. package/scripts/deploy-pages-secrets.sh +6 -0
  99. package/scripts/deploy-primershear-emails.sh +167 -0
  100. package/worker-configuration.d.ts +7493 -7491
  101. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  102. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  103. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  104. package/workers/data-worker/wrangler.jsonc.example +1 -1
  105. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  106. package/workers/image-worker/wrangler.jsonc.example +1 -1
  107. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  108. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  109. package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
  110. package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
  111. package/workers/pdf-worker/src/report-types.ts +3 -0
  112. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  113. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  114. package/workers/user-worker/src/user-worker.example.ts +6 -1
  115. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  116. package/workers/user-worker/wrangler.jsonc.example +1 -1
  117. package/wrangler.toml.example +1 -1
  118. 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: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
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={onExtendSession}
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, FormMessage } from '../form';
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 [error, setError] = useState('');
27
- const [success, setSuccess] = useState('');
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
- setError('');
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
- setSuccess(ERROR_MESSAGES.PROFILE_UPDATED);
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
- setError(message);
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
- <div className={styles.modalOverlay} onClick={handleCloseRequest} role="presentation">
162
- {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */}
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={handleCloseRequest}
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<boolean> => {
217
+ const checkUserExists = async (currentUser: User): Promise<UserData | null> => {
216
218
  try {
217
- const userData = await getUserData(currentUser);
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 userExists = await checkUserExists(currentUser);
257
+ const userData = await checkUserExists(currentUser);
257
258
  setIsCheckingUser(false);
258
259
 
259
- if (!userExists) {
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
- setWelcomeToastMessage(`Welcome to Striae, ${getUserFirstName(currentUser)}!`);
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="success"
776
+ type={welcomeToastType}
766
777
  isVisible={isWelcomeToastVisible}
767
778
  onClose={() => setIsWelcomeToastVisible(false)}
768
779
  />