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,
});
};