mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 04:15:04 +03:00
## Screenshots ### Table editor: foreign record selector Before: <img width="802" height="213" alt="image" src="https://github.com/user-attachments/assets/82ee3ce6-ac72-4b49-b1b0-2e635688cbb1" /> After: <img width="609" height="194" alt="image" src="https://github.com/user-attachments/assets/e9cc09c1-1c6b-4099-8cae-abe08f50fda9" /> ### Account - Add TOTP Before: <img width="527" height="679" alt="image" src="https://github.com/user-attachments/assets/b9f4a626-e24b-46e3-8385-700ef181308b" /> After: <img width="531" height="684" alt="image" src="https://github.com/user-attachments/assets/549745a7-9655-4a7d-9e0e-51f75b6a1c61" /> ### Organisation Audit Logs Details Before: <img width="673" height="1321" alt="image" src="https://github.com/user-attachments/assets/0bb360cf-6f27-4574-b9af-485a3836b17b" /> After: <img width="669" height="1273" alt="image" src="https://github.com/user-attachments/assets/0382c662-e270-41fd-a8ee-08528dedfce3" /> ### Data API Integration Docs Before: <img width="1115" height="891" alt="image" src="https://github.com/user-attachments/assets/db0c7698-53b7-4422-aac3-5e674b0bf151" /> After: <img width="1193" height="1272" alt="image" src="https://github.com/user-attachments/assets/927e5c43-413b-49c1-9b71-8ab628179c70" /> ### Edge Function Edit Secret Before: <img width="599" height="255" alt="image" src="https://github.com/user-attachments/assets/d6aa2f87-e247-4724-9e43-02b71933241c" /> After: <img width="596" height="261" alt="image" src="https://github.com/user-attachments/assets/d94acb41-07e1-497f-9697-830390526f4a" /> ### JWT Key Details Before: <img width="536" height="549" alt="image" src="https://github.com/user-attachments/assets/43672adc-dc0e-4e65-b7d4-b4537d22f6ea" /> After: <img width="523" height="517" alt="image" src="https://github.com/user-attachments/assets/e501e8a8-7f41-46a0-bb69-d240cea594f0" /> ### Realtime Filter Popover Before: <img width="403" height="576" alt="image" src="https://github.com/user-attachments/assets/73842450-ba87-456b-98fc-625b99149449" /> After: <img width="387" height="564" alt="image" src="https://github.com/user-attachments/assets/f2b35035-947c-4342-84dd-3548f9bd5e9f" /> ### Realtime broadcast message dialog Before: <img width="520" height="393" alt="image" src="https://github.com/user-attachments/assets/4f4a1a93-e0cf-4268-ae4e-baf8b8a62e74" /> After: <img width="525" height="392" alt="image" src="https://github.com/user-attachments/assets/e1c1934a-1812-4013-8606-9b846dc2498d" /> ### Impersonation Popover Before: <img width="604" height="501" alt="image" src="https://github.com/user-attachments/assets/9abdc604-94f8-4ed4-9a95-4688e6504e76" /> <img width="587" height="599" alt="image" src="https://github.com/user-attachments/assets/5293c80c-9abd-43eb-899f-da759c83b598" /> After: <img width="594" height="585" alt="image" src="https://github.com/user-attachments/assets/5eaf2162-2d7f-444c-9052-c9afb00080f6" /> <img width="590" height="597" alt="image" src="https://github.com/user-attachments/assets/149dc7c1-689c-4e0f-a884-c6f5b0228ebc" /> ### Storage move item Before: <img width="521" height="285" alt="image" src="https://github.com/user-attachments/assets/7d0f945f-add5-412b-813a-9325b260ab28" /> After: <img width="529" height="274" alt="image" src="https://github.com/user-attachments/assets/ab0891a1-b31b-40b6-be53-92afc95095ea" /> ### Table Editor - Spreadsheet import Before: <img width="673" height="506" alt="image" src="https://github.com/user-attachments/assets/7a722908-10c2-4c04-95fb-b12d3c23557c" /> After: <img width="671" height="638" alt="image" src="https://github.com/user-attachments/assets/689b1fb6-031c-4a02-9e7f-739356c1453d" /> ### Org Billing downgrade survey Before: <img width="788" height="655" alt="image" src="https://github.com/user-attachments/assets/c7a0d4c6-e9b9-4c6c-9cf1-e7d05016233f" /> After: <img width="1630" height="1354" alt="image" src="https://github.com/user-attachments/assets/e3f5473b-db9a-42b1-9242-40480c25fc02" /> ### Project API Docs Before: <img width="1030" height="396" alt="image" src="https://github.com/user-attachments/assets/95643b21-811a-4ba7-918a-5e655c262ac1" /> After: <img width="1012" height="457" alt="image" src="https://github.com/user-attachments/assets/d5559646-bb89-43b6-ad62-c5684b54b3fb" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Standardized form field layouts across panels, dialogs, and modals for a more consistent editing and reading experience. * Replaced several Input-based textareas with dedicated TextArea/ExpandingTextArea controls and aligned labels with wrapper layouts for clearer accessibility. * Introduced grouped/composable input controls, added additional read-only detail fields and labeled value/copy blocks, and tightened header/layout spacing and control alignment. * Swapped notice styles for improved warning/admonition presentation. * **Chores** * Removed a deprecated AutoTextArea component. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
269 lines
8.3 KiB
TypeScript
269 lines
8.3 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
import { LOCAL_STORAGE_KEYS } from 'common'
|
|
import { useEffect, useState } from 'react'
|
|
import { useForm, type SubmitHandler } from 'react-hook-form'
|
|
import { toast } from 'sonner'
|
|
import { Form, FormControl, FormField, Input_Shadcn_ } from 'ui'
|
|
import { Input } from 'ui-patterns/DataInputs/Input'
|
|
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
import { z } from 'zod'
|
|
|
|
import InformationBox from '@/components/ui/InformationBox'
|
|
import { organizationKeys } from '@/data/organizations/keys'
|
|
import { useMfaChallengeAndVerifyMutation } from '@/data/profile/mfa-challenge-and-verify-mutation'
|
|
import { useMfaEnrollMutation } from '@/data/profile/mfa-enroll-mutation'
|
|
import { useMfaUnenrollMutation } from '@/data/profile/mfa-unenroll-mutation'
|
|
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
|
|
|
|
type TOTP = { qr_code: string; secret: string; uri: string }
|
|
|
|
interface AddNewFactorModalProps {
|
|
visible: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) => {
|
|
const { data, mutate: enroll, isPending: isEnrolling, reset } = useMfaEnrollMutation()
|
|
|
|
useEffect(() => {
|
|
if (!visible) reset()
|
|
}, [reset, visible])
|
|
|
|
return (
|
|
<>
|
|
<FirstStep
|
|
visible={visible && !Boolean(data)}
|
|
isEnrolling={isEnrolling}
|
|
enroll={enroll}
|
|
reset={reset}
|
|
onClose={onClose}
|
|
/>
|
|
<SecondStep
|
|
visible={visible && Boolean(data)}
|
|
factorName={data?.friendly_name ?? ''}
|
|
factor={data as Extract<typeof data, { type: 'totp' }>}
|
|
isLoading={isEnrolling}
|
|
onClose={onClose}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
interface FirstStepProps {
|
|
visible: boolean
|
|
isEnrolling: boolean
|
|
reset: () => void
|
|
enroll: (params: { factorType: 'totp'; friendlyName?: string }) => void
|
|
onClose: () => void
|
|
}
|
|
|
|
const FirstStep = ({ visible, isEnrolling, enroll, onClose }: FirstStepProps) => {
|
|
const FormSchema = z.object({
|
|
name: z.string().min(1, 'Please provide a name to identify this app'),
|
|
})
|
|
const form = useForm<z.infer<typeof FormSchema>>({
|
|
resolver: zodResolver(FormSchema),
|
|
defaultValues: { name: '' },
|
|
mode: 'onChange',
|
|
})
|
|
|
|
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
|
|
enroll({ factorType: 'totp', friendlyName: values.name })
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!visible) {
|
|
// Generate a name with a number between 0 and 1000
|
|
form.reset({ name: `App ${Math.floor(Math.random() * 1000)}` })
|
|
}
|
|
}, [form, visible])
|
|
|
|
return (
|
|
<ConfirmationModal
|
|
size="medium"
|
|
visible={visible}
|
|
title="Add a new authenticator app as a factor"
|
|
confirmLabel="Generate QR"
|
|
confirmLabelLoading="Generating QR"
|
|
loading={isEnrolling}
|
|
onCancel={onClose}
|
|
onConfirm={form.handleSubmit(onSubmit)}
|
|
>
|
|
<Form {...form}>
|
|
<form
|
|
id="verify-otp-form"
|
|
className="flex flex-col gap-4"
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
>
|
|
<FormField
|
|
key="name"
|
|
name="name"
|
|
control={form.control}
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
name="name"
|
|
label="Provide a name to identify this app"
|
|
description="A string will be randomly generated if a name is not provided"
|
|
>
|
|
<FormControl>
|
|
<Input_Shadcn_ id="name" {...field} />
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</form>
|
|
</Form>
|
|
</ConfirmationModal>
|
|
)
|
|
}
|
|
|
|
interface SecondStepProps {
|
|
visible: boolean
|
|
factorName: string
|
|
factor?: {
|
|
id: string
|
|
type: 'totp'
|
|
totp: TOTP
|
|
}
|
|
isLoading: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
const SecondStep = ({
|
|
visible,
|
|
factorName,
|
|
factor: outerFactor,
|
|
isLoading,
|
|
onClose,
|
|
}: SecondStepProps) => {
|
|
const queryClient = useQueryClient()
|
|
const [lastVisitedOrganization] = useLocalStorageQuery(
|
|
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
|
|
''
|
|
)
|
|
|
|
const FormSchema = z.object({
|
|
code: z.string().min(1, 'Please provide a code from your authenticator app'),
|
|
})
|
|
const form = useForm<z.infer<typeof FormSchema>>({
|
|
resolver: zodResolver(FormSchema),
|
|
defaultValues: { code: '' },
|
|
mode: 'onChange',
|
|
})
|
|
|
|
const [factor, setFactor] = useState<{ id: string; type: 'totp'; totp: TOTP } | null>(null)
|
|
|
|
const { mutate: unenroll } = useMfaUnenrollMutation({ onSuccess: () => onClose() })
|
|
const { mutate: challengeAndVerify, isPending: isVerifying } = useMfaChallengeAndVerifyMutation({
|
|
onError: (error) => {
|
|
toast.error(`Failed to add a second factor authentication: ${error?.message}`)
|
|
},
|
|
onSuccess: async () => {
|
|
if (lastVisitedOrganization) {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: organizationKeys.members(lastVisitedOrganization),
|
|
})
|
|
}
|
|
toast.success(`Successfully added a second factor authentication`)
|
|
onClose()
|
|
},
|
|
})
|
|
|
|
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
|
|
if (!factor) return toast.error('Factor required')
|
|
challengeAndVerify({ factorId: factor.id, code: values.code })
|
|
}
|
|
|
|
// this useEffect is to keep the factor until a new one comes. This is a fix to an issue which
|
|
// happens when closing the modal, the outer factor is reset to null too soon and the modal
|
|
// removes a big div mid transition.
|
|
useEffect(() => {
|
|
if (outerFactor && factor?.id !== outerFactor.id) {
|
|
setFactor(outerFactor)
|
|
form.reset({ code: '' })
|
|
}
|
|
}, [outerFactor])
|
|
|
|
return (
|
|
<ConfirmationModal
|
|
size="medium"
|
|
visible={visible}
|
|
className="py-5"
|
|
title={`Verify new factor ${factorName}`}
|
|
confirmLabel="Confirm"
|
|
confirmLabelLoading="Confirming"
|
|
loading={isVerifying}
|
|
onCancel={() => {
|
|
// If a factor has been created (but not verified), unenroll it. This will be run as a
|
|
// side effect so that it's not confusing to the user why the modal stays open while
|
|
// unenrolling.
|
|
if (factor) unenroll({ factorId: factor.id })
|
|
}}
|
|
onConfirm={form.handleSubmit(onSubmit)}
|
|
>
|
|
<p className="text-sm">
|
|
Use an authenticator app to scan the following QR code, and provide the code from the app to
|
|
complete the enrolment.
|
|
</p>
|
|
|
|
{isLoading && (
|
|
<div className="pb-4 px-4">
|
|
<GenericSkeletonLoader />
|
|
</div>
|
|
)}
|
|
|
|
{factor && (
|
|
<div className="flex flex-col gap-y-4">
|
|
<div className="flex justify-center py-6">
|
|
<div className="h-48 w-48 bg-white rounded-sm">
|
|
<img width={190} height={190} src={factor.totp.qr_code} alt={factor.totp.uri} />
|
|
</div>
|
|
</div>
|
|
|
|
<InformationBox
|
|
title="Unable to scan?"
|
|
description={
|
|
<FormItemLayout
|
|
isReactForm={false}
|
|
label="You can also enter this secret key into your authenticator app"
|
|
>
|
|
<Input copy disabled id="ref" size="small" value={factor.totp.secret} />
|
|
</FormItemLayout>
|
|
}
|
|
/>
|
|
|
|
<Form {...form}>
|
|
<form
|
|
id="verify-otp-form"
|
|
className="flex flex-col gap-4"
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
>
|
|
<FormField
|
|
key="code"
|
|
name="code"
|
|
control={form.control}
|
|
render={({ field }) => (
|
|
<FormItemLayout name="code" label="Authentication code">
|
|
<FormControl>
|
|
<Input_Shadcn_
|
|
id="code"
|
|
autoFocus
|
|
{...field}
|
|
placeholder="XXXXXX"
|
|
className="font-mono"
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</form>
|
|
</Form>
|
|
</div>
|
|
)}
|
|
</ConfirmationModal>
|
|
)
|
|
}
|