@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.
- package/.env.example +100 -0
- package/LICENSE +190 -0
- package/NOTICE +18 -0
- package/README.md +133 -0
- package/app/components/actions/case-export/core-export.ts +328 -0
- package/app/components/actions/case-export/data-processing.ts +167 -0
- package/app/components/actions/case-export/download-handlers.ts +900 -0
- package/app/components/actions/case-export/index.ts +41 -0
- package/app/components/actions/case-export/metadata-helpers.ts +107 -0
- package/app/components/actions/case-export/types-constants.ts +56 -0
- package/app/components/actions/case-export/validation-utils.ts +25 -0
- package/app/components/actions/case-export.ts +4 -0
- package/app/components/actions/case-import/annotation-import.ts +35 -0
- package/app/components/actions/case-import/confirmation-import.ts +363 -0
- package/app/components/actions/case-import/image-operations.ts +61 -0
- package/app/components/actions/case-import/index.ts +39 -0
- package/app/components/actions/case-import/orchestrator.ts +420 -0
- package/app/components/actions/case-import/storage-operations.ts +270 -0
- package/app/components/actions/case-import/validation.ts +189 -0
- package/app/components/actions/case-import/zip-processing.ts +413 -0
- package/app/components/actions/case-manage.ts +524 -0
- package/app/components/actions/case-review.ts +4 -0
- package/app/components/actions/confirm-export.ts +351 -0
- package/app/components/actions/generate-pdf.ts +210 -0
- package/app/components/actions/image-manage.ts +385 -0
- package/app/components/actions/notes-manage.ts +33 -0
- package/app/components/actions/signout.module.css +15 -0
- package/app/components/actions/signout.tsx +50 -0
- package/app/components/audit/user-audit-viewer.tsx +975 -0
- package/app/components/audit/user-audit.module.css +568 -0
- package/app/components/auth/auth-provider.tsx +78 -0
- package/app/components/auth/mfa-enrollment.module.css +268 -0
- package/app/components/auth/mfa-enrollment.tsx +398 -0
- package/app/components/auth/mfa-verification.module.css +251 -0
- package/app/components/auth/mfa-verification.tsx +295 -0
- package/app/components/button/button.module.css +63 -0
- package/app/components/button/button.tsx +46 -0
- package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
- package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
- package/app/components/canvas/canvas.module.css +314 -0
- package/app/components/canvas/canvas.tsx +449 -0
- package/app/components/canvas/confirmation/confirmation.module.css +187 -0
- package/app/components/canvas/confirmation/confirmation.tsx +214 -0
- package/app/components/colors/colors.module.css +59 -0
- package/app/components/colors/colors.tsx +68 -0
- package/app/components/form/base-form.tsx +21 -0
- package/app/components/form/form-button.tsx +28 -0
- package/app/components/form/form-field.tsx +53 -0
- package/app/components/form/form-message.tsx +17 -0
- package/app/components/form/form-toggle.tsx +23 -0
- package/app/components/form/form.module.css +427 -0
- package/app/components/form/index.ts +6 -0
- package/app/components/icon/icon.module.css +3 -0
- package/app/components/icon/icon.tsx +27 -0
- package/app/components/icon/icons.svg +102 -0
- package/app/components/icon/manifest.json +110 -0
- package/app/components/sidebar/case-export/case-export.module.css +386 -0
- package/app/components/sidebar/case-export/case-export.tsx +317 -0
- package/app/components/sidebar/case-import/case-import.module.css +626 -0
- package/app/components/sidebar/case-import/case-import.tsx +404 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
- package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
- package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
- package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
- package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
- package/app/components/sidebar/case-import/index.ts +18 -0
- package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
- package/app/components/sidebar/cases/cases-modal.module.css +166 -0
- package/app/components/sidebar/cases/cases-modal.tsx +201 -0
- package/app/components/sidebar/cases/cases.module.css +713 -0
- package/app/components/sidebar/files/files-modal.module.css +209 -0
- package/app/components/sidebar/files/files-modal.tsx +239 -0
- package/app/components/sidebar/hash/hash-utility.module.css +366 -0
- package/app/components/sidebar/hash/hash-utility.tsx +982 -0
- package/app/components/sidebar/notes/notes-modal.tsx +51 -0
- package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
- package/app/components/sidebar/notes/notes.module.css +360 -0
- package/app/components/sidebar/sidebar-container.tsx +149 -0
- package/app/components/sidebar/sidebar.module.css +321 -0
- package/app/components/sidebar/sidebar.tsx +215 -0
- package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
- package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
- package/app/components/theme-provider/theme-provider.tsx +131 -0
- package/app/components/theme-provider/theme.ts +155 -0
- package/app/components/toast/toast.module.css +137 -0
- package/app/components/toast/toast.tsx +56 -0
- package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
- package/app/components/toolbar/toolbar.module.css +42 -0
- package/app/components/toolbar/toolbar.tsx +167 -0
- package/app/components/user/delete-account.module.css +274 -0
- package/app/components/user/delete-account.tsx +471 -0
- package/app/components/user/inactivity-warning.module.css +145 -0
- package/app/components/user/inactivity-warning.tsx +84 -0
- package/app/components/user/manage-profile.module.css +190 -0
- package/app/components/user/manage-profile.tsx +253 -0
- package/app/components/user/mfa-phone-update.tsx +739 -0
- package/app/config-example/admin-service.json +13 -0
- package/app/config-example/config.json +17 -0
- package/app/config-example/firebase.ts +21 -0
- package/app/config-example/inactivity.ts +13 -0
- package/app/config-example/meta-config.json +6 -0
- package/app/contexts/auth.context.ts +12 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +44 -0
- package/app/hooks/useInactivityTimeout.ts +110 -0
- package/app/root.tsx +170 -0
- package/app/routes/_index.tsx +16 -0
- package/app/routes/auth/emailActionHandler.module.css +232 -0
- package/app/routes/auth/emailActionHandler.tsx +405 -0
- package/app/routes/auth/emailVerification.tsx +120 -0
- package/app/routes/auth/login.module.css +523 -0
- package/app/routes/auth/login.tsx +654 -0
- package/app/routes/auth/passwordReset.module.css +274 -0
- package/app/routes/auth/passwordReset.tsx +154 -0
- package/app/routes/auth/route.ts +16 -0
- package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
- package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
- package/app/routes/mobile-prevented/route.ts +14 -0
- package/app/routes/striae/striae.module.css +30 -0
- package/app/routes/striae/striae.tsx +417 -0
- package/app/services/audit-export.service.ts +755 -0
- package/app/services/audit.service.ts +1454 -0
- package/app/services/firebase-errors.ts +106 -0
- package/app/services/firebase.ts +15 -0
- package/app/styles/legal-pages.module.css +113 -0
- package/app/styles/root.module.css +146 -0
- package/app/tailwind.css +225 -0
- package/app/types/annotations.ts +45 -0
- package/app/types/audit.ts +301 -0
- package/app/types/case.ts +90 -0
- package/app/types/export.ts +8 -0
- package/app/types/file.ts +30 -0
- package/app/types/import.ts +107 -0
- package/app/types/index.ts +24 -0
- package/app/types/user.ts +38 -0
- package/app/utils/SHA256.ts +461 -0
- package/app/utils/annotation-timestamp.ts +25 -0
- package/app/utils/audit-export-signature.ts +117 -0
- package/app/utils/auth-action-settings.ts +48 -0
- package/app/utils/auth.ts +34 -0
- package/app/utils/batch-operations.ts +135 -0
- package/app/utils/confirmation-signature.ts +193 -0
- package/app/utils/data-operations.ts +871 -0
- package/app/utils/device-detection.ts +5 -0
- package/app/utils/html-sanitizer.ts +80 -0
- package/app/utils/id-generator.ts +36 -0
- package/app/utils/meta.ts +48 -0
- package/app/utils/mfa-phone.ts +97 -0
- package/app/utils/mfa.ts +79 -0
- package/app/utils/password-policy.ts +28 -0
- package/app/utils/permissions.ts +562 -0
- package/app/utils/signature-utils.ts +160 -0
- package/app/utils/style.ts +83 -0
- package/app/utils/version.ts +5 -0
- package/firebase.json +11 -0
- package/functions/[[path]].ts +10 -0
- package/package.json +138 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/publickey.info@striae.org.asc +17 -0
- package/public/.well-known/security.txt +7 -0
- package/public/_headers +28 -0
- package/public/_routes.json +13 -0
- package/public/assets/striae.jpg +0 -0
- package/public/clear.jpg +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-dark.png +0 -0
- package/public/manifest.json +25 -0
- package/public/oin-badge.png +0 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/striae-ascii.txt +10 -0
- package/scripts/deploy-all.sh +100 -0
- package/scripts/deploy-config.sh +940 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-worker-secrets.sh +215 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/install-workers.sh +88 -0
- package/scripts/run-eslint.cjs +35 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/tailwind.config.ts +22 -0
- package/tsconfig.json +33 -0
- package/vite.config.ts +35 -0
- package/worker-configuration.d.ts +7490 -0
- package/workers/audit-worker/package.json +17 -0
- package/workers/audit-worker/src/audit-worker.example.ts +195 -0
- package/workers/audit-worker/worker-configuration.d.ts +7448 -0
- package/workers/audit-worker/wrangler.jsonc.example +29 -0
- package/workers/data-worker/package.json +17 -0
- package/workers/data-worker/src/data-worker.example.ts +267 -0
- package/workers/data-worker/src/signature-utils.ts +79 -0
- package/workers/data-worker/src/signing-payload-utils.ts +290 -0
- package/workers/data-worker/worker-configuration.d.ts +7448 -0
- package/workers/data-worker/wrangler.jsonc.example +30 -0
- package/workers/image-worker/package.json +17 -0
- package/workers/image-worker/src/image-worker.example.ts +180 -0
- package/workers/image-worker/worker-configuration.d.ts +7447 -0
- package/workers/image-worker/wrangler.jsonc.example +22 -0
- package/workers/keys-worker/package.json +17 -0
- package/workers/keys-worker/src/keys.example.ts +66 -0
- package/workers/keys-worker/src/keys.ts +66 -0
- package/workers/keys-worker/worker-configuration.d.ts +7447 -0
- package/workers/keys-worker/wrangler.jsonc.example +22 -0
- package/workers/pdf-worker/package.json +17 -0
- package/workers/pdf-worker/src/format-striae.ts +534 -0
- package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
- package/workers/pdf-worker/src/report-types.ts +69 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
- package/workers/pdf-worker/wrangler.jsonc.example +26 -0
- package/workers/user-worker/package.json +17 -0
- package/workers/user-worker/src/user-worker.example.ts +636 -0
- package/workers/user-worker/worker-configuration.d.ts +7448 -0
- package/workers/user-worker/wrangler.jsonc.example +29 -0
- 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: '<script>alert("xss")</script>'
|
|
24
|
+
*
|
|
25
|
+
* escapeHtml('John & Jane <test@example.com>')
|
|
26
|
+
* // Returns: 'John & Jane <test@example.com>'
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function escapeHtml(text: string | null | undefined): string {
|
|
30
|
+
if (!text) return '';
|
|
31
|
+
|
|
32
|
+
return String(text)
|
|
33
|
+
.replace(/&/g, '&')
|
|
34
|
+
.replace(/</g, '<')
|
|
35
|
+
.replace(/>/g, '>')
|
|
36
|
+
.replace(/"/g, '"')
|
|
37
|
+
.replace(/'/g, ''')
|
|
38
|
+
.replace(/\//g, '/');
|
|
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: '<script>alert("xss")</script>',
|
|
61
|
+
* // email: 'test@example.com',
|
|
62
|
+
* // company: 'ACME & 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
|
+
};
|
package/app/utils/mfa.ts
ADDED
|
@@ -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
|
+
};
|