From 1e3dea91aa04f9b95adb9085ff941f2e795f7834 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 25 Apr 2023 17:01:26 +0800 Subject: [PATCH 1/7] Init API authorization page and scaffold RQ for API auth requests --- .../layouts/APIAuthorizationLayout.tsx | 49 ++++++++++ .../api-authorization-approve-mutation.ts | 43 +++++++++ .../api-authorization-decline-mutation.ts | 40 ++++++++ .../api-authorization-query.ts | 52 ++++++++++ studio/data/api-authorization/keys.ts | 3 + studio/data/organizations/keys.ts | 1 + .../data/organizations/organizations-query.ts | 37 +++++++ studio/pages/authorize.tsx | 96 +++++++++++++++++++ 8 files changed, 321 insertions(+) create mode 100644 studio/components/layouts/APIAuthorizationLayout.tsx create mode 100644 studio/data/api-authorization/api-authorization-approve-mutation.ts create mode 100644 studio/data/api-authorization/api-authorization-decline-mutation.ts create mode 100644 studio/data/api-authorization/api-authorization-query.ts create mode 100644 studio/data/api-authorization/keys.ts create mode 100644 studio/data/organizations/organizations-query.ts create mode 100644 studio/pages/authorize.tsx diff --git a/studio/components/layouts/APIAuthorizationLayout.tsx b/studio/components/layouts/APIAuthorizationLayout.tsx new file mode 100644 index 00000000000..f43656a4303 --- /dev/null +++ b/studio/components/layouts/APIAuthorizationLayout.tsx @@ -0,0 +1,49 @@ +import Head from 'next/head' +import Image from 'next/image' +import { useTheme } from 'common' +import { PropsWithChildren } from 'react' +import { BASE_PATH } from 'lib/constants' +import Divider from 'components/ui/Divider' + +export interface APIAuthorizationLayoutProps {} + +const APIAuthorizationLayout = ({ children }: PropsWithChildren) => { + const { isDarkMode } = useTheme() + return ( + <> + + Authorize API access | Supabase + +
+
+
+
+
+
+ Supabase + Supabase Logo +
+
+
+
+
+ +
+

Authorize API Access

+ {children} +
+
+ + ) +} + +export default APIAuthorizationLayout diff --git a/studio/data/api-authorization/api-authorization-approve-mutation.ts b/studio/data/api-authorization/api-authorization-approve-mutation.ts new file mode 100644 index 00000000000..5654041d540 --- /dev/null +++ b/studio/data/api-authorization/api-authorization-approve-mutation.ts @@ -0,0 +1,43 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { post } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +// import { resourceKeys } from './keys' + +export type ApiAuthorizationApproveVariables = { + id: string +} + +export async function approveApiAuthorization({ id }: ApiAuthorizationApproveVariables) { + if (!id) throw new Error('Authorization ID is required') + + const response = await post( + `${API_ADMIN_URL}/oauth/authorization/${id}?skip_browser_redirect=true`, + {} + ) + if (response.error) throw response.error + return response +} + +type ApiAuthorizationApproveData = Awaited> + +export const useApiAuthorizationApproveMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => approveApiAuthorization(vars), + { + async onSuccess(data, variables, context) { + // const { id } = variables + // await queryClient.invalidateQueries(networkRestrictionKeys.list(projectRef)) + // await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/api-authorization/api-authorization-decline-mutation.ts b/studio/data/api-authorization/api-authorization-decline-mutation.ts new file mode 100644 index 00000000000..f623f7d2905 --- /dev/null +++ b/studio/data/api-authorization/api-authorization-decline-mutation.ts @@ -0,0 +1,40 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { delete_ } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +// import { resourceKeys } from './keys' + +export type ApiAuthorizationDeclineVariables = { + id: string +} + +export async function declineApiAuthorization({ id }: ApiAuthorizationDeclineVariables) { + if (!id) throw new Error('Authorization ID is required') + + const response = await delete_(`${API_ADMIN_URL}/oauth/authorization/${id}`, {}) + if (response.error) throw response.error + return response +} + +type ApiAuthorizationDeclineData = Awaited> + +export const useApiAuthorizationDeclineMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => declineApiAuthorization(vars), + { + async onSuccess(data, variables, context) { + // const { id } = variables + // await queryClient.invalidateQueries(networkRestrictionKeys.list(projectRef)) + // await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/api-authorization/api-authorization-query.ts b/studio/data/api-authorization/api-authorization-query.ts new file mode 100644 index 00000000000..f687e49c403 --- /dev/null +++ b/studio/data/api-authorization/api-authorization-query.ts @@ -0,0 +1,52 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +import { useCallback } from 'react' +import { resourceKeys } from './keys' + +export type ApiAuthorizationVariables = { + id?: string +} + +export type ApiAuthorizationResponse = { + id: string +} + +export async function getApiAuthorizationDetails( + { id }: ApiAuthorizationVariables, + signal?: AbortSignal +) { + if (!id) throw new Error('Authorization ID is required') + + const response = await get(`${API_ADMIN_URL}/oauth/authorization/${id}`, { signal }) + if (response.error) throw response.error + return response as ApiAuthorizationResponse +} + +export type ResourceData = Awaited> +export type ResourceError = unknown + +export const useApiAuthorizationQuery = ( + { id }: ApiAuthorizationVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + resourceKeys.resource(id), + ({ signal }) => getApiAuthorizationDetails({ id }, signal), + { + enabled: enabled && typeof id !== 'undefined', + ...options, + } + ) + +export const useApiAuthorizationPrefetch = ({ id }: ApiAuthorizationVariables) => { + const client = useQueryClient() + + return useCallback(() => { + if (id) { + client.prefetchQuery(resourceKeys.resource(id), ({ signal }) => + getApiAuthorizationDetails({ id }, signal) + ) + } + }, [id]) +} diff --git a/studio/data/api-authorization/keys.ts b/studio/data/api-authorization/keys.ts new file mode 100644 index 00000000000..69660c24f42 --- /dev/null +++ b/studio/data/api-authorization/keys.ts @@ -0,0 +1,3 @@ +export const resourceKeys = { + resource: (id: string | undefined) => ['api-authorization', id] as const, +} diff --git a/studio/data/organizations/keys.ts b/studio/data/organizations/keys.ts index 5b7250c028d..4aabe753ab3 100644 --- a/studio/data/organizations/keys.ts +++ b/studio/data/organizations/keys.ts @@ -1,4 +1,5 @@ export const organizationKeys = { + list: () => ['organizations'] as const, detail: (slug: string | undefined) => ['organizations', slug, 'detail'] as const, roles: (slug: string | undefined) => ['organizations', slug, 'roles'] as const, freeProjectLimitCheck: (slug: string | undefined) => diff --git a/studio/data/organizations/organizations-query.ts b/studio/data/organizations/organizations-query.ts new file mode 100644 index 00000000000..598c8b17df4 --- /dev/null +++ b/studio/data/organizations/organizations-query.ts @@ -0,0 +1,37 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { useCallback } from 'react' +import { Organization } from 'types' +import { organizationKeys } from './keys' + +export type OrganizationsResponse = Organization[] + +export async function getOrganizations(signal?: AbortSignal) { + const data = await get(`${API_URL}/organizations`, { signal }) + if (data.error) throw data.error + + return data as OrganizationsResponse +} + +export type OrganizationsData = Awaited> +export type OrganizationsError = unknown + +export const useOrganizationsQuery = ({ + enabled = true, + ...options +}: UseQueryOptions = {}) => + useQuery( + organizationKeys.list(), + ({ signal }) => getOrganizations(signal), + { enabled: enabled, ...options } + ) + +export const useOrganizationsPrefetch = () => { + const client = useQueryClient() + + return useCallback( + () => client.prefetchQuery(organizationKeys.list(), ({ signal }) => getOrganizations(signal)), + [] + ) +} diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx new file mode 100644 index 00000000000..9d4e3aed96f --- /dev/null +++ b/studio/pages/authorize.tsx @@ -0,0 +1,96 @@ +import { useParams } from 'common' + +import { withAuth } from 'hooks' +import { NextPageWithLayout } from 'types' +import { FormPanel } from 'components/ui/Forms' +import APIAuthorizationLayout from 'components/layouts/APIAuthorizationLayout' +import { Alert, Button, Listbox } from 'ui' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useApiAuthorizationQuery } from 'data/api-authorization/api-authorization-query' + +const APIAuthorizationPage: NextPageWithLayout = () => { + const { auth_id } = useParams() + const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery() + // const { data: apiAuthDetails, isLoading, isSuccess, isError } = useApiAuthorizationQuery({ id: auth_id }) + + // [Joshen] To be replaced with actual API call above + const apiAuthDetails = { + icon: 'https://cdn-icons-png.flaticon.com/512/5969/5969044.png', + host: 'cloudflare.com', + name: 'Cloudflare', + } + + const isExpired = false + + return ( + +
+ + +
+ + } + > +
+ {/* API Authorization requester details */} +
+

Authorize {apiAuthDetails.name}

+
+
+
+
+
+
+

+ {apiAuthDetails.name} is requesting API access to an organization. The application + will be able to{' '} + + read and write the organization's settings and projects + +

+
+
+ + {/* Expiry warning */} + {isExpired && ( + + Please retry your authorization request from the requesting app + + )} + + {/* Organization selection */} + {isLoadingOrganizations ? ( +
+ + +
+ ) : ( + + {(organizations ?? []).map((organization) => ( + + {organization.name} + + ))} + + )} +
+
+ ) +} + +APIAuthorizationPage.getLayout = (page) => {page} +export default withAuth(APIAuthorizationPage) From f65eb032c452f7b007ad0d6c6d5d98314e74d118 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 25 Apr 2023 17:06:58 +0800 Subject: [PATCH 2/7] Small UI change --- .../layouts/APIAuthorizationLayout.tsx | 1 - studio/pages/authorize.tsx | 33 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/studio/components/layouts/APIAuthorizationLayout.tsx b/studio/components/layouts/APIAuthorizationLayout.tsx index f43656a4303..143dd4a6526 100644 --- a/studio/components/layouts/APIAuthorizationLayout.tsx +++ b/studio/components/layouts/APIAuthorizationLayout.tsx @@ -38,7 +38,6 @@ const APIAuthorizationLayout = ({ children }: PropsWithChildren
-

Authorize API Access

{children}
diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx index 9d4e3aed96f..c41f134f31f 100644 --- a/studio/pages/authorize.tsx +++ b/studio/pages/authorize.tsx @@ -25,6 +25,7 @@ const APIAuthorizationPage: NextPageWithLayout = () => { return ( Authorize API access for {apiAuthDetails.name}

} footer={
@@ -40,25 +41,23 @@ const APIAuthorizationPage: NextPageWithLayout = () => { >
{/* API Authorization requester details */} -
-

Authorize {apiAuthDetails.name}

-
-
-
-
-
+ +
+
+
+
-

- {apiAuthDetails.name} is requesting API access to an organization. The application - will be able to{' '} - - read and write the organization's settings and projects - -

+

+ {apiAuthDetails.name} is requesting API access to an organization. The application will + be able to{' '} + + read and write the organization's settings and projects + +

{/* Expiry warning */} From 63267c1fa9fe41d5c30aeec2ccccc9b624d9cdbc Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 25 Apr 2023 17:26:41 +0800 Subject: [PATCH 3/7] Scaffold error states to be handled --- studio/pages/authorize.tsx | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx index c41f134f31f..6161eca20ec 100644 --- a/studio/pages/authorize.tsx +++ b/studio/pages/authorize.tsx @@ -21,8 +21,38 @@ const APIAuthorizationPage: NextPageWithLayout = () => { name: 'Cloudflare', } + // [Joshen] To be replaced with actual API call above + const isApiAuthDetailsError = false const isExpired = false + if (auth_id === undefined) { + return ( + Authorization for API access

}> +
+ + Please provide a valid authorization ID in the URL + +
+
+ ) + } + + if (isApiAuthDetailsError) { + return ( + Authorization for API access

}> +
+ + Please retry your authorization request from the requesting app + +
+
+ ) + } + return ( Authorize API access for {apiAuthDetails.name}

} From ac10f883b6c483a7f406dcc8df5cd7bfc4460827 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 5 May 2023 17:44:31 +0800 Subject: [PATCH 4/7] Hook up endpoints --- .../api-authorization-approve-mutation.ts | 31 ++-- .../api-authorization-decline-mutation.ts | 22 +-- .../api-authorization-query.ts | 28 ++- studio/pages/authorize.tsx | 170 ++++++++++++++---- 4 files changed, 186 insertions(+), 65 deletions(-) diff --git a/studio/data/api-authorization/api-authorization-approve-mutation.ts b/studio/data/api-authorization/api-authorization-approve-mutation.ts index 5654041d540..5ac01111adc 100644 --- a/studio/data/api-authorization/api-authorization-approve-mutation.ts +++ b/studio/data/api-authorization/api-authorization-approve-mutation.ts @@ -1,21 +1,29 @@ -import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { post } from 'lib/common/fetch' import { API_ADMIN_URL } from 'lib/constants' -// import { resourceKeys } from './keys' export type ApiAuthorizationApproveVariables = { id: string + organization_id: string } -export async function approveApiAuthorization({ id }: ApiAuthorizationApproveVariables) { +export type ApiAuthorizationApproveResponse = { + url: string +} + +export async function approveApiAuthorization({ + id, + organization_id, +}: ApiAuthorizationApproveVariables) { if (!id) throw new Error('Authorization ID is required') + if (!organization_id) throw new Error('Organization slug is required') const response = await post( - `${API_ADMIN_URL}/oauth/authorization/${id}?skip_browser_redirect=true`, - {} + `${API_ADMIN_URL}/oauth/authorizations/${id}?skip_browser_redirect=true`, + { organization_id } ) if (response.error) throw response.error - return response + return response as ApiAuthorizationApproveResponse } type ApiAuthorizationApproveData = Awaited> @@ -27,17 +35,8 @@ export const useApiAuthorizationApproveMutation = ({ UseMutationOptions, 'mutationFn' > = {}) => { - const queryClient = useQueryClient() - return useMutation( (vars) => approveApiAuthorization(vars), - { - async onSuccess(data, variables, context) { - // const { id } = variables - // await queryClient.invalidateQueries(networkRestrictionKeys.list(projectRef)) - // await onSuccess?.(data, variables, context) - }, - ...options, - } + options ) } diff --git a/studio/data/api-authorization/api-authorization-decline-mutation.ts b/studio/data/api-authorization/api-authorization-decline-mutation.ts index f623f7d2905..7825bb01688 100644 --- a/studio/data/api-authorization/api-authorization-decline-mutation.ts +++ b/studio/data/api-authorization/api-authorization-decline-mutation.ts @@ -1,18 +1,21 @@ -import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { delete_ } from 'lib/common/fetch' import { API_ADMIN_URL } from 'lib/constants' -// import { resourceKeys } from './keys' export type ApiAuthorizationDeclineVariables = { id: string } +export type ApiAuthorizationDeclineResponse = { + id: string +} + export async function declineApiAuthorization({ id }: ApiAuthorizationDeclineVariables) { if (!id) throw new Error('Authorization ID is required') - const response = await delete_(`${API_ADMIN_URL}/oauth/authorization/${id}`, {}) + const response = await delete_(`${API_ADMIN_URL}/oauth/authorizations/${id}`, {}) if (response.error) throw response.error - return response + return response as ApiAuthorizationDeclineResponse } type ApiAuthorizationDeclineData = Awaited> @@ -24,17 +27,8 @@ export const useApiAuthorizationDeclineMutation = ({ UseMutationOptions, 'mutationFn' > = {}) => { - const queryClient = useQueryClient() - return useMutation( (vars) => declineApiAuthorization(vars), - { - async onSuccess(data, variables, context) { - // const { id } = variables - // await queryClient.invalidateQueries(networkRestrictionKeys.list(projectRef)) - // await onSuccess?.(data, variables, context) - }, - ...options, - } + options ) } diff --git a/studio/data/api-authorization/api-authorization-query.ts b/studio/data/api-authorization/api-authorization-query.ts index f687e49c403..bf73ad67872 100644 --- a/studio/data/api-authorization/api-authorization-query.ts +++ b/studio/data/api-authorization/api-authorization-query.ts @@ -9,7 +9,12 @@ export type ApiAuthorizationVariables = { } export type ApiAuthorizationResponse = { - id: string + name: string + website: string + domain: string + expires_at: string + approved_at: string | null + approved_organization_slug?: string } export async function getApiAuthorizationDetails( @@ -18,8 +23,25 @@ export async function getApiAuthorizationDetails( ) { if (!id) throw new Error('Authorization ID is required') - const response = await get(`${API_ADMIN_URL}/oauth/authorization/${id}`, { signal }) - if (response.error) throw response.error + const response = await get(`${API_ADMIN_URL}/oauth/authorizations/${id}`, { signal }) + if (response.error) { + // 404 is a valid error in which the auth id is invalid + const isInvalid = + (response.error as any)?.code === 404 && + (response.error as any)?.message?.includes('OAuth authorization request does not exist') + + if (isInvalid) { + return { + name: '', + website: '', + domain: '', + expires_at: '', + approved_at: null, + } as ApiAuthorizationResponse + } + + throw response.error + } return response as ApiAuthorizationResponse } diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx index 6161eca20ec..5ea6e61ae0a 100644 --- a/studio/pages/authorize.tsx +++ b/studio/pages/authorize.tsx @@ -1,29 +1,101 @@ import { useParams } from 'common' - -import { withAuth } from 'hooks' -import { NextPageWithLayout } from 'types' -import { FormPanel } from 'components/ui/Forms' import APIAuthorizationLayout from 'components/layouts/APIAuthorizationLayout' -import { Alert, Button, Listbox } from 'ui' -import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import { FormPanel } from 'components/ui/Forms' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useApiAuthorizationApproveMutation } from 'data/api-authorization/api-authorization-approve-mutation' +import { useApiAuthorizationDeclineMutation } from 'data/api-authorization/api-authorization-decline-mutation' import { useApiAuthorizationQuery } from 'data/api-authorization/api-authorization-query' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import dayjs from 'dayjs' +import { useStore, withAuth } from 'hooks' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { NextPageWithLayout } from 'types' +import { Alert, Button, Listbox } from 'ui' + +// Need to handle if no organizations in account +// Need to handle if not logged in yet state const APIAuthorizationPage: NextPageWithLayout = () => { + const { ui } = useStore() + const router = useRouter() const { auth_id } = useParams() - const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery() - // const { data: apiAuthDetails, isLoading, isSuccess, isError } = useApiAuthorizationQuery({ id: auth_id }) + const [isSubmitting, setIsSubmitting] = useState(false) + const [selectedOrg, setSelectedOrg] = useState() - // [Joshen] To be replaced with actual API call above - const apiAuthDetails = { - icon: 'https://cdn-icons-png.flaticon.com/512/5969/5969044.png', - host: 'cloudflare.com', - name: 'Cloudflare', + const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery() + const { data: requester, isLoading, isError } = useApiAuthorizationQuery({ id: auth_id }) + const isApproved = requester?.approved_at !== null + const isInvalid = requester?.name.length === 0 && requester.expires_at.length === 0 + const isExpired = dayjs().isAfter(dayjs(requester?.expires_at)) + + const { mutateAsync: approveRequest } = useApiAuthorizationApproveMutation() + const { mutateAsync: declineRequest } = useApiAuthorizationDeclineMutation() + + useEffect(() => { + if (!isLoadingOrganizations) { + setSelectedOrg(organizations?.[0].slug ?? undefined) + } + }, [isLoadingOrganizations]) + + const onApproveRequest = async () => { + if (!auth_id) { + return ui.setNotification({ + category: 'error', + message: 'Unable to approve request: auth_id is missing ', + }) + } + if (!selectedOrg) { + return ui.setNotification({ + category: 'error', + message: 'Unable to approve request: No organization selected', + }) + } + + try { + setIsSubmitting(true) + const res = await approveRequest({ id: auth_id, organization_id: selectedOrg }) + router.push(res.url) + } catch (error: any) { + ui.setNotification({ + category: 'error', + message: `Failed to approve request: ${error.message}`, + }) + setIsSubmitting(false) + } } - // [Joshen] To be replaced with actual API call above - const isApiAuthDetailsError = false - const isExpired = false + const onDeclineRequest = async () => { + if (!auth_id) + return ui.setNotification({ + category: 'error', + message: 'Unable to decline request: auth_id is missing ', + }) + + try { + setIsSubmitting(true) + await declineRequest({ id: auth_id }) + router.push('/projects') + } catch (error: any) { + ui.setNotification({ + category: 'error', + message: `Failed to decline request: ${error.message}`, + }) + setIsSubmitting(false) + } + } + + if (isLoading) { + return ( + Authorize API access

}> +
+ + + +
+
+ ) + } if (auth_id === undefined) { return ( @@ -37,9 +109,9 @@ const APIAuthorizationPage: NextPageWithLayout = () => { ) } - if (isApiAuthDetailsError) { + if (isInvalid || isError) { return ( - Authorization for API access

}> + Authorize API access

}>
{ ) } + if (isApproved) { + const approvedOrganization = organizations?.find( + (org) => org.slug === requester.approved_organization_slug + ) + + return ( + Authorize API access for {requester?.name}

}> +
+ +

+ {requester.name} has read and write access to the organization " + {approvedOrganization?.name ?? 'Unknown'}" and all of its projects +

+

+ Approved on: {dayjs(requester.approved_at).format('DD MMM YYYY HH:mm:ss (ZZ)')} +

+
+
+
+ ) + } + return ( Authorize API access for {apiAuthDetails.name}

} + header={

Authorize API access for {requester?.name}

} footer={
- -
@@ -76,23 +174,26 @@ const APIAuthorizationPage: NextPageWithLayout = () => {
+ className="w-8 h-8 md:w-10 md:h-10 bg-center bg-no-repeat bg-cover flex items-center justify-center" + // [Joshen] For when we support icons + // style={{ backgroundImage: `url('${requester?.icon}')` }} + > +

{requester?.name[0]}

+

- {apiAuthDetails.name} is requesting API access to an organization. The application will - be able to{' '} - - read and write the organization's settings and projects + {requester?.name} ({requester?.domain}) is requesting API access to an organization. The + application will be able to{' '} + + read and write the organization's settings and all of its projects.

{/* Expiry warning */} {isExpired && ( - + Please retry your authorization request from the requesting app )} @@ -104,12 +205,17 @@ const APIAuthorizationPage: NextPageWithLayout = () => {
) : ( - + {(organizations ?? []).map((organization) => ( {organization.name} From 6c8952711d4e9456f01dbbc83aa6cc4d1ead178a Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Sat, 6 May 2023 15:53:27 +0800 Subject: [PATCH 5/7] Small improvements --- .../api-authorization-query.ts | 21 ++----------------- studio/pages/authorize.tsx | 9 ++++---- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/studio/data/api-authorization/api-authorization-query.ts b/studio/data/api-authorization/api-authorization-query.ts index bf73ad67872..652c19c4d5f 100644 --- a/studio/data/api-authorization/api-authorization-query.ts +++ b/studio/data/api-authorization/api-authorization-query.ts @@ -24,29 +24,12 @@ export async function getApiAuthorizationDetails( if (!id) throw new Error('Authorization ID is required') const response = await get(`${API_ADMIN_URL}/oauth/authorizations/${id}`, { signal }) - if (response.error) { - // 404 is a valid error in which the auth id is invalid - const isInvalid = - (response.error as any)?.code === 404 && - (response.error as any)?.message?.includes('OAuth authorization request does not exist') - - if (isInvalid) { - return { - name: '', - website: '', - domain: '', - expires_at: '', - approved_at: null, - } as ApiAuthorizationResponse - } - - throw response.error - } + if (response.error) throw response.error return response as ApiAuthorizationResponse } export type ResourceData = Awaited> -export type ResourceError = unknown +export type ResourceError = { errorEventId: string; message: string } export const useApiAuthorizationQuery = ( { id }: ApiAuthorizationVariables, diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx index 5ea6e61ae0a..86559bebe6e 100644 --- a/studio/pages/authorize.tsx +++ b/studio/pages/authorize.tsx @@ -24,9 +24,8 @@ const APIAuthorizationPage: NextPageWithLayout = () => { const [selectedOrg, setSelectedOrg] = useState() const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery() - const { data: requester, isLoading, isError } = useApiAuthorizationQuery({ id: auth_id }) + const { data: requester, isLoading, isError, error } = useApiAuthorizationQuery({ id: auth_id }) const isApproved = requester?.approved_at !== null - const isInvalid = requester?.name.length === 0 && requester.expires_at.length === 0 const isExpired = dayjs().isAfter(dayjs(requester?.expires_at)) const { mutateAsync: approveRequest } = useApiAuthorizationApproveMutation() @@ -75,6 +74,7 @@ const APIAuthorizationPage: NextPageWithLayout = () => { try { setIsSubmitting(true) await declineRequest({ id: auth_id }) + ui.setNotification({ category: 'success', message: 'Declined API authorization request' }) router.push('/projects') } catch (error: any) { ui.setNotification({ @@ -109,7 +109,7 @@ const APIAuthorizationPage: NextPageWithLayout = () => { ) } - if (isInvalid || isError) { + if (isError) { return ( Authorize API access

}>
@@ -118,7 +118,8 @@ const APIAuthorizationPage: NextPageWithLayout = () => { variant="warning" title="Failed to fetch details for API authorization request" > - Please retry your authorization request from the requesting app +

Please retry your authorization request from the requesting app

+ {error !== undefined &&

Error: {error?.message}

}
From bd60d6c7134f933ce50de73f0672e9e469870643 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Sat, 6 May 2023 16:05:38 +0800 Subject: [PATCH 6/7] Small fix --- studio/pages/authorize.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx index 86559bebe6e..cc40fb28461 100644 --- a/studio/pages/authorize.tsx +++ b/studio/pages/authorize.tsx @@ -85,18 +85,6 @@ const APIAuthorizationPage: NextPageWithLayout = () => { } } - if (isLoading) { - return ( - Authorize API access

}> -
- - - -
-
- ) - } - if (auth_id === undefined) { return ( Authorization for API access

}> @@ -109,6 +97,18 @@ const APIAuthorizationPage: NextPageWithLayout = () => { ) } + if (isLoading) { + return ( + Authorize API access

}> +
+ + + +
+
+ ) + } + if (isError) { return ( Authorize API access

}> From d3ac673cfb20c32a8f679a3e220268e4f531cd4d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 8 May 2023 19:49:16 +0800 Subject: [PATCH 7/7] Swap logic for isLoading and auth id undefined --- studio/pages/authorize.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/studio/pages/authorize.tsx b/studio/pages/authorize.tsx index cc40fb28461..86559bebe6e 100644 --- a/studio/pages/authorize.tsx +++ b/studio/pages/authorize.tsx @@ -85,18 +85,6 @@ const APIAuthorizationPage: NextPageWithLayout = () => { } } - if (auth_id === undefined) { - return ( - Authorization for API access

}> -
- - Please provide a valid authorization ID in the URL - -
-
- ) - } - if (isLoading) { return ( Authorize API access

}> @@ -109,6 +97,18 @@ const APIAuthorizationPage: NextPageWithLayout = () => { ) } + if (auth_id === undefined) { + return ( + Authorization for API access

}> +
+ + Please provide a valid authorization ID in the URL + +
+
+ ) + } + if (isError) { return ( Authorize API access

}>