0
Fork 0
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:
Gao Sun 2023-03-02 13:20:21 +08:00
parent 2a7c623240
commit d29432f45d
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
5 changed files with 46 additions and 13 deletions

View file

@ -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');

View file

@ -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;
}
};

View file

@ -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'

View file

@ -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(

View file

@ -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];