diff --git a/packages/cli/src/commands/database/seed/tables.ts b/packages/cli/src/commands/database/seed/tables.ts index 23cc77cb1..6338175be 100644 --- a/packages/cli/src/commands/database/seed/tables.ts +++ b/packages/cli/src/commands/database/seed/tables.ts @@ -4,7 +4,6 @@ import path from 'path'; import { defaultSignInExperience, createDefaultAdminConsoleConfig, - createDemoAppApplication, defaultTenantId, adminTenantId, defaultManagementApi, @@ -125,8 +124,6 @@ export const seedTables = async ( await Promise.all([ connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')), connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')), - // TODO: @gao remove demo app - connection.query(insertInto(createDemoAppApplication(defaultTenantId), 'applications')), updateDatabaseTimestamp(connection, latestTimestamp), ]); }; diff --git a/packages/console/src/pages/GetStarted/hook.ts b/packages/console/src/pages/GetStarted/hook.ts index b9912aa17..1692f7df1 100644 --- a/packages/console/src/pages/GetStarted/hook.ts +++ b/packages/console/src/pages/GetStarted/hook.ts @@ -1,9 +1,7 @@ import type { AdminConsoleKey } from '@logto/phrases'; -import type { Application } from '@logto/schemas'; -import { AppearanceMode, demoAppApplicationId } from '@logto/schemas'; +import { AppearanceMode } from '@logto/schemas'; import { useContext, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; -import useSWR from 'swr'; import CheckDemoDark from '@/assets/images/check-demo-dark.svg'; import CheckDemo from '@/assets/images/check-demo.svg'; @@ -19,7 +17,6 @@ import SocialDark from '@/assets/images/social-dark.svg'; import Social from '@/assets/images/social.svg'; import { ConnectorsTabs } from '@/consts/page-tabs'; import { AppEndpointsContext } from '@/containers/AppEndpointsProvider'; -import { RequestError } from '@/hooks/use-api'; import useConfigs from '@/hooks/use-configs'; import useDocumentationUrl from '@/hooks/use-documentation-url'; import { useTheme } from '@/hooks/use-theme'; @@ -41,21 +38,7 @@ const useGetStartedMetadata = () => { const { userEndpoint } = useContext(AppEndpointsContext); const theme = useTheme(); const isLightMode = theme === AppearanceMode.LightMode; - const { data: demoApp, error } = useSWR( - `api/applications/${demoAppApplicationId}`, - { - shouldRetryOnError: (error: unknown) => { - if (error instanceof RequestError) { - return error.status !== 404; - } - - return true; - }, - } - ); const navigate = useNavigate(); - const isLoadingDemoApp = !demoApp && !error; - const hideDemo = error?.status === 404; const data = useMemo(() => { const metadataItems: GetStartedMetadata[] = [ @@ -66,7 +49,6 @@ const useGetStartedMetadata = () => { icon: isLightMode ? CheckDemo : CheckDemoDark, buttonText: 'general.check_out', isComplete: configs?.demoChecked, - isHidden: hideDemo, onClick: async () => { void updateConfigs({ demoChecked: true }); window.open(new URL('/demo-app', userEndpoint), '_blank'); @@ -142,7 +124,6 @@ const useGetStartedMetadata = () => { configs?.passwordlessConfigured, configs?.socialSignInConfigured, configs?.furtherReadingsChecked, - hideDemo, updateConfigs, userEndpoint, navigate, @@ -153,7 +134,7 @@ const useGetStartedMetadata = () => { data, completedCount: data.filter(({ isComplete }) => isComplete).length, totalCount: data.length, - isLoading: isLoadingDemoApp, + isLoading: false, }; }; diff --git a/packages/core/src/env-set/utils.ts b/packages/core/src/env-set/utils.ts index cb8834b30..9e39496a1 100644 --- a/packages/core/src/env-set/utils.ts +++ b/packages/core/src/env-set/utils.ts @@ -1,5 +1,6 @@ import { adminTenantId } from '@logto/schemas'; -import { trySafe } from '@silverhand/essentials'; +import type { Optional } from '@silverhand/essentials'; +import { deduplicate, trySafe } from '@silverhand/essentials'; import type GlobalValues from './GlobalValues.js'; @@ -23,3 +24,29 @@ export const getTenantEndpoint = ( return tenantUrl; }; + +export const getTenantLocalhost = ( + id: string, + { urlSet, adminUrlSet, isDomainBasedMultiTenancy }: GlobalValues +): Optional => { + const adminUrl = trySafe(() => adminUrlSet.localhostUrl); + + if (adminUrl && id === adminTenantId) { + return adminUrl; + } + + if (!isDomainBasedMultiTenancy) { + return trySafe(() => urlSet.localhostUrl); + } +}; + +export const getTenantUrls = (id: string, globalValues: GlobalValues): URL[] => { + const endpoint = getTenantEndpoint(id, globalValues); + const localhost = getTenantLocalhost(id, globalValues); + + return deduplicate( + [endpoint.toString(), localhost?.toString()].filter( + (value): value is string => typeof value === 'string' + ) + ).map((element) => new URL(element)); +}; diff --git a/packages/core/src/middleware/koa-check-demo-app.ts b/packages/core/src/middleware/koa-check-demo-app.ts deleted file mode 100644 index cfb1a4199..000000000 --- a/packages/core/src/middleware/koa-check-demo-app.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { demoAppApplicationId } from '@logto/schemas'; -import type { MiddlewareType } from 'koa'; - -import type Queries from '#src/tenants/Queries.js'; - -export default function koaCheckDemoApp( - queries: Queries -): MiddlewareType { - const { findApplicationById } = queries.applications; - - return async (ctx, next) => { - try { - await findApplicationById(demoAppApplicationId); - - await next(); - - return; - } catch { - ctx.throw(404); - } - }; -} diff --git a/packages/core/src/oidc/adapter.ts b/packages/core/src/oidc/adapter.ts index df9b28744..ee079f702 100644 --- a/packages/core/src/oidc/adapter.ts +++ b/packages/core/src/oidc/adapter.ts @@ -1,13 +1,13 @@ -import type { CreateApplication, OidcClientMetadata } from '@logto/schemas'; +import type { CreateApplication } from '@logto/schemas'; import { ApplicationType, adminConsoleApplicationId, demoAppApplicationId } from '@logto/schemas'; import { tryThat } from '@logto/shared'; -import { deduplicate } from '@silverhand/essentials'; import { addSeconds } from 'date-fns'; import type { AdapterFactory, AllClientMetadata } from 'oidc-provider'; import { errors } from 'oidc-provider'; import snakecaseKeys from 'snakecase-keys'; -import { EnvSet, UserApps } from '#src/env-set/index.js'; +import { EnvSet } from '#src/env-set/index.js'; +import { getTenantUrls } from '#src/env-set/utils.js'; import type Queries from '#src/tenants/Queries.js'; import { appendPath } from '#src/utils/url.js'; @@ -28,18 +28,18 @@ const buildAdminConsoleClientMetadata = (envSet: EnvSet): AllClientMetadata => { }; }; -const buildDemoAppUris = ( - oidcClientMetadata: OidcClientMetadata -): Pick => { - const { urlSet } = EnvSet.values; - const urls = urlSet.deduplicated().map((url) => appendPath(url, UserApps.DemoApp).toString()); +const buildDemoAppClientMetadata = (envSet: EnvSet): AllClientMetadata => { + const urls = getTenantUrls(envSet.tenantId, EnvSet.values).map((url) => + appendPath(url, '/demo-app').toString() + ); - const data = { - redirectUris: deduplicate([...urls, ...oidcClientMetadata.redirectUris]), - postLogoutRedirectUris: deduplicate([...urls, ...oidcClientMetadata.postLogoutRedirectUris]), + return { + ...getConstantClientMetadata(envSet, ApplicationType.SPA), + client_id: demoAppApplicationId, + client_name: 'Demo App', + redirect_uris: urls, + post_logout_redirect_uris: urls, }; - - return data; }; export default function postgresAdapter( @@ -76,8 +76,6 @@ export default function postgresAdapter( client_name, ...getConstantClientMetadata(envSet, type), ...snakecaseKeys(oidcClientMetadata), - ...(client_id === demoAppApplicationId && - snakecaseKeys(buildDemoAppUris(oidcClientMetadata))), // `node-oidc-provider` won't camelCase custom parameter keys, so we need to keep the keys camelCased ...customClientMetadata, }); @@ -90,6 +88,10 @@ export default function postgresAdapter( return buildAdminConsoleClientMetadata(envSet); } + if (id === demoAppApplicationId) { + return buildDemoAppClientMetadata(envSet); + } + return transpileClient( await tryThat(findApplicationById(id), new errors.InvalidClient(`invalid client ${id}`)) ); diff --git a/packages/core/src/tenants/Tenant.test.ts b/packages/core/src/tenants/Tenant.test.ts index d7f259940..97a5c9db6 100644 --- a/packages/core/src/tenants/Tenant.test.ts +++ b/packages/core/src/tenants/Tenant.test.ts @@ -25,7 +25,6 @@ const middlewareList = Object.freeze( 'oidc-error-handler', 'slonik-error-handler', 'spa-proxy', - 'check-demo-app', 'console-redirect-proxy', ].map((name) => [name, buildMockMiddleware(name)] as const) ); diff --git a/packages/core/src/tenants/Tenant.ts b/packages/core/src/tenants/Tenant.ts index cb5cb80a6..23f6264ee 100644 --- a/packages/core/src/tenants/Tenant.ts +++ b/packages/core/src/tenants/Tenant.ts @@ -7,7 +7,6 @@ import mount from 'koa-mount'; import type Provider from 'oidc-provider'; import { AdminApps, EnvSet, UserApps } from '#src/env-set/index.js'; -import koaCheckDemoApp from '#src/middleware/koa-check-demo-app.js'; import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js'; import koaConsoleRedirectProxy from '#src/middleware/koa-console-redirect-proxy.js'; import koaErrorHandler from '#src/middleware/koa-error-handler.js'; @@ -97,10 +96,7 @@ export default class Tenant implements TenantContext { app.use( mount( '/' + UserApps.DemoApp, - compose([ - koaCheckDemoApp(this.queries), - koaSpaProxy(mountedApps, UserApps.DemoApp, 5003, UserApps.DemoApp), - ]) + koaSpaProxy(mountedApps, UserApps.DemoApp, 5003, UserApps.DemoApp) ) ); } diff --git a/packages/integration-tests/src/tests/api/application.test.ts b/packages/integration-tests/src/tests/api/application.test.ts index 3d2b5d979..84b0d7e89 100644 --- a/packages/integration-tests/src/tests/api/application.test.ts +++ b/packages/integration-tests/src/tests/api/application.test.ts @@ -1,4 +1,4 @@ -import { ApplicationType, demoAppApplicationId } from '@logto/schemas'; +import { ApplicationType } from '@logto/schemas'; import { HTTPError } from 'got'; import { @@ -9,12 +9,6 @@ import { } from '#src/api/index.js'; describe('admin console application', () => { - it('should get demo app details successfully', async () => { - const demoApp = await getApplication(demoAppApplicationId); - - expect(demoApp.id).toBe(demoAppApplicationId); - }); - it('should create application successfully', async () => { const applicationName = 'test-create-app'; const applicationType = ApplicationType.SPA; diff --git a/packages/schemas/alterations/next-1676906977-remove-demo-app.ts b/packages/schemas/alterations/next-1676906977-remove-demo-app.ts new file mode 100644 index 000000000..39573e24c --- /dev/null +++ b/packages/schemas/alterations/next-1676906977-remove-demo-app.ts @@ -0,0 +1,52 @@ +import { generateStandardId } from '@logto/core-kit'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const defaultTenantId = 'default'; + +const alteration: AlterationScript = { + up: async (pool) => { + const isCi = process.env.CI; + const { confirm } = await inquirer.prompt<{ confirm: boolean }>({ + type: 'confirm', + name: 'confirm', + message: String( + chalk.bold(chalk.yellow('***CAUTION***')) + + '\n' + + 'The application `demo-app` will be removed from your database.\n' + + 'Usually this is harmless since the demo app will be still functional with predefined data.\n' + + 'Are you sure to continue?' + ), + default: false, + when: !isCi, + }); + + if (!isCi && !confirm) { + throw new Error('User cancelled alteration.'); + } + + await pool.query(sql` + delete from applications where id = 'demo-app'; + `); + }, + down: async (pool) => { + await pool.query(sql` + insert into applications + (tenant_id, id, secret, name, description, type, oidc_client_metadata) + values ( + 'default', + 'demo-app', + ${generateStandardId()}, + 'Demo App', + 'Logto demo app.', + 'SPA', + '{ "redirectUris": [], "postLogoutRedirectUris": [] }'::jsonb + ) + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 6f25285af..a2072c04a 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -48,6 +48,7 @@ "@types/node": "^18.11.18", "@types/pluralize": "^0.0.29", "camelcase": "^7.0.0", + "chalk": "^5.0.0", "eslint": "^8.34.0", "jest": "^29.1.2", "lint-staged": "^13.0.0", diff --git a/packages/schemas/src/seeds/application.ts b/packages/schemas/src/seeds/application.ts index 6049689c3..2f2c5aa41 100644 --- a/packages/schemas/src/seeds/application.ts +++ b/packages/schemas/src/seeds/application.ts @@ -1,8 +1,3 @@ -import { generateStandardId } from '@logto/core-kit'; - -import type { CreateApplication } from '../db-entries/index.js'; -import { ApplicationType } from '../db-entries/index.js'; - /** * The fixed application ID for Admin Console. * @@ -11,14 +6,3 @@ import { ApplicationType } from '../db-entries/index.js'; export const adminConsoleApplicationId = 'admin-console'; export const demoAppApplicationId = 'demo-app'; - -/** @deprecated Demo app database entity will be removed soon. */ -export const createDemoAppApplication = (forTenantId: string): Readonly => ({ - tenantId: forTenantId, - id: demoAppApplicationId, - secret: generateStandardId(), - name: 'Demo App', - description: 'Logto demo app.', - type: ApplicationType.SPA, - oidcClientMetadata: { redirectUris: [], postLogoutRedirectUris: [] }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9995a3e30..a3545b436 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -653,6 +653,7 @@ importers: '@types/pluralize': ^0.0.29 '@withtyped/server': ^0.8.0 camelcase: ^7.0.0 + chalk: ^5.0.0 eslint: ^8.34.0 jest: ^29.1.2 lint-staged: ^13.0.0 @@ -680,6 +681,7 @@ importers: '@types/node': 18.11.18 '@types/pluralize': 0.0.29 camelcase: 7.0.0 + chalk: 5.1.2 eslint: 8.34.0 jest: 29.1.2_@types+node@18.11.18 lint-staged: 13.0.0 @@ -5283,7 +5285,6 @@ packages: /chalk/5.1.2: resolution: {integrity: sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: false /char-regex/1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}