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

feat(core): use PUT instead of POST and refactor code

This commit is contained in:
Darcy Ye 2024-03-05 20:05:17 +08:00
parent 7c4019b408
commit 92a04f08bd
No known key found for this signature in database
GPG key ID: B46F4C07EDEFC610
37 changed files with 96 additions and 226 deletions

View file

@ -92,16 +92,27 @@ export const isGuardMiddleware = <Type extends IMiddleware>(
): function_ is WithGuardConfig<Type> =>
function_.name === 'guardMiddleware' && has(function_, 'config');
export const tryParse = <Output, Definition extends ZodTypeDef, Input>(
export const parse = <Output, Definition extends ZodTypeDef, Input>(
type: 'query' | 'body' | 'params' | 'files',
guard: ZodType<Output, Definition, Input>,
data: unknown
) => {
try {
return guard.parse(data);
} catch (error: unknown) {
throw new RequestError({ code: 'guard.invalid_input', type }, error);
}
};
const tryParse = <Output, Definition extends ZodTypeDef, Input>(
type: 'query' | 'body' | 'params' | 'files',
guard: Optional<ZodType<Output, Definition, Input>>,
data: unknown
) => {
try {
return guard?.parse(data);
} catch (error: unknown) {
throw new RequestError({ code: 'guard.invalid_input', type }, error);
if (!guard) {
return;
}
return parse(type, guard, data);
};
export default function koaGuard<

View file

@ -53,7 +53,7 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => {
`);
// Can not narrow down the type of value if we utilize `buildInsertIntoWithPool` method.
const insertJwtCustomizer = async <T extends LogtoJwtTokenKey>(
const insertOrUpdateJwtCustomizer = async <T extends LogtoJwtTokenKey>(
key: T,
value: z.infer<(typeof jwtCustomizerConfigGuard)[T]>
) =>
@ -61,7 +61,9 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => {
sql`
insert into ${table} (${fields.key}, ${fields.value})
values (${key}, ${sql.jsonb(value)})
on conflict (${fields.tenantId}, ${fields.key}) do nothing
on conflict (${fields.tenantId}, ${fields.key}) do update set ${
fields.value
} = ${sql.jsonb(value)}
returning *
`
);
@ -72,6 +74,6 @@ export const createLogtoConfigQueries = (pool: CommonQueryMethods) => {
getCloudConnectionData,
getRowsByKeys,
updateOidcConfigsByKey,
insertJwtCustomizer,
insertOrUpdateJwtCustomizer,
};
};

View file

@ -106,9 +106,9 @@
}
},
"/api/configs/jwt-customizer/{tokenType}": {
"post": {
"summary": "Create JWT customizer",
"description": "Create a JWT customizer for the given token type.",
"put": {
"summary": "Create or update JWT customizer",
"description": "Create or update a JWT customizer for the given token type.",
"parameters": [
{
"in": "path",
@ -139,14 +139,14 @@
}
},
"responses": {
"200": {
"description": "The updated JWT customizer."
},
"201": {
"description": "The created JWT customizer."
},
"400": {
"description": "The request body is invalid."
},
"409": {
"description": "The JWT customizer already exists."
}
}
}

View file

@ -54,7 +54,7 @@ const logtoConfigQueries = {
}),
updateOidcConfigsByKey: jest.fn(),
getRowsByKeys: jest.fn(async () => mockLogtoConfigRows),
insertJwtCustomizer: jest.fn(),
insertOrUpdateJwtCustomizer: jest.fn(),
};
const logtoConfigLibraries = {
@ -226,36 +226,43 @@ describe('configs routes', () => {
);
});
it('POST /configs/jwt-customizer/:tokenType should add a record successfully', async () => {
it('PUT /configs/jwt-customizer/:tokenType should add a record successfully', async () => {
logtoConfigQueries.getRowsByKeys.mockResolvedValueOnce({
...mockLogtoConfigRows,
rows: [],
rowCount: 0,
});
logtoConfigQueries.insertJwtCustomizer.mockResolvedValueOnce(
logtoConfigQueries.insertOrUpdateJwtCustomizer.mockResolvedValueOnce(
mockJwtCustomizerConfigForAccessToken
);
const response = await routeRequester
.post('/configs/jwt-customizer/access-token')
.send(mockJwtCustomizerConfigForAccessToken);
expect(logtoConfigQueries.insertJwtCustomizer).toHaveBeenCalledWith(
.put(`/configs/jwt-customizer/access-token`)
.send(mockJwtCustomizerConfigForAccessToken.value);
expect(logtoConfigQueries.insertOrUpdateJwtCustomizer).toHaveBeenCalledWith(
LogtoJwtTokenKey.AccessToken,
mockJwtCustomizerConfigForAccessToken
mockJwtCustomizerConfigForAccessToken.value
);
expect(response.status).toEqual(201);
expect(response.body).toEqual(mockJwtCustomizerConfigForAccessToken.value);
});
it('POST /configs/jwt-customizer/:tokenType should fail ', async () => {
it('PUT /configs/jwt-customizer/:tokenType should update a record successfully', async () => {
logtoConfigQueries.getRowsByKeys.mockResolvedValueOnce({
...mockLogtoConfigRows,
rows: [mockJwtCustomizerConfigForAccessToken],
rowCount: 1,
});
logtoConfigQueries.insertOrUpdateJwtCustomizer.mockResolvedValueOnce(
mockJwtCustomizerConfigForAccessToken
);
const response = await routeRequester
.post('/configs/jwt-customizer/access-token')
.send(mockJwtCustomizerConfigForAccessToken);
expect(logtoConfigQueries.insertJwtCustomizer).not.toHaveBeenCalled();
expect(response.status).toEqual(409);
.put('/configs/jwt-customizer/access-token')
.send(mockJwtCustomizerConfigForAccessToken.value);
expect(logtoConfigQueries.insertOrUpdateJwtCustomizer).toHaveBeenCalledWith(
LogtoJwtTokenKey.AccessToken,
mockJwtCustomizerConfigForAccessToken.value
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(mockJwtCustomizerConfigForAccessToken.value);
});
});

View file

@ -20,8 +20,7 @@ import {
import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard, { tryParse } from '#src/middleware/koa-guard.js';
import assertThat from '#src/utils/assert-that.js';
import koaGuard, { parse } from '#src/middleware/koa-guard.js';
import { exportJWK } from '#src/utils/jwks.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
@ -39,6 +38,16 @@ const getLogtoJwtTokenKey = (key: LogtoJwtTokenKeyType): LogtoJwtTokenKey =>
? LogtoJwtTokenKey.AccessToken
: LogtoJwtTokenKey.ClientCredentials;
const guardJwtCustomizerBody = (tokenType: LogtoJwtTokenKeyType, body: unknown) => {
// Manually implement the request body type check, the flow aligns with the actual `koaGuard()`.
// Use ternary operator to get the specific guard brings difficulties to type inference.
if (tokenType === LogtoJwtTokenKeyType.AccessToken) {
return parse('body', jwtCustomizerAccessTokenGuard, body);
}
return parse('body', jwtCustomizerClientCredentialsGuard, body);
};
/**
* Remove actual values of the private keys from response.
* @param type Logto config key DB column name. Values are either `oidc.privateKeys` or `oidc.cookieKeys`.
@ -73,7 +82,7 @@ export default function logtoConfigRoutes<T extends AuthedRouter>(
const {
getAdminConsoleConfig,
getRowsByKeys,
insertJwtCustomizer,
insertOrUpdateJwtCustomizer,
updateAdminConsoleConfig,
updateOidcConfigsByKey,
} = queries.logtoConfigs;
@ -198,7 +207,7 @@ export default function logtoConfigRoutes<T extends AuthedRouter>(
}
);
router.post(
router.put(
'/configs/jwt-customizer/:tokenType',
koaGuard({
params: z.object({
@ -208,43 +217,28 @@ export default function logtoConfigRoutes<T extends AuthedRouter>(
* Use `z.unknown()` to guard the request body as a JSON object, since the actual guard depends
* on the `tokenType` and we can not get the value of `tokenType` before parsing the request body,
* we will do more specific guard as long as we can get the value of `tokenType`.
*
* Should specify `body` in koaGuard, otherwise the request body is not accessible even via `ctx.request.body`.
*/
body: z.unknown(),
response: jwtCustomizerAccessTokenGuard.or(jwtCustomizerClientCredentialsGuard),
status: [201, 400, 409],
status: [200, 201, 400],
}),
async (ctx, next) => {
const {
params: { tokenType },
body,
body: rawBody,
} = ctx.guard;
const key = getLogtoJwtTokenKey(tokenType);
const body = guardJwtCustomizerBody(tokenType, rawBody);
// Manually implement the request body type check, the flow aligns with the actual `koaGuard()`.
// Use ternary operator to get the specific guard brings difficulties to type inference.
if (tokenType === LogtoJwtTokenKeyType.AccessToken) {
tryParse('body', jwtCustomizerAccessTokenGuard, body);
} else {
tryParse('body', jwtCustomizerClientCredentialsGuard, body);
const { rows } = await getRowsByKeys([key]);
const jwtCustomizer = await insertOrUpdateJwtCustomizer(key, body);
if (rows.length === 0) {
ctx.status = 201;
}
const { rows } = await getRowsByKeys([getLogtoJwtTokenKey(tokenType)]);
assertThat(
rows.length === 0,
new RequestError({
code: 'logto_config.jwt.customizer_exists',
tokenType,
status: 409,
})
);
const jwtCustomizer = await insertJwtCustomizer(
getLogtoJwtTokenKey(tokenType),
// Since we applied the detailed guard manually, we can safely cast the `body` to the specific type.
// eslint-disable-next-line no-restricted-syntax
body as Parameters<typeof insertJwtCustomizer>[1]
);
ctx.status = 201;
ctx.body = jwtCustomizer.value;
return next();

View file

@ -34,7 +34,7 @@ export const rotateOidcKeys = async (
.post(`configs/oidc/${keyType}/rotate`, { json: { signingKeyAlgorithm } })
.json<OidcConfigKeysResponse[]>();
export const insertJwtCustomizer = async (keyType: LogtoJwtTokenKeyType, value: unknown) =>
export const insertOrUpdateJwtCustomizer = async (keyType: LogtoJwtTokenKeyType, value: unknown) =>
authedAdminApi
.post(`configs/jwt-customizer/${keyType}`, { json: value })
.put(`configs/jwt-customizer/${keyType}`, { json: value })
.json<JwtCustomizerAccessToken | JwtCustomizerClientCredentials>();

View file

@ -11,7 +11,7 @@ import {
getOidcKeys,
rotateOidcKeys,
updateAdminConsoleConfig,
insertJwtCustomizer,
insertOrUpdateJwtCustomizer,
} from '#src/api/index.js';
import { expectRejects } from '#src/helpers/index.js';
@ -141,15 +141,35 @@ describe('admin console sign-in experience', () => {
...accessTokenJwtCustomizerPayload,
contextSample: {},
};
const accessToken = await insertJwtCustomizer(
const accessToken = await insertOrUpdateJwtCustomizer(
LogtoJwtTokenKeyType.AccessToken,
accessTokenJwtCustomizerPayload
);
expect(accessToken).toMatchObject(accessTokenJwtCustomizerPayload);
const clientCredentials = await insertJwtCustomizer(
const newAccessTokenJwtCustomizerPayload = {
...accessTokenJwtCustomizerPayload,
script: 'new script',
};
const updatedAccessToken = await insertOrUpdateJwtCustomizer(
LogtoJwtTokenKeyType.AccessToken,
newAccessTokenJwtCustomizerPayload
);
expect(updatedAccessToken).toMatchObject(newAccessTokenJwtCustomizerPayload);
const clientCredentials = await insertOrUpdateJwtCustomizer(
LogtoJwtTokenKeyType.ClientCredentials,
clientCredentialsJwtCustomizerPayload
);
expect(clientCredentials).toMatchObject(clientCredentialsJwtCustomizerPayload);
const newClientCredentialsJwtCustomizerPayload = {
...clientCredentialsJwtCustomizerPayload,
script: 'new script client credentials',
};
const updatedClientCredentials = await insertOrUpdateJwtCustomizer(
LogtoJwtTokenKeyType.ClientCredentials,
newClientCredentialsJwtCustomizerPayload
);
expect(updatedClientCredentials).toMatchObject(newClientCredentialsJwtCustomizerPayload);
});
});

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,8 +0,0 @@
const logto_config = {
jwt: {
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);

View file

@ -7,7 +7,6 @@ import guard from './guard.js';
import hook from './hook.js';
import localization from './localization.js';
import log from './log.js';
import logto_config from './logto-config.js';
import oidc from './oidc.js';
import organization from './organization.js';
import password from './password.js';
@ -36,7 +35,6 @@ const errors = {
verification_code,
sign_in_experiences,
localization,
logto_config,
swagger,
entity,
log,

View file

@ -1,9 +0,0 @@
const logto_config = {
jwt: {
/** UNTRANSLATED */
customizer_exists:
'Can not create a new customizer since there is already one for the token type `{{tokenType}}`.',
},
};
export default Object.freeze(logto_config);