index.tsx 7.5 KB

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