From bfd027d4e8641cc2d8cba2b6f7e89acd18d3e1bf Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 18 Dec 2023 13:07:30 +0800 Subject: [PATCH] refactor(console): use organization token for management api --- packages/console/src/App.tsx | 38 +++++++------ .../src/containers/TenantAccess/index.tsx | 48 ++++------------ .../console/src/contexts/TenantsProvider.tsx | 17 +----- packages/console/src/hooks/use-api.ts | 56 ++++++++++++++++--- 4 files changed, 80 insertions(+), 79 deletions(-) diff --git a/packages/console/src/App.tsx b/packages/console/src/App.tsx index 4252ebcb2..56f79cd4f 100644 --- a/packages/console/src/App.tsx +++ b/packages/console/src/App.tsx @@ -1,8 +1,13 @@ import { AppInsightsBoundary } from '@logto/app-insights/react'; import { UserScope } from '@logto/core-kit'; import { LogtoProvider, useLogto } from '@logto/react'; -import { adminConsoleApplicationId, PredefinedScope } from '@logto/schemas'; -import { conditionalArray, deduplicate } from '@silverhand/essentials'; +import { + adminConsoleApplicationId, + defaultTenantId, + PredefinedScope, + TenantScope, +} from '@logto/schemas'; +import { conditionalArray } from '@silverhand/essentials'; import { useContext, useMemo } from 'react'; import { Helmet } from 'react-helmet'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; @@ -66,22 +71,16 @@ export default App; * different components. */ function Providers() { - const { tenants, currentTenantId } = useContext(TenantsContext); + const { currentTenantId } = useContext(TenantsContext); + // For Cloud, we use Management API proxy for accessing tenant data. + // For OSS, we directly call the tenant API with the default tenant API resource. const resources = useMemo( () => - deduplicate( - conditionalArray( - // Explicitly add `currentTenantId` and deduplicate since the user may directly - // access a URL with Tenant ID, adding the ID from the URL here can possibly remove one - // additional redirect. - currentTenantId && getManagementApi(currentTenantId).indicator, - ...tenants.map(({ id }) => getManagementApi(id).indicator), - isCloud && cloudApi.indicator, - meApi.indicator - ) - ), - [currentTenantId, tenants] + isCloud + ? [cloudApi.indicator, meApi.indicator] + : [getManagementApi(defaultTenantId).indicator, meApi.indicator], + [] ); const scopes = useMemo( @@ -92,9 +91,12 @@ function Providers() { UserScope.Organizations, PredefinedScope.All, ...conditionalArray( - isCloud && cloudApi.scopes.CreateTenant, - isCloud && cloudApi.scopes.ManageTenant, - isCloud && cloudApi.scopes.ManageTenantSelf + isCloud && [ + ...Object.values(TenantScope), + cloudApi.scopes.CreateTenant, + cloudApi.scopes.ManageTenant, + cloudApi.scopes.ManageTenantSelf, + ] ), ], [] diff --git a/packages/console/src/containers/TenantAccess/index.tsx b/packages/console/src/containers/TenantAccess/index.tsx index 3d92438ba..781b225c9 100644 --- a/packages/console/src/containers/TenantAccess/index.tsx +++ b/packages/console/src/containers/TenantAccess/index.tsx @@ -1,11 +1,8 @@ import { useLogto } from '@logto/react'; -import { trySafe } from '@silverhand/essentials'; import { useContext, useEffect } from 'react'; import { Outlet } from 'react-router-dom'; import { useSWRConfig } from 'swr'; -import { type TenantResponse } from '@/cloud/types/router'; -import AppLoading from '@/components/AppLoading'; // Used in the docs // eslint-disable-next-line unused-imports/no-unused-imports import type ProtectedRoutes from '@/containers/ProtectedRoutes'; @@ -43,9 +40,8 @@ import { TenantsContext } from '@/contexts/TenantsProvider'; * @see ProtectedRoutes */ export default function TenantAccess() { - const { getAccessToken, signIn, isAuthenticated } = useLogto(); - const { currentTenant, currentTenantId, currentTenantStatus, setCurrentTenantStatus } = - useContext(TenantsContext); + const { isAuthenticated } = useLogto(); + const { currentTenant, currentTenantId } = useContext(TenantsContext); const { mutate } = useSWRConfig(); // Clean the cache when the current tenant ID changes. This is required because the @@ -68,39 +64,17 @@ export default function TenantAccess() { }, [mutate, currentTenantId]); useEffect(() => { - const validate = async ({ indicator }: TenantResponse) => { - // Test fetching an access token for the current Tenant ID. - // If failed, it means the user finishes the first auth, ands still needs to auth again to - // fetch the full-scoped (with all available tenants) token. - if (await trySafe(getAccessToken(indicator))) { - setCurrentTenantStatus('validated'); - } - // If failed, it will be treated as a session expired error, and will be handled by the - // upper ``. - }; - - if (isAuthenticated && currentTenantId && currentTenantStatus === 'pending') { - setCurrentTenantStatus('validating'); - + if ( + isAuthenticated && + currentTenantId && // The current tenant is unavailable to the user, maybe a deleted tenant or a tenant that // the user has no access to. Fall back to the home page. - if (!currentTenant) { - // eslint-disable-next-line @silverhand/fp/no-mutation - window.location.href = '/'; - return; - } - - void validate(currentTenant); + !currentTenant + ) { + // eslint-disable-next-line @silverhand/fp/no-mutation + window.location.href = '/'; } - }, [ - currentTenant, - currentTenantId, - currentTenantStatus, - getAccessToken, - isAuthenticated, - setCurrentTenantStatus, - signIn, - ]); + }, [currentTenant, currentTenantId, isAuthenticated]); - return currentTenantStatus === 'validated' ? : ; + return ; } diff --git a/packages/console/src/contexts/TenantsProvider.tsx b/packages/console/src/contexts/TenantsProvider.tsx index 41f9cf837..b31355122 100644 --- a/packages/console/src/contexts/TenantsProvider.tsx +++ b/packages/console/src/contexts/TenantsProvider.tsx @@ -58,13 +58,6 @@ type Tenants = { currentTenantId: string; currentTenant?: TenantResponse; isDevTenant: boolean; - /** - * Indicates if the Access Token has been validated for use. Will be reset to `pending` when the current tenant changes. - * - * @see {@link CurrentTenantStatus} - */ - currentTenantStatus: CurrentTenantStatus; - setCurrentTenantStatus: (status: CurrentTenantStatus) => void; /** Navigate to the given tenant ID. */ navigateTenant: (tenantId: string) => void; }; @@ -106,8 +99,6 @@ export const TenantsContext = createContext({ updateTenant: noop, currentTenantId: '', isDevTenant: false, - currentTenantStatus: 'pending', - setCurrentTenantStatus: noop, navigateTenant: noop, }); @@ -142,13 +133,10 @@ function TenantsProvider({ children }: Props) { return match.params.tenantId ?? ''; }, [match]); - const [currentTenantStatus, setCurrentTenantStatus] = useState('pending'); const navigateTenant = useCallback( (tenantId: string) => { navigate(`/${tenantId}`); - - setCurrentTenantStatus('pending'); }, [navigate] ); @@ -163,7 +151,6 @@ function TenantsProvider({ children }: Props) { tenants, resetTenants: (tenants: TenantResponse[]) => { setTenants(tenants); - setCurrentTenantStatus('pending'); setIsInitComplete(true); }, prependTenant: (tenant: TenantResponse) => { @@ -181,11 +168,9 @@ function TenantsProvider({ children }: Props) { currentTenantId, isDevTenant: currentTenant?.tag === TenantTag.Development, currentTenant, - currentTenantStatus, - setCurrentTenantStatus, navigateTenant, }), - [currentTenant, currentTenantId, currentTenantStatus, isInitComplete, navigateTenant, tenants] + [currentTenant, currentTenantId, isInitComplete, navigateTenant, tenants] ); return {children}; diff --git a/packages/console/src/hooks/use-api.ts b/packages/console/src/hooks/use-api.ts index f41be5098..0b2dcdb19 100644 --- a/packages/console/src/hooks/use-api.ts +++ b/packages/console/src/hooks/use-api.ts @@ -1,7 +1,17 @@ -import { httpCodeToMessage } from '@logto/core-kit'; +import { + buildOrganizationUrn, + getOrganizationIdFromUrn, + httpCodeToMessage, + organizationUrnPrefix, +} from '@logto/core-kit'; import { useLogto } from '@logto/react'; -import { getManagementApiResourceIndicator, type RequestErrorBody } from '@logto/schemas'; -import { conditionalArray } from '@silverhand/essentials'; +import { + getTenantOrganizationId, + type RequestErrorBody, + getManagementApiResourceIndicator, + defaultTenantId, +} from '@logto/schemas'; +import { appendPath, conditionalArray } from '@silverhand/essentials'; import ky from 'ky'; import { type KyInstance } from 'node_modules/ky/distribution/types/ky'; import { useCallback, useContext, useMemo } from 'react'; @@ -9,6 +19,7 @@ import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { requestTimeout } from '@/consts'; +import { isCloud } from '@/consts/env'; import { AppDataContext } from '@/contexts/AppDataProvider'; import { TenantsContext } from '@/contexts/TenantsProvider'; @@ -35,7 +46,7 @@ export const useStaticApi = ({ hideErrorToast, resourceIndicator, }: StaticApiProps): KyInstance => { - const { isAuthenticated, getAccessToken, signOut } = useLogto(); + const { isAuthenticated, getAccessToken, getOrganizationToken, signOut } = useLogto(); const { t, i18n } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { show } = useConfirmModal(); const postSignOutRedirectUri = useRedirectUri('signOut'); @@ -84,7 +95,9 @@ export const useStaticApi = ({ beforeRequest: [ async (request) => { if (isAuthenticated) { - const accessToken = await getAccessToken(resourceIndicator); + const accessToken = await (resourceIndicator.startsWith(organizationUrnPrefix) + ? getOrganizationToken(getOrganizationIdFromUrn(resourceIndicator)) + : getAccessToken(resourceIndicator)); request.headers.set('Authorization', `Bearer ${accessToken ?? ''}`); request.headers.set('Accept-Language', i18n.language); } @@ -97,8 +110,9 @@ export const useStaticApi = ({ hideErrorToast, toastError, isAuthenticated, - getAccessToken, resourceIndicator, + getOrganizationToken, + getAccessToken, i18n.language, ] ); @@ -106,14 +120,40 @@ export const useStaticApi = ({ return api; }; +/** A hook to get a Ky instance with the current tenant's Management API prefix URL. */ const useApi = (props: Omit = {}) => { const { tenantEndpoint } = useContext(AppDataContext); const { currentTenantId } = useContext(TenantsContext); + /** + * The config object for the Ky instance. + * + * - In Cloud, it uses the Management API proxy endpoint with tenant organization tokens. + * - In OSS, it directly uses the tenant endpoint (Management API). + * + * Since we removes all user roles for the Management API except the one for the default tenant, + * the OSS version should be used for the default tenant only. + */ + const config = useMemo( + () => + isCloud + ? { + prefixUrl: appendPath(new URL(window.location.origin), 'm', currentTenantId), + resourceIndicator: buildOrganizationUrn(getTenantOrganizationId(currentTenantId)), + } + : { + prefixUrl: tenantEndpoint, + resourceIndicator: getManagementApiResourceIndicator(currentTenantId), + }, + [currentTenantId, tenantEndpoint] + ); + + if (!isCloud && currentTenantId !== defaultTenantId) { + throw new Error('Only the default tenant is supported in OSS.'); + } return useStaticApi({ ...props, - prefixUrl: tenantEndpoint, - resourceIndicator: getManagementApiResourceIndicator(currentTenantId), + ...config, }); };