Browse Source

merge with master

pull/4023/head
Martin McKeaveney 5 years ago
parent
commit
df927e3feb
  1. 14
      .github/ISSUE_TEMPLATE/feature_request.md
  2. 2
      README.md
  3. 8
      hosting/envoy.dev.yaml.hbs
  4. 10
      hosting/envoy.yaml
  5. 2
      lerna.json
  6. 4
      package.json
  7. 5
      packages/auth/db.js
  8. 3
      packages/auth/package.json
  9. 14
      packages/auth/src/cache/user.js
  10. 5
      packages/auth/src/constants.js
  11. 17
      packages/auth/src/db/constants.js
  12. 95
      packages/auth/src/db/utils.js
  13. 6
      packages/auth/src/db/views.js
  14. 1
      packages/auth/src/environment.js
  15. 7
      packages/auth/src/index.js
  16. 59
      packages/auth/src/middleware/authenticated.js
  17. 2
      packages/auth/src/middleware/index.js
  18. 33
      packages/auth/src/middleware/matchers.js
  19. 10
      packages/auth/src/middleware/passport/google.js
  20. 18
      packages/auth/src/middleware/passport/local.js
  21. 10
      packages/auth/src/middleware/passport/tests/google.spec.js
  22. 24
      packages/auth/src/middleware/passport/third-party-common.js
  23. 14
      packages/auth/src/middleware/tenancy.js
  24. 7
      packages/auth/src/security/sessions.js
  25. 73
      packages/auth/src/tenancy/FunctionContext.js
  26. 81
      packages/auth/src/tenancy/context.js
  27. 4
      packages/auth/src/tenancy/index.js
  28. 105
      packages/auth/src/tenancy/tenancy.js
  29. 13
      packages/auth/src/utils.js
  30. 1
      packages/auth/tenancy.js
  31. 35
      packages/auth/yarn.lock
  32. 2
      packages/bbui/package.json
  33. 8
      packages/builder/package.json
  34. 2
      packages/builder/src/builderStore/store/frontend.js
  35. 4
      packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte
  36. 2
      packages/builder/src/components/design/AppPreview/AppThemeSelect.svelte
  37. 14
      packages/builder/src/components/design/AppPreview/iframeTemplate.js
  38. 2
      packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte
  39. 3
      packages/builder/src/components/design/PropertiesPanel/ConditionalUISection.svelte
  40. 2
      packages/builder/src/components/design/PropertiesPanel/DesignSection.svelte
  41. 30
      packages/builder/src/components/design/PropertiesPanel/PropertiesPanel.svelte
  42. 13
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte
  43. 16
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte
  44. 3
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte
  45. 3
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventPropertyControl.svelte
  46. 4
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/DeleteRow.svelte
  47. 9
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ExecuteQuery.svelte
  48. 7
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/NavigateTo.svelte
  49. 9
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte
  50. 2
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte
  51. 6
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/TriggerAutomation.svelte
  52. 4
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte
  53. 9
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/LuceneFilterBuilder.svelte
  54. 18
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte
  55. 3
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/URLSelect.svelte
  56. 2
      packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte
  57. 2
      packages/builder/src/components/design/PropertiesPanel/StyleSection.svelte
  58. 14
      packages/builder/src/pages/builder/_layout.svelte
  59. 28
      packages/builder/src/pages/builder/admin/index.svelte
  60. 10
      packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/CreateEditRelationship/CreateEditRelationship.svelte
  61. 8
      packages/builder/src/pages/builder/auth/_components/GoogleButton.svelte
  62. 2
      packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte
  63. 11
      packages/builder/src/pages/builder/auth/forgot.svelte
  64. 22
      packages/builder/src/pages/builder/auth/index.svelte
  65. 23
      packages/builder/src/pages/builder/auth/login.svelte
  66. 71
      packages/builder/src/pages/builder/auth/org.svelte
  67. 8
      packages/builder/src/pages/builder/index.svelte
  68. 114
      packages/builder/src/pages/builder/portal/manage/auth/index.svelte
  69. 15
      packages/builder/src/pages/builder/portal/manage/email/index.svelte
  70. 4
      packages/builder/src/pages/builder/portal/manage/users/[userId].svelte
  71. 2
      packages/builder/src/pages/builder/portal/settings/organisation.svelte
  72. 9
      packages/builder/src/pages/index.svelte
  73. 53
      packages/builder/src/stores/portal/admin.js
  74. 110
      packages/builder/src/stores/portal/auth.js
  75. 6
      packages/builder/src/stores/portal/email.js
  76. 10
      packages/builder/src/stores/portal/oidc.js
  77. 8
      packages/builder/src/stores/portal/organisation.js
  78. 12
      packages/builder/src/stores/portal/users.js
  79. 2
      packages/cli/package.json
  80. 8
      packages/client/package.json
  81. 4
      packages/client/src/api/auth.js
  82. 5
      packages/client/src/components/ClientApp.svelte
  83. 2
      packages/server/__mocks__/node-fetch.ts
  84. 17
      packages/server/package.json
  85. 41
      packages/server/scripts/dev/manage.js
  86. 11
      packages/server/scripts/integrations/postgres/init.sql
  87. 8
      packages/server/scripts/multiTenancy.js
  88. 30
      packages/server/src/api/controllers/apikeys.js
  89. 14
      packages/server/src/api/controllers/application.js
  90. 2
      packages/server/src/api/controllers/auth.js
  91. 23
      packages/server/src/api/controllers/deploy/index.js
  92. 3
      packages/server/src/api/controllers/dev.js
  93. 10
      packages/server/src/api/controllers/row/internal.js
  94. 2
      packages/server/src/api/controllers/user.js
  95. 12
      packages/server/src/api/index.js
  96. 2
      packages/server/src/api/routes/application.js
  97. 6
      packages/server/src/api/routes/dev.js
  98. 2
      packages/server/src/api/routes/tests/row.spec.js
  99. 5
      packages/server/src/api/routes/tests/utilities/TestFunctions.js
  100. 2
      packages/server/src/automations/steps/serverLog.js

14
.github/ISSUE_TEMPLATE/feature_request.md

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

2
README.md

@ -57,7 +57,7 @@
- **Open source and extensible.** Budibase is open-source - licensed as GPL v3. 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, 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).
- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, 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 component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).

8
hosting/envoy.dev.yaml.hbs

@ -26,18 +26,10 @@ static_resources:
cluster: couchdb-service
prefix_rewrite: "/"
- match: { prefix: "/api/system/" }
route:
cluster: worker-dev
- match: { prefix: "/api/admin/" }
route:
cluster: worker-dev
- match: { prefix: "/api/global/" }
route:
cluster: worker-dev
- match: { prefix: "/api/" }
route:
cluster: server-dev

10
hosting/envoy.yaml

@ -37,19 +37,11 @@ static_resources:
route:
cluster: app-service
# special cases for worker admin (deprecated), global and system API
- match: { prefix: "/api/global/" }
route:
cluster: worker-service
# special case for worker admin API
- match: { prefix: "/api/admin/" }
route:
cluster: worker-service
- match: { prefix: "/api/system/" }
route:
cluster: worker-service
- match: { path: "/" }
route:
cluster: app-service

2
lerna.json

@ -1,5 +1,5 @@
{
"version": "0.9.87-alpha.9",
"version": "0.9.95",
"npmClient": "yarn",
"packages": [
"packages/*"

4
package.json

@ -43,8 +43,6 @@
"test:e2e": "lerna run cy:test",
"test:e2e:ci": "lerna run cy:ci",
"build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -",
"build:docker:develop": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"multi:enable": "lerna run multi:enable",
"multi:disable": "lerna run multi:disable"
"build:docker:develop": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -"
}
}

5
packages/auth/db.js

@ -1,4 +1 @@
module.exports = {
...require("./src/db/utils"),
...require("./src/db/constants"),
}
module.exports = require("./src/db/utils")

3
packages/auth/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/auth",
"version": "0.9.87-alpha.9",
"version": "0.9.95",
"description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js",
"author": "Budibase",
@ -13,7 +13,6 @@
"@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.901.0",
"bcryptjs": "^2.4.3",
"cls-hooked": "^4.2.2",
"ioredis": "^4.27.1",
"jsonwebtoken": "^8.5.1",
"koa-passport": "^4.1.4",

14
packages/auth/src/cache/user.js

@ -1,21 +1,15 @@
const { getDB } = require("../db")
const { StaticDatabases } = require("../db/utils")
const redis = require("../redis/authRedis")
const { getTenantId, lookupTenantId, getGlobalDB } = require("../tenancy")
const EXPIRY_SECONDS = 3600
exports.getUser = async (userId, tenantId = null) => {
if (!tenantId) {
try {
tenantId = getTenantId()
} catch (err) {
tenantId = await lookupTenantId(userId)
}
}
exports.getUser = async userId => {
const client = await redis.getUserClient()
// try cache
let user = await client.get(userId)
if (!user) {
user = await getGlobalDB(tenantId).get(userId)
user = await getDB(StaticDatabases.GLOBAL.name).get(userId)
client.store(userId, user, EXPIRY_SECONDS)
}
return user

5
packages/auth/src/constants.js

@ -14,14 +14,13 @@ exports.Headers = {
API_VER: "x-budibase-api-version",
APP_ID: "x-budibase-app-id",
TYPE: "x-budibase-type",
TENANT_ID: "x-budibase-tenant-id",
}
exports.GlobalRoles = {
OWNER: "owner",
ADMIN: "admin",
BUILDER: "builder",
WORKSPACE_MANAGER: "workspace_manager",
GROUP_MANAGER: "group_manager",
}
exports.Configs = {
@ -32,5 +31,3 @@ exports.Configs = {
OIDC: "oidc",
OIDC_LOGOS: "logos_oidc",
}
exports.DEFAULT_TENANT_ID = "default"

17
packages/auth/src/db/constants.js

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

95
packages/auth/src/db/utils.js

@ -1,36 +1,36 @@
const { newid } = require("../hashing")
const Replication = require("./Replication")
const { DEFAULT_TENANT_ID } = require("../constants")
const env = require("../environment")
const { StaticDatabases, SEPARATOR } = require("./constants")
const { getTenantId } = require("../tenancy")
const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_"
exports.ViewNames = {
USER_BY_EMAIL: "by_email",
}
exports.StaticDatabases = StaticDatabases
const PRE_APP = "app"
const PRE_DEV = "dev"
exports.StaticDatabases = {
GLOBAL: {
name: "global-db",
},
DEPLOYMENTS: {
name: "deployments",
},
}
const DocumentTypes = {
USER: "us",
WORKSPACE: "workspace",
GROUP: "group",
CONFIG: "config",
TEMPLATE: "template",
APP: PRE_APP,
DEV: PRE_DEV,
APP_DEV: `${PRE_APP}${SEPARATOR}${PRE_DEV}`,
APP_METADATA: `${PRE_APP}${SEPARATOR}metadata`,
APP: "app",
APP_DEV: "app_dev",
APP_METADATA: "app_metadata",
ROLE: "role",
}
exports.DocumentTypes = DocumentTypes
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
exports.SEPARATOR = SEPARATOR
function isDevApp(app) {
@ -61,21 +61,21 @@ function getDocParams(docType, docId = null, otherProps = {}) {
}
/**
* Generates a new workspace ID.
* @returns {string} The new workspace ID which the workspace doc can be stored under.
* Generates a new group ID.
* @returns {string} The new group ID which the group doc can be stored under.
*/
exports.generateWorkspaceID = () => {
return `${DocumentTypes.WORKSPACE}${SEPARATOR}${newid()}`
exports.generateGroupID = () => {
return `${DocumentTypes.GROUP}${SEPARATOR}${newid()}`
}
/**
* Gets parameters for retrieving workspaces.
* Gets parameters for retrieving groups.
*/
exports.getWorkspaceParams = (id = "", otherProps = {}) => {
exports.getGroupParams = (id = "", otherProps = {}) => {
return {
...otherProps,
startkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}`,
endkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`,
startkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}`,
endkey: `${DocumentTypes.GROUP}${SEPARATOR}${id}${UNICODE_MAX}`,
}
}
@ -103,14 +103,14 @@ exports.getGlobalUserParams = (globalId, otherProps = {}) => {
/**
* Generates a template ID.
* @param ownerId The owner/user of the template, this could be global or a workspace level.
* @param ownerId The owner/user of the template, this could be global or a group level.
*/
exports.generateTemplateID = ownerId => {
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
}
/**
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level.
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a group level.
*/
exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => {
if (!templateId) {
@ -163,26 +163,11 @@ exports.getDeployedAppID = appId => {
* different users/companies apps as there is no security around it - all apps are returned.
* @return {Promise<object[]>} returns the app information document stored in each app database.
*/
exports.getAllApps = async (CouchDB, { dev, all } = {}) => {
let tenantId = getTenantId()
if (!env.MULTI_TENANCY && !tenantId) {
tenantId = DEFAULT_TENANT_ID
}
exports.getAllApps = async ({ CouchDB, dev, all } = {}) => {
let allDbs = await CouchDB.allDbs()
const appDbNames = allDbs.filter(dbName => {
const split = dbName.split(SEPARATOR)
// it is an app, check the tenantId
if (split[0] === DocumentTypes.APP) {
const noTenantId = split.length === 2 || split[1] === DocumentTypes.DEV
// tenantId is always right before the UUID
const possibleTenantId = split[split.length - 2]
return (
(tenantId === DEFAULT_TENANT_ID && noTenantId) ||
possibleTenantId === tenantId
)
}
return false
})
const appDbNames = allDbs.filter(dbName =>
dbName.startsWith(exports.APP_PREFIX)
)
const appPromises = appDbNames.map(db =>
// skip setup otherwise databases could be re-created
new CouchDB(db, { skip_setup: true }).get(DocumentTypes.APP_METADATA)
@ -229,8 +214,8 @@ exports.dbExists = async (CouchDB, dbName) => {
* Generates a new configuration ID.
* @returns {string} The new configuration ID which the config doc can be stored under.
*/
const generateConfigID = ({ type, workspace, user }) => {
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
const generateConfigID = ({ type, group, user }) => {
const scope = [type, group, user].filter(Boolean).join(SEPARATOR)
return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`
}
@ -238,8 +223,8 @@ const generateConfigID = ({ type, workspace, user }) => {
/**
* Gets parameters for retrieving configurations.
*/
const getConfigParams = ({ type, workspace, user }, otherProps = {}) => {
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
const getConfigParams = ({ type, group, user }, otherProps = {}) => {
const scope = [type, group, user].filter(Boolean).join(SEPARATOR)
return {
...otherProps,
@ -249,15 +234,15 @@ const getConfigParams = ({ type, workspace, user }, otherProps = {}) => {
}
/**
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
* Returns the most granular configuration document from the DB based on the type, group and userID passed.
* @param {Object} db - db instance to query
* @param {Object} scopes - the type, workspace and userID scopes of the configuration.
* @param {Object} scopes - the type, group and userID scopes of the configuration.
* @returns The most granular configuration document based on the scope.
*/
const getScopedFullConfig = async function (db, { type, user, workspace }) {
const getScopedFullConfig = async function (db, { type, user, group }) {
const response = await db.allDocs(
getConfigParams(
{ type, user, workspace },
{ type, user, group },
{
include_docs: true,
}
@ -267,14 +252,14 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) {
function determineScore(row) {
const config = row.doc
// Config is specific to a user and a workspace
if (config._id.includes(generateConfigID({ type, user, workspace }))) {
// Config is specific to a user and a group
if (config._id.includes(generateConfigID({ type, user, group }))) {
return 4
} else if (config._id.includes(generateConfigID({ type, user }))) {
// Config is specific to a user only
return 3
} else if (config._id.includes(generateConfigID({ type, workspace }))) {
// Config is specific to a workspace only
} else if (config._id.includes(generateConfigID({ type, group }))) {
// Config is specific to a group only
return 2
} else if (config._id.includes(generateConfigID({ type }))) {
// Config is specific to a type only

6
packages/auth/src/db/views.js

@ -1,4 +1,5 @@
const { DocumentTypes, ViewNames } = require("./utils")
const { DocumentTypes, ViewNames, StaticDatabases } = require("./utils")
const { getDB } = require("./index")
function DesignDoc() {
return {
@ -9,7 +10,8 @@ function DesignDoc() {
}
}
exports.createUserEmailView = async db => {
exports.createUserEmailView = async () => {
const db = getDB(StaticDatabases.GLOBAL.name)
let designDoc
try {
designDoc = await db.get("_design/database")

1
packages/auth/src/environment.js

@ -16,7 +16,6 @@ module.exports = {
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
MINIO_URL: process.env.MINIO_URL,
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
MULTI_TENANCY: process.env.MULTI_TENANCY,
isTest,
_set(key, value) {
process.env[key] = value

7
packages/auth/src/index.js

@ -2,7 +2,6 @@ const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy
const { StaticDatabases } = require("./db/utils")
const { getGlobalDB } = require("./tenancy")
const {
jwt,
local,
@ -10,9 +9,8 @@ const {
google,
oidc,
auditLog,
tenancy,
} = require("./middleware")
const { setDB } = require("./db")
const { setDB, getDB } = require("./db")
const userCache = require("./cache/user")
// Strategies
@ -22,7 +20,7 @@ passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
passport.serializeUser((user, done) => done(null, user))
passport.deserializeUser(async (user, done) => {
const db = getGlobalDB()
const db = getDB(StaticDatabases.GLOBAL.name)
try {
const user = await db.get(user._id)
@ -56,7 +54,6 @@ module.exports = {
google,
oidc,
jwt: require("jsonwebtoken"),
buildTenancyMiddleware: tenancy,
auditLog,
},
cache: {

59
packages/auth/src/middleware/authenticated.js

@ -2,34 +2,46 @@ const { Cookies, Headers } = require("../constants")
const { getCookie, clearCookie } = require("../utils")
const { getUser } = require("../cache/user")
const { getSession, updateSessionTTL } = require("../security/sessions")
const { buildMatcherRegex, matches } = require("./matchers")
const env = require("../environment")
function finalise(
ctx,
{ authenticated, user, internal, version, publicEndpoint } = {}
) {
ctx.publicEndpoint = publicEndpoint || false
const PARAM_REGEX = /\/:(.*?)\//g
function buildNoAuthRegex(patterns) {
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) {
route = route.replace(match, "/.*/")
}
}
return { regex: new RegExp(route), method }
})
}
function finalise(ctx, { authenticated, user, internal, version } = {}) {
ctx.isAuthenticated = authenticated || false
ctx.user = user
ctx.internal = internal || false
ctx.version = version
}
/**
* This middleware is tenancy aware, so that it does not depend on other middlewares being used.
* The tenancy modules should not be used here and it should be assumed that the tenancy context
* has not yet been populated.
*/
module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
module.exports = (noAuthPatterns = [], opts) => {
const noAuthOptions = noAuthPatterns ? buildNoAuthRegex(noAuthPatterns) : []
return async (ctx, next) => {
let publicEndpoint = false
const version = ctx.request.headers[Headers.API_VER]
// the path is not authenticated
const found = matches(ctx, noAuthOptions)
if (found) {
publicEndpoint = true
const found = noAuthOptions.find(({ regex, method }) => {
return (
regex.test(ctx.request.url) &&
ctx.request.method.toLowerCase() === method.toLowerCase()
)
})
if (found != null) {
return next()
}
try {
// check the actual user is authenticated first
@ -46,7 +58,7 @@ module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
error = "No session found"
} else {
try {
user = await getUser(userId, session.tenantId)
user = await getUser(userId)
delete user.password
authenticated = true
} catch (err) {
@ -54,6 +66,7 @@ module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
}
}
if (error) {
console.error("Auth Error", error)
// remove the cookie as the user does not exist anymore
clearCookie(ctx, Cookies.Auth)
} else {
@ -62,26 +75,22 @@ module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => {
}
}
const apiKey = ctx.request.headers[Headers.API_KEY]
const tenantId = ctx.request.headers[Headers.TENANT_ID]
// this is an internal request, no user made it
if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) {
authenticated = true
internal = true
}
if (!user && tenantId) {
user = { tenantId }
}
// be explicit
if (authenticated !== true) {
authenticated = false
}
// isAuthenticated is a function, so use a variable to be able to check authed state
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
finalise(ctx, { authenticated, user, internal, version })
return next()
} catch (err) {
// allow configuring for public access
if ((opts && opts.publicAllowed) || publicEndpoint) {
finalise(ctx, { authenticated: false, version, publicEndpoint })
if (opts && opts.publicAllowed) {
finalise(ctx, { authenticated: false, version })
} else {
ctx.throw(err.status || 403, err)
}

2
packages/auth/src/middleware/index.js

@ -4,7 +4,6 @@ const google = require("./passport/google")
const oidc = require("./passport/oidc")
const authenticated = require("./authenticated")
const auditLog = require("./auditLog")
const tenancy = require("./tenancy")
module.exports = {
google,
@ -13,5 +12,4 @@ module.exports = {
local,
authenticated,
auditLog,
tenancy,
}

33
packages/auth/src/middleware/matchers.js

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

10
packages/auth/src/middleware/passport/google.js

@ -27,13 +27,13 @@ async function authenticate(accessToken, refreshToken, profile, done) {
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
* @returns Dynamically configured Passport Google Strategy
*/
exports.strategyFactory = async function (config, callbackUrl) {
exports.strategyFactory = async function (config) {
try {
const { clientID, clientSecret } = config
const { clientID, clientSecret, callbackURL } = config
if (!clientID || !clientSecret) {
if (!clientID || !clientSecret || !callbackURL) {
throw new Error(
"Configuration invalid. Must contain google clientID and clientSecret"
"Configuration invalid. Must contain google clientID, clientSecret and callbackURL"
)
}
@ -41,7 +41,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
{
clientID: config.clientID,
clientSecret: config.clientSecret,
callbackURL: callbackUrl,
callbackURL: config.callbackURL,
},
authenticate
)

18
packages/auth/src/middleware/passport/local.js

@ -6,23 +6,19 @@ const { getGlobalUserByEmail } = require("../../utils")
const { authError } = require("./utils")
const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
const { getTenantId } = require("../../tenancy")
const INVALID_ERR = "Invalid Credentials"
exports.options = {
passReqToCallback: true,
}
exports.options = {}
/**
* Passport Local Authentication Middleware.
* @param {*} ctx the request structure
* @param {*} email username to login with
* @param {*} password plain text password to log in with
* @param {*} done callback from passport to return user information and errors
* @param {*} email - username to login with
* @param {*} password - plain text password to log in with
* @param {*} done - callback from passport to return user information and errors
* @returns The authenticated user, or errors if they occur
*/
exports.authenticate = async function (ctx, email, password, done) {
exports.authenticate = async function (email, password, done) {
if (!email) return authError(done, "Email Required")
if (!password) return authError(done, "Password Required")
@ -39,14 +35,12 @@ exports.authenticate = async function (ctx, email, password, done) {
// authenticate
if (await compare(password, dbUser.password)) {
const sessionId = newid()
const tenantId = getTenantId()
await createASession(dbUser._id, { sessionId, tenantId })
await createASession(dbUser._id, sessionId)
dbUser.token = jwt.sign(
{
userId: dbUser._id,
sessionId,
tenantId,
},
env.JWT_SECRET
)

10
packages/auth/src/middleware/passport/tests/google.spec.js

@ -2,9 +2,8 @@
const { data } = require("./utilities/mock-data")
const TENANT_ID = "default"
const googleConfig = {
callbackURL: "http://somecallbackurl",
clientID: data.clientID,
clientSecret: data.clientSecret,
}
@ -27,14 +26,13 @@ describe("google", () => {
it("should create successfully create a google strategy", async () => {
const google = require("../google")
const callbackUrl = `/api/global/auth/${TENANT_ID}/google/callback`
await google.strategyFactory(googleConfig, callbackUrl)
await google.strategyFactory(googleConfig)
const expectedOptions = {
clientID: googleConfig.clientID,
clientSecret: googleConfig.clientSecret,
callbackURL: callbackUrl,
callbackURL: googleConfig.callbackURL,
}
expect(mockStrategy).toHaveBeenCalledWith(

24
packages/auth/src/middleware/passport/third-party-common.js

@ -1,12 +1,11 @@
const env = require("../../environment")
const jwt = require("jsonwebtoken")
const { generateGlobalUserID } = require("../../db/utils")
const database = require("../../db")
const { StaticDatabases, generateGlobalUserID } = require("../../db/utils")
const { authError } = require("./utils")
const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
const { getGlobalUserByEmail } = require("../../utils")
const { getGlobalDB, getTenantId } = require("../../tenancy")
const fetch = require("node-fetch")
/**
* Common authentication logic for third parties. e.g. OAuth, OIDC.
@ -16,22 +15,20 @@ exports.authenticateThirdParty = async function (
requireLocalAccount = true,
done
) {
if (!thirdPartyUser.provider) {
if (!thirdPartyUser.provider)
return authError(done, "third party user provider required")
}
if (!thirdPartyUser.userId) {
if (!thirdPartyUser.userId)
return authError(done, "third party user id required")
}
if (!thirdPartyUser.email) {
if (!thirdPartyUser.email)
return authError(done, "third party user email required")
}
// use the third party id
const userId = generateGlobalUserID(thirdPartyUser.userId)
const db = getGlobalDB()
const db = database.getDB(StaticDatabases.GLOBAL.name)
let dbUser
// use the third party id
const userId = generateGlobalUserID(thirdPartyUser.userId)
// try to load by id
try {
dbUser = await db.get(userId)
@ -76,8 +73,7 @@ exports.authenticateThirdParty = async function (
// authenticate
const sessionId = newid()
const tenantId = getTenantId()
await createASession(dbUser._id, { sessionId, tenantId })
await createASession(dbUser._id, sessionId)
dbUser.token = jwt.sign(
{

14
packages/auth/src/middleware/tenancy.js

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

7
packages/auth/src/security/sessions.js

@ -12,13 +12,12 @@ function makeSessionID(userId, sessionId) {
return `${userId}/${sessionId}`
}
exports.createASession = async (userId, session) => {
exports.createASession = async (userId, sessionId) => {
const client = await redis.getSessionClient()
const sessionId = session.sessionId
session = {
const session = {
createdAt: new Date().toISOString(),
lastAccessedAt: new Date().toISOString(),
...session,
sessionId,
userId,
}
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)

73
packages/auth/src/tenancy/FunctionContext.js

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

81
packages/auth/src/tenancy/context.js

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

4
packages/auth/src/tenancy/index.js

@ -1,4 +0,0 @@
module.exports = {
...require("./context"),
...require("./tenancy"),
}

105
packages/auth/src/tenancy/tenancy.js

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

13
packages/auth/src/utils.js

@ -1,9 +1,14 @@
const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils")
const {
DocumentTypes,
SEPARATOR,
ViewNames,
StaticDatabases,
} = require("./db/utils")
const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt")
const { createUserEmailView } = require("./db/views")
const { getDB } = require("./db")
const { Headers } = require("./constants")
const { getGlobalDB } = require("./tenancy")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -106,7 +111,7 @@ exports.getGlobalUserByEmail = async email => {
if (email == null) {
throw "Must supply an email address to view"
}
const db = getGlobalDB()
const db = getDB(StaticDatabases.GLOBAL.name)
try {
let users = (
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
@ -118,7 +123,7 @@ exports.getGlobalUserByEmail = async email => {
return users.length <= 1 ? users[0] : users
} catch (err) {
if (err != null && err.name === "not_found") {
await createUserEmailView(db)
await createUserEmailView()
return exports.getGlobalUserByEmail(email)
} else {
throw err

1
packages/auth/tenancy.js

@ -1 +0,0 @@
module.exports = require("./src/tenancy")

35
packages/auth/yarn.lock

@ -798,13 +798,6 @@ ast-types@0.9.6:
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=
async-hook-jl@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
dependencies:
stack-chain "^1.3.7"
async@~2.1.4:
version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
@ -1151,15 +1144,6 @@ clone-buffer@1.0.0:
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
cls-hooked@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
dependencies:
async-hook-jl "^1.7.6"
emitter-listener "^1.0.1"
semver "^5.4.1"
cluster-key-slot@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
@ -1460,13 +1444,6 @@ electron-to-chromium@^1.3.723:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz#046517d1f2cea753e06fff549995b9dc45e20082"
integrity sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q==
emitter-listener@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
dependencies:
shimmer "^1.2.0"
emittery@^0.7.1:
version "0.7.2"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82"
@ -4058,7 +4035,7 @@ saxes@^5.0.1:
dependencies:
xmlchars "^2.2.0"
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -4119,11 +4096,6 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
shimmer@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -4278,11 +4250,6 @@ sshpk@^1.7.0:
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
stack-chain@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=
stack-utils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277"

2
packages/bbui/package.json

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

8
packages/builder/package.json

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

2
packages/builder/src/builderStore/store/frontend.js

@ -70,7 +70,7 @@ export const getFrontendStore = () => {
url: application.url,
layouts,
screens,
theme: application.theme,
theme: application.theme || "spectrum--light",
hasAppPackage: true,
appInstance: application.instance,
clientLibPath,

4
packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte

@ -18,8 +18,8 @@
const dispatch = createEventDispatcher()
let bindingDrawer
$: tempValue = Array.isArray(value) ? value : []
$: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue
const handleClose = () => {
onChange(tempValue)
@ -56,7 +56,7 @@
slot="body"
value={readableValue}
close={handleClose}
on:update={event => (tempValue = event.detail)}
on:change={event => (tempValue = event.detail)}
bindableProperties={bindings}
/>
</Drawer>

2
packages/builder/src/components/design/AppPreview/AppThemeSelect.svelte

@ -24,7 +24,7 @@
<div>
<Select
value={$store.theme || "spectrum--light"}
value={$store.theme}
options={themeOptions}
placeholder={null}
on:change={e => store.actions.theme.save(e.detail)}

14
packages/builder/src/components/design/AppPreview/iframeTemplate.js

@ -47,6 +47,18 @@ export default `
return
}
// Parse received message
// If parsing fails, just ignore and wait for the next message
let parsed
try {
parsed = JSON.parse(event.data)
} catch (error) {
// Ignore
}
if (!parsed) {
return
}
// Extract data from message
const {
selectedComponentId,
@ -55,7 +67,7 @@ export default `
previewType,
appId,
theme
} = JSON.parse(event.data)
} = parsed
// Set some flags so the app knows we're in the builder
window["##BUDIBASE_IN_BUILDER##"] = true

2
packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte

@ -11,6 +11,7 @@
export let componentDefinition
export let componentInstance
export let assetInstance
export let bindings
const layoutDefinition = []
const screenDefinition = [
@ -65,6 +66,7 @@
options: setting.options,
placeholder: setting.placeholder,
}}
{bindings}
/>
{/if}
{/each}

3
packages/builder/src/components/design/PropertiesPanel/ConditionalUISection.svelte

@ -4,6 +4,7 @@
import ConditionalUIDrawer from "./PropertyControls/ConditionalUIDrawer.svelte"
export let componentInstance
export let bindings
let tempValue
let drawer
@ -32,5 +33,5 @@
Show, hide and update components in response to conditions being met.
</svelte:fragment>
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} />
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
</Drawer>

2
packages/builder/src/components/design/PropertiesPanel/DesignSection.svelte

@ -4,6 +4,7 @@
export let componentDefinition
export let componentInstance
export let bindings
const getStyles = def => {
if (!def?.styles?.length) {
@ -29,6 +30,7 @@
columns={style.columns}
properties={style.settings}
{componentInstance}
{bindings}
/>
{/each}
{/if}

30
packages/builder/src/components/design/PropertiesPanel/PropertiesPanel.svelte

@ -1,27 +1,45 @@
<script>
import { store, selectedComponent } from "builderStore"
import { store, selectedComponent, currentAsset } from "builderStore"
import { Tabs, Tab } from "@budibase/bbui"
import ScreenSettingsSection from "./ScreenSettingsSection.svelte"
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
import DesignSection from "./DesignSection.svelte"
import CustomStylesSection from "./CustomStylesSection.svelte"
import ConditionalUISection from "./ConditionalUISection.svelte"
import { getBindableProperties } from "builderStore/dataBinding"
$: componentInstance = $selectedComponent
$: componentDefinition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
</script>
<Tabs selected="Settings" noPadding>
<Tab title="Settings">
<div class="container">
{#key componentInstance?._id}
<ScreenSettingsSection {componentInstance} {componentDefinition} />
<ComponentSettingsSection {componentInstance} {componentDefinition} />
<DesignSection {componentInstance} {componentDefinition} />
<CustomStylesSection {componentInstance} {componentDefinition} />
<ConditionalUISection {componentInstance} {componentDefinition} />
<ScreenSettingsSection
{componentInstance}
{componentDefinition}
{bindings}
/>
<ComponentSettingsSection
{componentInstance}
{componentDefinition}
{bindings}
/>
<DesignSection {componentInstance} {componentDefinition} {bindings} />
<CustomStylesSection
{componentInstance}
{componentDefinition}
{bindings}
/>
<ConditionalUISection
{componentInstance}
{componentDefinition}
{bindings}
/>
{/key}
</div>
</Tab>

13
packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte

@ -13,12 +13,12 @@
import { generate } from "shortid"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
import { getBindableProperties } from "builderStore/dataBinding"
import { currentAsset, selectedComponent, store } from "builderStore"
import { selectedComponent, store } from "builderStore"
import { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte"
export let conditions = []
export let bindings = []
const flipDurationMs = 150
const actionOptions = [
@ -64,10 +64,6 @@
value: setting.key,
}
})
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
$: conditions.forEach(link => {
if (!link.id) {
link.id = generate()
@ -194,6 +190,7 @@
placeholder: getSettingDefinition(condition.setting)
.placeholder,
}}
{bindings}
/>
{:else}
<Select disabled placeholder=" " />
@ -201,7 +198,7 @@
{/if}
<div>IF</div>
<DrawerBindableInput
bindings={bindableProperties}
{bindings}
placeholder="Value"
value={condition.newValue}
on:change={e => (condition.newValue = e.detail)}
@ -222,7 +219,7 @@
{#if ["string", "number"].includes(condition.valueType)}
<DrawerBindableInput
disabled={condition.noValue}
bindings={bindableProperties}
{bindings}
placeholder="Value"
value={condition.referenceValue}
on:change={e => (condition.referenceValue = e.detail)}

16
packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte

@ -1,8 +1,5 @@
<script>
import {
getBindableProperties,
getDataProviderComponents,
} from "builderStore/dataBinding"
import { getDataProviderComponents } from "builderStore/dataBinding"
import {
Button,
Popover,
@ -31,6 +28,7 @@
export let value = {}
export let otherSources
export let showAllQueries
export let bindings = []
$: text = value?.label ?? "Choose an option"
$: tables = $tablesStore.list.map(m => ({
@ -60,10 +58,6 @@
parameters: query.parameters,
type: "query",
}))
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
$: dataProviders = getDataProviderComponents(
$currentAsset,
$store.selectedComponentId
@ -75,13 +69,13 @@
type: "provider",
schema: provider.schema,
}))
$: queryBindableProperties = bindableProperties.map(property => ({
$: queryBindableProperties = bindings.map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.readableBinding,
}))
$: links = bindableProperties
$: links = bindings
.filter(x => x.fieldSchema?.type === "link")
.map(property => {
return {
@ -138,7 +132,7 @@
bind:customParams={value.queryParams}
parameters={queries.find(query => query._id === value._id)
.parameters}
bindings={queryBindableProperties}
{bindings}
/>
{/if}
<IntegrationQueryEditor

3
packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventEditor.svelte

@ -13,10 +13,10 @@
import { generate } from "shortid"
const flipDurationMs = 150
const EVENT_TYPE_KEY = "##eventHandlerType"
export let actions
export let bindings = []
// dndzone needs an id on the array items, so this adds some temporary ones.
$: {
@ -121,6 +121,7 @@
<svelte:component
this={selectedActionComponent}
parameters={selectedAction.parameters}
{bindings}
/>
</div>
{/if}

3
packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/EventPropertyControl.svelte

@ -9,6 +9,7 @@
export let value = []
export let name
export let bindings
let drawer
@ -57,5 +58,5 @@
Define what actions to run.
</svelte:fragment>
<Button cta slot="buttons" on:click={saveEventData}>Save</Button>
<EventEditor slot="body" bind:actions={value} eventType={name} />
<EventEditor slot="body" bind:actions={value} eventType={name} {bindings} />
</Drawer>

4
packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/DeleteRow.svelte

@ -1,14 +1,12 @@
<script>
import { Select, Label, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import { tables } from "stores/backend"
import { getBindableProperties } from "builderStore/dataBinding"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters
export let bindings = []
$: tableOptions = $tables.list || []
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
</script>
<div class="root">

9
packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/ExecuteQuery.svelte

@ -1,21 +1,16 @@
<script>
import { Select, Layout, Input, Checkbox } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import { datasources, integrations, queries } from "stores/backend"
import { getBindableProperties } from "builderStore/dataBinding"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte"
export let parameters
export let bindings = []
$: query = $queries.list.find(q => q._id === parameters.queryId)
$: datasource = $datasources.list.find(
ds => ds._id === parameters.datasourceId
)
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
function fetchQueryDefinition(query) {
const source = $datasources.list.find(
@ -61,7 +56,7 @@
<ParameterBuilder
bind:customParams={parameters.queryParams}
parameters={query.parameters}
bindings={bindableProperties}
{bindings}
/>
<IntegrationQueryEditor
height={200}

7
packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/NavigateTo.svelte

@ -1,12 +1,9 @@
<script>
import { Label, Checkbox } from "@budibase/bbui"
import { getBindableProperties } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore"
import { Label } from "@budibase/bbui"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
export let bindings = []
</script>
<div class="root">

9
packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveFields.svelte

@ -1,7 +1,5 @@
<script>
import { Label, ActionButton, Button, Select, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
import { createEventDispatcher } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
@ -11,13 +9,10 @@
export let schemaFields
export let fieldLabel = "Column"
export let valueLabel = "Value"
export let bindings = []
let fields = Object.entries(parameterFields || {})
$: onChange(fields)
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
const addField = () => {
fields = [...fields.filter(field => field[0]), ["", ""]]
@ -69,7 +64,7 @@
<DrawerBindableInput
title={`Value for "${field[0]}"`}
value={field[1]}
bindings={bindableProperties}
{bindings}
on:change={event => updateFieldValue(idx, event.detail)}
/>
<ActionButton

2
packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte

@ -9,6 +9,7 @@
import SaveFields from "./SaveFields.svelte"
export let parameters
export let bindings = []
$: dataProviderComponents = getDataProviderComponents(
$currentAsset,
@ -70,6 +71,7 @@
parameterFields={parameters.fields}
{schemaFields}
on:change={onFieldsChanged}
{bindings}
/>
</div>
{/if}

6
packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/TriggerAutomation.svelte

@ -3,13 +3,14 @@
import { automationStore } from "builderStore"
import SaveFields from "./SaveFields.svelte"
export let parameters = {}
export let bindings = []
const AUTOMATION_STATUS = {
NEW: "new",
EXISTING: "existing",
}
export let parameters = {}
let automationStatus = parameters.automationId
? AUTOMATION_STATUS.EXISTING
: AUTOMATION_STATUS.NEW
@ -109,6 +110,7 @@
parameterFields={parameters.fields}
fieldLabel="Field"
on:change={onFieldsChanged}
{bindings}
/>
{/key}
</div>

4
packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte

@ -20,6 +20,8 @@
export let value = []
export let componentInstance
export let bindings = []
let drawer
let tempValue = value || []
@ -51,7 +53,7 @@
constraints.
{/if}
</Body>
<LuceneFilterBuilder bind:value={tempValue} {schemaFields} />
<LuceneFilterBuilder bind:value={tempValue} {schemaFields} {bindings} />
</Layout>
</DrawerContent>
</Drawer>

9
packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/LuceneFilterBuilder.svelte

@ -7,20 +7,15 @@
Combobox,
Input,
} from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { generate } from "shortid"
import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
export let schemaFields
export let value
export let bindings = []
const BannedTypes = ["link", "attachment"]
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
$: fieldOptions = (schemaFields ?? [])
.filter(field => !BannedTypes.includes(field.type))
.map(field => field.name)
@ -101,7 +96,7 @@
title={`Value for "${expression.field}"`}
value={expression.value}
placeholder="Value"
bindings={bindableProperties}
{bindings}
on:change={event => (expression.value = event.detail)}
/>
{:else if ["string", "longform", "number"].includes(expression.type)}

18
packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte

@ -1,8 +1,6 @@
<script>
import { Button, Icon, Drawer, Label } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import {
getBindableProperties,
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
@ -18,18 +16,15 @@
export let value = null
export let props = {}
export let onChange = () => {}
export let bindings = []
let bindingDrawer
let anchor
let valid
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)
$: safeValue = getSafeValue(value, props.defaultValue, bindings)
$: tempValue = safeValue
$: replaceBindings = val => readableToRuntimeBinding(bindableProperties, val)
$: replaceBindings = val => readableToRuntimeBinding(bindings, val)
const handleClose = () => {
handleChange(tempValue)
@ -61,8 +56,8 @@
// The "safe" value is the value with any bindings made readable
// If there is no value set, any default value is used
const getSafeValue = (value, defaultValue, bindableProperties) => {
const enriched = runtimeToReadableBinding(bindableProperties, value)
const getSafeValue = (value, defaultValue, bindings) => {
const enriched = runtimeToReadableBinding(bindings, value)
return enriched == null && defaultValue !== undefined
? defaultValue
: enriched
@ -83,6 +78,7 @@
updateOnChange={false}
on:change={handleChange}
onChange={handleChange}
{bindings}
name={key}
text={label}
{type}
@ -108,7 +104,7 @@
bind:valid
value={safeValue}
on:change={e => (tempValue = e.detail)}
{bindableProperties}
bindableProperties={bindings}
/>
</Drawer>
{/if}

3
packages/builder/src/components/design/PropertiesPanel/PropertyControls/URLSelect.svelte

@ -3,10 +3,11 @@
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
export let value
export let bindings
$: urlOptions = $store.screens
.map(screen => screen.routing?.route)
.filter(x => x != null)
</script>
<DrawerBindableCombobox {value} on:change options={urlOptions} />
<DrawerBindableCombobox {value} {bindings} on:change options={urlOptions} />

2
packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte

@ -9,6 +9,7 @@
import { FrontendTypes } from "constants"
export let componentInstance
export let bindings
function setAssetProps(name, value) {
const selectedAsset = get(currentAsset)
@ -44,6 +45,7 @@
key={def.key}
value={deepGet($currentAsset, def.key)}
onChange={val => setAssetProps(def.key, val)}
{bindings}
/>
{/each}
</DetailSummary>

2
packages/builder/src/components/design/PropertiesPanel/StyleSection.svelte

@ -7,6 +7,7 @@
export let columns
export let properties
export let componentInstance
export let bindings = []
$: style = componentInstance._styles.normal || {}
$: changed = properties?.some(prop => hasPropChanged(style, prop)) ?? false
@ -36,6 +37,7 @@
value={style[prop.key]}
onChange={val => store.actions.components.updateStyle(prop.key, val)}
props={getControlProps(prop)}
{bindings}
/>
</div>
{/each}

14
packages/builder/src/pages/builder/_layout.svelte

@ -4,10 +4,7 @@
import { onMount } from "svelte"
let loaded = false
$: multiTenancyEnabled = $admin.multiTenancy
$: hasAdminUser = !!$admin?.checklist?.adminUser
$: tenantSet = $auth.tenantSet
onMount(async () => {
await admin.init()
@ -15,14 +12,9 @@
loaded = true
})
// Force creation of an admin user if one doesn't exist
$: {
const apiReady = $admin.loaded && $auth.loaded
// if tenant is not set go to it
if (loaded && apiReady && multiTenancyEnabled && !tenantSet) {
$redirect("./auth/org")
}
// Force creation of an admin user if one doesn't exist
else if (loaded && apiReady && !hasAdminUser) {
if (loaded && !hasAdminUser) {
$redirect("./admin")
}
}
@ -37,7 +29,7 @@
!$isActive("./invite")
) {
const returnUrl = encodeURIComponent(window.location.pathname)
$redirect("./auth?", { returnUrl })
$redirect("./auth/login?", { returnUrl })
} else if ($auth?.user?.forceResetPassword) {
$redirect("./auth/reset")
}

28
packages/builder/src/pages/builder/admin/index.svelte

@ -6,25 +6,20 @@
Layout,
Input,
Body,
ActionButton,
} from "@budibase/bbui"
import { goto } from "@roxi/routify"
import api from "builderStore/api"
import { admin, auth } from "stores/portal"
import { admin } from "stores/portal"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
import Logo from "assets/bb-emblem.svg"
let adminUser = {}
let error
$: tenantId = $auth.tenantId
$: multiTenancyEnabled = $admin.multiTenancy
async function save() {
try {
adminUser.tenantId = tenantId
// Save the admin user
const response = await api.post(`/api/global/users/init`, adminUser)
const response = await api.post(`/api/admin/users/init`, adminUser)
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
@ -52,22 +47,9 @@
<Input label="Email" bind:value={adminUser.email} />
<PasswordRepeatInput bind:password={adminUser.password} bind:error />
</Layout>
<Layout gap="XS" noPadding>
<Button cta disabled={error} on:click={save}>
Create super admin user
</Button>
{#if multiTenancyEnabled}
<ActionButton
quiet
on:click={() => {
admin.unload()
$goto("../auth/org")
}}
>
Change organisation
</ActionButton>
{/if}
</Layout>
<Button cta disabled={error} on:click={save}>
Create super admin user
</Button>
</Layout>
</div>
</section>

10
packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/CreateEditRelationship/CreateEditRelationship.svelte

@ -158,10 +158,16 @@
fieldName: fromTable.primary[0],
}
} else {
// the relateFrom.fieldName should remain the same, as it is the foreignKey in the other
// table, this is due to the way that budibase represents relationships, the fieldName in a
// link column schema is the column linked to (FK in this case). The foreignKey column is
// essentially what is linked to in the from table, this is unique to SQL as this isn't a feature
// of Budibase internal tables.
// Essentially this means the fieldName is what we are linking to in the other table, and the
// foreignKey is what is linking out of the current table.
relateFrom = {
...relateFrom,
foreignKey: relateFrom.fieldName,
fieldName: fromTable.primary[0],
foreignKey: fromTable.primary[0],
}
relateTo = {
...relateTo,

8
packages/builder/src/pages/builder/auth/_components/GoogleButton.svelte

@ -1,18 +1,14 @@
<script>
import { ActionButton } from "@budibase/bbui"
import GoogleLogo from "assets/google-logo.png"
import { auth, organisation } from "stores/portal"
import { organisation } from "stores/portal"
let show
$: tenantId = $auth.tenantId
$: show = $organisation.google
</script>
{#if show}
<ActionButton
on:click={() =>
window.open(`/api/global/auth/${tenantId}/google`, "_blank")}
on:click={() => window.open("/api/admin/auth/google", "_blank")}
>
<div class="inner">
<img src={GoogleLogo} alt="google icon" />

2
packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte

@ -31,7 +31,7 @@
{#if show}
<ActionButton
on:click={() =>
window.open(`/api/global/auth/oidc/configs/${$oidc.uuid}`, "_blank")}
window.open(`/api/admin/auth/oidc/configs/${$oidc.uuid}`, "_blank")}
>
<div class="inner">
<img {src} alt="oidc icon" />

11
packages/builder/src/pages/builder/auth/forgot.svelte

@ -6,12 +6,10 @@
Layout,
Body,
Heading,
ActionButton,
} from "@budibase/bbui"
import { organisation, auth } from "stores/portal"
import Logo from "assets/bb-emblem.svg"
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
let email = ""
@ -43,12 +41,9 @@
</Body>
<Input label="Email" bind:value={email} />
</Layout>
<Layout gap="XS" nopadding>
<Button cta on:click={forgot} disabled={!email}>
Reset your password
</Button>
<ActionButton quiet on:click={() => $goto("../")}>Back</ActionButton>
</Layout>
<Button cta on:click={forgot} disabled={!email}>
Reset your password
</Button>
</Layout>
</div>
</div>

22
packages/builder/src/pages/builder/auth/index.svelte

@ -1,24 +1,4 @@
<script>
import { redirect } from "@roxi/routify"
import { auth, admin } from "stores/portal"
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
})
$redirect("./login")
</script>

23
packages/builder/src/pages/builder/auth/login.svelte

@ -10,7 +10,7 @@
notifications,
} from "@budibase/bbui"
import { goto, params } from "@roxi/routify"
import { auth, organisation, oidc, admin } from "stores/portal"
import { auth, organisation, oidc } from "stores/portal"
import GoogleButton from "./_components/GoogleButton.svelte"
import OIDCButton from "./_components/OIDCButton.svelte"
import Logo from "assets/bb-emblem.svg"
@ -18,10 +18,8 @@
let username = ""
let password = ""
let loaded = false
$: company = $organisation.company || "Budibase"
$: multiTenancyEnabled = $admin.multiTenancy
async function login() {
try {
@ -29,6 +27,7 @@
username,
password,
})
notifications.success("Logged in successfully")
if ($auth?.user?.forceResetPassword) {
$goto("./reset")
} else {
@ -51,7 +50,6 @@
onMount(async () => {
await organisation.init()
loaded = true
})
</script>
@ -63,10 +61,8 @@
<img alt="logo" src={$organisation.logoUrl || Logo} />
<Heading>Sign in to {company}</Heading>
</Layout>
{#if loaded}
<GoogleButton />
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
{/if}
<GoogleButton />
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
<Divider noGrid />
<Layout gap="XS" noPadding>
<Body size="S" textAlign="center">Sign in with email</Body>
@ -83,17 +79,6 @@
<ActionButton quiet on:click={() => $goto("./forgot")}>
Forgot password?
</ActionButton>
{#if multiTenancyEnabled}
<ActionButton
quiet
on:click={() => {
admin.unload()
$goto("./org")
}}
>
Change organisation
</ActionButton>
{/if}
</Layout>
</Layout>
</div>

71
packages/builder/src/pages/builder/auth/org.svelte

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

8
packages/builder/src/pages/builder/index.svelte

@ -2,15 +2,13 @@
import { redirect } from "@roxi/routify"
import { auth } from "stores/portal"
auth.checkQueryString()
$: {
if (!$auth.user) {
$redirect(`./auth`)
$redirect("./auth/login")
} else if ($auth.user.builder?.global) {
$redirect(`./portal`)
$redirect("./portal")
} else {
$redirect(`./apps`)
$redirect("./apps")
}
}
</script>

114
packages/builder/src/pages/builder/portal/manage/auth/index.svelte

@ -7,6 +7,7 @@
import OneLoginLogo from "assets/onelogin-logo.png"
import OidcLogoPng from "assets/oidc-logo.png"
import { isEqual, cloneDeep } from "lodash/fp"
import {
Button,
Heading,
@ -21,51 +22,36 @@
} from "@budibase/bbui"
import { onMount } from "svelte"
import api from "builderStore/api"
import { organisation, auth, admin } from "stores/portal"
import { organisation } from "stores/portal"
import { uuid } from "builderStore/uuid"
$: tenantId = $auth.tenantId
$: multiTenancyEnabled = $admin.multiTenancy
const ConfigTypes = {
Google: "google",
OIDC: "oidc",
// Github: "github",
// AzureAD: "ad",
}
function callbackUrl(tenantId, end) {
let url = `/api/global/auth`
if (multiTenancyEnabled && tenantId) {
url += `/${tenantId}`
}
url += end
return url
const GoogleConfigFields = {
Google: ["clientID", "clientSecret", "callbackURL"],
}
$: GoogleConfigFields = {
Google: [
{ name: "clientID", label: "Client ID" },
{ name: "clientSecret", label: "Client secret" },
{
name: "callbackURL",
label: "Callback URL",
readonly: true,
placeholder: callbackUrl(tenantId, "/google/callback"),
},
],
const GoogleConfigLabels = {
Google: {
clientID: "Client ID",
clientSecret: "Client secret",
callbackURL: "Callback URL",
},
}
$: OIDCConfigFields = {
Oidc: [
{ name: "configUrl", label: "Config URL" },
{ name: "clientID", label: "Client ID" },
{ name: "clientSecret", label: "Client Secret" },
{
name: "callbackURL",
label: "Callback URL",
readonly: true,
placeholder: callbackUrl(tenantId, "/oidc/callback"),
},
],
const OIDCConfigFields = {
Oidc: ["configUrl", "clientID", "clientSecret"],
}
const OIDCConfigLabels = {
Oidc: {
configUrl: "Config URL",
clientID: "Client ID",
clientSecret: "Client Secret",
},
}
let iconDropdownOptions = [
@ -123,13 +109,17 @@
// Create a flag so that it will only try to save completed forms
$: partialGoogle =
providers.google?.config?.clientID || providers.google?.config?.clientSecret
providers.google?.config?.clientID ||
providers.google?.config?.clientSecret ||
providers.google?.config?.callbackURL
$: partialOidc =
providers.oidc?.config?.configs[0].configUrl ||
providers.oidc?.config?.configs[0].clientID ||
providers.oidc?.config?.configs[0].clientSecret
$: googleComplete =
providers.google?.config?.clientID && providers.google?.config?.clientSecret
providers.google?.config?.clientID &&
providers.google?.config?.clientSecret &&
providers.google?.config?.callbackURL
$: oidcComplete =
providers.oidc?.config?.configs[0].configUrl &&
providers.oidc?.config?.configs[0].clientID &&
@ -139,7 +129,7 @@
let data = new FormData()
data.append("file", file)
const res = await api.post(
`/api/global/configs/upload/logos_oidc/${file.name}`,
`/api/admin/configs/upload/logos_oidc/${file.name}`,
data,
{}
)
@ -159,21 +149,17 @@
let calls = []
docs.forEach(element => {
if (element.type === ConfigTypes.OIDC) {
//Add a UUID here so each config is distinguishable when it arrives at the login page
for (let config of element.config.configs) {
if (!config.uuid) {
config.uuid = uuid()
}
// callback urls shouldn't be included
delete config.callbackURL
}
//Add a UUID here so each config is distinguishable when it arrives at the login page.
element.config.configs.forEach(config => {
!config.uuid && (config.uuid = uuid())
})
if (partialOidc) {
if (!oidcComplete) {
notifications.error(
`Please fill in all required ${ConfigTypes.OIDC} fields`
)
} else {
calls.push(api.post(`/api/global/configs`, element))
calls.push(api.post(`/api/admin/configs`, element))
// turn the save button grey when clicked
oidcSaveButtonDisabled = true
originalOidcDoc = cloneDeep(providers.oidc)
@ -187,8 +173,7 @@
`Please fill in all required ${ConfigTypes.Google} fields`
)
} else {
delete element.config.callbackURL
calls.push(api.post(`/api/global/configs`, element))
calls.push(api.post(`/api/admin/configs`, element))
googleSaveButtonDisabled = true
originalGoogleDoc = cloneDeep(providers.google)
}
@ -221,7 +206,7 @@
await organisation.init()
// fetch the configs for oauth
const googleResponse = await api.get(
`/api/global/configs/${ConfigTypes.Google}`
`/api/admin/configs/${ConfigTypes.Google}`
)
const googleDoc = await googleResponse.json()
@ -242,7 +227,7 @@
//Get the list of user uploaded logos and push it to the dropdown options.
//This needs to be done before the config call so they're available when the dropdown renders
const res = await api.get(`/api/global/configs/logos_oidc`)
const res = await api.get(`/api/admin/configs/logos_oidc`)
const configSettings = await res.json()
if (configSettings.config) {
@ -257,16 +242,17 @@
})
})
}
const oidcResponse = await api.get(
`/api/global/configs/${ConfigTypes.OIDC}`
)
const oidcResponse = await api.get(`/api/admin/configs/${ConfigTypes.OIDC}`)
const oidcDoc = await oidcResponse.json()
if (!oidcDoc._id) {
console.log("hi")
providers.oidc = {
type: ConfigTypes.OIDC,
config: { configs: [{ activated: true }] },
}
} else {
console.log("hello")
originalOidcDoc = cloneDeep(oidcDoc)
providers.oidc = oidcDoc
}
@ -309,12 +295,8 @@
<Layout gap="XS" noPadding>
{#each GoogleConfigFields.Google as field}
<div class="form-row">
<Label size="L">{field.label}</Label>
<Input
bind:value={providers.google.config[field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
<Label size="L">{GoogleConfigLabels.Google[field]}</Label>
<Input bind:value={providers.google.config[field]} />
</div>
{/each}
<div class="form-row">
@ -353,14 +335,14 @@
<Layout gap="XS" noPadding>
{#each OIDCConfigFields.Oidc as field}
<div class="form-row">
<Label size="L">{field.label}</Label>
<Input
bind:value={providers.oidc.config.configs[0][field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
<Label size="L">{OIDCConfigLabels.Oidc[field]}</Label>
<Input bind:value={providers.oidc.config.configs[0][field]} />
</div>
{/each}
<div class="form-row">
<Label size="L">Callback URL</Label>
<Input readonly placeholder="/api/admin/auth/oidc/callback" />
</div>
<br />
<Body size="S">
To customize your login button, fill out the fields below.

15
packages/builder/src/pages/builder/portal/manage/email/index.svelte

@ -53,7 +53,7 @@
delete smtp.config.auth
}
// Save your SMTP config
const response = await api.post(`/api/global/configs`, smtp)
const response = await api.post(`/api/admin/configs`, smtp)
if (response.status !== 200) {
const error = await response.text()
@ -75,9 +75,7 @@
async function fetchSmtp() {
loading = true
// fetch the configs for smtp
const smtpResponse = await api.get(
`/api/global/configs/${ConfigTypes.SMTP}`
)
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`)
const smtpDoc = await smtpResponse.json()
if (!smtpDoc._id) {
@ -94,13 +92,8 @@
requireAuth = smtpConfig.config.auth != null
// always attach the auth for the forms purpose -
// this will be removed later if required
if (!smtpDoc.config) {
smtpDoc.config = {}
}
if (!smtpDoc.config.auth) {
smtpConfig.config.auth = {
type: "login",
}
smtpConfig.config.auth = {
type: "login",
}
}

4
packages/builder/src/pages/builder/portal/manage/users/[userId].svelte

@ -47,8 +47,8 @@
})
let selectedApp
const userFetch = fetchData(`/api/global/users/${userId}`)
const apps = fetchData(`/api/global/roles`)
const userFetch = fetchData(`/api/admin/users/${userId}`)
const apps = fetchData(`/api/admin/roles`)
async function deleteUser() {
const res = await users.delete(userId)

2
packages/builder/src/pages/builder/portal/settings/organisation.svelte

@ -37,7 +37,7 @@
async function uploadLogo(file) {
let data = new FormData()
data.append("file", file)
const res = await post("/api/global/configs/upload/settings/logo", data, {})
const res = await post("/api/admin/configs/upload/settings/logo", data, {})
return await res.json()
}

9
packages/builder/src/pages/index.svelte

@ -1,11 +1,4 @@
<script>
import { redirect } from "@roxi/routify"
import { auth } from "../stores/portal"
import { onMount } from "svelte"
auth.checkQueryString()
onMount(() => {
$redirect(`./builder`)
})
$redirect("./builder")
</script>

53
packages/builder/src/stores/portal/admin.js

@ -1,18 +1,12 @@
import { writable, get } from "svelte/store"
import { writable } from "svelte/store"
import api from "builderStore/api"
import { auth } from "stores/portal"
export function createAdminStore() {
const admin = writable({
loaded: false,
})
const { subscribe, set } = writable({})
async function init() {
try {
const tenantId = get(auth).tenantId
const response = await api.get(
`/api/global/configs/checklist?tenantId=${tenantId}`
)
const response = await api.get("/api/admin/configs/checklist")
const json = await response.json()
const onboardingSteps = Object.keys(json)
@ -22,49 +16,20 @@ export function createAdminStore() {
0
)
await multiTenancyEnabled()
admin.update(store => {
store.loaded = true
store.checklist = json
store.onboardingProgress =
(stepsComplete / onboardingSteps.length) * 100
return store
set({
checklist: json,
onboardingProgress: (stepsComplete / onboardingSteps.length) * 100,
})
} catch (err) {
admin.update(store => {
store.checklist = null
return store
set({
checklist: null,
})
}
}
async function multiTenancyEnabled() {
let enabled = false
try {
const response = await api.get(`/api/system/flags`)
const json = await response.json()
enabled = json.multiTenancy
} catch (err) {
// just let it stay disabled
}
admin.update(store => {
store.multiTenancy = enabled
return store
})
return enabled
}
function unload() {
admin.update(store => {
store.loaded = false
return store
})
}
return {
subscribe: admin.subscribe,
subscribe,
init,
unload,
}
}

110
packages/builder/src/stores/portal/auth.js

@ -1,124 +1,74 @@
import { derived, writable, get } from "svelte/store"
import api from "../../builderStore/api"
import { admin } from "stores/portal"
export function createAuthStore() {
const auth = writable({
user: null,
tenantId: "default",
tenantSet: false,
loaded: false,
})
const store = derived(auth, $store => {
const user = writable(null)
const store = derived(user, $user => {
let initials = null
let isAdmin = false
let isBuilder = false
if ($store.user) {
const user = $store.user
if (user.firstName) {
initials = user.firstName[0]
if (user.lastName) {
initials += user.lastName[0]
if ($user) {
if ($user.firstName) {
initials = $user.firstName[0]
if ($user.lastName) {
initials += $user.lastName[0]
}
} else if (user.email) {
initials = user.email[0]
} else if ($user.email) {
initials = $user.email[0]
} else {
initials = "Unknown"
}
isAdmin = !!user.admin?.global
isBuilder = !!user.builder?.global
isAdmin = !!$user.admin?.global
isBuilder = !!$user.builder?.global
}
return {
user: $store.user,
tenantId: $store.tenantId,
tenantSet: $store.tenantSet,
loaded: $store.loaded,
user: $user,
initials,
isAdmin,
isBuilder,
}
})
function setUser(user) {
auth.update(store => {
store.loaded = true
store.user = user
if (user) {
store.tenantId = user.tenantId || "default"
store.tenantSet = true
}
return store
})
}
async function setOrganisation(tenantId) {
const prevId = get(store).tenantId
auth.update(store => {
store.tenantId = tenantId
store.tenantSet = !!tenantId
return store
})
if (prevId !== tenantId) {
// re-init admin after setting org
await admin.init()
}
}
return {
subscribe: store.subscribe,
checkQueryString: async () => {
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.has("tenantId")) {
const tenantId = urlParams.get("tenantId")
await setOrganisation(tenantId)
}
},
setOrg: async tenantId => {
await setOrganisation(tenantId)
},
checkAuth: async () => {
const response = await api.get("/api/global/users/self")
const response = await api.get("/api/admin/users/self")
if (response.status !== 200) {
setUser(null)
user.set(null)
} else {
const json = await response.json()
setUser(json)
user.set(json)
}
},
login: async creds => {
const tenantId = get(store).tenantId
const response = await api.post(
`/api/global/auth/${tenantId}/login`,
creds
)
const response = await api.post(`/api/admin/auth`, creds)
const json = await response.json()
if (response.status === 200) {
setUser(json.user)
user.set(json.user)
} else {
throw "Invalid credentials"
}
return json
},
logout: async () => {
const response = await api.post(`/api/global/auth/logout`)
const response = await api.post(`/api/admin/auth/logout`)
if (response.status !== 200) {
throw "Unable to create logout"
}
await response.json()
setUser(null)
user.set(null)
},
updateSelf: async fields => {
const newUser = { ...get(auth).user, ...fields }
const response = await api.post("/api/global/users/self", newUser)
const newUser = { ...get(user), ...fields }
const response = await api.post("/api/admin/users/self", newUser)
if (response.status === 200) {
setUser(newUser)
user.set(newUser)
} else {
throw "Unable to update user details"
}
},
forgotPassword: async email => {
const tenantId = get(store).tenantId
const response = await api.post(`/api/global/auth/${tenantId}/reset`, {
const response = await api.post(`/api/admin/auth/reset`, {
email,
})
if (response.status !== 200) {
@ -127,21 +77,17 @@ export function createAuthStore() {
await response.json()
},
resetPassword: async (password, code) => {
const tenantId = get(store).tenantId
const response = await api.post(
`/api/global/auth/${tenantId}/reset/update`,
{
password,
resetCode: code,
}
)
const response = await api.post(`/api/admin/auth/reset/update`, {
password,
resetCode: code,
})
if (response.status !== 200) {
throw "Unable to reset password"
}
await response.json()
},
createUser: async user => {
const response = await api.post(`/api/global/users`, user)
const response = await api.post(`/api/admin/users`, user)
if (response.status !== 200) {
throw "Unable to create user"
}

6
packages/builder/src/stores/portal/email.js

@ -9,11 +9,11 @@ export function createEmailStore() {
templates: {
fetch: async () => {
// fetch the email template definitions
const response = await api.get(`/api/global/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/global/template/email`)
const templatesResponse = await api.get(`/api/admin/template/email`)
const templates = await templatesResponse.json()
store.set({
@ -23,7 +23,7 @@ export function createEmailStore() {
},
save: async template => {
// Save your template config
const response = await api.post(`/api/global/template`, template)
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

10
packages/builder/src/stores/portal/oidc.js

@ -1,6 +1,5 @@
import { writable, get } from "svelte/store"
import { writable } from "svelte/store"
import api from "builderStore/api"
import { auth } from "stores/portal"
const OIDC_CONFIG = {
logo: undefined,
@ -13,13 +12,10 @@ export function createOidcStore() {
const { set, subscribe } = store
async function init() {
const tenantId = get(auth).tenantId
const res = await api.get(
`/api/global/configs/public/oidc?tenantId=${tenantId}`
)
const res = await api.get(`/api/admin/configs/publicOidc`)
const json = await res.json()
if (json.status === 400 || Object.keys(json).length === 0) {
if (json.status === 400) {
set(OIDC_CONFIG)
} else {
// Just use the first config for now. We will be support multiple logins buttons later on.

8
packages/builder/src/stores/portal/organisation.js

@ -1,9 +1,8 @@
import { writable, get } from "svelte/store"
import api from "builderStore/api"
import { auth } from "stores/portal"
const DEFAULT_CONFIG = {
platformUrl: "http://localhost:10000",
platformUrl: "http://localhost:1000",
logoUrl: undefined,
docsUrl: undefined,
company: "Budibase",
@ -16,8 +15,7 @@ export function createOrganisationStore() {
const { subscribe, set } = store
async function init() {
const tenantId = get(auth).tenantId
const res = await api.get(`/api/global/configs/public?tenantId=${tenantId}`)
const res = await api.get(`/api/admin/configs/public`)
const json = await res.json()
if (json.status === 400) {
@ -28,7 +26,7 @@ export function createOrganisationStore() {
}
async function save(config) {
const res = await api.post("/api/global/configs", {
const res = await api.post("/api/admin/configs", {
type: "settings",
config: { ...get(store), ...config },
_rev: get(store)._rev,

12
packages/builder/src/stores/portal/users.js

@ -6,7 +6,7 @@ export function createUsersStore() {
const { subscribe, set } = writable([])
async function init() {
const response = await api.get(`/api/global/users`)
const response = await api.get(`/api/admin/users`)
const json = await response.json()
set(json)
}
@ -23,12 +23,12 @@ export function createUsersStore() {
global: true,
}
}
const response = await api.post(`/api/global/users/invite`, body)
const response = await api.post(`/api/admin/users/invite`, body)
return await response.json()
}
async function acceptInvite(inviteCode, password) {
const response = await api.post("/api/global/users/invite/accept", {
const response = await api.post("/api/admin/users/invite/accept", {
inviteCode,
password,
})
@ -47,20 +47,20 @@ export function createUsersStore() {
if (admin) {
body.admin = { global: true }
}
const response = await api.post("/api/global/users", body)
const response = await api.post("/api/admin/users", body)
await init()
return await response.json()
}
async function del(id) {
const response = await api.delete(`/api/global/users/${id}`)
const response = await api.delete(`/api/admin/users/${id}`)
update(users => users.filter(user => user._id !== id))
return await response.json()
}
async function save(data) {
try {
const res = await post(`/api/global/users`, data)
const res = await post(`/api/admin/users`, data)
return await res.json()
} catch (error) {
console.log(error)

2
packages/cli/package.json

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

8
packages/client/package.json

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

4
packages/client/src/api/auth.js

@ -13,7 +13,7 @@ export const logIn = async ({ email, password }) => {
return API.error("Please enter your password")
}
return await API.post({
url: "/api/global/auth",
url: "/api/admin/auth",
body: { username: email, password },
})
}
@ -23,7 +23,7 @@ export const logIn = async ({ email, password }) => {
*/
export const fetchSelf = async () => {
const user = await API.get({ url: "/api/self" })
if (user && user._id) {
if (user?._id) {
if (user.roleId === "PUBLIC") {
// Don't try to enrich a public user as it will 403
return user

5
packages/client/src/components/ClientApp.svelte

@ -94,7 +94,7 @@
</div>
{:else if $screenStore.activeLayout}
<Provider key="user" data={$authStore} {actions}>
<div id="app-root">
<div id="app-root" class:preview={$builderStore.inBuilder}>
{#key $screenStore.activeLayout._id}
<Component instance={$screenStore.activeLayout.props} />
{/key}
@ -133,6 +133,9 @@
#app-root {
position: relative;
}
#app-root.preview {
border: 1px solid var(--spectrum-global-color-gray-300);
}
/* Custom scrollbars */
:global(::-webkit-scrollbar) {

2
packages/server/__mocks__/node-fetch.ts

@ -16,7 +16,7 @@ module FetchMock {
}
}
if (url.includes("/api/global")) {
if (url.includes("/api/admin")) {
return json({
email: "test@test.com",
_id: "us_test@test.com",

17
packages/server/package.json

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.9.87-alpha.9",
"version": "0.9.95",
"description": "Budibase Web Server",
"main": "src/index.js",
"repository": {
@ -23,9 +23,7 @@
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
"lint": "eslint --fix src/",
"lint:fix": "yarn run format && yarn run lint",
"initialise": "node scripts/initialise.js",
"multi:enable": "node scripts/multiTenancy.js enable",
"multi:disable": "node scripts/multiTenancy.js disable"
"initialise": "node scripts/initialise.js"
},
"jest": {
"preset": "ts-jest",
@ -62,9 +60,9 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.87-alpha.9",
"@budibase/client": "^0.9.87-alpha.9",
"@budibase/string-templates": "^0.9.87-alpha.9",
"@budibase/auth": "^0.9.95",
"@budibase/client": "^0.9.95",
"@budibase/string-templates": "^0.9.95",
"@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",
@ -117,7 +115,7 @@
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@budibase/standard-components": "^0.9.87-alpha.9",
"@budibase/standard-components": "^0.9.95",
"@jest/test-sequencer": "^24.8.0",
"@types/bull": "^3.15.1",
"@types/jest": "^26.0.23",
@ -138,8 +136,7 @@
"supertest": "^4.0.2",
"ts-jest": "^27.0.3",
"ts-node": "^10.0.0",
"typescript": "^4.3.4",
"update-dotenv": "^1.1.1"
"typescript": "^4.3.4"
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}

41
packages/server/scripts/dev/manage.js

@ -33,29 +33,26 @@ async function init() {
fs.writeFileSync(envoyOutputPath, processStringSync(contents, config))
const envFilePath = path.join(process.cwd(), ".env")
if (!fs.existsSync(envFilePath)) {
const envFileJson = {
PORT: 4001,
MINIO_URL: "http://localhost:10000/",
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",
MINIO_SECRET_KEY: "budibase",
COUCH_DB_PASSWORD: "budibase",
COUCH_DB_USER: "budibase",
SELF_HOSTED: 1,
MULTI_TENANCY: "",
}
let envFile = ""
Object.keys(envFileJson).forEach(key => {
envFile += `${key}=${envFileJson[key]}\n`
})
fs.writeFileSync(envFilePath, envFile)
const envFileJson = {
PORT: 4001,
MINIO_URL: "http://localhost:10000/",
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",
MINIO_SECRET_KEY: "budibase",
COUCH_DB_PASSWORD: "budibase",
COUCH_DB_USER: "budibase",
SELF_HOSTED: 1,
}
let envFile = ""
Object.keys(envFileJson).forEach(key => {
envFile += `${key}=${envFileJson[key]}\n`
})
fs.writeFileSync(envFilePath, envFile)
}
async function up() {

11
packages/server/scripts/integrations/postgres/init.sql

@ -10,11 +10,10 @@ CREATE TABLE Persons (
CREATE TABLE Tasks (
TaskID SERIAL PRIMARY KEY,
PersonID INT,
Completed BOOLEAN,
TaskName varchar(255),
CONSTRAINT fkPersons
FOREIGN KEY(PersonID)
REFERENCES Persons(PersonID)
REFERENCES Persons(PersonID)
);
CREATE TABLE Products (
ProductID SERIAL PRIMARY KEY,
@ -25,15 +24,15 @@ CREATE TABLE Products_Tasks (
TaskID INT NOT NULL,
CONSTRAINT fkProducts
FOREIGN KEY(ProductID)
REFERENCES Products(ProductID),
REFERENCES Products(ProductID),
CONSTRAINT fkTasks
FOREIGN KEY(TaskID)
REFERENCES Tasks(TaskID),
REFERENCES Tasks(TaskID),
PRIMARY KEY (ProductID, TaskID)
);
INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast');
INSERT INTO Tasks (PersonID, TaskName, Completed) VALUES (1, 'assembling', TRUE);
INSERT INTO Tasks (PersonID, TaskName, Completed) VALUES (1, 'processing', FALSE);
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling');
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing');
INSERT INTO Products (ProductName) VALUES ('Computers');
INSERT INTO Products (ProductName) VALUES ('Laptops');
INSERT INTO Products (ProductName) VALUES ('Chairs');

8
packages/server/scripts/multiTenancy.js

@ -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!"))

30
packages/server/src/api/controllers/apikeys.js

@ -1,30 +1,8 @@
const { StaticDatabases } = require("@budibase/auth/db")
const { getGlobalDB } = require("@budibase/auth/tenancy")
const KEYS_DOC = StaticDatabases.GLOBAL.docs.apiKeys
async function getBuilderMainDoc() {
const db = getGlobalDB()
try {
return await db.get(KEYS_DOC)
} catch (err) {
// doesn't exist yet, nothing to get
return {
_id: KEYS_DOC,
}
}
}
async function setBuilderMainDoc(doc) {
// make sure to override the ID
doc._id = KEYS_DOC
const db = getGlobalDB()
return db.put(doc)
}
const builderDB = require("../../db/builder")
exports.fetch = async function (ctx) {
try {
const mainDoc = await getBuilderMainDoc()
const mainDoc = await builderDB.getBuilderMainDoc()
ctx.body = mainDoc.apiKeys ? mainDoc.apiKeys : {}
} catch (err) {
/* istanbul ignore next */
@ -37,12 +15,12 @@ exports.update = async function (ctx) {
const value = ctx.request.body.value
try {
const mainDoc = await getBuilderMainDoc()
const mainDoc = await builderDB.getBuilderMainDoc()
if (mainDoc.apiKeys == null) {
mainDoc.apiKeys = {}
}
mainDoc.apiKeys[key] = value
const resp = await setBuilderMainDoc(mainDoc)
const resp = await builderDB.setBuilderMainDoc(mainDoc)
ctx.body = {
_id: resp.id,
_rev: resp.rev,

14
packages/server/src/api/controllers/application.js

@ -25,7 +25,7 @@ const { BASE_LAYOUTS } = require("../../constants/layouts")
const { createHomeScreen } = require("../../constants/screens")
const { cloneDeep } = require("lodash/fp")
const { processObject } = require("@budibase/string-templates")
const { getAllApps } = require("@budibase/auth/db")
const { getAllApps } = require("../../utilities")
const { USERS_TABLE_SCHEMA } = require("../../constants")
const {
getDeployedApps,
@ -38,7 +38,6 @@ const {
backupClientLibrary,
revertClientLibrary,
} = require("../../utilities/fileSystem/clientLibrary")
const { getTenantId, isMultiTenant } = require("@budibase/auth/tenancy")
const URL_REGEX_SLASH = /\/|\\/g
@ -94,8 +93,7 @@ async function getAppUrlIfNotInUse(ctx) {
}
async function createInstance(template) {
const tenantId = isMultiTenant() ? getTenantId() : null
const baseAppId = generateAppID(tenantId)
const baseAppId = generateAppID()
const appId = generateDevAppID(baseAppId)
const db = new CouchDB(appId)
@ -130,7 +128,7 @@ async function createInstance(template) {
exports.fetch = async function (ctx) {
const dev = ctx.query && ctx.query.status === AppStatus.DEV
const all = ctx.query && ctx.query.status === AppStatus.ALL
const apps = await getAllApps(CouchDB, { dev, all })
const apps = await getAllApps({ CouchDB, dev, all })
// get the locks for all the dev apps
if (dev || all) {
@ -222,12 +220,10 @@ exports.create = async function (ctx) {
url: url,
template: ctx.request.body.template,
instance: instance,
tenantId: getTenantId(),
updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
}
const response = await db.put(newApplication, { force: true })
newApplication._rev = response.rev
await db.put(newApplication, { force: true })
await createEmptyAppPackage(ctx, newApplication)
/* istanbul ignore next */
@ -299,7 +295,7 @@ exports.delete = async function (ctx) {
await deleteApp(ctx.params.appId)
}
// make sure the app/role doesn't stick around after the app has been deleted
await removeAppFromUserRoles(ctx, ctx.params.appId)
await removeAppFromUserRoles(ctx.params.appId)
ctx.status = 200
ctx.body = result

2
packages/server/src/api/controllers/auth.js

@ -22,7 +22,7 @@ exports.fetchSelf = async ctx => {
const userTable = await db.get(InternalTables.USER_METADATA)
const metadata = await db.get(userId)
// specifically needs to make sure is enriched
ctx.body = await outputProcessing(ctx, userTable, {
ctx.body = await outputProcessing(appId, userTable, {
...user,
...metadata,
})

23
packages/server/src/api/controllers/deploy/index.js

@ -1,6 +1,6 @@
const CouchDB = require("../../../db")
const PouchDB = require("../../../db")
const Deployment = require("./Deployment")
const { Replication } = require("@budibase/auth/db")
const { Replication, StaticDatabases } = require("@budibase/auth/db")
const { DocumentTypes } = require("../../../db/utils")
// the max time we can wait for an invalidation to complete before considering it failed
@ -31,14 +31,13 @@ async function checkAllDeployments(deployments) {
async function storeDeploymentHistory(deployment) {
const appId = deployment.getAppId()
const deploymentJSON = deployment.getJSON()
const db = new CouchDB(appId)
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
let deploymentDoc
try {
// theres only one deployment doc per app database
deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
deploymentDoc = await db.get(appId)
} catch (err) {
deploymentDoc = { _id: DocumentTypes.DEPLOYMENTS, history: {} }
deploymentDoc = { _id: appId, history: {} }
}
const deploymentId = deploymentJSON._id
@ -68,7 +67,7 @@ async function deployApp(deployment) {
})
await replication.replicate()
const db = new CouchDB(productionAppId)
const db = new PouchDB(productionAppId)
const appDoc = await db.get(DocumentTypes.APP_METADATA)
appDoc.appId = productionAppId
appDoc.instance._id = productionAppId
@ -99,9 +98,8 @@ async function deployApp(deployment) {
exports.fetchDeployments = async function (ctx) {
try {
const appId = ctx.appId
const db = new CouchDB(appId)
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
const deploymentDoc = await db.get(ctx.appId)
const { updated, deployments } = await checkAllDeployments(
deploymentDoc,
ctx.user
@ -117,9 +115,8 @@ exports.fetchDeployments = async function (ctx) {
exports.deploymentProgress = async function (ctx) {
try {
const appId = ctx.appId
const db = new CouchDB(appId)
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
const deploymentDoc = await db.get(ctx.appId)
ctx.body = deploymentDoc[ctx.params.deploymentId]
} catch (err) {
ctx.throw(

3
packages/server/src/api/controllers/dev.js

@ -9,9 +9,8 @@ const { DocumentTypes } = require("../../db/utils")
async function redirect(ctx, method) {
const { devPath } = ctx.params
const queryString = ctx.originalUrl.split("?")[1] || ""
const response = await fetch(
checkSlashesInUrl(`${env.WORKER_URL}/api/global/${devPath}?${queryString}`),
checkSlashesInUrl(`${env.WORKER_URL}/api/admin/${devPath}`),
request(
ctx,
{

10
packages/server/src/api/controllers/row/internal.js

@ -161,7 +161,7 @@ exports.fetchView = async ctx => {
schema: {},
}
}
rows = await outputProcessing(ctx, table, response.rows)
rows = await outputProcessing(appId, table, response.rows)
}
if (calculation === CALCULATION_TYPES.STATS) {
@ -204,7 +204,7 @@ exports.fetch = async ctx => {
)
rows = response.rows.map(row => row.doc)
}
return outputProcessing(ctx, table, rows)
return outputProcessing(appId, table, rows)
}
exports.find = async ctx => {
@ -212,7 +212,7 @@ exports.find = async ctx => {
const db = new CouchDB(appId)
const table = await db.get(ctx.params.tableId)
let row = await findRow(ctx, db, ctx.params.tableId, ctx.params.rowId)
row = await outputProcessing(ctx, table, row)
row = await outputProcessing(appId, table, row)
return row
}
@ -291,7 +291,7 @@ exports.search = async ctx => {
// Enrich search results with relationships
if (response.rows && response.rows.length) {
const table = await db.get(tableId)
response.rows = await outputProcessing(ctx, table, response.rows)
response.rows = await outputProcessing(appId, table, response.rows)
}
return response
@ -328,7 +328,7 @@ exports.fetchEnrichedRow = async ctx => {
})
// need to include the IDs in these rows for any links they may have
let linkedRows = await outputProcessing(
ctx,
appId,
table,
response.rows.map(row => row.doc)
)

2
packages/server/src/api/controllers/user.js

@ -17,7 +17,7 @@ function removeGlobalProps(user) {
exports.fetchMetadata = async function (ctx) {
const database = new CouchDB(ctx.appId)
const global = await getGlobalUsers(ctx, ctx.appId)
const global = await getGlobalUsers(ctx.appId)
const metadata = (
await database.allDocs(
getUserMetadataParams(null, {

12
packages/server/src/api/index.js

@ -1,6 +1,5 @@
const Router = require("@koa/router")
const { buildAuthMiddleware, auditLog, buildTenancyMiddleware } =
require("@budibase/auth").auth
const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth
const currentApp = require("../middleware/currentapp")
const compress = require("koa-compress")
const zlib = require("zlib")
@ -10,13 +9,6 @@ const env = require("../environment")
const router = new Router()
const NO_TENANCY_ENDPOINTS = [
{
route: "/api/analytics",
method: "GET",
},
]
router
.use(
compress({
@ -44,8 +36,6 @@ router
publicAllowed: true,
})
)
// nothing in the server should allow query string tenants
.use(buildTenancyMiddleware(null, NO_TENANCY_ENDPOINTS))
.use(currentApp)
.use(auditLog)

2
packages/server/src/api/routes/application.js

@ -6,11 +6,11 @@ const { BUILDER } = require("@budibase/auth/permissions")
const router = Router()
router
.post("/api/applications", authorized(BUILDER), controller.create)
.get("/api/applications/:appId/definition", controller.fetchAppDefinition)
.get("/api/applications", controller.fetch)
.get("/api/applications/:appId/appPackage", controller.fetchAppPackage)
.put("/api/applications/:appId", authorized(BUILDER), controller.update)
.post("/api/applications", authorized(BUILDER), controller.create)
.post(
"/api/applications/:appId/client/update",
authorized(BUILDER),

6
packages/server/src/api/routes/dev.js

@ -8,9 +8,9 @@ const router = Router()
if (env.isDev() || env.isTest()) {
router
.get("/api/global/:devPath(.*)", controller.redirectGet)
.post("/api/global/:devPath(.*)", controller.redirectPost)
.delete("/api/global/:devPath(.*)", controller.redirectDelete)
.get("/api/admin/:devPath(.*)", controller.redirectGet)
.post("/api/admin/:devPath(.*)", controller.redirectPost)
.delete("/api/admin/:devPath(.*)", controller.redirectDelete)
}
router

2
packages/server/src/api/routes/tests/row.spec.js

@ -387,7 +387,7 @@ describe("/rows", () => {
})
// the environment needs configured for this
await setup.switchToSelfHosted(async () => {
const enriched = await outputProcessing({ appId: config.getAppId() }, table, [row])
const enriched = await outputProcessing(config.getAppId(), table, [row])
expect(enriched[0].attachment[0].url).toBe(
`/prod-budi-app-assets/${config.getAppId()}/attachments/test/thing.csv`
)

5
packages/server/src/api/routes/tests/utilities/TestFunctions.js

@ -3,7 +3,6 @@ const appController = require("../../../controllers/application")
const CouchDB = require("../../../../db")
const { AppStatus } = require("../../../../db/utils")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { TENANT_ID } = require("../../../../tests/utilities/structures")
function Request(appId, params) {
this.appId = appId
@ -17,8 +16,8 @@ exports.getAllTableRows = async config => {
return req.body
}
exports.clearAllApps = async (tenantId = TENANT_ID) => {
const req = { query: { status: AppStatus.DEV }, user: { tenantId } }
exports.clearAllApps = async () => {
const req = { query: { status: AppStatus.DEV } }
await appController.fetch(req)
const apps = req.body
if (!apps || apps.length <= 0) {

2
packages/server/src/automations/steps/serverLog.js

@ -19,7 +19,7 @@ module.exports.definition = {
properties: {
text: {
type: "string",
title: "Log",
title: "URL",
},
},
required: ["text"],

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save