Updated magic-link to accept a TokenProvider

no-issue

This adds a layer of abstraction between the magic-link module and the
token generation, allowing us to switch out the token generation in the
future, when implementing single use tokens stored in a database
This commit is contained in:
Fabien O'Carroll 2020-09-17 15:52:27 +01:00 committed by Fabien 'egg' O'Carroll
parent 6957c2725b
commit 37c8c15dd6

View File

@ -1,29 +1,38 @@
const jwt = require('jsonwebtoken');
/** /**
* @typedef { import('jsonwebtoken').Secret } Secret
* @typedef { import('nodemailer').Transporter } MailTransporter * @typedef { import('nodemailer').Transporter } MailTransporter
* @typedef { import('nodemailer').SentMessageInfo } SentMessageInfo * @typedef { import('nodemailer').SentMessageInfo } SentMessageInfo
* @typedef { string } JSONWebToken
* @typedef { string } URL * @typedef { string } URL
*/ */
/**
* @template T
* @template D
* @typedef {Object} TokenProvider<T, D>
* @prop {(data: D) => Promise<T>} create
* @prop {(token: T) => Promise<D>} validate
*/
/**
* MagicLink
* @template Token
* @template TokenData
*/
class MagicLink { class MagicLink {
/** /**
* @param {object} options * @param {object} options
* @param {MailTransporter} options.transporter * @param {MailTransporter} options.transporter
* @param {Secret} options.secret * @param {TokenProvider<Token, TokenData>} options.tokenProvider
* @param {(token: JSONWebToken, type: string) => URL} options.getSigninURL * @param {(token: Token, type: string) => URL} options.getSigninURL
* @param {typeof defaultGetText} [options.getText] * @param {typeof defaultGetText} [options.getText]
* @param {typeof defaultGetHTML} [options.getHTML] * @param {typeof defaultGetHTML} [options.getHTML]
* @param {typeof defaultGetSubject} [options.getSubject] * @param {typeof defaultGetSubject} [options.getSubject]
*/ */
constructor(options) { constructor(options) {
if (!options || !options.transporter || !options.secret || !options.getSigninURL) { if (!options || !options.transporter || !options.tokenProvider || !options.getSigninURL) {
throw new Error('Missing options. Expects {transporter, secret, getSigninURL}'); throw new Error('Missing options. Expects {transporter, tokenProvider, getSigninURL}');
} }
this.transporter = options.transporter; this.transporter = options.transporter;
this.secret = options.secret; this.tokenProvider = options.tokenProvider;
this.getSigninURL = options.getSigninURL; this.getSigninURL = options.getSigninURL;
this.getText = options.getText || defaultGetText; this.getText = options.getText || defaultGetText;
this.getHTML = options.getHTML || defaultGetHTML; this.getHTML = options.getHTML || defaultGetHTML;
@ -35,15 +44,12 @@ class MagicLink {
* *
* @param {object} options * @param {object} options
* @param {string} options.email - The email to send magic link to * @param {string} options.email - The email to send magic link to
* @param {object} options.tokenData - The data for token * @param {TokenData} options.tokenData - The data for token
* @param {string=} [options.type='signin'] - The type to be passed to the url and content generator functions * @param {string=} [options.type='signin'] - The type to be passed to the url and content generator functions
* @returns {Promise<{token: JSONWebToken, info: SentMessageInfo}>} * @returns {Promise<{token: Token, info: SentMessageInfo}>}
*/ */
async sendMagicLink(options) { async sendMagicLink(options) {
const token = jwt.sign(options.tokenData, this.secret, { const token = await this.tokenProvider.create(options.tokenData);
algorithm: 'HS256',
expiresIn: '10m'
});
const type = options.type || 'signin'; const type = options.type || 'signin';
@ -63,15 +69,12 @@ class MagicLink {
* getMagicLink * getMagicLink
* *
* @param {object} options * @param {object} options
* @param {object} options.tokenData - The data for token * @param {TokenData} options.tokenData - The data for token
* @param {string=} [options.type='signin'] - The type to be passed to the url and content generator functions * @param {string=} [options.type='signin'] - The type to be passed to the url and content generator functions
* @returns {Promise<URL>} - signin URL * @returns {Promise<URL>} - signin URL
*/ */
async getMagicLink(options) { async getMagicLink(options) {
const token = jwt.sign(options.tokenData, this.secret, { const token = await this.tokenProvider.create(options.tokenData);
algorithm: 'HS256',
expiresIn: '10m'
});
const type = options.type || 'signin'; const type = options.type || 'signin';
@ -81,15 +84,11 @@ class MagicLink {
/** /**
* getDataFromToken * getDataFromToken
* *
* @param {JSONWebToken} token - The token to decode * @param {Token} token - The token to decode
* @returns {Promise<object>} data - The data object associated with the magic link * @returns {Promise<TokenData>} data - The data object associated with the magic link
*/ */
async getDataFromToken(token) { async getDataFromToken(token) {
/** @type {object} */ const tokenData = await this.tokenProvider.validate(token);
const tokenData = (jwt.verify(token, this.secret, {
algorithms: ['HS256'],
maxAge: '10m'
}));
return tokenData; return tokenData;
} }
} }