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

This commit is contained in:
Andrew Kingston 2021-05-25 08:21:38 +01:00
commit 6477b2d2ed
33 changed files with 307 additions and 145 deletions

View File

@ -105,6 +105,8 @@ services:
restart: always restart: always
image: redis image: redis
command: redis-server --requirepass ${REDIS_PASSWORD} command: redis-server --requirepass ${REDIS_PASSWORD}
ports:
- "${REDIS_PORT}:6379"
volumes: volumes:
- redis_data:/data - redis_data:/data

View File

@ -1,5 +1,5 @@
{ {
"version": "0.9.1", "version": "0.9.3",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/auth", "name": "@budibase/auth",
"version": "0.9.1", "version": "0.9.3",
"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

@ -34,11 +34,15 @@ function sanitizeKey(input) {
return sanitize(sanitizeBucket(input)).replace(/\\/g, "/") return sanitize(sanitizeBucket(input)).replace(/\\/g, "/")
} }
exports.sanitizeKey = sanitizeKey
// simply handles the dev app to app conversion // simply handles the dev app to app conversion
function sanitizeBucket(input) { function sanitizeBucket(input) {
return input.replace(new RegExp(APP_DEV_PREFIX, "g"), APP_PREFIX) return input.replace(new RegExp(APP_DEV_PREFIX, "g"), APP_PREFIX)
} }
exports.sanitizeBucket = sanitizeBucket
function publicPolicy(bucketName) { function publicPolicy(bucketName) {
return { return {
Version: "2012-10-17", Version: "2012-10-17",

View File

@ -3,22 +3,51 @@ const env = require("../environment")
const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis")
const { addDbPrefix, removeDbPrefix, getRedisOptions } = require("./utils") const { addDbPrefix, removeDbPrefix, getRedisOptions } = require("./utils")
const RETRY_PERIOD_MS = 2000
const STARTUP_TIMEOUT_MS = 5000
const CLUSTERED = false const CLUSTERED = false
// for testing just generate the client once // for testing just generate the client once
let CONNECTED = false let CLOSED = false
let CLIENT = env.isTest() ? new Redis(getRedisOptions()) : null let CLIENT = env.isTest() ? new Redis(getRedisOptions()) : null
// if in test always connected
let CONNECTED = !!env.isTest()
function connectionError(timeout, err) {
// manually shut down, ignore errors
if (CLOSED) {
return
}
// always clear this on error
clearTimeout(timeout)
CONNECTED = false
console.error("Redis connection failed - " + err)
setTimeout(() => {
init()
}, RETRY_PERIOD_MS)
}
/** /**
* Inits the system, will error if unable to connect to redis cluster (may take up to 10 seconds) otherwise * Inits the system, will error if unable to connect to redis cluster (may take up to 10 seconds) otherwise
* will return the ioredis client which will be ready to use. * will return the ioredis client which will be ready to use.
* @return {Promise<object>} The ioredis client.
*/ */
function init() { function init() {
return new Promise((resolve, reject) => { let timeout
CLOSED = false
// testing uses a single in memory client // testing uses a single in memory client
if (env.isTest() || (CLIENT && CONNECTED)) { if (env.isTest() || (CLIENT && CONNECTED)) {
return resolve(CLIENT) return
}
// start the timer - only allowed 5 seconds to connect
timeout = setTimeout(() => {
if (!CONNECTED) {
connectionError(timeout)
}
}, STARTUP_TIMEOUT_MS)
// disconnect any lingering client
if (CLIENT) {
CLIENT.disconnect()
} }
const { opts, host, port } = getRedisOptions(CLUSTERED) const { opts, host, port } = getRedisOptions(CLUSTERED)
if (CLUSTERED) { if (CLUSTERED) {
@ -26,18 +55,34 @@ function init() {
} else { } else {
CLIENT = new Redis(opts) CLIENT = new Redis(opts)
} }
// attach handlers
CLIENT.on("end", err => { CLIENT.on("end", err => {
reject(err) connectionError(timeout, err)
CONNECTED = false
}) })
CLIENT.on("error", err => { CLIENT.on("error", err => {
reject(err) connectionError(timeout, err)
CONNECTED = false
}) })
CLIENT.on("connect", () => { CLIENT.on("connect", () => {
resolve(CLIENT) clearTimeout(timeout)
CONNECTED = true CONNECTED = true
}) })
}
function waitForConnection() {
return new Promise(resolve => {
if (CLIENT == null) {
init()
} else if (CONNECTED) {
resolve()
return
}
// check if the connection is ready
const interval = setInterval(() => {
if (CONNECTED) {
clearInterval(interval)
resolve()
}
}, 500)
}) })
} }
@ -85,31 +130,32 @@ class RedisWrapper {
} }
async init() { async init() {
this._client = await init() CLOSED = false
init()
await waitForConnection()
return this return this
} }
async finish() { async finish() {
this._client.disconnect() CLOSED = true
CLIENT.disconnect()
} }
async scan() { async scan() {
const db = this._db, const db = this._db
client = this._client
let stream let stream
if (CLUSTERED) { if (CLUSTERED) {
let node = client.nodes("master") let node = CLIENT.nodes("master")
stream = node[0].scanStream({ match: db + "-*", count: 100 }) stream = node[0].scanStream({ match: db + "-*", count: 100 })
} else { } else {
stream = client.scanStream({ match: db + "-*", count: 100 }) stream = CLIENT.scanStream({ match: db + "-*", count: 100 })
} }
return promisifyStream(stream) return promisifyStream(stream)
} }
async get(key) { async get(key) {
const db = this._db, const db = this._db
client = this._client let response = await CLIENT.get(addDbPrefix(db, key))
let response = await client.get(addDbPrefix(db, key))
// overwrite the prefixed key // overwrite the prefixed key
if (response != null && response.key) { if (response != null && response.key) {
response.key = key response.key = key
@ -123,22 +169,20 @@ class RedisWrapper {
} }
async store(key, value, expirySeconds = null) { async store(key, value, expirySeconds = null) {
const db = this._db, const db = this._db
client = this._client
if (typeof value === "object") { if (typeof value === "object") {
value = JSON.stringify(value) value = JSON.stringify(value)
} }
const prefixedKey = addDbPrefix(db, key) const prefixedKey = addDbPrefix(db, key)
await client.set(prefixedKey, value) await CLIENT.set(prefixedKey, value)
if (expirySeconds) { if (expirySeconds) {
await client.expire(prefixedKey, expirySeconds) await CLIENT.expire(prefixedKey, expirySeconds)
} }
} }
async delete(key) { async delete(key) {
const db = this._db, const db = this._db
client = this._client await CLIENT.del(addDbPrefix(db, key))
await client.del(addDbPrefix(db, key))
} }
async clear() { async clear() {

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.1", "version": "0.9.3",
"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

@ -11,12 +11,23 @@ Cypress.Commands.add("login", () => {
if (cookie) return if (cookie) return
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.contains("Create Test User").click()
// cy.get("button").then(btn => {
// const adminUserButton = "Create super admin user"
// console.log(btn.first().first())
// if (!btn.first().contains(adminUserButton)) {
// // create admin user
// cy.get("input").first().type("test@test.com")
// cy.get('input[type="password"]').first().type("test")
// cy.get('input[type="password"]').eq(1).type("test")
// cy.contains(adminUserButton).click()
// }
// login
cy.get("input").first().type("test@test.com") cy.get("input").first().type("test@test.com")
cy.get('input[type="password"]').type("test") cy.get('input[type="password"]').type("test")
cy.contains("Login").click() cy.contains("Login").click()
// })
}) })
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.9.1", "version": "0.9.3",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -65,10 +65,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.1", "@budibase/bbui": "^0.9.3",
"@budibase/client": "^0.9.1", "@budibase/client": "^0.9.3",
"@budibase/colorpicker": "1.1.2", "@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.1", "@budibase/string-templates": "^0.9.3",
"@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

@ -10,13 +10,13 @@
</script> </script>
{#if show} {#if show}
<ActionButton> <ActionButton
<a target="_blank" href="/api/admin/auth/google"> on:click={() => window.open("/api/admin/auth/google", "_blank")}
>
<div class="inner"> <div class="inner">
<img src={GoogleLogo} alt="google icon" /> <img src={GoogleLogo} alt="google icon" />
<p>Sign in with Google</p> <p>Sign in with Google</p>
</div> </div>
</a>
</ActionButton> </ActionButton>
{/if} {/if}

View File

@ -5,6 +5,8 @@
Select, Select,
ModalContent, ModalContent,
notifications, notifications,
Toggle,
Label,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation" import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal" import { users } from "stores/portal"
@ -13,12 +15,12 @@
const options = ["Email onboarding", "Basic onboarding"] const options = ["Email onboarding", "Basic onboarding"]
let selected = options[0] let selected = options[0]
let builder, admin
const [email, error, touched] = createValidationStore("", emailValidator) const [email, error, touched] = createValidationStore("", emailValidator)
async function createUserFlow() { async function createUserFlow() {
const res = await users.invite($email) const res = await users.invite({ email: $email, builder, admin })
console.log(res)
if (res.status) { if (res.status) {
notifications.error(res.message) notifications.error(res.message)
} else { } else {
@ -56,4 +58,23 @@
placeholder="john@doe.com" placeholder="john@doe.com"
label="Email" label="Email"
/> />
<div>
<div class="toggle">
<Label size="L">Development access</Label>
<Toggle text="" bind:value={builder} />
</div>
<div class="toggle">
<Label size="L">Administration access</Label>
<Toggle text="" bind:value={admin} />
</div>
</div>
</ModalContent> </ModalContent>
<style>
.toggle {
display: grid;
grid-template-columns: 78% 1fr;
align-items: center;
width: 50%;
}
</style>

View File

@ -1,13 +1,22 @@
<script> <script>
import { ModalContent, Body, Input, notifications } from "@budibase/bbui" import {
ModalContent,
Body,
Input,
notifications,
Toggle,
Label,
} from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation" import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal" import { users } from "stores/portal"
const [email, error, touched] = createValidationStore("", emailValidator) const [email, error, touched] = createValidationStore("", emailValidator)
const password = Math.random().toString(36).substr(2, 20) const password = Math.random().toString(36).substr(2, 20)
let builder = false,
admin = false
async function createUser() { async function createUser() {
const res = await users.create({ email: $email, password }) const res = await users.create({ email: $email, password, builder, admin })
if (res.status) { if (res.status) {
notifications.error(res.message) notifications.error(res.message)
} else { } else {
@ -37,4 +46,23 @@
error={$touched && $error} error={$touched && $error}
/> />
<Input disabled label="Password" value={password} /> <Input disabled label="Password" value={password} />
<div>
<div class="toggle">
<Label size="L">Development access</Label>
<Toggle text="" bind:value={builder} />
</div>
<div class="toggle">
<Label size="L">Administration access</Label>
<Toggle text="" bind:value={admin} />
</div>
</div>
</ModalContent> </ModalContent>
<style>
.toggle {
display: grid;
grid-template-columns: 78% 1fr;
align-items: center;
width: 50%;
}
</style>

View File

@ -11,10 +11,22 @@ export function createUsersStore() {
set(json) set(json)
} }
async function invite(email) { async function invite({ email, builder, admin }) {
const response = await api.post(`/api/admin/users/invite`, { email }) const body = { email, userInfo: {} }
if (admin) {
body.userInfo.admin = {
global: true,
}
}
if (builder) {
body.userInfo.builder = {
global: true,
}
}
const response = await api.post(`/api/admin/users/invite`, body)
return await response.json() return await response.json()
} }
async function acceptInvite(inviteCode, password) { async function acceptInvite(inviteCode, password) {
const response = await api.post("/api/admin/users/invite/accept", { const response = await api.post("/api/admin/users/invite/accept", {
inviteCode, inviteCode,
@ -23,14 +35,20 @@ export function createUsersStore() {
return await response.json() return await response.json()
} }
async function create({ email, password }) { async function create({ email, password, admin, builder }) {
const response = await api.post("/api/admin/users", { const body = {
email, email,
password, password,
builder: { global: true },
roles: {}, roles: {},
}) }
init() if (builder) {
body.builder = { global: true }
}
if (admin) {
body.admin = { global: true }
}
const response = await api.post("/api/admin/users", body)
await init()
return await response.json() return await response.json()
} }
@ -43,8 +61,7 @@ export function createUsersStore() {
async function save(data) { async function save(data) {
try { try {
const res = await post(`/api/admin/users`, data) const res = await post(`/api/admin/users`, data)
const json = await res.json() return await res.json()
return json
} catch (error) { } catch (error) {
console.log(error) console.log(error)
return error return error

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "0.9.1", "version": "0.9.3",
"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

@ -101,10 +101,15 @@ async function init(type) {
async function start() { async function start() {
await checkDockerConfigured() await checkDockerConfigured()
checkInitComplete() checkInitComplete()
console.log(info("Starting services, this may take a moment.")) console.log(
info(
"Starting services, this may take a moment - first time this may take a few minutes to download images."
)
)
const port = makeEnv.get("MAIN_PORT") const port = makeEnv.get("MAIN_PORT")
await handleError(async () => { await handleError(async () => {
await compose.upAll({ cwd: "./", log: false }) // need to log as it makes it more clear
await compose.upAll({ cwd: "./", log: true })
}) })
console.log( console.log(
success( success(

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "0.9.1", "version": "0.9.3",
"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",
@ -18,13 +18,13 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/string-templates": "^0.9.1", "@budibase/string-templates": "^0.9.3",
"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"
}, },
"devDependencies": { "devDependencies": {
"@budibase/standard-components": "^0.9.1", "@budibase/standard-components": "^0.9.3",
"@rollup/plugin-commonjs": "^18.0.0", "@rollup/plugin-commonjs": "^18.0.0",
"@rollup/plugin-node-resolve": "^11.2.1", "@rollup/plugin-node-resolve": "^11.2.1",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",

View File

@ -1,36 +1,13 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "0.9.1", "version": "0.9.3",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/electron.js", "main": "src/electron.js",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/Budibase/budibase.git" "url": "https://github.com/Budibase/budibase.git"
}, },
"build": {
"icon": "./build/icons/512x512.png",
"appId": "com.budibase.builder",
"productName": "Budibase Builder",
"afterSign": "electron-builder-notarize",
"mac": {
"icon": "./assets/icons/icon.icns",
"category": "public.app-category.developer-tools",
"hardenedRuntime": true
},
"linux": {
"maintainer": "Budibase",
"icon": "./build/icons/",
"target": [
"deb",
"AppImage"
],
"category": "Development"
},
"extraMetadata": {
"name": "Budibase Builder"
}
},
"scripts": { "scripts": {
"test": "jest --testPathIgnorePatterns=routes && yarn run test:integration", "test": "jest --testPathIgnorePatterns=routes && yarn run test:integration",
"test:integration": "jest --coverage --detectOpenHandles", "test:integration": "jest --coverage --detectOpenHandles",
@ -41,9 +18,6 @@
"dev:stack:down": "node scripts/dev/manage.js down", "dev:stack:down": "node scripts/dev/manage.js down",
"dev:stack:nuke": "node scripts/dev/manage.js nuke", "dev:stack:nuke": "node scripts/dev/manage.js nuke",
"dev:builder": "yarn run dev:stack:up && nodemon src/index.js", "dev:builder": "yarn run dev:stack:up && nodemon src/index.js",
"electron": "electron src/electron.js",
"build:electron": "electron-builder --dir",
"publish:electron": "electron-builder -mwl --publish always",
"lint": "eslint --fix src/", "lint": "eslint --fix src/",
"initialise": "node scripts/initialise.js" "initialise": "node scripts/initialise.js"
}, },
@ -81,9 +55,9 @@
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "^0.9.1", "@budibase/auth": "^0.9.3",
"@budibase/client": "^0.9.1", "@budibase/client": "^0.9.3",
"@budibase/string-templates": "^0.9.1", "@budibase/string-templates": "^0.9.3",
"@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",
@ -133,7 +107,7 @@
"zlib": "1.0.5" "zlib": "1.0.5"
}, },
"devDependencies": { "devDependencies": {
"@budibase/standard-components": "^0.9.1", "@budibase/standard-components": "^0.9.3",
"@jest/test-sequencer": "^24.8.0", "@jest/test-sequencer": "^24.8.0",
"docker-compose": "^0.23.6", "docker-compose": "^0.23.6",
"eslint": "^6.8.0", "eslint": "^6.8.0",

View File

@ -23,7 +23,16 @@ async function redirect(ctx, method) {
if (cookie) { if (cookie) {
ctx.set("set-cookie", cookie) ctx.set("set-cookie", cookie)
} }
let body
try {
body = await response.json()
} catch (err) {
// don't worry about errors, likely no JSON
}
ctx.status = response.status ctx.status = response.status
if (body) {
ctx.body = body
}
ctx.cookies ctx.cookies
} }

View File

@ -384,7 +384,7 @@ describe("/rows", () => {
name: "test", name: "test",
description: "test", description: "test",
attachment: [{ attachment: [{
key: `/assets/${config.getAppId()}/attachment/test/thing.csv`, key: `${config.getAppId()}/attachment/test/thing.csv`,
}], }],
tableId: table._id, tableId: table._id,
}) })
@ -392,7 +392,7 @@ describe("/rows", () => {
await setup.switchToSelfHosted(async () => { await setup.switchToSelfHosted(async () => {
const enriched = await outputProcessing(config.getAppId(), table, [row]) const enriched = await outputProcessing(config.getAppId(), table, [row])
expect(enriched[0].attachment[0].url).toBe( expect(enriched[0].attachment[0].url).toBe(
`/prod-budi-app-assets/assets/${config.getAppId()}/attachment/test/thing.csv` `/prod-budi-app-assets/${config.getAppId()}/attachment/test/thing.csv`
) )
}) })
}) })

View File

@ -73,10 +73,11 @@ if (env.isProd()) {
const server = http.createServer(app.callback()) const server = http.createServer(app.callback())
destroyable(server) destroyable(server)
server.on("close", () => { server.on("close", async () => {
if (env.NODE_ENV !== "jest") { if (env.NODE_ENV !== "jest") {
console.log("Server Closed") console.log("Server Closed")
} }
await redis.shutdown()
}) })
module.exports = server.listen(env.PORT || 0, async () => { module.exports = server.listen(env.PORT || 0, async () => {

View File

@ -101,7 +101,7 @@ exports.AutoFieldSubTypes = {
AUTO_ID: "autoID", AUTO_ID: "autoID",
} }
exports.OBJ_STORE_DIRECTORY = "/app-assets/assets" exports.OBJ_STORE_DIRECTORY = "/prod-budi-app-assets"
exports.BaseQueryVerbs = { exports.BaseQueryVerbs = {
CREATE: "create", CREATE: "create",
READ: "read", READ: "read",

View File

@ -13,7 +13,7 @@ const {
deleteFolder, deleteFolder,
downloadTarball, downloadTarball,
} = require("./utilities") } = require("./utilities")
const { downloadLibraries, newAppPublicPath } = require("./newApp") const { downloadLibraries, uploadClientLibrary } = require("./newApp")
const download = require("download") const download = require("download")
const env = require("../../environment") const env = require("../../environment")
const { homedir } = require("os") const { homedir } = require("os")
@ -134,7 +134,7 @@ exports.performBackup = async (appId, backupName) => {
*/ */
exports.createApp = async appId => { exports.createApp = async appId => {
await downloadLibraries(appId) await downloadLibraries(appId)
await newAppPublicPath(appId) await uploadClientLibrary(appId)
} }
/** /**

View File

@ -26,10 +26,9 @@ exports.downloadLibraries = async appId => {
return paths return paths
} }
exports.newAppPublicPath = async appId => { exports.uploadClientLibrary = async appId => {
const path = join(appId, "public")
const sourcepath = require.resolve("@budibase/client") const sourcepath = require.resolve("@budibase/client")
const destPath = join(path, "budibase-client.js") const destPath = join(appId, "budibase-client.js")
await streamUpload(BUCKET_NAME, destPath, fs.createReadStream(sourcepath)) await streamUpload(BUCKET_NAME, destPath, fs.createReadStream(sourcepath))
} }

View File

@ -1,6 +1,7 @@
const env = require("../environment") const env = require("../environment")
const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants") const { OBJ_STORE_DIRECTORY, ObjectStoreBuckets } = require("../constants")
const { getAllApps } = require("@budibase/auth/db") const { getAllApps } = require("@budibase/auth/db")
const { sanitizeKey } = require("@budibase/auth/src/objectStore")
const BB_CDN = "https://cdn.app.budi.live/assets" const BB_CDN = "https://cdn.app.budi.live/assets"
@ -43,7 +44,9 @@ exports.objectStoreUrl = () => {
*/ */
exports.clientLibraryPath = appId => { exports.clientLibraryPath = appId => {
if (env.isProd()) { if (env.isProd()) {
return `${exports.objectStoreUrl()}/${appId}/budibase-client.js` return `${exports.objectStoreUrl()}/${sanitizeKey(
appId
)}/budibase-client.js`
} else { } else {
return `/api/assets/client` return `/api/assets/client`
} }

View File

@ -11,6 +11,11 @@ exports.init = async () => {
debounceClient = await new Client(utils.Databases.DEBOUNCE).init() debounceClient = await new Client(utils.Databases.DEBOUNCE).init()
} }
exports.shutdown = async () => {
await devAppClient.finish()
await debounceClient.finish()
}
exports.doesUserHaveLock = async (devAppId, user) => { exports.doesUserHaveLock = async (devAppId, user) => {
const value = await devAppClient.get(devAppId) const value = await devAppClient.get(devAppId)
if (!value) { if (!value) {

View File

@ -29,11 +29,11 @@
"keywords": [ "keywords": [
"svelte" "svelte"
], ],
"version": "0.9.1", "version": "0.9.3",
"license": "MIT", "license": "MIT",
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc", "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.1", "@budibase/bbui": "^0.9.3",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"apexcharts": "^3.22.1", "apexcharts": "^3.22.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "0.9.1", "version": "0.9.3",
"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.1", "version": "0.9.3",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -21,8 +21,8 @@
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "^0.9.1", "@budibase/auth": "^0.9.3",
"@budibase/string-templates": "^0.9.1", "@budibase/string-templates": "^0.9.3",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"aws-sdk": "^2.811.0", "aws-sdk": "^2.811.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",

View File

@ -6,10 +6,8 @@ const {
getGlobalUserParams, getGlobalUserParams,
getScopedFullConfig, getScopedFullConfig,
} = require("@budibase/auth").db } = require("@budibase/auth").db
const fetch = require("node-fetch")
const { Configs } = require("../../../constants") const { Configs } = require("../../../constants")
const email = require("../../../utilities/email") const email = require("../../../utilities/email")
const env = require("../../../environment")
const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore const { upload, ObjectStoreBuckets } = require("@budibase/auth").objectStore
const APP_PREFIX = "app_" const APP_PREFIX = "app_"
@ -155,12 +153,7 @@ exports.configChecklist = async function (ctx) {
// TODO: Watch get started video // TODO: Watch get started video
// Apps exist // Apps exist
let allDbs let allDbs = await CouchDB.allDbs()
if (env.COUCH_DB_URL) {
allDbs = await (await fetch(`${env.COUCH_DB_URL}/_all_dbs`)).json()
} else {
allDbs = await CouchDB.allDbs()
}
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX)) const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
// They have set up SMTP // They have set up SMTP

View File

@ -167,13 +167,14 @@ exports.find = async ctx => {
} }
exports.invite = async ctx => { exports.invite = async ctx => {
const { email } = ctx.request.body const { email, userInfo } = ctx.request.body
const existing = await getGlobalUserByEmail(email) const existing = await getGlobalUserByEmail(email)
if (existing) { if (existing) {
ctx.throw(400, "Email address already in use.") ctx.throw(400, "Email address already in use.")
} }
await sendEmail(email, EmailTemplatePurpose.INVITATION, { await sendEmail(email, EmailTemplatePurpose.INVITATION, {
subject: "{{ company }} platform invitation", subject: "{{ company }} platform invitation",
info: userInfo,
}) })
ctx.body = { ctx.body = {
message: "Invitation has been sent.", message: "Invitation has been sent.",
@ -183,13 +184,15 @@ exports.invite = async ctx => {
exports.inviteAccept = async ctx => { exports.inviteAccept = async ctx => {
const { inviteCode, password, firstName, lastName } = ctx.request.body const { inviteCode, password, firstName, lastName } = ctx.request.body
try { try {
const email = await checkInviteCode(inviteCode) // info is an extension of the user object that was stored by admin
const { email, info } = await checkInviteCode(inviteCode)
// only pass through certain props for accepting // only pass through certain props for accepting
ctx.request.body = { ctx.request.body = {
firstName, firstName,
lastName, lastName,
password, password,
email, email,
...info,
} }
// this will flesh out the body response // this will flesh out the body response
await exports.save(ctx) await exports.save(ctx)

View File

@ -6,6 +6,17 @@ const Joi = require("joi")
const router = Router() const router = Router()
function buildAdminInitValidation() {
return joiValidator.body(
Joi.object({
email: Joi.string().required(),
password: Joi.string().required(),
})
.required()
.unknown(false)
)
}
function buildUserSaveValidation(isSelf = false) { function buildUserSaveValidation(isSelf = false) {
let schema = { let schema = {
email: Joi.string().allow(null, ""), email: Joi.string().allow(null, ""),
@ -36,6 +47,7 @@ function buildInviteValidation() {
// prettier-ignore // prettier-ignore
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.object({
email: Joi.string().required(), email: Joi.string().required(),
userInfo: Joi.object().optional(),
}).required()) }).required())
} }
@ -74,7 +86,11 @@ router
buildInviteAcceptValidation(), buildInviteAcceptValidation(),
controller.inviteAccept controller.inviteAccept
) )
.post("/api/admin/users/init", controller.adminUser) .post(
"/api/admin/users/init",
buildAdminInitValidation(),
controller.adminUser
)
.get("/api/admin/users/self", controller.getSelf) .get("/api/admin/users/self", controller.getSelf)
// admin endpoint but needs to come at end (blocks other endpoints otherwise) // admin endpoint but needs to come at end (blocks other endpoints otherwise)
.get("/api/admin/users/:id", adminOnly, controller.find) .get("/api/admin/users/:id", adminOnly, controller.find)

View File

@ -9,6 +9,7 @@ const { passport } = require("@budibase/auth").auth
const logger = require("koa-pino-logger") const logger = require("koa-pino-logger")
const http = require("http") const http = require("http")
const api = require("./api") const api = require("./api")
const redis = require("./utilities/redis")
const app = new Koa() const app = new Koa()
@ -34,10 +35,16 @@ app.use(api.routes())
const server = http.createServer(app.callback()) const server = http.createServer(app.callback())
destroyable(server) destroyable(server)
server.on("close", () => console.log("Server Closed")) server.on("close", async () => {
if (env.isProd()) {
console.log("Server Closed")
}
await redis.shutdown()
})
module.exports = server.listen(parseInt(env.PORT || 4002), async () => { module.exports = server.listen(parseInt(env.PORT || 4002), async () => {
console.log(`Worker running on ${JSON.stringify(server.address())}`) console.log(`Worker running on ${JSON.stringify(server.address())}`)
await redis.init()
}) })
process.on("uncaughtException", err => { process.on("uncaughtException", err => {

View File

@ -46,12 +46,12 @@ function createSMTPTransport(config) {
return nodemailer.createTransport(options) return nodemailer.createTransport(options)
} }
async function getLinkCode(purpose, email, user) { async function getLinkCode(purpose, email, user, info = null) {
switch (purpose) { switch (purpose) {
case EmailTemplatePurpose.PASSWORD_RECOVERY: case EmailTemplatePurpose.PASSWORD_RECOVERY:
return getResetPasswordCode(user._id) return getResetPasswordCode(user._id)
case EmailTemplatePurpose.INVITATION: case EmailTemplatePurpose.INVITATION:
return getInviteCode(email) return getInviteCode(email, info)
default: default:
return null return null
} }
@ -136,13 +136,14 @@ exports.isEmailConfigured = async (groupId = null) => {
* @param {string|undefined} from If sending from an address that is not what is configured in the SMTP config. * @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} 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. * @param {string|undefined} subject A custom subject can be specified if the config one is not desired.
* @param {object|undefined} info Pass in a structure of information to be stored alongside the invitation.
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on * @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
* nodemailer response. * nodemailer response.
*/ */
exports.sendEmail = async ( exports.sendEmail = async (
email, email,
purpose, purpose,
{ groupId, user, from, contents, subject } = {} { groupId, user, from, contents, subject, info } = {}
) => { ) => {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
let config = (await getSmtpConfiguration(db, groupId)) || {} let config = (await getSmtpConfiguration(db, groupId)) || {}
@ -151,7 +152,7 @@ exports.sendEmail = async (
} }
const transport = createSMTPTransport(config) const transport = createSMTPTransport(config)
// if there is a link code needed this will retrieve it // if there is a link code needed this will retrieve it
const code = await getLinkCode(purpose, email, user) const code = await getLinkCode(purpose, email, user, info)
const context = await getSettingsTemplateContext(purpose, code) const context = await getSettingsTemplateContext(purpose, code)
const message = { const message = {
from: from || config.from, from: from || config.from,

View File

@ -12,15 +12,21 @@ function getExpirySecondsForDB(db) {
} }
} }
async function getClient(db) { let pwResetClient, invitationClient
return await new Client(db).init()
function getClient(db) {
switch (db) {
case utils.Databases.PW_RESETS:
return pwResetClient
case utils.Databases.INVITATIONS:
return invitationClient
}
} }
async function writeACode(db, value) { async function writeACode(db, value) {
const client = await getClient(db) const client = await getClient(db)
const code = newid() const code = newid()
await client.store(code, value, getExpirySecondsForDB(db)) await client.store(code, value, getExpirySecondsForDB(db))
client.finish()
return code return code
} }
@ -33,10 +39,22 @@ async function getACode(db, code, deleteCode = true) {
if (deleteCode) { if (deleteCode) {
await client.delete(code) await client.delete(code)
} }
client.finish()
return value return value
} }
exports.init = async () => {
pwResetClient = await new Client(utils.Databases.PW_RESETS).init()
invitationClient = await new Client(utils.Databases.PW_RESETS).init()
}
/**
* make sure redis connection is closed.
*/
exports.shutdown = async () => {
await pwResetClient.finish()
await invitationClient.finish()
}
/** /**
* Given a user ID this will store a code (that is returned) for an hour in redis. * Given a user ID this will store a code (that is returned) for an hour in redis.
* The user can then return this code for resetting their password (through their reset link). * The user can then return this code for resetting their password (through their reset link).
@ -64,17 +82,18 @@ exports.checkResetPasswordCode = async (resetCode, deleteCode = true) => {
/** /**
* Generates an invitation code and writes it to redis - which can later be checked for user creation. * Generates an invitation code and writes it to redis - which can later be checked for user creation.
* @param {string} email the email address which the code is being sent to (for use later). * @param {string} email the email address which the code is being sent to (for use later).
* @param {object|null} info Information to be carried along with the invitation.
* @return {Promise<string>} returns the code that was stored to redis. * @return {Promise<string>} returns the code that was stored to redis.
*/ */
exports.getInviteCode = async email => { exports.getInviteCode = async (email, info) => {
return writeACode(utils.Databases.INVITATIONS, email) return writeACode(utils.Databases.INVITATIONS, { email, info })
} }
/** /**
* Checks that the provided invite code is valid - will return the email address of user that was invited. * Checks that the provided invite code is valid - will return the email address of user that was invited.
* @param {string} inviteCode the invite code that was provided as part of the link. * @param {string} inviteCode the invite code that was provided as part of the link.
* @param {boolean} deleteCode whether or not the code should be deleted after retrieval - defaults to true. * @param {boolean} deleteCode whether or not the code should be deleted after retrieval - defaults to true.
* @return {Promise<string>} If the code is valid then an email address will be returned. * @return {Promise<object>} If the code is valid then an email address will be returned.
*/ */
exports.checkInviteCode = async (inviteCode, deleteCode = true) => { exports.checkInviteCode = async (inviteCode, deleteCode = true) => {
try { try {