0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-10 22:22:45 -05:00

Merge pull request #5465 from logto-io/yemq-log-8283-add-GET-configs-jwt-customizer-API

feat(core): add GET /configs/jwt-customizer API
This commit is contained in:
Darcy Ye 2024-03-07 14:14:09 +08:00 committed by GitHub
commit ce2abe740c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 126 additions and 10 deletions

View file

@ -35,6 +35,7 @@ const logtoConfigs: LogtoConfigLibrary = {
}), }),
getOidcConfigs: jest.fn(), getOidcConfigs: jest.fn(),
upsertJwtCustomizer: jest.fn(), upsertJwtCustomizer: jest.fn(),
getJwtCustomizer: jest.fn(),
}; };
describe('getAccessToken()', () => { describe('getAccessToken()', () => {

View file

@ -1,4 +1,5 @@
import { import {
LogtoConfigs,
cloudApiIndicator, cloudApiIndicator,
cloudConnectionDataGuard, cloudConnectionDataGuard,
logtoOidcConfigGuard, logtoOidcConfigGuard,
@ -6,13 +7,16 @@ import {
jwtCustomizerConfigGuard, jwtCustomizerConfigGuard,
} from '@logto/schemas'; } from '@logto/schemas';
import type { LogtoOidcConfigType, LogtoJwtTokenKey } from '@logto/schemas'; import type { LogtoOidcConfigType, LogtoJwtTokenKey } from '@logto/schemas';
import { convertToIdentifiers } from '@logto/shared';
import chalk from 'chalk'; import chalk from 'chalk';
import { z, ZodError } from 'zod'; import { z, ZodError } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import type Queries from '#src/tenants/Queries.js'; import type Queries from '#src/tenants/Queries.js';
import { consoleLog } from '#src/utils/console.js'; import { consoleLog } from '#src/utils/console.js';
export type LogtoConfigLibrary = ReturnType<typeof createLogtoConfigLibrary>; export type LogtoConfigLibrary = ReturnType<typeof createLogtoConfigLibrary>;
const { table } = convertToIdentifiers(LogtoConfigs);
export const createLogtoConfigLibrary = ({ export const createLogtoConfigLibrary = ({
logtoConfigs: { logtoConfigs: {
@ -77,5 +81,21 @@ export const createLogtoConfigLibrary = ({
}; };
}; };
return { getOidcConfigs, getCloudConnectionData, upsertJwtCustomizer }; const getJwtCustomizer = async <T extends LogtoJwtTokenKey>(key: T) => {
const { rows } = await getRowsByKeys([key]);
// If the record does not exist (`rows` is empty)
if (rows.length === 0) {
throw new RequestError({
code: 'entity.not_exists',
name: table,
id: key,
status: 404,
});
}
return z.object({ value: jwtCustomizerConfigGuard[key] }).parse(rows[0]);
};
return { getOidcConfigs, getCloudConnectionData, upsertJwtCustomizer, getJwtCustomizer };
}; };

View file

@ -58,6 +58,7 @@ const cloudConnection = createCloudConnectionLibrary({
}), }),
getOidcConfigs: jest.fn(), getOidcConfigs: jest.fn(),
upsertJwtCustomizer: jest.fn(), upsertJwtCustomizer: jest.fn(),
getJwtCustomizer: jest.fn(),
}); });
const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors'); const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors');

View file

@ -1,5 +1,5 @@
import { import {
type jwtCustomizerConfigGuard, jwtCustomizerConfigGuard,
LogtoTenantConfigKey, LogtoTenantConfigKey,
LogtoConfigs, LogtoConfigs,
type AdminConsoleData, type AdminConsoleData,
@ -12,7 +12,9 @@ import {
import { convertToIdentifiers } from '@logto/shared'; import { convertToIdentifiers } from '@logto/shared';
import type { CommonQueryMethods } from 'slonik'; import type { CommonQueryMethods } from 'slonik';
import { sql } from 'slonik'; import { sql } from 'slonik';
import type { z } from 'zod'; import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
const { table, fields } = convertToIdentifiers(LogtoConfigs); const { table, fields } = convertToIdentifiers(LogtoConfigs);
@ -67,6 +69,22 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => {
` `
); );
const getJwtCustomizer = async <T extends LogtoJwtTokenKey>(key: T) => {
const { rows } = await getRowsByKeys([key]);
// If the record does not exist (`rows` is empty)
if (rows.length === 0) {
throw new RequestError({
code: 'entity.not_exists',
name: table,
id: key,
status: 404,
});
}
return z.object({ value: jwtCustomizerConfigGuard[key] }).parse(rows[0]);
};
return { return {
getAdminConsoleConfig, getAdminConsoleConfig,
updateAdminConsoleConfig, updateAdminConsoleConfig,
@ -74,5 +92,6 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => {
getRowsByKeys, getRowsByKeys,
updateOidcConfigsByKey, updateOidcConfigsByKey,
upsertJwtCustomizer, upsertJwtCustomizer,
getJwtCustomizer,
}; };
}; };

View file

@ -149,6 +149,25 @@
"description": "The request body is invalid." "description": "The request body is invalid."
} }
} }
},
"get": {
"summary": "Get JWT customizer",
"description": "Get the JWT customizer for the given token type.",
"parameters": [
{
"in": "path",
"name": "tokenTypePath",
"description": "The token type to get the JWT customizer for."
}
],
"responses": {
"200": {
"description": "The JWT customizer."
},
"404": {
"description": "The JWT customizer does not exist."
}
}
} }
} }
} }

View file

@ -54,7 +54,6 @@ const logtoConfigQueries = {
}), }),
updateOidcConfigsByKey: jest.fn(), updateOidcConfigsByKey: jest.fn(),
getRowsByKeys: jest.fn(async () => mockLogtoConfigRows), getRowsByKeys: jest.fn(async () => mockLogtoConfigRows),
// UpsertJwtCustomizer: jest.fn(),
}; };
const logtoConfigLibraries = { const logtoConfigLibraries = {
@ -63,6 +62,7 @@ const logtoConfigLibraries = {
[LogtoOidcConfigKey.CookieKeys]: mockCookieKeys, [LogtoOidcConfigKey.CookieKeys]: mockCookieKeys,
})), })),
upsertJwtCustomizer: jest.fn(), upsertJwtCustomizer: jest.fn(),
getJwtCustomizer: jest.fn(),
}; };
const settingRoutes = await pickDefault(import('./logto-config.js')); const settingRoutes = await pickDefault(import('./logto-config.js'));
@ -266,4 +266,13 @@ describe('configs routes', () => {
expect(response.status).toEqual(200); expect(response.status).toEqual(200);
expect(response.body).toEqual(mockJwtCustomizerConfigForAccessToken.value); expect(response.body).toEqual(mockJwtCustomizerConfigForAccessToken.value);
}); });
it('GET /configs/jwt-customizer/:tokenType should return the record', async () => {
logtoConfigLibraries.getJwtCustomizer.mockResolvedValueOnce(
mockJwtCustomizerConfigForAccessToken
);
const response = await routeRequester.get('/configs/jwt-customizer/access-token');
expect(response.status).toEqual(200);
expect(response.body).toEqual(mockJwtCustomizerConfigForAccessToken.value);
});
}); });

View file

@ -83,7 +83,7 @@ export default function logtoConfigRoutes<T extends AuthedRouter>(
) { ) {
const { getAdminConsoleConfig, getRowsByKeys, updateAdminConsoleConfig, updateOidcConfigsByKey } = const { getAdminConsoleConfig, getRowsByKeys, updateAdminConsoleConfig, updateOidcConfigsByKey } =
queries.logtoConfigs; queries.logtoConfigs;
const { getOidcConfigs, upsertJwtCustomizer } = logtoConfigs; const { getOidcConfigs, upsertJwtCustomizer, getJwtCustomizer } = logtoConfigs;
router.get( router.get(
'/configs/admin-console', '/configs/admin-console',
@ -240,4 +240,27 @@ export default function logtoConfigRoutes<T extends AuthedRouter>(
return next(); return next();
} }
); );
router.get(
'/configs/jwt-customizer/:tokenTypePath',
koaGuard({
params: z.object({
tokenTypePath: z.nativeEnum(LogtoJwtTokenPath),
}),
response: jwtCustomizerAccessTokenGuard.or(jwtCustomizerClientCredentialsGuard),
status: [200, 404],
}),
async (ctx, next) => {
const {
params: { tokenTypePath },
} = ctx.guard;
const { value } = await getJwtCustomizer(
tokenTypePath === LogtoJwtTokenPath.AccessToken
? LogtoJwtTokenKey.AccessToken
: LogtoJwtTokenKey.ClientCredentials
);
ctx.body = value;
return next();
}
);
} }

View file

@ -40,3 +40,8 @@ export const upsertJwtCustomizer = async (
authedAdminApi authedAdminApi
.put(`configs/jwt-customizer/${keyTypePath}`, { json: value }) .put(`configs/jwt-customizer/${keyTypePath}`, { json: value })
.json<JwtCustomizerAccessToken | JwtCustomizerClientCredentials>(); .json<JwtCustomizerAccessToken | JwtCustomizerClientCredentials>();
export const getJwtCustomizer = async (keyTypePath: 'access-token' | 'client-credentials') =>
authedAdminApi
.get(`configs/jwt-customizer/${keyTypePath}`)
.json<JwtCustomizerAccessToken | JwtCustomizerClientCredentials>();

View file

@ -11,6 +11,7 @@ import {
rotateOidcKeys, rotateOidcKeys,
updateAdminConsoleConfig, updateAdminConsoleConfig,
upsertJwtCustomizer, upsertJwtCustomizer,
getJwtCustomizer,
} from '#src/api/index.js'; } from '#src/api/index.js';
import { expectRejects } from '#src/helpers/index.js'; import { expectRejects } from '#src/helpers/index.js';
@ -125,7 +126,7 @@ describe('admin console sign-in experience', () => {
expect(privateKeys2[1]?.id).toBe(privateKeys[0]?.id); expect(privateKeys2[1]?.id).toBe(privateKeys[0]?.id);
}); });
it('should successfully add a new JWT customizer', async () => { it('should successfully POST and GET a JWT customizer (access token)', async () => {
const accessTokenJwtCustomizerPayload = { const accessTokenJwtCustomizerPayload = {
script: '', script: '',
envVars: {}, envVars: {},
@ -136,11 +137,11 @@ describe('admin console sign-in experience', () => {
}, },
}, },
}; };
const clientCredentialsJwtCustomizerPayload = {
...accessTokenJwtCustomizerPayload,
contextSample: {},
};
await expectRejects(getJwtCustomizer('access-token'), {
code: 'entity.not_exists',
statusCode: 404,
});
const accessToken = await upsertJwtCustomizer('access-token', accessTokenJwtCustomizerPayload); const accessToken = await upsertJwtCustomizer('access-token', accessTokenJwtCustomizerPayload);
expect(accessToken).toMatchObject(accessTokenJwtCustomizerPayload); expect(accessToken).toMatchObject(accessTokenJwtCustomizerPayload);
const newAccessTokenJwtCustomizerPayload = { const newAccessTokenJwtCustomizerPayload = {
@ -152,7 +153,22 @@ describe('admin console sign-in experience', () => {
newAccessTokenJwtCustomizerPayload newAccessTokenJwtCustomizerPayload
); );
expect(updatedAccessToken).toMatchObject(newAccessTokenJwtCustomizerPayload); expect(updatedAccessToken).toMatchObject(newAccessTokenJwtCustomizerPayload);
await expect(getJwtCustomizer('access-token')).resolves.toMatchObject(
newAccessTokenJwtCustomizerPayload
);
});
it('should successfully POST and GET a JWT customizer (client credentials)', async () => {
const clientCredentialsJwtCustomizerPayload = {
script: '',
envVars: {},
contextSample: {},
};
await expectRejects(getJwtCustomizer('client-credentials'), {
code: 'entity.not_exists',
statusCode: 404,
});
const clientCredentials = await upsertJwtCustomizer( const clientCredentials = await upsertJwtCustomizer(
'client-credentials', 'client-credentials',
clientCredentialsJwtCustomizerPayload clientCredentialsJwtCustomizerPayload
@ -167,5 +183,8 @@ describe('admin console sign-in experience', () => {
newClientCredentialsJwtCustomizerPayload newClientCredentialsJwtCustomizerPayload
); );
expect(updatedClientCredentials).toMatchObject(newClientCredentialsJwtCustomizerPayload); expect(updatedClientCredentials).toMatchObject(newClientCredentialsJwtCustomizerPayload);
await expect(getJwtCustomizer('client-credentials')).resolves.toMatchObject(
newClientCredentialsJwtCustomizerPayload
);
}); });
}); });