0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

Merge pull request #5110 from logto-io/gao-use-organization-token

refactor(console): use organization token for management api
This commit is contained in:
Gao Sun 2023-12-19 23:54:58 +08:00 committed by GitHub
commit 3f8e42af81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 80 additions and 79 deletions

View file

@ -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,
]
),
],
[]

View file

@ -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 `<ErrorBoundary />`.
};
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' ? <Outlet /> : <AppLoading />;
return <Outlet />;
}

View file

@ -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<Tenants>({
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<CurrentTenantStatus>('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 <TenantsContext.Provider value={memorizedContext}>{children}</TenantsContext.Provider>;

View file

@ -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<StaticApiProps, 'prefixUrl' | 'resourceIndicator'> = {}) => {
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,
});
};