@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,251 @@
1
+ .container {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background: rgba(0, 0, 0, 0.5);
8
+ display: flex;
9
+ justify-content: center;
10
+ align-items: center;
11
+ z-index: 1000;
12
+ }
13
+
14
+ .modal {
15
+ background: white;
16
+ border-radius: var(--spaceXS);
17
+ padding: var(--space2XL);
18
+ width: 100%;
19
+ max-width: 400px;
20
+ box-shadow: 0 var(--spaceM) var(--spaceXL) rgba(0, 0, 0, 0.2);
21
+ }
22
+
23
+ .title {
24
+ text-align: center;
25
+ color: var(--textTitle);
26
+ margin-bottom: var(--spaceXL);
27
+ font-size: var(--fontSizeH4);
28
+ }
29
+
30
+ .hintSelection {
31
+ margin-bottom: var(--spaceL);
32
+ }
33
+
34
+ .label {
35
+ display: block;
36
+ margin-bottom: var(--spaceS);
37
+ font-weight: 600;
38
+ color: var(--textBody);
39
+ }
40
+
41
+ .select {
42
+ width: 100%;
43
+ padding: var(--spaceM);
44
+ border: 1.5px solid color-mix(in lab, var(--text) 20%, transparent);
45
+ border-radius: var(--spaceXS);
46
+ font-size: var(--fontSizeBodyM);
47
+ background: white;
48
+ transition: border-color var(--durationS) var(--bezierFastoutSlowin);
49
+ box-sizing: border-box;
50
+ }
51
+
52
+ .select:focus {
53
+ outline: none;
54
+ border-color: var(--primary);
55
+ box-shadow: 0 0 0 2px color-mix(in lab, var(--primary) 25%, transparent);
56
+ }
57
+
58
+ .description {
59
+ text-align: center;
60
+ color: var(--textLight);
61
+ margin-bottom: var(--spaceL);
62
+ line-height: 1.5;
63
+ }
64
+
65
+ .errorMessage {
66
+ background: linear-gradient(135deg,
67
+ color-mix(in lab, var(--error) 12%, transparent),
68
+ color-mix(in lab, var(--error) 8%, transparent)
69
+ );
70
+ border: 1px solid color-mix(in lab, var(--error) 30%, transparent);
71
+ border-left: 4px solid var(--error);
72
+ color: color-mix(in lab, var(--error) 90%, var(--black));
73
+ padding: var(--spaceL);
74
+ border-radius: var(--spaceS);
75
+ font-size: var(--fontSizeBodyS);
76
+ margin-bottom: var(--spaceL);
77
+ text-align: left;
78
+ line-height: 1.4;
79
+ position: relative;
80
+ overflow: hidden;
81
+ box-shadow: 0 4px 16px color-mix(in lab, var(--text) 10%, transparent);
82
+ animation: slideInError var(--durationM) var(--bezierFastoutSlowin);
83
+ }
84
+
85
+ .errorMessage::before {
86
+ content: '';
87
+ position: absolute;
88
+ top: 0;
89
+ left: 0;
90
+ right: 0;
91
+ height: 2px;
92
+ background: linear-gradient(90deg, var(--error), color-mix(in lab, var(--error) 60%, transparent));
93
+ animation: shimmer 2s ease-in-out infinite;
94
+ }
95
+
96
+ .errorMessage:empty {
97
+ display: none;
98
+ }
99
+
100
+ .input {
101
+ width: 100%;
102
+ padding: var(--spaceM);
103
+ border: 1.5px solid color-mix(in lab, var(--text) 20%, transparent);
104
+ border-radius: var(--spaceXS);
105
+ font-size: var(--fontSizeBodyM);
106
+ text-align: center;
107
+ font-family: monospace;
108
+ letter-spacing: 2px;
109
+ margin-bottom: var(--spaceL);
110
+ transition: border-color var(--durationS) var(--bezierFastoutSlowin);
111
+ box-sizing: border-box;
112
+ }
113
+
114
+ .input:focus {
115
+ outline: none;
116
+ border-color: var(--primary);
117
+ box-shadow: 0 0 0 2px color-mix(in lab, var(--primary) 25%, transparent);
118
+ }
119
+
120
+ .button {
121
+ width: 100%;
122
+ padding: var(--spaceM) var(--spaceL);
123
+ border-radius: var(--spaceXS);
124
+ font-size: var(--fontSizeBodyS);
125
+ cursor: pointer;
126
+ background-color: var(--primary);
127
+ color: var(--white);
128
+ border: none;
129
+ transition: all var(--durationS) var(--bezierFastoutSlowin);
130
+ margin-bottom: var(--spaceM);
131
+ box-sizing: border-box;
132
+ }
133
+
134
+ .button:hover:not(:disabled) {
135
+ background-color: color-mix(in lab, var(--primary) 85%, var(--black));
136
+ }
137
+
138
+ .button:disabled {
139
+ background-color: color-mix(in lab, var(--background) 95%, transparent);
140
+ color: var(--textLight);
141
+ cursor: not-allowed;
142
+ }
143
+
144
+ .buttons {
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: var(--spaceS);
148
+ }
149
+
150
+ .secondaryButton {
151
+ padding: var(--spaceS) var(--spaceM);
152
+ border: 1.5px solid color-mix(in lab, var(--text) 20%, transparent);
153
+ border-radius: var(--spaceXS);
154
+ background: transparent;
155
+ cursor: pointer;
156
+ font-size: var(--fontSizeBodyS);
157
+ color: var(--textBody);
158
+ transition: all var(--durationS) var(--bezierFastoutSlowin);
159
+ box-sizing: border-box;
160
+ width: 100%;
161
+ }
162
+
163
+ .secondaryButton:hover:not(:disabled) {
164
+ background-color: color-mix(in lab, var(--background) 95%, transparent);
165
+ border-color: color-mix(in lab, var(--text) 30%, transparent);
166
+ }
167
+
168
+ .actions {
169
+ margin-top: var(--spaceL);
170
+ text-align: center;
171
+ }
172
+
173
+ .cancelButton {
174
+ padding: var(--spaceS) var(--spaceL);
175
+ border: none;
176
+ background: transparent;
177
+ cursor: pointer;
178
+ font-size: var(--fontSizeBodyS);
179
+ color: var(--textLight);
180
+ transition: color var(--durationS) var(--bezierFastoutSlowin);
181
+ }
182
+
183
+ .cancelButton:hover {
184
+ color: var(--textBody);
185
+ }
186
+
187
+ .signOutContainer {
188
+ margin-top: var(--spaceL);
189
+ padding-top: var(--spaceL);
190
+ border-top: 1px solid var(--borderLight);
191
+ text-align: center;
192
+ }
193
+
194
+ .signOutText {
195
+ color: var(--textLight);
196
+ font-size: var(--fontSizeSmall);
197
+ margin-bottom: var(--spaceM);
198
+ }
199
+
200
+ .sendCode,
201
+ .verifyCode {
202
+ text-align: center;
203
+ }
204
+
205
+ #recaptcha-container {
206
+ margin-top: var(--spaceM);
207
+ }
208
+
209
+ /* Animations */
210
+ @keyframes slideInError {
211
+ from {
212
+ opacity: 0;
213
+ transform: translateY(calc(-1 * var(--spaceM))) scaleY(0.8);
214
+ max-height: 0;
215
+ padding-top: 0;
216
+ padding-bottom: 0;
217
+ margin-top: 0;
218
+ margin-bottom: 0;
219
+ }
220
+ to {
221
+ opacity: 1;
222
+ transform: translateY(0) scaleY(1);
223
+ max-height: 200px;
224
+ padding-top: var(--spaceL);
225
+ padding-bottom: var(--spaceL);
226
+ margin-top: 0;
227
+ margin-bottom: var(--spaceL);
228
+ }
229
+ }
230
+
231
+ @keyframes shimmer {
232
+ 0%, 100% {
233
+ opacity: 0.6;
234
+ transform: translateX(-100%);
235
+ }
236
+ 50% {
237
+ opacity: 1;
238
+ transform: translateX(100%);
239
+ }
240
+ }
241
+
242
+ /* Reduce motion for accessibility */
243
+ @media (prefers-reduced-motion: reduce) {
244
+ .errorMessage {
245
+ animation: none;
246
+ }
247
+
248
+ .errorMessage::before {
249
+ animation: none;
250
+ }
251
+ }
@@ -0,0 +1,295 @@
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ PhoneAuthProvider,
4
+ PhoneMultiFactorGenerator,
5
+ RecaptchaVerifier,
6
+ MultiFactorResolver,
7
+ UserCredential
8
+ } from 'firebase/auth';
9
+ import { auth } from '~/services/firebase';
10
+ import { handleAuthError, getValidationError } from '~/services/firebase-errors';
11
+ import { SignOut } from '~/components/actions/signout';
12
+ import { auditService } from '~/services/audit.service';
13
+ import { generateUniqueId } from '~/utils/id-generator';
14
+ import styles from './mfa-verification.module.css';
15
+
16
+ interface MFAVerificationProps {
17
+ resolver: MultiFactorResolver;
18
+ onSuccess: (result: UserCredential) => void;
19
+ onError: (error: string) => void;
20
+ onCancel: () => void;
21
+ }
22
+
23
+ const isRecaptchaResetError = (authError: { code?: string; message?: string }): boolean => {
24
+ return (
25
+ authError.code === 'auth/captcha-check-failed' ||
26
+ authError.code === 'auth/invalid-app-credential' ||
27
+ authError.code === 'auth/missing-app-credential' ||
28
+ (authError.message?.toLowerCase().includes('recaptcha') ?? false)
29
+ );
30
+ };
31
+
32
+ export const MFAVerification = ({ resolver, onSuccess, onError, onCancel }: MFAVerificationProps) => {
33
+ const [selectedHintIndex, setSelectedHintIndex] = useState(0);
34
+ const [verificationCode, setVerificationCode] = useState('');
35
+ const [verificationId, setVerificationId] = useState('');
36
+ const [loading, setLoading] = useState(false);
37
+ const [codeSent, setCodeSent] = useState(false);
38
+ const [recaptchaVerifier, setRecaptchaVerifier] = useState<RecaptchaVerifier | null>(null);
39
+ const [isClient, setIsClient] = useState(false);
40
+ const [errorMessage, setErrorMessage] = useState('');
41
+
42
+ useEffect(() => {
43
+ setIsClient(true);
44
+ }, []);
45
+
46
+ useEffect(() => {
47
+ if (!isClient) return;
48
+
49
+ // Initialize reCAPTCHA verifier
50
+ const verifier = new RecaptchaVerifier(auth, 'recaptcha-container', {
51
+ size: 'invisible',
52
+ callback: () => {
53
+ // reCAPTCHA solved
54
+ },
55
+ 'expired-callback': () => {
56
+ const error = getValidationError('MFA_RECAPTCHA_EXPIRED');
57
+ setErrorMessage(error);
58
+ onError(error);
59
+ }
60
+ });
61
+ setRecaptchaVerifier(verifier);
62
+
63
+ return () => {
64
+ verifier.clear();
65
+ };
66
+ }, [isClient, onError]);
67
+
68
+ const sendVerificationCode = async () => {
69
+ if (!recaptchaVerifier) {
70
+ const error = getValidationError('MFA_RECAPTCHA_ERROR');
71
+ setErrorMessage(error);
72
+ onError(error);
73
+ return;
74
+ }
75
+
76
+ setLoading(true);
77
+ setErrorMessage(''); // Clear any previous errors
78
+ try {
79
+ const phoneAuthProvider = new PhoneAuthProvider(auth);
80
+
81
+ const phoneInfoOptions = {
82
+ multiFactorHint: resolver.hints[selectedHintIndex],
83
+ session: resolver.session
84
+ };
85
+
86
+ const vId = await phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier);
87
+ setVerificationId(vId);
88
+ setCodeSent(true);
89
+ } catch (error: unknown) {
90
+ const authError = error as { code?: string; message?: string };
91
+ let errorMsg = handleAuthError(authError).message;
92
+
93
+ if (isRecaptchaResetError(authError)) {
94
+ errorMsg = getValidationError('MFA_RECAPTCHA_ERROR');
95
+ }
96
+ setErrorMessage(errorMsg);
97
+ onError(errorMsg);
98
+ if (recaptchaVerifier) {
99
+ recaptchaVerifier.clear();
100
+ }
101
+ } finally {
102
+ setLoading(false);
103
+ }
104
+ };
105
+
106
+ const verifyCode = async () => {
107
+ if (!verificationId) {
108
+ const error = getValidationError('MFA_NO_VERIFICATION_ID');
109
+ setErrorMessage(error);
110
+ onError(error);
111
+ return;
112
+ }
113
+
114
+ if (!verificationCode.trim()) {
115
+ const error = getValidationError('MFA_CODE_REQUIRED');
116
+ setErrorMessage(error);
117
+ onError(error);
118
+ return;
119
+ }
120
+
121
+ setLoading(true);
122
+ setErrorMessage(''); // Clear any previous errors
123
+ try {
124
+ const credential = PhoneAuthProvider.credential(verificationId, verificationCode);
125
+ const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(credential);
126
+
127
+ const result = await resolver.resolveSignIn(multiFactorAssertion);
128
+
129
+ // Log successful MFA authentication audit event
130
+ try {
131
+ const sessionId = `session_${result.user.uid}_${Date.now()}_${generateUniqueId(8)}`;
132
+ await auditService.logMfaAuthentication(
133
+ result.user,
134
+ 'sms',
135
+ 'success',
136
+ 1, // Assuming first successful attempt since we got here
137
+ sessionId,
138
+ navigator.userAgent
139
+ );
140
+ } catch (auditError) {
141
+ console.error('Failed to log MFA authentication success audit:', auditError);
142
+ // Continue with success flow even if audit logging fails
143
+ }
144
+
145
+ onSuccess(result);
146
+ } catch (error: unknown) {
147
+ const authError = error as { code?: string; message?: string };
148
+ let errorMsg = '';
149
+
150
+ if (authError.code === 'auth/invalid-verification-code') {
151
+ errorMsg = getValidationError('MFA_INVALID_CODE');
152
+ } else if (authError.code === 'auth/code-expired') {
153
+ errorMsg = getValidationError('MFA_CODE_EXPIRED');
154
+ } else if (isRecaptchaResetError(authError)) {
155
+ errorMsg = getValidationError('MFA_RECAPTCHA_ERROR');
156
+ } else {
157
+ const fallbackMessage = handleAuthError(authError).message;
158
+ errorMsg = fallbackMessage;
159
+ }
160
+ setErrorMessage(errorMsg);
161
+ onError(errorMsg);
162
+
163
+ // Log security violation for failed MFA attempts
164
+ try {
165
+ let severity: 'low' | 'medium' | 'high' | 'critical' = 'medium';
166
+ let incidentType: 'unauthorized-access' | 'brute-force' = 'unauthorized-access';
167
+
168
+ if (authError.code === 'auth/invalid-verification-code') {
169
+ severity = 'high'; // Invalid MFA codes are serious security events
170
+ incidentType = 'brute-force';
171
+ }
172
+
173
+ await auditService.logSecurityViolation(
174
+ null, // No user object during MFA verification failure
175
+ incidentType,
176
+ severity,
177
+ `Failed MFA verification: ${authError.code} - ${errorMsg}`,
178
+ 'mfa-verification-endpoint',
179
+ true // Blocked by system
180
+ );
181
+ } catch (auditError) {
182
+ console.error('Failed to log MFA security violation audit:', auditError);
183
+ // Continue with error flow even if audit logging fails
184
+ }
185
+ } finally {
186
+ setLoading(false);
187
+ }
188
+ };
189
+
190
+ const selectedHint = resolver.hints[selectedHintIndex];
191
+ const maskedPhoneNumber = selectedHint?.displayName || 'your phone';
192
+
193
+ if (!isClient) {
194
+ return null;
195
+ }
196
+
197
+ return (
198
+ <div className={styles.container}>
199
+ <div className={styles.modal}>
200
+ <h2 className={styles.title}>Two-Factor Authentication Required</h2>
201
+
202
+ {errorMessage && (
203
+ <div className={styles.errorMessage}>
204
+ {errorMessage}
205
+ </div>
206
+ )}
207
+
208
+ {resolver.hints.length > 1 && (
209
+ <div className={styles.hintSelection}>
210
+ <label htmlFor="hint-select" className={styles.label}>Choose verification method:</label>
211
+ <select
212
+ id="hint-select"
213
+ title="Select verification method"
214
+ value={selectedHintIndex}
215
+ onChange={(e) => {
216
+ setSelectedHintIndex(Number(e.target.value));
217
+ if (errorMessage) setErrorMessage(''); // Clear error when changing method
218
+ }}
219
+ className={styles.select}
220
+ >
221
+ {resolver.hints.map((hint, index) => (
222
+ <option key={index} value={index}>
223
+ {hint.displayName || `Phone verification ${index + 1}`}
224
+ </option>
225
+ ))}
226
+ </select>
227
+ </div>
228
+ )}
229
+
230
+ {!codeSent ? (
231
+ <div className={styles.sendCode}>
232
+ <p className={styles.description}>
233
+ We&apos;ll send a verification code to {maskedPhoneNumber}
234
+ </p>
235
+ <button
236
+ onClick={sendVerificationCode}
237
+ disabled={loading}
238
+ className={styles.button}
239
+ >
240
+ {loading ? 'Sending...' : 'Send Verification Code'}
241
+ </button>
242
+ </div>
243
+ ) : (
244
+ <div className={styles.verifyCode}>
245
+ <p className={styles.description}>
246
+ Enter the verification code sent to {maskedPhoneNumber}
247
+ </p>
248
+ <input
249
+ type="text"
250
+ placeholder="Enter 6-digit code"
251
+ value={verificationCode}
252
+ onChange={(e) => {
253
+ setVerificationCode(e.target.value);
254
+ if (errorMessage) setErrorMessage(''); // Clear error on input
255
+ }}
256
+ className={styles.input}
257
+ maxLength={6}
258
+ />
259
+ <div className={styles.buttons}>
260
+ <button
261
+ onClick={verifyCode}
262
+ disabled={loading || verificationCode.length !== 6}
263
+ className={styles.button}
264
+ >
265
+ {loading ? 'Verifying...' : 'Verify Code'}
266
+ </button>
267
+ <button
268
+ onClick={() => {
269
+ setCodeSent(false);
270
+ setVerificationCode('');
271
+ setVerificationId('');
272
+ setErrorMessage(''); // Clear errors when requesting new code
273
+ }}
274
+ className={styles.secondaryButton}
275
+ >
276
+ Send New Code
277
+ </button>
278
+ </div>
279
+ </div>
280
+ )}
281
+
282
+ <div className={styles.actions}>
283
+ <button onClick={onCancel} className={styles.cancelButton}>
284
+ Cancel
285
+ </button>
286
+ <div className={styles.signOutContainer}>
287
+ <p className={styles.signOutText}>Need to sign in with a different account?</p>
288
+ <SignOut redirectTo="/" />
289
+ </div>
290
+ </div>
291
+ <div id="recaptcha-container"></div>
292
+ </div>
293
+ </div>
294
+ );
295
+ };
@@ -0,0 +1,63 @@
1
+ .button {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ width: 50px;
6
+ height: 50px;
7
+ border-radius: var(--radiusM);
8
+ border: none;
9
+ background: var(--backgroundLight);
10
+ cursor: pointer;
11
+ transition: all var(--durationS) var(--bezierFastoutSlowin);
12
+ box-shadow: 0 1px 3px color-mix(in lab, var(--backgroundLight) 30%, transparent);
13
+ }
14
+
15
+ .button:hover {
16
+ background: color-mix(in lab, var(--backgroundLight) 85%, var(--black));
17
+ transform: translateY(-1px);
18
+ box-shadow: 0 2px 6px color-mix(in lab, var(--backgroundLight) 40%, transparent);
19
+ }
20
+
21
+ .button.active {
22
+ background: var(--success);
23
+ box-shadow: 0 1px 3px color-mix(in lab, var(--success) 30%, transparent);
24
+ }
25
+
26
+ .button.active:hover {
27
+ background: color-mix(in lab, var(--success) 85%, var(--black));
28
+ transform: translateY(-1px);
29
+ box-shadow: 0 2px 6px color-mix(in lab, var(--success) 40%, transparent);
30
+ }
31
+
32
+ .button.disabled {
33
+ opacity: 0.5;
34
+ cursor: not-allowed;
35
+ pointer-events: none;
36
+ transform: none;
37
+ box-shadow: none;
38
+ }
39
+
40
+ .button.disabled:hover {
41
+ background: var(--backgroundLight);
42
+ transform: none;
43
+ box-shadow: none;
44
+ }
45
+
46
+ .icon {
47
+ color: var(--text);
48
+ transition: color var(--durationS) var(--bezierFastoutSlowin);
49
+ }
50
+
51
+ .spinner {
52
+ width: 20px;
53
+ height: 20px;
54
+ border: 2px solid color-mix(in lab, var(--text) 20%, transparent);
55
+ border-top: 2px solid var(--text);
56
+ border-radius: 50%;
57
+ animation: spin 1s linear infinite;
58
+ }
59
+
60
+ @keyframes spin {
61
+ 0% { transform: rotate(0deg); }
62
+ 100% { transform: rotate(360deg); }
63
+ }
@@ -0,0 +1,46 @@
1
+ import { Icon } from '../icon/icon';
2
+ import styles from './button.module.css';
3
+ import { classes } from '~/utils/style';
4
+
5
+ interface ButtonProps {
6
+ iconId: string;
7
+ isActive?: boolean;
8
+ onClick?: () => void;
9
+ ariaLabel: string;
10
+ title?: string;
11
+ disabled?: boolean;
12
+ showSpinner?: boolean;
13
+ }
14
+
15
+ export const Button = ({
16
+ iconId,
17
+ isActive = false,
18
+ onClick,
19
+ ariaLabel,
20
+ title,
21
+ disabled = false,
22
+ showSpinner = false
23
+ }: ButtonProps) => {
24
+ return (
25
+ <button
26
+ className={classes(styles.button, isActive && styles.active, disabled && styles.disabled)}
27
+ onClick={disabled ? undefined : onClick}
28
+ aria-label={ariaLabel}
29
+ aria-pressed={isActive}
30
+ title={title || ariaLabel}
31
+ disabled={disabled}
32
+ >
33
+ {showSpinner ? (
34
+ <div className={styles.spinner}></div>
35
+ ) : (
36
+ <Icon
37
+ size={30}
38
+ icon={iconId}
39
+ className={styles.icon}
40
+ />
41
+ )}
42
+ </button>
43
+ );
44
+ };
45
+
46
+ Button.displayName = 'Button';