UNPKG

lbx-jwt

Version:

Provides JWT authentication for loopback applications. Includes storing roles inside tokens and handling refreshing. Built-in reuse detection.

268 lines (235 loc) 9.89 kB
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import path from 'path'; import { Transporter } from 'nodemailer'; import { BaseDefaultDynamicReplacements, BaseDefaultStaticReplacements } from './base-default.replacements'; import { Email } from './email.model'; import { ResetPasswordMailReplacements } from './reset-password-mail.replacements'; import { HandlebarsUtilities } from '../../encapsulation/handlebars.utilities'; import { BaseUserWithRelations } from '../../models'; import { PasswordResetTokenWithRelations } from '../../models/password-reset-token.model'; /** * The directory for jwt mail templates (eg. For Password reset.). */ export const LBX_JWT_MAIL_TEMPLATE_DIRECTORY: string = path.join(__dirname, './templates'); /** * A service that handles sending emails to users. */ export abstract class BaseMailService< RoleType extends string, DefaultStaticReplacementsType extends BaseDefaultStaticReplacements = BaseDefaultStaticReplacements > { /** * The name of the mail address that sends any automated emails. */ protected abstract readonly WEBSERVER_MAIL: string; /** * The base link before '.../token' for resetting the users password. */ protected abstract readonly BASE_RESET_PASSWORD_LINK: string; /** * The path to the base email template. */ protected readonly BASE_MAIL_TEMPLATE_PATH: string = `${LBX_JWT_MAIL_TEMPLATE_DIRECTORY}/base-mail.hbs`; /** * The email transporter that sends all the emails. */ protected abstract readonly webserverMailTransporter: Transporter; /** * Whether or not this service is currently in production mode. * This is needed to determine if the email should be sent, or if the email content should be saved locally for testing purposes. */ protected abstract readonly PRODUCTION: boolean; /** * The path where emails should be saved to when this service is not in production mode. */ protected abstract readonly SAVED_EMAILS_PATH: string; /** * The url for the logo that is placed inside the header. */ protected readonly LOGO_HEADER_URL?: string; /** * The width of the logo placed inside the header. */ protected readonly LOGO_HEADER_WIDTH: number = 165; /** * The url for the logo that is placed inside the footer. */ protected readonly LOGO_FOOTER_URL?: string; /** * The width of the logo placed inside the footer. */ protected readonly LOGO_FOOTER_WIDTH: number = 500; /** * Lines of the address that is displayed inside the footer. */ protected abstract readonly ADDRESS_LINES: string[]; /** * The label for Password Reset. * @default 'Password Reset' */ protected readonly PASSWORD_RESET_LABEL: string = 'Password Reset'; /** * A css color value for the address lines in the footer. */ protected readonly ADDRESS_LINES_COLOR: string = '#999999'; /** * A css color value for the background of emails. * @default 'whitesmoke' */ protected readonly BACKGROUND_COLOR: string = 'whitesmoke'; /** * A css color value for the background of the content box of the email. * @default 'white' */ protected readonly CONTENT_BACKGROUND_COLOR: string = 'white'; /** * A css color value for any text elements. * @default '#363636' */ protected readonly TEXT_COLOR: string = '#363636'; /** * The default css font family value for text elements. * @default 'Arial, sans-serif' */ protected readonly DEFAULT_FONT_FAMILY: string = 'Arial, sans-serif'; /** * A css color value for headline text elements. * @default '#363636' */ protected readonly HEADLINE_TEXT_COLOR: string = '#363636'; /** * The css for button elements. */ protected readonly BUTTON_CSS: string = ` display: inline-block; font-weight: bold; padding: 15px; padding-left: 20px; padding-right: 20px; background-color: white; transition: background-color .3s; color: black; text-decoration: none; border-radius: 5px; box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12); `; /** * The css for button elements that are hovered. */ protected readonly BUTTON_HOVER_CSS: string = 'background-color: whitesmoke;'; // eslint-disable-next-line jsdoc/require-returns /** * Defines static replacements that are useful for multiple email templates. * * By default this contains: * - addressLine1 * - addressLine2 * - logoHeaderUrl * - logoFooterUrl. * * Does not contain the html title, as this is unique per template and also not required. */ protected get defaultStaticReplacements(): DefaultStaticReplacementsType { const res: BaseDefaultStaticReplacements = { addressLines: this.ADDRESS_LINES, addressLinesColor: this.ADDRESS_LINES_COLOR, logoHeaderUrl: this.LOGO_HEADER_URL, logoHeaderWidth: this.LOGO_HEADER_WIDTH, logoFooterUrl: this.LOGO_FOOTER_URL, logoFooterWidth: this.LOGO_FOOTER_WIDTH, backgroundColor: this.BACKGROUND_COLOR, contentBackgroundColor: this.CONTENT_BACKGROUND_COLOR, textColor: this.TEXT_COLOR, defaultFontFamily: this.DEFAULT_FONT_FAMILY, headlineTextColor: this.HEADLINE_TEXT_COLOR, buttonCss: this.BUTTON_CSS, buttonHoverCss: this.BUTTON_HOVER_CSS }; return res as DefaultStaticReplacementsType; } constructor() { } /** * Sends an email for resetting the password of a specific user. * Contains a link that is active for a limited amount of time. * @param user - The user that should receive the email. * @param resetToken - The reset token needed to generate the link. */ async sendResetPasswordMail(user: BaseUserWithRelations<RoleType>, resetToken: PasswordResetTokenWithRelations): Promise<void> { const replacements: BaseDefaultStaticReplacements & BaseDefaultDynamicReplacements = { headline: this.PASSWORD_RESET_LABEL, title: this.PASSWORD_RESET_LABEL, content: this.getResetPasswordContent(resetToken, user), ...this.defaultStaticReplacements }; const email: Email = { to: user.email, from: this.WEBSERVER_MAIL, subject: this.PASSWORD_RESET_LABEL, html: this.getTemplate(this.BASE_MAIL_TEMPLATE_PATH)(replacements) }; await this.handleEmail(email); } /** * Gets the content for the reset password email. * @param resetToken - The reset token needed for resetting the password. * @param user - The user that tries to reset his password. * @returns The finished html string that will be inserted inside the base template. */ protected getResetPasswordContent(resetToken: PasswordResetTokenWithRelations, user: BaseUserWithRelations<RoleType>): string { const contentReplacements: ResetPasswordMailReplacements = { link: `${this.BASE_RESET_PASSWORD_LINK}/${resetToken.value}`, firstLine: this.getFirstLineForUser(user), paragraphsBeforeButton: [ 'someone requested to change the password for your account.', 'Follow the link below to proceed:' ], resetPasswordButtonLabel: 'Reset Password', paragraphsAfterButton: [ 'This link is valid for the next 5 minutes.', 'If you did not request to change your password, just ignore this email and your password will remain unchanged.' ] }; return this.getTemplate(`${LBX_JWT_MAIL_TEMPLATE_DIRECTORY}/reset-password.hbs`)(contentReplacements); } /** * Defines what to do with the email. * In a production environment this sends the email to the recipients. * In a non production environment this saves the email data in a file for testing purposes. * @param email - The email that should be handled. */ protected async handleEmail(email: Email): Promise<void> { if (this.PRODUCTION) { await this.webserverMailTransporter.sendMail(email); return; } if (!existsSync(this.SAVED_EMAILS_PATH)) { mkdirSync(this.SAVED_EMAILS_PATH); } // for testing emails writeFileSync(`${this.SAVED_EMAILS_PATH}/${email.subject.replaceAll(' ', '')}.test.html`, email.html); } /** * Gets the first line for the email based on the user the mail is sent to. * This can be used to address the user correctly. * @param user - The user that should receive the email. * @returns The string that should be the first line inside the email. */ /** * Gets the first line to use in an email (eg. "Dear Mr. X") based on the user that the email is sent to. * @param user - The user that the email is sent to. * @returns A string, most likely some sort of greeting. */ // eslint-disable-next-line unusedImports/no-unused-vars protected getFirstLineForUser(user: BaseUserWithRelations<RoleType>): string { return 'Hi,'; } /** * Gets the handlebars html template from the given path. * @param path - The path of the template. * @returns The compiled handlebars template. */ protected getTemplate(path: string): HandlebarsTemplateDelegate<unknown> { const sourceData: string = readFileSync(path, 'utf8').toString(); return HandlebarsUtilities.compile(sourceData); } }