diff --git a/packages/cloud/package.json b/packages/cloud/package.json index d9835cd7d..3191ef7c4 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -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", diff --git a/packages/cloud/src/libraries/tenants.ts b/packages/cloud/src/libraries/tenants.ts index 8953117b0..3f784f44b 100644 --- a/packages/cloud/src/libraries/tenants.ts +++ b/packages/cloud/src/libraries/tenants.ts @@ -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 = z.object({ indicator: z.string(), }); +const oidcConfigBuilders: { + [key in LogtoOidcConfigKey]: () => Promise; +} = { + [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 }; } diff --git a/packages/cloud/src/queries/tenants.ts b/packages/cloud/src/queries/tenants.ts index ea379dfaa..75d79262e 100644 --- a/packages/cloud/src/queries/tenants.ts +++ b/packages/cloud/src/queries/tenants.ts @@ -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) => { ); }; - 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, + }; }; diff --git a/packages/console/src/cloud/pages/Main/Tenants.tsx b/packages/console/src/cloud/pages/Main/Tenants.tsx index 9d52a43b2..67eaa69f7 100644 --- a/packages/console/src/cloud/pages/Main/Tenants.tsx +++ b/packages/console/src/cloud/pages/Main/Tenants.tsx @@ -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()); + }, [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) => {