UNPKG

ghost

Version:

The professional publishing platform

112 lines (97 loc) 3.77 kB
// @ts-check const {ValidationError} = require('@tryghost/errors'); class SingleUseTokenProvider { /** * @param {Object} dependencies * @param {import('../../models/base')} dependencies.SingleUseTokenModel - A model for creating and retrieving tokens. * @param {number} dependencies.validityPeriod - How long a token is valid for from it's creation in milliseconds. * @param {number} dependencies.validityPeriodAfterUsage - How long a token is valid after first usage, in milliseconds. * @param {number} dependencies.maxUsageCount - How many times a token can be used. */ constructor({SingleUseTokenModel, validityPeriod, validityPeriodAfterUsage, maxUsageCount}) { this.model = SingleUseTokenModel; this.validityPeriod = validityPeriod; this.validityPeriodAfterUsage = validityPeriodAfterUsage; this.maxUsageCount = maxUsageCount; } /** * @method create * Creates and stores a token, with the passed data associated with it. * Returns the created token value. * * @param {Object<string, any>} data * * @returns {Promise<string>} */ async create(data) { const model = await this.model.add({ data: JSON.stringify(data) }); return model.get('token'); } /** * @method validate * Validates a token, returning any parsable data associated. * If the token is invalid the returned Promise will reject. * * @param {string} token * * @returns {Promise<Object<string, any>>} */ async validate(token, options = {}) { if (!options.transacting) { return await this.model.transaction((transacting) => { return this.validate(token, { ...options, transacting }); }); } const model = await this.model.findOne({token}, {transacting: options.transacting, forUpdate: true}); if (!model) { throw new ValidationError({ message: 'Invalid token provided' }); } if (model.get('used_count') >= this.maxUsageCount) { throw new ValidationError({ message: 'Token expired' }); } const createdAtEpoch = model.get('created_at').getTime(); const firstUsedAtEpoch = model.get('first_used_at')?.getTime() ?? createdAtEpoch; // Is this token already used? if (model.get('used_count') > 0) { const timeSinceFirstUsage = Date.now() - firstUsedAtEpoch; if (timeSinceFirstUsage > this.validityPeriodAfterUsage) { throw new ValidationError({ message: 'Token expired' }); } } const tokenLifetimeMilliseconds = Date.now() - createdAtEpoch; if (tokenLifetimeMilliseconds > this.validityPeriod) { throw new ValidationError({ message: 'Token expired' }); } if (!model.get('first_used_at')) { await model.save({ first_used_at: new Date(), updated_at: new Date(), used_count: model.get('used_count') + 1 }, {autoRefresh: false, patch: true, transacting: options.transacting}); } else { await model.save({ used_count: model.get('used_count') + 1, updated_at: new Date() }, {autoRefresh: false, patch: true, transacting: options.transacting}); } try { return JSON.parse(model.get('data')); } catch (err) { return {}; } } } module.exports = SingleUseTokenProvider;