@striae-org/striae 3.0.4

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