diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index 88bcc8dfb..8cc73fad9 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -1,4 +1,4 @@ -import { Application } from '@logto/schemas'; +import { Application, ApplicationDTO } from '@logto/schemas'; import React, { useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; @@ -31,26 +31,16 @@ import { noSpaceRegex } from '@/utilities/regex'; import DeleteForm from './components/DeleteForm'; import * as styles from './index.module.scss'; -// TODO LOG-1908: OidcConfig in Application Details -type OidcConfig = { - authorization_endpoint: string; - userinfo_endpoint: string; - token_endpoint: string; -}; - const ApplicationDetails = () => { const { id } = useParams(); const location = useLocation(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { data, error, mutate } = useSWR( + const { data, error, mutate } = useSWR( id && `/api/applications/${id}` ); - // TODO LOG-1908: OidcConfig in Application Details - const { data: oidcConfig, error: fetchOidcConfigError } = useSWR( - '/oidc/.well-known/openid-configuration' - ); - const isLoading = !data && !error && !oidcConfig && !fetchOidcConfigError; + + const isLoading = !data && !error; const [isReadmeOpen, setIsReadmeOpen] = useState(false); @@ -90,14 +80,14 @@ const ApplicationDetails = () => { }, }, }) - .json(); + .json(); void mutate(updatedApplication); toast.success(t('application_details.save_success')); }); const isAdvancedSettings = location.pathname.includes('advanced-settings'); - const SettingsPage = oidcConfig && ( + const SettingsPage = data && ( <> { title="admin_console.application_details.authorization_endpoint" className={styles.textField} > - + { ); - const AdvancedSettingsPage = oidcConfig && ( + const AdvancedSettingsPage = data && ( <> - + - + ); @@ -191,7 +184,7 @@ const ApplicationDetails = () => { /> {isLoading &&
loading
} {error &&
{`error occurred: ${error.body.message}`}
} - {data && oidcConfig && ( + {data && ( <> diff --git a/packages/console/src/pages/Applications/index.tsx b/packages/console/src/pages/Applications/index.tsx index a514d5c0f..37467e1f0 100644 --- a/packages/console/src/pages/Applications/index.tsx +++ b/packages/console/src/pages/Applications/index.tsx @@ -1,4 +1,4 @@ -import { Application } from '@logto/schemas'; +import { ApplicationDTO } from '@logto/schemas'; import classNames from 'classnames'; import React, { useState } from 'react'; import { toast } from 'react-hot-toast'; @@ -33,7 +33,7 @@ const Applications = () => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const [query, setQuery] = useSearchParams(); const pageIndex = Number(query.get('page') ?? '1'); - const { data, error, mutate } = useSWR<[Application[], number], RequestError>( + const { data, error, mutate } = useSWR<[ApplicationDTO[], number], RequestError>( `/api/applications?page=${pageIndex}&page_size=${pageSize}` ); const isLoading = !data && !error; diff --git a/packages/core/package.json b/packages/core/package.json index 01de4e35d..61205e37b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,6 +23,7 @@ "@logto/phrases": "^0.1.0", "@logto/schemas": "^0.1.0", "@silverhand/essentials": "^1.1.0", + "camelcase-keys": "^7.0.2", "dayjs": "^1.10.5", "decamelize": "^5.0.0", "dotenv": "^10.0.0", diff --git a/packages/core/src/__mocks__/index.ts b/packages/core/src/__mocks__/index.ts index 5404e4eb1..aacfa110d 100644 --- a/packages/core/src/__mocks__/index.ts +++ b/packages/core/src/__mocks__/index.ts @@ -1,20 +1,29 @@ import { Application, + ApplicationDTO, ApplicationType, Passcode, PasscodeType, Resource, Role, Setting, + SnakeCaseOidcConfig, UserLog, UserLogResult, UserLogType, } from '@logto/schemas'; +import camelcaseKeys from 'camelcase-keys'; export * from './connector'; export * from './sign-in-experience'; export * from './user'; +export const mockOidcConfig: SnakeCaseOidcConfig = { + authorization_endpoint: 'https://logto.dev/oidc/auth', + userinfo_endpoint: 'https://logto.dev/oidc/userinfo', + token_endpoint: 'https://logto.dev/oidc/token', +}; + export const mockApplication: Application = { id: 'foo', name: 'foo', @@ -32,6 +41,11 @@ export const mockApplication: Application = { createdAt: 1_645_334_775_356, }; +export const mockApplicationDTO: ApplicationDTO = { + ...mockApplication, + oidcConfig: camelcaseKeys(mockOidcConfig), +}; + export const mockResource: Resource = { id: 'logto_api', name: 'management api', diff --git a/packages/core/src/routes/application.test.ts b/packages/core/src/routes/application.test.ts index 7aa3b2d84..522a3e667 100644 --- a/packages/core/src/routes/application.test.ts +++ b/packages/core/src/routes/application.test.ts @@ -1,6 +1,8 @@ import { Application, CreateApplication, ApplicationType } from '@logto/schemas'; +import nock from 'nock'; -import { mockApplication } from '@/__mocks__'; +import { mockApplicationDTO, mockOidcConfig } from '@/__mocks__'; +import { port } from '@/env/consts'; import { findApplicationById } from '@/queries/application'; import { createRequester } from '@/utils/test-utils'; @@ -8,22 +10,22 @@ import applicationRoutes from './application'; jest.mock('@/queries/application', () => ({ findTotalNumberOfApplications: jest.fn(async () => ({ count: 10 })), - findAllApplications: jest.fn(async () => [mockApplication]), - findApplicationById: jest.fn(async () => mockApplication), + findAllApplications: jest.fn(async () => [mockApplicationDTO]), + findApplicationById: jest.fn(async () => mockApplicationDTO), deleteApplicationById: jest.fn(), insertApplication: jest.fn( async (body: CreateApplication): Promise => ({ - ...mockApplication, + ...mockApplicationDTO, ...body, oidcClientMetadata: { - ...mockApplication.oidcClientMetadata, + ...mockApplicationDTO.oidcClientMetadata, ...body.oidcClientMetadata, }, }) ), updateApplicationById: jest.fn( async (_, data: Partial): Promise => ({ - ...mockApplication, + ...mockApplicationDTO, ...data, }) ), @@ -43,10 +45,15 @@ const customClientMetadata = { describe('application route', () => { const applicationRequest = createRequester({ authedRoutes: applicationRoutes }); + beforeEach(() => { + const discoveryUrl = `http://localhost:${port}/oidc/.well-known/openid-configuration`; + nock(discoveryUrl).get('').reply(200, mockOidcConfig); + }); + it('GET /applications', async () => { const response = await applicationRequest.get('/applications'); expect(response.status).toEqual(200); - expect(response.body).toEqual([mockApplication]); + expect(response.body).toEqual([mockApplicationDTO]); expect(response.header).toHaveProperty('total-number', '10'); }); @@ -60,7 +67,7 @@ describe('application route', () => { expect(response.status).toEqual(200); expect(response.body).toEqual({ - ...mockApplication, + ...mockApplicationDTO, id: 'randomId', name, type, @@ -98,7 +105,7 @@ describe('application route', () => { const response = await applicationRequest.get('/applications/foo'); expect(response.status).toEqual(200); - expect(response.body).toEqual(mockApplication); + expect(response.body).toEqual(mockApplicationDTO); }); it('PATCH /applications/:applicationId', async () => { @@ -109,7 +116,7 @@ describe('application route', () => { .send({ name, customClientMetadata }); expect(response.status).toEqual(200); - expect(response.body).toEqual({ ...mockApplication, name, customClientMetadata }); + expect(response.body).toEqual({ ...mockApplicationDTO, name, customClientMetadata }); }); it('PATCH /applications/:applicationId expect to throw with invalid properties', async () => { diff --git a/packages/core/src/routes/application.ts b/packages/core/src/routes/application.ts index 94be881b9..583abe72c 100644 --- a/packages/core/src/routes/application.ts +++ b/packages/core/src/routes/application.ts @@ -1,6 +1,9 @@ -import { Applications } from '@logto/schemas'; +import { Applications, SnakeCaseOidcConfig } from '@logto/schemas'; +import camelcaseKeys from 'camelcase-keys'; +import got from 'got'; import { object, string } from 'zod'; +import { port } from '@/env/consts'; import koaGuard from '@/middleware/koa-guard'; import koaPagination from '@/middleware/koa-pagination'; import { buildOidcClientMetadata } from '@/oidc/utils'; @@ -18,18 +21,24 @@ import { AuthedRouter } from './types'; const applicationId = buildIdGenerator(21); +const discoveryUrl = `http://localhost:${port}/oidc/.well-known/openid-configuration`; + export default function applicationRoutes(router: T) { router.get('/applications', koaPagination(), async (ctx, next) => { const { limit, offset } = ctx.pagination; - const [{ count }, applications] = await Promise.all([ + const [{ count }, applications, oidcConfig] = await Promise.all([ findTotalNumberOfApplications(), findAllApplications(limit, offset), + got(discoveryUrl).json(), ]); // Return totalCount to pagination middleware ctx.pagination.totalCount = count; - ctx.body = applications; + ctx.body = applications.map((application) => ({ + ...application, + oidcConfig: camelcaseKeys(oidcConfig), + })); return next(); }); @@ -45,13 +54,21 @@ export default function applicationRoutes(router: T) { async (ctx, next) => { const { name, type, oidcClientMetadata, customClientMetadata } = ctx.guard.body; - ctx.body = await insertApplication({ - id: applicationId(), - type, - name, - oidcClientMetadata: buildOidcClientMetadata(oidcClientMetadata), - customClientMetadata, - }); + const [application, oidcConfig] = await Promise.all([ + insertApplication({ + id: applicationId(), + type, + name, + oidcClientMetadata: buildOidcClientMetadata(oidcClientMetadata), + customClientMetadata, + }), + got(discoveryUrl).json(), + ]); + + ctx.body = { + ...application, + oidcConfig: camelcaseKeys(oidcConfig), + }; return next(); } @@ -67,7 +84,15 @@ export default function applicationRoutes(router: T) { params: { id }, } = ctx.guard; - ctx.body = await findApplicationById(id); + const [application, oidcConfig] = await Promise.all([ + findApplicationById(id), + got(discoveryUrl).json(), + ]); + + ctx.body = { + ...application, + oidcConfig: camelcaseKeys(oidcConfig), + }; return next(); } @@ -85,9 +110,17 @@ export default function applicationRoutes(router: T) { body, } = ctx.guard; - ctx.body = await updateApplicationById(id, { - ...body, - }); + const [application, oidcConfig] = await Promise.all([ + updateApplicationById(id, { + ...body, + }), + got(discoveryUrl).json(), + ]); + + ctx.body = { + ...application, + oidcConfig: camelcaseKeys(oidcConfig), + }; return next(); } diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 3297473bf..bf13ab7b3 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "@silverhand/eslint-config": "^0.10.2", - "@silverhand/essentials": "^1.1.0", + "@silverhand/essentials": "^1.1.6", "@silverhand/ts-config": "^0.10.2", "@types/lodash.uniq": "^4.5.6", "@types/node": "14", diff --git a/packages/schemas/src/types/application.ts b/packages/schemas/src/types/application.ts new file mode 100644 index 000000000..054f9e5d2 --- /dev/null +++ b/packages/schemas/src/types/application.ts @@ -0,0 +1,6 @@ +import { Application } from '../db-entries'; +import { OidcConfig } from './oidc-config'; + +export interface ApplicationDTO extends Application { + oidcConfig: OidcConfig; +} diff --git a/packages/schemas/src/types/index.ts b/packages/schemas/src/types/index.ts index 065e10da7..f860c5a19 100644 --- a/packages/schemas/src/types/index.ts +++ b/packages/schemas/src/types/index.ts @@ -1,2 +1,4 @@ export * from './user'; export * from './connector'; +export * from './oidc-config'; +export * from './application'; diff --git a/packages/schemas/src/types/oidc-config.ts b/packages/schemas/src/types/oidc-config.ts new file mode 100644 index 000000000..df95b07e1 --- /dev/null +++ b/packages/schemas/src/types/oidc-config.ts @@ -0,0 +1,9 @@ +import { KeysToCamelCase } from '@silverhand/essentials'; + +export type SnakeCaseOidcConfig = { + authorization_endpoint: string; + userinfo_endpoint: string; + token_endpoint: string; +}; + +export type OidcConfig = KeysToCamelCase; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fbc47dc1..a50387489 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -165,6 +165,7 @@ importers: '@types/node': ^16.3.1 '@types/oidc-provider': ^7.8.0 '@types/supertest': ^2.0.11 + camelcase-keys: ^7.0.2 copyfiles: ^2.4.1 dayjs: ^1.10.5 decamelize: ^5.0.0 @@ -206,6 +207,7 @@ importers: '@logto/phrases': link:../phrases '@logto/schemas': link:../schemas '@silverhand/essentials': 1.1.2 + camelcase-keys: 7.0.2 dayjs: 1.10.7 decamelize: 5.0.1 dotenv: 10.0.0 @@ -315,7 +317,7 @@ importers: specifiers: '@logto/phrases': ^0.1.0 '@silverhand/eslint-config': ^0.10.2 - '@silverhand/essentials': ^1.1.0 + '@silverhand/essentials': ^1.1.6 '@silverhand/ts-config': ^0.10.2 '@types/lodash.uniq': ^4.5.6 '@types/node': '14' @@ -334,7 +336,7 @@ importers: zod: 3.14.3 devDependencies: '@silverhand/eslint-config': 0.10.2_3a533fa6cc3da0cf8525ef55d41c4384 - '@silverhand/essentials': 1.1.2 + '@silverhand/essentials': 1.1.7 '@silverhand/ts-config': 0.10.2_typescript@4.6.2 '@types/lodash.uniq': 4.5.6 '@types/node': 14.18.0 @@ -5260,6 +5262,7 @@ packages: dependencies: lodash.orderby: 4.6.0 lodash.pick: 4.4.0 + dev: false /@silverhand/essentials/1.1.4: resolution: {integrity: sha512-5pHjIz42CjILcqGWhmfP7/RCbmlWIWmj0H3RMJDGW3QKZyNkWawG6gKwtEQ75N0MoZOzXjNE4HD4DK3moPa5sg==} @@ -5283,7 +5286,6 @@ packages: dependencies: lodash.orderby: 4.6.0 lodash.pick: 4.4.0 - dev: false /@silverhand/ts-config-react/0.10.3_typescript@4.6.2: resolution: {integrity: sha512-xGOwcw1HTixfP3PSSdJT3leGnlUV0dLna9xp58bDDLul7UCnIn+PNp1VNJxUZ/HvtKbV4ZSYdGsGE6Xqmwn7Ag==} @@ -7374,7 +7376,6 @@ packages: /camelcase/6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - dev: false /caniuse-api/3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} @@ -11898,7 +11899,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - camelcase: 6.2.1 + camelcase: 6.3.0 chalk: 4.1.2 jest-get-type: 27.5.1 leven: 3.1.0