@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,5 @@
1
+ const mobileOrTabletUserAgentPattern = /mobile|android|iphone|ipad|ipod|blackberry|iemobile|opera mini|tablet|silk|kindle|playbook|webos|windows phone/i;
2
+
3
+ export const isMobileOrTabletUserAgent = (userAgent: string): boolean => {
4
+ return mobileOrTabletUserAgentPattern.test(userAgent);
5
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * HTML Sanitization Utility
3
+ *
4
+ * Provides secure HTML escaping for user-generated content
5
+ * embedded in email templates and other HTML contexts.
6
+ *
7
+ * @module html-sanitizer
8
+ */
9
+
10
+ /**
11
+ * Escapes HTML special characters to prevent injection attacks
12
+ *
13
+ * Converts characters that have special meaning in HTML into their
14
+ * HTML entity equivalents to ensure they are rendered as text rather
15
+ * than interpreted as markup.
16
+ *
17
+ * @param text - Raw user input that may contain HTML characters
18
+ * @returns Safely escaped string suitable for embedding in HTML
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * escapeHtml('<script>alert("xss")</script>')
23
+ * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
24
+ *
25
+ * escapeHtml('John & Jane <test@example.com>')
26
+ * // Returns: 'John &amp; Jane &lt;test@example.com&gt;'
27
+ * ```
28
+ */
29
+ export function escapeHtml(text: string | null | undefined): string {
30
+ if (!text) return '';
31
+
32
+ return String(text)
33
+ .replace(/&/g, '&amp;')
34
+ .replace(/</g, '&lt;')
35
+ .replace(/>/g, '&gt;')
36
+ .replace(/"/g, '&quot;')
37
+ .replace(/'/g, '&#x27;')
38
+ .replace(/\//g, '&#x2F;');
39
+ }
40
+
41
+ /**
42
+ * Escapes multiple string values for safe HTML embedding
43
+ *
44
+ * Convenience function for escaping an object of string values.
45
+ * Useful when preparing multiple user inputs for email templates.
46
+ *
47
+ * @param values - Object containing string values to escape
48
+ * @returns New object with all string values HTML-escaped
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const userInput = {
53
+ * name: '<script>alert("xss")</script>',
54
+ * email: 'test@example.com',
55
+ * company: 'ACME & Co.'
56
+ * };
57
+ *
58
+ * const safe = escapeHtmlObject(userInput);
59
+ * // Returns: {
60
+ * // name: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;',
61
+ * // email: 'test@example.com',
62
+ * // company: 'ACME &amp; Co.'
63
+ * // }
64
+ * ```
65
+ */
66
+ export function escapeHtmlObject<T extends Record<string, any>>(
67
+ values: T
68
+ ): T {
69
+ const escaped = {} as T;
70
+
71
+ for (const [key, value] of Object.entries(values)) {
72
+ if (typeof value === 'string') {
73
+ escaped[key as keyof T] = escapeHtml(value) as T[keyof T];
74
+ } else {
75
+ escaped[key as keyof T] = value;
76
+ }
77
+ }
78
+
79
+ return escaped;
80
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Utility functions for generating unique identifiers
3
+ */
4
+
5
+ /**
6
+ * Generate a unique alphanumeric ID
7
+ * @param length - Length of the ID (default: 10)
8
+ * @returns Unique alphanumeric string
9
+ */
10
+ export function generateUniqueId(length: number = 10): string {
11
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
12
+ let result = '';
13
+ for (let i = 0; i < length; i++) {
14
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
15
+ }
16
+ return result;
17
+ }
18
+
19
+ /**
20
+ * Generate a confirmation ID with specific format
21
+ * @returns Confirmation ID in format: CONF-XXXXXXXXXX
22
+ */
23
+ export function generateConfirmationId(): string {
24
+ return `CONF-${generateUniqueId(10)}`;
25
+ }
26
+
27
+ /**
28
+ * Generate a workflow ID for audit trails
29
+ * @param caseNumber - Case number to include in workflow ID
30
+ * @returns Workflow ID in format: caseNumber-timestamp-random
31
+ */
32
+ export function generateWorkflowId(caseNumber: string): string {
33
+ const timestamp = Date.now().toString(36);
34
+ const random = generateUniqueId(8);
35
+ return `${caseNumber}-${timestamp}-${random}`;
36
+ }
@@ -0,0 +1,48 @@
1
+ import config from '~/config/meta-config.json';
2
+
3
+ interface AppConfig {
4
+ name: string;
5
+ author: string;
6
+ title: string;
7
+ url: string;
8
+ }
9
+
10
+ const { name, author, url } = config as AppConfig;
11
+ const defaultOgImage = `${url}/social-image.png`;
12
+
13
+
14
+ interface MetaParams {
15
+ title: string;
16
+ description: string;
17
+ prefix?: string;
18
+ ogImage?: string;
19
+ }
20
+
21
+ export function baseMeta({
22
+ title: pageTitle,
23
+ description,
24
+ ogImage = defaultOgImage,
25
+ }: MetaParams) {
26
+ const titleText = `${name} | ${pageTitle}`;
27
+
28
+ return [
29
+ { title: titleText },
30
+ { name: 'description', content: description },
31
+ { name: 'author', content: author },
32
+ { property: 'og:image', content: ogImage },
33
+ { property: 'og:image:alt', content: 'Banner for the site' },
34
+ { property: 'og:image:width', content: '1020' },
35
+ { property: 'og:image:height', content: '484' },
36
+ { property: 'og:title', content: titleText },
37
+ { property: 'og:site_name', content: name },
38
+ { property: 'og:type', content: 'website' },
39
+ { property: 'og:url', content: url },
40
+ { property: 'og:description', content: description },
41
+ { property: 'twitter:card', content: 'summary_large_image' },
42
+ { property: 'twitter:description', content: description },
43
+ { property: 'twitter:title', content: titleText },
44
+ { property: 'twitter:site', content: url },
45
+ { property: 'twitter:creator', content: author },
46
+ { property: 'twitter:image', content: ogImage },
47
+ ];
48
+ }
@@ -0,0 +1,97 @@
1
+ import type { MultiFactorInfo } from 'firebase/auth';
2
+ import { getValidationError } from '~/services/firebase-errors';
3
+
4
+ export interface PhoneValidationResult {
5
+ isValid: boolean;
6
+ errorMessage?: string;
7
+ }
8
+
9
+ export const formatPhoneNumberForMfa = (phone: string): string => {
10
+ const trimmedPhone = phone.trim();
11
+ if (trimmedPhone.startsWith('+')) {
12
+ return `+${trimmedPhone.slice(1).replace(/\D/g, '')}`;
13
+ }
14
+
15
+ const digitsOnly = trimmedPhone.replace(/\D/g, '');
16
+ if (digitsOnly.startsWith('1') && digitsOnly.length === 11) {
17
+ return `+${digitsOnly}`;
18
+ }
19
+
20
+ return `+1${digitsOnly}`;
21
+ };
22
+
23
+ export const maskPhoneNumber = (phone: string): string => {
24
+ const digits = phone.replace(/\D/g, '');
25
+ if (digits.length < 4) {
26
+ return '***-***-****';
27
+ }
28
+
29
+ return `***-***-${digits.slice(-4)}`;
30
+ };
31
+
32
+ export const getPhoneDisplayValue = (factor: MultiFactorInfo): string => {
33
+ const displayName = factor.displayName ?? '';
34
+ if (displayName.toLowerCase().startsWith('phone:')) {
35
+ return displayName.slice('phone:'.length).trim();
36
+ }
37
+
38
+ return displayName;
39
+ };
40
+
41
+ export const getMaskedFactorDisplay = (factor: MultiFactorInfo | null): string => {
42
+ if (!factor) {
43
+ return 'your enrolled phone';
44
+ }
45
+
46
+ const phoneDisplayValue = getPhoneDisplayValue(factor);
47
+ if (!phoneDisplayValue) {
48
+ return 'your enrolled phone';
49
+ }
50
+
51
+ return maskPhoneNumber(phoneDisplayValue);
52
+ };
53
+
54
+ export const validatePhoneNumber = (phone: string): PhoneValidationResult => {
55
+ if (!phone.trim()) {
56
+ return { isValid: false, errorMessage: getValidationError('MFA_INVALID_PHONE') };
57
+ }
58
+
59
+ const cleanPhone = phone.replace(/\D/g, '');
60
+ if (cleanPhone === '15551234567' || cleanPhone === '5551234567') {
61
+ return {
62
+ isValid: false,
63
+ errorMessage: 'Please enter your actual phone number, not the demo number.',
64
+ };
65
+ }
66
+
67
+ if (cleanPhone.length < 7 || cleanPhone.length > 15) {
68
+ return { isValid: false, errorMessage: 'Phone number must be between 7-15 digits.' };
69
+ }
70
+
71
+ if (phone.startsWith('+1') || (!phone.startsWith('+') && cleanPhone.length === 10)) {
72
+ const usPhone = cleanPhone.startsWith('1') ? cleanPhone.slice(1) : cleanPhone;
73
+ if (usPhone.length !== 10) {
74
+ return { isValid: false, errorMessage: 'US/Canada phone numbers must be 10 digits.' };
75
+ }
76
+
77
+ if (usPhone[0] === '0' || usPhone[0] === '1') {
78
+ return {
79
+ isValid: false,
80
+ errorMessage: 'Invalid area code. Area codes cannot start with 0 or 1.',
81
+ };
82
+ }
83
+
84
+ if (usPhone[3] === '0' || usPhone[3] === '1') {
85
+ return { isValid: false, errorMessage: 'Invalid phone number format.' };
86
+ }
87
+ }
88
+
89
+ if (phone.startsWith('+') && cleanPhone.length < 8) {
90
+ return {
91
+ isValid: false,
92
+ errorMessage: 'International phone numbers must have at least 8 digits including country code.',
93
+ };
94
+ }
95
+
96
+ return { isValid: true };
97
+ };
@@ -0,0 +1,79 @@
1
+ // MFA Configuration Helper
2
+ // This file contains utilities and documentation for managing MFA in your Firebase project
3
+
4
+ import { multiFactor, User } from 'firebase/auth';
5
+
6
+ /**
7
+ * Check if a user has MFA enrolled
8
+ * @param user - Firebase User object
9
+ * @returns boolean indicating if user has any MFA factors enrolled
10
+ */
11
+ export const userHasMFA = (user: User): boolean => {
12
+ return multiFactor(user).enrolledFactors.length > 0;
13
+ };
14
+
15
+ /**
16
+ * Get the number of MFA factors enrolled for a user
17
+ * @param user - Firebase User object
18
+ * @returns number of enrolled MFA factors
19
+ */
20
+ export const getMFAFactorCount = (user: User): number => {
21
+ return multiFactor(user).enrolledFactors.length;
22
+ };
23
+
24
+ /**
25
+ * Get MFA factor information for a user
26
+ * @param user - Firebase User object
27
+ * @returns array of MFA factor information
28
+ */
29
+ export const getMFAFactors = (user: User) => {
30
+ return multiFactor(user).enrolledFactors.map(factor => ({
31
+ uid: factor.uid,
32
+ factorId: factor.factorId,
33
+ displayName: factor.displayName,
34
+ enrollmentTime: factor.enrollmentTime
35
+ }));
36
+ };
37
+
38
+ /*
39
+ FIREBASE CONSOLE CONFIGURATION STEPS:
40
+
41
+ 1. **Enable Multi-Factor Authentication in Firebase Console:**
42
+ - Go to Firebase Console → Authentication → Sign-in method
43
+ - Scroll down to "Multi-factor authentication"
44
+ - Click "Enable" next to SMS
45
+ - Configure your SMS settings
46
+
47
+ 2. **Enable reCAPTCHA for Phone Auth:**
48
+ - In the same section, make sure reCAPTCHA is enabled
49
+ - Add your domain to the authorized domains list
50
+
51
+ 3. **Configure Test Phone Numbers (Optional):**
52
+ - Go to Authentication → Sign-in method → Phone
53
+ - Add test phone numbers if needed for development
54
+
55
+ 4. **Set MFA Enforcement (Optional):**
56
+ - You can set MFA as required for all users in Firebase Console
57
+ - Or use the approach in this app where we enforce it programmatically
58
+
59
+ 5. **Monitor MFA Usage:**
60
+ - Go to Authentication → Users to see which users have MFA enabled
61
+ - Look for the "Multi-factor" column in the user list
62
+
63
+ TESTING MFA:
64
+
65
+ 1. Create a new user account through registration
66
+ 2. After email verification and login, the MFA enrollment modal will appear
67
+ 3. Enter a valid phone number (use your real phone for testing)
68
+ 4. Complete the SMS verification process
69
+ 5. Try logging out and back in - you'll now need both password and SMS code
70
+
71
+ PRODUCTION CONSIDERATIONS:
72
+
73
+ - Ensure you have proper SMS quotas set up in Firebase
74
+ - Consider implementing backup codes for users who lose phone access
75
+ - Monitor SMS costs and usage
76
+ - Test the flow thoroughly with real phone numbers
77
+ - Consider implementing MFA recovery mechanisms
78
+
79
+ */
@@ -0,0 +1,28 @@
1
+ export interface PasswordPolicyResult {
2
+ hasMinLength: boolean;
3
+ hasUpperCase: boolean;
4
+ hasNumber: boolean;
5
+ hasSpecialChar: boolean;
6
+ passwordsMatch: boolean;
7
+ isStrong: boolean;
8
+ }
9
+
10
+ const MIN_PASSWORD_LENGTH = 10;
11
+ const SPECIAL_CHAR_REGEX = /[!@#$%^&*(),.?":{}|<>]/;
12
+
13
+ export const evaluatePasswordPolicy = (password: string, confirmPassword?: string): PasswordPolicyResult => {
14
+ const hasMinLength = password.length >= MIN_PASSWORD_LENGTH;
15
+ const hasUpperCase = /[A-Z]/.test(password);
16
+ const hasNumber = /[0-9]/.test(password);
17
+ const hasSpecialChar = SPECIAL_CHAR_REGEX.test(password);
18
+ const passwordsMatch = confirmPassword === undefined ? true : password === confirmPassword;
19
+
20
+ return {
21
+ hasMinLength,
22
+ hasUpperCase,
23
+ hasNumber,
24
+ hasSpecialChar,
25
+ passwordsMatch,
26
+ isStrong: hasMinLength && hasUpperCase && hasNumber && hasSpecialChar && passwordsMatch,
27
+ };
28
+ };