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:
commit
ce2abe740c
9 changed files with 126 additions and 10 deletions
|
@ -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()', () => {
|
||||||
|
|
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue