index.tsx211 lines · main
| 1 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 2 | import { useQuery } from '@tanstack/react-query' |
| 3 | import { useParams } from 'common' |
| 4 | import { Loader2 } from 'lucide-react' |
| 5 | import { useState } from 'react' |
| 6 | import { toast } from 'sonner' |
| 7 | import { cn } from 'ui' |
| 8 | import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' |
| 9 | import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational' |
| 10 | import { |
| 11 | PageSection, |
| 12 | PageSectionAside, |
| 13 | PageSectionContent, |
| 14 | PageSectionDescription, |
| 15 | PageSectionMeta, |
| 16 | PageSectionSummary, |
| 17 | PageSectionTitle, |
| 18 | } from 'ui-patterns/PageSection' |
| 19 | |
| 20 | import { AddIntegrationDropdown } from './AddIntegrationDropdown' |
| 21 | import { CreateAuth0IntegrationDialog } from './CreateAuth0Dialog' |
| 22 | import { CreateAwsCognitoAuthIntegrationDialog } from './CreateAwsCognitoAuthDialog' |
| 23 | import { CreateClerkAuthIntegrationDialog } from './CreateClerkAuthDialog' |
| 24 | import { CreateFirebaseAuthIntegrationDialog } from './CreateFirebaseAuthDialog' |
| 25 | import { CreateWorkOSIntegrationDialog } from './CreateWorkOSDialog' |
| 26 | import { IntegrationCard } from './IntegrationCard' |
| 27 | import { |
| 28 | getIntegrationType, |
| 29 | getIntegrationTypeLabel, |
| 30 | INTEGRATION_TYPES, |
| 31 | } from './ThirdPartyAuthForm.utils' |
| 32 | import AlertError from '@/components/ui/AlertError' |
| 33 | import { DocsButton } from '@/components/ui/DocsButton' |
| 34 | import { InlineLink } from '@/components/ui/InlineLink' |
| 35 | import { useDeleteThirdPartyAuthIntegrationMutation } from '@/data/third-party-auth/integration-delete-mutation' |
| 36 | import { |
| 37 | ThirdPartyAuthIntegration, |
| 38 | thirdPartyAuthIntegrationsQueryOptions, |
| 39 | } from '@/data/third-party-auth/integrations-query' |
| 40 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 41 | import { DOCS_URL } from '@/lib/constants' |
| 42 | import { SHORTCUT_IDS } from '@/state/shortcuts/registry' |
| 43 | import { useShortcut } from '@/state/shortcuts/useShortcut' |
| 44 | |
| 45 | export const ThirdPartyAuthForm = () => { |
| 46 | const { ref: projectRef } = useParams() |
| 47 | const { |
| 48 | data: integrationsData, |
| 49 | isPending: isLoading, |
| 50 | isError, |
| 51 | isSuccess, |
| 52 | error, |
| 53 | } = useQuery(thirdPartyAuthIntegrationsQueryOptions({ projectRef })) |
| 54 | const integrations = integrationsData || [] |
| 55 | |
| 56 | const [selectedIntegration, setSelectedIntegration] = useState<INTEGRATION_TYPES>() |
| 57 | const [selectedIntegrationForDeletion, setSelectedIntegrationForDeletion] = |
| 58 | useState<ThirdPartyAuthIntegration>() |
| 59 | const [addIntegrationOpen, setAddIntegrationOpen] = useState(false) |
| 60 | |
| 61 | useShortcut(SHORTCUT_IDS.LIST_PAGE_NEW_ITEM, () => setAddIntegrationOpen(true), { |
| 62 | label: 'Add provider', |
| 63 | }) |
| 64 | |
| 65 | const { mutateAsync: deleteIntegration } = useDeleteThirdPartyAuthIntegrationMutation() |
| 66 | const { can: canUpdateConfig } = useAsyncCheckPermissions( |
| 67 | PermissionAction.UPDATE, |
| 68 | 'custom_config_gotrue' |
| 69 | ) |
| 70 | |
| 71 | if (isError) { |
| 72 | return ( |
| 73 | <AlertError |
| 74 | error={error} |
| 75 | subject="Failed to retrieve auth configuration for Third-Party Auth Integrations" |
| 76 | /> |
| 77 | ) |
| 78 | } |
| 79 | |
| 80 | return ( |
| 81 | <PageSection> |
| 82 | <PageSectionMeta> |
| 83 | <PageSectionSummary> |
| 84 | <PageSectionTitle>Third-Party Auth</PageSectionTitle> |
| 85 | <PageSectionDescription> |
| 86 | Billing is based on the number of monthly active users (MAUs) requesting your API |
| 87 | throughout the billing period.{' '} |
| 88 | <InlineLink |
| 89 | href={`${DOCS_URL}/guides/platform/manage-your-usage/monthly-active-users-third-party`} |
| 90 | > |
| 91 | Learn more |
| 92 | </InlineLink> |
| 93 | </PageSectionDescription> |
| 94 | </PageSectionSummary> |
| 95 | <PageSectionAside> |
| 96 | <DocsButton href={`${DOCS_URL}/guides/auth/third-party/overview`} /> |
| 97 | <AddIntegrationDropdown |
| 98 | open={addIntegrationOpen} |
| 99 | onOpenChange={setAddIntegrationOpen} |
| 100 | onSelectIntegrationType={setSelectedIntegration} |
| 101 | /> |
| 102 | </PageSectionAside> |
| 103 | </PageSectionMeta> |
| 104 | <PageSectionContent> |
| 105 | {isLoading && ( |
| 106 | <div |
| 107 | className={cn( |
| 108 | 'border rounded-sm border-default px-20 py-16 flex flex-col items-center justify-center space-y-4' |
| 109 | )} |
| 110 | > |
| 111 | <Loader2 size={24} className="animate-spin" /> |
| 112 | </div> |
| 113 | )} |
| 114 | |
| 115 | {isSuccess ? ( |
| 116 | integrations.length === 0 ? ( |
| 117 | <EmptyStatePresentational |
| 118 | title="Add an authentication provider" |
| 119 | description="Use third-party authentication systems based on JWTs to access your project." |
| 120 | > |
| 121 | <AddIntegrationDropdown |
| 122 | align="center" |
| 123 | type="default" |
| 124 | onSelectIntegrationType={setSelectedIntegration} |
| 125 | /> |
| 126 | </EmptyStatePresentational> |
| 127 | ) : ( |
| 128 | <div className="-space-y-px"> |
| 129 | {integrations.map((integration) => { |
| 130 | return ( |
| 131 | <IntegrationCard |
| 132 | key={integration.id} |
| 133 | integration={integration} |
| 134 | canUpdateConfig={canUpdateConfig} |
| 135 | onDelete={() => { |
| 136 | setSelectedIntegrationForDeletion(integration) |
| 137 | }} |
| 138 | /> |
| 139 | ) |
| 140 | })} |
| 141 | </div> |
| 142 | ) |
| 143 | ) : null} |
| 144 | |
| 145 | <CreateFirebaseAuthIntegrationDialog |
| 146 | visible={selectedIntegration === 'firebase'} |
| 147 | onDelete={() => {}} |
| 148 | onClose={() => setSelectedIntegration(undefined)} |
| 149 | /> |
| 150 | |
| 151 | <CreateAwsCognitoAuthIntegrationDialog |
| 152 | visible={selectedIntegration === 'awsCognito'} |
| 153 | onDelete={() => {}} |
| 154 | onClose={() => setSelectedIntegration(undefined)} |
| 155 | /> |
| 156 | |
| 157 | <CreateAuth0IntegrationDialog |
| 158 | visible={selectedIntegration === 'auth0'} |
| 159 | onDelete={() => {}} |
| 160 | onClose={() => setSelectedIntegration(undefined)} |
| 161 | /> |
| 162 | |
| 163 | <CreateClerkAuthIntegrationDialog |
| 164 | visible={selectedIntegration === 'clerk'} |
| 165 | onDelete={() => {}} |
| 166 | onClose={() => setSelectedIntegration(undefined)} |
| 167 | /> |
| 168 | |
| 169 | <CreateWorkOSIntegrationDialog |
| 170 | visible={selectedIntegration === 'workos'} |
| 171 | onDelete={() => {}} |
| 172 | onClose={() => setSelectedIntegration(undefined)} |
| 173 | /> |
| 174 | |
| 175 | <ConfirmationModal |
| 176 | size="medium" |
| 177 | visible={!!selectedIntegrationForDeletion} |
| 178 | variant="destructive" |
| 179 | title="Confirm to delete integration" |
| 180 | confirmLabel="Delete" |
| 181 | confirmLabelLoading="Deleting" |
| 182 | onCancel={() => setSelectedIntegrationForDeletion(undefined)} |
| 183 | onConfirm={async () => { |
| 184 | if (!selectedIntegrationForDeletion) { |
| 185 | return |
| 186 | } |
| 187 | const type = getIntegrationType(selectedIntegrationForDeletion) |
| 188 | try { |
| 189 | await deleteIntegration({ |
| 190 | projectRef: projectRef!, |
| 191 | tpaId: selectedIntegrationForDeletion.id, |
| 192 | }) |
| 193 | toast.success(`Successfully deleted ${getIntegrationTypeLabel(type)}.`) |
| 194 | setSelectedIntegrationForDeletion(undefined) |
| 195 | setSelectedIntegration(undefined) |
| 196 | } catch (error) { |
| 197 | toast.error(`Failed to delete ${getIntegrationTypeLabel(type)}.`) |
| 198 | console.error(error) |
| 199 | } |
| 200 | }} |
| 201 | > |
| 202 | <p className="text-sm text-foreground-light"> |
| 203 | Are you sure you want to delete the{' '} |
| 204 | {getIntegrationTypeLabel(getIntegrationType(selectedIntegrationForDeletion))}{' '} |
| 205 | integration? |
| 206 | </p> |
| 207 | </ConfirmationModal> |
| 208 | </PageSectionContent> |
| 209 | </PageSection> |
| 210 | ) |
| 211 | } |