0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

feat(core): add management API to fetch one-time token by ID

This commit is contained in:
Charles Zhao 2025-04-03 22:03:39 +08:00
parent 0a50875310
commit 391846b5f3
No known key found for this signature in database
GPG key ID: 55CFA7C080C98029
6 changed files with 93 additions and 24 deletions

View file

@ -2,6 +2,7 @@ import { OneTimeTokens, OneTimeTokenStatus, type OneTimeToken } from '@logto/sch
import type { CommonQueryMethods } from '@silverhand/slonik';
import { sql } from '@silverhand/slonik';
import { buildFindEntityByIdWithPool } from '#src/database/find-entity-by-id.js';
import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
import { convertToIdentifiers } from '#src/utils/sql.js';
@ -12,6 +13,8 @@ export const createOneTimeTokenQueries = (pool: CommonQueryMethods) => {
returning: true,
});
const getOneTimeTokenById = buildFindEntityByIdWithPool(pool)(OneTimeTokens);
const updateExpiredOneTimeTokensStatusByEmail = async (email: string) =>
pool.query(sql`
update ${table}
@ -39,6 +42,7 @@ export const createOneTimeTokenQueries = (pool: CommonQueryMethods) => {
return {
insertOneTimeToken,
updateExpiredOneTimeTokensStatusByEmail,
getOneTimeTokenById,
getOneTimeTokenByToken,
updateOneTimeTokenStatus,
};

View file

@ -39,6 +39,17 @@
}
}
},
"/api/one-time-tokens/{id}": {
"get": {
"summary": "Get one-time token by ID",
"description": "Get a one-time token by its ID.",
"responses": {
"200": {
"description": "The one-time token found by ID."
}
}
}
},
"/api/one-time-tokens/verify": {
"post": {
"summary": "Verify one-time token",
@ -62,12 +73,6 @@
"responses": {
"200": {
"description": "The one-time token was verified successfully."
},
"400": {
"description": "The token has been consumed or is expired, or the email does not match."
},
"404": {
"description": "The one-time token was not found or is not active."
}
}
}

View file

@ -16,7 +16,11 @@ export default function oneTimeTokenRoutes<T extends ManagementApiRouter>(
router,
{
queries: {
oneTimeTokens: { insertOneTimeToken, updateExpiredOneTimeTokensStatusByEmail },
oneTimeTokens: {
insertOneTimeToken,
updateExpiredOneTimeTokensStatusByEmail,
getOneTimeTokenById,
},
},
libraries: {
oneTimeTokens: { verifyOneTimeToken },
@ -24,6 +28,25 @@ export default function oneTimeTokenRoutes<T extends ManagementApiRouter>(
},
]: RouterInitArgs<T>
) {
router.get(
'/one-time-tokens/:id',
koaGuard({
params: z.object({
id: z.string().min(1),
}),
response: OneTimeTokens.guard,
status: [200, 400, 404],
}),
async (ctx, next) => {
const { params } = ctx.guard;
const { id } = params;
const oneTimeToken = await getOneTimeTokenById(id);
ctx.body = oneTimeToken;
ctx.status = 200;
return next();
}
);
router.post(
'/one-time-tokens',
koaGuard({
@ -45,7 +68,6 @@ export default function oneTimeTokenRoutes<T extends ManagementApiRouter>(
const { body } = ctx.guard;
const { expiresIn, ...rest } = body;
// TODO: add an integration test for this, once GET API is added.
void trySafe(async () => updateExpiredOneTimeTokensStatusByEmail(rest.email));
const expiresAt = addSeconds(new Date(), expiresIn ?? defaultExpiresTime);

View file

@ -27,7 +27,8 @@ import {
} from './general.js';
import { buildPathIdParameters, customParameters, mergeParameters } from './parameters.js';
// Add more components here to cover more ID parameters in paths. For example, if there is a
// Add more components here to cover more ID parameters in paths. For example, if there is a new API
// identifiable entity `/api/entities`, and you want to use `/api/entities/{id}`, add the entity here.
const managementApiIdentifiableEntityNames = Object.freeze([
'key',
'connector-factory',
@ -50,6 +51,7 @@ const managementApiIdentifiableEntityNames = Object.freeze([
'saml-application',
'secret',
'email-template',
'one-time-token',
]);
/** Additional tags that cannot be inferred from the path. */

View file

@ -20,3 +20,6 @@ export const verifyOneTimeToken = async (
json: verifyOneTimeToken,
})
.json<OneTimeToken>();
export const getOneTimeTokenById = async (id: string) =>
authedAdminApi.get(`one-time-tokens/${id}`).json<OneTimeToken>();

View file

@ -1,14 +1,18 @@
import { OneTimeTokenStatus } from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { createOneTimeToken, verifyOneTimeToken } from '#src/api/one-time-token.js';
import {
createOneTimeToken,
verifyOneTimeToken,
getOneTimeTokenById,
} from '#src/api/one-time-token.js';
import { expectRejects } from '#src/helpers/index.js';
import { devFeatureTest } from '#src/utils.js';
import { devFeatureTest, waitFor } from '#src/utils.js';
const { it, describe } = devFeatureTest;
describe('one time tokens API', () => {
it('should create one time token with default 10 mins expiration time', async () => {
describe('one-time tokens API', () => {
it('should create one-time token with default 10 mins expiration time', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({
email,
@ -22,7 +26,7 @@ describe('one time tokens API', () => {
expect(oneTimeToken.token.length).toBe(32);
});
it('should create one time token with custom expiration time', async () => {
it('should create one-time token with custom expiration time', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({
email,
@ -36,7 +40,7 @@ describe('one time tokens API', () => {
expect(oneTimeToken.token.length).toBe(32);
});
it('should create one time token with `applicationIds` and `jitOrganizationIds` configured', async () => {
it('should create one-time token with `applicationIds` and `jitOrganizationIds` configured', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({
email,
@ -53,10 +57,41 @@ describe('one time tokens API', () => {
expect(oneTimeToken.token.length).toBe(32);
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
it(`update expired tokens' status to expired when creating new one time token`, async () => {});
it('should be able to get one-time token by its ID', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({
email,
context: {
jitOrganizationIds: ['org-1'],
},
});
it('should verify one time token', async () => {
const token = await getOneTimeTokenById(oneTimeToken.id);
expect(token).toEqual(oneTimeToken);
});
it('should throw when getting a non-existent one-time token', async () => {
await expectRejects(getOneTimeTokenById('non-existent-id'), {
code: 'entity.not_exists_with_id',
status: 404,
});
});
it(`update expired tokens' status to expired when creating new one-time token`, async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({
email,
expiresIn: 1,
});
await waitFor(1001);
await createOneTimeToken({ email });
const reFetchedToken = await getOneTimeTokenById(oneTimeToken.id);
expect(reFetchedToken.status).toBe(OneTimeTokenStatus.Expired);
});
it('should verify one-time token', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({
email,
@ -88,7 +123,7 @@ describe('one time tokens API', () => {
);
});
it('should not succeed to verify one time token with expired token', async () => {
it('should not succeed to verify one-time token with expired token', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({
email,
@ -99,9 +134,7 @@ describe('one time tokens API', () => {
});
// Wait for the token to be expired
await new Promise((resolve) => {
setTimeout(resolve, 2000);
});
await waitFor(1001);
await expectRejects(
verifyOneTimeToken({
@ -115,7 +148,7 @@ describe('one time tokens API', () => {
);
});
it('should not succeed to verify one time token wrong email', async () => {
it('should not succeed to verify one-time token wrong email', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({
email,
@ -136,7 +169,7 @@ describe('one time tokens API', () => {
);
});
it('should not succeed to verify one time token wrong token', async () => {
it('should not succeed to verify one-time token wrong token', async () => {
const email = `foo${generateStandardId()}@bar.com`;
await createOneTimeToken({
email,