0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

feat(console): support multi-tenancy (1/2) (#3205)

This commit is contained in:
Gao Sun 2023-02-28 16:04:12 +08:00 committed by GitHub
parent 5b7f44d19b
commit d789a08e41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 411 additions and 203 deletions

View file

@ -1,3 +0,0 @@
export enum CloudScope {
CreateTenant = 'create:tenant',
}

View file

@ -1,5 +1,5 @@
import { generateStandardId } from '@logto/core-kit';
import type { TenantModel } from '@logto/schemas';
import type { TenantInfo, TenantModel } from '@logto/schemas';
import {
LogtoConfigs,
SignInExperiences,
@ -20,11 +20,6 @@ import { getDatabaseName } from '#src/queries/utils.js';
import { insertInto } from '#src/utils/query.js';
import { getTenantIdFromManagementApiIndicator } from '#src/utils/tenant.js';
export type TenantInfo = {
id: string;
indicator: string;
};
export const tenantInfoGuard: ZodType<TenantInfo> = z.object({
id: z.string(),
indicator: z.string(),

View file

@ -1,6 +1,6 @@
import { CloudScope } from '@logto/schemas';
import { createRouter, RequestError } from '@withtyped/server';
import { CloudScope } from '#src/consts/rbac.js';
import { tenantInfoGuard, TenantsLibrary } from '#src/libraries/tenants.js';
import type { WithAuthContext } from '#src/middleware/with-auth.js';
import { Queries } from '#src/queries/index.js';

View file

@ -103,6 +103,7 @@
},
"alias": {
"@/*": "./src/$1",
"@cloud/*": "./src/cloud/$1",
"@mdx/components/*": "./src/mdx-components/$1"
},
"eslintConfig": {

View file

@ -1,9 +1,8 @@
import { UserScope } from '@logto/core-kit';
import { LogtoProvider } from '@logto/react';
import { adminConsoleApplicationId } from '@logto/schemas';
import { adminConsoleApplicationId, PredefinedScope } from '@logto/schemas';
import { deduplicate } from '@silverhand/essentials';
import { useContext } from 'react';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { SWRConfig } from 'swr';
import 'overlayscrollbars/styles/overlayscrollbars.css';
import './scss/normalized.scss';
@ -12,183 +11,62 @@ import './scss/overlayscrollbars.scss';
// eslint-disable-next-line import/no-unassigned-import
import '@fontsource/roboto-mono';
import CloudApp from '@/cloud/App';
import AppLoading from '@/components/AppLoading';
import Toast from '@/components/Toast';
import { getManagementApi, meApi } from '@/consts/management-api';
import AppBoundary from '@/containers/AppBoundary';
import AppLayout from '@/containers/AppLayout';
import ErrorBoundary from '@/containers/ErrorBoundary';
import useSwrOptions from '@/hooks/use-swr-options';
import { cloudApi, getManagementApi, meApi } from '@/consts/resources';
import initI18n from '@/i18n/init';
import ApiResourceDetails from '@/pages/ApiResourceDetails';
import ApiResources from '@/pages/ApiResources';
import ApplicationDetails from '@/pages/ApplicationDetails';
import Applications from '@/pages/Applications';
import AuditLogDetails from '@/pages/AuditLogDetails';
import AuditLogs from '@/pages/AuditLogs';
import Callback from '@/pages/Callback';
import ConnectorDetails from '@/pages/ConnectorDetails';
import Connectors from '@/pages/Connectors';
import Dashboard from '@/pages/Dashboard';
import GetStarted from '@/pages/GetStarted';
import NotFound from '@/pages/NotFound';
import RoleDetails from '@/pages/RoleDetails';
import Roles from '@/pages/Roles';
import Settings from '@/pages/Settings';
import SignInExperience from '@/pages/SignInExperience';
import UserDetails from '@/pages/UserDetails';
import Users from '@/pages/Users';
import Welcome from '@/pages/Welcome';
import {
ApiResourceDetailsTabs,
ConnectorsTabs,
RoleDetailsTabs,
SignInExperiencePage,
UserDetailsTabs,
adminTenantEndpoint,
getUserTenantId,
getBasename,
} from './consts';
import AppContent from './containers/AppContent';
import AppEndpointsProvider, { AppEndpointsContext } from './containers/AppEndpointsProvider';
import ApiResourcePermissions from './pages/ApiResourceDetails/ApiResourcePermissions';
import ApiResourceSettings from './pages/ApiResourceDetails/ApiResourceSettings';
import Profile from './pages/Profile';
import RolePermissions from './pages/RoleDetails/RolePermissions';
import RoleSettings from './pages/RoleDetails/RoleSettings';
import RoleUsers from './pages/RoleDetails/RoleUsers';
import UserLogs from './pages/UserDetails/UserLogs';
import UserRoles from './pages/UserDetails/UserRoles';
import UserSettings from './pages/UserDetails/UserSettings';
import { adminTenantEndpoint, getUserTenantId } from './consts';
import { isCloud } from './consts/cloud';
import AppEndpointsProvider from './contexts/AppEndpointsProvider';
import TenantsProvider, { TenantsContext } from './contexts/TenantsProvider';
import Main from './pages/Main';
void initI18n();
const Main = () => {
const swrOptions = useSwrOptions();
const { userEndpoint } = useContext(AppEndpointsContext);
const Content = () => {
const {
tenants: { data, isSettle },
} = useContext(TenantsContext);
const currentTenantId = getUserTenantId();
if (!userEndpoint) {
return <AppLoading />;
}
const resources = deduplicate([
...(currentTenantId && [getManagementApi(currentTenantId).indicator]),
...(data ?? []).map(({ id }) => getManagementApi(id).indicator),
...(isCloud ? [cloudApi.indicator] : []),
meApi.indicator,
]);
const scopes = [
UserScope.Email,
UserScope.Identities,
UserScope.CustomData,
PredefinedScope.All,
cloudApi.scopes.CreateTenant, // It's fine to keep scope here since core will filter
];
return (
<ErrorBoundary>
<SWRConfig value={swrOptions}>
<AppBoundary>
<Toast />
<Routes>
<Route path="callback" element={<Callback />} />
<Route path="welcome" element={<Welcome />} />
<Route element={<AppLayout />}>
<Route element={<AppContent />}>
<Route path="*" element={<NotFound />} />
<Route path="get-started" element={<GetStarted />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="applications">
<Route index element={<Applications />} />
<Route path="create" element={<Applications />} />
<Route path=":id" element={<ApplicationDetails />} />
</Route>
<Route path="api-resources">
<Route index element={<ApiResources />} />
<Route path="create" element={<ApiResources />} />
<Route path=":id" element={<ApiResourceDetails />}>
<Route
index
element={<Navigate replace to={ApiResourceDetailsTabs.Settings} />}
/>
<Route
path={ApiResourceDetailsTabs.Settings}
element={<ApiResourceSettings />}
/>
<Route
path={ApiResourceDetailsTabs.Permissions}
element={<ApiResourcePermissions />}
/>
</Route>
</Route>
<Route path="sign-in-experience">
<Route
index
element={<Navigate replace to={SignInExperiencePage.BrandingTab} />}
/>
<Route path=":tab" element={<SignInExperience />} />
</Route>
<Route path="connectors">
<Route index element={<Navigate replace to={ConnectorsTabs.Passwordless} />} />
<Route path=":tab" element={<Connectors />} />
<Route path=":tab/create/:createType" element={<Connectors />} />
<Route path=":tab/:connectorId" element={<ConnectorDetails />} />
</Route>
<Route path="users">
<Route index element={<Users />} />
<Route path="create" element={<Users />} />
<Route path=":id" element={<UserDetails />}>
<Route index element={<Navigate replace to={UserDetailsTabs.Settings} />} />
<Route path={UserDetailsTabs.Settings} element={<UserSettings />} />
<Route path={UserDetailsTabs.Roles} element={<UserRoles />} />
<Route path={UserDetailsTabs.Logs} element={<UserLogs />} />
</Route>
<Route
path={`:id/${UserDetailsTabs.Logs}/:logId`}
element={<AuditLogDetails />}
/>
</Route>
<Route path="audit-logs">
<Route index element={<AuditLogs />} />
<Route path=":logId" element={<AuditLogDetails />} />
</Route>
<Route path="roles">
<Route index element={<Roles />} />
<Route path="create" element={<Roles />} />
<Route path=":id" element={<RoleDetails />}>
<Route index element={<Navigate replace to={RoleDetailsTabs.Settings} />} />
<Route path={RoleDetailsTabs.Settings} element={<RoleSettings />} />
<Route path={RoleDetailsTabs.Permissions} element={<RolePermissions />} />
<Route path={RoleDetailsTabs.Users} element={<RoleUsers />} />
</Route>
</Route>
<Route path="settings" element={<Settings />} />
<Route path="profile" element={<Profile />} />
</Route>
</Route>
</Routes>
</AppBoundary>
</SWRConfig>
</ErrorBoundary>
<LogtoProvider
config={{
endpoint: adminTenantEndpoint,
appId: adminConsoleApplicationId,
resources,
scopes,
}}
>
{!isCloud || (data && isSettle && currentTenantId) ? (
<AppEndpointsProvider>
<Main />
</AppEndpointsProvider>
) : (
<CloudApp />
)}
</LogtoProvider>
);
};
const App = () => {
const tenantId = getUserTenantId();
if (!tenantId) {
return <CloudApp />;
}
const managementApi = getManagementApi(tenantId);
return (
<BrowserRouter basename={getBasename()}>
<AppEndpointsProvider>
<LogtoProvider
config={{
endpoint: adminTenantEndpoint,
appId: adminConsoleApplicationId,
resources: [managementApi.indicator, meApi.indicator],
scopes: [
UserScope.Email,
UserScope.Identities,
UserScope.CustomData,
managementApi.scopeAll,
],
}}
>
<Main />
</LogtoProvider>
</AppEndpointsProvider>
</BrowserRouter>
<TenantsProvider>
<Content />
</TenantsProvider>
);
};
export default App;

View file

@ -1,5 +1,7 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Callback from '@cloud/pages/Callback';
import * as styles from './App.module.scss';
import Main from './pages/Main';
import Onboard from './pages/Onboard';
@ -10,8 +12,10 @@ const App = () => {
<BrowserRouter>
<div className={styles.app}>
<Routes>
<Route path="/" element={<Main />} />
<Route path={CloudRoute.Onboard + '/*'} element={<Onboard />} />
<Route path={`/${CloudRoute.Onboard}/*`} element={<Onboard />} />
<Route path={`/${CloudRoute.Callback}`} element={<Callback />} />
<Route path={`/:tenantId/${CloudRoute.Callback}`} element={<Callback />} />
<Route path="/*" element={<Main />} />
</Routes>
</div>
</BrowserRouter>

View file

@ -0,0 +1,27 @@
import { useLogto } from '@logto/react';
import ky from 'ky';
import { useMemo } from 'react';
import { cloudApi } from '@/consts';
export const useCloudApi = () => {
const { isAuthenticated, getAccessToken } = useLogto();
const api = useMemo(
() =>
ky.create({
hooks: {
beforeRequest: [
async (request) => {
if (isAuthenticated) {
const accessToken = await getAccessToken(cloudApi.indicator);
request.headers.set('Authorization', `Bearer ${accessToken ?? ''}`);
}
},
],
},
}),
[getAccessToken, isAuthenticated]
);
return api;
};

View file

@ -0,0 +1,26 @@
import { useHandleSignInCallback } from '@logto/react';
import { useNavigate } from 'react-router-dom';
import AppLoading from '@/components/AppLoading';
import { getUserTenantId } from '@/consts/tenants';
const Callback = () => {
const navigate = useNavigate();
const { error } = useHandleSignInCallback(() => {
navigate('/' + getUserTenantId(), { replace: true });
});
if (error) {
return (
<div>
Error Occurred:
<br />
{error.message}
</div>
);
}
return <AppLoading />;
};
export default Callback;

View file

@ -11,7 +11,7 @@ import * as pageLayout from '@/cloud/scss/layout.module.scss';
import Button from '@/components/Button';
import Divider from '@/components/Divider';
import OverlayScrollbar from '@/components/OverlayScrollbar';
import { AppEndpointsContext } from '@/containers/AppEndpointsProvider';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
import { OnboardPage } from '../../types';
import { getOnboardPagePathname } from '../../utils';

View file

@ -1,5 +1,77 @@
import { useLogto } from '@logto/react';
import type { TenantInfo } from '@logto/schemas';
import { trySafe } from '@silverhand/essentials';
import { useCallback, useContext, useEffect } from 'react';
import { useHref, useNavigate } from 'react-router-dom';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import AppLoading from '@/components/AppLoading';
import { getUserTenantId } from '@/consts/tenants';
import { TenantsContext } from '@/contexts/TenantsProvider';
const Main = () => {
return <div>Main</div>;
const { isAuthenticated, isLoading, signIn, getAccessToken } = useLogto();
const api = useCloudApi();
const {
tenants: { data, isSettle },
setTenants,
} = useContext(TenantsContext);
const navigate = useNavigate();
const href = useHref(getUserTenantId() + '/callback');
const loadTenants = useCallback(async () => {
const data = await api.get('/api/tenants').json<TenantInfo[]>();
const currentId = getUserTenantId();
const current = data.find(({ id }) => id === currentId);
if (currentId) {
if (current) {
// 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(current.indicator)))) {
setTenants({ data, isSettle: false });
return;
}
} else {
// TODO: this tenant id is not in the list, should show an error
navigate('/', { replace: true });
}
}
setTenants({ data, isSettle: true });
}, [api, getAccessToken, navigate, setTenants]);
useEffect(() => {
if (isAuthenticated && data === undefined) {
void loadTenants();
}
}, [data, isAuthenticated, loadTenants]);
useEffect(() => {
if ((!isLoading && !isAuthenticated) || (isAuthenticated && !isSettle)) {
void signIn(new URL(href, window.location.origin).toString());
}
}, [href, isAuthenticated, isLoading, isSettle, signIn]);
if (data) {
if (data.length === 0) {
return <div>no tenant, should automatically create one</div>;
}
if (data.length === 1) {
return <div>single tenant: {data[0]?.id}, should automatically redirect</div>;
}
if (data.length > 1) {
return (
<div>multiple tenants: {data.map(({ id }) => id).join(', ')}, should let user choose</div>
);
}
}
return <AppLoading />;
};
export default Main;

View file

@ -1,6 +1,7 @@
import { z } from 'zod';
export enum CloudRoute {
Callback = 'callback',
Onboard = 'onboard',
}

View file

@ -1,7 +1,7 @@
export * from './applications';
export * from './connectors';
export * from './logs';
export * from './management-api';
export * from './resources';
export * from './tenants';
export * from './page-tabs';
export * from './external-links';

View file

@ -1,12 +0,0 @@
import { adminTenantId, getManagementApiResourceIndicator, PredefinedScope } from '@logto/schemas';
export const getManagementApi = (tenantId: string) =>
Object.freeze({
indicator: getManagementApiResourceIndicator(tenantId),
scopeAll: PredefinedScope.All,
});
export const meApi = Object.freeze({
indicator: getManagementApiResourceIndicator(adminTenantId, 'me'),
scopeAll: PredefinedScope.All,
});

View file

@ -0,0 +1,28 @@
import {
adminTenantId,
cloudApiIndicator,
CloudScope,
getManagementApiResourceIndicator,
PredefinedScope,
} from '@logto/schemas';
export type ApiResource = {
indicator: string;
scopes: Record<string, string>;
};
export const getManagementApi = (tenantId: string) =>
Object.freeze({
indicator: getManagementApiResourceIndicator(tenantId),
scopes: PredefinedScope,
} satisfies ApiResource);
export const meApi = Object.freeze({
indicator: getManagementApiResourceIndicator(adminTenantId, 'me'),
scopes: PredefinedScope,
} satisfies ApiResource);
export const cloudApi = Object.freeze({
indicator: cloudApiIndicator,
scopes: CloudScope,
} satisfies ApiResource);

View file

@ -13,8 +13,6 @@ export type AppEndpoints = {
adminEndpoint?: URL;
};
export type AppEndpointKey = keyof AppEndpoints;
export const AppEndpointsContext = createContext<AppEndpoints>({});
const AppEndpointsProvider = ({ children }: Props) => {

View file

@ -0,0 +1,40 @@
import type { TenantInfo } from '@logto/schemas';
import { defaultManagementApi } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import type { ReactNode } from 'react';
import { useMemo, createContext, useState } from 'react';
import { isCloud } from '@/consts/cloud';
type Props = {
children: ReactNode;
};
type Payload = { data?: TenantInfo[]; isSettle: boolean };
export type Tenants = {
tenants: Payload;
setTenants: (payload: Payload) => void;
};
const { tenantId, indicator } = defaultManagementApi.resource;
const initialPayload: Payload = {
data: conditional(!isCloud && [{ id: tenantId, indicator }]),
isSettle: true,
};
export const TenantsContext = createContext<Tenants>({
tenants: initialPayload,
setTenants: () => {
throw new Error('Not implemented');
},
});
const TenantsProvider = ({ children }: Props) => {
const [tenants, setTenants] = useState(initialPayload);
const memorizedContext = useMemo(() => ({ tenants, setTenants }), [tenants]);
return <TenantsContext.Provider value={memorizedContext}>{children}</TenantsContext.Provider>;
};
export default TenantsProvider;

View file

@ -6,7 +6,7 @@ import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { getManagementApi, getUserTenantId, requestTimeout } from '@/consts';
import { AppEndpointsContext } from '@/containers/AppEndpointsProvider';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
export class RequestError extends Error {
status: number;

View file

@ -8,7 +8,7 @@ import { useContext, cloneElement, lazy, Suspense, useEffect, useState } from 'r
import CodeEditor from '@/components/CodeEditor';
import TextLink from '@/components/TextLink';
import { AppEndpointsContext } from '@/containers/AppEndpointsProvider';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
import DetailsSummary from '@/mdx-components/DetailsSummary';
import type { SupportedSdk } from '@/types/applications';
import { applicationTypeAndSdkTypeMappings } from '@/types/applications';

View file

@ -20,7 +20,7 @@ import Passwordless from '@/assets/images/passwordless.svg';
import { discordLink, githubLink } from '@/consts';
import { isCloud } from '@/consts/cloud';
import { ConnectorsTabs } from '@/consts/page-tabs';
import { AppEndpointsContext } from '@/containers/AppEndpointsProvider';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
import useConfigs from '@/hooks/use-configs';
import { useTheme } from '@/hooks/use-theme';

View file

@ -0,0 +1,145 @@
import { useContext } from 'react';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { SWRConfig } from 'swr';
import AppLoading from '@/components/AppLoading';
import Toast from '@/components/Toast';
import {
ApiResourceDetailsTabs,
ConnectorsTabs,
getBasename,
RoleDetailsTabs,
SignInExperiencePage,
UserDetailsTabs,
} from '@/consts';
import AppBoundary from '@/containers/AppBoundary';
import AppContent from '@/containers/AppContent';
import AppLayout from '@/containers/AppLayout';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
import useSwrOptions from '@/hooks/use-swr-options';
import ApiResourceDetails from '@/pages/ApiResourceDetails';
import ApiResourcePermissions from '@/pages/ApiResourceDetails/ApiResourcePermissions';
import ApiResourceSettings from '@/pages/ApiResourceDetails/ApiResourceSettings';
import ApiResources from '@/pages/ApiResources';
import ApplicationDetails from '@/pages/ApplicationDetails';
import Applications from '@/pages/Applications';
import AuditLogDetails from '@/pages/AuditLogDetails';
import AuditLogs from '@/pages/AuditLogs';
import Callback from '@/pages/Callback';
import ConnectorDetails from '@/pages/ConnectorDetails';
import Connectors from '@/pages/Connectors';
import Dashboard from '@/pages/Dashboard';
import GetStarted from '@/pages/GetStarted';
import NotFound from '@/pages/NotFound';
import Profile from '@/pages/Profile';
import RoleDetails from '@/pages/RoleDetails';
import RolePermissions from '@/pages/RoleDetails/RolePermissions';
import RoleSettings from '@/pages/RoleDetails/RoleSettings';
import RoleUsers from '@/pages/RoleDetails/RoleUsers';
import Roles from '@/pages/Roles';
import Settings from '@/pages/Settings';
import SignInExperience from '@/pages/SignInExperience';
import UserDetails from '@/pages/UserDetails';
import UserLogs from '@/pages/UserDetails/UserLogs';
import UserRoles from '@/pages/UserDetails/UserRoles';
import UserSettings from '@/pages/UserDetails/UserSettings';
import Users from '@/pages/Users';
import Welcome from '@/pages/Welcome';
const Main = () => {
const swrOptions = useSwrOptions();
const { userEndpoint } = useContext(AppEndpointsContext);
if (!userEndpoint) {
return <AppLoading />;
}
return (
<BrowserRouter basename={getBasename()}>
<SWRConfig value={swrOptions}>
<AppBoundary>
<Toast />
<Routes>
<Route path="callback" element={<Callback />} />
<Route path="welcome" element={<Welcome />} />
<Route element={<AppLayout />}>
<Route element={<AppContent />}>
<Route path="*" element={<NotFound />} />
<Route path="get-started" element={<GetStarted />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="applications">
<Route index element={<Applications />} />
<Route path="create" element={<Applications />} />
<Route path=":id" element={<ApplicationDetails />} />
</Route>
<Route path="api-resources">
<Route index element={<ApiResources />} />
<Route path="create" element={<ApiResources />} />
<Route path=":id" element={<ApiResourceDetails />}>
<Route
index
element={<Navigate replace to={ApiResourceDetailsTabs.Settings} />}
/>
<Route
path={ApiResourceDetailsTabs.Settings}
element={<ApiResourceSettings />}
/>
<Route
path={ApiResourceDetailsTabs.Permissions}
element={<ApiResourcePermissions />}
/>
</Route>
</Route>
<Route path="sign-in-experience">
<Route
index
element={<Navigate replace to={SignInExperiencePage.BrandingTab} />}
/>
<Route path=":tab" element={<SignInExperience />} />
</Route>
<Route path="connectors">
<Route index element={<Navigate replace to={ConnectorsTabs.Passwordless} />} />
<Route path=":tab" element={<Connectors />} />
<Route path=":tab/create/:createType" element={<Connectors />} />
<Route path=":tab/:connectorId" element={<ConnectorDetails />} />
</Route>
<Route path="users">
<Route index element={<Users />} />
<Route path="create" element={<Users />} />
<Route path=":id" element={<UserDetails />}>
<Route index element={<Navigate replace to={UserDetailsTabs.Settings} />} />
<Route path={UserDetailsTabs.Settings} element={<UserSettings />} />
<Route path={UserDetailsTabs.Roles} element={<UserRoles />} />
<Route path={UserDetailsTabs.Logs} element={<UserLogs />} />
</Route>
<Route
path={`:id/${UserDetailsTabs.Logs}/:logId`}
element={<AuditLogDetails />}
/>
</Route>
<Route path="audit-logs">
<Route index element={<AuditLogs />} />
<Route path=":logId" element={<AuditLogDetails />} />
</Route>
<Route path="roles">
<Route index element={<Roles />} />
<Route path="create" element={<Roles />} />
<Route path=":id" element={<RoleDetails />}>
<Route index element={<Navigate replace to={RoleDetailsTabs.Settings} />} />
<Route path={RoleDetailsTabs.Settings} element={<RoleSettings />} />
<Route path={RoleDetailsTabs.Permissions} element={<RolePermissions />} />
<Route path={RoleDetailsTabs.Users} element={<RoleUsers />} />
</Route>
</Route>
<Route path="settings" element={<Settings />} />
<Route path="profile" element={<Profile />} />
</Route>
</Route>
</Routes>
</AppBoundary>
</SWRConfig>
</BrowserRouter>
);
};
export default Main;

View file

@ -12,7 +12,7 @@ import useSWR from 'swr';
import PhoneInfo from '@/assets/images/phone-info.svg';
import Select from '@/components/Select';
import TabNav, { TabNavItem } from '@/components/TabNav';
import { AppEndpointsContext } from '@/containers/AppEndpointsProvider';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
import type { RequestError } from '@/hooks/use-api';
import useUiLanguages from '@/hooks/use-ui-languages';

View file

@ -3,9 +3,8 @@
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"],
"@cloud/*": ["./src/cloud/*"]
}
},
"include": [

View file

@ -1,6 +1,10 @@
/** The API Resource Indicator for Logto Cloud. It's only useful when domain-based multi-tenancy is enabled. */
export const cloudApiIndicator = 'https://cloud.logto.io/api';
export enum CloudScope {
CreateTenant = 'create:tenant',
}
/**
* In OSS:
*

View file

@ -11,3 +11,4 @@ export * from './role.js';
export * from './verification-code.js';
export * from './application.js';
export * from './system.js';
export * from './tenant.js';

View file

@ -0,0 +1,4 @@
export type TenantInfo = {
id: string;
indicator: string;
};