1
0
Fork 0
mirror of synced 2024-06-30 03:50:37 +12:00

Merge branch 'next' into admin/user-management-ui

This commit is contained in:
Keviin Åberg Kultalahti 2021-05-12 14:08:58 +02:00
commit 0da5247a03
47 changed files with 2783 additions and 535 deletions

View file

@ -8,10 +8,10 @@
</h1>
<h3 align="center">
Build internal tools 50x faster on your own infrastructure
Build custom business tools in minutes and on your own infrastructure
</h3>
<p align="center">
Budibase is an open-source low-code platform, helping developers and IT professionals build, automate, and ship internal tools 50x faster on their own infrastructure.
Budibase is an open-source low-code platform, helping developers and IT professionals build, automate, and ship custom business apps in minutes and on their own infrastructure.
</p>
<h3 align="center">

View file

@ -16,7 +16,7 @@ services:
MINIO_URL: http://minio-service:9000
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
HOSTING_KEY: ${HOSTING_KEY}
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
PORT: 4002
JWT_SECRET: ${JWT_SECRET}
@ -44,7 +44,7 @@ services:
COUCH_DB_USERNAME: ${COUCH_DB_USER}
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
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_PASSWORD: ${REDIS_PASSWORD}
depends_on:

View file

@ -1,10 +1,6 @@
# Use the main port in the builder for your self hosting URL, e.g. localhost: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
# These should be updated
JWT_SECRET=testsecret
@ -13,6 +9,7 @@ MINIO_SECRET_KEY=budibase
COUCH_DB_PASSWORD=budibase
COUCH_DB_USER=budibase
REDIS_PASSWORD=budibase
INTERNAL_API_KEY=budibase
# This section contains variables that do not need to be altered under normal circumstances
APP_PORT=4002

198
i18n/README.es.md Normal file
View file

@ -0,0 +1,198 @@
<p align="center">
<a href="https://www.budibase.com">
<img alt="Budibase" src="https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg" width="60" />
</a>
</p>
<h1 align="center">
Budibase
</h1>
<h3 align="center">
Construye herramientas empresariales personalizadas en cuestión de minutos y en su propia infraestructura.
</h3>
<p align="center">
Budibase es una plataforma de código bajo de código abierto, que ayuda a desarrolladores y profesionales de TI a crear, automatizar y enviar aplicaciones empresariales personalizadas en cuestión de minutos y en su propia infraestructura
</p>
<h3 align="center">
🤖 🎨 🚀
</h3>
<p align="center">
<img src="https://i.imgur.com/tPQHruf.png">
</p>
<p align="center">
<a href="https://github.com/Budibase/budibase/releases">
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/Budibase/budibase/total">
</a>
<a href="https://github.com/Budibase/budibase/releases">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/Budibase/budibase">
</a>
<a href="https://discord.gg/rCYayfe">
<img alt="Discord" src="https://img.shields.io/discord/733030666647765003">
</a>
<a href="https://twitter.com/intent/follow?screen_name=budibase">
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Follow @budibase" />
</a>
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Code of conduct" />
<a href="https://codecov.io/gh/Budibase/budibase">
<img src="https://codecov.io/gh/Budibase/budibase/graph/badge.svg?token=E8W2ZFXQOH"/>
</a>
</p>
<h3 align="center">
<a href="https://portal.budi.live/signup">Sign-up</a>
<span> · </span>
<a href="https://docs.budibase.com">Docs</a>
<span> · </span>
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
<span> · </span>
<a href="https://github.com/Budibase/budibase/issues">Report a bug</a>
<span> · </span>
Support: <a href="https://github.com/Budibase/budibase/discussions">Discussions</a>
<span> & </span>
<a href="https://discord.gg/rCYayfe">Discord</a>
</h3>
## ✨ Features
When other platforms chose the closed source route, we decided to go open source. When other platforms chose cloud builders, we decided a local builder offered the better developer experience. We like to do things differently at Budibase.
- **Build and ship real software.** Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing your users with a great experience.
- **Open source and extensable.** Budibase is open-source. The builder is licensed AGPL v3, the server is GPL v3, and the client is MPL. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, mySQL, Airtable, Google Sheets, S3, DyanmoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
- **Design and build apps with powerful pre-made components.** Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new components](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
- **Automate processes, integrate with other tools, and connect to webhooks.** Save time by automating manual processes and workflows. From connecting to webhooks, to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [request new integrations here](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
- **Cloud hosting and self-hosting.** Users can self-host (see below), or host their apps with Budibase. Currently, our cloud hosting offering is limited to the free tier but we aim to change this in the future. For heavy usage, we advise users to self-host.
<p align="center">
<img alt="Budibase design ui" src="https://imgur.com/v8m6v3q.png">
</p>
## ⌛ Status
- [x] Alpha: We are demoing Budibase to users and receiving feedback
- [x] Private Beta: We are testing Budibase with a closed set of customers
- [x] Public Beta: Anyone can [sign-up and use Budibase](https://portal.budi.live/signup).
- [ ] Official Launch
Watch "releases" of this repo to get notified of major updates, and give the star button a click whilst you're there.
<p align="center">
<img src="https://i.imgur.com/cJpgqm8.png">
</p>
### Stargazers over time
[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) to clear down your environment.
## 🏁 Getting Started with Budibase
The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps below to get started:
- [ ] [Sign-up to Budibase](https://portal.budi.live/signup)
- [ ] Create a username and password
- [ ] Copy your API key
- [ ] Download Budibase
- [ ] Open Budibase and enter your API key
[Here is a guided tutorial](https://docs.budibase.com/tutorial/tutorial-signing-up) if you need extra help.
## 🤖 Self-hosting
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb&region=nyc1&refcode=0caaa6085a82&image=budibase-20-04)
## 🎓 Learning Budibase
The Budibase [documentation lives here](https://docs.budibase.com).
You can also follow a quick tutorial on [how to build a CRM with Budibase](https://docs.budibase.com/tutorial/tutorial-introduction)
## Roadmap
Checkout our [Public Roadmap](https://github.com/Budibase/budibase/projects/10). If you would like to discuss some of the items on the roadmap, please feel to reach out on [Discord](https://discord.gg/rCYayfe), or via [Github discussions](https://github.com/Budibase/budibase/discussions)
## ❗ Code of Conduct
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Please read it.
## 🙌 Contributing to Budibase
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API please create an issue first. This way we can ensure your work is not in vain.
### Not Sure Where to Start?
A good place to start contributing, is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
### How the repository is organized
Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client side svelte application.
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it.
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system.
For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)
## 📝 License
Budibase is open-source. The builder is licensed [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html), the server is licensed [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html), and the client is licensed [MPL](https://directory.fsf.org/wiki/License:MPL-2.0).
## 💬 Get in touch
If you have a question or would like to talk with other Budibase users, please hop over to [Github discussions](https://github.com/Budibase/budibase/discussions) or join our Discord server:
[Discord chatroom](https://discord.gg/rCYayfe)
![Discord Shield](https://discordapp.com/api/guilds/733030666647765003/widget.png?style=shield)
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://martinmck.com"><img src="https://avatars1.githubusercontent.com/u/11256663?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martin McKeaveney</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Tests">⚠️</a> <a href="#infra-shogunpurple" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="http://www.michaeldrury.co.uk/"><img src="https://avatars2.githubusercontent.com/u/4407001?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Drury</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Tests">⚠️</a> <a href="#infra-mike12345567" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/aptkingston"><img src="https://avatars3.githubusercontent.com/u/9075550?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew Kingston</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Tests">⚠️</a> <a href="#design-aptkingston" title="Design">🎨</a></td>
<td align="center"><a href="https://budibase.com/"><img src="https://avatars3.githubusercontent.com/u/3524181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Shanks</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/kevmodrome"><img src="https://avatars3.githubusercontent.com/u/534488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Åberg Kultalahti</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Tests">⚠️</a></td>
<td align="center"><a href="https://www.budibase.com/"><img src="https://avatars2.githubusercontent.com/u/49767913?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Code">💻</a> <a href="#content-joebudi" title="Content">🖋</a> <a href="#design-joebudi" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/HugoLd"><img src="https://avatars0.githubusercontent.com/u/26521848?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HugoLd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=HugoLd" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/victoriasloan"><img src="https://avatars.githubusercontent.com/u/9913651?v=4?s=100" width="100px;" alt=""/><br /><sub><b>victoriasloan</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=victoriasloan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/yashank09"><img src="https://avatars.githubusercontent.com/u/37672190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yashank09</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=yashank09" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SOVLOOKUP"><img src="https://avatars.githubusercontent.com/u/53158137?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SOVLOOKUP</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SOVLOOKUP" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View file

@ -71,7 +71,7 @@ exports.getGlobalUserParams = (globalId, otherProps = {}) => {
* @param ownerId The owner/user of the template, this could be global or a group level.
*/
exports.generateTemplateID = ownerId => {
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${newid()}`
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
}
/**

View file

@ -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_API_KEY: process.env.INTERNAL_API_KEY,
isTest,
}

View file

@ -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_API_KEY) {
ctx.isAuthenticated = true
} else if (authCookie) {
try {
const db = database.getDB(StaticDatabases.GLOBAL.name)
const user = await db.get(authCookie.userId)

View file

@ -15,6 +15,11 @@
</p>
<style>
p {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
.noPadding {
padding: 0;
margin: 0;

View file

@ -7,6 +7,10 @@
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.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 webhookModal
@ -70,6 +74,10 @@
on:change={e => (block.inputs[key] = e.detail)}
{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"}
<TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === "row"}
@ -78,6 +86,17 @@
<WebhookDisplay value={block.inputs[key]} />
{:else if value.customType === "triggerSchema"}
<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"}
<DrawerBindableInput
panel={AutomationBindingPanel}

View file

@ -11,8 +11,9 @@
}
</script>
<Modal bind:this={modal} width="60%">
<Modal bind:this={modal}>
<ModalContent
size="XL"
title="Edit Code"
showConfirmButton={false}
showCancelButton={false}

View file

@ -1,8 +1,8 @@
<script>
import { queries } from "stores/backend"
import { Select } from "@budibase/bbui"
import DrawerBindableInput from "../../common/DrawerBindableInput.svelte"
import AutomationBindingPanel from "./AutomationBindingPanel.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
export let value
export let bindings
@ -13,7 +13,6 @@
// Ensure any nullish queryId values get set to empty string so
// that the select works
$: if (value?.queryId == null) value = { queryId: "" }
$: console.log("daValuz", value)
</script>
<div class="block-field">

View file

@ -117,7 +117,7 @@
readOnly,
autoCloseBrackets: true,
autoCloseTags: true,
theme: $themeStore.darkMode ? THEMES.DARK : THEMES.LIGHT,
theme: $themeStore.theme.includes("light") ? THEMES.LIGHT : THEMES.DARK,
}
if (!tab)

View file

@ -147,7 +147,5 @@
}
.content {
overflow: auto;
display: grid;
grid-template-rows: 1fr;
}
</style>

View file

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

View file

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

View file

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

View file

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

View file

@ -91,7 +91,7 @@
</div>
{/each}
<div>
<Button primary on:click={() => save(google)}>Save</Button>
<Button cta on:click={() => save(google)}>Save</Button>
</div>
<Divider />
{/if}

View file

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

View file

@ -2,3 +2,4 @@ export { organisation } from "./organisation"
export { users } from "./users"
export { admin } from "./admin"
export { apps } from "./apps"
export { email } from "./email"

View file

@ -1,4 +1,4 @@
const { string, number } = require("../questions")
const { number } = require("../questions")
const { success } = require("../utils")
const fs = require("fs")
const path = require("path")
@ -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_API_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(

View file

@ -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_API_KEY: "budibase",
JWT_SECRET: "testsecret",
REDIS_PASSWORD: "budibase",
MINIO_ACCESS_KEY: "budibase",

View file

@ -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 updateRow = require("./steps/updateRow")
const deleteRow = require("./steps/deleteRow")
@ -14,7 +15,8 @@ const {
} = require("../utilities/fileSystem")
const BUILTIN_ACTIONS = {
SEND_EMAIL: sendEmail.run,
SEND_EMAIL: sendgridEmail.run,
SEND_EMAIL_SMTP: sendSmtpEmail.run,
CREATE_ROW: createRow.run,
UPDATE_ROW: updateRow.run,
DELETE_ROW: deleteRow.run,
@ -24,7 +26,8 @@ const BUILTIN_ACTIONS = {
EXECUTE_QUERY: executeQuery.run,
}
const BUILTIN_DEFINITIONS = {
SEND_EMAIL: sendEmail.definition,
SEND_EMAIL: sendgridEmail.definition,
SEND_EMAIL_SMTP: sendSmtpEmail.definition,
CREATE_ROW: createRow.definition,
UPDATE_ROW: updateRow.definition,
DELETE_ROW: deleteRow.definition,

View file

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

View file

@ -1,8 +1,8 @@
module.exports.definition = {
description: "Send an email",
description: "Send an email using SendGrid",
tagline: "Send email to {{inputs.to}}",
icon: "ri-mail-open-line",
name: "Send Email",
name: "Send Email (SendGrid)",
type: "ACTION",
stepId: "SEND_EMAIL",
inputs: {},

View file

@ -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_API_KEY: process.env.INTERNAL_API_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

View file

@ -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,31 @@ function request(ctx, 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 => {
if (!env.SELF_HOSTED) {
throw "Can only check apps for self hosted environments"

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@ async function init() {
SELF_HOSTED: 1,
PORT: 4002,
JWT_SECRET: "testsecret",
INTERNAL_API_KEY: "budibase",
MINIO_ACCESS_KEY: "budibase",
MINIO_SECRET_KEY: "budibase",
COUCH_DB_USER: "budibase",

View file

@ -54,7 +54,10 @@ exports.reset = async ctx => {
}
try {
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) {
// don't throw any kind of error to the user, this might give away something
}

View file

@ -5,13 +5,27 @@ const authPkg = require("@budibase/auth")
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
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
if (userId) {
const db = new CouchDB(GLOBAL_DB)
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 = {
...response,
message: `Email sent to ${email}.`,

View file

@ -1,5 +1,5 @@
const { generateTemplateID, StaticDatabases } = require("@budibase/auth").db
const { CouchDB } = require("../../../db")
const CouchDB = require("../../../db")
const {
TemplateMetadata,
TemplateBindings,
@ -11,7 +11,6 @@ const GLOBAL_DB = StaticDatabases.GLOBAL.name
exports.save = async ctx => {
const db = new CouchDB(GLOBAL_DB)
const type = ctx.params.type
let template = ctx.request.body
if (!template.ownerId) {
template.ownerId = GLOBAL_OWNER
@ -20,10 +19,7 @@ exports.save = async ctx => {
template._id = generateTemplateID(template.ownerId)
}
const response = await db.put({
...template,
type,
})
const response = await db.put(template)
ctx.body = {
...template,
_rev: response.rev,
@ -31,9 +27,17 @@ exports.save = async ctx => {
}
exports.definitions = async ctx => {
const bindings = {}
for (let template of TemplateMetadata.email) {
bindings[template.purpose] = template.bindings
}
ctx.body = {
purpose: TemplateMetadata,
bindings: Object.values(TemplateBindings),
bindings: {
...bindings,
common: Object.values(TemplateBindings),
},
}
}

View file

@ -136,7 +136,9 @@ exports.invite = async ctx => {
if (existing) {
ctx.throw(400, "Email address already in use.")
}
await sendEmail(email, EmailTemplatePurpose.INVITATION)
await sendEmail(email, EmailTemplatePurpose.INVITATION, {
subject: "{{ company }} platform invitation",
})
ctx.body = {
message: "Invitation has been sent.",
}

View file

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

View file

@ -17,7 +17,7 @@ function smtpValidation() {
auth: Joi.object({
type: Joi.string().valid("login", "oauth2", null),
user: Joi.string().required(),
pass: Joi.string().valid("", null),
pass: Joi.string().allow("", null),
}).optional(),
}).unknown(true)
}

View file

@ -10,8 +10,11 @@ function buildEmailSendValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
email: Joi.string().email(),
purpose: Joi.string().valid(...Object.values(EmailTemplatePurpose)),
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))
}

View file

@ -2,8 +2,13 @@ const setup = require("./utilities")
const { EmailTemplatePurpose } = require("../../../constants")
// mock the email system
const sendMailMock = jest.fn()
jest.mock("nodemailer")
const sendMailMock = setup.emailMock()
const nodemailer = require("nodemailer")
nodemailer.createTransport.mockReturnValue({
sendMail: sendMailMock,
verify: jest.fn()
})
describe("/api/admin/email", () => {
let request = setup.getRequest()

View file

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

View file

@ -27,7 +27,6 @@ const TemplateTypes = {
const EmailTemplatePurpose = {
BASE: "base",
STYLES: "styles",
PASSWORD_RECOVERY: "password_recovery",
INVITATION: "invitation",
WELCOME: "welcome",
@ -35,47 +34,105 @@ const EmailTemplatePurpose = {
}
const TemplateBindings = {
PLATFORM_URL: "platformUrl",
COMPANY: "company",
LOGO_URL: "logoUrl",
STYLES: "styles",
BODY: "body",
REGISTRATION_URL: "registrationUrl",
EMAIL: "email",
RESET_URL: "resetUrl",
USER: "user",
REQUEST: "request",
DOCS_URL: "docsUrl",
LOGIN_URL: "loginUrl",
CURRENT_YEAR: "currentYear",
CURRENT_DATE: "currentDate",
RESET_CODE: "resetCode",
INVITE_CODE: "inviteCode",
PLATFORM_URL: {
name: "platformUrl",
description: "The URL used to access the budibase platform",
},
COMPANY: {
name: "company",
description: "The name of your organization",
},
LOGO_URL: {
name: "logoUrl",
description: "The URL of your organizations logo.",
},
EMAIL: {
name: "email",
description: "The recipients email address.",
},
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 = {
[TemplateTypes.EMAIL]: [
{
name: "Styling",
purpose: EmailTemplatePurpose.STYLES,
bindings: ["url", "company", "companyUrl", "styles", "body"],
},
{
name: "Base Format",
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",
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",
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",
purpose: EmailTemplatePurpose.CUSTOM,
bindings: [
{
name: "contents",
description: "Custom content body.",
},
],
},
],
}

View file

@ -9,7 +9,426 @@
<meta name="supported-color-schemes" content="light dark" />
<title></title>
<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>
<!--[if mso]>
<style type="text/css">

View file

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

View file

@ -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) => {

View file

@ -22,7 +22,7 @@
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<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>
</tr>
</table>
@ -38,7 +38,7 @@
<tr>
<td>
<p class="f-fallback sub">If youre 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>
</tr>
</table>

View file

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

View file

@ -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_API_KEY: process.env.INTERNAL_API_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) {

View file

@ -7,6 +7,7 @@ const { getSettingsTemplateContext } = require("./templates")
const { processString } = require("@budibase/string-templates")
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
const TEST_MODE = false
const GLOBAL_DB = StaticDatabases.GLOBAL.name
const TYPE = TemplateTypes.EMAIL
@ -14,18 +15,32 @@ const FULL_EMAIL_PURPOSES = [
EmailTemplatePurpose.INVITATION,
EmailTemplatePurpose.PASSWORD_RECOVERY,
EmailTemplatePurpose.WELCOME,
EmailTemplatePurpose.CUSTOM,
]
function createSMTPTransport(config) {
const options = {
port: config.port,
host: config.host,
secure: config.secure || false,
auth: config.auth,
}
if (config.selfSigned) {
options.tls = {
rejectUnauthorized: false,
let options
if (!TEST_MODE) {
options = {
port: config.port,
host: config.host,
secure: config.secure || false,
auth: config.auth,
}
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)
@ -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).
* @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 {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.
*/
async function buildEmail(purpose, email, user) {
async function buildEmail(purpose, email, context, { user, contents } = {}) {
// this isn't a full email
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
throw `Unable to build an email of type ${purpose}`
}
let [base, styles, body] = await Promise.all([
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
const code = await getLinkCode(purpose, email, user)
const context = {
...(await getSettingsTemplateContext(purpose, code)),
context = {
...context,
contents,
email,
user: 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,24 +128,38 @@ 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.
* @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
* 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 config = await getSmtpConfiguration(db, groupId)
if (!config) {
let config = (await getSmtpConfiguration(db, groupId)) || {}
if (Object.keys(config).length === 0 && !TEST_MODE) {
throw "Unable to find SMTP configuration."
}
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 = {
from: config.from,
subject: config.subject,
from: from || config.from,
subject: await processString(subject || config.subject, context),
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
}
/**

View file

@ -14,8 +14,8 @@ const BASE_COMPANY = "Budibase"
exports.getSettingsTemplateContext = async (purpose, code = null) => {
const db = new CouchDB(StaticDatabases.GLOBAL.name)
// TODO: use more granular settings in the future if required
const settings = await getScopedConfig(db, { type: Configs.SETTINGS })
if (!settings.platformUrl) {
let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {}
if (!settings || !settings.platformUrl) {
settings.platformUrl = LOCAL_URL
}
const URL = settings.platformUrl
@ -40,7 +40,7 @@ exports.getSettingsTemplateContext = async (purpose, code = null) => {
break
case EmailTemplatePurpose.INVITATION:
context[TemplateBindings.INVITE_CODE] = code
context[TemplateBindings.REGISTRATION_URL] = checkSlashesInUrl(
context[TemplateBindings.INVITE_URL] = checkSlashesInUrl(
`${URL}/invite?code=${code}`
)
break