mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(core): support path-based multi-tenancy
This commit is contained in:
parent
2a7c623240
commit
d29432f45d
5 changed files with 46 additions and 13 deletions
|
@ -8,7 +8,6 @@ export default class GlobalValues {
|
|||
public readonly isProduction = getEnv('NODE_ENV') === 'production';
|
||||
public readonly isTest = getEnv('NODE_ENV') === 'test';
|
||||
public readonly isIntegrationTest = yes(getEnv('INTEGRATION_TEST'));
|
||||
public readonly isCloud = yes(process.env.IS_CLOUD);
|
||||
|
||||
public readonly httpsCert = process.env.HTTPS_CERT_PATH;
|
||||
public readonly httpsKey = process.env.HTTPS_KEY_PATH;
|
||||
|
@ -52,6 +51,9 @@ export default class GlobalValues {
|
|||
/** @see urlSet For detailed explanation. */
|
||||
public readonly isDomainBasedMultiTenancy = this.urlSet.endpoint.hostname.includes('*');
|
||||
|
||||
public readonly isPathBasedMultiTenancy =
|
||||
!this.isDomainBasedMultiTenancy && yes(getEnv('PATH_BASED_MULTI_TENANCY'));
|
||||
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
public readonly databaseUrl = tryThat(() => assertEnv('DB_URL'), throwErrorWithDsnMessage);
|
||||
public readonly developmentTenantId = getEnv('DEVELOPMENT_TENANT_ID');
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import path from 'path';
|
||||
|
||||
import { adminTenantId } from '@logto/schemas';
|
||||
import type { Optional } from '@silverhand/essentials';
|
||||
import { deduplicate, trySafe } from '@silverhand/essentials';
|
||||
|
@ -6,7 +8,7 @@ import type GlobalValues from './GlobalValues.js';
|
|||
|
||||
export const getTenantEndpoint = (
|
||||
id: string,
|
||||
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy }: GlobalValues
|
||||
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy, isPathBasedMultiTenancy }: GlobalValues
|
||||
): URL => {
|
||||
const adminUrl = trySafe(() => adminUrlSet.endpoint);
|
||||
|
||||
|
@ -14,6 +16,10 @@ export const getTenantEndpoint = (
|
|||
return adminUrl;
|
||||
}
|
||||
|
||||
if (isPathBasedMultiTenancy) {
|
||||
return new URL(path.join(urlSet.endpoint.pathname, id), urlSet.endpoint);
|
||||
}
|
||||
|
||||
if (!isDomainBasedMultiTenancy) {
|
||||
return urlSet.endpoint;
|
||||
}
|
||||
|
@ -27,7 +33,7 @@ export const getTenantEndpoint = (
|
|||
|
||||
export const getTenantLocalhost = (
|
||||
id: string,
|
||||
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy }: GlobalValues
|
||||
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy, isPathBasedMultiTenancy }: GlobalValues
|
||||
): Optional<URL> => {
|
||||
const adminUrl = trySafe(() => adminUrlSet.localhostUrl);
|
||||
|
||||
|
@ -35,8 +41,14 @@ export const getTenantLocalhost = (
|
|||
return adminUrl;
|
||||
}
|
||||
|
||||
const localhost = trySafe(() => urlSet.localhostUrl);
|
||||
|
||||
if (isPathBasedMultiTenancy && localhost) {
|
||||
return new URL(path.join(localhost.pathname, id), localhost);
|
||||
}
|
||||
|
||||
if (!isDomainBasedMultiTenancy) {
|
||||
return trySafe(() => urlSet.localhostUrl);
|
||||
return localhost;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -27,9 +27,13 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{
|
|||
keys: JWK[];
|
||||
issuer: string[];
|
||||
}> => {
|
||||
const { isDomainBasedMultiTenancy, adminUrlSet } = EnvSet.values;
|
||||
const { isDomainBasedMultiTenancy, isPathBasedMultiTenancy, adminUrlSet } = EnvSet.values;
|
||||
|
||||
if (!isDomainBasedMultiTenancy && adminUrlSet.deduplicated().length === 0) {
|
||||
if (
|
||||
!isDomainBasedMultiTenancy &&
|
||||
!isPathBasedMultiTenancy &&
|
||||
adminUrlSet.deduplicated().length === 0
|
||||
) {
|
||||
return { keys: [], issuer: [] };
|
||||
}
|
||||
|
||||
|
@ -48,7 +52,7 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{
|
|||
keys: await Promise.all(publicKeys.map(async (key) => exportJWK(key))),
|
||||
issuer: [
|
||||
appendPath(
|
||||
isDomainBasedMultiTenancy
|
||||
isDomainBasedMultiTenancy || isPathBasedMultiTenancy
|
||||
? getTenantEndpoint(adminTenantId, EnvSet.values)
|
||||
: adminUrlSet.endpoint,
|
||||
'/oidc'
|
||||
|
|
|
@ -43,6 +43,14 @@ export default class Tenant implements TenantContext {
|
|||
public readonly app: Koa;
|
||||
|
||||
get run(): MiddlewareType {
|
||||
if (
|
||||
EnvSet.values.isPathBasedMultiTenancy &&
|
||||
// If admin URL Set is specified, consider that URL first
|
||||
!(EnvSet.values.adminUrlSet.deduplicated().length > 0 && this.id === adminTenantId)
|
||||
) {
|
||||
return mount('/' + this.id, this.app);
|
||||
}
|
||||
|
||||
return mount(this.app);
|
||||
}
|
||||
|
||||
|
@ -79,6 +87,8 @@ export default class Tenant implements TenantContext {
|
|||
// Mount APIs
|
||||
app.use(mount('/api', initApis(tenantContext)));
|
||||
|
||||
const { isDomainBasedMultiTenancy, isPathBasedMultiTenancy } = EnvSet.values;
|
||||
|
||||
// Mount admin tenant APIs and app
|
||||
if (id === adminTenantId) {
|
||||
// Mount `/me` APIs for admin tenant
|
||||
|
@ -86,7 +96,7 @@ export default class Tenant implements TenantContext {
|
|||
|
||||
// Mount Admin Console when needed
|
||||
// Skip in domain-based multi-tenancy since Logto Cloud serves Admin Console in this case
|
||||
if (!EnvSet.values.isDomainBasedMultiTenancy) {
|
||||
if (!isDomainBasedMultiTenancy && !isPathBasedMultiTenancy) {
|
||||
app.use(koaConsoleRedirectProxy(queries));
|
||||
app.use(
|
||||
mount(
|
||||
|
@ -101,7 +111,7 @@ export default class Tenant implements TenantContext {
|
|||
// while distinguishing "demo app from admin tenant" and "demo app from user tenant";
|
||||
// on the cloud, we need to configure admin tenant sign-in experience, so a preview is needed for
|
||||
// testing without signing out of the admin console.
|
||||
if (id !== adminTenantId || EnvSet.values.isDomainBasedMultiTenancy) {
|
||||
if (id !== adminTenantId || isDomainBasedMultiTenancy || isPathBasedMultiTenancy) {
|
||||
// Mount demo app
|
||||
app.use(
|
||||
mount(
|
||||
|
|
|
@ -17,6 +17,7 @@ const isEndpointOf = (current: URL, endpoint: URL) => {
|
|||
export const getTenantId = (url: URL) => {
|
||||
const {
|
||||
isDomainBasedMultiTenancy,
|
||||
isPathBasedMultiTenancy,
|
||||
isProduction,
|
||||
isIntegrationTest,
|
||||
developmentTenantId,
|
||||
|
@ -34,13 +35,17 @@ export const getTenantId = (url: URL) => {
|
|||
return developmentTenantId;
|
||||
}
|
||||
|
||||
if (
|
||||
!isDomainBasedMultiTenancy ||
|
||||
(!urlSet.isLocalhostDisabled && isEndpointOf(url, urlSet.localhostUrl))
|
||||
) {
|
||||
if (!isDomainBasedMultiTenancy && !isPathBasedMultiTenancy) {
|
||||
return defaultTenantId;
|
||||
}
|
||||
|
||||
if (isPathBasedMultiTenancy) {
|
||||
const urlSegments = url.pathname.split('/');
|
||||
const endpointSegments = urlSet.endpoint.pathname.split('/');
|
||||
|
||||
return urlSegments[endpointSegments.length - 1];
|
||||
}
|
||||
|
||||
const toMatch = urlSet.endpoint.hostname.replace('*', '([^.]*)');
|
||||
const matchedId = new RegExp(toMatch).exec(url.hostname)?.[1];
|
||||
|
||||
|
|
Loading…
Reference in a new issue