@striae-org/striae 5.3.2 → 5.4.1

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 (112) hide show
  1. package/app/components/auth/auth.module.css +531 -0
  2. package/app/components/auth/mfa-enrollment.tsx +132 -79
  3. package/app/components/auth/mfa-totp-enrollment.tsx +231 -0
  4. package/app/components/auth/mfa-verification.tsx +162 -33
  5. package/app/components/{sidebar/cases/cases-modal.tsx → navbar/case-modals/all-cases-modal.tsx} +4 -4
  6. package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -10
  7. package/app/components/navbar/case-modals/case-modal-shared.module.css +88 -0
  8. package/app/components/navbar/case-modals/delete-case-modal.tsx +9 -10
  9. package/app/components/navbar/case-modals/export-case-modal.tsx +9 -10
  10. package/app/components/navbar/case-modals/export-confirmations-modal.tsx +9 -10
  11. package/app/components/navbar/case-modals/open-case-modal.tsx +4 -4
  12. package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -10
  13. package/app/components/navbar/navbar.tsx +1 -1
  14. package/app/components/sidebar/files/delete-files-modal.tsx +3 -3
  15. package/app/components/sidebar/files/files-modal.module.css +29 -0
  16. package/app/components/sidebar/notes/{class-details-fields.tsx → class-details/class-details-fields.tsx} +1 -1
  17. package/app/components/sidebar/notes/{class-details-modal.tsx → class-details/class-details-modal.tsx} +1 -1
  18. package/app/components/sidebar/notes/{class-details-sections.tsx → class-details/class-details-sections.tsx} +1 -1
  19. package/app/components/sidebar/notes/notes-editor-form.tsx +2 -2
  20. package/app/components/sidebar/notes/notes-editor-modal.tsx +6 -6
  21. package/app/components/sidebar/notes/notes.module.css +52 -0
  22. package/app/components/toolbar/toolbar-color-selector.tsx +8 -8
  23. package/app/components/toolbar/toolbar.module.css +181 -2
  24. package/app/components/user/delete-account.tsx +7 -7
  25. package/app/components/user/inactivity-warning.tsx +6 -6
  26. package/app/components/user/manage-profile.tsx +18 -1
  27. package/app/components/user/mfa-enrolled-factors.tsx +117 -0
  28. package/app/components/user/mfa-phone-update.tsx +8 -4
  29. package/app/components/user/mfa-totp-section.tsx +446 -0
  30. package/app/components/user/user.module.css +665 -0
  31. package/app/routes/striae/striae.tsx +1 -1
  32. package/app/services/audit/audit.service.ts +1 -1
  33. package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
  34. package/app/services/firebase/errors.ts +2 -0
  35. package/app/utils/auth/mfa.ts +35 -1
  36. package/package.json +23 -28
  37. package/scripts/deploy-all.sh +166 -0
  38. package/scripts/deploy-config/modules/env-utils.sh +322 -0
  39. package/scripts/deploy-config/modules/keys.sh +404 -0
  40. package/scripts/deploy-config/modules/prompt.sh +375 -0
  41. package/scripts/deploy-config/modules/scaffolding.sh +310 -0
  42. package/scripts/deploy-config/modules/validation.sh +354 -0
  43. package/scripts/deploy-config.sh +236 -0
  44. package/scripts/deploy-pages-secrets.sh +231 -0
  45. package/scripts/deploy-pages.sh +34 -0
  46. package/scripts/deploy-primershear-emails.sh +167 -0
  47. package/scripts/deploy-worker-secrets.sh +385 -0
  48. package/scripts/dev.cjs +23 -0
  49. package/scripts/enable-totp-mfa.mjs +57 -0
  50. package/scripts/install-workers.sh +87 -0
  51. package/scripts/run-eslint.cjs +43 -0
  52. package/scripts/unenroll-totp-mfa.mjs +82 -0
  53. package/scripts/update-compatibility-dates.cjs +124 -0
  54. package/scripts/update-markdown-versions.cjs +43 -0
  55. package/workers/audit-worker/.editorconfig +12 -0
  56. package/workers/audit-worker/.prettierrc +6 -0
  57. package/workers/audit-worker/package.json +1 -1
  58. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  59. package/workers/data-worker/.editorconfig +12 -0
  60. package/workers/data-worker/.prettierrc +6 -0
  61. package/workers/data-worker/package.json +1 -1
  62. package/workers/data-worker/wrangler.jsonc.example +1 -1
  63. package/workers/image-worker/.editorconfig +12 -0
  64. package/workers/image-worker/.prettierrc +6 -0
  65. package/workers/image-worker/package.json +1 -1
  66. package/workers/image-worker/wrangler.jsonc.example +1 -1
  67. package/workers/pdf-worker/.editorconfig +12 -0
  68. package/workers/pdf-worker/.prettierrc +6 -0
  69. package/workers/pdf-worker/package.json +1 -1
  70. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  71. package/workers/user-worker/.editorconfig +12 -0
  72. package/workers/user-worker/.prettierrc +6 -0
  73. package/workers/user-worker/package.json +1 -1
  74. package/workers/user-worker/wrangler.jsonc.example +1 -1
  75. package/wrangler.toml.example +1 -1
  76. package/app/components/auth/mfa-enrollment.module.css +0 -276
  77. package/app/components/auth/mfa-verification.module.css +0 -259
  78. package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -34
  79. package/app/components/navbar/case-modals/delete-case-modal.module.css +0 -9
  80. package/app/components/navbar/case-modals/export-case-modal.module.css +0 -27
  81. package/app/components/navbar/case-modals/export-confirmations-modal.module.css +0 -24
  82. package/app/components/navbar/case-modals/open-case-modal.module.css +0 -82
  83. package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -9
  84. package/app/components/sidebar/files/delete-files-modal.module.css +0 -26
  85. package/app/components/sidebar/notes/notes-editor-modal.module.css +0 -49
  86. package/app/components/toolbar/toolbar-color-selector.module.css +0 -171
  87. package/app/components/user/delete-account.module.css +0 -277
  88. package/app/components/user/inactivity-warning.module.css +0 -148
  89. package/app/components/user/manage-profile.module.css +0 -192
  90. package/app/routes/auth/login.module.css +0 -523
  91. package/app/routes/auth/login.tsx +0 -705
  92. package/workers/audit-worker/worker-configuration.d.ts +0 -7448
  93. package/workers/data-worker/worker-configuration.d.ts +0 -7448
  94. package/workers/image-worker/worker-configuration.d.ts +0 -7448
  95. package/workers/pdf-worker/worker-configuration.d.ts +0 -7447
  96. package/workers/user-worker/worker-configuration.d.ts +0 -7450
  97. /package/app/components/{sidebar → navbar}/case-import/case-import.module.css +0 -0
  98. /package/app/components/{sidebar → navbar}/case-import/case-import.tsx +0 -0
  99. /package/app/components/{sidebar → navbar}/case-import/components/CasePreviewSection.tsx +0 -0
  100. /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationDialog.tsx +0 -0
  101. /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationPreviewSection.tsx +0 -0
  102. /package/app/components/{sidebar → navbar}/case-import/components/ExistingCaseSection.tsx +0 -0
  103. /package/app/components/{sidebar → navbar}/case-import/components/FileSelector.tsx +0 -0
  104. /package/app/components/{sidebar → navbar}/case-import/components/ProgressSection.tsx +0 -0
  105. /package/app/components/{sidebar → navbar}/case-import/hooks/useFilePreview.ts +0 -0
  106. /package/app/components/{sidebar → navbar}/case-import/hooks/useImportExecution.ts +0 -0
  107. /package/app/components/{sidebar → navbar}/case-import/hooks/useImportState.ts +0 -0
  108. /package/app/components/{sidebar → navbar}/case-import/index.ts +0 -0
  109. /package/app/components/{sidebar → navbar}/case-import/utils/file-validation.ts +0 -0
  110. /package/app/components/{sidebar/cases/cases-modal.module.css → navbar/case-modals/all-cases-modal.module.css} +0 -0
  111. /package/app/components/sidebar/notes/{class-details-shared.ts → class-details/class-details-shared.ts} +0 -0
  112. /package/app/components/sidebar/notes/{use-class-details-state.ts → class-details/use-class-details-state.ts} +0 -0
@@ -1,705 +0,0 @@
1
- import { useState, useEffect, useRef } from 'react';
2
- import { Link, useSearchParams } from 'react-router';
3
- import { auth } from '~/services/firebase';
4
- import {
5
- signInWithEmailAndPassword,
6
- createUserWithEmailAndPassword,
7
- onAuthStateChanged,
8
- sendEmailVerification,
9
- type User,
10
- updateProfile,
11
- getMultiFactorResolver,
12
- type MultiFactorResolver,
13
- type MultiFactorError
14
- } from 'firebase/auth';
15
- import { PasswordReset } from '~/routes/auth/passwordReset';
16
- import { EmailVerification } from '~/routes/auth/emailVerification';
17
- import { EmailActionHandler } from '~/routes/auth/emailActionHandler';
18
- import { handleAuthError } from '~/services/firebase/errors';
19
- import { MFAVerification } from '~/components/auth/mfa-verification';
20
- import { MFAEnrollment } from '~/components/auth/mfa-enrollment';
21
- import { Toast } from '~/components/toast/toast';
22
- import { Icon } from '~/components/icon/icon';
23
- import styles from './login.module.css';
24
- import { Striae } from '~/routes/striae/striae';
25
- import { getUserData, createUser } from '~/utils/data';
26
- import { auditService } from '~/services/audit';
27
- import { generateUniqueId } from '~/utils/common';
28
- import { evaluatePasswordPolicy, buildActionCodeSettings, userHasMFA } from '~/utils/auth';
29
- import type { UserData } from '~/types';
30
-
31
- const DEMO_COMPANY_NAME = 'STRIAE DEMO';
32
-
33
- const SUPPORTED_EMAIL_ACTION_MODES = new Set(['resetPassword', 'verifyEmail', 'recoverEmail']);
34
-
35
- const getUserFirstName = (user: User): string => {
36
- const displayName = user.displayName?.trim();
37
- if (displayName) {
38
- const [firstName] = displayName.split(/\s+/);
39
- if (firstName) {
40
- return firstName;
41
- }
42
- }
43
-
44
- const emailPrefix = user.email?.split('@')[0]?.trim();
45
- if (emailPrefix) {
46
- return emailPrefix;
47
- }
48
-
49
- return 'User';
50
- };
51
-
52
- export const Login = () => {
53
- const [searchParams] = useSearchParams();
54
- const shouldShowWelcomeToastRef = useRef(false);
55
-
56
- const [error, setError] = useState('');
57
- const [success, setSuccess] = useState('');
58
- const [welcomeToastMessage, setWelcomeToastMessage] = useState('');
59
- const [welcomeToastType, setWelcomeToastType] = useState<'success' | 'warning'>('success');
60
- const [isWelcomeToastVisible, setIsWelcomeToastVisible] = useState(false);
61
- const [isLogin, setIsLogin] = useState(true);
62
- const [isLoading, setIsLoading] = useState(false);
63
- const [isCheckingUser, setIsCheckingUser] = useState(false);
64
- const [user, setUser] = useState<User | null>(null);
65
- const [passwordStrength, setPasswordStrength] = useState('');
66
- const [isResetting, setIsResetting] = useState(false);
67
- const [showPassword, setShowPassword] = useState(false);
68
- const [showConfirmPassword, setShowConfirmPassword] = useState(false);
69
- const [isClient, setIsClient] = useState(false);
70
- const [firstName, setFirstName] = useState('');
71
- const [lastName, setLastName] = useState('');
72
- const [company, setCompany] = useState(DEMO_COMPANY_NAME);
73
- const [badgeId, setBadgeId] = useState('');
74
- const [confirmPasswordValue, setConfirmPasswordValue] = useState('');
75
-
76
- // MFA state
77
- const [mfaResolver, setMfaResolver] = useState<MultiFactorResolver | null>(null);
78
- const [showMfaVerification, setShowMfaVerification] = useState(false);
79
- const [showMfaEnrollment, setShowMfaEnrollment] = useState(false);
80
-
81
- const actionMode = searchParams.get('mode');
82
- const actionCode = searchParams.get('oobCode');
83
- const continueUrl = searchParams.get('continueUrl');
84
- const actionLang = searchParams.get('lang');
85
-
86
- const shouldHandleEmailAction = Boolean(
87
- actionMode &&
88
- actionCode &&
89
- SUPPORTED_EMAIL_ACTION_MODES.has(actionMode)
90
- );
91
-
92
- // Check if we're on the client side
93
- useEffect(() => {
94
- setIsClient(true);
95
- }, []);
96
-
97
- // Email validation with regex
98
- const validateRegistrationEmail = (email: string): { valid: boolean } => {
99
- const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
100
-
101
- if (!emailRegex.test(email)) {
102
- return { valid: false };
103
- }
104
-
105
- return { valid: true };
106
- };
107
-
108
- const checkPasswordStrength = (password: string, confirmPassword?: string): boolean => {
109
- const normalizedConfirmPassword = confirmPassword ?? '';
110
- if (password.length === 0 && normalizedConfirmPassword.length === 0) {
111
- setPasswordStrength('');
112
- return false;
113
- }
114
-
115
- const policy = evaluatePasswordPolicy(password, confirmPassword);
116
-
117
- setPasswordStrength(
118
- `Password must contain:
119
- ${!policy.hasMinLength ? '❌' : '✅'} At least 10 characters
120
- ${!policy.hasUpperCase ? '❌' : '✅'} Capital letters
121
- ${!policy.hasNumber ? '❌' : '✅'} Numbers
122
- ${!policy.hasSpecialChar ? '❌' : '✅'} Special characters${confirmPassword !== undefined ? `
123
- ${!policy.passwordsMatch ? '❌' : '✅'} Passwords must match` : ''}`
124
- );
125
-
126
- return policy.isStrong;
127
- };
128
-
129
- // Check if user exists in the USER_DB using centralized function
130
- const checkUserExists = async (currentUser: User): Promise<UserData | null> => {
131
- try {
132
- return await getUserData(currentUser);
133
- } catch (error) {
134
- console.error('Error checking user existence:', error);
135
- // On network/API errors, throw error to prevent login
136
- throw new Error('System error. Please try logging in at a later time.');
137
- }
138
- };
139
-
140
- useEffect(() => {
141
- const unsubscribe = onAuthStateChanged(auth, async (user) => {
142
- if (user) {
143
- let currentUser = user;
144
-
145
- // Refresh auth profile so emailVerified is accurate right after email verification.
146
- try {
147
- await currentUser.reload();
148
- if (auth.currentUser) {
149
- currentUser = auth.currentUser;
150
- }
151
- } catch (reloadError) {
152
- console.error('Failed to refresh user verification status:', reloadError);
153
- }
154
-
155
- setUser(currentUser);
156
-
157
- if (!currentUser.emailVerified) {
158
- // Don't sign out immediately - let them see the verification prompt
159
- setError('');
160
- setSuccess('Please verify your email before continuing. Check your inbox for the verification link.');
161
- setShowMfaEnrollment(false);
162
- setIsCheckingUser(false);
163
- return;
164
- }
165
-
166
- // Check if user exists in the USER_DB
167
- setIsCheckingUser(true);
168
- try {
169
- const userData = await checkUserExists(currentUser);
170
- setIsCheckingUser(false);
171
-
172
- if (!userData) {
173
- handleSignOut();
174
- setError('This account does not exist or has been deleted');
175
- return;
176
- }
177
- } catch (error) {
178
- setIsCheckingUser(false);
179
- handleSignOut();
180
- setError(error instanceof Error ? error.message : 'System error. Please try logging in at a later time.');
181
- return;
182
- }
183
-
184
- // Check if user has MFA enrolled
185
- if (!userHasMFA(currentUser)) {
186
- // User has no MFA factors enrolled - require enrollment
187
- setShowMfaEnrollment(true);
188
- return;
189
- }
190
-
191
- console.log("User signed in:", currentUser.email);
192
- setShowMfaEnrollment(false);
193
-
194
- if (shouldShowWelcomeToastRef.current) {
195
- setWelcomeToastType('success');
196
- setWelcomeToastMessage(`Welcome to Striae, ${getUserFirstName(currentUser)}!`);
197
- setIsWelcomeToastVisible(true);
198
- shouldShowWelcomeToastRef.current = false;
199
- }
200
-
201
- // Log successful login audit
202
- try {
203
- const sessionId = `session_${currentUser.uid}_${Date.now()}_${generateUniqueId(8)}`;
204
- await auditService.logUserLogin(
205
- currentUser,
206
- sessionId,
207
- 'firebase',
208
- navigator.userAgent
209
- );
210
- } catch (auditError) {
211
- console.error('Failed to log user login audit:', auditError);
212
- // Continue with login even if audit logging fails
213
- }
214
- } else {
215
- setUser(null);
216
- setShowMfaEnrollment(false);
217
- setIsCheckingUser(false);
218
- setIsWelcomeToastVisible(false);
219
- setWelcomeToastType('success');
220
- shouldShowWelcomeToastRef.current = false;
221
- }
222
- });
223
-
224
- return () => unsubscribe();
225
- }, []);
226
-
227
- useEffect(() => {
228
- if (shouldHandleEmailAction) {
229
- return;
230
- }
231
-
232
- const currentUser = auth.currentUser;
233
- if (!currentUser) {
234
- return;
235
- }
236
-
237
- let isMounted = true;
238
-
239
- const syncMfaAfterEmailAction = async () => {
240
- try {
241
- await currentUser.reload();
242
- const refreshedUser = auth.currentUser ?? currentUser;
243
-
244
- if (!isMounted) {
245
- return;
246
- }
247
-
248
- setUser(refreshedUser);
249
-
250
- if (!refreshedUser.emailVerified) {
251
- return;
252
- }
253
-
254
- setShowMfaEnrollment(!userHasMFA(refreshedUser));
255
- } catch (refreshError) {
256
- console.error('Failed to sync MFA state after email action:', refreshError);
257
- }
258
- };
259
-
260
- void syncMfaAfterEmailAction();
261
-
262
- return () => {
263
- isMounted = false;
264
- };
265
- }, [shouldHandleEmailAction]);
266
-
267
- const handleSubmit = async (e: React.FormEvent) => {
268
- e.preventDefault();
269
- setIsLoading(true);
270
- setError('');
271
- setSuccess('');
272
-
273
- const formData = new FormData(e.currentTarget as HTMLFormElement);
274
- const email = formData.get('email') as string;
275
- const password = formData.get('password') as string;
276
- const confirmPassword = formData.get('confirmPassword') as string;
277
- // Use state values for these fields instead of FormData
278
- const formFirstName = firstName;
279
- const formLastName = lastName;
280
- const formCompany = company;
281
- const formBadgeId = badgeId;
282
-
283
- try {
284
- if (!isLogin) {
285
- const emailValidation = validateRegistrationEmail(email);
286
- if (!emailValidation.valid) {
287
- setError('Please enter a valid email address');
288
- setIsLoading(false);
289
- return;
290
- }
291
-
292
- if (password !== confirmPassword) {
293
- setError('Passwords do not match');
294
- setIsLoading(false);
295
- return;
296
- }
297
-
298
- if (!checkPasswordStrength(password)) {
299
- setError('Password does not meet requirements');
300
- setIsLoading(false);
301
- return;
302
- }
303
- }
304
-
305
- if (!isLogin) {
306
- // Registration
307
- const createCredential = await createUserWithEmailAndPassword(auth, email, password);
308
- await updateProfile(createCredential.user, {
309
- displayName: `${formFirstName} ${formLastName}`
310
- });
311
-
312
- const companyName = formCompany.trim();
313
-
314
- // Create user data using centralized function
315
- await createUser(
316
- createCredential.user,
317
- formFirstName,
318
- formLastName,
319
- companyName || '',
320
- true,
321
- formBadgeId.trim()
322
- );
323
-
324
- // Log user registration audit event
325
- try {
326
- await auditService.logUserRegistration(
327
- createCredential.user,
328
- formFirstName,
329
- formLastName,
330
- companyName || '',
331
- 'email-password',
332
- navigator.userAgent
333
- );
334
- } catch (auditError) {
335
- console.error('Failed to log user registration audit:', auditError);
336
- // Continue with registration flow even if audit logging fails
337
- }
338
-
339
- await sendEmailVerification(createCredential.user, buildActionCodeSettings());
340
-
341
- // Log email verification sent audit event
342
- try {
343
- // This logs that we sent the verification email, not that it was verified
344
- // The actual verification happens when user clicks the email link
345
- await auditService.logEmailVerification(
346
- createCredential.user,
347
- 'pending', // Status pending until user clicks verification link
348
- 'email-link',
349
- 1, // First attempt
350
- undefined, // No sessionId during registration
351
- navigator.userAgent,
352
- [] // No errors since we successfully sent the email
353
- );
354
- } catch (auditError) {
355
- console.error('Failed to log email verification audit:', auditError);
356
- // Continue with registration flow even if audit logging fails
357
- }
358
-
359
- setError('');
360
- setSuccess('Account created successfully! Please check your email to verify your account.');
361
- // Don't sign out - let user stay logged in but unverified to see verification screen
362
- } else {
363
- // Login
364
- shouldShowWelcomeToastRef.current = true;
365
- try {
366
- await signInWithEmailAndPassword(auth, email, password);
367
- } catch (loginError: unknown) {
368
- // Check if it's a Firebase Auth error with MFA requirement
369
- if (
370
- loginError &&
371
- typeof loginError === 'object' &&
372
- 'code' in loginError &&
373
- loginError.code === 'auth/multi-factor-auth-required'
374
- ) {
375
- // Handle MFA requirement
376
- const resolver = getMultiFactorResolver(auth, loginError as MultiFactorError);
377
- setMfaResolver(resolver);
378
- setShowMfaVerification(true);
379
- setIsLoading(false);
380
- return;
381
- }
382
- shouldShowWelcomeToastRef.current = false;
383
- throw loginError; // Re-throw non-MFA errors
384
- }
385
- }
386
- } catch (err) {
387
- shouldShowWelcomeToastRef.current = false;
388
- const { message } = handleAuthError(err);
389
- setError(message);
390
-
391
- // Log security violation for failed authentication attempts
392
- try {
393
- // Extract error details for audit
394
- const errorCode = err && typeof err === 'object' && 'code' in err ? err.code : 'unknown';
395
- const isAuthError = typeof errorCode === 'string' && errorCode.startsWith('auth/');
396
-
397
- if (isAuthError) {
398
- // Determine severity based on error type
399
- let severity: 'low' | 'medium' | 'high' | 'critical' = 'medium';
400
- let incidentType: 'unauthorized-access' | 'brute-force' | 'privilege-escalation' = 'unauthorized-access';
401
-
402
- if (errorCode === 'auth/too-many-requests') {
403
- severity = 'high';
404
- incidentType = 'brute-force';
405
- } else if (errorCode === 'auth/user-disabled') {
406
- severity = 'critical';
407
- }
408
-
409
- await auditService.logSecurityViolation(
410
- null, // No user object for failed auth
411
- incidentType,
412
- severity,
413
- `Failed authentication attempt: ${errorCode} - ${message}`,
414
- 'authentication-endpoint',
415
- true // Blocked by system
416
- );
417
- }
418
- } catch (auditError) {
419
- console.error('Failed to log security violation audit:', auditError);
420
- // Continue with error flow even if audit logging fails
421
- }
422
- } finally {
423
- setIsLoading(false);
424
- }
425
- };
426
-
427
- // Add proper sign out handling
428
- const handleSignOut = async () => {
429
- try {
430
- await auth.signOut();
431
- setUser(null);
432
- setIsLoading(false);
433
- setShowMfaEnrollment(false);
434
- setShowMfaVerification(false);
435
- setMfaResolver(null);
436
- setIsWelcomeToastVisible(false);
437
- setWelcomeToastType('success');
438
- shouldShowWelcomeToastRef.current = false;
439
- } catch (err) {
440
- console.error('Sign out error:', err);
441
- }
442
- };
443
-
444
- // MFA handlers
445
- const handleMfaSuccess = () => {
446
- setShowMfaVerification(false);
447
- setMfaResolver(null);
448
- // The auth state listener will handle the rest
449
- };
450
-
451
- const handleMfaError = (errorMessage: string) => {
452
- setError(errorMessage);
453
- };
454
-
455
- const handleMfaCancel = () => {
456
- setShowMfaVerification(false);
457
- setMfaResolver(null);
458
- setError('Authentication cancelled');
459
- };
460
-
461
- // MFA enrollment handlers
462
- const handleMfaEnrollmentSuccess = () => {
463
- setShowMfaEnrollment(false);
464
- setError('');
465
- // The auth state listener will re-evaluate the user's MFA status
466
- };
467
-
468
- const handleMfaEnrollmentError = (errorMessage: string) => {
469
- setError(errorMessage);
470
- };
471
-
472
- return (
473
- <>
474
- {shouldHandleEmailAction ? (
475
- <EmailActionHandler
476
- mode={actionMode}
477
- oobCode={actionCode}
478
- continueUrl={continueUrl}
479
- lang={actionLang}
480
- />
481
- ) : user ? (
482
- user.emailVerified ? (
483
- <Striae user={user} />
484
- ) : (
485
- <EmailVerification
486
- user={user}
487
- error={error}
488
- success={success}
489
- onError={setError}
490
- onSuccess={setSuccess}
491
- onSignOut={handleSignOut}
492
- />
493
- )
494
- ) : isResetting ? (
495
- <PasswordReset onBack={() => setIsResetting(false)}/>
496
- ) : (
497
- <div className={styles.container}>
498
- <Link
499
- viewTransition
500
- prefetch="intent"
501
- to="/"
502
- className={styles.logoLink}>
503
- <div className={styles.logo} />
504
- </Link>
505
- <div className={styles.formWrapper}>
506
- <h1 className={styles.title}>{isLogin ? 'Login to Striae' : 'Register a Striae Account'}</h1>
507
-
508
- <form onSubmit={handleSubmit} className={styles.form}>
509
- <input
510
- type="email"
511
- name="email"
512
- placeholder={isLogin ? "Email" : "Email Address"}
513
- autoComplete="email"
514
- className={styles.input}
515
- required
516
- disabled={isLoading}
517
- />
518
- <div className={styles.passwordField}>
519
- <input
520
- type={showPassword ? "text" : "password"}
521
- name="password"
522
- placeholder="Password"
523
- autoComplete={isLogin ? "current-password" : "new-password"}
524
- className={styles.input}
525
- required
526
- disabled={isLoading}
527
- onChange={(e) => !isLogin && checkPasswordStrength(e.target.value, confirmPasswordValue)}
528
- />
529
- <button
530
- type="button"
531
- className={styles.passwordToggle}
532
- onClick={() => setShowPassword(!showPassword)}
533
- aria-label={showPassword ? "Hide password" : "Show password"}
534
- >
535
- <Icon icon={showPassword ? "eye-off" : "eye"} />
536
- </button>
537
- </div>
538
-
539
- {!isLogin && (
540
- <>
541
- <div className={styles.passwordField}>
542
- <input
543
- type={showConfirmPassword ? "text" : "password"}
544
- name="confirmPassword"
545
- placeholder="Confirm Password"
546
- autoComplete="new-password"
547
- className={styles.input}
548
- required
549
- disabled={isLoading}
550
- value={confirmPasswordValue}
551
- onChange={(e) => {
552
- setConfirmPasswordValue(e.target.value);
553
- const passwordInput = (e.target.form?.elements.namedItem('password') as HTMLInputElement);
554
- if (passwordInput) {
555
- checkPasswordStrength(passwordInput.value, e.target.value);
556
- }
557
- }}
558
- />
559
- <button
560
- type="button"
561
- className={styles.passwordToggle}
562
- onClick={() => setShowConfirmPassword(!showConfirmPassword)}
563
- aria-label={showConfirmPassword ? "Hide confirm password" : "Show confirm password"}
564
- >
565
- <Icon icon={showConfirmPassword ? "eye-off" : "eye"} />
566
- </button>
567
- </div>
568
-
569
- <input
570
- type="text"
571
- name="firstName"
572
- required
573
- placeholder="First Name (required)"
574
- autoComplete="given-name"
575
- className={styles.input}
576
- disabled={isLoading}
577
- value={firstName}
578
- onChange={(e) => setFirstName(e.target.value)}
579
- />
580
- <input
581
- type="text"
582
- name="lastName"
583
- required
584
- placeholder="Last Name (required)"
585
- autoComplete="family-name"
586
- className={styles.input}
587
- disabled={isLoading}
588
- value={lastName}
589
- onChange={(e) => setLastName(e.target.value)}
590
- />
591
- <input
592
- type="text"
593
- name="company"
594
- required
595
- placeholder="Company/Lab (required)"
596
- autoComplete="organization"
597
- className={styles.input}
598
- disabled={isLoading}
599
- value={company}
600
- onChange={(e) => setCompany(e.target.value)}
601
- />
602
- <input
603
- type="text"
604
- name="badgeId"
605
- required
606
- placeholder="Badge/ID # (required)"
607
- autoComplete="off"
608
- className={styles.input}
609
- disabled={isLoading}
610
- value={badgeId}
611
- onChange={(e) => setBadgeId(e.target.value)}
612
- />
613
- {passwordStrength && (
614
- <div className={styles.passwordStrength}>
615
- <pre>{passwordStrength}</pre>
616
- </div>
617
- )}
618
- </>
619
- )}
620
-
621
- {isLogin && (
622
- <button
623
- type="button"
624
- onClick={() => setIsResetting(true)}
625
- className={styles.resetLink}
626
- >
627
- Forgot Password?
628
- </button>
629
- )}
630
-
631
- {error && <p className={styles.error}>{error}</p>}
632
- {success && <p className={styles.success}>{success}</p>}
633
-
634
- <button
635
- type="submit"
636
- className={styles.button}
637
- disabled={isLoading || isCheckingUser}
638
- >
639
- {isCheckingUser
640
- ? 'Verifying account...'
641
- : isLoading
642
- ? 'Loading...'
643
- : isLogin
644
- ? 'Login'
645
- : 'Register'}
646
- </button>
647
- </form>
648
-
649
- <p className={styles.toggle}>
650
- {isLogin ? "Don't have an account? " : "Already have an account? "}
651
- <button
652
- onClick={() => {
653
- setIsLogin(!isLogin);
654
- setShowPassword(false);
655
- setShowConfirmPassword(false);
656
- setPasswordStrength('');
657
- setError('');
658
- setFirstName('');
659
- setLastName('');
660
- setCompany('');
661
- setBadgeId('');
662
- setConfirmPasswordValue('');
663
- }}
664
- className={styles.toggleButton}
665
- disabled={isLoading || isCheckingUser}
666
- >
667
- {isLogin ? 'Register' : 'Login'}
668
- </button>
669
- </p>
670
- </div>
671
- </div>
672
- )}
673
-
674
- {!shouldHandleEmailAction && isClient && showMfaVerification && mfaResolver && (
675
- <MFAVerification
676
- resolver={mfaResolver}
677
- onSuccess={handleMfaSuccess}
678
- onError={handleMfaError}
679
- onCancel={handleMfaCancel}
680
- />
681
- )}
682
-
683
- {!shouldHandleEmailAction && isClient && showMfaEnrollment && user && (
684
- <MFAEnrollment
685
- user={user}
686
- onSuccess={handleMfaEnrollmentSuccess}
687
- onError={handleMfaEnrollmentError}
688
- mandatory={true}
689
- />
690
- )}
691
-
692
- {!shouldHandleEmailAction && (
693
- <Toast
694
- message={welcomeToastMessage}
695
- type={welcomeToastType}
696
- isVisible={isWelcomeToastVisible}
697
- onClose={() => setIsWelcomeToastVisible(false)}
698
- />
699
- )}
700
-
701
- </>
702
- );
703
- };
704
-
705
- export default Login;