mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor: seed data for multi-tenancy
This commit is contained in:
parent
a6d4494c0e
commit
9775db7af8
12 changed files with 190 additions and 27 deletions
|
@ -11,6 +11,7 @@ import {
|
|||
createDefaultSignInExperience,
|
||||
createAdminTenantSignInExperience,
|
||||
createDefaultAdminConsoleApplication,
|
||||
createCloudApi,
|
||||
} from '@logto/schemas';
|
||||
import { Hooks, Tenants } from '@logto/schemas/models';
|
||||
import type { DatabaseTransactionConnection } from 'slonik';
|
||||
|
@ -121,13 +122,16 @@ export const seedTables = async (
|
|||
await createTenant(connection, adminTenantId);
|
||||
await seedOidcConfigs(connection, adminTenantId);
|
||||
await seedAdminData(connection, createAdminDataInAdminTenant(defaultTenantId));
|
||||
await seedAdminData(connection, createAdminDataInAdminTenant(adminTenantId));
|
||||
await seedAdminData(connection, createMeApiInAdminTenant());
|
||||
await seedAdminData(connection, createCloudApi());
|
||||
|
||||
await Promise.all([
|
||||
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
|
||||
connection.query(
|
||||
insertInto(createDefaultSignInExperience(defaultTenantId), 'sign_in_experiences')
|
||||
),
|
||||
connection.query(insertInto(createDefaultAdminConsoleConfig(adminTenantId), 'logto_configs')),
|
||||
connection.query(insertInto(createAdminTenantSignInExperience(), 'sign_in_experiences')),
|
||||
connection.query(insertInto(createDefaultAdminConsoleApplication(), 'applications')),
|
||||
updateDatabaseTimestamp(connection, latestTimestamp),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import type { TenantModel, AdminData, UpdateAdminData } from '@logto/schemas';
|
||||
import { CreateRolesScope } from '@logto/schemas';
|
||||
import type { TenantModel, AdminData } from '@logto/schemas';
|
||||
import { createTenantMetadata } from '@logto/shared';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
|
@ -23,7 +23,10 @@ export const createTenant = async (pool: CommonQueryMethods, tenantId: string) =
|
|||
`);
|
||||
};
|
||||
|
||||
export const seedAdminData = async (pool: CommonQueryMethods, data: AdminData) => {
|
||||
export const seedAdminData = async (
|
||||
pool: CommonQueryMethods,
|
||||
data: AdminData | UpdateAdminData
|
||||
) => {
|
||||
const { resource, scope, role } = data;
|
||||
|
||||
assert(
|
||||
|
@ -31,14 +34,32 @@ export const seedAdminData = async (pool: CommonQueryMethods, data: AdminData) =
|
|||
new Error('All data should have the same tenant ID')
|
||||
);
|
||||
|
||||
const processRole = async () => {
|
||||
if ('id' in role) {
|
||||
await pool.query(insertInto(role, 'roles'));
|
||||
|
||||
return role.id;
|
||||
}
|
||||
|
||||
// Query by role name for existing roles
|
||||
const { id } = await pool.one<{ id: string }>(sql`
|
||||
select id from roles
|
||||
where name=${role.name}
|
||||
and tenant_id=${String(role.tenantId)}
|
||||
`);
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
await pool.query(insertInto(resource, 'resources'));
|
||||
await pool.query(insertInto(scope, 'scopes'));
|
||||
await pool.query(insertInto(role, 'roles'));
|
||||
|
||||
const roleId = await processRole();
|
||||
await pool.query(
|
||||
insertInto(
|
||||
{
|
||||
id: generateStandardId(),
|
||||
roleId: role.id,
|
||||
roleId,
|
||||
scopeId: scope.id,
|
||||
tenantId: resource.tenantId,
|
||||
} satisfies CreateRolesScope,
|
||||
|
|
|
@ -76,7 +76,8 @@ export default function withAuth<InputContext extends RequestContext>({
|
|||
audience,
|
||||
}),
|
||||
(error) => {
|
||||
throw error;
|
||||
console.error(error);
|
||||
throw new RequestError('JWT verification failed.', 401);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -80,6 +80,11 @@ export default class GlobalValues {
|
|||
public readonly isPathBasedMultiTenancy =
|
||||
!this.isDomainBasedMultiTenancy && yes(getEnv('PATH_BASED_MULTI_TENANCY'));
|
||||
|
||||
/** Alias for `isDomainBasedMultiTenancy || isPathBasedMultiTenancy`. */
|
||||
public get isMultiTenancy(): boolean {
|
||||
return this.isDomainBasedMultiTenancy || this.isPathBasedMultiTenancy;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
public readonly databaseUrl = tryThat(() => assertEnv('DB_URL'), throwErrorWithDsnMessage);
|
||||
public readonly developmentTenantId = getEnv('DEVELOPMENT_TENANT_ID');
|
||||
|
|
|
@ -27,13 +27,9 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{
|
|||
keys: JWK[];
|
||||
issuer: string[];
|
||||
}> => {
|
||||
const { isDomainBasedMultiTenancy, isPathBasedMultiTenancy, adminUrlSet } = EnvSet.values;
|
||||
const { isMultiTenancy, adminUrlSet } = EnvSet.values;
|
||||
|
||||
if (
|
||||
!isDomainBasedMultiTenancy &&
|
||||
!isPathBasedMultiTenancy &&
|
||||
adminUrlSet.deduplicated().length === 0
|
||||
) {
|
||||
if (!isMultiTenancy && adminUrlSet.deduplicated().length === 0) {
|
||||
return { keys: [], issuer: [] };
|
||||
}
|
||||
|
||||
|
@ -52,9 +48,7 @@ export const getAdminTenantTokenValidationSet = async (): Promise<{
|
|||
keys: await Promise.all(publicKeys.map(async (key) => exportJWK(key))),
|
||||
issuer: [
|
||||
appendPath(
|
||||
isDomainBasedMultiTenancy || isPathBasedMultiTenancy
|
||||
? getTenantEndpoint(adminTenantId, EnvSet.values)
|
||||
: adminUrlSet.endpoint,
|
||||
isMultiTenancy ? getTenantEndpoint(adminTenantId, EnvSet.values) : adminUrlSet.endpoint,
|
||||
'/oidc'
|
||||
).toString(),
|
||||
],
|
||||
|
|
|
@ -87,7 +87,7 @@ export default class Tenant implements TenantContext {
|
|||
// Mount APIs
|
||||
app.use(mount('/api', initApis(tenantContext)));
|
||||
|
||||
const { isDomainBasedMultiTenancy, isPathBasedMultiTenancy } = EnvSet.values;
|
||||
const { isMultiTenancy } = EnvSet.values;
|
||||
|
||||
// Mount admin tenant APIs and app
|
||||
if (id === adminTenantId) {
|
||||
|
@ -95,8 +95,8 @@ export default class Tenant implements TenantContext {
|
|||
app.use(mount('/me', initMeApis(tenantContext)));
|
||||
|
||||
// Mount Admin Console when needed
|
||||
// Skip in domain-based multi-tenancy since Logto Cloud serves Admin Console in this case
|
||||
if (!isDomainBasedMultiTenancy && !isPathBasedMultiTenancy) {
|
||||
// Skip in multi-tenancy mode since Logto Cloud serves Admin Console in this case
|
||||
if (!isMultiTenancy) {
|
||||
app.use(koaConsoleRedirectProxy(queries));
|
||||
app.use(
|
||||
mount(
|
||||
|
@ -111,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 || isDomainBasedMultiTenancy || isPathBasedMultiTenancy) {
|
||||
if (id !== adminTenantId || isMultiTenancy) {
|
||||
// Mount demo app
|
||||
app.use(
|
||||
mount(
|
||||
|
|
|
@ -43,7 +43,7 @@ const matchPathBasedTenantId = (urlSet: UrlSet, url: URL) => {
|
|||
|
||||
export const getTenantId = (url: URL) => {
|
||||
const {
|
||||
isDomainBasedMultiTenancy,
|
||||
isMultiTenancy,
|
||||
isPathBasedMultiTenancy,
|
||||
isProduction,
|
||||
isIntegrationTest,
|
||||
|
@ -62,7 +62,7 @@ export const getTenantId = (url: URL) => {
|
|||
return developmentTenantId;
|
||||
}
|
||||
|
||||
if (!isDomainBasedMultiTenancy && !isPathBasedMultiTenancy) {
|
||||
if (!isMultiTenancy) {
|
||||
return defaultTenantId;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import type { CommonQueryMethods } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||
|
||||
const adminTenantId = 'admin';
|
||||
|
||||
const addApiData = async (pool: CommonQueryMethods) => {
|
||||
const adminApi = {
|
||||
resourceId: generateStandardId(),
|
||||
scopeId: generateStandardId(),
|
||||
};
|
||||
const cloudApi = {
|
||||
resourceId: generateStandardId(),
|
||||
scopeId: generateStandardId(),
|
||||
};
|
||||
|
||||
await pool.query(sql`
|
||||
insert into resources (tenant_id, id, indicator, name)
|
||||
values (
|
||||
${adminTenantId},
|
||||
${adminApi.resourceId},
|
||||
'https://admin.logto.app/api',
|
||||
'Logto Management API for tenant admin'
|
||||
), (
|
||||
${adminTenantId},
|
||||
${cloudApi.resourceId},
|
||||
'https://cloud.logto.io/api',
|
||||
'Logto Management API for tenant admin'
|
||||
);
|
||||
`);
|
||||
await pool.query(sql`
|
||||
insert into scopes (tenant_id, id, name, description, resource_id)
|
||||
values (
|
||||
${adminTenantId},
|
||||
${adminApi.scopeId},
|
||||
'all',
|
||||
'Default scope for Management API, allows all permissions.',
|
||||
${adminApi.scopeId}
|
||||
), (
|
||||
${adminTenantId},
|
||||
${cloudApi.scopeId},
|
||||
'create:tenant',
|
||||
'Allow creating new tenants.',
|
||||
${cloudApi.scopeId}
|
||||
);
|
||||
`);
|
||||
|
||||
const { id: roleId } = await pool.one<{ id: string }>(sql`
|
||||
select id from roles
|
||||
where tenant_id = ${adminTenantId}
|
||||
and name = 'user'
|
||||
`);
|
||||
|
||||
await pool.query(sql`
|
||||
insert into roles_scopes (tenant_id, id, role_id, scope_id)
|
||||
values (
|
||||
${adminTenantId},
|
||||
${generateStandardId()},
|
||||
${roleId},
|
||||
${adminApi.scopeId}
|
||||
), (
|
||||
${adminTenantId},
|
||||
${generateStandardId()},
|
||||
${roleId},
|
||||
${cloudApi.scopeId}
|
||||
);
|
||||
`);
|
||||
};
|
||||
|
||||
const alteration: AlterationScript = {
|
||||
up: async (pool) => {
|
||||
await addApiData(pool);
|
||||
await pool.query(sql`
|
||||
insert into logto_configs (tenant_id, key, value)
|
||||
values (
|
||||
${adminTenantId},
|
||||
'adminConsole',
|
||||
${sql.jsonb({
|
||||
language: 'en',
|
||||
appearanceMode: 'system',
|
||||
livePreviewChecked: false,
|
||||
applicationCreated: false,
|
||||
signInExperienceCustomized: false,
|
||||
passwordlessConfigured: false,
|
||||
selfHostingChecked: false,
|
||||
communityChecked: false,
|
||||
m2mApplicationCreated: false,
|
||||
})}
|
||||
);
|
||||
`);
|
||||
},
|
||||
down: async (pool) => {
|
||||
await pool.query(sql`
|
||||
delete from applications
|
||||
where tenant_id = 'admin'
|
||||
and id = 'admin-console';
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -1,10 +1,3 @@
|
|||
/** 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:
|
||||
*
|
||||
|
|
36
packages/schemas/src/seeds/cloud-api.ts
Normal file
36
packages/schemas/src/seeds/cloud-api.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
|
||||
import { UserRole } from '../types/index.js';
|
||||
import type { UpdateAdminData } from './management-api.js';
|
||||
import { adminTenantId } from './tenant.js';
|
||||
|
||||
/** 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',
|
||||
}
|
||||
|
||||
export const createCloudApi = (): Readonly<UpdateAdminData> => {
|
||||
const resourceId = generateStandardId();
|
||||
|
||||
return Object.freeze({
|
||||
resource: {
|
||||
tenantId: adminTenantId,
|
||||
id: resourceId,
|
||||
indicator: cloudApiIndicator,
|
||||
name: `Logto Cloud API`,
|
||||
},
|
||||
scope: {
|
||||
tenantId: adminTenantId,
|
||||
id: generateStandardId(),
|
||||
name: CloudScope.CreateTenant,
|
||||
description: 'Allow creating new tenants.',
|
||||
resourceId,
|
||||
},
|
||||
role: {
|
||||
tenantId: adminTenantId,
|
||||
name: UserRole.User,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
export * from './application.js';
|
||||
export * from './cloud-api.js';
|
||||
export * from './management-api.js';
|
||||
export * from './logto-config.js';
|
||||
export * from './sign-in-experience.js';
|
||||
|
|
|
@ -10,6 +10,11 @@ export type AdminData = {
|
|||
role: CreateRole;
|
||||
};
|
||||
|
||||
export type UpdateAdminData = Omit<AdminData, 'role'> & {
|
||||
/** Attach to an existing role instead of creating one. */
|
||||
role: Pick<CreateRole, 'tenantId' | 'name'>;
|
||||
};
|
||||
|
||||
// Consider remove the dependency of IDs
|
||||
const defaultResourceId = 'management-api';
|
||||
const defaultScopeAllId = 'management-api-all';
|
||||
|
|
Loading…
Reference in a new issue