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 update one-time token status (#7234)

feat(core): add API to update one-time token status
This commit is contained in:
Charles Zhao 2025-04-07 10:43:51 +08:00
parent 391846b5f3
commit ceb5098f0b
No known key found for this signature in database
GPG key ID: 55CFA7C080C98029
5 changed files with 132 additions and 10 deletions

View file

@ -5,9 +5,17 @@ import type Queries from '#src/tenants/Queries.js';
import assertThat from '#src/utils/assert-that.js';
export const createOneTimeTokenLibrary = (queries: Queries) => {
const { updateOneTimeTokenStatus: updateOneTimeTokenStatusQuery, getOneTimeTokenByToken } =
queries.oneTimeTokens;
const {
updateOneTimeTokenStatus: updateOneTimeTokenStatusQuery,
getOneTimeTokenById,
getOneTimeTokenByToken,
} = queries.oneTimeTokens;
const updateOneTimeTokenStatusById = async (id: string, status: OneTimeTokenStatus) => {
const oneTimeTokenRecord = await getOneTimeTokenById(id);
return updateOneTimeTokenStatus(oneTimeTokenRecord.token, status);
};
const updateOneTimeTokenStatus = async (token: string, status: OneTimeTokenStatus) => {
assertThat(status !== OneTimeTokenStatus.Active, 'one_time_token.cannot_reactivate_token');
@ -48,5 +56,10 @@ export const createOneTimeTokenLibrary = (queries: Queries) => {
return updateOneTimeTokenStatus(token, OneTimeTokenStatus.Consumed);
};
return { checkOneTimeToken, updateOneTimeTokenStatus, verifyOneTimeToken };
return {
checkOneTimeToken,
updateOneTimeTokenStatus,
updateOneTimeTokenStatusById,
verifyOneTimeToken,
};
};

View file

@ -76,6 +76,30 @@
}
}
}
},
"/api/one-time-tokens/{id}/status": {
"put": {
"summary": "Update one-time token status",
"description": "Update the status of a one-time token by its ID. This can be used to mark the token as consumed or expired.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"status": {
"description": "The new status of the one-time token."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The one-time token status was updated successfully."
}
}
}
}
}
}

View file

@ -1,4 +1,4 @@
import { OneTimeTokens } from '@logto/schemas';
import { OneTimeTokens, oneTimeTokenStatusGuard } from '@logto/schemas';
import { generateStandardId, generateStandardSecret } from '@logto/shared';
import { trySafe } from '@silverhand/essentials';
import { addSeconds } from 'date-fns';
@ -23,7 +23,7 @@ export default function oneTimeTokenRoutes<T extends ManagementApiRouter>(
},
},
libraries: {
oneTimeTokens: { verifyOneTimeToken },
oneTimeTokens: { verifyOneTimeToken, updateOneTimeTokenStatusById },
},
},
]: RouterInitArgs<T>
@ -103,4 +103,28 @@ export default function oneTimeTokenRoutes<T extends ManagementApiRouter>(
return next();
}
);
router.put(
'/one-time-tokens/:id/status',
koaGuard({
params: z.object({
id: z.string().min(1),
}),
body: z.object({
status: oneTimeTokenStatusGuard,
}),
response: OneTimeTokens.guard,
status: [200, 400, 404],
}),
async (ctx, next) => {
const { params, body } = ctx.guard;
const { id } = params;
const { status } = body;
const oneTimeToken = await updateOneTimeTokenStatusById(id, status);
ctx.body = oneTimeToken;
ctx.status = 200;
return next();
}
);
}

View file

@ -1,4 +1,4 @@
import { type OneTimeToken } from '@logto/schemas';
import { type OneTimeTokenStatus, type OneTimeToken } from '@logto/schemas';
import { authedAdminApi } from './api.js';
@ -23,3 +23,10 @@ export const verifyOneTimeToken = async (
export const getOneTimeTokenById = async (id: string) =>
authedAdminApi.get(`one-time-tokens/${id}`).json<OneTimeToken>();
export const updateOneTimeTokenStatus = async (id: string, status: OneTimeTokenStatus) =>
authedAdminApi
.put(`one-time-tokens/${id}/status`, {
json: { status },
})
.json<OneTimeToken>();

View file

@ -5,6 +5,7 @@ import {
createOneTimeToken,
verifyOneTimeToken,
getOneTimeTokenById,
updateOneTimeTokenStatus,
} from '#src/api/one-time-token.js';
import { expectRejects } from '#src/helpers/index.js';
import { devFeatureTest, waitFor } from '#src/utils.js';
@ -190,9 +191,62 @@ describe('one-time tokens API', () => {
);
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
it('should throw token_expired error and update token status to expired (token already expired but status is not updated)', async () => {});
it('should throw token_expired error and update token status to expired (token already expired but status is not updated)', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({ email, expiresIn: 1 });
// eslint-disable-next-line @typescript-eslint/no-empty-function
it('should throw token_revoked error', async () => {});
await waitFor(1001);
await expectRejects(
verifyOneTimeToken({
email,
token: oneTimeToken.token,
}),
{
code: 'one_time_token.token_expired',
status: 400,
}
);
const updatedToken = await getOneTimeTokenById(oneTimeToken.id);
expect(updatedToken.status).toBe(OneTimeTokenStatus.Expired);
});
it('should be able to revoke a token by updating the status', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({ email, expiresIn: 1 });
await updateOneTimeTokenStatus(oneTimeToken.id, OneTimeTokenStatus.Revoked);
const updatedToken = await getOneTimeTokenById(oneTimeToken.id);
expect(updatedToken.status).toBe(OneTimeTokenStatus.Revoked);
});
it('should throw when trying to re-activate a token', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({ email, expiresIn: 1 });
await expectRejects(updateOneTimeTokenStatus(oneTimeToken.id, OneTimeTokenStatus.Active), {
code: 'one_time_token.cannot_reactivate_token',
status: 400,
});
});
it('should throw when verifying a revoked token', async () => {
const email = `foo${generateStandardId()}@bar.com`;
const oneTimeToken = await createOneTimeToken({ email });
await updateOneTimeTokenStatus(oneTimeToken.id, OneTimeTokenStatus.Revoked);
await expectRejects(
verifyOneTimeToken({
email,
token: oneTimeToken.token,
}),
{
code: 'one_time_token.token_revoked',
status: 400,
}
);
});
});