Ghost/ghost/magic-link/lib/MagicLink.js
Daniel Lockyer 54c143a1b4
Fixed optional syntax style for jsdoc
refs https://jsdoc.app/tags-param.html#optional-parameters-and-default-values

- using an equals sign in the type definition is part of the Google
  Closure syntax but we use the JSDoc syntax in all other places, and
  tsc detects the different syntax
- this commit standardizes the syntax ahead of enforcing a certain style
  down the line
2022-10-16 14:48:05 +07:00

155 lines
5.0 KiB
JavaScript

const {IncorrectUsageError, BadRequestError} = require('@tryghost/errors');
const {isEmail} = require('@tryghost/validator');
const tpl = require('@tryghost/tpl');
const messages = {
invalidEmail: 'Email is not valid'
};
/**
* @typedef { import('nodemailer').Transporter } MailTransporter
* @typedef { import('nodemailer').SentMessageInfo } SentMessageInfo
* @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 {
/**
* @param {object} options
* @param {MailTransporter} options.transporter
* @param {TokenProvider<Token, TokenData>} options.tokenProvider
* @param {(token: Token, type: string, referrer?: string) => URL} options.getSigninURL
* @param {typeof defaultGetText} [options.getText]
* @param {typeof defaultGetHTML} [options.getHTML]
* @param {typeof defaultGetSubject} [options.getSubject]
*/
constructor(options) {
if (!options || !options.transporter || !options.tokenProvider || !options.getSigninURL) {
throw new IncorrectUsageError({message: 'Missing options. Expects {transporter, tokenProvider, getSigninURL}'});
}
this.transporter = options.transporter;
this.tokenProvider = options.tokenProvider;
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 {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.referrer=null] - The referrer of the request, if exists
* @returns {Promise<{token: Token, info: SentMessageInfo}>}
*/
async sendMagicLink(options) {
if (!isEmail(options.email)) {
throw new BadRequestError({
message: tpl(messages.invalidEmail)
});
}
const token = await this.tokenProvider.create(options.tokenData);
const type = options.type || 'signin';
const url = this.getSigninURL(token, type, options.referrer);
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};
}
/**
* getMagicLink
*
* @param {object} options
* @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. This type will also get stored in the token data.
* @returns {Promise<URL>} - signin URL
*/
async getMagicLink(options) {
const type = options.type ?? 'signin';
const token = await this.tokenProvider.create({...options.tokenData, type});
return this.getSigninURL(token, type);
}
/**
* getDataFromToken
*
* @param {Token} token - The token to decode
* @returns {Promise<TokenData>} data - The data object associated with the magic link
*/
async getDataFromToken(token) {
const tokenData = await this.tokenProvider.validate(token);
return tokenData;
}
}
/**
* 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!`;
}
module.exports = MagicLink;
module.exports.JWTTokenProvider = require('./JWTTokenProvider');