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:
commit
3f8e42af81
4 changed files with 80 additions and 79 deletions
|
@ -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,
|
||||
]
|
||||
),
|
||||
],
|
||||
[]
|
||||
|
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue