Fleshing out the main work behind the email generation.

This commit is contained in:
mike12345567 2021-04-22 17:57:38 +01:00
parent 27846e1bee
commit 737dd356c3
12 changed files with 251 additions and 161 deletions

View File

@ -20,6 +20,7 @@
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "0.0.1", "@budibase/auth": "0.0.1",
"@budibase/string-templates": "^0.8.16",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"aws-sdk": "^2.811.0", "aws-sdk": "^2.811.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",

View File

@ -1,6 +1,10 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const authPkg = require("@budibase/auth") const {
const { utils, StaticDatabases } = authPkg generateConfigID,
StaticDatabases,
getConfigParams,
determineScopedConfig,
} = require("@budibase/auth").db
const GLOBAL_DB = StaticDatabases.GLOBAL.name const GLOBAL_DB = StaticDatabases.GLOBAL.name
@ -11,7 +15,7 @@ exports.save = async function(ctx) {
// Config does not exist yet // Config does not exist yet
if (!configDoc._id) { if (!configDoc._id) {
configDoc._id = utils.generateConfigID({ configDoc._id = generateConfigID({
type, type,
group, group,
user, user,
@ -33,12 +37,11 @@ exports.save = async function(ctx) {
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const response = await db.allDocs( const response = await db.allDocs(
utils.getConfigParams(undefined, { getConfigParams(undefined, {
include_docs: true, include_docs: true,
}) })
) )
const groups = response.rows.map(row => row.doc) ctx.body = response.rows.map(row => row.doc)
ctx.body = groups
} }
/** /**
@ -60,7 +63,7 @@ exports.find = async function(ctx) {
try { try {
// Find the config with the most granular scope based on context // 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, type: ctx.params.type,
user: userId, user: userId,
group, group,

View File

@ -1,59 +1,17 @@
const { const {
generateTemplateID, generateTemplateID,
getTemplateParams,
StaticDatabases, StaticDatabases,
} = require("@budibase/auth").db } = require("@budibase/auth").db
const { CouchDB } = require("../../../db") const { CouchDB } = require("../../../db")
const { const {
TemplatePurposePretty, TemplateMetadata,
TemplateTypes, TemplateBindings,
EmailTemplatePurpose,
TemplatePurpose,
} = require("../../../constants") } = require("../../../constants")
const { getTemplateByPurpose } = require("../../../constants/templates") const { getTemplates } = require("../../../constants/templates")
const GLOBAL_DB = StaticDatabases.GLOBAL.name const GLOBAL_DB = StaticDatabases.GLOBAL.name
const GLOBAL_OWNER = "global" 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 => { exports.save = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const type = ctx.params.type const type = ctx.params.type
@ -77,7 +35,8 @@ exports.save = async ctx => {
exports.definitions = async ctx => { exports.definitions = async ctx => {
ctx.body = { ctx.body = {
purpose: TemplatePurposePretty, purpose: TemplateMetadata,
bindings: Object.values(TemplateBindings),
} }
} }

View File

@ -1,3 +1,6 @@
exports.LOGO_URL =
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
exports.UserStatus = { exports.UserStatus = {
ACTIVE: "active", ACTIVE: "active",
INACTIVE: "inactive", INACTIVE: "inactive",
@ -19,39 +22,48 @@ const TemplateTypes = {
} }
const EmailTemplatePurpose = { const EmailTemplatePurpose = {
HEADER: "header", BASE: "base",
FOOTER: "footer",
STYLES: "styles", STYLES: "styles",
PASSWORD_RECOVERY: "password_recovery", PASSWORD_RECOVERY: "password_recovery",
INVITATION: "invitation", INVITATION: "invitation",
CUSTOM: "custom", 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]: [ [TemplateTypes.EMAIL]: [
{ {
name: "Styling", name: "Styling",
value: EmailTemplatePurpose.STYLES, purpose: EmailTemplatePurpose.STYLES,
bindings: ["url", "company", "companyUrl", "styles", "body"]
}, },
{ {
name: "Header", name: "Base Format",
value: EmailTemplatePurpose.HEADER, purpose: EmailTemplatePurpose.BASE,
}, bindings: ["company", "registrationUrl"]
{
name: "Footer",
value: EmailTemplatePurpose.FOOTER,
}, },
{ {
name: "Password Recovery", name: "Password Recovery",
value: EmailTemplatePurpose.PASSWORD_RECOVERY, purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
}, },
{ {
name: "New User Invitation", name: "New User Invitation",
value: EmailTemplatePurpose.INVITATION, purpose: EmailTemplatePurpose.INVITATION,
}, },
{ {
name: "Custom", name: "Custom",
value: EmailTemplatePurpose.CUSTOM, purpose: EmailTemplatePurpose.CUSTOM,
}, },
], ],
} }
@ -62,4 +74,5 @@ exports.TemplatePurpose = {
} }
exports.TemplateTypes = TemplateTypes exports.TemplateTypes = TemplateTypes
exports.EmailTemplatePurpose = EmailTemplatePurpose exports.EmailTemplatePurpose = EmailTemplatePurpose
exports.TemplatePurposePretty = TemplatePurposePretty exports.TemplateMetadata = TemplateMetadata
exports.TemplateBindings = TemplateBindings

View File

@ -0,0 +1,82 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<style>
{{ styles }}
</style>
</head>
<body>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: auto;">
<tbody>
<tr>
<td class="bg_white logo" style="padding: 1em 2.5em; text-align: center">
<h1><a href="{{ companyUrl }}">{{ company }}</a></h1>
</td>
</tr>
<tr>
<td class="bg_white logo" style="padding: 1em 2.5em; text-align: center">
{{ body }}
</td>
</tr>
<tr>
<td class="bg_white logo" style="padding: 1em 2.5em; text-align: center">
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: auto;">
<tbody><tr>
<td valign="middle" class="bg_black footer email-section">
<table>
<tbody><tr>
<td valign="top" width="50%" style="padding-top: 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tbody><tr>
<td style="text-align: left; padding-right: 10px;">
<h3 class="heading">{{ company }}</h3>
<p>Company information.</p>
</td>
</tr>
</tbody></table>
</td>
<td valign="top" width="50%" style="padding-top: 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tbody><tr>
<td style="text-align: left; padding-left: 10px;">
<h3 class="heading"><a href="{{ url }}">Budibase Platform</a></h3>
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</td>
</tr>
<tr>
<td valign="middle" class="bg_black footer email-section">
<table>
<tbody><tr>
<td valign="top" width="50%">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tbody><tr>
<td style="text-align: left; padding-right: 10px;">
<p>© 2021 Restobar. All Rights Reserved</p>
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>

View File

@ -1,48 +0,0 @@
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: auto;">
<tbody><tr>
<td valign="middle" class="bg_black footer email-section">
<table>
<tbody><tr>
<td valign="top" width="50%" style="padding-top: 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tbody><tr>
<td style="text-align: left; padding-right: 10px;">
<h3 class="heading">{{ company }}</h3>
<p>Company information.</p>
</td>
</tr>
</tbody></table>
</td>
<td valign="top" width="50%" style="padding-top: 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tbody><tr>
<td style="text-align: left; padding-left: 10px;">
<h3 class="heading">Budibase Portal</h3>
{{ portalUrl }}
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</td>
</tr>
<tr>
<td valign="middle" class="bg_black footer email-section">
<table>
<tbody><tr>
<td valign="top" width="50%">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tbody><tr>
<td style="text-align: left; padding-right: 10px;">
<p>© 2021 Restobar. All Rights Reserved</p>
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody>
</table>

View File

@ -1,36 +0,0 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<style>
{{ styles }}
</style>
</head>
<body>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: auto;">
<tbody>
<tr>
<td class="bg_white logo" style="padding: 1em 2.5em; text-align: center">
<h1><a href="{{ companyUrl }}">{{ company }}</a></h1>
</td>
</tr>
<tr>
<td class="bg_white logo" style="padding: 1em 2.5em; text-align: center">
{{ body }}
</td>
</tr>
<tr>
<td class="bg_white logo" style="padding: 1em 2.5em; text-align: center">
{{ footer }}
</td>
</tr>
</tbody>
</table>
</body>

View File

@ -1,6 +1,11 @@
const { readStaticFile } = require("../../utilities/fileSystem") const { readStaticFile } = require("../../utilities/fileSystem")
const { EmailTemplatePurpose } = require("../index") const { EmailTemplatePurpose, TemplateTypes, TemplatePurpose } = require("../index")
const { join } = require("path") const { join } = require("path")
const CouchDB = require("../../db")
const {
getTemplateParams,
StaticDatabases,
} = require("@budibase/auth").db
const TEMPLATE_PATH = join(__dirname, "..", "constants", "templates") const TEMPLATE_PATH = join(__dirname, "..", "constants", "templates")
@ -11,19 +16,56 @@ exports.EmailTemplates = {
[EmailTemplatePurpose.INVITATION]: readStaticFile( [EmailTemplatePurpose.INVITATION]: readStaticFile(
join(TEMPLATE_PATH, "invitation.html") join(TEMPLATE_PATH, "invitation.html")
), ),
[EmailTemplatePurpose.HEADER]: readStaticFile( [EmailTemplatePurpose.BASE]: readStaticFile(
join(TEMPLATE_PATH, "header.html") join(TEMPLATE_PATH, "base.html")
),
[EmailTemplatePurpose.FOOTER]: readStaticFile(
join(TEMPLATE_PATH, "footer.html")
), ),
[EmailTemplatePurpose.STYLES]: readStaticFile( [EmailTemplatePurpose.STYLES]: readStaticFile(
join(TEMPLATE_PATH, "style.css") join(TEMPLATE_PATH, "style.css")
), ),
} }
exports.getTemplateByPurpose = purpose => { exports.addBaseTemplates = (templates, type = null) => {
if (exports.EmailTemplates[purpose]) { let purposeList
return exports.EmailTemplates[purpose] 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)
}

View File

@ -6,7 +6,7 @@
<span class="subheading">Budibase Password reset</span> <span class="subheading">Budibase Password reset</span>
<h2>Please follow the below link to reset your password.</h2> <h2>Please follow the below link to reset your password.</h2>
<p><a href="{{ resetUrl }}" class="btn btn-primary">Reset password</a></p> <p><a href="{{ resetUrl }}" class="btn btn-primary">Reset password</a></p>
<p>This password reset was required for {{ user }} if you did not <p>This password reset was required for {{ email }} if you did not
request this then please contact your administrator.</p> request this then please contact your administrator.</p>
</div> </div>
</td> </td>

View File

@ -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,
})
}

View File

@ -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")
}

View File

@ -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,
}
}