Merge branch 'develop' of github.com:Budibase/budibase into ak-fixes

This commit is contained in:
Andrew Kingston 2021-10-04 15:56:37 +01:00
commit 5c6752a132
51 changed files with 341 additions and 81 deletions

View File

@ -89,6 +89,8 @@ spec:
value: {{ .Values.globals.selfHosted | quote }} value: {{ .Values.globals.selfHosted | quote }}
- name: ACCOUNT_PORTAL_URL - name: ACCOUNT_PORTAL_URL
value: {{ .Values.globals.accountPortalUrl | quote }} value: {{ .Values.globals.accountPortalUrl | quote }}
- name: ACCOUNT_PORTAL_API_KEY
value: {{ .Values.globals.accountPortalApiKey | quote }}
- name: COOKIE_DOMAIN - name: COOKIE_DOMAIN
value: {{ .Values.globals.cookieDomain | quote }} value: {{ .Values.globals.cookieDomain | quote }}
image: budibase/worker image: budibase/worker

View File

@ -90,6 +90,7 @@ globals:
logLevel: info logLevel: info
selfHosted: 1 selfHosted: 1
accountPortalUrL: "" accountPortalUrL: ""
accountPortalApiKey: ""
cookieDomain: "" cookieDomain: ""
createSecrets: true # creates an internal API key, JWT secrets and redis password for you createSecrets: true # creates an internal API key, JWT secrets and redis password for you

View File

@ -50,6 +50,11 @@ static_resources:
route: route:
cluster: app-service cluster: app-service
- match: { path: "/api/deploy" }
route:
timeout: 60s
cluster: app-service
# special case for when API requests are made, can just forward, not to minio # special case for when API requests are made, can just forward, not to minio
- match: { prefix: "/api/" } - match: { prefix: "/api/" }
route: route:

View File

@ -1,5 +1,5 @@
{ {
"version": "0.9.148-alpha.7", "version": "0.9.150-alpha.0",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -50,6 +50,8 @@
"multi:disable": "lerna run multi:disable", "multi:disable": "lerna run multi:disable",
"selfhost:enable": "lerna run selfhost:enable", "selfhost:enable": "lerna run selfhost:enable",
"selfhost:disable": "lerna run selfhost:disable", "selfhost:disable": "lerna run selfhost:disable",
"localdomain:enable": "lerna run localdomain:enable",
"localdomain:disable": "lerna run localdomain:disable",
"postinstall": "husky install" "postinstall": "husky install"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/auth", "name": "@budibase/auth",
"version": "0.9.148-alpha.7", "version": "0.9.150-alpha.0",
"description": "Authentication middlewares for budibase builder and apps", "description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -1,16 +1,18 @@
const API = require("./api") const API = require("./api")
const env = require("../environment") const env = require("../environment")
const { Headers } = require("../constants")
const api = new API(env.ACCOUNT_PORTAL_URL) const api = new API(env.ACCOUNT_PORTAL_URL)
// TODO: Authorization
exports.getAccount = async email => { exports.getAccount = async email => {
const payload = { const payload = {
email, email,
} }
const response = await api.post(`/api/accounts/search`, { const response = await api.post(`/api/accounts/search`, {
body: payload, body: payload,
headers: {
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY,
},
}) })
const json = await response.json() const json = await response.json()

View File

@ -21,6 +21,7 @@ module.exports = {
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
MULTI_TENANCY: process.env.MULTI_TENANCY, MULTI_TENANCY: process.env.MULTI_TENANCY,
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL, ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED), SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,

View File

@ -7,6 +7,7 @@ exports.buildMatcherRegex = patterns => {
return patterns.map(pattern => { return patterns.map(pattern => {
const isObj = typeof pattern === "object" && pattern.route const isObj = typeof pattern === "object" && pattern.route
const method = isObj ? pattern.method : "GET" const method = isObj ? pattern.method : "GET"
const strict = pattern.strict ? pattern.strict : false
let route = isObj ? pattern.route : pattern let route = isObj ? pattern.route : pattern
const matches = route.match(PARAM_REGEX) const matches = route.match(PARAM_REGEX)
@ -16,13 +17,19 @@ exports.buildMatcherRegex = patterns => {
route = route.replace(match, pattern) route = route.replace(match, pattern)
} }
} }
return { regex: new RegExp(route), method } return { regex: new RegExp(route), method, strict, route }
}) })
} }
exports.matches = (ctx, options) => { exports.matches = (ctx, options) => {
return options.find(({ regex, method }) => { return options.find(({ regex, method, strict, route }) => {
const urlMatch = regex.test(ctx.request.url) let urlMatch
if (strict) {
urlMatch = ctx.request.url === route
} else {
urlMatch = regex.test(ctx.request.url)
}
const methodMatch = const methodMatch =
method === "ALL" method === "ALL"
? true ? true

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "0.9.148-alpha.7", "version": "0.9.150-alpha.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.9.148-alpha.7", "version": "0.9.150-alpha.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -65,10 +65,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.148-alpha.7", "@budibase/bbui": "^0.9.150-alpha.0",
"@budibase/client": "^0.9.148-alpha.7", "@budibase/client": "^0.9.150-alpha.0",
"@budibase/colorpicker": "1.1.2", "@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.148-alpha.7", "@budibase/string-templates": "^0.9.150-alpha.0",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -1,23 +1,15 @@
<script> <script>
import { Input, Icon, notifications } from "@budibase/bbui" import { Input, Icon, notifications } from "@budibase/bbui"
import { store, hostingStore } from "builderStore"
export let value export let value
export let production = false
$: appId = $store.appId
$: appUrl = $hostingStore.appUrl
function fullWebhookURL(uri) { function fullWebhookURL(uri) {
if (!uri) { if (!uri) {
return "" return ""
} }
if (production) {
return `${appUrl}/${uri}`
} else {
return `${window.location.origin}/${uri}` return `${window.location.origin}/${uri}`
} }
}
function copyToClipboard() { function copyToClipboard() {
const dummy = document.createElement("textarea") const dummy = document.createElement("textarea")

View File

@ -75,7 +75,7 @@
}} }}
> >
<Layout noPadding> <Layout noPadding>
<Body size="XS" <Body size="S"
>All apps need data. You can connect to a data source below, or add data >All apps need data. You can connect to a data source below, or add data
to your app using Budibase's built-in database. to your app using Budibase's built-in database.
</Body> </Body>

View File

@ -5,6 +5,7 @@
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui" import { Input, Select, ModalContent, Toggle } from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates" import getTemplates from "builderStore/store/screenTemplates"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
const CONTAINER = "@budibase/standard-components/container" const CONTAINER = "@budibase/standard-components/container"
@ -84,7 +85,7 @@
if (!event.detail.startsWith("/")) { if (!event.detail.startsWith("/")) {
route = "/" + event.detail route = "/" + event.detail
} }
route = route.replace(/ +/g, "-") route = sanitizeUrl(route)
} }
</script> </script>

View File

@ -7,6 +7,7 @@
import RoleSelect from "./PropertyControls/RoleSelect.svelte" import RoleSelect from "./PropertyControls/RoleSelect.svelte"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { FrontendTypes } from "constants" import { FrontendTypes } from "constants"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
export let componentInstance export let componentInstance
export let bindings export let bindings
@ -37,7 +38,12 @@
key: "routing.route", key: "routing.route",
label: "Route", label: "Route",
control: Input, control: Input,
parser: val => val.replace(/ +/g, "-"), parser: val => {
if (!val.startsWith("/")) {
val = "/" + val
}
return sanitizeUrl(val)
},
}, },
{ key: "routing.roleId", label: "Access", control: RoleSelect }, { key: "routing.roleId", label: "Access", control: RoleSelect },
{ key: "layoutId", label: "Layout", control: LayoutSelect }, { key: "layoutId", label: "Layout", control: LayoutSelect },

View File

@ -1,10 +1,11 @@
<script> <script>
import { Modal, ModalContent, Button } from "@budibase/bbui" import { Modal, ModalContent, Button } from "@budibase/bbui"
import { admin } from "stores/portal"
let upgradeModal let upgradeModal
const onConfirm = () => { const onConfirm = () => {
window.open("https://account.budibase.app/portal/install", "_blank") window.open(`${$admin.accountPortalUrl}/portal/install`, "_blank")
} }
</script> </script>
@ -25,8 +26,8 @@
confirmText="Self-host Budibase" confirmText="Self-host Budibase"
> >
<span> <span>
Self-host Budibase for free, and get SSO, unlimited apps, and more - and Self-host budibase for free to get unlimited apps and more - and it only
it only takes a few minutes! takes a few minutes!
</span> </span>
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -14,16 +14,30 @@
$: useAccountPortal = cloud && !$admin.disableAccountPortal $: useAccountPortal = cloud && !$admin.disableAccountPortal
const validateTenantId = async () => { const validateTenantId = async () => {
// set the tenant from the url in the cloud const host = window.location.host
const tenantId = window.location.host.split(".")[0] if (host.includes("localhost:")) {
// ignore local dev
return
}
if (!tenantId.includes("localhost:")) { if (user && user.tenantId) {
// user doesn't have permission to access this tenant - kick them out let urlTenantId
if (user && user.tenantId !== tenantId) { const hostParts = host.split(".")
await auth.logout()
await auth.setOrganisation(null) // only run validation when we know we are in a tenant url
// not when we visit the root budibase.app domain
// e.g. ['tenant', 'budibase', 'app'] vs ['budibase', 'app']
if (hostParts.length > 2) {
urlTenantId = hostParts[0]
} else { } else {
await auth.setOrganisation(tenantId) // no tenant in the url - send to account portal to fix this
window.location.href = $admin.accountPortalUrl
return
}
if (user.tenantId !== urlTenantId) {
// user should not be here - play it safe and log them out
await auth.logout()
} }
} }
} }
@ -32,7 +46,7 @@
await auth.checkAuth() await auth.checkAuth()
await admin.init() await admin.init()
if (cloud && multiTenancyEnabled) { if (useAccountPortal && multiTenancyEnabled) {
await validateTenantId() await validateTenantId()
} }

View File

@ -92,7 +92,7 @@
<ActionGroup /> <ActionGroup />
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
{#if $admin.cloud} {#if $admin.cloud && $auth.user.account}
<UpgradeModal /> <UpgradeModal />
{/if} {/if}
<VersionModal /> <VersionModal />

View File

@ -156,6 +156,8 @@
...relateTo, ...relateTo,
through: through._id, through: through._id,
fieldName: fromTable.primary[0], fieldName: fromTable.primary[0],
throughFrom: relateFrom.throughTo,
throughTo: relateFrom.throughFrom,
} }
} else { } else {
// the relateFrom.fieldName should remain the same, as it is the foreignKey in the other // the relateFrom.fieldName should remain the same, as it is the foreignKey in the other
@ -251,6 +253,22 @@
bind:error={errors.through} bind:error={errors.through}
bind:value={fromRelationship.through} bind:value={fromRelationship.through}
/> />
{#if fromTable && toTable && through}
<Select
label={`Foreign Key (${fromTable?.name})`}
options={Object.keys(through?.schema)}
on:change={() => ($touched.fromForeign = true)}
bind:error={errors.fromForeign}
bind:value={fromRelationship.throughTo}
/>
<Select
label={`Foreign Key (${toTable?.name})`}
options={Object.keys(through?.schema)}
on:change={() => ($touched.toForeign = true)}
bind:error={errors.toForeign}
bind:value={fromRelationship.throughFrom}
/>
{/if}
{:else if fromRelationship?.relationshipType && toTable} {:else if fromRelationship?.relationshipType && toTable}
<Select <Select
label={`Foreign Key (${toTable?.name})`} label={`Foreign Key (${toTable?.name})`}

View File

@ -327,6 +327,13 @@
gap: 10px; gap: 10px;
} }
@media only screen and (max-width: 560px) {
.title {
flex-direction: column;
align-items: flex-start;
}
}
.select { .select {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;

View File

@ -52,11 +52,11 @@
async function deleteUser() { async function deleteUser() {
const res = await users.delete(userId) const res = await users.delete(userId)
if (res.message) { if (res.status === 200) {
notifications.success(`User ${$userFetch?.data?.email} deleted.`) notifications.success(`User ${$userFetch?.data?.email} deleted.`)
$goto("./") $goto("./")
} else { } else {
notifications.error("Failed to delete user.") notifications.error(res?.message ? res.message : "Failed to delete user.")
} }
} }

View File

@ -55,7 +55,11 @@ export function createUsersStore() {
async function del(id) { async function del(id) {
const response = await api.delete(`/api/global/users/${id}`) const response = await api.delete(`/api/global/users/${id}`)
update(users => users.filter(user => user._id !== id)) update(users => users.filter(user => user._id !== id))
return await response.json() const json = await response.json()
return {
...json,
status: response.status,
}
} }
async function save(data) { async function save(data) {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "0.9.148-alpha.7", "version": "0.9.150-alpha.0",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "0.9.148-alpha.7", "version": "0.9.150-alpha.0",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.148-alpha.7", "@budibase/bbui": "^0.9.150-alpha.0",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^0.9.148-alpha.7", "@budibase/string-templates": "^0.9.150-alpha.0",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "0.9.148-alpha.7", "version": "0.9.150-alpha.0",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -27,7 +27,9 @@
"multi:enable": "node scripts/multiTenancy.js enable", "multi:enable": "node scripts/multiTenancy.js enable",
"multi:disable": "node scripts/multiTenancy.js disable", "multi:disable": "node scripts/multiTenancy.js disable",
"selfhost:enable": "node scripts/selfhost.js enable", "selfhost:enable": "node scripts/selfhost.js enable",
"selfhost:disable": "node scripts/selfhost.js disable" "selfhost:disable": "node scripts/selfhost.js disable",
"localdomain:enable": "node scripts/localdomain.js enable",
"localdomain:disable": "node scripts/localdomain.js disable"
}, },
"jest": { "jest": {
"preset": "ts-jest", "preset": "ts-jest",
@ -64,9 +66,9 @@
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "^0.9.148-alpha.7", "@budibase/auth": "^0.9.150-alpha.0",
"@budibase/client": "^0.9.148-alpha.7", "@budibase/client": "^0.9.150-alpha.0",
"@budibase/string-templates": "^0.9.148-alpha.7", "@budibase/string-templates": "^0.9.150-alpha.0",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0", "@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1", "@sendgrid/mail": "7.1.1",

View File

@ -0,0 +1,28 @@
version: "3.8"
services:
db:
container_name: postgres
image: postgres
restart: always
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: root
POSTGRES_DB: main
ports:
- "5432:5432"
volumes:
#- pg_data:/var/lib/postgresql/data/
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
pgadmin:
container_name: pgadmin-pg
image: dpage/pgadmin4
restart: always
environment:
PGADMIN_DEFAULT_EMAIL: root@root.com
PGADMIN_DEFAULT_PASSWORD: root
ports:
- "5050:80"
#volumes:
# pg_data:

View File

@ -0,0 +1,41 @@
SELECT 'CREATE DATABASE main'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec
CREATE TABLE categories
(
name text COLLATE pg_catalog."default",
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
CONSTRAINT categories_pkey PRIMARY KEY (id)
);
CREATE TABLE customers
(
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
name text COLLATE pg_catalog."default",
email text COLLATE pg_catalog."default",
age integer,
"dateOfBirth" date,
CONSTRAINT customers_pkey PRIMARY KEY (id)
);
CREATE TABLE customer_category
(
customer_id integer,
category_id integer,
notes text COLLATE pg_catalog."default",
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
CONSTRAINT "Category" FOREIGN KEY (category_id)
REFERENCES public.categories (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID,
CONSTRAINT "Customer" FOREIGN KEY (customer_id)
REFERENCES public.customers (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID
);
INSERT INTO customers (name, email, age) VALUES ('Mike', 'mike@mike.com', 30);
INSERT INTO categories (name) VALUES ('Books');

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker-compose down
docker volume prune -f

View File

@ -0,0 +1,22 @@
#!/usr/bin/env node
const updateDotEnv = require("update-dotenv")
const arg = process.argv.slice(2)[0]
/**
* For testing multi tenancy sub domains locally.
*
* Relies on an entry in /etc/hosts e.g:
*
* 127.0.0.1 local.com
*
* and an entry for each tenant you wish to test locally e.g:
*
* 127.0.0.1 t1.local.com
* 127.0.0.1 t2.local.com
*/
updateDotEnv({
ACCOUNT_PORTAL_URL:
arg === "enable" ? "http://local.com:10001" : "http://localhost:10001",
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
}).then(() => console.log("Updated worker!"))

View File

@ -7,14 +7,19 @@ if (env.POSTHOG_TOKEN && env.ENABLE_ANALYTICS && !env.SELF_HOSTED) {
posthogClient = new PostHog(env.POSTHOG_TOKEN) posthogClient = new PostHog(env.POSTHOG_TOKEN)
} }
exports.isEnabled = async function (ctx) { exports.isEnabled = async ctx => {
ctx.body = { ctx.body = {
enabled: !env.SELF_HOSTED && env.ENABLE_ANALYTICS === "true", enabled: !env.SELF_HOSTED && env.ENABLE_ANALYTICS === "true",
} }
} }
exports.endUserPing = async (ctx, next) => { exports.endUserPing = async ctx => {
if (!posthogClient) return next() if (!posthogClient) {
ctx.body = {
ping: false,
}
return
}
posthogClient.capture("budibase:end_user_ping", { posthogClient.capture("budibase:end_user_ping", {
userId: ctx.user && ctx.user._id, userId: ctx.user && ctx.user._id,

View File

@ -82,7 +82,7 @@ async function getAppUrlIfNotInUse(ctx) {
if (!env.SELF_HOSTED) { if (!env.SELF_HOSTED) {
return url return url
} }
const deployedApps = await getDeployedApps(ctx) const deployedApps = await getDeployedApps()
if ( if (
url && url &&
deployedApps[url] != null && deployedApps[url] != null &&

View File

@ -64,6 +64,7 @@ async function storeDeploymentHistory(deployment) {
async function initDeployedApp(prodAppId) { async function initDeployedApp(prodAppId) {
const db = new CouchDB(prodAppId) const db = new CouchDB(prodAppId)
console.log("Reading automation docs")
const automations = ( const automations = (
await db.allDocs( await db.allDocs(
getAutomationParams(null, { getAutomationParams(null, {
@ -71,12 +72,17 @@ async function initDeployedApp(prodAppId) {
}) })
) )
).rows.map(row => row.doc) ).rows.map(row => row.doc)
console.log("You have " + automations.length + " automations")
const promises = [] const promises = []
console.log("Disabling prod crons..")
await disableAllCrons(prodAppId) await disableAllCrons(prodAppId)
console.log("Prod Cron triggers disabled..")
console.log("Enabling cron triggers for deployed app..")
for (let automation of automations) { for (let automation of automations) {
promises.push(enableCronTrigger(prodAppId, automation)) promises.push(enableCronTrigger(prodAppId, automation))
} }
await Promise.all(promises) await Promise.all(promises)
console.log("Enabled cron triggers for deployed app..")
} }
async function deployApp(deployment) { async function deployApp(deployment) {
@ -88,13 +94,18 @@ async function deployApp(deployment) {
target: productionAppId, target: productionAppId,
}) })
console.log("Replication object created")
await replication.replicate() await replication.replicate()
console.log("replication complete.. replacing app meta doc")
const db = new CouchDB(productionAppId) const db = new CouchDB(productionAppId)
const appDoc = await db.get(DocumentTypes.APP_METADATA) const appDoc = await db.get(DocumentTypes.APP_METADATA)
appDoc.appId = productionAppId appDoc.appId = productionAppId
appDoc.instance._id = productionAppId appDoc.instance._id = productionAppId
await db.put(appDoc) await db.put(appDoc)
console.log("New app doc written successfully.")
console.log("Setting up live repl between dev and prod")
// Set up live sync between the live and dev instances // Set up live sync between the live and dev instances
const liveReplication = new Replication({ const liveReplication = new Replication({
source: productionAppId, source: productionAppId,
@ -105,8 +116,11 @@ async function deployApp(deployment) {
return doc._id !== DocumentTypes.APP_METADATA return doc._id !== DocumentTypes.APP_METADATA
}, },
}) })
console.log("Set up live repl between dev and prod")
console.log("Initialising deployed app")
await initDeployedApp(productionAppId) await initDeployedApp(productionAppId)
console.log("Init complete, setting deployment to successful")
deployment.setStatus(DeploymentStatus.SUCCESS) deployment.setStatus(DeploymentStatus.SUCCESS)
await storeDeploymentHistory(deployment) await storeDeploymentHistory(deployment)
} catch (err) { } catch (err) {
@ -153,9 +167,13 @@ exports.deploymentProgress = async function (ctx) {
exports.deployApp = async function (ctx) { exports.deployApp = async function (ctx) {
let deployment = new Deployment(ctx.appId) let deployment = new Deployment(ctx.appId)
console.log("Deployment object created")
deployment.setStatus(DeploymentStatus.PENDING) deployment.setStatus(DeploymentStatus.PENDING)
console.log("Deployment object set to pending")
deployment = await storeDeploymentHistory(deployment) deployment = await storeDeploymentHistory(deployment)
console.log("Stored deployment history")
console.log("Deploying app...")
await deployApp(deployment) await deployApp(deployment)
ctx.body = deployment ctx.body = deployment

View File

@ -18,5 +18,5 @@ exports.fetchUrls = async ctx => {
} }
exports.getDeployedApps = async ctx => { exports.getDeployedApps = async ctx => {
ctx.body = await getDeployedApps(ctx) ctx.body = await getDeployedApps()
} }

View File

@ -205,9 +205,13 @@ module External {
} else { } else {
// we're not inserting a doc, will be a bunch of update calls // we're not inserting a doc, will be a bunch of update calls
const isUpdate = !field.through const isUpdate = !field.through
const thisKey: string = isUpdate ? "id" : linkTablePrimary const thisKey: string = isUpdate
? "id"
: field.throughTo || linkTablePrimary
// @ts-ignore // @ts-ignore
const otherKey: string = isUpdate ? field.fieldName : tablePrimary const otherKey: string = isUpdate
? field.fieldName
: field.throughFrom || tablePrimary
row[key].map((relationship: any) => { row[key].map((relationship: any) => {
// we don't really support composite keys for relationships, this is why [0] is used // we don't really support composite keys for relationships, this is why [0] is used
manyRelationships.push({ manyRelationships.push({
@ -328,12 +332,11 @@ module External {
if (!table.primary || !linkTable.primary) { if (!table.primary || !linkTable.primary) {
continue continue
} }
const definition = { const definition: any = {
// if no foreign key specified then use the name of the field in other table // if no foreign key specified then use the name of the field in other table
from: field.foreignKey || table.primary[0], from: field.foreignKey || table.primary[0],
to: field.fieldName, to: field.fieldName,
tableName: linkTableName, tableName: linkTableName,
through: undefined,
// need to specify where to put this back into // need to specify where to put this back into
column: fieldName, column: fieldName,
} }
@ -343,8 +346,10 @@ module External {
) )
definition.through = throughTableName definition.through = throughTableName
// don't support composite keys for relationships // don't support composite keys for relationships
definition.from = table.primary[0] definition.from = field.throughFrom || table.primary[0]
definition.to = linkTable.primary[0] definition.to = field.throughTo || linkTable.primary[0]
definition.fromPrimary = table.primary[0]
definition.toPrimary = linkTable.primary[0]
} }
relationships.push(definition) relationships.push(definition)
} }
@ -369,7 +374,8 @@ module External {
} }
const isMany = field.relationshipType === RelationshipTypes.MANY_TO_MANY const isMany = field.relationshipType === RelationshipTypes.MANY_TO_MANY
const tableId = isMany ? field.through : field.tableId const tableId = isMany ? field.through : field.tableId
const fieldName = isMany ? primaryKey : field.fieldName const manyKey = field.throughFrom || primaryKey
const fieldName = isMany ? manyKey : field.fieldName
const response = await makeExternalQuery(this.appId, { const response = await makeExternalQuery(this.appId, {
endpoint: getEndpoint(tableId, DataSourceOperation.READ), endpoint: getEndpoint(tableId, DataSourceOperation.READ),
filters: { filters: {

View File

@ -40,7 +40,7 @@ async function prepareUpload({ s3Key, bucket, metadata, file }) {
async function checkForSelfHostedURL(ctx) { async function checkForSelfHostedURL(ctx) {
// the "appId" component of the URL may actually be a specific self hosted URL // the "appId" component of the URL may actually be a specific self hosted URL
let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}` let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}`
const apps = await getDeployedApps(ctx) const apps = await getDeployedApps()
if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) { if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) {
return apps[possibleAppUrl].appId return apps[possibleAppUrl].appId
} else { } else {

View File

@ -3,7 +3,8 @@ const controller = require("../controllers/analytics")
const router = Router() const router = Router()
router.get("/api/analytics", controller.isEnabled) router
router.post("/api/analytics/ping", controller.endUserPing) .get("/api/analytics", controller.isEnabled)
.post("/api/analytics/ping", controller.endUserPing)
module.exports = router module.exports = router

View File

@ -15,6 +15,8 @@ export interface FieldSchema {
through?: string through?: string
foreignKey?: string foreignKey?: string
autocolumn?: boolean autocolumn?: boolean
throughFrom?: string
throughTo?: string
constraints?: { constraints?: {
type?: string type?: string
email?: boolean email?: boolean

View File

@ -121,6 +121,8 @@ export interface RelationshipsJson {
through?: string through?: string
from?: string from?: string
to?: string to?: string
fromPrimary?: string
toPrimary?: string
tableName: string tableName: string
column: string column: string
} }

View File

@ -112,14 +112,16 @@ function addRelationships(
) )
} else { } else {
const throughTable = relationship.through const throughTable = relationship.through
const fromPrimary = relationship.fromPrimary
const toPrimary = relationship.toPrimary
query = query query = query
// @ts-ignore // @ts-ignore
.leftJoin( .leftJoin(
throughTable, throughTable,
`${fromTable}.${from}`, `${fromTable}.${fromPrimary}`,
`${throughTable}.${from}` `${throughTable}.${from}`
) )
.leftJoin(toTable, `${toTable}.${to}`, `${throughTable}.${to}`) .leftJoin(toTable, `${toTable}.${toPrimary}`, `${throughTable}.${to}`)
} }
} }
return query return query

View File

@ -99,6 +99,7 @@ function processAutoColumn(
row, row,
opts = { reprocessing: false, noAutoRelationships: false } opts = { reprocessing: false, noAutoRelationships: false }
) { ) {
let noUser = !user || !user.userId
let now = new Date().toISOString() let now = new Date().toISOString()
// if a row doesn't have a revision then it doesn't exist yet // if a row doesn't have a revision then it doesn't exist yet
const creating = !row._rev const creating = !row._rev
@ -108,7 +109,12 @@ function processAutoColumn(
} }
switch (schema.subtype) { switch (schema.subtype) {
case AutoFieldSubTypes.CREATED_BY: case AutoFieldSubTypes.CREATED_BY:
if (creating && !opts.reprocessing && !opts.noAutoRelationships) { if (
creating &&
!opts.reprocessing &&
!opts.noAutoRelationships &&
!noUser
) {
row[key] = [user.userId] row[key] = [user.userId]
} }
break break
@ -118,7 +124,7 @@ function processAutoColumn(
} }
break break
case AutoFieldSubTypes.UPDATED_BY: case AutoFieldSubTypes.UPDATED_BY:
if (!opts.reprocessing && !opts.noAutoRelationships) { if (!opts.reprocessing && !opts.noAutoRelationships && !noUser) {
row[key] = [user.userId] row[key] = [user.userId]
} }
break break

View File

@ -52,16 +52,17 @@ exports.sendSmtpEmail = async (to, from, subject, contents, automation) => {
) )
if (response.status !== 200) { if (response.status !== 200) {
throw "Unable to send email." const error = await response.text()
throw `Unable to send email - ${error}`
} }
return response.json() return response.json()
} }
exports.getDeployedApps = async ctx => { exports.getDeployedApps = async () => {
try { try {
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/apps`), checkSlashesInUrl(env.WORKER_URL + `/api/apps`),
request(ctx, { request(null, {
method: "GET", method: "GET",
}) })
) )

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "0.9.148-alpha.7", "version": "0.9.150-alpha.0",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "0.9.148-alpha.7", "version": "0.9.150-alpha.0",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -20,13 +20,15 @@
"multi:enable": "node scripts/multiTenancy.js enable", "multi:enable": "node scripts/multiTenancy.js enable",
"multi:disable": "node scripts/multiTenancy.js disable", "multi:disable": "node scripts/multiTenancy.js disable",
"selfhost:enable": "node scripts/selfhost.js enable", "selfhost:enable": "node scripts/selfhost.js enable",
"selfhost:disable": "node scripts/selfhost.js disable" "selfhost:disable": "node scripts/selfhost.js disable",
"localdomain:enable": "node scripts/localdomain.js enable",
"localdomain:disable": "node scripts/localdomain.js disable"
}, },
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "^0.9.148-alpha.7", "@budibase/auth": "^0.9.150-alpha.0",
"@budibase/string-templates": "^0.9.148-alpha.7", "@budibase/string-templates": "^0.9.150-alpha.0",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.811.0", "aws-sdk": "^2.811.0",

View File

@ -23,6 +23,8 @@ async function init() {
MULTI_TENANCY: "", MULTI_TENANCY: "",
DISABLE_ACCOUNT_PORTAL: "", DISABLE_ACCOUNT_PORTAL: "",
ACCOUNT_PORTAL_URL: "http://localhost:10001", ACCOUNT_PORTAL_URL: "http://localhost:10001",
ACCOUNT_PORTAL_API_KEY: "budibase",
PLATFORM_URL: "http://localhost:10000",
} }
let envFile = "" let envFile = ""
Object.keys(envFileJson).forEach(key => { Object.keys(envFileJson).forEach(key => {

View File

@ -0,0 +1,24 @@
#!/usr/bin/env node
const updateDotEnv = require("update-dotenv")
const arg = process.argv.slice(2)[0]
/**
* For testing multi tenancy sub domains locally.
*
* Relies on an entry in /etc/hosts e.g:
*
* 127.0.0.1 local.com
*
* and an entry for each tenant you wish to test locally e.g:
*
* 127.0.0.1 t1.local.com
* 127.0.0.1 t2.local.com
*/
updateDotEnv({
ACCOUNT_PORTAL_URL:
arg === "enable" ? "http://local.com:10001" : "http://localhost:10001",
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
PLATFORM_URL:
arg === "enable" ? "http://local.com:10000" : "http://localhost:10000",
}).then(() => console.log("Updated worker!"))

View File

@ -205,6 +205,18 @@ exports.adminUser = async ctx => {
exports.destroy = async ctx => { exports.destroy = async ctx => {
const db = getGlobalDB() const db = getGlobalDB()
const dbUser = await db.get(ctx.params.id) const dbUser = await db.get(ctx.params.id)
// root account holder can't be deleted from inside budibase
const email = dbUser.email
const account = await accounts.getAccount(email)
if (account) {
if (email === ctx.user.email) {
ctx.throw(400, 'Please visit "Account" to delete this user')
} else {
ctx.throw(400, "Account holder cannot be deleted")
}
}
await removeUserFromInfoDB(dbUser) await removeUserFromInfoDB(dbUser)
await db.remove(dbUser._id, dbUser._rev) await db.remove(dbUser._id, dbUser._rev)
await userCache.invalidateUser(dbUser._id) await userCache.invalidateUser(dbUser._id)

View File

@ -87,7 +87,7 @@ router
if (ctx.publicEndpoint) { if (ctx.publicEndpoint) {
return next() return next()
} }
if (!ctx.isAuthenticated || !ctx.user.budibaseAccess) { if ((!ctx.isAuthenticated || !ctx.user.budibaseAccess) && !ctx.internal) {
ctx.throw(403, "Unauthorized - no public worker access") ctx.throw(403, "Unauthorized - no public worker access")
} }
return next() return next()

View File

@ -3,6 +3,7 @@ const controller = require("../../controllers/global/users")
const joiValidator = require("../../../middleware/joi-validator") const joiValidator = require("../../../middleware/joi-validator")
const adminOnly = require("../../../middleware/adminOnly") const adminOnly = require("../../../middleware/adminOnly")
const Joi = require("joi") const Joi = require("joi")
const cloudRestricted = require("../../../middleware/cloudRestricted")
const router = Router() const router = Router()
@ -90,6 +91,7 @@ router
) )
.post( .post(
"/api/global/users/init", "/api/global/users/init",
cloudRestricted,
buildAdminInitValidation(), buildAdminInitValidation(),
controller.adminUser controller.adminUser
) )

View File

@ -40,6 +40,7 @@ module.exports = {
SMTP_HOST: process.env.SMTP_HOST, SMTP_HOST: process.env.SMTP_HOST,
SMTP_PORT: process.env.SMTP_PORT, SMTP_PORT: process.env.SMTP_PORT,
SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS, SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS,
PLATFORM_URL: process.env.PLATFORM_URL,
_set(key, value) { _set(key, value) {
process.env[key] = value process.env[key] = value
module.exports[key] = value module.exports[key] = value

View File

@ -0,0 +1,17 @@
const env = require("../environment")
const { Headers } = require("@budibase/auth").constants
/**
* This is a restricted endpoint in the cloud.
* Ensure that the correct API key has been supplied.
*/
module.exports = async (ctx, next) => {
if (!env.SELF_HOSTED) {
const apiKey = ctx.request.headers[Headers.API_KEY]
if (apiKey !== env.INTERNAL_API_KEY) {
ctx.throw(403, "Unauthorized")
}
}
return next()
}

View File

@ -8,8 +8,6 @@ const {
const { checkSlashesInUrl } = require("./index") const { checkSlashesInUrl } = require("./index")
const env = require("../environment") const env = require("../environment")
const { getGlobalDB, addTenantToUrl } = require("@budibase/auth/tenancy") const { getGlobalDB, addTenantToUrl } = require("@budibase/auth/tenancy")
const LOCAL_URL = `http://localhost:${env.CLUSTER_PORT || 10000}`
const BASE_COMPANY = "Budibase" const BASE_COMPANY = "Budibase"
exports.getSettingsTemplateContext = async (purpose, code = null) => { exports.getSettingsTemplateContext = async (purpose, code = null) => {
@ -17,7 +15,7 @@ exports.getSettingsTemplateContext = async (purpose, code = null) => {
// TODO: use more granular settings in the future if required // TODO: use more granular settings in the future if required
let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {} let settings = (await getScopedConfig(db, { type: Configs.SETTINGS })) || {}
if (!settings || !settings.platformUrl) { if (!settings || !settings.platformUrl) {
settings.platformUrl = LOCAL_URL settings.platformUrl = env.PLATFORM_URL
} }
const URL = settings.platformUrl const URL = settings.platformUrl
const context = { const context = {