index.tsx211 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useQuery } from '@tanstack/react-query'
3import { useParams } from 'common'
4import { Loader2 } from 'lucide-react'
5import { useState } from 'react'
6import { toast } from 'sonner'
7import { cn } from 'ui'
8import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
9import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
10import {
11 PageSection,
12 PageSectionAside,
13 PageSectionContent,
14 PageSectionDescription,
15 PageSectionMeta,
16 PageSectionSummary,
17 PageSectionTitle,
18} from 'ui-patterns/PageSection'
19
20import { AddIntegrationDropdown } from './AddIntegrationDropdown'
21import { CreateAuth0IntegrationDialog } from './CreateAuth0Dialog'
22import { CreateAwsCognitoAuthIntegrationDialog } from './CreateAwsCognitoAuthDialog'
23import { CreateClerkAuthIntegrationDialog } from './CreateClerkAuthDialog'
24import { CreateFirebaseAuthIntegrationDialog } from './CreateFirebaseAuthDialog'
25import { CreateWorkOSIntegrationDialog } from './CreateWorkOSDialog'
26import { IntegrationCard } from './IntegrationCard'
27import {
28 getIntegrationType,
29 getIntegrationTypeLabel,
30 INTEGRATION_TYPES,
31} from './ThirdPartyAuthForm.utils'
32import AlertError from '@/components/ui/AlertError'
33import { DocsButton } from '@/components/ui/DocsButton'
34import { InlineLink } from '@/components/ui/InlineLink'
35import { useDeleteThirdPartyAuthIntegrationMutation } from '@/data/third-party-auth/integration-delete-mutation'
36import {
37 ThirdPartyAuthIntegration,
38 thirdPartyAuthIntegrationsQueryOptions,
39} from '@/data/third-party-auth/integrations-query'
40import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
41import { DOCS_URL } from '@/lib/constants'
42import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
43import { useShortcut } from '@/state/shortcuts/useShortcut'
44
45export 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}