Merge pull request #1479 from Budibase/smtp-configuration
Smtp configuration
This commit is contained in:
commit
306dd298d6
|
@ -16,7 +16,7 @@ services:
|
||||||
MINIO_URL: http://minio-service:9000
|
MINIO_URL: http://minio-service:9000
|
||||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||||
HOSTING_KEY: ${HOSTING_KEY}
|
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||||
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
|
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
|
||||||
PORT: 4002
|
PORT: 4002
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
@ -44,7 +44,7 @@ services:
|
||||||
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
||||||
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
||||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||||
SELF_HOST_KEY: ${HOSTING_KEY}
|
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||||
REDIS_URL: redis-service:6379
|
REDIS_URL: redis-service:6379
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
||||||
MAIN_PORT=10000
|
MAIN_PORT=10000
|
||||||
|
|
||||||
# Use this password when configuring your self hosting settings
|
|
||||||
# This should be updated
|
|
||||||
HOSTING_KEY=budibase
|
|
||||||
|
|
||||||
# This section contains all secrets pertaining to the system
|
# This section contains all secrets pertaining to the system
|
||||||
# These should be updated
|
# These should be updated
|
||||||
JWT_SECRET=testsecret
|
JWT_SECRET=testsecret
|
||||||
|
@ -13,6 +9,7 @@ MINIO_SECRET_KEY=budibase
|
||||||
COUCH_DB_PASSWORD=budibase
|
COUCH_DB_PASSWORD=budibase
|
||||||
COUCH_DB_USER=budibase
|
COUCH_DB_USER=budibase
|
||||||
REDIS_PASSWORD=budibase
|
REDIS_PASSWORD=budibase
|
||||||
|
INTERNAL_API_KEY=budibase
|
||||||
|
|
||||||
# This section contains variables that do not need to be altered under normal circumstances
|
# This section contains variables that do not need to be altered under normal circumstances
|
||||||
APP_PORT=4002
|
APP_PORT=4002
|
||||||
|
|
|
@ -71,7 +71,7 @@ exports.getGlobalUserParams = (globalId, otherProps = {}) => {
|
||||||
* @param ownerId The owner/user of the template, this could be global or a group level.
|
* @param ownerId The owner/user of the template, this could be global or a group level.
|
||||||
*/
|
*/
|
||||||
exports.generateTemplateID = ownerId => {
|
exports.generateTemplateID = ownerId => {
|
||||||
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${newid()}`
|
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15,5 +15,6 @@ module.exports = {
|
||||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||||
MINIO_URL: process.env.MINIO_URL,
|
MINIO_URL: process.env.MINIO_URL,
|
||||||
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
isTest,
|
isTest,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ const { Cookies } = require("../constants")
|
||||||
const database = require("../db")
|
const database = require("../db")
|
||||||
const { getCookie, clearCookie } = require("../utils")
|
const { getCookie, clearCookie } = require("../utils")
|
||||||
const { StaticDatabases } = require("../db/utils")
|
const { StaticDatabases } = require("../db/utils")
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
const PARAM_REGEX = /\/:(.*?)\//g
|
const PARAM_REGEX = /\/:(.*?)\//g
|
||||||
|
|
||||||
|
@ -35,10 +36,14 @@ module.exports = (noAuthPatterns = [], opts) => {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const apiKey = ctx.request.headers["x-budibase-api-key"]
|
||||||
// check the actual user is authenticated first
|
// check the actual user is authenticated first
|
||||||
const authCookie = getCookie(ctx, Cookies.Auth)
|
const authCookie = getCookie(ctx, Cookies.Auth)
|
||||||
|
|
||||||
if (authCookie) {
|
// this is an internal request, no user made it
|
||||||
|
if (apiKey && apiKey === env.INTERNAL_API_KEY) {
|
||||||
|
ctx.isAuthenticated = true
|
||||||
|
} else if (authCookie) {
|
||||||
try {
|
try {
|
||||||
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||||
const user = await db.get(authCookie.userId)
|
const user = await db.get(authCookie.userId)
|
||||||
|
|
|
@ -15,6 +15,11 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
p {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
.noPadding {
|
.noPadding {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||||
|
import QuerySelector from "./QuerySelector.svelte"
|
||||||
|
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||||
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let webhookModal
|
export let webhookModal
|
||||||
|
@ -70,6 +74,10 @@
|
||||||
on:change={e => (block.inputs[key] = e.detail)}
|
on:change={e => (block.inputs[key] = e.detail)}
|
||||||
{bindings}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
|
{:else if value.customType === "query"}
|
||||||
|
<QuerySelector bind:value={block.inputs[key]} />
|
||||||
|
{:else if value.customType === "queryParams"}
|
||||||
|
<QueryParamSelector bind:value={block.inputs[key]} {bindings} />
|
||||||
{:else if value.customType === "table"}
|
{:else if value.customType === "table"}
|
||||||
<TableSelector bind:value={block.inputs[key]} />
|
<TableSelector bind:value={block.inputs[key]} />
|
||||||
{:else if value.customType === "row"}
|
{:else if value.customType === "row"}
|
||||||
|
@ -78,6 +86,17 @@
|
||||||
<WebhookDisplay value={block.inputs[key]} />
|
<WebhookDisplay value={block.inputs[key]} />
|
||||||
{:else if value.customType === "triggerSchema"}
|
{:else if value.customType === "triggerSchema"}
|
||||||
<SchemaSetup bind:value={block.inputs[key]} />
|
<SchemaSetup bind:value={block.inputs[key]} />
|
||||||
|
{:else if value.customType === "code"}
|
||||||
|
<CodeEditorModal>
|
||||||
|
<pre>{JSON.stringify(bindings, null, 2)}</pre>
|
||||||
|
<Editor
|
||||||
|
mode="javascript"
|
||||||
|
on:change={e => {
|
||||||
|
block.inputs[key] = e.detail.value
|
||||||
|
}}
|
||||||
|
value={block.inputs[key]}
|
||||||
|
/>
|
||||||
|
</CodeEditorModal>
|
||||||
{:else if value.type === "string" || value.type === "number"}
|
{:else if value.type === "string" || value.type === "number"}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
|
|
|
@ -11,8 +11,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={modal} width="60%">
|
<Modal bind:this={modal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
size="XL"
|
||||||
title="Edit Code"
|
title="Edit Code"
|
||||||
showConfirmButton={false}
|
showConfirmButton={false}
|
||||||
showCancelButton={false}
|
showCancelButton={false}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { queries } from "stores/backend"
|
import { queries } from "stores/backend"
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import DrawerBindableInput from "../../common/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "./AutomationBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let bindings
|
export let bindings
|
||||||
|
@ -13,7 +13,6 @@
|
||||||
// Ensure any nullish queryId values get set to empty string so
|
// Ensure any nullish queryId values get set to empty string so
|
||||||
// that the select works
|
// that the select works
|
||||||
$: if (value?.queryId == null) value = { queryId: "" }
|
$: if (value?.queryId == null) value = { queryId: "" }
|
||||||
$: console.log("daValuz", value)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="block-field">
|
<div class="block-field">
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
readOnly,
|
readOnly,
|
||||||
autoCloseBrackets: true,
|
autoCloseBrackets: true,
|
||||||
autoCloseTags: true,
|
autoCloseTags: true,
|
||||||
theme: $themeStore.darkMode ? THEMES.DARK : THEMES.LIGHT,
|
theme: THEMES.DARK,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tab)
|
if (!tab)
|
||||||
|
|
|
@ -161,7 +161,5 @@
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Detail,
|
||||||
|
MenuSection,
|
||||||
|
DetailSummary,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let bindings
|
||||||
|
export let onBindingClick = () => {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Menu>
|
||||||
|
{#each bindings as binding}
|
||||||
|
<MenuItem on:click={() => onBindingClick(binding)}>
|
||||||
|
<Detail size="M">{binding.name}</Detail>
|
||||||
|
<Body size="XS" noPadding>{binding.description}</Body>
|
||||||
|
</MenuItem>
|
||||||
|
{/each}
|
||||||
|
</Menu>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span on:click={() => $goto(`./${value}`)}>{value}</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
span {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,142 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Button,
|
||||||
|
Detail,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
notifications,
|
||||||
|
Layout,
|
||||||
|
Icon,
|
||||||
|
Body,
|
||||||
|
Page,
|
||||||
|
Select,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
MenuSection,
|
||||||
|
MenuSeparator,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
import { email } from "stores/portal"
|
||||||
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
|
import TemplateBindings from "./TemplateBindings.svelte"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
const ConfigTypes = {
|
||||||
|
SMTP: "smtp",
|
||||||
|
}
|
||||||
|
|
||||||
|
export let template
|
||||||
|
|
||||||
|
let selected = "Edit"
|
||||||
|
let selectedBindingTab = "Template"
|
||||||
|
let htmlEditor
|
||||||
|
|
||||||
|
$: selectedTemplate = $email.templates.find(
|
||||||
|
({ purpose }) => purpose === template
|
||||||
|
)
|
||||||
|
$: templateBindings =
|
||||||
|
$email.definitions?.bindings[selectedTemplate.purpose] || []
|
||||||
|
|
||||||
|
async function saveTemplate() {
|
||||||
|
try {
|
||||||
|
// Save your template config
|
||||||
|
await email.templates.save(selectedTemplate)
|
||||||
|
notifications.success(`Template saved.`)
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to update template settings. ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTemplateBinding(binding) {
|
||||||
|
htmlEditor.update((selectedTemplate.contents += `{{ ${binding.name} }}`))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Page wide gap="L">
|
||||||
|
<div class="backbutton" on:click={() => $goto("./")}>
|
||||||
|
<Icon name="BackAndroid" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<header>
|
||||||
|
<Heading>
|
||||||
|
Email Template: {template}
|
||||||
|
</Heading>
|
||||||
|
<Button cta on:click={saveTemplate}>Save</Button>
|
||||||
|
</header>
|
||||||
|
<Tabs {selected}>
|
||||||
|
<Tab title="Edit">
|
||||||
|
<div class="template-editor">
|
||||||
|
<Editor
|
||||||
|
editorHeight={800}
|
||||||
|
bind:this={htmlEditor}
|
||||||
|
mode="handlebars"
|
||||||
|
on:change={e => {
|
||||||
|
selectedTemplate.contents = e.detail.value
|
||||||
|
}}
|
||||||
|
value={selectedTemplate.contents}
|
||||||
|
/>
|
||||||
|
<div class="bindings-editor">
|
||||||
|
<Detail size="L">Bindings</Detail>
|
||||||
|
<Tabs selected={selectedBindingTab}>
|
||||||
|
<Tab title="Template">
|
||||||
|
<TemplateBindings
|
||||||
|
title="Template Bindings"
|
||||||
|
bindings={templateBindings}
|
||||||
|
onBindingClick={setTemplateBinding}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Common">
|
||||||
|
<TemplateBindings
|
||||||
|
title="Common Bindings"
|
||||||
|
bindings={$email.definitions.bindings.common}
|
||||||
|
onBindingClick={setTemplateBinding}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div></Tab
|
||||||
|
>
|
||||||
|
<Tab title="Preview">
|
||||||
|
<div class="preview" transition:fade>
|
||||||
|
{@html selectedTemplate.contents}
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Page>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.template-editor {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 20%;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--spacing-l);
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
background: white;
|
||||||
|
height: 800px;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backbutton {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,191 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
notifications,
|
||||||
|
Layout,
|
||||||
|
Input,
|
||||||
|
TextArea,
|
||||||
|
Body,
|
||||||
|
Page,
|
||||||
|
Select,
|
||||||
|
MenuSection,
|
||||||
|
MenuSeparator,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { email } from "stores/portal"
|
||||||
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
|
import TemplateBindings from "./TemplateBindings.svelte"
|
||||||
|
import TemplateLink from "./TemplateLink.svelte"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
const ConfigTypes = {
|
||||||
|
SMTP: "smtp",
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateSchema = {
|
||||||
|
purpose: {
|
||||||
|
displayName: "Email",
|
||||||
|
editable: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRenderers = [
|
||||||
|
{
|
||||||
|
column: "purpose",
|
||||||
|
component: TemplateLink,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
let smtpConfig
|
||||||
|
let bindingsOpen = false
|
||||||
|
let htmlModal
|
||||||
|
let htmlEditor
|
||||||
|
let loading
|
||||||
|
|
||||||
|
async function saveSmtp() {
|
||||||
|
try {
|
||||||
|
// Save your SMTP config
|
||||||
|
const response = await api.post(`/api/admin/configs`, smtpConfig)
|
||||||
|
const json = await response.json()
|
||||||
|
if (response.status !== 200) throw new Error(json.message)
|
||||||
|
smtpConfig._rev = json._rev
|
||||||
|
smtpConfig._id = json._id
|
||||||
|
|
||||||
|
notifications.success(`Settings saved.`)
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to save email settings. ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTemplate() {
|
||||||
|
try {
|
||||||
|
await email.templates.save(selectedTemplate)
|
||||||
|
notifications.success(`Template saved.`)
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to update template settings. ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSmtp() {
|
||||||
|
// fetch the configs for smtp
|
||||||
|
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`)
|
||||||
|
const smtpDoc = await smtpResponse.json()
|
||||||
|
|
||||||
|
if (!smtpDoc._id) {
|
||||||
|
smtpConfig = {
|
||||||
|
type: ConfigTypes.SMTP,
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
type: "login",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
smtpConfig = smtpDoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
loading = true
|
||||||
|
await fetchSmtp()
|
||||||
|
await email.templates.fetch()
|
||||||
|
loading = false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Page>
|
||||||
|
<header>
|
||||||
|
<Heading size="M">Email</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Sending email is not required, but highly recommended for processes such
|
||||||
|
as password recovery. To setup automated auth emails, simply add the
|
||||||
|
values below and click activate.
|
||||||
|
</Body>
|
||||||
|
</header>
|
||||||
|
<Divider />
|
||||||
|
{#if smtpConfig}
|
||||||
|
<div class="config-form">
|
||||||
|
<Heading size="S">SMTP</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
To allow your app to benefit from automated auth emails, add your SMTP
|
||||||
|
details below.
|
||||||
|
</Body>
|
||||||
|
<Layout gap="S">
|
||||||
|
<Heading size="S">
|
||||||
|
<span />
|
||||||
|
</Heading>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>Host</Label>
|
||||||
|
<Input bind:value={smtpConfig.config.host} />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>Port</Label>
|
||||||
|
<Input type="number" bind:value={smtpConfig.config.port} />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>User</Label>
|
||||||
|
<Input bind:value={smtpConfig.config.auth.user} />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>Password</Label>
|
||||||
|
<Input type="password" bind:value={smtpConfig.config.auth.pass} />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>From email address</Label>
|
||||||
|
<Input type="email" bind:value={smtpConfig.config.from} />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<Button cta on:click={saveSmtp}>Save</Button>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div class="config-form">
|
||||||
|
<Heading size="S">Templates</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Budibase comes out of the box with ready-made email templates to help
|
||||||
|
with user onboarding. Please refrain from changing the links.
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
{customRenderers}
|
||||||
|
data={$email.templates}
|
||||||
|
schema={templateSchema}
|
||||||
|
{loading}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
allowEditColumns={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Page>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.config-form {
|
||||||
|
margin-top: 42px;
|
||||||
|
margin-bottom: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 42px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -91,7 +91,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div>
|
<div>
|
||||||
<Button primary on:click={() => save(google)}>Save</Button>
|
<Button cta on:click={() => save(google)}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
export function createEmailStore() {
|
||||||
|
const store = writable([])
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
templates: {
|
||||||
|
fetch: async () => {
|
||||||
|
// fetch the email template definitions
|
||||||
|
const response = await api.get(`/api/admin/template/definitions`)
|
||||||
|
const definitions = await response.json()
|
||||||
|
|
||||||
|
// fetch the email templates themselves
|
||||||
|
const templatesResponse = await api.get(`/api/admin/template/email`)
|
||||||
|
const templates = await templatesResponse.json()
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
definitions,
|
||||||
|
templates,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
save: async template => {
|
||||||
|
// Save your template config
|
||||||
|
const response = await api.post(`/api/admin/template`, template)
|
||||||
|
const json = await response.json()
|
||||||
|
if (response.status !== 200) throw new Error(json.message)
|
||||||
|
template._rev = json._rev
|
||||||
|
template._id = json._id
|
||||||
|
|
||||||
|
store.update(state => {
|
||||||
|
const currentIdx = state.templates.findIndex(
|
||||||
|
template => template.purpose === json.purpose
|
||||||
|
)
|
||||||
|
state.templates.splice(currentIdx, 1, template)
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const email = createEmailStore()
|
|
@ -1,3 +1,4 @@
|
||||||
export { organisation } from "./organisation"
|
export { organisation } from "./organisation"
|
||||||
export { admin } from "./admin"
|
export { admin } from "./admin"
|
||||||
export { apps } from "./apps"
|
export { apps } from "./apps"
|
||||||
|
export { email } from "./email"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { string, number } = require("../questions")
|
const { number } = require("../questions")
|
||||||
const { success } = require("../utils")
|
const { success } = require("../utils")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
|
@ -6,14 +6,11 @@ const randomString = require("randomstring")
|
||||||
|
|
||||||
const FILE_PATH = path.resolve("./.env")
|
const FILE_PATH = path.resolve("./.env")
|
||||||
|
|
||||||
function getContents(port, hostingKey) {
|
function getContents(port) {
|
||||||
return `
|
return `
|
||||||
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
|
||||||
MAIN_PORT=${port}
|
MAIN_PORT=${port}
|
||||||
|
|
||||||
# Use this password when configuring your self hosting settings
|
|
||||||
HOSTING_KEY=${hostingKey}
|
|
||||||
|
|
||||||
# This section contains all secrets pertaining to the system
|
# This section contains all secrets pertaining to the system
|
||||||
JWT_SECRET=${randomString.generate()}
|
JWT_SECRET=${randomString.generate()}
|
||||||
MINIO_ACCESS_KEY=${randomString.generate()}
|
MINIO_ACCESS_KEY=${randomString.generate()}
|
||||||
|
@ -21,6 +18,7 @@ MINIO_SECRET_KEY=${randomString.generate()}
|
||||||
COUCH_DB_PASSWORD=${randomString.generate()}
|
COUCH_DB_PASSWORD=${randomString.generate()}
|
||||||
COUCH_DB_USER=${randomString.generate()}
|
COUCH_DB_USER=${randomString.generate()}
|
||||||
REDIS_PASSWORD=${randomString.generate()}
|
REDIS_PASSWORD=${randomString.generate()}
|
||||||
|
INTERNAL_API_KEY=${randomString.generate()}
|
||||||
|
|
||||||
# This section contains variables that do not need to be altered under normal circumstances
|
# This section contains variables that do not need to be altered under normal circumstances
|
||||||
APP_PORT=4002
|
APP_PORT=4002
|
||||||
|
@ -33,7 +31,6 @@ BUDIBASE_ENVIRONMENT=PRODUCTION`
|
||||||
|
|
||||||
module.exports.filePath = FILE_PATH
|
module.exports.filePath = FILE_PATH
|
||||||
module.exports.ConfigMap = {
|
module.exports.ConfigMap = {
|
||||||
HOSTING_KEY: "key",
|
|
||||||
MAIN_PORT: "port",
|
MAIN_PORT: "port",
|
||||||
}
|
}
|
||||||
module.exports.QUICK_CONFIG = {
|
module.exports.QUICK_CONFIG = {
|
||||||
|
@ -42,18 +39,13 @@ module.exports.QUICK_CONFIG = {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.make = async (inputs = {}) => {
|
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 =
|
const hostingPort =
|
||||||
inputs.port ||
|
inputs.port ||
|
||||||
(await number(
|
(await number(
|
||||||
"Please enter the port on which you want your installation to run: ",
|
"Please enter the port on which you want your installation to run: ",
|
||||||
10000
|
10000
|
||||||
))
|
))
|
||||||
const fileContents = getContents(hostingPort, hostingKey)
|
const fileContents = getContents(hostingPort)
|
||||||
fs.writeFileSync(FILE_PATH, fileContents)
|
fs.writeFileSync(FILE_PATH, fileContents)
|
||||||
console.log(
|
console.log(
|
||||||
success(
|
success(
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -39,6 +39,7 @@ async function init() {
|
||||||
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
||||||
REDIS_URL: "localhost:6379",
|
REDIS_URL: "localhost:6379",
|
||||||
WORKER_URL: "http://localhost:4002",
|
WORKER_URL: "http://localhost:4002",
|
||||||
|
INTERNAL_API_KEY: "budibase",
|
||||||
JWT_SECRET: "testsecret",
|
JWT_SECRET: "testsecret",
|
||||||
REDIS_PASSWORD: "budibase",
|
REDIS_PASSWORD: "budibase",
|
||||||
MINIO_ACCESS_KEY: "budibase",
|
MINIO_ACCESS_KEY: "budibase",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const sendEmail = require("./steps/sendEmail")
|
const sendgridEmail = require("./steps/sendgridEmail")
|
||||||
|
const sendSmtpEmail = require("./steps/sendSmtpEmail")
|
||||||
const createRow = require("./steps/createRow")
|
const createRow = require("./steps/createRow")
|
||||||
const updateRow = require("./steps/updateRow")
|
const updateRow = require("./steps/updateRow")
|
||||||
const deleteRow = require("./steps/deleteRow")
|
const deleteRow = require("./steps/deleteRow")
|
||||||
|
@ -14,7 +15,8 @@ const {
|
||||||
} = require("../utilities/fileSystem")
|
} = require("../utilities/fileSystem")
|
||||||
|
|
||||||
const BUILTIN_ACTIONS = {
|
const BUILTIN_ACTIONS = {
|
||||||
SEND_EMAIL: sendEmail.run,
|
SEND_EMAIL: sendgridEmail.run,
|
||||||
|
SEND_EMAIL_SMTP: sendSmtpEmail.run,
|
||||||
CREATE_ROW: createRow.run,
|
CREATE_ROW: createRow.run,
|
||||||
UPDATE_ROW: updateRow.run,
|
UPDATE_ROW: updateRow.run,
|
||||||
DELETE_ROW: deleteRow.run,
|
DELETE_ROW: deleteRow.run,
|
||||||
|
@ -24,7 +26,8 @@ const BUILTIN_ACTIONS = {
|
||||||
EXECUTE_QUERY: executeQuery.run,
|
EXECUTE_QUERY: executeQuery.run,
|
||||||
}
|
}
|
||||||
const BUILTIN_DEFINITIONS = {
|
const BUILTIN_DEFINITIONS = {
|
||||||
SEND_EMAIL: sendEmail.definition,
|
SEND_EMAIL: sendgridEmail.definition,
|
||||||
|
SEND_EMAIL_SMTP: sendSmtpEmail.definition,
|
||||||
CREATE_ROW: createRow.definition,
|
CREATE_ROW: createRow.definition,
|
||||||
UPDATE_ROW: updateRow.definition,
|
UPDATE_ROW: updateRow.definition,
|
||||||
DELETE_ROW: deleteRow.definition,
|
DELETE_ROW: deleteRow.definition,
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
const { sendSmtpEmail } = require("../../utilities/workerRequests")
|
||||||
|
|
||||||
|
module.exports.definition = {
|
||||||
|
description: "Send an email using SMTP",
|
||||||
|
tagline: "Send SMTP email to {{inputs.to}}",
|
||||||
|
icon: "ri-mail-open-line",
|
||||||
|
name: "Send Email (SMTP)",
|
||||||
|
type: "ACTION",
|
||||||
|
stepId: "SEND_EMAIL_SMTP",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
to: {
|
||||||
|
type: "string",
|
||||||
|
title: "Send To",
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
type: "string",
|
||||||
|
title: "Send From",
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
type: "string",
|
||||||
|
title: "Email Subject",
|
||||||
|
},
|
||||||
|
contents: {
|
||||||
|
type: "string",
|
||||||
|
title: "HTML Contents",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["to", "from", "subject", "contents"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
success: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Whether the email was sent",
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
type: "object",
|
||||||
|
description: "A response from the email client, this may be an error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["success"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.run = async function ({ inputs }) {
|
||||||
|
let { to, from, subject, contents } = inputs
|
||||||
|
if (!contents) {
|
||||||
|
contents = "<h1>No content</h1>"
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let response = await sendSmtpEmail(to, from, subject, contents)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
response,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
response: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
module.exports.definition = {
|
module.exports.definition = {
|
||||||
description: "Send an email",
|
description: "Send an email using SendGrid",
|
||||||
tagline: "Send email to {{inputs.to}}",
|
tagline: "Send email to {{inputs.to}}",
|
||||||
icon: "ri-mail-open-line",
|
icon: "ri-mail-open-line",
|
||||||
name: "Send Email",
|
name: "Send Email (SendGrid)",
|
||||||
type: "ACTION",
|
type: "ACTION",
|
||||||
stepId: "SEND_EMAIL",
|
stepId: "SEND_EMAIL",
|
||||||
inputs: {},
|
inputs: {},
|
|
@ -34,6 +34,7 @@ module.exports = {
|
||||||
USE_QUOTAS: process.env.USE_QUOTAS,
|
USE_QUOTAS: process.env.USE_QUOTAS,
|
||||||
REDIS_URL: process.env.REDIS_URL,
|
REDIS_URL: process.env.REDIS_URL,
|
||||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
// environment
|
// environment
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
||||||
|
@ -53,7 +54,6 @@ module.exports = {
|
||||||
BUDIBASE_API_KEY: process.env.BUDIBASE_API_KEY,
|
BUDIBASE_API_KEY: process.env.BUDIBASE_API_KEY,
|
||||||
USERID_API_KEY: process.env.USERID_API_KEY,
|
USERID_API_KEY: process.env.USERID_API_KEY,
|
||||||
DEPLOYMENT_CREDENTIALS_URL: process.env.DEPLOYMENT_CREDENTIALS_URL,
|
DEPLOYMENT_CREDENTIALS_URL: process.env.DEPLOYMENT_CREDENTIALS_URL,
|
||||||
HOSTING_KEY: process.env.HOSTING_KEY,
|
|
||||||
_set(key, value) {
|
_set(key, value) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
module.exports[key] = value
|
module.exports[key] = value
|
||||||
|
|
|
@ -28,7 +28,7 @@ function request(ctx, request) {
|
||||||
} else {
|
} else {
|
||||||
delete request.body
|
delete request.body
|
||||||
}
|
}
|
||||||
if (ctx.headers) {
|
if (ctx && ctx.headers) {
|
||||||
request.headers.cookie = ctx.headers.cookie
|
request.headers.cookie = ctx.headers.cookie
|
||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
|
@ -36,6 +36,31 @@ function request(ctx, request) {
|
||||||
|
|
||||||
exports.request = request
|
exports.request = request
|
||||||
|
|
||||||
|
exports.sendSmtpEmail = async (to, from, subject, contents) => {
|
||||||
|
const response = await fetch(
|
||||||
|
checkSlashesInUrl(env.WORKER_URL + `/api/admin/email/send`),
|
||||||
|
request(null, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"x-budibase-api-key": env.INTERNAL_API_KEY,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
email: to,
|
||||||
|
from,
|
||||||
|
contents,
|
||||||
|
subject,
|
||||||
|
purpose: "custom",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
if (json.status !== 200 && response.status !== 200) {
|
||||||
|
throw "Unable to send email."
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
exports.getDeployedApps = async ctx => {
|
exports.getDeployedApps = async ctx => {
|
||||||
if (!env.SELF_HOSTED) {
|
if (!env.SELF_HOSTED) {
|
||||||
throw "Can only check apps for self hosted environments"
|
throw "Can only check apps for self hosted environments"
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,7 @@ async function init() {
|
||||||
SELF_HOSTED: 1,
|
SELF_HOSTED: 1,
|
||||||
PORT: 4002,
|
PORT: 4002,
|
||||||
JWT_SECRET: "testsecret",
|
JWT_SECRET: "testsecret",
|
||||||
|
INTERNAL_API_KEY: "budibase",
|
||||||
MINIO_ACCESS_KEY: "budibase",
|
MINIO_ACCESS_KEY: "budibase",
|
||||||
MINIO_SECRET_KEY: "budibase",
|
MINIO_SECRET_KEY: "budibase",
|
||||||
COUCH_DB_USER: "budibase",
|
COUCH_DB_USER: "budibase",
|
||||||
|
|
|
@ -54,7 +54,10 @@ exports.reset = async ctx => {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const user = await getGlobalUserByEmail(email)
|
const user = await getGlobalUserByEmail(email)
|
||||||
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { user })
|
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
|
||||||
|
user,
|
||||||
|
subject: "{{ company }} platform password reset",
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// don't throw any kind of error to the user, this might give away something
|
// don't throw any kind of error to the user, this might give away something
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,27 @@ const authPkg = require("@budibase/auth")
|
||||||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
exports.sendEmail = async ctx => {
|
exports.sendEmail = async ctx => {
|
||||||
const { groupId, email, userId, purpose } = ctx.request.body
|
const {
|
||||||
|
groupId,
|
||||||
|
email,
|
||||||
|
userId,
|
||||||
|
purpose,
|
||||||
|
contents,
|
||||||
|
from,
|
||||||
|
subject,
|
||||||
|
} = ctx.request.body
|
||||||
let user
|
let user
|
||||||
if (userId) {
|
if (userId) {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
user = await db.get(userId)
|
user = await db.get(userId)
|
||||||
}
|
}
|
||||||
const response = await sendEmail(email, purpose, { groupId, user })
|
const response = await sendEmail(email, purpose, {
|
||||||
|
groupId,
|
||||||
|
user,
|
||||||
|
contents,
|
||||||
|
from,
|
||||||
|
subject,
|
||||||
|
})
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
...response,
|
...response,
|
||||||
message: `Email sent to ${email}.`,
|
message: `Email sent to ${email}.`,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { generateTemplateID, StaticDatabases } = require("@budibase/auth").db
|
const { generateTemplateID, StaticDatabases } = require("@budibase/auth").db
|
||||||
const { CouchDB } = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
const {
|
const {
|
||||||
TemplateMetadata,
|
TemplateMetadata,
|
||||||
TemplateBindings,
|
TemplateBindings,
|
||||||
|
@ -11,7 +11,6 @@ const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
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
|
|
||||||
let template = ctx.request.body
|
let template = ctx.request.body
|
||||||
if (!template.ownerId) {
|
if (!template.ownerId) {
|
||||||
template.ownerId = GLOBAL_OWNER
|
template.ownerId = GLOBAL_OWNER
|
||||||
|
@ -20,10 +19,7 @@ exports.save = async ctx => {
|
||||||
template._id = generateTemplateID(template.ownerId)
|
template._id = generateTemplateID(template.ownerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await db.put({
|
const response = await db.put(template)
|
||||||
...template,
|
|
||||||
type,
|
|
||||||
})
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
...template,
|
...template,
|
||||||
_rev: response.rev,
|
_rev: response.rev,
|
||||||
|
@ -31,9 +27,17 @@ exports.save = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.definitions = async ctx => {
|
exports.definitions = async ctx => {
|
||||||
|
const bindings = {}
|
||||||
|
|
||||||
|
for (let template of TemplateMetadata.email) {
|
||||||
|
bindings[template.purpose] = template.bindings
|
||||||
|
}
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
purpose: TemplateMetadata,
|
bindings: {
|
||||||
bindings: Object.values(TemplateBindings),
|
...bindings,
|
||||||
|
common: Object.values(TemplateBindings),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,9 @@ exports.invite = async ctx => {
|
||||||
if (existing) {
|
if (existing) {
|
||||||
ctx.throw(400, "Email address already in use.")
|
ctx.throw(400, "Email address already in use.")
|
||||||
}
|
}
|
||||||
await sendEmail(email, EmailTemplatePurpose.INVITATION)
|
await sendEmail(email, EmailTemplatePurpose.INVITATION, {
|
||||||
|
subject: "{{ company }} platform invitation",
|
||||||
|
})
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "Invitation has been sent.",
|
message: "Invitation has been sent.",
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
const authPkg = require("@budibase/auth")
|
||||||
|
const { google } = require("@budibase/auth/src/middleware")
|
||||||
|
const { Configs } = require("../../constants")
|
||||||
|
const CouchDB = require("../../db")
|
||||||
|
const { clearCookie } = authPkg.utils
|
||||||
|
const { Cookies } = authPkg.constants
|
||||||
|
const { passport } = authPkg.auth
|
||||||
|
|
||||||
|
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
|
||||||
|
|
||||||
|
exports.authenticate = async (ctx, next) => {
|
||||||
|
return passport.authenticate("local", async (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
return ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
const expires = new Date()
|
||||||
|
expires.setDate(expires.getDate() + 1)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.cookies.set(Cookies.Auth, user.token, {
|
||||||
|
expires,
|
||||||
|
path: "/",
|
||||||
|
httpOnly: false,
|
||||||
|
overwrite: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
delete user.token
|
||||||
|
|
||||||
|
ctx.body = { user }
|
||||||
|
})(ctx, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.logout = async ctx => {
|
||||||
|
clearCookie(ctx, Cookies.Auth)
|
||||||
|
ctx.body = { message: "User logged out" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial call that google authentication makes to take you to the google login screen.
|
||||||
|
* On a successful login, you will be redirected to the googleAuth callback route.
|
||||||
|
*/
|
||||||
|
exports.googlePreAuth = async (ctx, next) => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const config = await authPkg.db.getScopedFullConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
group: ctx.query.group,
|
||||||
|
})
|
||||||
|
const strategy = await google.strategyFactory(config)
|
||||||
|
|
||||||
|
return passport.authenticate(strategy, {
|
||||||
|
scope: ["profile", "email"],
|
||||||
|
})(ctx, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.googleAuth = async (ctx, next) => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
|
||||||
|
const config = await authPkg.db.getScopedFullConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
group: ctx.query.group,
|
||||||
|
})
|
||||||
|
const strategy = await google.strategyFactory(config)
|
||||||
|
|
||||||
|
return passport.authenticate(
|
||||||
|
strategy,
|
||||||
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
|
async (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
return ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
const expires = new Date()
|
||||||
|
expires.setDate(expires.getDate() + 1)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.cookies.set(Cookies.Auth, user.token, {
|
||||||
|
expires,
|
||||||
|
path: "/",
|
||||||
|
httpOnly: false,
|
||||||
|
overwrite: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.redirect("/")
|
||||||
|
}
|
||||||
|
)(ctx, next)
|
||||||
|
}
|
|
@ -53,9 +53,9 @@ router
|
||||||
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
||||||
// for now no public access is allowed to worker (bar health check)
|
// for now no public access is allowed to worker (bar health check)
|
||||||
.use((ctx, next) => {
|
.use((ctx, next) => {
|
||||||
if (!ctx.isAuthenticated) {
|
// if (!ctx.isAuthenticated) {
|
||||||
ctx.throw(403, "Unauthorized - no public worker access")
|
// ctx.throw(403, "Unauthorized - no public worker access")
|
||||||
}
|
// }
|
||||||
return next()
|
return next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ function smtpValidation() {
|
||||||
auth: Joi.object({
|
auth: Joi.object({
|
||||||
type: Joi.string().valid("login", "oauth2", null),
|
type: Joi.string().valid("login", "oauth2", null),
|
||||||
user: Joi.string().required(),
|
user: Joi.string().required(),
|
||||||
pass: Joi.string().valid("", null),
|
pass: Joi.string().allow("", null),
|
||||||
}).optional(),
|
}).optional(),
|
||||||
}).unknown(true)
|
}).unknown(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,11 @@ function buildEmailSendValidation() {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return joiValidator.body(Joi.object({
|
return joiValidator.body(Joi.object({
|
||||||
email: Joi.string().email(),
|
email: Joi.string().email(),
|
||||||
|
purpose: Joi.string().valid(...Object.values(EmailTemplatePurpose)),
|
||||||
groupId: Joi.string().allow("", null),
|
groupId: Joi.string().allow("", null),
|
||||||
purpose: Joi.string().allow(...Object.values(EmailTemplatePurpose)),
|
fromt: Joi.string().allow("", null),
|
||||||
|
contents: Joi.string().allow("", null),
|
||||||
|
subject: Joi.string().allow("", null),
|
||||||
}).required().unknown(true))
|
}).required().unknown(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,13 @@ const setup = require("./utilities")
|
||||||
const { EmailTemplatePurpose } = require("../../../constants")
|
const { EmailTemplatePurpose } = require("../../../constants")
|
||||||
|
|
||||||
// mock the email system
|
// mock the email system
|
||||||
|
const sendMailMock = jest.fn()
|
||||||
jest.mock("nodemailer")
|
jest.mock("nodemailer")
|
||||||
const sendMailMock = setup.emailMock()
|
const nodemailer = require("nodemailer")
|
||||||
|
nodemailer.createTransport.mockReturnValue({
|
||||||
|
sendMail: sendMailMock,
|
||||||
|
verify: jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
describe("/api/admin/email", () => {
|
describe("/api/admin/email", () => {
|
||||||
let request = setup.getRequest()
|
let request = setup.getRequest()
|
||||||
|
|
|
@ -29,6 +29,7 @@ describe("/api/admin/email", () => {
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.message).toBeDefined()
|
expect(res.body.message).toBeDefined()
|
||||||
const testUrl = nodemailer.getTestMessageUrl(res.body)
|
const testUrl = nodemailer.getTestMessageUrl(res.body)
|
||||||
|
console.log(`${purpose} URL: ${testUrl}`)
|
||||||
expect(testUrl).toBeDefined()
|
expect(testUrl).toBeDefined()
|
||||||
const response = await fetch(testUrl)
|
const response = await fetch(testUrl)
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
|
|
|
@ -27,7 +27,6 @@ const TemplateTypes = {
|
||||||
|
|
||||||
const EmailTemplatePurpose = {
|
const EmailTemplatePurpose = {
|
||||||
BASE: "base",
|
BASE: "base",
|
||||||
STYLES: "styles",
|
|
||||||
PASSWORD_RECOVERY: "password_recovery",
|
PASSWORD_RECOVERY: "password_recovery",
|
||||||
INVITATION: "invitation",
|
INVITATION: "invitation",
|
||||||
WELCOME: "welcome",
|
WELCOME: "welcome",
|
||||||
|
@ -35,47 +34,105 @@ const EmailTemplatePurpose = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateBindings = {
|
const TemplateBindings = {
|
||||||
PLATFORM_URL: "platformUrl",
|
PLATFORM_URL: {
|
||||||
COMPANY: "company",
|
name: "platformUrl",
|
||||||
LOGO_URL: "logoUrl",
|
description: "The URL used to access the budibase platform",
|
||||||
STYLES: "styles",
|
},
|
||||||
BODY: "body",
|
COMPANY: {
|
||||||
REGISTRATION_URL: "registrationUrl",
|
name: "company",
|
||||||
EMAIL: "email",
|
description: "The name of your organization",
|
||||||
RESET_URL: "resetUrl",
|
},
|
||||||
USER: "user",
|
LOGO_URL: {
|
||||||
REQUEST: "request",
|
name: "logoUrl",
|
||||||
DOCS_URL: "docsUrl",
|
description: "The URL of your organizations logo.",
|
||||||
LOGIN_URL: "loginUrl",
|
},
|
||||||
CURRENT_YEAR: "currentYear",
|
EMAIL: {
|
||||||
CURRENT_DATE: "currentDate",
|
name: "email",
|
||||||
RESET_CODE: "resetCode",
|
description: "The recipients email address.",
|
||||||
INVITE_CODE: "inviteCode",
|
},
|
||||||
|
USER: {
|
||||||
|
name: "user",
|
||||||
|
description: "The recipients user object.",
|
||||||
|
},
|
||||||
|
REQUEST: {
|
||||||
|
name: "request",
|
||||||
|
description: "Additional request metadata.",
|
||||||
|
},
|
||||||
|
DOCS_URL: {
|
||||||
|
name: "docsUrl",
|
||||||
|
description: "Organization documentation URL.",
|
||||||
|
},
|
||||||
|
LOGIN_URL: {
|
||||||
|
name: "loginUrl",
|
||||||
|
description: "The URL used to log into the organization budibase instance.",
|
||||||
|
},
|
||||||
|
CURRENT_YEAR: {
|
||||||
|
name: "currentYear",
|
||||||
|
description: "The current year.",
|
||||||
|
},
|
||||||
|
CURRENT_DATE: {
|
||||||
|
name: "currentDate",
|
||||||
|
description: "The current date.",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateMetadata = {
|
const TemplateMetadata = {
|
||||||
[TemplateTypes.EMAIL]: [
|
[TemplateTypes.EMAIL]: [
|
||||||
{
|
|
||||||
name: "Styling",
|
|
||||||
purpose: EmailTemplatePurpose.STYLES,
|
|
||||||
bindings: ["url", "company", "companyUrl", "styles", "body"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Base Format",
|
name: "Base Format",
|
||||||
purpose: EmailTemplatePurpose.BASE,
|
purpose: EmailTemplatePurpose.BASE,
|
||||||
bindings: ["company", "registrationUrl"],
|
bindings: [
|
||||||
|
{
|
||||||
|
name: "body",
|
||||||
|
description: "The main body of another email template.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "styles",
|
||||||
|
description: "The contents of the Styling email template.",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Password Recovery",
|
name: "Password Recovery",
|
||||||
purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
|
purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
name: "resetUrl",
|
||||||
|
description:
|
||||||
|
"The URL the recipient must click to reset their password.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resetCode",
|
||||||
|
description:
|
||||||
|
"The temporary password reset code used in the recipients password reset URL.",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "New User Invitation",
|
name: "New User Invitation",
|
||||||
purpose: EmailTemplatePurpose.INVITATION,
|
purpose: EmailTemplatePurpose.INVITATION,
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
name: "inviteUrl",
|
||||||
|
description:
|
||||||
|
"The URL the recipient must click to accept the invitation and activate their account.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inviteCode",
|
||||||
|
description:
|
||||||
|
"The temporary invite code used in the recipients invitation URL.",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Custom",
|
name: "Custom",
|
||||||
purpose: EmailTemplatePurpose.CUSTOM,
|
purpose: EmailTemplatePurpose.CUSTOM,
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
name: "contents",
|
||||||
|
description: "Custom content body.",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,426 @@
|
||||||
<meta name="supported-color-schemes" content="light dark" />
|
<meta name="supported-color-schemes" content="light dark" />
|
||||||
<title></title>
|
<title></title>
|
||||||
<style type="text/css" rel="stylesheet" media="all">
|
<style type="text/css" rel="stylesheet" media="all">
|
||||||
{{ styles }}
|
@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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<!--[if mso]>
|
<!--[if mso]>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<tr>
|
||||||
|
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
|
||||||
|
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<!-- Body content -->
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell">
|
||||||
|
<div class="f-fallback">
|
||||||
|
{{ contents }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
|
@ -17,10 +17,10 @@ exports.EmailTemplates = {
|
||||||
join(__dirname, "invitation.hbs")
|
join(__dirname, "invitation.hbs")
|
||||||
),
|
),
|
||||||
[EmailTemplatePurpose.BASE]: readStaticFile(join(__dirname, "base.hbs")),
|
[EmailTemplatePurpose.BASE]: readStaticFile(join(__dirname, "base.hbs")),
|
||||||
[EmailTemplatePurpose.STYLES]: readStaticFile(join(__dirname, "style.hbs")),
|
|
||||||
[EmailTemplatePurpose.WELCOME]: readStaticFile(
|
[EmailTemplatePurpose.WELCOME]: readStaticFile(
|
||||||
join(__dirname, "welcome.hbs")
|
join(__dirname, "welcome.hbs")
|
||||||
),
|
),
|
||||||
|
[EmailTemplatePurpose.CUSTOM]: readStaticFile(join(__dirname, "custom.hbs")),
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.addBaseTemplates = (templates, type = null) => {
|
exports.addBaseTemplates = (templates, type = null) => {
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="{{ registrationUrl }}" class="f-fallback button" target="_blank">Set up account</a>
|
<a href="{{ inviteUrl }}" class="f-fallback button" target="_blank">Set up account</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
||||||
<p class="f-fallback sub">{{ registrationUrl }}</p>
|
<p class="f-fallback sub">{{ inviteUrl }}</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -28,8 +28,8 @@ module.exports = {
|
||||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||||
REDIS_URL: process.env.REDIS_URL,
|
REDIS_URL: process.env.REDIS_URL,
|
||||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
/* TODO: to remove - once deployment removed */
|
/* TODO: to remove - once deployment removed */
|
||||||
SELF_HOST_KEY: process.env.SELF_HOST_KEY,
|
|
||||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME,
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USERNAME,
|
||||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||||
_set(key, value) {
|
_set(key, value) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ const { getSettingsTemplateContext } = require("./templates")
|
||||||
const { processString } = require("@budibase/string-templates")
|
const { processString } = require("@budibase/string-templates")
|
||||||
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
|
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
|
||||||
|
|
||||||
|
const TEST_MODE = false
|
||||||
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
||||||
const TYPE = TemplateTypes.EMAIL
|
const TYPE = TemplateTypes.EMAIL
|
||||||
|
|
||||||
|
@ -14,18 +15,32 @@ const FULL_EMAIL_PURPOSES = [
|
||||||
EmailTemplatePurpose.INVITATION,
|
EmailTemplatePurpose.INVITATION,
|
||||||
EmailTemplatePurpose.PASSWORD_RECOVERY,
|
EmailTemplatePurpose.PASSWORD_RECOVERY,
|
||||||
EmailTemplatePurpose.WELCOME,
|
EmailTemplatePurpose.WELCOME,
|
||||||
|
EmailTemplatePurpose.CUSTOM,
|
||||||
]
|
]
|
||||||
|
|
||||||
function createSMTPTransport(config) {
|
function createSMTPTransport(config) {
|
||||||
const options = {
|
let options
|
||||||
port: config.port,
|
if (!TEST_MODE) {
|
||||||
host: config.host,
|
options = {
|
||||||
secure: config.secure || false,
|
port: config.port,
|
||||||
auth: config.auth,
|
host: config.host,
|
||||||
}
|
secure: config.secure || false,
|
||||||
if (config.selfSigned) {
|
auth: config.auth,
|
||||||
options.tls = {
|
}
|
||||||
rejectUnauthorized: false,
|
if (config.selfSigned) {
|
||||||
|
options.tls = {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options = {
|
||||||
|
port: 587,
|
||||||
|
host: "smtp.ethereal.email",
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: "don.bahringer@ethereal.email",
|
||||||
|
pass: "yCKSH8rWyUPbnhGYk9",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nodemailer.createTransport(options)
|
return nodemailer.createTransport(options)
|
||||||
|
@ -46,40 +61,36 @@ async function getLinkCode(purpose, email, user) {
|
||||||
* Builds an email using handlebars and the templates found in the system (default or otherwise).
|
* Builds an email using handlebars and the templates found in the system (default or otherwise).
|
||||||
* @param {string} purpose the purpose of the email being built, e.g. invitation, password reset.
|
* @param {string} purpose the purpose of the email being built, e.g. invitation, password reset.
|
||||||
* @param {string} email the address which it is being sent to for contextual purposes.
|
* @param {string} email the address which it is being sent to for contextual purposes.
|
||||||
* @param {object|null} user If being sent to an existing user then the object can be provided for context.
|
* @param {object} context the context which is being used for building the email (hbs context).
|
||||||
|
* @param {object|null} user if being sent to an existing user then the object can be provided for context.
|
||||||
|
* @param {string|null} contents if using a custom template can supply contents for context.
|
||||||
* @return {Promise<string>} returns the built email HTML if all provided parameters were valid.
|
* @return {Promise<string>} returns the built email HTML if all provided parameters were valid.
|
||||||
*/
|
*/
|
||||||
async function buildEmail(purpose, email, user) {
|
async function buildEmail(purpose, email, context, { user, contents } = {}) {
|
||||||
// this isn't a full email
|
// this isn't a full email
|
||||||
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
||||||
throw `Unable to build an email of type ${purpose}`
|
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.BASE),
|
||||||
getTemplateByPurpose(TYPE, EmailTemplatePurpose.STYLES),
|
|
||||||
getTemplateByPurpose(TYPE, purpose),
|
getTemplateByPurpose(TYPE, purpose),
|
||||||
])
|
])
|
||||||
if (!base || !styles || !body) {
|
if (!base || !body) {
|
||||||
throw "Unable to build email, missing base components"
|
throw "Unable to build email, missing base components"
|
||||||
}
|
}
|
||||||
base = base.contents
|
base = base.contents
|
||||||
styles = styles.contents
|
|
||||||
body = body.contents
|
body = body.contents
|
||||||
|
context = {
|
||||||
// if there is a link code needed this will retrieve it
|
...context,
|
||||||
const code = await getLinkCode(purpose, email, user)
|
contents,
|
||||||
const context = {
|
|
||||||
...(await getSettingsTemplateContext(purpose, code)),
|
|
||||||
email,
|
email,
|
||||||
user: user || {},
|
user: user || {},
|
||||||
}
|
}
|
||||||
|
|
||||||
body = await processString(body, context)
|
body = await processString(body, context)
|
||||||
styles = await processString(styles, context)
|
|
||||||
// this should now be the complete email HTML
|
// this should now be the complete email HTML
|
||||||
return processString(base, {
|
return processString(base, {
|
||||||
...context,
|
...context,
|
||||||
styles,
|
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -117,24 +128,38 @@ exports.isEmailConfigured = async (groupId = null) => {
|
||||||
* @param {string} email The email address to send to.
|
* @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} 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 {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.
|
||||||
|
* @param {string|undefined} subject A custom subject can be specified if the config one is not desired.
|
||||||
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
|
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
|
||||||
* nodemailer response.
|
* nodemailer response.
|
||||||
*/
|
*/
|
||||||
exports.sendEmail = async (email, purpose, { groupId, user } = {}) => {
|
exports.sendEmail = async (
|
||||||
|
email,
|
||||||
|
purpose,
|
||||||
|
{ groupId, user, from, contents, subject } = {}
|
||||||
|
) => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
const config = await getSmtpConfiguration(db, groupId)
|
let config = (await getSmtpConfiguration(db, groupId)) || {}
|
||||||
if (!config) {
|
if (Object.keys(config).length === 0 && !TEST_MODE) {
|
||||||
throw "Unable to find SMTP configuration."
|
throw "Unable to find SMTP configuration."
|
||||||
}
|
}
|
||||||
const transport = createSMTPTransport(config)
|
const transport = createSMTPTransport(config)
|
||||||
|
// if there is a link code needed this will retrieve it
|
||||||
|
const code = await getLinkCode(purpose, email, user)
|
||||||
|
const context = await getSettingsTemplateContext(purpose, code)
|
||||||
const message = {
|
const message = {
|
||||||
from: config.from,
|
from: from || config.from,
|
||||||
subject: config.subject,
|
subject: await processString(subject || config.subject, context),
|
||||||
to: email,
|
to: email,
|
||||||
html: await buildEmail(purpose, email, user),
|
html: await buildEmail(purpose, email, context, { user, contents }),
|
||||||
}
|
}
|
||||||
return transport.sendMail(message)
|
const response = await transport.sendMail(message)
|
||||||
|
if (TEST_MODE) {
|
||||||
|
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
|
||||||
|
}
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15,8 +15,8 @@ const BASE_COMPANY = "Budibase"
|
||||||
exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
||||||
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
const db = new CouchDB(StaticDatabases.GLOBAL.name)
|
||||||
// TODO: use more granular settings in the future if required
|
// TODO: use more granular settings in the future if required
|
||||||
const settings = await getScopedConfig(db, { type: Configs.SETTINGS })
|
let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {}
|
||||||
if (!settings.platformUrl) {
|
if (!settings || !settings.platformUrl) {
|
||||||
settings.platformUrl = LOCAL_URL
|
settings.platformUrl = LOCAL_URL
|
||||||
}
|
}
|
||||||
const URL = settings.platformUrl
|
const URL = settings.platformUrl
|
||||||
|
@ -41,7 +41,7 @@ exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
||||||
break
|
break
|
||||||
case EmailTemplatePurpose.INVITATION:
|
case EmailTemplatePurpose.INVITATION:
|
||||||
context[TemplateBindings.INVITE_CODE] = code
|
context[TemplateBindings.INVITE_CODE] = code
|
||||||
context[TemplateBindings.REGISTRATION_URL] = checkSlashesInUrl(
|
context[TemplateBindings.INVITE_URL] = checkSlashesInUrl(
|
||||||
`${URL}/invite?code=${code}`
|
`${URL}/invite?code=${code}`
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
Loading…
Reference in New Issue