diff --git a/packages/auth/src/environment.js b/packages/auth/src/environment.js index db24aaafcc..f582bd118a 100644 --- a/packages/auth/src/environment.js +++ b/packages/auth/src/environment.js @@ -15,5 +15,6 @@ module.exports = { MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, MINIO_URL: process.env.MINIO_URL, + INTERNAL_KEY: process.env.INTERNAL_KEY, isTest, } diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index 5d8d4e7e13..913e7c8cc8 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -2,6 +2,7 @@ const { Cookies } = require("../constants") const database = require("../db") const { getCookie, clearCookie } = require("../utils") const { StaticDatabases } = require("../db/utils") +const env = require("../environment") const PARAM_REGEX = /\/:(.*?)\//g @@ -35,10 +36,14 @@ module.exports = (noAuthPatterns = [], opts) => { return next() } try { + const apiKey = ctx.request.headers["x-budibase-api-key"] // check the actual user is authenticated first const authCookie = getCookie(ctx, Cookies.Auth) - if (authCookie) { + // this is an internal request, no user made it + if (apiKey && apiKey === env.INTERNAL_KEY) { + ctx.isAuthenticated = true + } else if (authCookie) { try { const db = database.getDB(StaticDatabases.GLOBAL.name) const user = await db.get(authCookie.userId) diff --git a/packages/cli/src/hosting/makeEnv.js b/packages/cli/src/hosting/makeEnv.js index a4fbce6ee0..e8c4a8c830 100644 --- a/packages/cli/src/hosting/makeEnv.js +++ b/packages/cli/src/hosting/makeEnv.js @@ -6,14 +6,11 @@ const randomString = require("randomstring") const FILE_PATH = path.resolve("./.env") -function getContents(port, hostingKey) { +function getContents(port) { return ` # Use the main port in the builder for your self hosting URL, e.g. localhost:10000 MAIN_PORT=${port} -# Use this password when configuring your self hosting settings -HOSTING_KEY=${hostingKey} - # This section contains all secrets pertaining to the system JWT_SECRET=${randomString.generate()} MINIO_ACCESS_KEY=${randomString.generate()} @@ -21,6 +18,7 @@ MINIO_SECRET_KEY=${randomString.generate()} COUCH_DB_PASSWORD=${randomString.generate()} COUCH_DB_USER=${randomString.generate()} REDIS_PASSWORD=${randomString.generate()} +INTERNAL_KEY=${randomString.generate()} # This section contains variables that do not need to be altered under normal circumstances APP_PORT=4002 @@ -33,7 +31,6 @@ BUDIBASE_ENVIRONMENT=PRODUCTION` module.exports.filePath = FILE_PATH module.exports.ConfigMap = { - HOSTING_KEY: "key", MAIN_PORT: "port", } module.exports.QUICK_CONFIG = { @@ -42,18 +39,13 @@ module.exports.QUICK_CONFIG = { } module.exports.make = async (inputs = {}) => { - const hostingKey = - inputs.key || - (await string( - "Please input the password you'd like to use as your hosting key: " - )) const hostingPort = inputs.port || (await number( "Please enter the port on which you want your installation to run: ", 10000 )) - const fileContents = getContents(hostingPort, hostingKey) + const fileContents = getContents(hostingPort) fs.writeFileSync(FILE_PATH, fileContents) console.log( success( diff --git a/packages/server/scripts/dev/manage.js b/packages/server/scripts/dev/manage.js index 24cac981cf..5349158036 100644 --- a/packages/server/scripts/dev/manage.js +++ b/packages/server/scripts/dev/manage.js @@ -39,6 +39,7 @@ async function init() { COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/", REDIS_URL: "localhost:6379", WORKER_URL: "http://localhost:4002", + INTERNAL_KEY: "budibase", JWT_SECRET: "testsecret", REDIS_PASSWORD: "budibase", MINIO_ACCESS_KEY: "budibase", diff --git a/packages/server/src/automations/actions.js b/packages/server/src/automations/actions.js index a83608da30..0f40fd6aae 100644 --- a/packages/server/src/automations/actions.js +++ b/packages/server/src/automations/actions.js @@ -1,4 +1,4 @@ -const sendEmail = require("./steps/sendEmail") +const sendEmail = require("./steps/sendgridEmail") const createRow = require("./steps/createRow") const updateRow = require("./steps/updateRow") const deleteRow = require("./steps/deleteRow") diff --git a/packages/server/src/automations/steps/sendSmtpEmail.js b/packages/server/src/automations/steps/sendSmtpEmail.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/server/src/automations/steps/sendEmail.js b/packages/server/src/automations/steps/sendgridEmail.js similarity index 97% rename from packages/server/src/automations/steps/sendEmail.js rename to packages/server/src/automations/steps/sendgridEmail.js index e08a3d8bc4..26c404257e 100644 --- a/packages/server/src/automations/steps/sendEmail.js +++ b/packages/server/src/automations/steps/sendgridEmail.js @@ -2,7 +2,7 @@ module.exports.definition = { description: "Send an email", tagline: "Send email to {{inputs.to}}", icon: "ri-mail-open-line", - name: "Send Email", + name: "Send Email (SendGrid)", type: "ACTION", stepId: "SEND_EMAIL", inputs: {}, diff --git a/packages/server/src/environment.js b/packages/server/src/environment.js index 061f38a985..412b05cab0 100644 --- a/packages/server/src/environment.js +++ b/packages/server/src/environment.js @@ -34,6 +34,7 @@ module.exports = { USE_QUOTAS: process.env.USE_QUOTAS, REDIS_URL: process.env.REDIS_URL, REDIS_PASSWORD: process.env.REDIS_PASSWORD, + INTERNAL_KEY: process.env.INTERNAL_KEY, // environment NODE_ENV: process.env.NODE_ENV, JEST_WORKER_ID: process.env.JEST_WORKER_ID, @@ -53,7 +54,6 @@ module.exports = { BUDIBASE_API_KEY: process.env.BUDIBASE_API_KEY, USERID_API_KEY: process.env.USERID_API_KEY, DEPLOYMENT_CREDENTIALS_URL: process.env.DEPLOYMENT_CREDENTIALS_URL, - HOSTING_KEY: process.env.HOSTING_KEY, _set(key, value) { process.env[key] = value module.exports[key] = value diff --git a/packages/server/src/utilities/workerRequests.js b/packages/server/src/utilities/workerRequests.js index cebd892b55..441f4f8b2a 100644 --- a/packages/server/src/utilities/workerRequests.js +++ b/packages/server/src/utilities/workerRequests.js @@ -28,7 +28,7 @@ function request(ctx, request) { } else { delete request.body } - if (ctx.headers) { + if (ctx && ctx.headers) { request.headers.cookie = ctx.headers.cookie } return request @@ -36,6 +36,19 @@ function request(ctx, request) { exports.request = request +exports.sendSmtpEmail = async (to, from, contents) => { + const response = await fetch( + checkSlashesInUrl(env.WORKER_URL + `/api/`), + request(null, { + method: "POST", + headers: { + "x-budibase-api-key": env.INTERNAL_KEY, + }, + body: {}, + }) + ) +} + exports.getDeployedApps = async ctx => { if (!env.SELF_HOSTED) { throw "Can only check apps for self hosted environments" diff --git a/packages/worker/scripts/dev/manage.js b/packages/worker/scripts/dev/manage.js index 7322349b72..f2784750c6 100644 --- a/packages/worker/scripts/dev/manage.js +++ b/packages/worker/scripts/dev/manage.js @@ -8,6 +8,7 @@ async function init() { SELF_HOSTED: 1, PORT: 4002, JWT_SECRET: "testsecret", + INTERNAL_KEY: "budibase", MINIO_ACCESS_KEY: "budibase", MINIO_SECRET_KEY: "budibase", COUCH_DB_USER: "budibase", diff --git a/packages/worker/src/api/routes/tests/realEmail.spec.js b/packages/worker/src/api/routes/tests/realEmail.spec.js index f593b2cc09..e87c5d5bf5 100644 --- a/packages/worker/src/api/routes/tests/realEmail.spec.js +++ b/packages/worker/src/api/routes/tests/realEmail.spec.js @@ -29,6 +29,7 @@ describe("/api/admin/email", () => { .expect(200) expect(res.body.message).toBeDefined() const testUrl = nodemailer.getTestMessageUrl(res.body) + console.log(`${purpose} URL: ${testUrl}`) expect(testUrl).toBeDefined() const response = await fetch(testUrl) const text = await response.text() diff --git a/packages/worker/src/constants/index.js b/packages/worker/src/constants/index.js index b780b71048..2defebc903 100644 --- a/packages/worker/src/constants/index.js +++ b/packages/worker/src/constants/index.js @@ -27,7 +27,6 @@ const TemplateTypes = { const EmailTemplatePurpose = { BASE: "base", - STYLES: "styles", PASSWORD_RECOVERY: "password_recovery", INVITATION: "invitation", WELCOME: "welcome", @@ -79,10 +78,6 @@ const TemplateBindings = { const TemplateMetadata = { [TemplateTypes.EMAIL]: [ - { - name: "Styling", - purpose: EmailTemplatePurpose.STYLES, - }, { name: "Base Format", purpose: EmailTemplatePurpose.BASE, @@ -132,6 +127,12 @@ const TemplateMetadata = { { name: "Custom", purpose: EmailTemplatePurpose.CUSTOM, + bindings: [ + { + name: "contents", + description: "Custom content body.", + }, + ], }, ], } diff --git a/packages/worker/src/constants/templates/base.hbs b/packages/worker/src/constants/templates/base.hbs index 38ceff023a..960d6faff1 100644 --- a/packages/worker/src/constants/templates/base.hbs +++ b/packages/worker/src/constants/templates/base.hbs @@ -9,7 +9,426 @@ + + +
+ {{ contents }} +
+ + + + + \ 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 23e5508341..c677f504c4 100644 --- a/packages/worker/src/constants/templates/index.js +++ b/packages/worker/src/constants/templates/index.js @@ -17,10 +17,10 @@ exports.EmailTemplates = { join(__dirname, "invitation.hbs") ), [EmailTemplatePurpose.BASE]: readStaticFile(join(__dirname, "base.hbs")), - [EmailTemplatePurpose.STYLES]: readStaticFile(join(__dirname, "style.hbs")), [EmailTemplatePurpose.WELCOME]: readStaticFile( join(__dirname, "welcome.hbs") ), + [EmailTemplatePurpose.CUSTOM]: readStaticFile(join(__dirname, "custom.hbs")), } exports.addBaseTemplates = (templates, type = null) => { diff --git a/packages/worker/src/constants/templates/style.hbs b/packages/worker/src/constants/templates/style.hbs deleted file mode 100644 index 244e901787..0000000000 --- a/packages/worker/src/constants/templates/style.hbs +++ /dev/null @@ -1,408 +0,0 @@ -/* 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; -} - -a { -color: #3869D4; -} - -a img { -border: none; -} - -td { -word-break: break-word; -} - -.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; -} -/* Type ------------------------------ */ - -body, -td, -th { -font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; -} - -h1 { -margin-top: 0; -color: #333333; -font-size: 22px; -font-weight: bold; -text-align: left; -} - -h2 { -margin-top: 0; -color: #333333; -font-size: 16px; -font-weight: bold; -text-align: left; -} - -h3 { -margin-top: 0; -color: #333333; -font-size: 14px; -font-weight: bold; -text-align: left; -} - -td, -th { -font-size: 16px; -} - -p, -ul, -ol, -blockquote { -margin: .4em 0 1.1875em; -font-size: 16px; -line-height: 1.625; -} - -p.sub { -font-size: 13px; -} -/* Utilities ------------------------------ */ - -.align-right { -text-align: right; -} - -.align-left { -text-align: left; -} - -.align-center { -text-align: center; -} -/* 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; -} - -.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; -} - -.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; -} - -@media only screen and (max-width: 500px) { -.button { -width: 100% !important; -text-align: center !important; -} -} -/* Attribute list ------------------------------ */ - -.attributes { -margin: 0 0 21px; -} - -.attributes_content { -background-color: #F4F4F7; -padding: 16px; -} - -.attributes_item { -padding: 0; -} -/* Related Items ------------------------------ */ - -.related { -width: 100%; -margin: 0; -padding: 25px 0 0 0; --premailer-width: 100%; --premailer-cellpadding: 0; --premailer-cellspacing: 0; -} - -.related_item { -padding: 10px 0; -color: #CBCCCF; -font-size: 15px; -line-height: 18px; -} - -.related_item-title { -display: block; -margin: .5em 0 0; -} - -.related_item-thumb { -display: block; -padding-bottom: 10px; -} - -.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/environment.js b/packages/worker/src/environment.js index 04c010ce16..dda3e842f1 100644 --- a/packages/worker/src/environment.js +++ b/packages/worker/src/environment.js @@ -28,8 +28,8 @@ module.exports = { SALT_ROUNDS: process.env.SALT_ROUNDS, REDIS_URL: process.env.REDIS_URL, REDIS_PASSWORD: process.env.REDIS_PASSWORD, + INTERNAL_KEY: process.env.INTERNAL_KEY, /* TODO: to remove - once deployment removed */ - SELF_HOST_KEY: process.env.SELF_HOST_KEY, COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME, COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD, _set(key, value) { diff --git a/packages/worker/src/utilities/email.js b/packages/worker/src/utilities/email.js index e06b98a31e..7a2069bbb4 100644 --- a/packages/worker/src/utilities/email.js +++ b/packages/worker/src/utilities/email.js @@ -54,16 +54,14 @@ async function buildEmail(purpose, email, user) { if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) { throw `Unable to build an email of type ${purpose}` } - let [base, styles, body] = await Promise.all([ + let [base, body] = await Promise.all([ getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE), - getTemplateByPurpose(TYPE, EmailTemplatePurpose.STYLES), getTemplateByPurpose(TYPE, purpose), ]) - if (!base || !styles || !body) { + if (!base || !body) { throw "Unable to build email, missing base components" } base = base.contents - styles = styles.contents body = body.contents // if there is a link code needed this will retrieve it @@ -75,11 +73,9 @@ async function buildEmail(purpose, 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, }) } @@ -117,11 +113,17 @@ exports.isEmailConfigured = async (groupId = null) => { * @param {string} email The email address to send to. * @param {string} purpose The purpose of the email being sent (e.g. reset password). * @param {string|undefined} groupId If finer grain controls being used then this will lookup config for group. - * @param {object|undefined} user if sending to an existing user the object can be provided, this is used in the context. + * @param {object|undefined} user If sending to an existing user the object can be provided, this is used in the context. + * @param {string|undefined} from If sending from an address that is not what is configured in the SMTP config. + * @param {string|undefined} contents If sending a custom email then can supply contents which will be added to it. * @return {Promise} returns details about the attempt to send email, e.g. if it is successful; based on * nodemailer response. */ -exports.sendEmail = async (email, purpose, { groupId, user } = {}) => { +exports.sendEmail = async ( + email, + purpose, + { groupId, user, from, contents } = {} +) => { const db = new CouchDB(GLOBAL_DB) const config = await getSmtpConfiguration(db, groupId) if (!config) { @@ -129,7 +131,7 @@ exports.sendEmail = async (email, purpose, { groupId, user } = {}) => { } const transport = createSMTPTransport(config) const message = { - from: config.from, + from: from || config.from, subject: config.subject, to: email, html: await buildEmail(purpose, email, user),