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:
parent
5b7f44d19b
commit
d789a08e41
25 changed files with 411 additions and 203 deletions
|
@ -1,3 +0,0 @@
|
|||
export enum CloudScope {
|
||||
CreateTenant = 'create:tenant',
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -103,6 +103,7 @@
|
|||
},
|
||||
"alias": {
|
||||
"@/*": "./src/$1",
|
||||
"@cloud/*": "./src/cloud/$1",
|
||||
"@mdx/components/*": "./src/mdx-components/$1"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
27
packages/console/src/cloud/hooks/use-cloud-api.ts
Normal file
27
packages/console/src/cloud/hooks/use-cloud-api.ts
Normal 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;
|
||||
};
|
26
packages/console/src/cloud/pages/Callback/index.tsx
Normal file
26
packages/console/src/cloud/pages/Callback/index.tsx
Normal 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;
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export enum CloudRoute {
|
||||
Callback = 'callback',
|
||||
Onboard = 'onboard',
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
});
|
28
packages/console/src/consts/resources.ts
Normal file
28
packages/console/src/consts/resources.ts
Normal 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);
|
|
@ -13,8 +13,6 @@ export type AppEndpoints = {
|
|||
adminEndpoint?: URL;
|
||||
};
|
||||
|
||||
export type AppEndpointKey = keyof AppEndpoints;
|
||||
|
||||
export const AppEndpointsContext = createContext<AppEndpoints>({});
|
||||
|
||||
const AppEndpointsProvider = ({ children }: Props) => {
|
40
packages/console/src/contexts/TenantsProvider.tsx
Normal file
40
packages/console/src/contexts/TenantsProvider.tsx
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
145
packages/console/src/pages/Main/index.tsx
Normal file
145
packages/console/src/pages/Main/index.tsx
Normal 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;
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
"@/*": ["./src/*"],
|
||||
"@cloud/*": ["./src/cloud/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
|
|
@ -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:
|
||||
*
|
||||
|
|
|
@ -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';
|
||||
|
|
4
packages/schemas/src/types/tenant.ts
Normal file
4
packages/schemas/src/types/tenant.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export type TenantInfo = {
|
||||
id: string;
|
||||
indicator: string;
|
||||
};
|
Loading…
Add table
Reference in a new issue