mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
feat: create tenant for new users (#3255)
This commit is contained in:
parent
6246fc58ba
commit
44909140bf
16 changed files with 116 additions and 53 deletions
|
@ -19,11 +19,12 @@
|
|||
"start": "NODE_ENV=production node ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/cli": "workspace:*",
|
||||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@logto/shared": "workspace:*",
|
||||
"@silverhand/essentials": "2.2.0",
|
||||
"@withtyped/postgres": "^0.8.0",
|
||||
"@withtyped/postgres": "^0.8.1",
|
||||
"@withtyped/server": "^0.8.0",
|
||||
"chalk": "^5.0.0",
|
||||
"decamelize": "^6.0.0",
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import type { TenantInfo, TenantModel } from '@logto/schemas';
|
||||
import {
|
||||
generateOidcCookieKey,
|
||||
generateOidcPrivateKey,
|
||||
} from '@logto/cli/lib/commands/database/utils.js';
|
||||
import { generateStandardId } from '@logto/core-kit';
|
||||
import type { LogtoOidcConfigType, TenantInfo, TenantModel } from '@logto/schemas';
|
||||
import {
|
||||
LogtoOidcConfigKey,
|
||||
LogtoConfigs,
|
||||
SignInExperiences,
|
||||
createDefaultAdminConsoleConfig,
|
||||
|
@ -25,6 +30,13 @@ export const tenantInfoGuard: ZodType<TenantInfo> = z.object({
|
|||
indicator: z.string(),
|
||||
});
|
||||
|
||||
const oidcConfigBuilders: {
|
||||
[key in LogtoOidcConfigKey]: () => Promise<LogtoOidcConfigType[key]>;
|
||||
} = {
|
||||
[LogtoOidcConfigKey.CookieKeys]: async () => [generateOidcCookieKey()],
|
||||
[LogtoOidcConfigKey.PrivateKeys]: async () => [await generateOidcPrivateKey()],
|
||||
};
|
||||
|
||||
export class TenantsLibrary {
|
||||
constructor(public readonly queries: Queries) {}
|
||||
|
||||
|
@ -50,7 +62,7 @@ export class TenantsLibrary {
|
|||
const tenants = createTenantsQueries(transaction);
|
||||
const users = createUsersQueries(transaction);
|
||||
|
||||
// Start
|
||||
/* --- Start --- */
|
||||
await transaction.start();
|
||||
|
||||
// Init tenant
|
||||
|
@ -70,14 +82,24 @@ export class TenantsLibrary {
|
|||
|
||||
// Create initial configs
|
||||
await Promise.all([
|
||||
...Object.entries(oidcConfigBuilders).map(async ([key, build]) =>
|
||||
transaction.query(insertInto({ tenantId, key, value: await build() }, LogtoConfigs.table))
|
||||
),
|
||||
transaction.query(insertInto(createDefaultAdminConsoleConfig(tenantId), LogtoConfigs.table)),
|
||||
transaction.query(
|
||||
insertInto(createDefaultSignInExperience(tenantId), SignInExperiences.table)
|
||||
),
|
||||
]);
|
||||
|
||||
// End
|
||||
// Update Redirect URI for Admin Console
|
||||
await tenants.appendAdminConsoleRedirectUris(
|
||||
...['http://localhost:3003', 'https://cloud.logto.dev'].map(
|
||||
(endpoint) => new URL(`/${tenantModel.id}/callback`, endpoint)
|
||||
)
|
||||
);
|
||||
|
||||
await transaction.end();
|
||||
/* --- End --- */
|
||||
|
||||
return { id: tenantId, indicator: adminDataInAdminTenant.resource.indicator };
|
||||
}
|
||||
|
|
|
@ -3,13 +3,14 @@ import assert from 'node:assert';
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import type { AdminData, TenantModel } from '@logto/schemas';
|
||||
import {
|
||||
adminConsoleApplicationId,
|
||||
adminTenantId,
|
||||
getManagementApiResourceIndicator,
|
||||
PredefinedScope,
|
||||
CreateRolesScope,
|
||||
} from '@logto/schemas';
|
||||
import type { PostgreSql } from '@withtyped/postgres';
|
||||
import { dangerousRaw, id, sql } from '@withtyped/postgres';
|
||||
import { jsonb, dangerousRaw, id, sql } from '@withtyped/postgres';
|
||||
import type { Queryable } from '@withtyped/server';
|
||||
|
||||
import { insertInto } from '#src/utils/query.js';
|
||||
|
@ -72,5 +73,26 @@ export const createTenantsQueries = (client: Queryable<PostgreSql>) => {
|
|||
);
|
||||
};
|
||||
|
||||
return { getManagementApiLikeIndicatorsForUser, insertTenant, createTenantRole, insertAdminData };
|
||||
const appendAdminConsoleRedirectUris = async (...urls: URL[]) => {
|
||||
const metadataKey = id('oidc_client_metadata');
|
||||
|
||||
await client.query(sql`
|
||||
update applications
|
||||
set ${metadataKey} = jsonb_set(
|
||||
${metadataKey},
|
||||
'{redirectUris}',
|
||||
${metadataKey}->'redirectUris' || ${jsonb(urls.map(String))}
|
||||
)
|
||||
where id = ${adminConsoleApplicationId}
|
||||
and tenant_id = ${adminTenantId}
|
||||
`);
|
||||
};
|
||||
|
||||
return {
|
||||
getManagementApiLikeIndicatorsForUser,
|
||||
insertTenant,
|
||||
createTenantRole,
|
||||
insertAdminData,
|
||||
appendAdminConsoleRedirectUris,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { TenantInfo } from '@logto/schemas';
|
||||
import { useEffect } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { AppLoadingOffline } from '@/components/AppLoading/Offline';
|
||||
import Button from '@/components/Button';
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
|
@ -9,18 +10,27 @@ import * as styles from './index.module.scss';
|
|||
|
||||
type Props = {
|
||||
data: TenantInfo[];
|
||||
onAdd: (tenant: TenantInfo) => void;
|
||||
};
|
||||
|
||||
const Tenants = ({ data }: Props) => {
|
||||
const Tenants = ({ data, onAdd }: Props) => {
|
||||
const api = useCloudApi();
|
||||
|
||||
const createTenant = useCallback(async () => {
|
||||
onAdd(await api.post('api/tenants').json<TenantInfo>());
|
||||
}, [api, onAdd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length <= 1) {
|
||||
if (data[0]) {
|
||||
window.location.assign('/' + data[0].id);
|
||||
} else {
|
||||
// Todo: create tenant
|
||||
}
|
||||
if (data.length > 1) {
|
||||
return;
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (data[0]) {
|
||||
window.location.assign('/' + data[0].id);
|
||||
} else {
|
||||
void createTenant();
|
||||
}
|
||||
}, [createTenant, data]);
|
||||
|
||||
if (data.length > 1) {
|
||||
return (
|
||||
|
@ -31,6 +41,8 @@ const Tenants = ({ data }: Props) => {
|
|||
<Button title={<DangerousRaw>{id}</DangerousRaw>} />
|
||||
</a>
|
||||
))}
|
||||
<h3>Create a tenant</h3>
|
||||
<Button title={<DangerousRaw>Create New</DangerousRaw>} onClick={createTenant} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,14 @@ const Protected = () => {
|
|||
return <Redirect tenants={tenants} toTenantId={currentTenantId} />;
|
||||
}
|
||||
|
||||
return <Tenants data={tenants} />;
|
||||
return (
|
||||
<Tenants
|
||||
data={tenants}
|
||||
onAdd={(tenant) => {
|
||||
setTenants([...tenants, tenant]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <AppLoadingOffline />;
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
"../*/lib/",
|
||||
"../core/src/",
|
||||
"../core/node_modules/",
|
||||
".env"
|
||||
".env",
|
||||
"../../.env"
|
||||
],
|
||||
"ext": "json,js,jsx,ts,tsx",
|
||||
"delay": 500
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"@logto/schemas": "workspace:*",
|
||||
"@logto/shared": "workspace:*",
|
||||
"@silverhand/essentials": "2.2.0",
|
||||
"@withtyped/postgres": "^0.8.0",
|
||||
"@withtyped/postgres": "^0.8.1",
|
||||
"@withtyped/server": "^0.8.0",
|
||||
"chalk": "^5.0.0",
|
||||
"clean-deep": "^3.4.0",
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { tryThat } from '@logto/shared';
|
||||
import { assertEnv, getEnv, getEnvAsStringArray } from '@silverhand/essentials';
|
||||
import { assertEnv, getEnv, getEnvAsStringArray, yes } from '@silverhand/essentials';
|
||||
|
||||
import UrlSet from './UrlSet.js';
|
||||
import { isTrue } from './parameters.js';
|
||||
import { throwErrorWithDsnMessage } from './throw-errors.js';
|
||||
|
||||
export default class GlobalValues {
|
||||
public readonly isProduction = getEnv('NODE_ENV') === 'production';
|
||||
public readonly isTest = getEnv('NODE_ENV') === 'test';
|
||||
public readonly isIntegrationTest = isTrue(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 httpsKey = process.env.HTTPS_KEY_PATH;
|
||||
|
@ -25,8 +25,8 @@ export default class GlobalValues {
|
|||
public readonly developmentTenantId = getEnv('DEVELOPMENT_TENANT_ID');
|
||||
public readonly userDefaultRoleNames = getEnvAsStringArray('USER_DEFAULT_ROLE_NAMES');
|
||||
public readonly developmentUserId = getEnv('DEVELOPMENT_USER_ID');
|
||||
public readonly trustProxyHeader = isTrue(getEnv('TRUST_PROXY_HEADER'));
|
||||
public readonly ignoreConnectorVersionCheck = isTrue(getEnv('IGNORE_CONNECTOR_VERSION_CHECK'));
|
||||
public readonly trustProxyHeader = yes(getEnv('TRUST_PROXY_HEADER'));
|
||||
public readonly ignoreConnectorVersionCheck = yes(getEnv('IGNORE_CONNECTOR_VERSION_CHECK'));
|
||||
|
||||
public get dbUrl(): string {
|
||||
return this.databaseUrl;
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { deduplicate, getEnv, trySafe } from '@silverhand/essentials';
|
||||
|
||||
import { isTrue } from './parameters.js';
|
||||
import { deduplicate, getEnv, trySafe, yes } from '@silverhand/essentials';
|
||||
|
||||
export default class UrlSet {
|
||||
readonly #port = Number(getEnv(this.envPrefix + 'PORT') || this.defaultPort);
|
||||
readonly #endpoint = getEnv(this.envPrefix + 'ENDPOINT');
|
||||
|
||||
public readonly isLocalhostDisabled = isTrue(getEnv(this.envPrefix + 'DISABLE_LOCALHOST'));
|
||||
public readonly isLocalhostDisabled = yes(getEnv(this.envPrefix + 'DISABLE_LOCALHOST'));
|
||||
|
||||
constructor(
|
||||
public readonly isHttpsEnabled: boolean,
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import type { Nullable } from '@silverhand/essentials';
|
||||
|
||||
export const isTrue = (value?: Nullable<string>) =>
|
||||
// We need to leverage the native type guard
|
||||
// eslint-disable-next-line no-implicit-coercion
|
||||
!!value && ['1', 'true', 'y', 'yes', 'yep', 'yeah'].includes(value.toLowerCase());
|
|
@ -18,8 +18,11 @@ import { getConstantClientMetadata } from './utils.js';
|
|||
* as Admin Console is attached to the admin tenant in OSS and its endpoints are dynamic (from env variable).
|
||||
*/
|
||||
const transpileMetadata = (clientId: string, data: AllClientMetadata): AllClientMetadata => {
|
||||
const { adminUrlSet } = EnvSet.values;
|
||||
const urls = adminUrlSet.deduplicated().map((url) => appendPath(url, '/console').toString());
|
||||
const { adminUrlSet, cloudUrlSet } = EnvSet.values;
|
||||
const urls = [
|
||||
...adminUrlSet.deduplicated().map((url) => appendPath(url, '/console').toString()),
|
||||
...cloudUrlSet.deduplicated().map(String),
|
||||
];
|
||||
|
||||
if (clientId === adminConsoleApplicationId) {
|
||||
return {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { buildIdGenerator } from '@logto/core-kit';
|
||||
import { Resources, Scopes } from '@logto/schemas';
|
||||
import { tryThat } from '@logto/shared';
|
||||
import { yes } from '@silverhand/essentials';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
import { isTrue } from '#src/env-set/parameters.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
|
@ -54,7 +54,7 @@ export default function resourceRoutes<T extends AuthedRouter>(
|
|||
|
||||
if (disabled) {
|
||||
const resources = await findAllResources();
|
||||
ctx.body = isTrue(includeScopes) ? await attachScopesToResources(resources) : resources;
|
||||
ctx.body = yes(includeScopes) ? await attachScopesToResources(resources) : resources;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ export default function resourceRoutes<T extends AuthedRouter>(
|
|||
]);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
ctx.body = isTrue(includeScopes) ? await attachScopesToResources(resources) : resources;
|
||||
ctx.body = yes(includeScopes) ? await attachScopesToResources(resources) : resources;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { SearchJointMode, SearchMatchMode } from '@logto/schemas';
|
||||
import type { Nullable, Optional } from '@silverhand/essentials';
|
||||
import { conditionalString } from '@silverhand/essentials';
|
||||
import { yes, conditionalString } from '@silverhand/essentials';
|
||||
import { sql } from 'slonik';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
||||
import { isTrue } from '#src/env-set/parameters.js';
|
||||
|
||||
import assertThat from './assert-that.js';
|
||||
import { isEnum } from './type.js';
|
||||
|
||||
|
@ -83,7 +81,7 @@ const getSearchMetadata = (searchParameters: URLSearchParams, allowedFields?: st
|
|||
const matchMode = new Map<Optional<string>, SearchMatchMode>();
|
||||
const matchValues = new Map<Optional<string>, string[]>();
|
||||
const joint = getJointMode(searchParameters.get('joint') ?? searchParameters.get('jointMode'));
|
||||
const isCaseSensitive = isTrue(searchParameters.get('isCaseSensitive') ?? 'false');
|
||||
const isCaseSensitive = yes(searchParameters.get('isCaseSensitive') ?? 'false');
|
||||
|
||||
// Parse the following values and return:
|
||||
// 1. Search modes per field, if available
|
||||
|
|
|
@ -24,14 +24,16 @@ export const getTenantId = (url: URL) => {
|
|||
adminUrlSet,
|
||||
} = EnvSet.values;
|
||||
|
||||
if ((!isProduction || isIntegrationTest) && developmentTenantId) {
|
||||
return developmentTenantId;
|
||||
}
|
||||
|
||||
if (adminUrlSet.deduplicated().some((endpoint) => isEndpointOf(url, endpoint))) {
|
||||
return adminTenantId;
|
||||
}
|
||||
|
||||
if ((!isProduction || isIntegrationTest) && developmentTenantId) {
|
||||
console.log(`Found dev tenant ID ${developmentTenantId}.`);
|
||||
|
||||
return developmentTenantId;
|
||||
}
|
||||
|
||||
if (
|
||||
!isDomainBasedMultiTenancy ||
|
||||
(!urlSet.isLocalhostDisabled && isEndpointOf(url, urlSet.localhostUrl))
|
||||
|
|
|
@ -6,6 +6,7 @@ import { AdminConsoleConfigKey } from '../types/index.js';
|
|||
export const createDefaultAdminConsoleConfig = (
|
||||
forTenantId: string
|
||||
): Readonly<{
|
||||
tenantId: string;
|
||||
key: AdminConsoleConfigKey;
|
||||
value: AdminConsoleData;
|
||||
}> =>
|
||||
|
|
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
|
@ -106,6 +106,7 @@ importers:
|
|||
|
||||
packages/cloud:
|
||||
specifiers:
|
||||
'@logto/cli': workspace:*
|
||||
'@logto/core-kit': workspace:*
|
||||
'@logto/schemas': workspace:*
|
||||
'@logto/shared': workspace:*
|
||||
|
@ -115,7 +116,7 @@ importers:
|
|||
'@types/http-proxy': ^1.17.9
|
||||
'@types/mime-types': ^2.1.1
|
||||
'@types/node': ^18.11.18
|
||||
'@withtyped/postgres': ^0.8.0
|
||||
'@withtyped/postgres': ^0.8.1
|
||||
'@withtyped/server': ^0.8.0
|
||||
chalk: ^5.0.0
|
||||
decamelize: ^6.0.0
|
||||
|
@ -131,11 +132,12 @@ importers:
|
|||
typescript: ^4.9.4
|
||||
zod: ^3.20.2
|
||||
dependencies:
|
||||
'@logto/cli': link:../cli
|
||||
'@logto/core-kit': link:../toolkit/core-kit
|
||||
'@logto/schemas': link:../schemas
|
||||
'@logto/shared': link:../shared
|
||||
'@silverhand/essentials': 2.2.0
|
||||
'@withtyped/postgres': 0.8.0_@withtyped+server@0.8.0
|
||||
'@withtyped/postgres': 0.8.1_@withtyped+server@0.8.0
|
||||
'@withtyped/server': 0.8.0
|
||||
chalk: 5.1.2
|
||||
decamelize: 6.0.0
|
||||
|
@ -333,7 +335,7 @@ importers:
|
|||
'@types/semver': ^7.3.12
|
||||
'@types/sinon': ^10.0.13
|
||||
'@types/supertest': ^2.0.11
|
||||
'@withtyped/postgres': ^0.8.0
|
||||
'@withtyped/postgres': ^0.8.1
|
||||
'@withtyped/server': ^0.8.0
|
||||
chalk: ^5.0.0
|
||||
clean-deep: ^3.4.0
|
||||
|
@ -393,7 +395,7 @@ importers:
|
|||
'@logto/schemas': link:../schemas
|
||||
'@logto/shared': link:../shared
|
||||
'@silverhand/essentials': 2.2.0
|
||||
'@withtyped/postgres': 0.8.0_@withtyped+server@0.8.0
|
||||
'@withtyped/postgres': 0.8.1_@withtyped+server@0.8.0
|
||||
'@withtyped/server': 0.8.0
|
||||
chalk: 5.1.2
|
||||
clean-deep: 3.4.0
|
||||
|
@ -4563,8 +4565,8 @@ packages:
|
|||
eslint-visitor-keys: 3.3.0
|
||||
dev: true
|
||||
|
||||
/@withtyped/postgres/0.8.0_@withtyped+server@0.8.0:
|
||||
resolution: {integrity: sha512-cyhHR1lEV1cs0yDfmHvqR4R52kxgI/JCDd2fizOCzgIE6Z9g3GjYjeVtTl/fTaoDz/QDaXKwfkuxd0N7HSY7uw==}
|
||||
/@withtyped/postgres/0.8.1_@withtyped+server@0.8.0:
|
||||
resolution: {integrity: sha512-BkX1SPDV8bZFn1LEI6jzTes+y/4BGuPEBOi8p6jH9ZBySTuaW0/JqgEb1aKk5GtJ0aFGXhgq4Fk+JkLOc6zehA==}
|
||||
peerDependencies:
|
||||
'@withtyped/server': ^0.8.0
|
||||
dependencies:
|
||||
|
|
Loading…
Add table
Reference in a new issue