confirm.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import { OAuthScope } from '@supabase/shared-types/out/constants'
  2. import { useParams } from 'common'
  3. import { CheckCircle2, ChevronRight, ChevronsLeftRight } from 'lucide-react'
  4. import Image from 'next/image'
  5. import { useRouter } from 'next/router'
  6. import { toast } from 'sonner'
  7. import { Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
  8. import { Admonition } from 'ui-patterns/admonition'
  9. import { ScopeSection } from '../OAuthApps/AuthorizeRequesterDetails'
  10. import { PERMISSIONS_DESCRIPTIONS } from '../OAuthApps/OAuthApps.constants'
  11. import { ProjectClaimLayout } from './layout'
  12. import { useApiAuthorizationApproveMutation } from '@/data/api-authorization/api-authorization-approve-mutation'
  13. import { ApiAuthorizationResponse } from '@/data/api-authorization/api-authorization-query'
  14. import { useOrganizationProjectClaimMutation } from '@/data/organizations/organization-project-claim-mutation'
  15. import { OrganizationProjectClaimResponse } from '@/data/organizations/organization-project-claim-query'
  16. import { useInvalidateProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query'
  17. import { BASE_PATH } from '@/lib/constants'
  18. import type { Organization } from '@/types'
  19. export const ProjectClaimConfirm = ({
  20. selectedOrganization,
  21. projectClaim,
  22. requester,
  23. setStep,
  24. }: {
  25. selectedOrganization: Organization
  26. projectClaim: OrganizationProjectClaimResponse
  27. requester: ApiAuthorizationResponse
  28. setStep: (step: 'choose-org' | 'benefits' | 'confirm') => void
  29. }) => {
  30. const router = useRouter()
  31. const { auth_id, token: claimToken } = useParams()
  32. const { invalidateProjectsQuery } = useInvalidateProjectsInfiniteQuery()
  33. const { mutateAsync: approveRequest, isPending: isApproving } =
  34. useApiAuthorizationApproveMutation({ onError: () => {} })
  35. const { mutateAsync: claimProject, isPending: isClaiming } = useOrganizationProjectClaimMutation()
  36. const onClaimProject = async () => {
  37. try {
  38. const response = await approveRequest({ id: auth_id!, slug: selectedOrganization.slug })
  39. await claimProject({
  40. slug: selectedOrganization.slug,
  41. token: claimToken!,
  42. })
  43. toast.success('Project claimed successfully')
  44. try {
  45. // check if the redirect url is valid. If not, redirect the user to the org dashboard
  46. const url = new URL(response.url)
  47. window.location.href = url.toString()
  48. } catch {
  49. // invalidate the org projects to force them to be refetched
  50. await invalidateProjectsQuery()
  51. router.push(`/org/${selectedOrganization.slug}`)
  52. }
  53. } catch (error: any) {
  54. toast.error(`Failed to claim project ${error.message}`)
  55. }
  56. }
  57. const isLoading = isApproving || isClaiming
  58. return (
  59. <ProjectClaimLayout
  60. title={
  61. <>
  62. Claim a project <span className="text-brand">{projectClaim?.project?.name}</span> from{' '}
  63. <span className="text-brand">{requester?.name}</span>
  64. </>
  65. }
  66. >
  67. <div className="py-6 space-y-8 text-sm">
  68. <div className="flex flex-col items-center mt-6">
  69. <div className="flex items-center">
  70. <div
  71. className={cn(
  72. 'w-8 h-8 bg-center bg-no-repeat bg-cover flex items-center justify-center rounded-md'
  73. )}
  74. style={{
  75. backgroundImage: !!requester.icon ? `url('${requester.icon}')` : 'none',
  76. }}
  77. >
  78. {!requester.icon && (
  79. <p className="text-foreground-light text-lg">{requester.name[0]}</p>
  80. )}
  81. </div>
  82. <div className="flex items-center justify-center w-28 relative">
  83. <div className="h-0.5 w-full border-2 border-dashed border-stronger" />
  84. <div className="rounded-full border flex items-center justify-center h-10 w-full shadow-xs">
  85. <ChevronsLeftRight className="text-muted-foreground" size={24} />
  86. </div>
  87. <div className="h-0.5 w-full border-2 border-dashed border-stronger z-10" />
  88. </div>
  89. <div className="w-8 h-8">
  90. <Image
  91. src={`${BASE_PATH}/img/briven-logo.svg`}
  92. alt="Briven Logo"
  93. className="w-full h-full"
  94. width={100}
  95. height={100}
  96. />
  97. </div>
  98. </div>
  99. </div>
  100. <div className="space-y-4 text-foreground-light">
  101. <p>
  102. By claiming the <span className="text-foreground">{projectClaim?.project?.name}</span>{' '}
  103. project from <span className="text-foreground">{requester?.name}</span>, the following
  104. will happen:
  105. </p>
  106. <ul className="space-y-3">
  107. <li className="flex space-x-2">
  108. <span>
  109. <CheckCircle2 className="text-brand h-5 w-5" />
  110. </span>
  111. <span>
  112. The project will be transferred to your Briven organization{' '}
  113. <span className="text-foreground">{selectedOrganization.name}.</span>{' '}
  114. <a
  115. href="#"
  116. onClick={() => setStep('choose-org')}
  117. className="text-foreground-light underline"
  118. >
  119. Choose another organization?
  120. </a>
  121. </span>
  122. </li>
  123. <li className="flex space-x-2">
  124. <span>
  125. <CheckCircle2 className="text-brand h-5 w-5" />
  126. </span>
  127. <span>
  128. <span className="text-foreground">{requester?.name}</span> will receive API access
  129. (permissions listed below) to all projects within your organization to continue
  130. providing its functionality to the application you've built.
  131. </span>
  132. </li>
  133. <li className="flex space-x-2">
  134. <span>
  135. <CheckCircle2 className="text-brand h-5 w-5" />
  136. </span>
  137. <span>
  138. You'll be responsible for maintaining the project, which may include additional
  139. costs.
  140. </span>
  141. </li>
  142. </ul>
  143. <Admonition type="caution">
  144. <div className="text-foreground-light">
  145. Upon claiming, the project may undergo a short downtime (less than 10 minutes) for
  146. resizing.
  147. </div>
  148. </Admonition>
  149. </div>
  150. <div className="flex space-y-4 flex-col">
  151. {requester.scopes.length === 0 ? (
  152. <span className="text-foreground-light">
  153. <span className="text-foreground">{requester?.name}</span> hasn't requested any
  154. permissions to operate. This is normal and no action is needed from your side.
  155. </span>
  156. ) : (
  157. <Collapsible>
  158. <CollapsibleTrigger className="pb-3 w-full flex items-center justify-between group">
  159. <p className="text-sm text-foreground-light text-left">
  160. <span className="font-foreground">List of permissions</span> that{' '}
  161. <span className="text-foreground">{requester.name}</span> will have for the{' '}
  162. <span className="text-amber-900">
  163. selected organization and all of its projects.
  164. </span>
  165. </p>
  166. <ChevronRight
  167. size={16}
  168. className="text-foreground-light transition-all group-data-open:rotate-90 w-20"
  169. strokeWidth={1}
  170. />
  171. </CollapsibleTrigger>
  172. <CollapsibleContent
  173. className={cn(
  174. 'flex flex-col gap-8 transition-all',
  175. 'data-closed:animate-collapsible-up data-open:animate-collapsible-down'
  176. )}
  177. >
  178. <div>
  179. <ScopeSection
  180. description={PERMISSIONS_DESCRIPTIONS.ANALYTICS}
  181. hasReadScope={requester.scopes.includes(OAuthScope.ANALYTICS_READ)}
  182. hasWriteScope={requester.scopes.includes(OAuthScope.ANALYTICS_WRITE)}
  183. />
  184. <ScopeSection
  185. description={PERMISSIONS_DESCRIPTIONS.ANALYTICS_CONFIG}
  186. hasReadScope={requester.scopes.includes(OAuthScope.ANALYTICS_CONFIG_READ)}
  187. hasWriteScope={requester.scopes.includes(OAuthScope.ANALYTICS_CONFIG_WRITE)}
  188. />
  189. <ScopeSection
  190. description={PERMISSIONS_DESCRIPTIONS.AUTH}
  191. hasReadScope={requester.scopes.includes(OAuthScope.AUTH_READ)}
  192. hasWriteScope={requester.scopes.includes(OAuthScope.AUTH_WRITE)}
  193. />
  194. <ScopeSection
  195. description={PERMISSIONS_DESCRIPTIONS.DATABASE}
  196. hasReadScope={requester.scopes.includes(OAuthScope.DATABASE_READ)}
  197. hasWriteScope={requester.scopes.includes(OAuthScope.DATABASE_WRITE)}
  198. />
  199. <ScopeSection
  200. description={PERMISSIONS_DESCRIPTIONS.DOMAINS}
  201. hasReadScope={requester.scopes.includes(OAuthScope.DOMAINS_READ)}
  202. hasWriteScope={requester.scopes.includes(OAuthScope.DOMAINS_WRITE)}
  203. />
  204. <ScopeSection
  205. description={PERMISSIONS_DESCRIPTIONS.EDGE_FUNCTIONS}
  206. hasReadScope={requester.scopes.includes(OAuthScope.EDGE_FUNCTIONS_READ)}
  207. hasWriteScope={requester.scopes.includes(OAuthScope.EDGE_FUNCTIONS_WRITE)}
  208. />
  209. <ScopeSection
  210. description={PERMISSIONS_DESCRIPTIONS.ENVIRONMENT}
  211. hasReadScope={requester.scopes.includes(OAuthScope.ENVIRONMENT_READ)}
  212. hasWriteScope={requester.scopes.includes(OAuthScope.ENVIRONMENT_WRITE)}
  213. />
  214. <ScopeSection
  215. description={PERMISSIONS_DESCRIPTIONS.ORGANIZATIONS}
  216. hasReadScope={requester.scopes.includes(OAuthScope.ORGANIZATIONS_READ)}
  217. hasWriteScope={requester.scopes.includes(OAuthScope.ORGANIZATIONS_WRITE)}
  218. />
  219. <ScopeSection
  220. description={PERMISSIONS_DESCRIPTIONS.PROJECTS}
  221. hasReadScope={requester.scopes.includes(OAuthScope.PROJECTS_READ)}
  222. hasWriteScope={requester.scopes.includes(OAuthScope.PROJECTS_WRITE)}
  223. />
  224. <ScopeSection
  225. description={PERMISSIONS_DESCRIPTIONS.REST}
  226. hasReadScope={requester.scopes.includes(OAuthScope.REST_READ)}
  227. hasWriteScope={requester.scopes.includes(OAuthScope.REST_WRITE)}
  228. />
  229. <ScopeSection
  230. description={PERMISSIONS_DESCRIPTIONS.SECRETS}
  231. hasReadScope={requester.scopes.includes(OAuthScope.SECRETS_READ)}
  232. hasWriteScope={requester.scopes.includes(OAuthScope.SECRETS_WRITE)}
  233. />
  234. <ScopeSection
  235. description={PERMISSIONS_DESCRIPTIONS.STORAGE}
  236. hasReadScope={requester.scopes.includes(OAuthScope.STORAGE_READ)}
  237. hasWriteScope={requester.scopes.includes(OAuthScope.STORAGE_WRITE)}
  238. />
  239. </div>
  240. </CollapsibleContent>
  241. </Collapsible>
  242. )}
  243. </div>
  244. </div>
  245. <div className="flex justify-center sticky bottom-0">
  246. <Button size="medium" loading={isLoading} disabled={isLoading} onClick={onClaimProject}>
  247. Claim project {projectClaim?.project?.name}
  248. </Button>
  249. </div>
  250. </ProjectClaimLayout>
  251. )
  252. }