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 isProduction = getEnv('NODE_ENV') === 'production';
|
||||||
public readonly isTest = getEnv('NODE_ENV') === 'test';
|
public readonly isTest = getEnv('NODE_ENV') === 'test';
|
||||||
public readonly isIntegrationTest = yes(getEnv('INTEGRATION_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 httpsCert = process.env.HTTPS_CERT_PATH;
|
||||||
public readonly httpsKey = process.env.HTTPS_KEY_PATH;
|
public readonly httpsKey = process.env.HTTPS_KEY_PATH;
|
||||||
|
@ -52,6 +51,9 @@ export default class GlobalValues {
|
||||||
/** @see urlSet For detailed explanation. */
|
/** @see urlSet For detailed explanation. */
|
||||||
public readonly isDomainBasedMultiTenancy = this.urlSet.endpoint.hostname.includes('*');
|
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
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||||
public readonly databaseUrl = tryThat(() => assertEnv('DB_URL'), throwErrorWithDsnMessage);
|
public readonly databaseUrl = tryThat(() => assertEnv('DB_URL'), throwErrorWithDsnMessage);
|
||||||
public readonly developmentTenantId = getEnv('DEVELOPMENT_TENANT_ID');
|
public readonly developmentTenantId = getEnv('DEVELOPMENT_TENANT_ID');
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import { adminTenantId } from '@logto/schemas';
|
import { adminTenantId } from '@logto/schemas';
|
||||||
import type { Optional } from '@silverhand/essentials';
|
import type { Optional } from '@silverhand/essentials';
|
||||||
import { deduplicate, trySafe } from '@silverhand/essentials';
|
import { deduplicate, trySafe } from '@silverhand/essentials';
|
||||||
|
@ -6,7 +8,7 @@ import type GlobalValues from './GlobalValues.js';
|
||||||
|
|
||||||
export const getTenantEndpoint = (
|
export const getTenantEndpoint = (
|
||||||
id: string,
|
id: string,
|
||||||
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy }: GlobalValues
|
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy, isPathBasedMultiTenancy }: GlobalValues
|
||||||
): URL => {
|
): URL => {
|
||||||
const adminUrl = trySafe(() => adminUrlSet.endpoint);
|
const adminUrl = trySafe(() => adminUrlSet.endpoint);
|
||||||
|
|
||||||
|
@ -14,6 +16,10 @@ export const getTenantEndpoint = (
|
||||||
return adminUrl;
|
return adminUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPathBasedMultiTenancy) {
|
||||||
|
return new URL(path.join(urlSet.endpoint.pathname, id), urlSet.endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDomainBasedMultiTenancy) {
|
if (!isDomainBasedMultiTenancy) {
|
||||||
return urlSet.endpoint;
|
return urlSet.endpoint;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +33,7 @@ export const getTenantEndpoint = (
|
||||||
|
|
||||||
export const getTenantLocalhost = (
|
export const getTenantLocalhost = (
|
||||||
id: string,
|
id: string,
|
||||||
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy }: GlobalValues
|
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy, isPathBasedMultiTenancy }: GlobalValues
|
||||||
): Optional<URL> => {
|
): Optional<URL> => {
|
||||||
const adminUrl = trySafe(() => adminUrlSet.localhostUrl);
|
const adminUrl = trySafe(() => adminUrlSet.localhostUrl);
|
||||||
|
|
||||||
|
@ -35,8 +41,14 @@ export const getTenantLocalhost = (
|
||||||
return adminUrl;
|
return adminUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localhost = trySafe(() => urlSet.localhostUrl);
|
||||||
|
|
||||||
|
if (isPathBasedMultiTenancy && localhost) {
|
||||||
|
return new URL(path.join(localhost.pathname, id), localhost);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDomainBasedMultiTenancy) {
|
if (!isDomainBasedMultiTenancy) {
|
||||||
return trySafe(() => urlSet.localhostUrl);
|
return localhost;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,13 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{
|
||||||
keys: JWK[];
|
keys: JWK[];
|
||||||
issuer: string[];
|
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: [] };
|
return { keys: [], issuer: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +52,7 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{
|
||||||
keys: await Promise.all(publicKeys.map(async (key) => exportJWK(key))),
|
keys: await Promise.all(publicKeys.map(async (key) => exportJWK(key))),
|
||||||
issuer: [
|
issuer: [
|
||||||
appendPath(
|
appendPath(
|
||||||
isDomainBasedMultiTenancy
|
isDomainBasedMultiTenancy || isPathBasedMultiTenancy
|
||||||
? getTenantEndpoint(adminTenantId, EnvSet.values)
|
? getTenantEndpoint(adminTenantId, EnvSet.values)
|
||||||
: adminUrlSet.endpoint,
|
: adminUrlSet.endpoint,
|
||||||
'/oidc'
|
'/oidc'
|
||||||
|
|
|
@ -43,6 +43,14 @@ export default class Tenant implements TenantContext {
|
||||||
public readonly app: Koa;
|
public readonly app: Koa;
|
||||||
|
|
||||||
get run(): MiddlewareType {
|
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);
|
return mount(this.app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +87,8 @@ export default class Tenant implements TenantContext {
|
||||||
// Mount APIs
|
// Mount APIs
|
||||||
app.use(mount('/api', initApis(tenantContext)));
|
app.use(mount('/api', initApis(tenantContext)));
|
||||||
|
|
||||||
|
const { isDomainBasedMultiTenancy, isPathBasedMultiTenancy } = EnvSet.values;
|
||||||
|
|
||||||
// Mount admin tenant APIs and app
|
// Mount admin tenant APIs and app
|
||||||
if (id === adminTenantId) {
|
if (id === adminTenantId) {
|
||||||
// Mount `/me` APIs for admin tenant
|
// Mount `/me` APIs for admin tenant
|
||||||
|
@ -86,7 +96,7 @@ export default class Tenant implements TenantContext {
|
||||||
|
|
||||||
// Mount Admin Console when needed
|
// Mount Admin Console when needed
|
||||||
// Skip in domain-based multi-tenancy since Logto Cloud serves Admin Console in this case
|
// 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(koaConsoleRedirectProxy(queries));
|
||||||
app.use(
|
app.use(
|
||||||
mount(
|
mount(
|
||||||
|
@ -101,7 +111,7 @@ export default class Tenant implements TenantContext {
|
||||||
// while distinguishing "demo app from admin tenant" and "demo app from user tenant";
|
// 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
|
// 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.
|
// testing without signing out of the admin console.
|
||||||
if (id !== adminTenantId || EnvSet.values.isDomainBasedMultiTenancy) {
|
if (id !== adminTenantId || isDomainBasedMultiTenancy || isPathBasedMultiTenancy) {
|
||||||
// Mount demo app
|
// Mount demo app
|
||||||
app.use(
|
app.use(
|
||||||
mount(
|
mount(
|
||||||
|
|
|
@ -17,6 +17,7 @@ const isEndpointOf = (current: URL, endpoint: URL) => {
|
||||||
export const getTenantId = (url: URL) => {
|
export const getTenantId = (url: URL) => {
|
||||||
const {
|
const {
|
||||||
isDomainBasedMultiTenancy,
|
isDomainBasedMultiTenancy,
|
||||||
|
isPathBasedMultiTenancy,
|
||||||
isProduction,
|
isProduction,
|
||||||
isIntegrationTest,
|
isIntegrationTest,
|
||||||
developmentTenantId,
|
developmentTenantId,
|
||||||
|
@ -34,13 +35,17 @@ export const getTenantId = (url: URL) => {
|
||||||
return developmentTenantId;
|
return developmentTenantId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!isDomainBasedMultiTenancy && !isPathBasedMultiTenancy) {
|
||||||
!isDomainBasedMultiTenancy ||
|
|
||||||
(!urlSet.isLocalhostDisabled && isEndpointOf(url, urlSet.localhostUrl))
|
|
||||||
) {
|
|
||||||
return defaultTenantId;
|
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 toMatch = urlSet.endpoint.hostname.replace('*', '([^.]*)');
|
||||||
const matchedId = new RegExp(toMatch).exec(url.hostname)?.[1];
|
const matchedId = new RegExp(toMatch).exec(url.hostname)?.[1];
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue