Fleshing out the main work behind the email generation.
This commit is contained in:
parent
f445cd4d86
commit
85441c6141
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue