diff --git a/modules/tokens/module.yaml b/modules/tokens/module.yaml index 5527f692..0f3c3bf0 100644 --- a/modules/tokens/module.yaml +++ b/modules/tokens/module.yaml @@ -23,6 +23,9 @@ scripts: validate: name: Validate Token description: Validate a token. Throws an error if the token is invalid. + extend: + name: Extend Token + description: Extend or remove the expiration date of a token. (Only works on valid tokens.) errors: token_not_found: name: Token Not Found diff --git a/modules/tokens/scripts/extend.ts b/modules/tokens/scripts/extend.ts new file mode 100644 index 00000000..cb723fda --- /dev/null +++ b/modules/tokens/scripts/extend.ts @@ -0,0 +1,37 @@ +import { ScriptContext } from "../_gen/scripts/extend.ts"; +import { TokenWithSecret } from "../types/common.ts"; +import { tokenFromRow } from "../types/common.ts"; + +export interface Request { + token: string; + newExpiration: string | null; +} + +export interface Response { + token: TokenWithSecret; +} + +export async function run( + ctx: ScriptContext, + req: Request, +): Promise { + // Ensure the token hasn't expired or been revoked yet + const { token } = await ctx.modules.tokens.validate({ + token: req.token, + }); + + // Update the token's expiration date + const newToken = await ctx.db.token.update({ + where: { + id: token.id, + }, + data: { + expireAt: req.newExpiration, + }, + }); + + // Return the updated token + return { + token: tokenFromRow(newToken), + }; +} diff --git a/modules/tokens/tests/validate.ts b/modules/tokens/tests/validate.ts index f962ee71..945f2586 100644 --- a/modules/tokens/tests/validate.ts +++ b/modules/tokens/tests/validate.ts @@ -2,6 +2,7 @@ import { RuntimeError, test, TestContext } from "../_gen/test.ts"; import { assertEquals, assertRejects, + assertGreater, } from "https://deno.land/std@0.217.0/assert/mod.ts"; test( @@ -10,7 +11,7 @@ test( const error = await assertRejects(async () => { await ctx.modules.tokens.validate({ token: "invalid token" }); }, RuntimeError); - assertEquals(error.code, "TOKEN_NOT_FOUND"); + assertEquals(error.code, "token_not_found"); }, ); @@ -27,7 +28,7 @@ test( const error = await assertRejects(async () => { await ctx.modules.tokens.validate({ token: token.token }); }, RuntimeError); - assertEquals(error.code, "TOKEN_REVOKED"); + assertEquals(error.code, "token_revoked"); }, ); @@ -52,6 +53,50 @@ test( const error = await assertRejects(async () => { await ctx.modules.tokens.validate({ token: token.token }); }, RuntimeError); - assertEquals(error.code, "TOKEN_EXPIRED"); + assertEquals(error.code, "token_expired"); + }, +); + +test( + "validate token extended not expired", + async (ctx: TestContext) => { + const { token } = await ctx.modules.tokens.create({ + type: "test", + meta: { foo: "bar" }, + // Set initial expiration to 200ms in the future + expireAt: new Date(Date.now() + 200).toISOString(), + }); + + // Token should be valid + const validateRes = await ctx.modules.tokens.validate({ + token: token.token, + }); + assertEquals(token.id, validateRes.token.id); + + // Extend token expiration by 10 seconds + await ctx.modules.tokens.extend({ + token: token.token, + newExpiration: new Date(Date.now() + 10000).toISOString(), + }); + + // Wait for 0.5 seconds to ensure token WOULD HAVE expired without + // extension. + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Token should STILL be valid, and have a different `expireAt` time + const validateResAfterWait = await ctx.modules.tokens.validate({ + token: token.token, + }); + + // Assert that everything except `expireAt` is the same and `expireAt` + // is greater. + assertGreater(validateResAfterWait.token.expireAt, token.expireAt); + assertEquals({ + ...validateResAfterWait.token, + expireAt: null, + }, { + ...token, + expireAt: null, + }) }, );