diff --git a/packages/server/src/tests/utilities/TestConfiguration.js b/packages/server/src/tests/utilities/TestConfiguration.js index 9b4328ba3a..c031cf082d 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.js +++ b/packages/server/src/tests/utilities/TestConfiguration.js @@ -50,6 +50,7 @@ class TestConfiguration { request.config = { jwtSecret: env.JWT_SECRET } request.appId = this.appId request.user = { appId: this.appId } + request.query = {} request.request = { body: config, } diff --git a/packages/worker/src/api/controllers/admin/configs.js b/packages/worker/src/api/controllers/admin/configs.js index dfd616fc6d..c8d1c3d632 100644 --- a/packages/worker/src/api/controllers/admin/configs.js +++ b/packages/worker/src/api/controllers/admin/configs.js @@ -34,7 +34,7 @@ exports.save = async function(ctx) { } try { - const response = await db.post(config) + const response = await db.put(config) ctx.body = { type, _id: response.id, diff --git a/packages/worker/src/api/controllers/admin/email.js b/packages/worker/src/api/controllers/admin/email.js index f11edbd2e7..9f4060d20f 100644 --- a/packages/worker/src/api/controllers/admin/email.js +++ b/packages/worker/src/api/controllers/admin/email.js @@ -16,6 +16,7 @@ const TYPE = TemplateTypes.EMAIL const FULL_EMAIL_PURPOSES = [ EmailTemplatePurpose.INVITATION, EmailTemplatePurpose.PASSWORD_RECOVERY, + EmailTemplatePurpose.WELCOME, ] async function buildEmail(purpose, email, user) { diff --git a/packages/worker/src/api/routes/admin/configs.js b/packages/worker/src/api/routes/admin/configs.js index 4b9b8396cf..5865259a29 100644 --- a/packages/worker/src/api/routes/admin/configs.js +++ b/packages/worker/src/api/routes/admin/configs.js @@ -27,6 +27,7 @@ function settingValidation() { return Joi.object({ platformUrl: Joi.string().valid("", null), logoUrl: Joi.string().valid("", null), + docsUrl: Joi.string().valid("", null), company: Joi.string().required(), }).unknown(true) } diff --git a/packages/worker/src/api/routes/tests/realEmail.spec.js b/packages/worker/src/api/routes/tests/realEmail.spec.js new file mode 100644 index 0000000000..c96b8ab561 --- /dev/null +++ b/packages/worker/src/api/routes/tests/realEmail.spec.js @@ -0,0 +1,60 @@ +const setup = require("./utilities") +const { EmailTemplatePurpose } = require("../../../constants") +const nodemailer = require("nodemailer") +const fetch = require("node-fetch") + +describe("/api/admin/email", () => { + let request = setup.getRequest() + let config = setup.getConfig() + + beforeAll(async () => { + await config.init() + }) + + afterAll(setup.afterAll) + + async function sendRealEmail(purpose) { + await config.saveEtherealSmtpConfig() + await config.saveSettingsConfig() + const res = await request + .post(`/api/admin/email/send`) + .send({ + email: "test@test.com", + purpose, + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.message).toBeDefined() + const testUrl = nodemailer.getTestMessageUrl(res.body) + expect(testUrl).toBeDefined() + const response = await fetch(testUrl) + const text = await response.text() + let toCheckFor + switch (purpose) { + case EmailTemplatePurpose.WELCOME: + toCheckFor = `Thanks for getting started with Budibase's Budibase platform.` + break + case EmailTemplatePurpose.INVITATION: + toCheckFor = `Use the button below to set up your account and get started:` + break + case EmailTemplatePurpose.PASSWORD_RECOVERY: + toCheckFor = `You recently requested to reset your password for your Budibase account in your Budibase platform` + break + } + expect(text).toContain(toCheckFor) + } + + it("should be able to send a welcome email", async () => { + await sendRealEmail(EmailTemplatePurpose.WELCOME) + + }) + + it("should be able to send a invitation email", async () => { + await sendRealEmail(EmailTemplatePurpose.INVITATION) + }) + + it("should be able to send a password recovery email", async () => { + const res = await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY) + }) +}) \ No newline at end of file diff --git a/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js b/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js index 900e5dea13..7ef4fa9caa 100644 --- a/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js +++ b/packages/worker/src/api/routes/tests/utilities/TestConfiguration.js @@ -3,7 +3,7 @@ const controllers = require("./controllers") const supertest = require("supertest") const { jwt } = require("@budibase/auth").auth const { Cookies } = require("@budibase/auth").constants -const { Configs } = require("../../../../constants") +const { Configs, LOGO_URL } = require("../../../../constants") class TestConfiguration { constructor(openServer = true) { @@ -26,6 +26,7 @@ class TestConfiguration { request.config = { jwtSecret: env.JWT_SECRET } request.appId = this.appId request.user = { appId: this.appId } + request.query = {} request.request = { body: config, } @@ -62,18 +63,34 @@ class TestConfiguration { } } + async deleteConfig(type) { + try { + const cfg = await this._req(null,{ + type, + }, controllers.config.find) + if (cfg) { + await this._req(null, { + id: cfg._id, + rev: cfg._rev, + }, controllers.config.destroy) + } + } catch (err) {} + } + async saveSettingsConfig() { + await this.deleteConfig(Configs.SETTINGS) await this._req({ type: Configs.SETTINGS, config: { platformUrl: "http://localhost:10000", - logoUrl: "http://localhost:10000/logo", - company: "TestCompany", + logoUrl: LOGO_URL, + company: "Budibase", } }, null, controllers.config.save) } async saveSmtpConfig() { + await this.deleteConfig(Configs.SMTP) await this._req({ type: Configs.SMTP, config: { @@ -84,6 +101,22 @@ class TestConfiguration { } }, null, controllers.config.save) } + + async saveEtherealSmtpConfig() { + await this.deleteConfig(Configs.SMTP) + await this._req({ + type: Configs.SMTP, + config: { + port: 587, + host: "smtp.ethereal.email", + secure: false, + auth: { + user: "don.bahringer@ethereal.email", + pass: "yCKSH8rWyUPbnhGYk9", + }, + } + }, null, controllers.config.save) + } } module.exports = TestConfiguration \ No newline at end of file diff --git a/packages/worker/src/constants/index.js b/packages/worker/src/constants/index.js index 3bc8dbc039..141a742ce0 100644 --- a/packages/worker/src/constants/index.js +++ b/packages/worker/src/constants/index.js @@ -26,6 +26,7 @@ const EmailTemplatePurpose = { STYLES: "styles", PASSWORD_RECOVERY: "password_recovery", INVITATION: "invitation", + WELCOME: "welcome", CUSTOM: "custom", } @@ -39,6 +40,9 @@ const TemplateBindings = { EMAIL: "email", RESET_URL: "resetUrl", USER: "user", + REQUEST: "request", + DOCS_URL: "docsUrl", + LOGIN_URL: "loginUrl", } const TemplateMetadata = { diff --git a/packages/worker/src/constants/templates/base.hbs b/packages/worker/src/constants/templates/base.hbs index 1d8ff52700..f804605020 100644 --- a/packages/worker/src/constants/templates/base.hbs +++ b/packages/worker/src/constants/templates/base.hbs @@ -1,83 +1,32 @@ - - + + + - - - - - - - - - - + + + + + + + + + - - +{{ body }} +
- - - - - - - - \ 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 dd375d1e86..7ddb85af47 100644 --- a/packages/worker/src/constants/templates/index.js +++ b/packages/worker/src/constants/templates/index.js @@ -20,6 +20,7 @@ exports.EmailTemplates = { [EmailTemplatePurpose.STYLES]: readStaticFile( join(__dirname, "style.hbs") ), + [EmailTemplatePurpose.WELCOME]: readStaticFile(join(__dirname, "welcome.hbs")), } exports.addBaseTemplates = (templates, type = null) => { diff --git a/packages/worker/src/constants/templates/invitation.hbs b/packages/worker/src/constants/templates/invitation.hbs index 8e154fe189..16b011a17f 100644 --- a/packages/worker/src/constants/templates/invitation.hbs +++ b/packages/worker/src/constants/templates/invitation.hbs @@ -1,14 +1,67 @@ - - + +You've been invited to use {{ company }}'s Budibase platform! +
- - \ No newline at end of file diff --git a/packages/worker/src/constants/templates/passwordRecovery.hbs b/packages/worker/src/constants/templates/passwordRecovery.hbs index 11f4eac1f4..838e8f816c 100644 --- a/packages/worker/src/constants/templates/passwordRecovery.hbs +++ b/packages/worker/src/constants/templates/passwordRecovery.hbs @@ -1,15 +1,62 @@ - - + +Use this link to reset your password. The link is only valid for 24 hours. +
- - \ No newline at end of file diff --git a/packages/worker/src/constants/templates/style.hbs b/packages/worker/src/constants/templates/style.hbs index abcd797830..244e901787 100644 --- a/packages/worker/src/constants/templates/style.hbs +++ b/packages/worker/src/constants/templates/style.hbs @@ -1,269 +1,408 @@ -@font-face { - font-family: 'Playfair Display'; - font-style: italic; - font-weight: 400; - src: url(/fonts.gstatic.com/s/playfairdisplay/v22/nuFkD-vYSZviVYUb_rj3ij__anPXDTnohkk72xU.woff2) format('woff2'); - unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; -} -html, -body { - margin: 0 auto !important; - padding: 0 !important; - height: 100% !important; - width: 100% !important; - background: #f1f1f1; -} -* { - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; -} -div[style*="margin: 16px 0"] { - margin: 0 !important; -} -table, -td { - mso-table-lspace: 0pt !important; - mso-table-rspace: 0pt !important; -} -table { - border-spacing: 0 !important; - border-collapse: collapse !important; - table-layout: fixed !important; - margin: 0 auto !important; -} -img { - -ms-interpolation-mode:bicubic; -} -a { - text-decoration: none; -} -*[x-apple-data-detectors], /* iOS */ -.unstyle-auto-detected-links *, -.aBn { - border-bottom: 0 !important; - cursor: default !important; - color: inherit !important; - text-decoration: none !important; - font-size: inherit !important; - font-family: inherit !important; - font-weight: inherit !important; - line-height: inherit !important; -} -.a6S { - display: none !important; - opacity: 0.01 !important; -} -.im { - color: inherit !important; -} -img.g-img + div { - display: none !important; -} -@media only screen and (min-device-width: 320px) and (max-device-width: 374px) { - u ~ div .email-container { - min-width: 320px !important; - } -} -@media only screen and (min-device-width: 375px) and (max-device-width: 413px) { - u ~ div .email-container { - min-width: 375px !important; - } -} -@media only screen and (min-device-width: 414px) { - u ~ div .email-container { - min-width: 414px !important; - } -} -.primary{ - background: #f3a333; -} -.bg_white{ - background: #ffffff; -} -.bg_light{ - background: #fafafa; -} -.bg_black{ - background: #000000; -} -.bg_dark{ - background: rgba(0,0,0,.8); -} -.email-section{ - padding:2.5em; -} -.btn{ - padding: 10px 15px; -} -.btn.btn-primary{ - border-radius: 30px; - background: #f3a333; - color: #ffffff; -} -h1,h2,h3,h4,h5,h6{ - font-family: 'Playfair Display', serif; - color: #000000; - margin-top: 0; -} -body{ - font-family: 'Montserrat', sans-serif; - font-weight: 400; - font-size: 15px; - line-height: 1.8; - color: rgba(0,0,0,.4); -} -a{ - color: #f3a333; -} -table{ -} -.logo h1{ - margin: 0; -} -.logo h1 a{ - color: #000; - font-size: 20px; - font-weight: 700; - text-transform: uppercase; - font-family: 'Montserrat', sans-serif; -} -.hero{ - position: relative; -} -.hero img{ +/* Based on templates: https://github.com/wildbit/postmark-templates/blob/master/templates/plain */ +/* Base ------------------------------ */ +@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap'); +body { +width: 100% !important; +height: 100%; +margin: 0; +-webkit-text-size-adjust: none; } -.hero .text{ - color: rgba(255,255,255,.8); + +a { +color: #3869D4; } -.hero .text h2{ - color: #ffffff; - font-size: 30px; - margin-bottom: 0; + +a img { +border: none; } -.heading-section{ + +td { +word-break: break-word; } -.heading-section h2{ - color: #000000; - font-size: 28px; - margin-top: 0; - line-height: 1.4; + +.preheader { +display: none !important; +visibility: hidden; +mso-hide: all; +font-size: 1px; +line-height: 1px; +max-height: 0; +max-width: 0; +opacity: 0; +overflow: hidden; } -.heading-section .subheading{ - margin-bottom: 20px !important; - display: inline-block; - font-size: 13px; - text-transform: uppercase; - letter-spacing: 2px; - color: rgba(0,0,0,.4); - position: relative; +/* Type ------------------------------ */ + +body, +td, +th { +font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; } -.heading-section .subheading::after{ - position: absolute; - left: 0; - right: 0; - bottom: -10px; - content: ''; - width: 100%; - height: 2px; - background: #f3a333; - margin: 0 auto; + +h1 { +margin-top: 0; +color: #333333; +font-size: 22px; +font-weight: bold; +text-align: left; } -.heading-section-white{ - color: rgba(255,255,255,.8); + +h2 { +margin-top: 0; +color: #333333; +font-size: 16px; +font-weight: bold; +text-align: left; } -.heading-section-white h2{ - font-size: 28px; - line-height: 1; - padding-bottom: 0; + +h3 { +margin-top: 0; +color: #333333; +font-size: 14px; +font-weight: bold; +text-align: left; } -.heading-section-white h2{ - color: #ffffff; + +td, +th { +font-size: 16px; } -.heading-section-white .subheading{ - margin-bottom: 0; - display: inline-block; - font-size: 13px; - text-transform: uppercase; - letter-spacing: 2px; - color: rgba(255,255,255,.4); + +p, +ul, +ol, +blockquote { +margin: .4em 0 1.1875em; +font-size: 16px; +line-height: 1.625; } -.icon{ - text-align: center; + +p.sub { +font-size: 13px; } -.icon img{ +/* Utilities ------------------------------ */ + +.align-right { +text-align: right; } -.text-services{ - padding: 10px 10px 0; - text-align: center; + +.align-left { +text-align: left; } -.text-services h3{ - font-size: 20px; + +.align-center { +text-align: center; } -.text-services .meta{ - text-transform: uppercase; - font-size: 14px; +/* Buttons ------------------------------ */ + +.button { +background-color: #3869D4; +border-top: 10px solid #3869D4; +border-right: 18px solid #3869D4; +border-bottom: 10px solid #3869D4; +border-left: 18px solid #3869D4; +display: inline-block; +color: #FFF; +text-decoration: none; +border-radius: 3px; +box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); +-webkit-text-size-adjust: none; +box-sizing: border-box; } -.img{ - width: 100%; - height: auto; - position: relative; + +.button--green { +background-color: #22BC66; +border-top: 10px solid #22BC66; +border-right: 18px solid #22BC66; +border-bottom: 10px solid #22BC66; +border-left: 18px solid #22BC66; } -.img .icon{ - position: absolute; - top: 50%; - left: 0; - right: 0; - bottom: 0; - margin-top: -25px; + +.button--red { +background-color: #FF6136; +border-top: 10px solid #FF6136; +border-right: 18px solid #FF6136; +border-bottom: 10px solid #FF6136; +border-left: 18px solid #FF6136; } -.img .icon a{ - display: block; - width: 60px; - position: absolute; - top: 0; - left: 50%; - margin-left: -25px; + +@media only screen and (max-width: 500px) { +.button { +width: 100% !important; +text-align: center !important; } -.counter-text{ - text-align: center; } -.counter-text .num{ - display: block; - color: #ffffff; - font-size: 34px; - font-weight: 700; +/* Attribute list ------------------------------ */ + +.attributes { +margin: 0 0 21px; } -.counter-text .name{ - display: block; - color: rgba(255,255,255,.9); - font-size: 13px; + +.attributes_content { +background-color: #F4F4F7; +padding: 16px; } -.footer{ - color: rgba(255,255,255,.5); + +.attributes_item { +padding: 0; } -.footer .heading{ - color: #ffffff; - font-size: 20px; +/* Related Items ------------------------------ */ + +.related { +width: 100%; +margin: 0; +padding: 25px 0 0 0; +-premailer-width: 100%; +-premailer-cellpadding: 0; +-premailer-cellspacing: 0; } -.footer ul{ - margin: 0; - padding: 0; + +.related_item { +padding: 10px 0; +color: #CBCCCF; +font-size: 15px; +line-height: 18px; } -.footer ul li{ - list-style: none; - margin-bottom: 10px; + +.related_item-title { +display: block; +margin: .5em 0 0; } -.footer ul li a{ - color: rgba(255,255,255,1); + +.related_item-thumb { +display: block; +padding-bottom: 10px; } -@media screen and (max-width: 500px) { - .icon{ - text-align: left; - } - .text-services{ - padding-left: 0; - padding-right: 20px; - text-align: left; - } + +.related_heading { +border-top: 1px solid #CBCCCF; +text-align: center; +padding: 25px 0 10px; +} +/* Discount Code ------------------------------ */ + +.discount { +width: 100%; +margin: 0; +padding: 24px; +-premailer-width: 100%; +-premailer-cellpadding: 0; +-premailer-cellspacing: 0; +background-color: #F4F4F7; +border: 2px dashed #CBCCCF; +} + +.discount_heading { +text-align: center; +} + +.discount_body { +text-align: center; +font-size: 15px; +} +/* Social Icons ------------------------------ */ + +.social { +width: auto; +} + +.social td { +padding: 0; +width: auto; +} + +.social_icon { +height: 20px; +margin: 0 8px 10px 8px; +padding: 0; +} +/* Data table ------------------------------ */ + +.purchase { +width: 100%; +margin: 0; +padding: 35px 0; +-premailer-width: 100%; +-premailer-cellpadding: 0; +-premailer-cellspacing: 0; +} + +.purchase_content { +width: 100%; +margin: 0; +padding: 25px 0 0 0; +-premailer-width: 100%; +-premailer-cellpadding: 0; +-premailer-cellspacing: 0; +} + +.purchase_item { +padding: 10px 0; +color: #51545E; +font-size: 15px; +line-height: 18px; +} + +.purchase_heading { +padding-bottom: 8px; +border-bottom: 1px solid #EAEAEC; +} + +.purchase_heading p { +margin: 0; +color: #85878E; +font-size: 12px; +} + +.purchase_footer { +padding-top: 15px; +border-top: 1px solid #EAEAEC; +} + +.purchase_total { +margin: 0; +text-align: right; +font-weight: bold; +color: #333333; +} + +.purchase_total--label { +padding: 0 15px 0 0; +} + +body { +background-color: #FFF; +color: #333; +} + +p { +color: #333; +} + +.email-wrapper { +width: 100%; +margin: 0; +padding: 0; +-premailer-width: 100%; +-premailer-cellpadding: 0; +-premailer-cellspacing: 0; +} + +.email-content { +width: 100%; +margin: 0; +padding: 0; +-premailer-width: 100%; +-premailer-cellpadding: 0; +-premailer-cellspacing: 0; +} +/* Masthead ----------------------- */ + +.email-masthead { +padding: 25px 0; +text-align: center; +} + +.email-masthead_logo { +width: 94px; +} + +.email-masthead_name { +font-size: 16px; +font-weight: bold; +color: #A8AAAF; +text-decoration: none; +text-shadow: 0 1px 0 white; +} +/* Body ------------------------------ */ + +.email-body { +width: 100%; +margin: 0; +padding: 0; +-premailer-width: 100%; +-premailer-cellpadding: 0; +-premailer-cellspacing: 0; +} + +.email-body_inner { +width: 570px; +margin: 0 auto; +padding: 0; +-premailer-width: 570px; +-premailer-cellpadding: 0; +-premailer-cellspacing: 0; +} + +.email-footer { +width: 570px; +margin: 0 auto; +padding: 0; +-premailer-width: 570px; +-premailer-cellpadding: 0; +-premailer-cellspacing: 0; +text-align: center; +} + +.email-footer p { +color: #A8AAAF; +} + +.body-action { +width: 100%; +margin: 30px auto; +padding: 0; +-premailer-width: 100%; +-premailer-cellpadding: 0; +-premailer-cellspacing: 0; +text-align: center; +} + +.body-sub { +margin-top: 25px; +padding-top: 25px; +border-top: 1px solid #EAEAEC; +} + +.content-cell { +padding: 35px; +} +/*Media Queries ------------------------------ */ + +@media only screen and (max-width: 600px) { +.email-body_inner, +.email-footer { +width: 100% !important; +} +} + +@media (prefers-color-scheme: dark) { +body { +background-color: #333333 !important; +color: #FFF !important; +} +p, +ul, +ol, +blockquote, +h1, +h2, +h3, +span, +.purchase_item { +color: #FFF !important; +} +.attributes_content, +.discount { +background-color: #222 !important; +} +.email-masthead_name { +text-shadow: none !important; +} +} + +:root { +color-scheme: light dark; +supported-color-schemes: light dark; } \ No newline at end of file diff --git a/packages/worker/src/constants/templates/welcome.hbs b/packages/worker/src/constants/templates/welcome.hbs new file mode 100644 index 0000000000..89d4136121 --- /dev/null +++ b/packages/worker/src/constants/templates/welcome.hbs @@ -0,0 +1,62 @@ + +Thanks for trying out [Product Name]. We’ve pulled together some information and resources to help you get started. + + + + + \ No newline at end of file diff --git a/packages/worker/src/utilities/templates.js b/packages/worker/src/utilities/templates.js index 53c492557b..56c4340a77 100644 --- a/packages/worker/src/utilities/templates.js +++ b/packages/worker/src/utilities/templates.js @@ -28,5 +28,7 @@ exports.getSettingsTemplateContext = async () => { ), [TemplateBindings.RESET_URL]: checkSlashesInUrl(`${URL}/reset`), [TemplateBindings.COMPANY]: settings.company || BASE_COMPANY, + [TemplateBindings.DOCS_URL]: settings.docsUrl || "https://docs.budibase.com/", + [TemplateBindings.LOGIN_URL]: checkSlashesInUrl(`${URL}/login`), } }