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",
"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",

View File

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

View File

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

View File

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

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 { 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)
}

View File

@ -6,7 +6,7 @@
<span class="subheading">Budibase Password reset</span>
<h2>Please follow the below link to reset your password.</h2>
<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>
</div>
</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,
}
}