diff --git a/apps/studio/components/interfaces/SignIn/SignInMfa.utils.test.ts b/apps/studio/components/interfaces/SignIn/SignInMfa.utils.test.ts new file mode 100644 index 00000000000..dbc48579ac2 --- /dev/null +++ b/apps/studio/components/interfaces/SignIn/SignInMfa.utils.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from 'vitest' + +import { + getFactorDisplayName, + getFactorFriendlyName, + MFA_SUPPORT_EMAIL_HREF, +} from './SignInMfa.utils' + +describe('getFactorFriendlyName', () => { + test('returns the trimmed friendly name', () => { + expect(getFactorFriendlyName({ friendly_name: ' iPhone Passwords ' })).toBe( + 'iPhone Passwords' + ) + }) + + test.each([ + ['null factor', null], + ['undefined factor', undefined], + ['missing name', { friendly_name: undefined }], + ['empty name', { friendly_name: '' }], + ['whitespace-only name', { friendly_name: ' ' }], + ])('returns undefined for %s', (_label, factor) => { + expect(getFactorFriendlyName(factor)).toBeUndefined() + }) +}) + +describe('getFactorDisplayName', () => { + test('returns the friendly name when present', () => { + expect(getFactorDisplayName({ friendly_name: 'Work phone' })).toBe('Work phone') + }) + + test('falls back to a generic label when the name is blank', () => { + expect(getFactorDisplayName({ friendly_name: ' ' })).toBe('your authenticator app') + }) +}) + +describe('MFA_SUPPORT_EMAIL_HREF', () => { + test('targets support with an encoded subject', () => { + const url = new URL(MFA_SUPPORT_EMAIL_HREF) + + expect(url.protocol).toBe('mailto:') + expect(url.pathname).toBe('support@supabase.com') + expect(url.searchParams.get('subject')).toBe('Unable to sign in via MFA') + }) +}) diff --git a/apps/studio/components/interfaces/SignIn/SignInMfa.utils.ts b/apps/studio/components/interfaces/SignIn/SignInMfa.utils.ts new file mode 100644 index 00000000000..512db3a4917 --- /dev/null +++ b/apps/studio/components/interfaces/SignIn/SignInMfa.utils.ts @@ -0,0 +1,20 @@ +import type { Factor } from '@supabase/supabase-js' + +export const MFA_SUPPORT_EMAIL_HREF = `mailto:support@supabase.com?subject=${encodeURIComponent('Unable to sign in via MFA')}` + +type FactorWithName = Pick | null | undefined + +/** + * Returns the user-provided name of a TOTP factor, or `undefined` if it has none. + * + * The name is a free-text label chosen at enrollment time. It does not identify which + * authenticator app actually generates the codes. + */ +export function getFactorFriendlyName(factor: FactorWithName): string | undefined { + const name = factor?.friendly_name?.trim() + return name && name.length > 0 ? name : undefined +} + +export function getFactorDisplayName(factor: FactorWithName): string { + return getFactorFriendlyName(factor) ?? 'your authenticator app' +} diff --git a/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx b/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx index 9c6e73dd10a..7e9da128c79 100644 --- a/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx +++ b/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx @@ -13,6 +13,11 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import z from 'zod' +import { + getFactorDisplayName, + getFactorFriendlyName, + MFA_SUPPORT_EMAIL_HREF, +} from './SignInMfa.utils' import { AlertError } from '@/components/ui/AlertError' import { useAddLoginEvent } from '@/data/misc/audit-login-mutation' import { useMfaChallengeAndVerifyMutation } from '@/data/profile/mfa-challenge-and-verify-mutation' @@ -28,13 +33,6 @@ const schema = z.object({ const formId = 'sign-in-mfa-form' -const SUPPORT_EMAIL_HREF = `mailto:support@supabase.com?subject=${encodeURIComponent('Unable to sign in via MFA')}` - -function getFactorDisplayName(factor: Pick | null | undefined): string { - const name = factor?.friendly_name?.trim() - return name && name.length > 0 ? name : 'your authenticator app' -} - interface SignInMfaFormProps { context?: 'forgot-password' | 'sign-in' } @@ -120,6 +118,7 @@ export const SignInMfaForm = ({ context = 'sign-in' }: SignInMfaFormProps) => { }, [code]) const error = useAuthError() + const selectedFactorName = getFactorFriendlyName(selectedFactor) if (error) { return ( @@ -133,7 +132,7 @@ export const SignInMfaForm = ({ context = 'sign-in' }: SignInMfaFormProps) => { Back to sign in } @@ -169,15 +168,16 @@ export const SignInMfaForm = ({ context = 'sign-in' }: SignInMfaFormProps) => { render={({ field }) => ( - Code generated by{' '} - - {getFactorDisplayName(selectedFactor)} - + From the authenticator app registered as{' '} + {selectedFactorName} - ) : null + ) : ( + 'From the authenticator app registered to this account' + ) } > @@ -270,6 +270,14 @@ export const SignInMfaForm = ({ context = 'sign-in' }: SignInMfaFormProps) => { Force sign out and clear cookies +
  • + + Email support to regain access + +
  • diff --git a/apps/studio/components/interfaces/SignIn/SignInRecoveryCodeForm.tsx b/apps/studio/components/interfaces/SignIn/SignInRecoveryCodeForm.tsx index 0789b140f9a..f8b6949496f 100644 --- a/apps/studio/components/interfaces/SignIn/SignInRecoveryCodeForm.tsx +++ b/apps/studio/components/interfaces/SignIn/SignInRecoveryCodeForm.tsx @@ -23,6 +23,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import z from 'zod' +import { MFA_SUPPORT_EMAIL_HREF } from './SignInMfa.utils' import { AlertError } from '@/components/ui/AlertError' import { useAddLoginEvent } from '@/data/misc/audit-login-mutation' import { useRecoveryCodesStatusQuery } from '@/data/recovery-codes/recovery-codes-status-query' @@ -37,8 +38,6 @@ const schema = z.object({ const formId = 'sign-in-recovery-code-form' -const SUPPORT_EMAIL_HREF = `mailto:support@supabase.com?subject=${encodeURIComponent('Unable to sign in via MFA')}` - export const SignInRecoveryCodeForm = () => { const router = useRouter() const signOut = useSignOut() @@ -112,7 +111,7 @@ export const SignInRecoveryCodeForm = () => { Back to sign in } @@ -238,6 +237,14 @@ export const SignInRecoveryCodeForm = () => { Force sign out and clear cookies +
  • + + Email support to regain access + +