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