confirm.tsx264 lines · main
| 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 | |
| 10 | import { ScopeSection } from '../OAuthApps/AuthorizeRequesterDetails' |
| 11 | import { PERMISSIONS_DESCRIPTIONS } from '../OAuthApps/OAuthApps.constants' |
| 12 | import { ProjectClaimLayout } from './layout' |
| 13 | import { useApiAuthorizationApproveMutation } from '@/data/api-authorization/api-authorization-approve-mutation' |
| 14 | import { ApiAuthorizationResponse } from '@/data/api-authorization/api-authorization-query' |
| 15 | import { useOrganizationProjectClaimMutation } from '@/data/organizations/organization-project-claim-mutation' |
| 16 | import { OrganizationProjectClaimResponse } from '@/data/organizations/organization-project-claim-query' |
| 17 | import { useInvalidateProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query' |
| 18 | import { BASE_PATH } from '@/lib/constants' |
| 19 | import type { Organization } from '@/types' |
| 20 | |
| 21 | export 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 | } |