forked from tsai/budibase
163 changed files with 1019 additions and 2457 deletions
@ -0,0 +1,14 @@ |
|||||
|
--- |
||||
|
name: Feature Request |
||||
|
about: Request a new budibase feature or enhancement |
||||
|
title: '' |
||||
|
labels: enhancement |
||||
|
assignees: '' |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Describe the feature request** |
||||
|
A clear and concise description of what the feature request. |
||||
|
|
||||
|
**Screenshots** |
||||
|
If applicable, add screenshots to help explain your problem. |
||||
@ -1,4 +1 @@ |
|||||
module.exports = { |
module.exports = require("./src/db/utils") |
||||
...require("./src/db/utils"), |
|
||||
...require("./src/db/constants"), |
|
||||
} |
|
||||
|
|||||
@ -1,17 +0,0 @@ |
|||||
exports.SEPARATOR = "_" |
|
||||
|
|
||||
exports.StaticDatabases = { |
|
||||
GLOBAL: { |
|
||||
name: "global-db", |
|
||||
docs: { |
|
||||
apiKeys: "apikeys", |
|
||||
}, |
|
||||
}, |
|
||||
// contains information about tenancy and so on
|
|
||||
PLATFORM_INFO: { |
|
||||
name: "global-info", |
|
||||
docs: { |
|
||||
tenants: "tenants", |
|
||||
}, |
|
||||
}, |
|
||||
} |
|
||||
@ -1,33 +0,0 @@ |
|||||
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g |
|
||||
|
|
||||
exports.buildMatcherRegex = patterns => { |
|
||||
if (!patterns) { |
|
||||
return [] |
|
||||
} |
|
||||
return patterns.map(pattern => { |
|
||||
const isObj = typeof pattern === "object" && pattern.route |
|
||||
const method = isObj ? pattern.method : "GET" |
|
||||
let route = isObj ? pattern.route : pattern |
|
||||
|
|
||||
const matches = route.match(PARAM_REGEX) |
|
||||
if (matches) { |
|
||||
for (let match of matches) { |
|
||||
const pattern = "/.*" + (match.endsWith("/") ? "/" : "") |
|
||||
route = route.replace(match, pattern) |
|
||||
} |
|
||||
} |
|
||||
return { regex: new RegExp(route), method } |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
exports.matches = (ctx, options) => { |
|
||||
return options.find(({ regex, method }) => { |
|
||||
const urlMatch = regex.test(ctx.request.url) |
|
||||
const methodMatch = |
|
||||
method === "ALL" |
|
||||
? true |
|
||||
: ctx.request.method.toLowerCase() === method.toLowerCase() |
|
||||
|
|
||||
return urlMatch && methodMatch |
|
||||
}) |
|
||||
} |
|
||||
@ -1,14 +0,0 @@ |
|||||
const { setTenantId } = require("../tenancy") |
|
||||
const ContextFactory = require("../tenancy/FunctionContext") |
|
||||
const { buildMatcherRegex, matches } = require("./matchers") |
|
||||
|
|
||||
module.exports = (allowQueryStringPatterns, noTenancyPatterns) => { |
|
||||
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) |
|
||||
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) |
|
||||
|
|
||||
return ContextFactory.getMiddleware(ctx => { |
|
||||
const allowNoTenant = !!matches(ctx, noTenancyOptions) |
|
||||
const allowQs = !!matches(ctx, allowQsOptions) |
|
||||
setTenantId(ctx, { allowQs, allowNoTenant }) |
|
||||
}) |
|
||||
} |
|
||||
@ -1,73 +0,0 @@ |
|||||
const cls = require("cls-hooked") |
|
||||
const { newid } = require("../hashing") |
|
||||
|
|
||||
const REQUEST_ID_KEY = "requestId" |
|
||||
|
|
||||
class FunctionContext { |
|
||||
static getMiddleware(updateCtxFn = null) { |
|
||||
const namespace = this.createNamespace() |
|
||||
|
|
||||
return async function (ctx, next) { |
|
||||
await new Promise( |
|
||||
namespace.bind(function (resolve, reject) { |
|
||||
// store a contextual request ID that can be used anywhere (audit logs)
|
|
||||
namespace.set(REQUEST_ID_KEY, newid()) |
|
||||
namespace.bindEmitter(ctx.req) |
|
||||
namespace.bindEmitter(ctx.res) |
|
||||
|
|
||||
if (updateCtxFn) { |
|
||||
updateCtxFn(ctx) |
|
||||
} |
|
||||
next().then(resolve).catch(reject) |
|
||||
}) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static run(callback) { |
|
||||
const namespace = this.createNamespace() |
|
||||
|
|
||||
return namespace.runAndReturn(callback) |
|
||||
} |
|
||||
|
|
||||
static setOnContext(key, value) { |
|
||||
const namespace = this.createNamespace() |
|
||||
namespace.set(key, value) |
|
||||
} |
|
||||
|
|
||||
static getContextStorage() { |
|
||||
if (this._namespace && this._namespace.active) { |
|
||||
let contextData = this._namespace.active |
|
||||
delete contextData.id |
|
||||
delete contextData._ns_name |
|
||||
return contextData |
|
||||
} |
|
||||
|
|
||||
return {} |
|
||||
} |
|
||||
|
|
||||
static getFromContext(key) { |
|
||||
const context = this.getContextStorage() |
|
||||
if (context) { |
|
||||
return context[key] |
|
||||
} else { |
|
||||
return null |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static destroyNamespace() { |
|
||||
if (this._namespace) { |
|
||||
cls.destroyNamespace("session") |
|
||||
this._namespace = null |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static createNamespace() { |
|
||||
if (!this._namespace) { |
|
||||
this._namespace = cls.createNamespace("session") |
|
||||
} |
|
||||
return this._namespace |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
module.exports = FunctionContext |
|
||||
@ -1,81 +0,0 @@ |
|||||
const env = require("../environment") |
|
||||
const { Headers } = require("../../constants") |
|
||||
const cls = require("./FunctionContext") |
|
||||
|
|
||||
exports.DEFAULT_TENANT_ID = "default" |
|
||||
|
|
||||
exports.isDefaultTenant = () => { |
|
||||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID |
|
||||
} |
|
||||
|
|
||||
exports.isMultiTenant = () => { |
|
||||
return env.MULTI_TENANCY |
|
||||
} |
|
||||
|
|
||||
const TENANT_ID = "tenantId" |
|
||||
|
|
||||
// used for automations, API endpoints should always be in context already
|
|
||||
exports.doInTenant = (tenantId, task) => { |
|
||||
return cls.run(() => { |
|
||||
// set the tenant id
|
|
||||
cls.setOnContext(TENANT_ID, tenantId) |
|
||||
|
|
||||
// invoke the task
|
|
||||
const result = task() |
|
||||
|
|
||||
return result |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
exports.updateTenantId = tenantId => { |
|
||||
cls.setOnContext(TENANT_ID, tenantId) |
|
||||
} |
|
||||
|
|
||||
exports.setTenantId = ( |
|
||||
ctx, |
|
||||
opts = { allowQs: false, allowNoTenant: false } |
|
||||
) => { |
|
||||
let tenantId |
|
||||
// exit early if not multi-tenant
|
|
||||
if (!exports.isMultiTenant()) { |
|
||||
cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
const allowQs = opts && opts.allowQs |
|
||||
const allowNoTenant = opts && opts.allowNoTenant |
|
||||
const header = ctx.request.headers[Headers.TENANT_ID] |
|
||||
const user = ctx.user || {} |
|
||||
if (allowQs) { |
|
||||
const query = ctx.request.query || {} |
|
||||
tenantId = query.tenantId |
|
||||
} |
|
||||
// override query string (if allowed) by user, or header
|
|
||||
// URL params cannot be used in a middleware, as they are
|
|
||||
// processed later in the chain
|
|
||||
tenantId = user.tenantId || header || tenantId |
|
||||
|
|
||||
if (!tenantId && !allowNoTenant) { |
|
||||
ctx.throw(403, "Tenant id not set") |
|
||||
} |
|
||||
// check tenant ID just incase no tenant was allowed
|
|
||||
if (tenantId) { |
|
||||
cls.setOnContext(TENANT_ID, tenantId) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
exports.isTenantIdSet = () => { |
|
||||
const tenantId = cls.getFromContext(TENANT_ID) |
|
||||
return !!tenantId |
|
||||
} |
|
||||
|
|
||||
exports.getTenantId = () => { |
|
||||
if (!exports.isMultiTenant()) { |
|
||||
return exports.DEFAULT_TENANT_ID |
|
||||
} |
|
||||
const tenantId = cls.getFromContext(TENANT_ID) |
|
||||
if (!tenantId) { |
|
||||
throw Error("Tenant id not found") |
|
||||
} |
|
||||
return tenantId |
|
||||
} |
|
||||
@ -1,4 +0,0 @@ |
|||||
module.exports = { |
|
||||
...require("./context"), |
|
||||
...require("./tenancy"), |
|
||||
} |
|
||||
@ -1,105 +0,0 @@ |
|||||
const { getDB } = require("../db") |
|
||||
const { SEPARATOR, StaticDatabases } = require("../db/constants") |
|
||||
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("./context") |
|
||||
const env = require("../environment") |
|
||||
|
|
||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants |
|
||||
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name |
|
||||
|
|
||||
exports.addTenantToUrl = url => { |
|
||||
const tenantId = getTenantId() |
|
||||
|
|
||||
if (isMultiTenant()) { |
|
||||
const char = url.indexOf("?") === -1 ? "?" : "&" |
|
||||
url += `${char}tenantId=${tenantId}` |
|
||||
} |
|
||||
|
|
||||
return url |
|
||||
} |
|
||||
|
|
||||
exports.doesTenantExist = async tenantId => { |
|
||||
const db = getDB(PLATFORM_INFO_DB) |
|
||||
let tenants |
|
||||
try { |
|
||||
tenants = await db.get(TENANT_DOC) |
|
||||
} catch (err) { |
|
||||
// if theres an error the doc doesn't exist, no tenants exist
|
|
||||
return false |
|
||||
} |
|
||||
return ( |
|
||||
tenants && |
|
||||
Array.isArray(tenants.tenantIds) && |
|
||||
tenants.tenantIds.indexOf(tenantId) !== -1 |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
exports.tryAddTenant = async (tenantId, userId, email) => { |
|
||||
const db = getDB(PLATFORM_INFO_DB) |
|
||||
const getDoc = async id => { |
|
||||
if (!id) { |
|
||||
return null |
|
||||
} |
|
||||
try { |
|
||||
return await db.get(id) |
|
||||
} catch (err) { |
|
||||
return { _id: id } |
|
||||
} |
|
||||
} |
|
||||
let [tenants, userIdDoc, emailDoc] = await Promise.all([ |
|
||||
getDoc(TENANT_DOC), |
|
||||
getDoc(userId), |
|
||||
getDoc(email), |
|
||||
]) |
|
||||
if (!Array.isArray(tenants.tenantIds)) { |
|
||||
tenants = { |
|
||||
_id: TENANT_DOC, |
|
||||
tenantIds: [], |
|
||||
} |
|
||||
} |
|
||||
let promises = [] |
|
||||
if (userIdDoc) { |
|
||||
userIdDoc.tenantId = tenantId |
|
||||
promises.push(db.put(userIdDoc)) |
|
||||
} |
|
||||
if (emailDoc) { |
|
||||
emailDoc.tenantId = tenantId |
|
||||
promises.push(db.put(emailDoc)) |
|
||||
} |
|
||||
if (tenants.tenantIds.indexOf(tenantId) === -1) { |
|
||||
tenants.tenantIds.push(tenantId) |
|
||||
promises.push(db.put(tenants)) |
|
||||
} |
|
||||
await Promise.all(promises) |
|
||||
} |
|
||||
|
|
||||
exports.getGlobalDB = (tenantId = null) => { |
|
||||
// tenant ID can be set externally, for example user API where
|
|
||||
// new tenants are being created, this may be the case
|
|
||||
if (!tenantId) { |
|
||||
tenantId = getTenantId() |
|
||||
} |
|
||||
|
|
||||
let dbName |
|
||||
|
|
||||
if (tenantId === DEFAULT_TENANT_ID) { |
|
||||
dbName = StaticDatabases.GLOBAL.name |
|
||||
} else { |
|
||||
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` |
|
||||
} |
|
||||
|
|
||||
return getDB(dbName) |
|
||||
} |
|
||||
|
|
||||
exports.lookupTenantId = async userId => { |
|
||||
const db = getDB(StaticDatabases.PLATFORM_INFO.name) |
|
||||
let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null |
|
||||
try { |
|
||||
const doc = await db.get(userId) |
|
||||
if (doc && doc.tenantId) { |
|
||||
tenantId = doc.tenantId |
|
||||
} |
|
||||
} catch (err) { |
|
||||
// just return the default
|
|
||||
} |
|
||||
return tenantId |
|
||||
} |
|
||||
@ -1 +0,0 @@ |
|||||
module.exports = require("./src/tenancy") |
|
||||
@ -1,24 +1,4 @@ |
|||||
<script> |
<script> |
||||
import { redirect } from "@roxi/routify" |
import { redirect } from "@roxi/routify" |
||||
import { auth, admin } from "stores/portal" |
$redirect("./login") |
||||
import { onMount } from "svelte" |
|
||||
|
|
||||
$: tenantSet = $auth.tenantSet |
|
||||
$: multiTenancyEnabled = $admin.multiTenancy |
|
||||
|
|
||||
let loaded = false |
|
||||
|
|
||||
$: { |
|
||||
if (loaded && multiTenancyEnabled && !tenantSet) { |
|
||||
$redirect("./org") |
|
||||
} else if (loaded) { |
|
||||
$redirect("./login") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onMount(async () => { |
|
||||
await admin.init() |
|
||||
await auth.checkQueryString() |
|
||||
loaded = true |
|
||||
}) |
|
||||
</script> |
</script> |
||||
|
|||||
@ -1,71 +0,0 @@ |
|||||
<script> |
|
||||
import { Button, Heading, Input, Layout } from "@budibase/bbui" |
|
||||
import { goto } from "@roxi/routify" |
|
||||
import { auth, admin } from "stores/portal" |
|
||||
import Logo from "assets/bb-emblem.svg" |
|
||||
import { get } from "svelte/store" |
|
||||
import { onMount } from "svelte" |
|
||||
|
|
||||
let tenantId = get(auth).tenantSet ? get(auth).tenantId : "" |
|
||||
$: multiTenancyEnabled = $admin.multiTenancy |
|
||||
|
|
||||
async function setOrg() { |
|
||||
if (tenantId == null || tenantId === "") { |
|
||||
tenantId = "default" |
|
||||
} |
|
||||
await auth.setOrg(tenantId) |
|
||||
// re-init now org selected |
|
||||
await admin.init() |
|
||||
$goto("../") |
|
||||
} |
|
||||
|
|
||||
function handleKeydown(evt) { |
|
||||
if (evt.key === "Enter") setOrg() |
|
||||
} |
|
||||
|
|
||||
onMount(async () => { |
|
||||
await auth.checkQueryString() |
|
||||
if (!multiTenancyEnabled) { |
|
||||
$goto("../") |
|
||||
} else { |
|
||||
admin.unload() |
|
||||
} |
|
||||
}) |
|
||||
</script> |
|
||||
|
|
||||
<svelte:window on:keydown={handleKeydown} /> |
|
||||
<div class="login"> |
|
||||
<div class="main"> |
|
||||
<Layout> |
|
||||
<Layout noPadding justifyItems="center"> |
|
||||
<img alt="logo" src={Logo} /> |
|
||||
<Heading>Set Budibase organisation</Heading> |
|
||||
</Layout> |
|
||||
<Layout gap="XS" noPadding> |
|
||||
<Input label="Organisation" bind:value={tenantId} /> |
|
||||
</Layout> |
|
||||
<Layout gap="XS" noPadding> |
|
||||
<Button cta on:click={setOrg}>Set organisation</Button> |
|
||||
</Layout> |
|
||||
</Layout> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.login { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
} |
|
||||
|
|
||||
.main { |
|
||||
width: 300px; |
|
||||
} |
|
||||
|
|
||||
img { |
|
||||
width: 48px; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,11 +1,4 @@ |
|||||
<script> |
<script> |
||||
import { redirect } from "@roxi/routify" |
import { redirect } from "@roxi/routify" |
||||
import { auth } from "../stores/portal" |
$redirect("./builder") |
||||
import { onMount } from "svelte" |
|
||||
|
|
||||
auth.checkQueryString() |
|
||||
|
|
||||
onMount(() => { |
|
||||
$redirect(`./builder`) |
|
||||
}) |
|
||||
</script> |
</script> |
||||
|
|||||
@ -1,8 +0,0 @@ |
|||||
#!/usr/bin/env node
|
|
||||
const updateDotEnv = require("update-dotenv") |
|
||||
|
|
||||
const arg = process.argv.slice(2)[0] |
|
||||
|
|
||||
updateDotEnv({ |
|
||||
MULTI_TENANCY: arg === "enable" ? "1" : "", |
|
||||
}).then(() => console.log("Updated server!")) |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue