diff --git a/packages/worker/package.json b/packages/worker/package.json index fd43af7b0f..c81e99acf1 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -20,6 +20,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@budibase/auth": "0.0.1", + "@budibase/string-templates": "^0.8.16", "@koa/router": "^8.0.0", "aws-sdk": "^2.811.0", "bcryptjs": "^2.4.3", diff --git a/packages/worker/src/api/controllers/admin/configs.js b/packages/worker/src/api/controllers/admin/configs.js index 67f3405fa4..df19dc9a56 100644 --- a/packages/worker/src/api/controllers/admin/configs.js +++ b/packages/worker/src/api/controllers/admin/configs.js @@ -1,6 +1,10 @@ const CouchDB = require("../../../db") -const authPkg = require("@budibase/auth") -const { utils, StaticDatabases } = authPkg +const { + generateConfigID, + StaticDatabases, + getConfigParams, + determineScopedConfig, +} = require("@budibase/auth").db const GLOBAL_DB = StaticDatabases.GLOBAL.name @@ -11,7 +15,7 @@ exports.save = async function(ctx) { // Config does not exist yet if (!configDoc._id) { - configDoc._id = utils.generateConfigID({ + configDoc._id = generateConfigID({ type, group, user, @@ -33,12 +37,11 @@ exports.save = async function(ctx) { exports.fetch = async function(ctx) { const db = new CouchDB(GLOBAL_DB) const response = await db.allDocs( - utils.getConfigParams(undefined, { + getConfigParams(undefined, { include_docs: true, }) ) - const groups = response.rows.map(row => row.doc) - ctx.body = groups + ctx.body = response.rows.map(row => row.doc) } /** @@ -60,7 +63,7 @@ exports.find = async function(ctx) { try { // Find the config with the most granular scope based on context - const scopedConfig = await authPkg.db.determineScopedConfig(db, { + const scopedConfig = await determineScopedConfig(db, { type: ctx.params.type, user: userId, group, diff --git a/packages/worker/src/api/controllers/admin/templates.js b/packages/worker/src/api/controllers/admin/templates.js index 0314ca6099..f01f8e2176 100644 --- a/packages/worker/src/api/controllers/admin/templates.js +++ b/packages/worker/src/api/controllers/admin/templates.js @@ -1,59 +1,17 @@ const { generateTemplateID, - getTemplateParams, StaticDatabases, } = require("@budibase/auth").db const { CouchDB } = require("../../../db") const { - TemplatePurposePretty, - TemplateTypes, - EmailTemplatePurpose, - TemplatePurpose, + TemplateMetadata, + TemplateBindings, } = require("../../../constants") -const { getTemplateByPurpose } = require("../../../constants/templates") +const { getTemplates } = require("../../../constants/templates") const GLOBAL_DB = StaticDatabases.GLOBAL.name const GLOBAL_OWNER = "global" -function addBaseTemplates(templates, type = null) { - let purposeList - switch (type) { - case TemplateTypes.EMAIL: - purposeList = Object.values(EmailTemplatePurpose) - break - default: - purposeList = Object.values(TemplatePurpose) - break - } - for (let purpose of purposeList) { - // check if a template exists already for purpose - if (templates.find(template => template.purpose === purpose)) { - continue - } - templates.push(getTemplateByPurpose(purpose)) - } - return templates -} - -async function getTemplates({ ownerId, type, id } = {}) { - const db = new CouchDB(GLOBAL_DB) - const response = await db.allDocs( - getTemplateParams(ownerId, id, { - include_docs: true, - }) - ) - let templates = response.rows.map(row => row.doc) - // should only be one template with ID - if (id) { - return templates[0] - } - if (type) { - templates = templates.filter(template => template.type === type) - } - - return addBaseTemplates(templates, type) -} - exports.save = async ctx => { const db = new CouchDB(GLOBAL_DB) const type = ctx.params.type @@ -77,7 +35,8 @@ exports.save = async ctx => { exports.definitions = async ctx => { ctx.body = { - purpose: TemplatePurposePretty, + purpose: TemplateMetadata, + bindings: Object.values(TemplateBindings), } } diff --git a/packages/worker/src/constants/index.js b/packages/worker/src/constants/index.js index 44ca57ea17..bb002bbd6f 100644 --- a/packages/worker/src/constants/index.js +++ b/packages/worker/src/constants/index.js @@ -1,3 +1,6 @@ +exports.LOGO_URL = + "https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" + exports.UserStatus = { ACTIVE: "active", INACTIVE: "inactive", @@ -19,39 +22,48 @@ const TemplateTypes = { } const EmailTemplatePurpose = { - HEADER: "header", - FOOTER: "footer", + BASE: "base", STYLES: "styles", PASSWORD_RECOVERY: "password_recovery", INVITATION: "invitation", CUSTOM: "custom", } -const TemplatePurposePretty = { +const TemplateBindings = { + URL: "url", + COMPANY: "company", + LOGO_URL: "logoUrl", + STYLES: "styles", + BODY: "body", + REGISTRATION_URL: "registrationUrl", + EMAIL: "email", + RESET_URL: "resetUrl", + USER: "user", +} + +const TemplateMetadata = { [TemplateTypes.EMAIL]: [ { name: "Styling", - value: EmailTemplatePurpose.STYLES, + purpose: EmailTemplatePurpose.STYLES, + bindings: ["url", "company", "companyUrl", "styles", "body"] }, { - name: "Header", - value: EmailTemplatePurpose.HEADER, - }, - { - name: "Footer", - value: EmailTemplatePurpose.FOOTER, + name: "Base Format", + purpose: EmailTemplatePurpose.BASE, + bindings: ["company", "registrationUrl"] }, { name: "Password Recovery", - value: EmailTemplatePurpose.PASSWORD_RECOVERY, + purpose: EmailTemplatePurpose.PASSWORD_RECOVERY, }, { name: "New User Invitation", - value: EmailTemplatePurpose.INVITATION, + purpose: EmailTemplatePurpose.INVITATION, }, { name: "Custom", - value: EmailTemplatePurpose.CUSTOM, + purpose: EmailTemplatePurpose.CUSTOM, }, ], } @@ -62,4 +74,5 @@ exports.TemplatePurpose = { } exports.TemplateTypes = TemplateTypes exports.EmailTemplatePurpose = EmailTemplatePurpose -exports.TemplatePurposePretty = TemplatePurposePretty +exports.TemplateMetadata = TemplateMetadata +exports.TemplateBindings = TemplateBindings diff --git a/packages/worker/src/constants/templates/base.html b/packages/worker/src/constants/templates/base.html new file mode 100644 index 0000000000..f728404be8 --- /dev/null +++ b/packages/worker/src/constants/templates/base.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ \ No newline at end of file diff --git a/packages/worker/src/constants/templates/footer.html b/packages/worker/src/constants/templates/footer.html deleted file mode 100644 index 693fd3c0c0..0000000000 --- a/packages/worker/src/constants/templates/footer.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - -
\ No newline at end of file diff --git a/packages/worker/src/constants/templates/header.html b/packages/worker/src/constants/templates/header.html deleted file mode 100644 index 7709bd30a8..0000000000 --- a/packages/worker/src/constants/templates/header.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - -
- \ No newline at end of file diff --git a/packages/worker/src/constants/templates/index.js b/packages/worker/src/constants/templates/index.js index 53db5f2b42..2878a3a4e9 100644 --- a/packages/worker/src/constants/templates/index.js +++ b/packages/worker/src/constants/templates/index.js @@ -1,6 +1,11 @@ const { readStaticFile } = require("../../utilities/fileSystem") -const { EmailTemplatePurpose } = require("../index") +const { EmailTemplatePurpose, TemplateTypes, TemplatePurpose } = require("../index") const { join } = require("path") +const CouchDB = require("../../db") +const { + getTemplateParams, + StaticDatabases, +} = require("@budibase/auth").db const TEMPLATE_PATH = join(__dirname, "..", "constants", "templates") @@ -11,19 +16,56 @@ exports.EmailTemplates = { [EmailTemplatePurpose.INVITATION]: readStaticFile( join(TEMPLATE_PATH, "invitation.html") ), - [EmailTemplatePurpose.HEADER]: readStaticFile( - join(TEMPLATE_PATH, "header.html") - ), - [EmailTemplatePurpose.FOOTER]: readStaticFile( - join(TEMPLATE_PATH, "footer.html") + [EmailTemplatePurpose.BASE]: readStaticFile( + join(TEMPLATE_PATH, "base.html") ), [EmailTemplatePurpose.STYLES]: readStaticFile( join(TEMPLATE_PATH, "style.css") ), } -exports.getTemplateByPurpose = purpose => { - if (exports.EmailTemplates[purpose]) { - return exports.EmailTemplates[purpose] +exports.addBaseTemplates = (templates, type = null) => { + let purposeList + switch (type) { + case TemplateTypes.EMAIL: + purposeList = Object.values(EmailTemplatePurpose) + break + default: + purposeList = Object.values(TemplatePurpose) + break } + for (let purpose of purposeList) { + // check if a template exists already for purpose + if (templates.find(template => template.purpose === purpose)) { + continue + } + if (exports.EmailTemplates[purpose]) { + templates.push(exports.EmailTemplates[purpose]) + } + } + return templates } + +exports.getTemplates = async ({ ownerId, type, id } = {}) => { + const db = new CouchDB(StaticDatabases.GLOBAL.name) + const response = await db.allDocs( + getTemplateParams(ownerId, id, { + include_docs: true, + }) + ) + let templates = response.rows.map(row => row.doc) + // should only be one template with ID + if (id) { + return templates[0] + } + if (type) { + templates = templates.filter(template => template.type === type) + } + return exports.addBaseTemplates(templates, type) +} + +exports.getTemplateByPurpose = async (type, purpose) => { + const templates = await exports.getTemplates({ type }) + return templates.find(template => template.purpose === purpose) +} + diff --git a/packages/worker/src/constants/templates/passwordRecovery.html b/packages/worker/src/constants/templates/passwordRecovery.html index e6b179ec81..11f4eac1f4 100644 --- a/packages/worker/src/constants/templates/passwordRecovery.html +++ b/packages/worker/src/constants/templates/passwordRecovery.html @@ -6,7 +6,7 @@ Budibase Password reset

Please follow the below link to reset your password.

Reset password

-

This password reset was required for {{ user }} if you did not +

This password reset was required for {{ email }} if you did not request this then please contact your administrator.

diff --git a/packages/worker/src/utilities/email.js b/packages/worker/src/utilities/email.js index e69de29bb2..6cab9cc4db 100644 --- a/packages/worker/src/utilities/email.js +++ b/packages/worker/src/utilities/email.js @@ -0,0 +1,36 @@ +const { EmailTemplatePurpose, TemplateTypes } = require("../constants") +const { getTemplateByPurpose } = require("../constants/templates") +const { processString } = require("@budibase/string-templates") +const { getSettingsTemplateContext } = require("./templates") + +const TYPE = TemplateTypes.EMAIL + +const FULL_EMAIL_PURPOSES = [EmailTemplatePurpose.INVITATION, EmailTemplatePurpose.PASSWORD_RECOVERY] + +exports.buildEmail = async (email, user, purpose) => { + // this isn't a full email + if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) { + throw `Unable to build an email of type ${purpose}` + } + let [base, styles, body] = await Promise.all([ + getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE), + getTemplateByPurpose(TYPE, EmailTemplatePurpose.STYLES), + getTemplateByPurpose(TYPE, purpose), + ]) + + // TODO: need to extend the context as much as possible + const context = { + ...await getSettingsTemplateContext(), + email, + user + } + + body = await processString(body, context) + styles = await processString(styles, context) + // this should now be the complete email HTML + return processString(base, { + ...context, + styles, + body, + }) +} \ No newline at end of file diff --git a/packages/worker/src/utilities/index.js b/packages/worker/src/utilities/index.js new file mode 100644 index 0000000000..b402a82cf3 --- /dev/null +++ b/packages/worker/src/utilities/index.js @@ -0,0 +1,9 @@ +/** + * Makes sure that a URL has the correct number of slashes, while maintaining the + * http(s):// double slashes. + * @param {string} url The URL to test and remove any extra double slashes. + * @return {string} The updated url. + */ +exports.checkSlashesInUrl = url => { + return url.replace(/(https?:\/\/)|(\/)+/g, "$1$2") +} diff --git a/packages/worker/src/utilities/templates.js b/packages/worker/src/utilities/templates.js new file mode 100644 index 0000000000..064776647d --- /dev/null +++ b/packages/worker/src/utilities/templates.js @@ -0,0 +1,29 @@ +const CouchDB = require("../../../db") +const { getConfigParams, StaticDatabases } = require("@budibase/auth").db +const { Configs, TemplateBindings, LOGO_URL } = require("../constants") +const { checkSlashesInUrl } = require("./index") +const env = require("../environment") + +const LOCAL_URL = `http://localhost:${env.PORT}` +const BASE_COMPANY = "Budibase" + +exports.getSettingsTemplateContext = async () => { + const db = new CouchDB(StaticDatabases.GLOBAL.name) + const response = await db.allDocs( + getConfigParams(Configs.SETTINGS, { + include_docs: true, + }) + ) + let settings = response.rows.map(row => row.doc)[0] || {} + if (!settings.url) { + settings.url = LOCAL_URL + } + // TODO: need to fully spec out the context + return { + [TemplateBindings.LOGO_URL]: settings.logoUrl || LOGO_URL, + [TemplateBindings.URL]: settings.url, + [TemplateBindings.REGISTRATION_URL]: checkSlashesInUrl(`${settings.url}/registration`), + [TemplateBindings.RESET_URL]: checkSlashesInUrl(`${settings.url}/reset`), + [TemplateBindings.COMPANY]: settings.company || BASE_COMPANY, + } +} \ No newline at end of file