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 delete one-time token

This commit is contained in:
Charles Zhao 2025-04-04 13:35:48 +08:00
parent ceb5098f0b
commit 76f31472f5
No known key found for this signature in database
GPG key ID: 55CFA7C080C98029
5 changed files with 83 additions and 2 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 { buildDeleteByIdWithPool } from '#src/database/delete-by-id.js';
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';
@ -15,6 +16,8 @@ export const createOneTimeTokenQueries = (pool: CommonQueryMethods) => {
const getOneTimeTokenById = buildFindEntityByIdWithPool(pool)(OneTimeTokens);
const deleteOneTimeTokenById = buildDeleteByIdWithPool(pool, OneTimeTokens.table);
const updateExpiredOneTimeTokensStatusByEmail = async (email: string) =>
pool.query(sql`
update ${table}
@ -40,6 +43,7 @@ export const createOneTimeTokenQueries = (pool: CommonQueryMethods) => {
`);
return {
deleteOneTimeTokenById,
insertOneTimeToken,
updateExpiredOneTimeTokensStatusByEmail,
getOneTimeTokenById,

View file

@ -48,6 +48,15 @@
"description": "The one-time token found by ID."
}
}
},
"delete": {
"summary": "Delete one-time token by ID",
"description": "Delete a one-time token by its ID.",
"responses": {
"204": {
"description": "The one-time token was deleted successfully."
}
}
}
},
"/api/one-time-tokens/verify": {

View file

@ -17,6 +17,7 @@ export default function oneTimeTokenRoutes<T extends ManagementApiRouter>(
{
queries: {
oneTimeTokens: {
deleteOneTimeTokenById,
insertOneTimeToken,
updateExpiredOneTimeTokensStatusByEmail,
getOneTimeTokenById,
@ -127,4 +128,22 @@ export default function oneTimeTokenRoutes<T extends ManagementApiRouter>(
return next();
}
);
router.delete(
'/one-time-tokens/:id',
koaGuard({
params: z.object({
id: z.string().min(1),
}),
status: [200, 400, 404],
}),
async (ctx, next) => {
const { params } = ctx.guard;
const { id } = params;
await deleteOneTimeTokenById(id);
ctx.status = 204;
return next();
}
);
}

View file

@ -30,3 +30,6 @@ export const updateOneTimeTokenStatus = async (id: string, status: OneTimeTokenS
json: { status },
})
.json<OneTimeToken>();
export const deleteOneTimeTokenById = async (id: string) =>
authedAdminApi.delete(`one-time-tokens/${id}`).json<OneTimeToken>();

View file

@ -6,6 +6,7 @@ import {
verifyOneTimeToken,
getOneTimeTokenById,
updateOneTimeTokenStatus,
deleteOneTimeTokenById,
} from '#src/api/one-time-token.js';
import { expectRejects } from '#src/helpers/index.js';
import { devFeatureTest, waitFor } from '#src/utils.js';
@ -25,6 +26,8 @@ describe('one-time tokens API', () => {
expect(oneTimeToken.context).toEqual({});
expect(oneTimeToken.email).toBe(email);
expect(oneTimeToken.token.length).toBe(32);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should create one-time token with custom expiration time', async () => {
@ -39,6 +42,8 @@ describe('one-time tokens API', () => {
expect(oneTimeToken.status).toBe(OneTimeTokenStatus.Active);
expect(oneTimeToken.email).toBe(email);
expect(oneTimeToken.token.length).toBe(32);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should create one-time token with `applicationIds` and `jitOrganizationIds` configured', async () => {
@ -56,6 +61,8 @@ describe('one-time tokens API', () => {
jitOrganizationIds: ['org-1'],
});
expect(oneTimeToken.token.length).toBe(32);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should be able to get one-time token by its ID', async () => {
@ -69,6 +76,8 @@ describe('one-time tokens API', () => {
const token = await getOneTimeTokenById(oneTimeToken.id);
expect(token).toEqual(oneTimeToken);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should throw when getting a non-existent one-time token', async () => {
@ -86,10 +95,13 @@ describe('one-time tokens API', () => {
});
await waitFor(1001);
await createOneTimeToken({ email });
const newOneTimeToken = await createOneTimeToken({ email });
const reFetchedToken = await getOneTimeTokenById(oneTimeToken.id);
expect(reFetchedToken.status).toBe(OneTimeTokenStatus.Expired);
void deleteOneTimeTokenById(oneTimeToken.id);
void deleteOneTimeTokenById(newOneTimeToken.id);
});
it('should verify one-time token', async () => {
@ -122,6 +134,8 @@ describe('one-time tokens API', () => {
status: 400,
}
);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should not succeed to verify one-time token with expired token', async () => {
@ -147,6 +161,8 @@ describe('one-time tokens API', () => {
status: 400,
}
);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should not succeed to verify one-time token wrong email', async () => {
@ -168,11 +184,13 @@ describe('one-time tokens API', () => {
status: 400,
}
);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should not succeed to verify one-time token wrong token', async () => {
const email = `foo${generateStandardId()}@bar.com`;
await createOneTimeToken({
const oneTimeToken = await createOneTimeToken({
email,
context: {
jitOrganizationIds: ['org-1'],
@ -189,6 +207,7 @@ describe('one-time tokens API', () => {
status: 404,
}
);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should throw token_expired error and update token status to expired (token already expired but status is not updated)', async () => {
@ -210,6 +229,8 @@ describe('one-time tokens API', () => {
const updatedToken = await getOneTimeTokenById(oneTimeToken.id);
expect(updatedToken.status).toBe(OneTimeTokenStatus.Expired);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should be able to revoke a token by updating the status', async () => {
@ -220,6 +241,8 @@ describe('one-time tokens API', () => {
const updatedToken = await getOneTimeTokenById(oneTimeToken.id);
expect(updatedToken.status).toBe(OneTimeTokenStatus.Revoked);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should throw when trying to re-activate a token', async () => {
@ -230,6 +253,8 @@ describe('one-time tokens API', () => {
code: 'one_time_token.cannot_reactivate_token',
status: 400,
});
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should throw when verifying a revoked token', async () => {
@ -248,5 +273,26 @@ describe('one-time tokens API', () => {
status: 400,
}
);
void deleteOneTimeTokenById(oneTimeToken.id);
});
it('should throw when trying to delete a non-existent one-time token', async () => {
await expectRejects(deleteOneTimeTokenById('non-existent-id'), {
code: 'entity.not_found',
status: 404,
});
});
it('should delete the one-time token successfully', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({ email });
await deleteOneTimeTokenById(oneTimeToken.id);
await expectRejects(getOneTimeTokenById(oneTimeToken.id), {
code: 'entity.not_exists_with_id',
status: 404,
});
});
});