mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
### Context Resolves FE-4355 - Users who reach `/authorize` with zero organizations see a "Create an organization" CTA that links to a bare `/new` - After creating the org, they land on `/new/<slug>?projectName=...` instead of back at `/authorize?auth_id=...` - `returnTo` handling used to exist here ([#30211](https://github.com/supabase/supabase/pull/30211), refactored in [#44522](https://github.com/supabase/supabase/pull/44522)) but was dropped as a side effect of the interstitial redesign in [#46359](https://github.com/supabase/supabase/pull/46359) - [#47760](https://github.com/supabase/supabase/pull/47760) later patched the missing CTA back in but not the returnTo round-trip
255 lines
7.7 KiB
TypeScript
255 lines
7.7 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import Head from 'next/head'
|
|
import { useEffect, useEffectEvent, useMemo, useState, type ReactNode } from 'react'
|
|
import { useForm, type UseFormReturn } from 'react-hook-form'
|
|
import { toast } from 'sonner'
|
|
|
|
import { ApiAuthorizationApprovedScreen } from './ApiAuthorization.Approved'
|
|
import { ApiAuthorizationErrorScreen } from './ApiAuthorization.Error'
|
|
import { ApiAuthorizationMainView } from './ApiAuthorization.Form'
|
|
import { ApiAuthorizationLoadingScreen } from './ApiAuthorization.Loading'
|
|
import {
|
|
approvalFormSchema,
|
|
type ApprovalState,
|
|
type IApprovalFormSchema,
|
|
} from './ApiAuthorization.Schema'
|
|
import { useApiAuthorizationApproveMutation } from '@/data/api-authorization/api-authorization-approve-mutation'
|
|
import { useApiAuthorizationDeclineMutation } from '@/data/api-authorization/api-authorization-decline-mutation'
|
|
import { useApiAuthorizationQuery } from '@/data/api-authorization/api-authorization-query'
|
|
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
|
|
import { buildStudioPageTitle } from '@/lib/page-title'
|
|
import type { Organization } from '@/types'
|
|
|
|
function getMatchingOrganization(
|
|
organization_slug: string | undefined,
|
|
organizations: Array<Organization> | undefined
|
|
): Organization | null {
|
|
if (!organization_slug || !organizations) return null
|
|
return organizations.find(({ slug }) => slug === organization_slug) ?? null
|
|
}
|
|
|
|
interface PreselectOrganizationSlugParameters {
|
|
form: UseFormReturn<IApprovalFormSchema>
|
|
organization_slug: string | undefined
|
|
organizations: Array<{ slug: string }>
|
|
}
|
|
|
|
function preselectOrganizationSlug({
|
|
form,
|
|
organization_slug,
|
|
organizations,
|
|
}: PreselectOrganizationSlugParameters) {
|
|
if (organization_slug) {
|
|
const preselected = organizations.find(({ slug }) => slug === organization_slug)
|
|
if (preselected) form.setValue('selectedOrgSlug', preselected.slug)
|
|
} else if (!form.getValues('selectedOrgSlug') && organizations.length === 1) {
|
|
form.setValue('selectedOrgSlug', organizations[0].slug)
|
|
}
|
|
}
|
|
|
|
function useOrganizationsState(organization_slug: string | undefined, enabled = true) {
|
|
const {
|
|
data: organizations,
|
|
isPending: isLoadingOrganizations,
|
|
isError: isErrorOrganizations,
|
|
error: organizationsError,
|
|
} = useOrganizationsQuery({ enabled })
|
|
|
|
const organizationsState = useMemo(
|
|
function calculateOrganizationsState() {
|
|
if (!enabled) {
|
|
return { _tag: 'loading' as const }
|
|
}
|
|
if (isLoadingOrganizations) {
|
|
return { _tag: 'loading' as const }
|
|
}
|
|
if (isErrorOrganizations) {
|
|
return { _tag: 'error' as const, error: organizationsError }
|
|
}
|
|
if (organizations.length === 0) {
|
|
return { _tag: 'empty' as const }
|
|
}
|
|
if (organization_slug) {
|
|
const matchingOrganization = getMatchingOrganization(organization_slug, organizations)
|
|
if (!matchingOrganization) {
|
|
return { _tag: 'not_member' as const }
|
|
}
|
|
}
|
|
return { _tag: 'success' as const, organizations }
|
|
},
|
|
[
|
|
enabled,
|
|
isLoadingOrganizations,
|
|
isErrorOrganizations,
|
|
organizationsError,
|
|
organizations,
|
|
organization_slug,
|
|
]
|
|
)
|
|
|
|
return organizationsState
|
|
}
|
|
|
|
function usePrefillFormOnOrganizationsSuccess(
|
|
form: UseFormReturn<IApprovalFormSchema>,
|
|
organizationsState: ReturnType<typeof useOrganizationsState>,
|
|
organization_slug: string | undefined
|
|
) {
|
|
const prefillForm = useEffectEvent(() => {
|
|
if (organizationsState._tag === 'success') {
|
|
preselectOrganizationSlug({
|
|
form,
|
|
organization_slug,
|
|
organizations: organizationsState.organizations,
|
|
})
|
|
}
|
|
})
|
|
useEffect(() => {
|
|
if (organizationsState._tag === 'success') {
|
|
prefillForm()
|
|
}
|
|
}, [organizationsState._tag])
|
|
}
|
|
|
|
export interface ApiAuthorizationValidScreenProps {
|
|
auth_id: string
|
|
organization_slug: string | undefined
|
|
navigate: (destination: string) => void
|
|
}
|
|
|
|
export function ApiAuthorizationValidScreen({
|
|
auth_id,
|
|
organization_slug,
|
|
navigate,
|
|
}: ApiAuthorizationValidScreenProps): ReactNode {
|
|
const [approvalState, setApprovalState] = useState<ApprovalState>('indeterminate')
|
|
|
|
const form = useForm<IApprovalFormSchema>({
|
|
resolver: zodResolver(approvalFormSchema),
|
|
defaultValues: { selectedOrgSlug: '' },
|
|
mode: 'onSubmit',
|
|
reValidateMode: 'onBlur',
|
|
})
|
|
|
|
const organizationsState = useOrganizationsState(organization_slug)
|
|
usePrefillFormOnOrganizationsSuccess(form, organizationsState, organization_slug)
|
|
|
|
const {
|
|
data: requester,
|
|
isPending: isLoading,
|
|
isError,
|
|
error,
|
|
} = useApiAuthorizationQuery({ id: auth_id })
|
|
const isApproved = (requester?.approved_at ?? null) !== null
|
|
|
|
const {
|
|
mutate: approveRequest,
|
|
error: approveError,
|
|
reset: resetApproveError,
|
|
} = useApiAuthorizationApproveMutation({
|
|
onSuccess: (res) => {
|
|
window.location.href = res.url
|
|
},
|
|
onError: () => {
|
|
setApprovalState('indeterminate')
|
|
},
|
|
})
|
|
const {
|
|
mutate: declineRequest,
|
|
error: declineError,
|
|
reset: resetDeclineError,
|
|
} = useApiAuthorizationDeclineMutation({
|
|
onSuccess: () => {
|
|
toast.success('Declined API authorization request')
|
|
navigate('/organizations')
|
|
},
|
|
onError: () => {
|
|
setApprovalState('indeterminate')
|
|
},
|
|
})
|
|
const actionError = approveError
|
|
? `Failed to authorize request: ${approveError.message}`
|
|
: declineError
|
|
? `Failed to cancel authorization request: ${declineError.message}`
|
|
: undefined
|
|
const resetActionError = () => {
|
|
resetApproveError()
|
|
resetDeclineError()
|
|
}
|
|
|
|
const onApproveRequest = form.handleSubmit((values) => {
|
|
if (approvalState !== 'indeterminate') {
|
|
return
|
|
}
|
|
resetActionError()
|
|
setApprovalState('approving')
|
|
approveRequest({ id: auth_id, slug: values.selectedOrgSlug })
|
|
})
|
|
|
|
const onDeclineRequest = form.handleSubmit((values) => {
|
|
if (approvalState !== 'indeterminate') {
|
|
return
|
|
}
|
|
resetActionError()
|
|
setApprovalState('declining')
|
|
declineRequest({ id: auth_id, slug: values.selectedOrgSlug })
|
|
})
|
|
|
|
if (isLoading) {
|
|
return <ApiAuthorizationLoadingScreen />
|
|
}
|
|
|
|
if (isError) {
|
|
return <ApiAuthorizationErrorScreen error={error} />
|
|
}
|
|
|
|
const effectiveRequester = requester
|
|
const effectiveOrganizationsState = organizationsState
|
|
const effectiveOrganizationSlug = organization_slug
|
|
const effectiveApprovalState = approvalState
|
|
const pageTitle = effectiveRequester
|
|
? buildStudioPageTitle({ section: `Authorize ${effectiveRequester.name}`, brand: 'Supabase' })
|
|
: undefined
|
|
|
|
if (!effectiveRequester) {
|
|
return <ApiAuthorizationErrorScreen error={undefined} />
|
|
}
|
|
|
|
if (isApproved) {
|
|
const approvedOrganization =
|
|
effectiveOrganizationsState._tag === 'success'
|
|
? effectiveOrganizationsState.organizations.find(
|
|
(org) => org.slug === effectiveRequester.approved_organization_slug
|
|
)
|
|
: undefined
|
|
|
|
return (
|
|
<>
|
|
<Head>{pageTitle && <title>{pageTitle}</title>}</Head>
|
|
<ApiAuthorizationApprovedScreen
|
|
requester={effectiveRequester}
|
|
organization={approvedOrganization}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Head>{pageTitle && <title>{pageTitle}</title>}</Head>
|
|
<ApiAuthorizationMainView
|
|
auth_id={auth_id}
|
|
approvalState={effectiveApprovalState}
|
|
form={form}
|
|
requester={effectiveRequester}
|
|
requestedOrganizationSlug={effectiveOrganizationSlug}
|
|
organizations={effectiveOrganizationsState}
|
|
actionError={actionError}
|
|
onOrganizationChange={resetActionError}
|
|
onApprove={onApproveRequest}
|
|
onDecline={onDeclineRequest}
|
|
/>
|
|
</>
|
|
)
|
|
}
|