Ghost/ghost/magic-link/index.js
Fabien O'Carroll 483654a4b6 Removed user object from magic links
no-issue

This means magic link will rely solely on the `sub` claim for identifying the user
2019-10-11 11:58:23 +07:00

129 lines
4.0 KiB
JavaScript

const jwt = require('jsonwebtoken');
module.exports = MagicLink;
/**
* @typedef { import('jsonwebtoken').Secret } Secret
* @typedef { import('nodemailer').Transporter } MailTransporter
* @typedef { import('nodemailer').SentMessageInfo } SentMessageInfo
* @typedef { string } JSONWebToken
* @typedef { string } URL
*/
/**
* defaultGetText
*
* @param {URL} url - The url which will trigger sign in flow
* @param {string} type - The type of email to send e.g. signin, signup
* @param {string} email - The recipient of the email to send
* @returns {string} text - The text content of an email to send
*/
function defaultGetText(url, type, email) {
let msg = 'sign in';
if (type === 'signup') {
msg = 'confirm your email address';
}
return `Click here to ${msg} ${url}. This msg was sent to ${email}`;
}
/**
* defaultGetHTML
*
* @param {URL} url - The url which will trigger sign in flow
* @param {string} type - The type of email to send e.g. signin, signup
* @param {string} email - The recipient of the email to send
* @returns {string} HTML - The HTML content of an email to send
*/
function defaultGetHTML(url, type, email) {
let msg = 'sign in';
if (type === 'signup') {
msg = 'confirm your email address';
}
return `<a href="${url}">Click here to ${msg}</a> This msg was sent to ${email}`;
}
/**
* defaultGetSubject
*
* @param {string} type - The type of email to send e.g. signin, signup
* @returns {string} subject - The subject of an email to send
*/
function defaultGetSubject(type) {
if (type === 'signup') {
return `Signup!`;
}
return `Signin!`;
}
/**
* MagicLink
* @constructor
*
* @param {object} options
* @param {MailTransporter} options.transporter
* @param {Secret} options.secret
* @param {(token: JSONWebToken, type: string) => URL} options.getSigninURL
* @param {typeof defaultGetText} [options.getText]
* @param {typeof defaultGetHTML} [options.getHTML]
* @param {typeof defaultGetSubject} [options.getSubject]
*/
function MagicLink(options) {
if (!options || !options.transporter || !options.secret || !options.getSigninURL) {
throw new Error('Missing options. Expects {transporter, secret, getSigninURL}');
}
this.transporter = options.transporter;
this.secret = options.secret;
this.getSigninURL = options.getSigninURL;
this.getText = options.getText || defaultGetText;
this.getHTML = options.getHTML || defaultGetHTML;
this.getSubject = options.getSubject || defaultGetSubject;
}
/**
* sendMagicLink
*
* @param {object} options
* @param {string} options.email - The email to send magic link to
* @param {object} options.subject - The subject to associate with the magic link (user id, or email)
* @param {string=} [options.type='signin'] - The type to be passed to the url and content generator functions
* @returns {Promise<{token: JSONWebToken, info: SentMessageInfo}>}
*/
MagicLink.prototype.sendMagicLink = async function sendMagicLink(options) {
const token = jwt.sign({}, this.secret, {
audience: '@tryghost/magic-link',
issuer: '@tryghost/magic-link',
algorithm: 'HS256',
subject: options.subject,
expiresIn: '10m'
});
const type = options.type || 'signin';
const url = this.getSigninURL(token, type);
const info = await this.transporter.sendMail({
to: options.email,
subject: this.getSubject(type),
text: this.getText(url, type, options.email),
html: this.getHTML(url, type, options.email)
});
return {token, info};
};
/**
* getUserFromToken
*
* @param {JSONWebToken} token - The token to decode
* @returns {object} user - The user object associated with the magic link
*/
MagicLink.prototype.getUserFromToken = function getUserFromToken(token) {
/** @type {object} */
const claims = jwt.verify(token, this.secret, {
audience: '@tryghost/magic-link',
issuer: '@tryghost/magic-link',
algorithms: ['HS256'],
maxAge: '10m'
});
return claims.sub;
};