mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Improve UI for org invites if MFA is enforced (#47067)
## Context When opening an invite to join an organization that's enforced MFA for their members, if a member does not have MFA enabled yet, they'll see this UI which is confusing as there's no clear direction on what to do <img width="500" alt="image" src="https://github.com/user-attachments/assets/ca2d1047-20bf-40ca-a9ea-91e81c390e40" /> ## Changes involved - Updating the UI to consider this error message and prompt users to set up MFA <img width="501" height="317" alt="image" src="https://github.com/user-attachments/assets/d074ac6d-fd74-4fe0-9078-473fd2401045" /> - Small UI nudges to account security page - Tight copywriting to explicitly say MFA - Opt to use Card instead of Collapsible (collapsible seems unnecessary given that this is the only UI on this page) - Before: <img width="811" height="360" alt="image" src="https://github.com/user-attachments/assets/1412da3b-3903-4966-85ea-46e0ff443177" /> - After: <img width="817" height="370" alt="image" src="https://github.com/user-attachments/assets/02d5a2f5-8c1f-4f78-8a20-10c7a4ff563c" /> - Tiny change to the user dropdown, say "account" instead of "account preferences" + change icon - This imo aligns better as the account page covers more than just preferences - Before: <img width="307" height="178" alt="image" src="https://github.com/user-attachments/assets/fea43cac-9b0c-4fe4-94a3-946ed0925901" /> - After: <img width="300" height="183" alt="image" src="https://github.com/user-attachments/assets/800357fe-222f-49b8-b52b-ce4fabff7b95" /> ## To test - [ ] Have an organization on paid plan with MFA enforced - [ ] Invite a user that doesn't have MFA enabled - [ ] Try to join the organization with that user <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Organization invites now detect and handle MFA requirements with specific error messaging * Redesigned Multi-factor authentication section on account security page * **Improvements** * Updated TOTP authenticator help text for clarity * Updated account menu navigation label <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
This commit is contained in:
1 parent
0d14e05cc5
commit
37b072aac2
5 files changed
+54
-54
No files matched your search
@@ -19,8 +19,7 @@ export const TOTPFactors = () => {
|
||||
<>
|
||||
<section className="space-y-3">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Generate one-time passwords via authenticator apps like 1Password, Authy, etc. as a second
|
||||
factor to verify your identity during sign-in.
|
||||
Use an authenticator app (like 1Password or Authy) to verify your identity at sign-in.
|
||||
</p>
|
||||
<div>
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
@@ -59,6 +59,7 @@ export const OrganizationInvite = () => {
|
||||
const isInvitationLoading = inviteStatus === 'loading'
|
||||
const inviteContent = getOrganizationInviteContent({
|
||||
data,
|
||||
error,
|
||||
isSignUpEnabled,
|
||||
status: inviteStatus,
|
||||
})
|
||||
@@ -66,6 +67,8 @@ export const OrganizationInvite = () => {
|
||||
const loginRedirectLink = `/sign-in?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
|
||||
const signupRedirectLink = `/sign-up?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
|
||||
|
||||
const mfaRequiredError = error?.message.includes('MFA required')
|
||||
|
||||
const { mutate: joinOrganization, isPending: isJoining } =
|
||||
useOrganizationAcceptInvitationMutation({
|
||||
onSuccess: () => {
|
||||
@@ -85,21 +88,21 @@ export const OrganizationInvite = () => {
|
||||
const withLayout = (children: ReactNode) => (
|
||||
<InterstitialLayout
|
||||
logo={<SupabaseLogo />}
|
||||
titleClassName="text-xl"
|
||||
title={
|
||||
isInvitationLoading ? (
|
||||
<ShimmeringLoader className="mx-auto h-7 w-36 max-w-full py-0" />
|
||||
) : inviteContent.title ? (
|
||||
) : (
|
||||
inviteContent.title
|
||||
) : undefined
|
||||
)
|
||||
}
|
||||
description={
|
||||
isInvitationLoading ? (
|
||||
<ShimmeringLoader className="mx-auto h-4 w-48 max-w-full py-0" />
|
||||
) : inviteContent.description ? (
|
||||
) : (
|
||||
inviteContent.description
|
||||
) : undefined
|
||||
)
|
||||
}
|
||||
titleClassName="text-xl"
|
||||
>
|
||||
<div className="px-6 pb-6">{children}</div>
|
||||
</InterstitialLayout>
|
||||
@@ -154,6 +157,16 @@ export const OrganizationInvite = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (mfaRequiredError) {
|
||||
return withLayout(
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button variant="default" block asChild>
|
||||
<Link href="/account/security">Go to account settings</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
return withLayout(
|
||||
<OrganizationInviteError
|
||||
|
||||
@@ -13,12 +13,6 @@ type OrganizationInviteStatusVariables = {
|
||||
profileExists: boolean
|
||||
}
|
||||
|
||||
type OrganizationInviteContentVariables = {
|
||||
data?: OrganizationInviteByToken
|
||||
isSignUpEnabled: boolean
|
||||
status: OrganizationInviteStatus
|
||||
}
|
||||
|
||||
export type OrganizationInviteStatus =
|
||||
| 'signed-out'
|
||||
| 'loading'
|
||||
@@ -65,9 +59,15 @@ export function getOrganizationInviteStatus({
|
||||
|
||||
export function getOrganizationInviteContent({
|
||||
data,
|
||||
error,
|
||||
isSignUpEnabled,
|
||||
status,
|
||||
}: OrganizationInviteContentVariables) {
|
||||
}: {
|
||||
data?: OrganizationInviteByToken
|
||||
error?: ResponseError | null
|
||||
isSignUpEnabled: boolean
|
||||
status: OrganizationInviteStatus
|
||||
}) {
|
||||
const signedOutDescription = `Sign in${
|
||||
isSignUpEnabled ? ' or create an account' : ''
|
||||
} to view this invitation`
|
||||
@@ -90,7 +90,14 @@ export function getOrganizationInviteContent({
|
||||
if (status === 'expired') return { title: 'Invite expired' }
|
||||
if (status === 'invalid') return { title: 'Invite invalid' }
|
||||
if (status === 'no-longer-valid') return { title: 'Invite no longer available' }
|
||||
if (status === 'error') return { title: 'Unable to load invitation' }
|
||||
if (status === 'error') {
|
||||
if (error?.message.includes('MFA required')) {
|
||||
return {
|
||||
title: 'Set up MFA for your account',
|
||||
description: 'MFA needs to be enabled on your account to join this organization',
|
||||
}
|
||||
} else return { title: 'Unable to load invitation' }
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useFlag } from 'common'
|
||||
import { FlaskConical, Loader2, ScrollText, Settings } from 'lucide-react'
|
||||
import { FlaskConical, Loader2, ScrollText, User2 } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -116,14 +116,13 @@ export function UserDropdown({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Settings size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
Account preferences
|
||||
<User2 size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
Account
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex gap-2 cursor-pointer"
|
||||
onClick={() => toggleFeaturePreviewModal(true)}
|
||||
// onSelect={() => toggleFeaturePreviewModal(true)}
|
||||
>
|
||||
<FlaskConical size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
Feature previews
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Smartphone } from 'lucide-react'
|
||||
import { Badge, cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
|
||||
import { Lock } from 'lucide-react'
|
||||
import { Badge, Card, CardContent, CardHeader } from 'ui'
|
||||
import { PageContainer } from 'ui-patterns/PageContainer'
|
||||
import {
|
||||
PageHeader,
|
||||
@@ -18,19 +18,6 @@ import { useMfaListFactorsQuery } from '@/data/profile/mfa-list-factors-query'
|
||||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
import type { NextPageWithLayout } from '@/types'
|
||||
|
||||
const collapsibleClasses = [
|
||||
'bg-surface-100',
|
||||
'hover:bg-surface-200',
|
||||
'data-open:bg-surface-200',
|
||||
'border-default',
|
||||
'hover:border-strong data-open:border-strong',
|
||||
'data-open:pb-px col-span-12 rounded-sm',
|
||||
'-space-y-px overflow-hidden',
|
||||
'border shadow-sm',
|
||||
'transition',
|
||||
'hover:z-50',
|
||||
]
|
||||
|
||||
const Security: NextPageWithLayout = () => {
|
||||
const showSecuritySettings = useIsFeatureEnabled('account:show_security_settings')
|
||||
|
||||
@@ -53,28 +40,23 @@ const Security: NextPageWithLayout = () => {
|
||||
</PageHeaderMeta>
|
||||
</PageHeader>
|
||||
<PageContainer size="small">
|
||||
<Collapsible className={cn('mt-8', collapsibleClasses)}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="group flex w-full items-center justify-between rounded-sm py-3 px-4 md:px-6 text-foreground"
|
||||
>
|
||||
<div className="flex flex-row gap-4 items-center py-1">
|
||||
<Smartphone strokeWidth={1.5} />
|
||||
<span className="text-sm">Authenticator app</span>
|
||||
</div>
|
||||
<Card className="mt-8">
|
||||
<CardHeader className="py-3 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row gap-4 items-center py-1 mb-0">
|
||||
<Lock size={18} strokeWidth={1.5} />
|
||||
<span className="text-sm">Multi-factor authentication (MFA)</span>
|
||||
</div>
|
||||
|
||||
{data ? (
|
||||
<Badge variant={data.totp.length === 0 ? 'default' : 'success'}>
|
||||
{data.totp.length} app{data.totp.length === 1 ? '' : 's'} configured
|
||||
</Badge>
|
||||
) : null}
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="group border-t border-default bg-surface-100 py-6 px-4 md:px-6 text-foreground">
|
||||
{data ? (
|
||||
<Badge variant={data.totp.length === 0 ? 'default' : 'success'}>
|
||||
{data.totp.length} app{data.totp.length === 1 ? '' : 's'} configured
|
||||
</Badge>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TOTPFactors />
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageContainer>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user