Merge branch 'next' of github.com:Budibase/budibase into user-app-list

This commit is contained in:
Andrew Kingston 2021-05-18 12:33:06 +01:00
commit 0251ee366f
154 changed files with 2291 additions and 2558 deletions

View File

@ -1 +1,12 @@
# Budibase Authentication Library # Budibase Core backend library
This library contains core functionality, like auth and security features
which are shared between backend services.
#### Note about top level JS files
For the purposes of being able to do say `require("@budibase/auth/permissions")` we need to
specify the exports at the top-level of the module.
For these files they should be limited to a single `require` of the file that should
be exported and then a single `module.exports = ...` to export the file in
commonJS.

1
packages/auth/db.js Normal file
View File

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

View File

@ -11,6 +11,7 @@
"ioredis": "^4.27.1", "ioredis": "^4.27.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa-passport": "^4.1.4", "koa-passport": "^4.1.4",
"lodash": "^4.17.21",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"passport-google-auth": "^1.0.2", "passport-google-auth": "^1.0.2",
"passport-google-oauth": "^2.0.0", "passport-google-oauth": "^2.0.0",

View File

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

4
packages/auth/redis.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
Client: require("./src/redis"),
utils: require("./src/redis/utils"),
}

1
packages/auth/roles.js Normal file
View File

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

View File

@ -14,3 +14,10 @@ exports.GlobalRoles = {
BUILDER: "builder", BUILDER: "builder",
GROUP_MANAGER: "group_manager", GROUP_MANAGER: "group_manager",
} }
exports.Configs = {
SETTINGS: "settings",
ACCOUNT: "account",
SMTP: "smtp",
GOOGLE: "google",
}

View File

@ -0,0 +1,79 @@
const { getDB } = require(".")
class Replication {
/**
*
* @param {String} source - the DB you want to replicate or rollback to
* @param {String} target - the DB you want to replicate to, or rollback from
*/
constructor({ source, target }) {
this.source = getDB(source)
this.target = getDB(target)
}
promisify(operation, opts = {}) {
return new Promise(resolve => {
operation(this.target, opts)
.on("denied", function (err) {
// a document failed to replicate (e.g. due to permissions)
throw new Error(`Denied: Document failed to replicate ${err}`)
})
.on("complete", function (info) {
return resolve(info)
})
.on("error", function (err) {
throw new Error(`Replication Error: ${err}`)
})
})
}
/**
* Two way replication operation, intended to be promise based.
* @param {Object} opts - PouchDB replication options
*/
sync(opts = {}) {
this.replication = this.promisify(this.source.sync, opts)
return this.replication
}
/**
* One way replication operation, intended to be promise based.
* @param {Object} opts - PouchDB replication options
*/
replicate(opts = {}) {
this.replication = this.promisify(this.source.replicate.to, opts)
return this.replication
}
/**
* Set up an ongoing live sync between 2 CouchDB databases.
* @param {Object} opts - PouchDB replication options
*/
subscribe(opts = {}) {
this.replication = this.source.replicate
.to(this.target, {
live: true,
retry: true,
...opts,
})
.on("error", function (err) {
throw new Error(`Replication Error: ${err}`)
})
}
/**
* Rollback the target DB back to the state of the source DB
*/
async rollback() {
await this.target.destroy()
// Recreate the DB again
this.target = getDB(this.target.name)
await this.replicate()
}
cancel() {
this.replication.cancel()
}
}
module.exports = Replication

View File

@ -7,3 +7,7 @@ module.exports.setDB = pouch => {
module.exports.getDB = dbName => { module.exports.getDB = dbName => {
return new Pouch(dbName) return new Pouch(dbName)
} }
module.exports.getCouch = () => {
return Pouch
}

View File

@ -1,4 +1,9 @@
const { newid } = require("../hashing") const { newid } = require("../hashing")
const Replication = require("./Replication")
const { getCouch } = require("./index")
const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_"
exports.ViewNames = { exports.ViewNames = {
USER_BY_EMAIL: "by_email", USER_BY_EMAIL: "by_email",
@ -8,23 +13,50 @@ exports.StaticDatabases = {
GLOBAL: { GLOBAL: {
name: "global-db", name: "global-db",
}, },
DEPLOYMENTS: {
name: "deployments",
},
} }
const DocumentTypes = { const DocumentTypes = {
USER: "us", USER: "us",
APP: "app",
GROUP: "group", GROUP: "group",
CONFIG: "config", CONFIG: "config",
TEMPLATE: "template", TEMPLATE: "template",
APP: "app",
APP_DEV: "app_dev",
APP_METADATA: "app_metadata",
ROLE: "role",
} }
exports.DocumentTypes = DocumentTypes exports.DocumentTypes = DocumentTypes
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
const UNICODE_MAX = "\ufff0" exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
const SEPARATOR = "_"
exports.SEPARATOR = SEPARATOR exports.SEPARATOR = SEPARATOR
/**
* If creating DB allDocs/query params with only a single top level ID this can be used, this
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
* More complex cases such as link docs and rows which have multiple levels of IDs that their
* ID consists of need their own functions to build the allDocs parameters.
* @param {string} docType The type of document which input params are being built for, e.g. user,
* link, app, table and so on.
* @param {string|null} docId The ID of the document minus its type - this is only needed if looking
* for a singular document.
* @param {object} otherProps Add any other properties onto the request, e.g. include_docs.
* @returns {object} Parameters which can then be used with an allDocs request.
*/
function getDocParams(docType, docId = null, otherProps = {}) {
if (docId == null) {
docId = ""
}
return {
...otherProps,
startkey: `${docType}${SEPARATOR}${docId}`,
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
}
}
/** /**
* Generates a new group ID. * Generates a new group ID.
* @returns {string} The new group ID which the group doc can be stored under. * @returns {string} The new group ID which the group doc can be stored under.
@ -94,6 +126,65 @@ exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => {
} }
} }
/**
* Generates a new role ID.
* @returns {string} The new role ID which the role doc can be stored under.
*/
exports.generateRoleID = id => {
return `${DocumentTypes.ROLE}${SEPARATOR}${id || newid()}`
}
/**
* Gets parameters for retrieving a role, this is a utility function for the getDocParams function.
*/
exports.getRoleParams = (roleId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.ROLE, roleId, otherProps)
}
/**
* Convert a development app ID to a deployed app ID.
*/
exports.getDeployedAppID = appId => {
// if dev, convert it
if (appId.startsWith(exports.APP_DEV_PREFIX)) {
const id = appId.split(exports.APP_DEV_PREFIX)[1]
return `${exports.APP_PREFIX}${id}`
}
return appId
}
/**
* Lots of different points in the system need to find the full list of apps, this will
* enumerate the entire CouchDB cluster and get the list of databases (every app).
* NOTE: this operation is fine in self hosting, but cannot be used when hosting many
* 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 (devApps = false) => {
const CouchDB = getCouch()
let allDbs = await CouchDB.allDbs()
const appDbNames = allDbs.filter(dbName =>
dbName.startsWith(exports.APP_PREFIX)
)
const appPromises = appDbNames.map(db =>
new CouchDB(db).get(DocumentTypes.APP_METADATA)
)
if (appPromises.length === 0) {
return []
} else {
const response = await Promise.allSettled(appPromises)
const apps = response
.filter(result => result.status === "fulfilled")
.map(({ value }) => value)
return apps.filter(app => {
if (devApps) {
return app.appId.startsWith(exports.APP_DEV_PREFIX)
}
return !app.appId.startsWith(exports.APP_DEV_PREFIX)
})
}
}
/** /**
* Generates a new configuration ID. * Generates a new configuration ID.
* @returns {string} The new configuration ID which the config doc can be stored under. * @returns {string} The new configuration ID which the config doc can be stored under.
@ -165,6 +256,7 @@ async function getScopedConfig(db, params) {
return configDoc && configDoc.config ? configDoc.config : configDoc return configDoc && configDoc.config ? configDoc.config : configDoc
} }
exports.Replication = Replication
exports.getScopedConfig = getScopedConfig exports.getScopedConfig = getScopedConfig
exports.generateConfigID = generateConfigID exports.generateConfigID = generateConfigID
exports.getConfigParams = getConfigParams exports.getConfigParams = getConfigParams

View File

@ -10,6 +10,7 @@ const fs = require("fs")
const env = require("../environment") const env = require("../environment")
const { budibaseTempDir, ObjectStoreBuckets } = require("./utils") const { budibaseTempDir, ObjectStoreBuckets } = require("./utils")
const { v4 } = require("uuid") const { v4 } = require("uuid")
const { APP_PREFIX, APP_DEV_PREFIX } = require("../db/utils")
const streamPipeline = promisify(stream.pipeline) const streamPipeline = promisify(stream.pipeline)
// use this as a temporary store of buckets that are being created // use this as a temporary store of buckets that are being created
@ -28,6 +29,16 @@ const STRING_CONTENT_TYPES = [
CONTENT_TYPE_MAP.js, CONTENT_TYPE_MAP.js,
] ]
// does normal sanitization and then swaps dev apps to apps
function sanitizeKey(input) {
return sanitize(sanitizeBucket(input)).replace(/\\/g, "/")
}
// simply handles the dev app to app conversion
function sanitizeBucket(input) {
return input.replace(new RegExp(APP_DEV_PREFIX, "g"), APP_PREFIX)
}
function publicPolicy(bucketName) { function publicPolicy(bucketName) {
return { return {
Version: "2012-10-17", Version: "2012-10-17",
@ -61,7 +72,7 @@ exports.ObjectStore = bucket => {
s3ForcePathStyle: true, s3ForcePathStyle: true,
signatureVersion: "v4", signatureVersion: "v4",
params: { params: {
Bucket: bucket, Bucket: sanitizeBucket(bucket),
}, },
} }
if (env.MINIO_URL) { if (env.MINIO_URL) {
@ -75,6 +86,7 @@ exports.ObjectStore = bucket => {
* if it does not exist then it will create it. * if it does not exist then it will create it.
*/ */
exports.makeSureBucketExists = async (client, bucketName) => { exports.makeSureBucketExists = async (client, bucketName) => {
bucketName = sanitizeBucket(bucketName)
try { try {
await client await client
.headBucket({ .headBucket({
@ -114,16 +126,22 @@ exports.makeSureBucketExists = async (client, bucketName) => {
* Uploads the contents of a file given the required parameters, useful when * Uploads the contents of a file given the required parameters, useful when
* temp files in use (for example file uploaded as an attachment). * temp files in use (for example file uploaded as an attachment).
*/ */
exports.upload = async ({ bucket, filename, path, type, metadata }) => { exports.upload = async ({
bucket: bucketName,
filename,
path,
type,
metadata,
}) => {
const extension = [...filename.split(".")].pop() const extension = [...filename.split(".")].pop()
const fileBytes = fs.readFileSync(path) const fileBytes = fs.readFileSync(path)
const objectStore = exports.ObjectStore(bucket) const objectStore = exports.ObjectStore(bucketName)
await exports.makeSureBucketExists(objectStore, bucket) await exports.makeSureBucketExists(objectStore, bucketName)
const config = { const config = {
// windows file paths need to be converted to forward slashes for s3 // windows file paths need to be converted to forward slashes for s3
Key: sanitize(filename).replace(/\\/g, "/"), Key: sanitizeKey(filename),
Body: fileBytes, Body: fileBytes,
ContentType: type || CONTENT_TYPE_MAP[extension.toLowerCase()], ContentType: type || CONTENT_TYPE_MAP[extension.toLowerCase()],
} }
@ -137,13 +155,13 @@ exports.upload = async ({ bucket, filename, path, type, metadata }) => {
* Similar to the upload function but can be used to send a file stream * Similar to the upload function but can be used to send a file stream
* through to the object store. * through to the object store.
*/ */
exports.streamUpload = async (bucket, filename, stream) => { exports.streamUpload = async (bucketName, filename, stream) => {
const objectStore = exports.ObjectStore(bucket) const objectStore = exports.ObjectStore(bucketName)
await exports.makeSureBucketExists(objectStore, bucket) await exports.makeSureBucketExists(objectStore, bucketName)
const params = { const params = {
Bucket: bucket, Bucket: sanitizeBucket(bucketName),
Key: sanitize(filename).replace(/\\/g, "/"), Key: sanitizeKey(filename),
Body: stream, Body: stream,
} }
return objectStore.upload(params).promise() return objectStore.upload(params).promise()
@ -153,11 +171,11 @@ exports.streamUpload = async (bucket, filename, stream) => {
* retrieves the contents of a file from the object store, if it is a known content type it * retrieves the contents of a file from the object store, if it is a known content type it
* will be converted, otherwise it will be returned as a buffer stream. * will be converted, otherwise it will be returned as a buffer stream.
*/ */
exports.retrieve = async (bucket, filepath) => { exports.retrieve = async (bucketName, filepath) => {
const objectStore = exports.ObjectStore(bucket) const objectStore = exports.ObjectStore(bucketName)
const params = { const params = {
Bucket: bucket, Bucket: sanitizeBucket(bucketName),
Key: sanitize(filepath).replace(/\\/g, "/"), Key: sanitizeKey(filepath),
} }
const response = await objectStore.getObject(params).promise() const response = await objectStore.getObject(params).promise()
// currently these are all strings // currently these are all strings
@ -171,17 +189,21 @@ exports.retrieve = async (bucket, filepath) => {
/** /**
* Same as retrieval function but puts to a temporary file. * Same as retrieval function but puts to a temporary file.
*/ */
exports.retrieveToTmp = async (bucket, filepath) => { exports.retrieveToTmp = async (bucketName, filepath) => {
const data = await exports.retrieve(bucket, filepath) bucketName = sanitizeBucket(bucketName)
filepath = sanitizeKey(filepath)
const data = await exports.retrieve(bucketName, filepath)
const outputPath = join(budibaseTempDir(), v4()) const outputPath = join(budibaseTempDir(), v4())
fs.writeFileSync(outputPath, data) fs.writeFileSync(outputPath, data)
return outputPath return outputPath
} }
exports.deleteFolder = async (bucket, folder) => { exports.deleteFolder = async (bucketName, folder) => {
const client = exports.ObjectStore(bucket) bucketName = sanitizeBucket(bucketName)
folder = sanitizeKey(folder)
const client = exports.ObjectStore(bucketName)
const listParams = { const listParams = {
Bucket: bucket, Bucket: bucketName,
Prefix: folder, Prefix: folder,
} }
@ -190,7 +212,7 @@ exports.deleteFolder = async (bucket, folder) => {
return return
} }
const deleteParams = { const deleteParams = {
Bucket: bucket, Bucket: bucketName,
Delete: { Delete: {
Objects: [], Objects: [],
}, },
@ -203,28 +225,31 @@ exports.deleteFolder = async (bucket, folder) => {
response = await client.deleteObjects(deleteParams).promise() response = await client.deleteObjects(deleteParams).promise()
// can only empty 1000 items at once // can only empty 1000 items at once
if (response.Deleted.length === 1000) { if (response.Deleted.length === 1000) {
return exports.deleteFolder(bucket, folder) return exports.deleteFolder(bucketName, folder)
} }
} }
exports.uploadDirectory = async (bucket, localPath, bucketPath) => { exports.uploadDirectory = async (bucketName, localPath, bucketPath) => {
bucketName = sanitizeBucket(bucketName)
let uploads = [] let uploads = []
const files = fs.readdirSync(localPath, { withFileTypes: true }) const files = fs.readdirSync(localPath, { withFileTypes: true })
for (let file of files) { for (let file of files) {
const path = join(bucketPath, file.name) const path = sanitizeKey(join(bucketPath, file.name))
const local = join(localPath, file.name) const local = join(localPath, file.name)
if (file.isDirectory()) { if (file.isDirectory()) {
uploads.push(exports.uploadDirectory(bucket, local, path)) uploads.push(exports.uploadDirectory(bucketName, local, path))
} else { } else {
uploads.push( uploads.push(
exports.streamUpload(bucket, path, fs.createReadStream(local)) exports.streamUpload(bucketName, path, fs.createReadStream(local))
) )
} }
} }
await Promise.all(uploads) await Promise.all(uploads)
} }
exports.downloadTarball = async (url, bucket, path) => { exports.downloadTarball = async (url, bucketName, path) => {
bucketName = sanitizeBucket(bucketName)
path = sanitizeKey(path)
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) { if (!response.ok) {
throw new Error(`unexpected response ${response.statusText}`) throw new Error(`unexpected response ${response.statusText}`)
@ -233,7 +258,7 @@ exports.downloadTarball = async (url, bucket, path) => {
const tmpPath = join(budibaseTempDir(), path) const tmpPath = join(budibaseTempDir(), path)
await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath)) await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath))
if (!env.isTest()) { if (!env.isTest()) {
await exports.uploadDirectory(bucket, tmpPath, path) await exports.uploadDirectory(bucketName, tmpPath, path)
} }
// return the temporary path incase there is a use for it // return the temporary path incase there is a use for it
return tmpPath return tmpPath

View File

@ -143,9 +143,8 @@ class RedisWrapper {
} }
async clear() { async clear() {
const db = this._db let items = await this.scan()
let items = await this.scan(db) await Promise.all(items.map(obj => this.delete(obj.key)))
await Promise.all(items.map(obj => this.delete(db, obj.key)))
} }
} }

View File

@ -9,6 +9,7 @@ const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD
exports.Databases = { exports.Databases = {
PW_RESETS: "pwReset", PW_RESETS: "pwReset",
INVITATIONS: "invitation", INVITATIONS: "invitation",
DEV_LOCKS: "devLocks",
} }
exports.getRedisOptions = (clustered = false) => { exports.getRedisOptions = (clustered = false) => {
@ -31,6 +32,9 @@ exports.getRedisOptions = (clustered = false) => {
} }
exports.addDbPrefix = (db, key) => { exports.addDbPrefix = (db, key) => {
if (key.includes(db)) {
return key
}
return `${db}${SEPARATOR}${key}` return `${db}${SEPARATOR}${key}`
} }

View File

@ -1,7 +1,12 @@
const CouchDB = require("../../db") const { getDB } = require("../db")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
const { BUILTIN_PERMISSION_IDS, higherPermission } = require("./permissions") const { BUILTIN_PERMISSION_IDS, higherPermission } = require("./permissions")
const { generateRoleID, DocumentTypes, SEPARATOR } = require("../../db/utils") const {
generateRoleID,
getRoleParams,
DocumentTypes,
SEPARATOR,
} = require("../db/utils")
const BUILTIN_IDS = { const BUILTIN_IDS = {
ADMIN: "ADMIN", ADMIN: "ADMIN",
@ -11,6 +16,14 @@ const BUILTIN_IDS = {
BUILDER: "BUILDER", BUILDER: "BUILDER",
} }
// exclude internal roles like builder
const EXTERNAL_BUILTIN_ROLE_IDS = [
BUILTIN_IDS.ADMIN,
BUILTIN_IDS.POWER,
BUILTIN_IDS.BASIC,
BUILTIN_IDS.PUBLIC,
]
function Role(id, name) { function Role(id, name) {
this._id = id this._id = id
this.name = name this.name = name
@ -116,7 +129,7 @@ exports.getRole = async (appId, roleId) => {
) )
} }
try { try {
const db = new CouchDB(appId) const db = getDB(appId)
const dbRole = await db.get(exports.getDBRoleID(roleId)) const dbRole = await db.get(exports.getDBRoleID(roleId))
role = Object.assign(role, dbRole) role = Object.assign(role, dbRole)
// finalise the ID // finalise the ID
@ -192,6 +205,39 @@ exports.getUserPermissions = async (appId, userRoleId) => {
} }
} }
/**
* Given an app ID this will retrieve all of the roles that are currently within that app.
* @param {string} appId The ID of the app to retrieve the roles from.
* @return {Promise<object[]>} An array of the role objects that were found.
*/
exports.getAllRoles = async appId => {
const db = getDB(appId)
const body = await db.allDocs(
getRoleParams(null, {
include_docs: true,
})
)
let roles = body.rows.map(row => row.doc)
const builtinRoles = exports.getBuiltinRoles()
// need to combine builtin with any DB record of them (for sake of permissions)
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
const builtinRole = builtinRoles[builtinRoleId]
const dbBuiltin = roles.filter(
dbRole => exports.getExternalRoleID(dbRole._id) === builtinRoleId
)[0]
if (dbBuiltin == null) {
roles.push(builtinRole)
} else {
// remove role and all back after combining with the builtin
roles = roles.filter(role => role._id !== dbBuiltin._id)
dbBuiltin._id = exports.getExternalRoleID(dbBuiltin._id)
roles.push(Object.assign(builtinRole, dbBuiltin))
}
}
return roles
}
class AccessController { class AccessController {
constructor(appId) { constructor(appId) {
this.appId = appId this.appId = appId

View File

@ -16,7 +16,10 @@
function getInitials(name) { function getInitials(name) {
let parts = name.split(" ") let parts = name.split(" ")
return parts.map(name => name[0]).join("") if (parts.length > 0) {
return parts.map(name => name[0]).join("")
}
return name
} }
</script> </script>

View File

@ -11,6 +11,7 @@
</script> </script>
<a <a
on:click
{href} {href}
{target} {target}
class:spectrum-Link--primary={primary} class:spectrum-Link--primary={primary}

View File

@ -282,6 +282,7 @@
{#if sortedRows?.length && fields.length} {#if sortedRows?.length && fields.length}
{#each sortedRows as row, idx} {#each sortedRows as row, idx}
<tr <tr
on:click={() => dispatch("click", row)}
on:click={() => toggleSelectRow(row)} on:click={() => toggleSelectRow(row)}
class="spectrum-Table-row" class="spectrum-Table-row"
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow} class:hidden={idx < firstVisibleRow || idx > lastVisibleRow}

View File

@ -1,6 +1,7 @@
<script> <script>
export let selected = false export let selected = false
export let open = false export let open = false
export let href = false
export let title export let title
export let icon export let icon
</script> </script>
@ -10,7 +11,7 @@
class:is-open={open} class:is-open={open}
class="spectrum-TreeView-item" class="spectrum-TreeView-item"
> >
<a on:click class="spectrum-TreeView-itemLink" href="#"> <a on:click class="spectrum-TreeView-itemLink" {href}>
{#if $$slots.default} {#if $$slots.default}
<svg <svg
class="spectrum-Icon spectrum-UIIcon-ChevronRight100 spectrum-TreeView-itemIndicator" class="spectrum-Icon spectrum-UIIcon-ChevronRight100 spectrum-TreeView-itemIndicator"

View File

@ -4,16 +4,17 @@
import { routes } from "../.routify/routes" import { routes } from "../.routify/routes"
import { initialise } from "builderStore" import { initialise } from "builderStore"
import { NotificationDisplay } from "@budibase/bbui" import { NotificationDisplay } from "@budibase/bbui"
import { parse, stringify } from "qs"
onMount(async () => { onMount(async () => {
await initialise() await initialise()
}) })
const config = {} const queryHandler = { parse, stringify }
</script> </script>
<NotificationDisplay /> <NotificationDisplay />
<Router {routes} {config} /> <Router {routes} config={{ queryHandler }} />
<div class="modal-container" /> <div class="modal-container" />
<style> <style>

View File

@ -51,14 +51,14 @@ export const getFrontendStore = () => {
store.actions = { store.actions = {
initialise: async pkg => { initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg const { layouts, screens, application, clientLibPath } = pkg
const components = await fetchComponentLibDefinitions(application._id) const components = await fetchComponentLibDefinitions(application.appId)
store.update(state => ({ store.update(state => ({
...state, ...state,
libraries: application.componentLibraries, libraries: application.componentLibraries,
components, components,
name: application.name, name: application.name,
description: application.description, description: application.description,
appId: application._id, appId: application.appId,
url: application.url, url: application.url,
layouts, layouts,
screens, screens,

View File

@ -2,7 +2,6 @@ import { writable } from "svelte/store"
import api, { get } from "../api" import api, { get } from "../api"
const INITIAL_HOSTING_UI_STATE = { const INITIAL_HOSTING_UI_STATE = {
hostingInfo: {},
appUrl: "", appUrl: "",
deployedApps: {}, deployedApps: {},
deployedAppNames: [], deployedAppNames: [],
@ -13,28 +12,12 @@ export const getHostingStore = () => {
const store = writable({ ...INITIAL_HOSTING_UI_STATE }) const store = writable({ ...INITIAL_HOSTING_UI_STATE })
store.actions = { store.actions = {
fetch: async () => { fetch: async () => {
const responses = await Promise.all([ const response = await api.get("/api/hosting/urls")
api.get("/api/hosting/"), const urls = await response.json()
api.get("/api/hosting/urls"),
])
const [info, urls] = await Promise.all(responses.map(resp => resp.json()))
store.update(state => { store.update(state => {
state.hostingInfo = info
state.appUrl = urls.app state.appUrl = urls.app
return state return state
}) })
return info
},
save: async hostingInfo => {
const response = await api.post("/api/hosting", hostingInfo)
const revision = (await response.json()).rev
store.update(state => {
state.hostingInfo = {
...hostingInfo,
_rev: revision,
}
return state
})
}, },
fetchDeployedApps: async () => { fetchDeployedApps: async () => {
let deployments = await (await get("/api/hosting/apps")).json() let deployments = await (await get("/api/hosting/apps")).json()

View File

@ -50,7 +50,7 @@
<div class="section"> <div class="section">
{#each categories as [categoryName, bindings]} {#each categories as [categoryName, bindings]}
<Heading size="XS">{categoryName}</Heading> <Heading size="XS">{categoryName}</Heading>
{#each bindableProperties.filter(binding => {#each bindings.filter(binding =>
binding.label.match(searchRgx) binding.label.match(searchRgx)
) as binding} ) as binding}
<div <div

View File

@ -0,0 +1,113 @@
<script>
import { onMount, onDestroy } from "svelte"
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api"
import analytics from "analytics"
import FeedbackIframe from "components/feedback/FeedbackIframe.svelte"
const DeploymentStatus = {
SUCCESS: "SUCCESS",
PENDING: "PENDING",
FAILURE: "FAILURE",
}
const POLL_INTERVAL = 1000
let loading = false
let feedbackModal
let deployments = []
let poll
let publishModal
$: appId = $store.appId
async function deployApp() {
try {
notifications.info(`Deployment started. Please wait.`)
const response = await api.post("/api/deploy")
const json = await response.json()
if (response.status !== 200) {
throw new Error()
}
if (analytics.requestFeedbackOnDeploy()) {
feedbackModal.show()
}
} catch (err) {
analytics.captureException(err)
notifications.error("Deployment unsuccessful. Please try again later.")
}
}
async function fetchDeployments() {
try {
const response = await api.get(`/api/deployments`)
const json = await response.json()
if (deployments.length > 0) {
checkIncomingDeploymentStatus(deployments, json)
}
deployments = json
} catch (err) {
console.error(err)
clearInterval(poll)
notifications.error(
"Error fetching deployment history. Please try again."
)
}
}
// Required to check any updated deployment statuses between polls
function checkIncomingDeploymentStatus(current, incoming) {
console.log(current, incoming)
for (let incomingDeployment of incoming) {
if (
incomingDeployment.status === DeploymentStatus.FAILURE ||
incomingDeployment.status === DeploymentStatus.SUCCESS
) {
const currentDeployment = current.find(
deployment => deployment._id === incomingDeployment._id
)
// We have just been notified of an ongoing deployments status change
if (
!currentDeployment ||
currentDeployment.status === DeploymentStatus.PENDING
) {
if (incomingDeployment.status === DeploymentStatus.FAILURE) {
notifications.error(incomingDeployment.err)
} else {
notifications.send(
"Published to Production.",
"success",
"CheckmarkCircle"
)
}
}
}
}
}
onMount(() => {
fetchDeployments()
poll = setInterval(fetchDeployments, POLL_INTERVAL)
})
onDestroy(() => clearInterval(poll))
</script>
<Button secondary on:click={publishModal.show}>Publish</Button>
<Modal bind:this={publishModal}>
<ModalContent
title="Publish to Production"
confirmText="Publish"
onConfirm={deployApp}
>
<span
>The changes you have made will be published to the production version of
the application.</span
>
</ModalContent>
</Modal>

View File

@ -36,8 +36,7 @@
let errorReason let errorReason
let poll let poll
let deployments = [] let deployments = []
let urlComponent = let urlComponent = $store.url || `/${appId}`
$hostingStore.hostingInfo.type === "self" ? $store.url : `/${appId}`
let deploymentUrl = `${$hostingStore.appUrl}${urlComponent}` let deploymentUrl = `${$hostingStore.appUrl}${urlComponent}`
const formatDate = (date, format) => const formatDate = (date, format) =>

View File

@ -0,0 +1,50 @@
<script>
import { onMount, onDestroy } from "svelte"
import {
Button,
Icon,
Modal,
notifications,
ModalContent,
} from "@budibase/bbui"
import { store } from "builderStore"
import { apps } from "stores/portal"
import api from "builderStore/api"
let revertModal
$: appId = $store.appId
const revert = async () => {
try {
const response = await api.post(`/api/dev/${appId}/revert`)
const json = await response.json()
if (response.status !== 200) throw json.message
// Reset frontend state after revert
const applicationPkg = await api.get(
`/api/applications/${appId}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
await store.actions.initialise(pkg)
} else {
throw new Error(pkg)
}
notifications.info("Changes reverted.")
} catch (err) {
notifications.error(`Error reverting changes: ${err}`)
}
}
</script>
<Icon name="Revert" hoverable on:click={revertModal.show} />
<Modal bind:this={revertModal}>
<ModalContent title="Revert Changes" confirmText="Revert" onConfirm={revert}>
<span
>The changes you have made will be deleted and the application reverted
back to its production state.</span
>
</ModalContent>
</Modal>

View File

@ -1,5 +1,5 @@
<script> <script>
import { General, DangerZone, APIKeys } from "./tabs" import { General, DangerZone } from "./tabs"
import { ModalContent, Tab, Tabs } from "@budibase/bbui" import { ModalContent, Tab, Tabs } from "@budibase/bbui"
</script> </script>
@ -13,9 +13,9 @@
<Tab title="General"> <Tab title="General">
<General /> <General />
</Tab> </Tab>
<Tab title="API Keys"> <!-- <Tab title="API Keys">
<APIKeys /> <APIKeys />
</Tab> </Tab> -->
<Tab title="Danger Zone"> <Tab title="Danger Zone">
<DangerZone /> <DangerZone />
</Tab> </Tab>

View File

@ -49,27 +49,22 @@
onMount(async () => { onMount(async () => {
const nameError = "Your application must have a name.", const nameError = "Your application must have a name.",
urlError = "Your application must have a URL." urlError = "Your application must have a URL."
let hostingInfo = await hostingStore.actions.fetch() await hostingStore.actions.fetchDeployedApps()
if (hostingInfo.type === "self") { const existingAppNames = get(hostingStore).deployedAppNames
await hostingStore.actions.fetchDeployedApps() const existingAppUrls = get(hostingStore).deployedAppUrls
const existingAppNames = get(hostingStore).deployedAppNames const nameIdx = existingAppNames.indexOf(get(store).name)
const existingAppUrls = get(hostingStore).deployedAppUrls const urlIdx = existingAppUrls.indexOf(get(store).url)
const nameIdx = existingAppNames.indexOf(get(store).name) if (nameIdx !== -1) {
const urlIdx = existingAppUrls.indexOf(get(store).url) existingAppNames.splice(nameIdx, 1)
if (nameIdx !== -1) { }
existingAppNames.splice(nameIdx, 1) if (urlIdx !== -1) {
} existingAppUrls.splice(urlIdx, 1)
if (urlIdx !== -1) { }
existingAppUrls.splice(urlIdx, 1) nameValidation = {
} name: string().required(nameError).notOneOf(existingAppNames),
nameValidation = { }
name: string().required(nameError).notOneOf(existingAppNames), urlValidation = {
} url: string().required(urlError).notOneOf(existingAppUrls),
urlValidation = {
url: string().required(urlError).notOneOf(existingAppUrls),
}
} else {
nameValidation = { name: string().required(nameError) }
} }
}) })
</script> </script>
@ -81,14 +76,12 @@
error={nameError} error={nameError}
label="App Name" label="App Name"
/> />
{#if $hostingStore.hostingInfo.type === "self"} <Input
<Input on:change={e => updateApplication({ url: e.detail })}
on:change={e => updateApplication({ url: e.detail })} value={$store.url}
value={$store.url} error={urlError}
error={urlError} label="App URL"
label="App URL" />
/>
{/if}
<TextArea <TextArea
on:change={e => updateApplication({ description: e.detail })} on:change={e => updateApplication({ description: e.detail })}
value={$store.description} value={$store.description}

View File

@ -9,18 +9,23 @@
Link, Link,
} from "@budibase/bbui" } from "@budibase/bbui"
import { gradient } from "actions" import { gradient } from "actions"
import { AppStatus } from "constants"
import { url } from "@roxi/routify" import { url } from "@roxi/routify"
import { auth } from "stores/backend"
export let app export let app
export let exportApp export let exportApp
export let openApp
export let deleteApp export let deleteApp
export let releaseLock
export let deletable
</script> </script>
<div class="wrapper"> <div class="wrapper">
<Layout noPadding gap="XS" alignContent="start"> <Layout noPadding gap="XS" alignContent="start">
<div class="preview" use:gradient={{ seed: app.name }} /> <div class="preview" use:gradient={{ seed: app.name }} />
<div class="title"> <div class="title">
<Link href={$url(`../../app/${app._id}`)}> <Link on:click={() => openApp(app)}>
<Heading size="XS"> <Heading size="XS">
{app.name} {app.name}
</Heading> </Heading>
@ -30,16 +35,23 @@
<MenuItem on:click={() => exportApp(app)} icon="Download"> <MenuItem on:click={() => exportApp(app)} icon="Download">
Export Export
</MenuItem> </MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete"> {#if deletable}
Delete <MenuItem on:click={() => deleteApp(app)} icon="Delete">
</MenuItem> Delete
</MenuItem>
{/if}
{#if app.lockedBy && app.lockedBy?.email === $auth.user?.email}
<MenuItem on:click={() => releaseLock(app.appId)} icon="LockOpen">
Release Lock
</MenuItem>
{/if}
</ActionMenu> </ActionMenu>
</div> </div>
<div class="status"> <div class="status">
<Body noPadding size="S"> <Body noPadding size="S">
Edited {Math.floor(1 + Math.random() * 10)} months ago Edited {Math.floor(1 + Math.random() * 10)} months ago
</Body> </Body>
{#if Math.random() > 0.5} {#if app.lockedBy}
<Icon name="LockClosed" /> <Icon name="LockClosed" />
{/if} {/if}
</div> </div>

View File

@ -8,18 +8,22 @@
MenuItem, MenuItem,
Link, Link,
} from "@budibase/bbui" } from "@budibase/bbui"
import { AppStatus } from "constants"
import { url } from "@roxi/routify" import { url } from "@roxi/routify"
import { auth } from "stores/backend"
export let app export let app
export let openApp export let openApp
export let exportApp export let exportApp
export let deleteApp export let deleteApp
export let releaseLock
export let last export let last
export let deletable
</script> </script>
<div class="title" class:last> <div class="title" class:last>
<div class="preview" use:gradient={{ seed: app.name }} /> <div class="preview" use:gradient={{ seed: app.name }} />
<Link href={$url(`../../app/${app._id}`)}> <Link on:click={() => openApp(app)}>
<Heading size="XS"> <Heading size="XS">
{app.name} {app.name}
</Heading> </Heading>
@ -29,15 +33,12 @@
Edited {Math.round(Math.random() * 10 + 1)} months ago Edited {Math.round(Math.random() * 10 + 1)} months ago
</div> </div>
<div class:last> <div class:last>
{#if Math.random() < 0.33} {#if app.lockedBy}
<div class="status status--locked-you" />
Locked by {app.lockedBy.email}
{:else}
<div class="status status--open" /> <div class="status status--open" />
Open Open
{:else if Math.random() < 0.33}
<div class="status status--locked-other" />
Locked by Will Wheaton
{:else}
<div class="status status--locked-you" />
Locked by you
{/if} {/if}
</div> </div>
<div class:last> <div class:last>
@ -45,7 +46,14 @@
<ActionMenu align="right"> <ActionMenu align="right">
<Icon hoverable slot="control" name="More" /> <Icon hoverable slot="control" name="More" />
<MenuItem on:click={() => exportApp(app)} icon="Download">Export</MenuItem> <MenuItem on:click={() => exportApp(app)} icon="Download">Export</MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem> {#if deletable}
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
{/if}
{#if app.lockedBy && app.lockedBy?.email === $auth.user?.email}
<MenuItem on:click={() => releaseLock(app.appId)} icon="LockOpen">
Release Lock
</MenuItem>
{/if}
</ActionMenu> </ActionMenu>
</div> </div>

View File

@ -1,47 +1,19 @@
<script> <script>
import { hostingStore } from "builderStore"
import { HostingTypes } from "constants/backend"
import { import {
Heading, Heading,
Divider, Divider,
notifications, notifications,
Input,
ModalContent, ModalContent,
Toggle, Toggle,
Body, Body,
} from "@budibase/bbui" } from "@budibase/bbui"
import ThemeEditor from "components/settings/ThemeEditor.svelte" import ThemeEditor from "components/settings/ThemeEditor.svelte"
import analytics from "analytics" import analytics from "analytics"
import { onMount } from "svelte"
let hostingInfo
let selfhosted = false
$: analyticsDisabled = analytics.disabled() $: analyticsDisabled = analytics.disabled()
async function save() { async function save() {
hostingInfo.type = selfhosted ? HostingTypes.SELF : HostingTypes.CLOUD notifications.success(`Settings saved.`)
if (!selfhosted && hostingInfo._rev) {
hostingInfo = {
type: hostingInfo.type,
_id: hostingInfo._id,
_rev: hostingInfo._rev,
}
}
try {
await hostingStore.actions.save(hostingInfo)
notifications.success(`Settings saved.`)
} catch (err) {
notifications.error(`Failed to update builder settings.`)
}
}
function updateSelfHosting(event) {
if (hostingInfo.type === HostingTypes.CLOUD && event.detail) {
hostingInfo.hostingUrl = "localhost:10000"
hostingInfo.useHttps = false
hostingInfo.selfHostKey = "budibase"
}
} }
function toggleAnalytics() { function toggleAnalytics() {
@ -51,33 +23,12 @@
analytics.optOut() analytics.optOut()
} }
} }
onMount(async () => {
hostingInfo = await hostingStore.actions.fetch()
selfhosted = hostingInfo.type === "self"
})
</script> </script>
<ModalContent title="Builder settings" confirmText="Save" onConfirm={save}> <ModalContent title="Builder settings" confirmText="Save" onConfirm={save}>
<Heading size="XS">Theme</Heading> <Heading size="XS">Theme</Heading>
<ThemeEditor /> <ThemeEditor />
<Divider noMargin noGrid /> <Divider noMargin noGrid />
<Heading size="XS">Hosting</Heading>
<Body size="S">
This section contains settings that relate to the deployment and hosting of
apps made in this builder.
</Body>
<Toggle
text="Self hosted"
on:change={updateSelfHosting}
bind:value={selfhosted}
/>
{#if selfhosted}
<Input bind:value={hostingInfo.hostingUrl} label="Hosting URL" />
<Input bind:value={hostingInfo.selfHostKey} label="Hosting Key" />
<Toggle text="HTTPS" bind:value={hostingInfo.useHttps} />
{/if}
<Divider noMargin noGrid />
<Heading size="XS">Analytics</Heading> <Heading size="XS">Analytics</Heading>
<Body size="S"> <Body size="S">
If you would like to send analytics that help us make budibase better, If you would like to send analytics that help us make budibase better,

View File

@ -10,8 +10,7 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { store, automationStore, hostingStore } from "builderStore" import { store, automationStore, hostingStore } from "builderStore"
import { string, mixed, object } from "yup" import { string, mixed, object } from "yup"
import api, { get } from "builderStore/api" import api, { get, post } from "builderStore/api"
import { post } from "builderStore/api"
import analytics from "analytics" import analytics from "analytics"
import { onMount } from "svelte" import { onMount } from "svelte"
import { capitalise } from "helpers" import { capitalise } from "helpers"
@ -97,7 +96,7 @@
// Select Correct Application/DB in prep for creating user // Select Correct Application/DB in prep for creating user
const applicationPkg = await get( const applicationPkg = await get(
`/api/applications/${appJson._id}/appPackage` `/api/applications/${appJson.instance._id}/appPackage`
) )
const pkg = await applicationPkg.json() const pkg = await applicationPkg.json()
if (applicationPkg.ok) { if (applicationPkg.ok) {
@ -113,7 +112,7 @@
} }
const userResp = await api.post(`/api/users/metadata/self`, user) const userResp = await api.post(`/api/users/metadata/self`, user)
await userResp.json() await userResp.json()
$goto(`/builder/app/${appJson._id}`) $goto(`/builder/app/${appJson.instance._id}`)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
notifications.error(error) notifications.error(error)

View File

@ -9,6 +9,11 @@ export const FrontendTypes = {
NONE: "none", NONE: "none",
} }
export const AppStatus = {
DEV: "dev",
PUBLISHED: "published",
}
// fields on the user table that cannot be edited // fields on the user table that cannot be edited
export const UNEDITABLE_USER_FIELDS = ["email", "password", "roleId", "status"] export const UNEDITABLE_USER_FIELDS = ["email", "password", "roleId", "status"]

View File

@ -0,0 +1,20 @@
import { writable } from "svelte/store"
import api from "builderStore/api"
export default function (url) {
const store = writable({ status: "LOADING", data: {}, error: {} })
async function get() {
store.update(u => ({ ...u, status: "LOADING" }))
try {
const response = await api.get(url)
store.set({ data: await response.json(), status: "SUCCESS" })
} catch (e) {
store.set({ data: {}, error: e, status: "ERROR" })
}
}
get()
return { subscribe: store.subscribe, refresh: get }
}

View File

@ -0,0 +1,9 @@
export { default as fetchData } from "./fetchData"
export {
buildStyle,
convertCamel,
pipe,
capitalise,
get_name,
get_capitalised_name,
} from "./helpers"

View File

@ -0,0 +1,2 @@
export { emailValidator, requiredValidator } from "./validators"
export { createValidationStore } from "./validation"

View File

@ -0,0 +1,23 @@
import { writable, derived } from "svelte/store"
export function createValidationStore(initialValue, ...validators) {
let touched = false
const value = writable(initialValue || "")
const error = derived(value, $v => validate($v, validators))
const touchedStore = derived(value, () => {
if (!touched) {
touched = true
return false
}
return touched
})
return [value, error, touchedStore]
}
function validate(value, validators) {
const failing = validators.find(v => v(value) !== true)
return failing && failing(value)
}

View File

@ -0,0 +1,16 @@
export function emailValidator(value) {
return (
(value &&
!!value.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
)) ||
"Please enter a valid email"
)
}
export function requiredValidator(value) {
return (
(value !== undefined && value !== null && value !== "") ||
"This field is required"
)
}

View File

@ -1,6 +1,6 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { goto } from "@roxi/routify" import { page, goto } from "@roxi/routify"
import { auth } from "stores/backend" import { auth } from "stores/backend"
import { admin } from "stores/portal" import { admin } from "stores/portal"
@ -22,7 +22,12 @@
// Redirect to log in at any time if the user isn't authenticated // Redirect to log in at any time if the user isn't authenticated
$: { $: {
if (loaded && hasAdminUser && !$auth.user) { if (
!$page.path.includes("/builder/invite") &&
loaded &&
hasAdminUser &&
!$auth.user
) {
$goto("./auth/login") $goto("./auth/login")
} }
} }

View File

@ -1,10 +1,21 @@
<script> <script>
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import { roles } from "stores/backend" import { roles } from "stores/backend"
import { Button, ActionGroup, ActionButton, Tabs, Tab } from "@budibase/bbui" import {
Button,
Icon,
Modal,
ModalContent,
ActionGroup,
ActionButton,
Tabs,
Tab,
} from "@budibase/bbui"
import SettingsLink from "components/settings/Link.svelte" import SettingsLink from "components/settings/Link.svelte"
import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte" import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte"
import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte" import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte"
import DeployModal from "components/deploy/DeployModal.svelte"
import RevertModal from "components/deploy/RevertModal.svelte"
import { get } from "builderStore/api" import { get } from "builderStore/api"
import { isActive, goto, layout } from "@roxi/routify" import { isActive, goto, layout } from "@roxi/routify"
import Logo from "/assets/bb-logo.svg" import Logo from "/assets/bb-logo.svg"
@ -81,25 +92,15 @@
<ActionGroup /> <ActionGroup />
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<ThemeEditorDropdown /> <RevertModal />
<FeedbackNavLink /> <Icon
<div class="topnavitemright"> name="Play"
<a hoverable
target="_blank"
href="https://github.com/Budibase/budibase/discussions"
>
<i class="ri-github-fill" />
</a>
</div>
<SettingsLink />
<Button
secondary
on:click={() => { on:click={() => {
window.open(`/${application}`) window.open(`/${application}`)
}} }}
> />
Preview <DeployModal />
</Button>
</div> </div>
</div> </div>
<div class="beta"> <div class="beta">
@ -153,6 +154,7 @@
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: var(--spacing-xl);
} }
.topleftnav { .topleftnav {

View File

@ -1,2 +0,0 @@
<!-- routify:options index=4 -->
<slot />

View File

@ -1,102 +0,0 @@
<script>
import { Button, Modal, notifications, Heading } from "@budibase/bbui"
import { store, hostingStore } from "builderStore"
import api from "builderStore/api"
import DeploymentHistory from "components/deploy/DeploymentHistory.svelte"
import analytics from "analytics"
import FeedbackIframe from "components/feedback/FeedbackIframe.svelte"
import Rocket from "/assets/deploy-rocket.jpg"
let loading = false
let deployments = []
let poll
let feedbackModal
$: appId = $store.appId
async function deployApp() {
// Must have cloud or self host API key to deploy
if (!$hostingStore.hostingInfo?.selfHostKey) {
const response = await api.get(`/api/keys/`)
const userKeys = await response.json()
if (!userKeys.budibase) {
notifications.error(
"No budibase API Keys configured. You must set either a self hosted or cloud API key to deploy your budibase app."
)
return
}
}
const DEPLOY_URL = `/api/deploy`
try {
notifications.info(`Deployment started. Please wait.`)
const response = await api.post(DEPLOY_URL)
const json = await response.json()
if (response.status !== 200) {
throw new Error()
}
analytics.captureEvent("Deployed App", {
appId,
hostingType: $hostingStore.hostingInfo?.type,
})
if (analytics.requestFeedbackOnDeploy()) {
feedbackModal.show()
}
} catch (err) {
analytics.captureEvent("Deploy App Failed", {
appId,
hostingType: $hostingStore.hostingInfo?.type,
})
analytics.captureException(err)
notifications.error("Deployment unsuccessful. Please try again later.")
}
}
</script>
<section>
<img src={Rocket} alt="Rocket flying through sky" />
<div>
<Heading size="M">It's time to shine!</Heading>
<Button size="XL" cta medium on:click={deployApp}>Deploy App</Button>
</div>
</section>
<Modal bind:this={feedbackModal}>
<FeedbackIframe on:finished={() => feedbackModal.hide()} />
</Modal>
<DeploymentHistory {appId} />
<style>
img {
width: 100%;
height: 100%;
object-fit: cover;
filter: brightness(80%);
}
section {
position: relative;
min-height: 100%;
}
div {
position: absolute;
display: flex;
text-align: center;
flex-direction: column;
align-items: center;
justify-content: center;
left: 0;
right: 0;
top: 20%;
margin-left: auto;
margin-right: auto;
width: 50%;
gap: var(--spacing-xl);
}
div :global(h1) {
color: white;
}
</style>

View File

@ -0,0 +1,97 @@
<script>
import {
Layout,
Heading,
Body,
Input,
Button,
notifications,
} from "@budibase/bbui"
import { goto, params } from "@roxi/routify"
import { createValidationStore, requiredValidator } from "helpers/validation"
import { users } from "stores/portal"
const [password, passwordError, passwordTouched] = createValidationStore(
"",
requiredValidator
)
const [repeat, _, repeatTouched] = createValidationStore(
"",
requiredValidator
)
const inviteCode = $params["?code"]
async function acceptInvite() {
try {
const res = await users.acceptInvite(inviteCode, $password)
if (!res) {
throw new Error(res.message)
}
notifications.success(`User created.`)
$goto("../auth/login")
} catch (err) {
notifications.error(err)
}
}
</script>
<section>
<div class="container">
<Layout gap="XS">
<img src="https://i.imgur.com/ZKyklgF.png" />
</Layout>
<div class="center">
<Layout gap="XS">
<Heading size="M">Accept Invitation</Heading>
<Body size="M">Please enter a password to setup your user.</Body>
</Layout>
</div>
<Layout gap="XS">
<Input
label="Password"
type="password"
error={$passwordTouched && $passwordError}
bind:value={$password}
/>
<Input
label="Repeat Password"
type="password"
error={$repeatTouched &&
$password !== $repeat &&
"Passwords must match"}
bind:value={$repeat}
/>
</Layout>
<Layout gap="S">
<Button
disabled={!$passwordTouched || !$repeatTouched || $password !== $repeat}
cta
on:click={acceptInvite}>Accept invite</Button
>
</Layout>
</div>
</section>
<style>
section {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.container {
margin: 0 auto;
width: 260px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.center {
text-align: center;
}
img {
width: 40px;
margin: 0 auto;
}
</style>

View File

@ -1,6 +1,5 @@
<script> <script>
import { isActive, goto } from "@roxi/routify" import { isActive, goto } from "@roxi/routify"
import { onMount } from "svelte"
import { import {
Icon, Icon,
Avatar, Avatar,
@ -13,34 +12,21 @@
Modal, Modal,
} from "@budibase/bbui" } from "@budibase/bbui"
import ConfigChecklist from "components/common/ConfigChecklist.svelte" import ConfigChecklist from "components/common/ConfigChecklist.svelte"
import { organisation, apps } from "stores/portal" import { organisation } from "stores/portal"
import { auth } from "stores/backend" import { auth } from "stores/backend"
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte" import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
let orgName
let orgLogo
let user
let oldSettingsModal let oldSettingsModal
async function getInfo() { organisation.init()
// fetch orgInfo
orgName = "ACME Inc."
orgLogo = "https://via.placeholder.com/150"
user = { name: "John Doe" }
}
onMount(() => {
organisation.init()
getInfo()
})
let menu = [ let menu = [
{ title: "Apps", href: "/builder/portal/apps" }, { title: "Apps", href: "/builder/portal/apps" },
{ title: "Drafts", href: "/builder/portal/drafts" }, { title: "Drafts", href: "/builder/portal/drafts" },
{ title: "Users", href: "/builder/portal/users", heading: "Manage" }, { title: "Users", href: "/builder/portal/manage/users", heading: "Manage" },
{ title: "Groups", href: "/builder/portal/groups" }, { title: "Groups", href: "/builder/portal/manage/groups" },
{ title: "Auth", href: "/builder/portal/oauth" }, { title: "Auth", href: "/builder/portal/manage/auth" },
{ title: "Email", href: "/builder/portal/email" }, { title: "Email", href: "/builder/portal/manage/email" },
{ {
title: "General", title: "General",
href: "/builder/portal/settings/general", href: "/builder/portal/settings/general",

View File

@ -18,13 +18,16 @@
import analytics from "analytics" import analytics from "analytics"
import { onMount } from "svelte" import { onMount } from "svelte"
import { apps } from "stores/portal" import { apps } from "stores/portal"
import { auth } from "stores/backend"
import download from "downloadjs" import download from "downloadjs"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import AppCard from "components/start/AppCard.svelte" import AppCard from "components/start/AppCard.svelte"
import AppRow from "components/start/AppRow.svelte" import AppRow from "components/start/AppRow.svelte"
import { AppStatus } from "constants"
let layout = "grid" let layout = "grid"
let appStatus = AppStatus.PUBLISHED
let template let template
let appToDelete let appToDelete
let creationModal let creationModal
@ -32,6 +35,8 @@
let creatingApp = false let creatingApp = false
let loaded = false let loaded = false
$: appStatus && apps.load(appStatus)
const checkKeys = async () => { const checkKeys = async () => {
const response = await api.get(`/api/keys/`) const response = await api.get(`/api/keys/`)
const keys = await response.json() const keys = await response.json()
@ -57,13 +62,24 @@
} }
const openApp = app => { const openApp = app => {
$goto(`../../app/${app._id}`) if (app.lockedBy && app.lockedBy?.email !== $auth.user?.email) {
notifications.error(
`App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.`
)
return
}
if (appStatus === AppStatus.DEV) {
$goto(`../../app/${app.appId}`)
} else {
window.open(`/${app.appId}`, "_blank")
}
} }
const exportApp = app => { const exportApp = app => {
try { try {
download( download(
`/api/backups/export?appId=${app._id}&appname=${encodeURIComponent( `/api/backups/export?appId=${app.appId}&appname=${encodeURIComponent(
app.name app.name
)}` )}`
) )
@ -83,54 +99,77 @@
if (!appToDelete) { if (!appToDelete) {
return return
} }
await del(`/api/applications/${appToDelete?._id}`) await del(`/api/applications/${appToDelete?._id}`)
await apps.load() await apps.load()
appToDelete = null appToDelete = null
notifications.success("App deleted successfully.")
}
const releaseLock = async appId => {
try {
const response = await del(`/api/dev/${appId}/lock`)
const json = await response.json()
if (response.status !== 200) throw json.message
notifications.success("Lock released")
await apps.load(appStatus)
} catch (err) {
notifications.error(`Error releasing lock: ${err}`)
}
} }
onMount(async () => { onMount(async () => {
checkKeys() checkKeys()
await apps.load() await apps.load(appStatus)
loaded = true loaded = true
}) })
</script> </script>
<Page wide> <Page wide>
{#if $apps.length} <Layout noPadding>
<Layout noPadding> <div class="title">
<div class="title"> <Heading>Apps</Heading>
<Heading>Apps</Heading> <ButtonGroup>
<ButtonGroup> <Button secondary on:click={initiateAppImport}>Import app</Button>
<Button secondary on:click={initiateAppImport}>Import app</Button> <Button cta on:click={initiateAppCreation}>Create new app</Button>
<Button cta on:click={initiateAppCreation}>Create new app</Button> </ButtonGroup>
</ButtonGroup> </div>
</div> <div class="filter">
<div class="filter"> <div class="select">
<div class="select"> <Select
<Select quiet placeholder="Filter by groups" /> bind:value={appStatus}
</div> options={[
<ActionGroup> { label: "Published", value: AppStatus.PUBLISHED },
<ActionButton { label: "In Development", value: AppStatus.DEV },
on:click={() => (layout = "grid")} ]}
selected={layout === "grid"} />
quiet
icon="ClassicGridView"
/>
<ActionButton
on:click={() => (layout = "table")}
selected={layout === "table"}
quiet
icon="ViewRow"
/>
</ActionGroup>
</div> </div>
<ActionGroup>
<ActionButton
on:click={() => (layout = "grid")}
selected={layout === "grid"}
quiet
icon="ClassicGridView"
/>
<ActionButton
on:click={() => (layout = "table")}
selected={layout === "table"}
quiet
icon="ViewRow"
/>
</ActionGroup>
</div>
{#if $apps.length}
<div <div
class:appGrid={layout === "grid"} class:appGrid={layout === "grid"}
class:appTable={layout === "table"} class:appTable={layout === "table"}
> >
{#each $apps as app, idx (app._id)} {#each $apps as app, idx (app.appId)}
<svelte:component <svelte:component
this={layout === "grid" ? AppCard : AppRow} this={layout === "grid" ? AppCard : AppRow}
deletable={appStatus === AppStatus.PUBLISHED}
{releaseLock}
{app} {app}
{openApp} {openApp}
{exportApp} {exportApp}
@ -139,8 +178,8 @@
/> />
{/each} {/each}
</div> </div>
</Layout> {/if}
{/if} </Layout>
{#if !$apps.length && !creatingApp && loaded} {#if !$apps.length && !creatingApp && loaded}
<div class="empty-wrapper"> <div class="empty-wrapper">
<Modal inline> <Modal inline>

View File

@ -0,0 +1,7 @@
<script>
import { Page } from "@budibase/bbui"
</script>
<Page>
<slot />
</Page>

View File

@ -0,0 +1,114 @@
<script>
import GoogleLogo from "./_logos/Google.svelte"
import {
Button,
Heading,
Divider,
Page,
Label,
notifications,
Layout,
Input,
Body,
} from "@budibase/bbui"
import { onMount } from "svelte"
import api from "builderStore/api"
const ConfigTypes = {
Google: "google",
// Github: "github",
// AzureAD: "ad",
}
const ConfigFields = {
Google: ["clientID", "clientSecret", "callbackURL"],
}
let google
async function save(doc) {
try {
// Save an oauth config
const response = await api.post(`/api/admin/configs`, doc)
const json = await response.json()
if (response.status !== 200) throw new Error(json.message)
google._rev = json._rev
google._id = json._id
notifications.success(`Settings saved.`)
} catch (err) {
notifications.error(`Failed to update OAuth settings. ${err}`)
}
}
onMount(async () => {
// fetch the configs for oauth
const googleResponse = await api.get(
`/api/admin/configs/${ConfigTypes.Google}`
)
const googleDoc = await googleResponse.json()
if (!googleDoc._id) {
google = {
type: ConfigTypes.Google,
config: {},
}
} else {
google = googleDoc
}
})
</script>
<Page>
<Layout noPadding>
<div>
<Heading size="M">OAuth</Heading>
<Body>
Every budibase app comes with basic authentication (email/password)
included. You can add additional authentication methods from the options
below.
</Body>
</div>
<Divider />
{#if google}
<div>
<Heading size="S">
<span>
<GoogleLogo />
Google
</span>
</Heading>
<Body>
To allow users to authenticate using their Google accounts, fill out
the fields below.
</Body>
</div>
{#each ConfigFields.Google as field}
<div class="form-row">
<Label size="L">{field}</Label>
<Input bind:value={google.config[field]} />
</div>
{/each}
<div>
<Button primary on:click={() => save(google)}>Save</Button>
</div>
<Divider />
{/if}
</Layout>
</Page>
<style>
.form-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
span {
display: flex;
align-items: center;
gap: var(--spacing-s);
}
</style>

View File

@ -1,32 +1,19 @@
<script> <script>
import { import {
Menu,
MenuItem,
Button, Button,
Detail, Detail,
Heading, Heading,
Divider,
Label,
Modal,
ModalContent,
notifications, notifications,
Layout,
Icon, Icon,
Body,
Page, Page,
Select,
Tabs, Tabs,
Tab, Tab,
MenuSection,
MenuSeparator,
} from "@budibase/bbui" } from "@budibase/bbui"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { onMount } from "svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { email } from "stores/portal" import { email } from "stores/portal"
import Editor from "components/integration/QueryEditor.svelte" import Editor from "components/integration/QueryEditor.svelte"
import TemplateBindings from "./TemplateBindings.svelte" import TemplateBindings from "./_components/TemplateBindings.svelte"
import api from "builderStore/api"
const ConfigTypes = { const ConfigTypes = {
SMTP: "smtp", SMTP: "smtp",

View File

@ -23,8 +23,8 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { email } from "stores/portal" import { email } from "stores/portal"
import Editor from "components/integration/QueryEditor.svelte" import Editor from "components/integration/QueryEditor.svelte"
import TemplateBindings from "./TemplateBindings.svelte" import TemplateBindings from "./_components/TemplateBindings.svelte"
import TemplateLink from "./TemplateLink.svelte" import TemplateLink from "./_components/TemplateLink.svelte"
import api from "builderStore/api" import api from "builderStore/api"
const ConfigTypes = { const ConfigTypes = {

View File

@ -0,0 +1,43 @@
<svg
width="18"
height="18"
viewBox="0 0 268 268"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0)">
<path
d="M58.8037 109.043C64.0284 93.2355 74.1116 79.4822 87.615 69.7447C101.118
60.0073 117.352 54.783 134 54.8172C152.872 54.8172 169.934 61.5172 183.334
72.4828L222.328 33.5C198.566 12.7858 168.114 0 134 0C81.1817 0 35.711
30.1277 13.8467 74.2583L58.8037 109.043Z"
fill="#EA4335"
/>
<path
d="M179.113 201.145C166.942 208.995 151.487 213.183 134 213.183C117.418
213.217 101.246 208.034 87.7727 198.369C74.2993 188.703 64.2077 175.044
58.9265 159.326L13.8132 193.574C24.8821 215.978 42.012 234.828 63.2572
247.984C84.5024 261.14 109.011 268.075 134 268C166.752 268 198.041 256.353
221.48 234.5L179.125 201.145H179.113Z"
fill="#34A853"
/>
<path
d="M221.48 234.5C245.991 211.631 261.903 177.595 261.903 134C261.903
126.072 260.686 117.552 258.866 109.634H134V161.414H205.869C202.329
178.823 192.804 192.301 179.125 201.145L221.48 234.5Z"
fill="#4A90E2"
/>
<path
d="M58.9265 159.326C56.1947 151.162 54.8068 142.609 54.8172 134C54.8172
125.268 56.213 116.882 58.8037 109.043L13.8467 74.2584C4.64957 92.825
-0.0915078 113.28 1.86708e-05 134C1.86708e-05 155.44 4.96919 175.652
13.8132 193.574L58.9265 159.326Z"
fill="#FBBC05"
/>
</g>
<defs>
<clipPath id="clip0">
<rect width="268" height="268" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,115 @@
<script>
import GoogleLogo from "./_logos/Google.svelte"
import {
Button,
Heading,
Divider,
Label,
notifications,
Layout,
Input,
Body,
Page,
} from "@budibase/bbui"
import { onMount } from "svelte"
import api from "builderStore/api"
const ConfigTypes = {
Google: "google",
// Github: "github",
// AzureAD: "ad",
}
const ConfigFields = {
Google: ["clientID", "clientSecret", "callbackURL"],
}
let google
async function save(doc) {
try {
// Save an oauth config
const response = await api.post(`/api/admin/configs`, doc)
const json = await response.json()
if (response.status !== 200) throw new Error(json.message)
google._rev = json._rev
google._id = json._id
notifications.success(`Settings saved.`)
} catch (err) {
notifications.error(`Failed to update OAuth settings. ${err}`)
}
}
onMount(async () => {
// fetch the configs for oauth
const googleResponse = await api.get(
`/api/admin/configs/${ConfigTypes.Google}`
)
const googleDoc = await googleResponse.json()
if (!googleDoc._id) {
google = {
type: ConfigTypes.Google,
config: {},
}
} else {
google = googleDoc
}
})
</script>
<Page>
<header>
<Heading size="M">OAuth</Heading>
<Body size="S">
Every budibase app comes with basic authentication (email/password)
included. You can add additional authentication methods from the options
below.
</Body>
</header>
<Divider />
{#if google}
<div class="config-form">
<Layout gap="S">
<Heading size="S">
<span>
<GoogleLogo />
Google
</span>
</Heading>
{#each ConfigFields.Google as field}
<div class="form-row">
<Label>{field}</Label>
<Input bind:value={google.config[field]} />
</div>
{/each}
</Layout>
<Button primary on:click={() => save(google)}>Save</Button>
</div>
<Divider />
{/if}
</Page>
<style>
.config-form {
margin-top: 42px;
margin-bottom: 42px;
}
.form-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
}
span {
display: flex;
align-items: center;
gap: var(--spacing-s);
}
header {
margin-bottom: 42px;
}
</style>

View File

@ -0,0 +1,168 @@
<script>
import { goto } from "@roxi/routify"
import {
ActionButton,
Button,
Layout,
Heading,
Body,
Divider,
Label,
Input,
Modal,
Table,
ModalContent,
notifications,
} from "@budibase/bbui"
import { fetchData } from "helpers"
import { users } from "stores/portal"
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
export let userId
let deleteUserModal
let editRolesModal
const roleSchema = {
name: { displayName: "App" },
role: {},
}
// Merge the Apps list and the roles response to get something that makes sense for the table
$: appList = Object.keys($apps?.data).map(id => ({
...$apps?.data?.[id],
_id: id,
role: [$roleFetch?.data?.roles?.[id]],
}))
let selectedApp
const roleFetch = fetchData(`/api/admin/users/${userId}`)
const apps = fetchData(`/api/admin/roles`)
async function deleteUser() {
const res = await users.del(userId)
if (res.message) {
notifications.success(`User ${$roleFetch?.data?.email} deleted.`)
$goto("./")
} else {
notifications.error("Failed to delete user.")
}
}
async function openUpdateRolesModal({ detail }) {
console.log(detail)
selectedApp = detail
editRolesModal.show()
}
</script>
<Layout noPadding gap="XS">
<div class="back">
<ActionButton on:click={() => $goto("./")} quiet size="S" icon="BackAndroid"
>Back to users</ActionButton
>
</div>
<div class="heading">
<Layout noPadding gap="XS">
<Heading>User: {$roleFetch?.data?.email}</Heading>
<Body
>Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis porro
ut nesciunt ipsam perspiciatis aliquam et hic minus alias beatae. Odit
veritatis quos quas laborum magnam tenetur perspiciatis ex hic.
</Body>
</Layout>
</div>
<Divider size="S" />
<div class="general">
<Heading size="S">General</Heading>
<div class="fields">
<div class="field">
<Label size="L">Email</Label>
<Input disabled thin value={$roleFetch?.data?.email} />
</div>
</div>
<div class="regenerate">
<ActionButton size="S" icon="Refresh" quiet
>Regenerate password</ActionButton
>
</div>
</div>
<Divider size="S" />
<div class="roles">
<Heading size="S">Configure roles</Heading>
<Table
on:click={openUpdateRolesModal}
schema={roleSchema}
data={appList}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[{ column: "role", component: TagsRenderer }]}
/>
</div>
<Divider size="S" />
<div class="delete">
<Layout gap="S" noPadding
><Heading size="S">Delete user</Heading>
<Body>Deleting a user completely removes them from your account.</Body>
<div class="delete-button">
<Button warning on:click={deleteUserModal.show}>Delete user</Button>
</div></Layout
>
</div>
</Layout>
<Modal bind:this={deleteUserModal}>
<ModalContent
warning
onConfirm={deleteUser}
title="Delete User"
confirmText="Delete user"
cancelText="Cancel"
showCloseIcon={false}
>
<Body
>Are you sure you want to delete <strong>{$roleFetch?.data?.email}</strong
></Body
>
</ModalContent>
</Modal>
<Modal bind:this={editRolesModal}>
<UpdateRolesModal
app={selectedApp}
user={$roleFetch.data}
on:update={roleFetch.refresh}
/>
</Modal>
<style>
.fields {
display: grid;
grid-gap: var(--spacing-m);
margin-top: var(--spacing-xl);
}
.field {
display: grid;
grid-template-columns: 32% 1fr;
align-items: center;
}
.heading {
margin-bottom: var(--spacing-xl);
}
.general {
position: relative;
margin: var(--spacing-xl) 0;
}
.roles {
margin: var(--spacing-xl) 0;
}
.delete {
margin-top: var(--spacing-xl);
}
.regenerate {
position: absolute;
top: 0;
right: 0;
}
</style>

View File

@ -0,0 +1,59 @@
<script>
import {
Body,
Input,
Select,
ModalContent,
notifications,
} from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal"
export let disabled
const options = ["Email onboarding", "Basic onboarding"]
let selected = options[0]
const [email, error, touched] = createValidationStore("", emailValidator)
async function createUserFlow() {
const res = await users.invite($email)
console.log(res)
if (res.status) {
notifications.error(res.message)
} else {
notifications.success(res.message)
}
}
</script>
<ModalContent
onConfirm={createUserFlow}
size="M"
title="Add new user options"
confirmText="Add user"
confirmDisabled={disabled}
cancelText="Cancel"
disabled={$error}
showCloseIcon={false}
>
<Body noPadding
>If you have SMTP configured and an email for the new user, you can use the
automated email onboarding flow. Otherwise, use our basic onboarding process
with autogenerated passwords.</Body
>
<Select
placeholder={null}
bind:value={selected}
on:change
{options}
label="Add new user via:"
/>
<Input
type="email"
bind:value={$email}
error={$touched && $error}
placeholder="john@doe.com"
label="Email"
/>
</ModalContent>

View File

@ -0,0 +1,40 @@
<script>
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal"
const [email, error, touched] = createValidationStore("", emailValidator)
const password = Math.random().toString(36).substr(2, 20)
async function createUser() {
const res = await users.create({ email: $email, password })
if (res.status) {
notifications.error(res.message)
} else {
notifications.success("Succesfully created user")
}
}
</script>
<ModalContent
onConfirm={createUser}
size="M"
title="Basic user onboarding"
confirmText="Continue"
cancelText="Cancel"
disabled={$error}
error={$touched && $error}
showCloseIcon={false}
>
<Body noPadding
>Below you will find the users username and password. The password will not
be accessible from this point. Please download the credentials.</Body
>
<Input
type="email"
label="Username"
bind:value={$email}
error={$touched && $error}
/>
<Input disabled label="Password" value={password} />
</ModalContent>

View File

@ -0,0 +1,20 @@
<script>
import { Tag, Tags } from "@budibase/bbui"
export let value
const displayLimit = 5
$: tags = value?.slice(0, displayLimit) ?? []
$: leftover = (value?.length ?? 0) - tags.length
</script>
<Tags>
{#each tags as tag}
<Tag>
{tag}
</Tag>
{/each}
{#if leftover}
<Tag>+{leftover} more</Tag>
{/if}
</Tags>

View File

@ -0,0 +1,51 @@
<script>
import { createEventDispatcher } from "svelte"
import { Body, Select, ModalContent, notifications } from "@budibase/bbui"
import { fetchData } from "helpers"
import { users } from "stores/portal"
export let app
export let user
const dispatch = createEventDispatcher()
const roles = app.roles
let options = roles.map(role => role._id)
let selectedRole
async function updateUserRoles() {
const res = await users.updateRoles({
...user,
roles: {
...user.roles,
[app._id]: selectedRole,
},
})
if (res.status === 400) {
notifications.error("Failed to update role")
} else {
notifications.success("Roles updated")
dispatch("update")
}
}
</script>
<ModalContent
onConfirm={updateUserRoles}
title="Update App Roles"
confirmText="Update roles"
cancelText="Cancel"
size="M"
showCloseIcon={false}
>
<Body noPadding
>Update {user.email}'s roles for <strong>{app.name}</strong>.</Body
>
<Select
placeholder={null}
bind:value={selectedRole}
on:change
{options}
label="Select roles:"
/>
</ModalContent>

View File

@ -0,0 +1,105 @@
<script>
import { goto } from "@roxi/routify"
import {
Heading,
Body,
Divider,
Button,
ButtonGroup,
Search,
Table,
Label,
Layout,
Modal,
} from "@budibase/bbui"
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
import AddUserModal from "./_components/AddUserModal.svelte"
import BasicOnboardingModal from "./_components/BasicOnboardingModal.svelte"
import { users } from "stores/portal"
users.init()
const schema = {
email: {},
status: { displayName: "Development Access", type: "boolean" },
// role: { type: "options" },
group: {},
// access: {},
// group: {}
}
let search
let email
$: filteredUsers = $users
.filter(user => user.email.includes(search || ""))
.map(user => ({ ...user, group: ["All"] }))
let createUserModal
let basicOnboardingModal
function openBasicOnoboardingModal() {
createUserModal.hide()
basicOnboardingModal.show()
}
</script>
<Layout>
<div class="heading">
<Heading>Users</Heading>
<Body
>Users are the common denominator in Budibase. Each user is assigned to a
group that contains apps and permissions. In this section, you can add
users, or edit and delete an existing user.</Body
>
</div>
<Divider size="S" />
<div class="users">
<Heading size="S">Users</Heading>
<div class="field">
<Label size="L">Search / filter</Label>
<Search bind:value={search} placeholder="" />
</div>
<div class="buttons">
<ButtonGroup>
<Button disabled secondary>Import users</Button>
<Button overBackground on:click={createUserModal.show}>Add user</Button>
</ButtonGroup>
</div>
<Table
on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema}
data={filteredUsers || $users}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[{ column: "group", component: TagsRenderer }]}
/>
</div>
</Layout>
<Modal bind:this={createUserModal}
><AddUserModal on:change={openBasicOnoboardingModal} /></Modal
>
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal>
<style>
.users {
position: relative;
}
.field {
display: flex;
align-items: center;
flex-direction: row;
grid-gap: var(--spacing-m);
margin: var(--spacing-xl) 0;
}
.field > :global(*) + :global(*) {
margin-left: var(--spacing-m);
}
.buttons {
position: absolute;
top: 0;
right: 0;
}
</style>

View File

@ -0,0 +1,43 @@
<svg
width="18"
height="18"
viewBox="0 0 268 268"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0)">
<path
d="M58.8037 109.043C64.0284 93.2355 74.1116 79.4822 87.615 69.7447C101.118
60.0073 117.352 54.783 134 54.8172C152.872 54.8172 169.934 61.5172 183.334
72.4828L222.328 33.5C198.566 12.7858 168.114 0 134 0C81.1817 0 35.711
30.1277 13.8467 74.2583L58.8037 109.043Z"
fill="#EA4335"
/>
<path
d="M179.113 201.145C166.942 208.995 151.487 213.183 134 213.183C117.418
213.217 101.246 208.034 87.7727 198.369C74.2993 188.703 64.2077 175.044
58.9265 159.326L13.8132 193.574C24.8821 215.978 42.012 234.828 63.2572
247.984C84.5024 261.14 109.011 268.075 134 268C166.752 268 198.041 256.353
221.48 234.5L179.125 201.145H179.113Z"
fill="#34A853"
/>
<path
d="M221.48 234.5C245.991 211.631 261.903 177.595 261.903 134C261.903
126.072 260.686 117.552 258.866 109.634H134V161.414H205.869C202.329
178.823 192.804 192.301 179.125 201.145L221.48 234.5Z"
fill="#4A90E2"
/>
<path
d="M58.9265 159.326C56.1947 151.162 54.8068 142.609 54.8172 134C54.8172
125.268 56.213 116.882 58.8037 109.043L13.8467 74.2584C4.64957 92.825
-0.0915078 113.28 1.86708e-05 134C1.86708e-05 155.44 4.96919 175.652
13.8132 193.574L58.9265 159.326Z"
fill="#FBBC05"
/>
</g>
<defs>
<clipPath id="clip0">
<rect width="268" height="268" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,7 +1,8 @@
<script> <script>
import GoogleLogo from "./logos/Google.svelte" import GoogleLogo from "./_logos/Google.svelte"
import { import {
Button, Button,
Page,
Heading, Heading,
Divider, Divider,
Label, Label,
@ -9,7 +10,6 @@
Layout, Layout,
Input, Input,
Body, Body,
Page,
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import api from "builderStore/api" import api from "builderStore/api"

View File

@ -12,6 +12,7 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { organisation } from "stores/portal" import { organisation } from "stores/portal"
import { post } from "builderStore/api"
import analytics from "analytics" import analytics from "analytics"
let analyticsDisabled = analytics.disabled() let analyticsDisabled = analytics.disabled()
@ -24,18 +25,30 @@
} }
let loading = false let loading = false
let file
$: company = $organisation?.company async function uploadLogo() {
$: logoUrl = $organisation.logoUrl let data = new FormData()
data.append("file", file)
const res = await post("/api/admin/configs/upload/settings/logo", data, {})
return await res.json()
}
async function saveConfig() { async function saveConfig() {
loading = true loading = true
await toggleAnalytics() await toggleAnalytics()
const res = await organisation.save({ ...$organisation, company }) if (file) {
await uploadLogo()
}
const res = await organisation.save({
company: $organisation.company,
platformUrl: $organisation.platformUrl,
})
if (res.status === 200) { if (res.status === 200) {
notifications.success("General settings saved.") notifications.success("Settings saved.")
} else { } else {
notifications.danger("Error when saving settings.") notifications.error(res.message)
} }
loading = false loading = false
} }
@ -46,10 +59,9 @@
<div class="intro"> <div class="intro">
<Heading size="M">General</Heading> <Heading size="M">General</Heading>
<Body> <Body>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Hic vero, aut General is the place where you edit your organisation name, logo. You
culpa provident sunt ratione! Voluptas doloremque, dicta nisi velit can also configure your platform URL as well as turn on or off
perspiciatis, ratione vel blanditiis totam, nam voluptate repellat analytics.
aperiam fuga!
</Body> </Body>
</div> </div>
<Divider size="S" /> <Divider size="S" />
@ -59,14 +71,30 @@
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label size="L">Organization name</Label> <Label size="L">Organization name</Label>
<Input thin bind:value={company} /> <Input thin bind:value={$organisation.company} />
</div> </div>
<!-- <div class="field"> <div class="field logo">
<Label>Logo</Label> <Label size="L">Logo</Label>
<div class="file"> <div class="file">
<Dropzone /> <Dropzone
value={[file]}
on:change={e => {
file = e.detail?.[0]
}}
/>
</div> </div>
</div> --> </div>
</div>
</div>
<Divider size="S" />
<div class="analytics">
<Heading size="S">Platform</Heading>
<Body>Here you can set up general platform settings.</Body>
<div class="fields">
<div class="field">
<Label size="L">Platform URL</Label>
<Input thin bind:value={$organisation.platformUrl} />
</div>
</div> </div>
</div> </div>
<Divider size="S" /> <Divider size="S" />
@ -103,6 +131,9 @@
.file { .file {
max-width: 30ch; max-width: 30ch;
} }
.logo {
align-items: start;
}
.intro { .intro {
display: grid; display: grid;
} }

View File

@ -4,15 +4,16 @@ import { get } from "builderStore/api"
export function createAppStore() { export function createAppStore() {
const store = writable([]) const store = writable([])
async function load() { async function load(status = "") {
try { try {
const res = await get("/api/applications") const res = await get(`/api/applications?status=${status}`)
const json = await res.json() const json = await res.json()
if (res.ok && Array.isArray(json)) { if (res.ok && Array.isArray(json)) {
store.set(json) store.set(json)
} else { } else {
store.set([]) store.set([])
} }
return json
} catch (error) { } catch (error) {
store.set([]) store.set([])
} }

View File

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

View File

@ -1,35 +1,46 @@
import { writable } from "svelte/store" import { writable, get } from "svelte/store"
import api from "builderStore/api" import api from "builderStore/api"
const FALLBACK_CONFIG = {
platformUrl: "",
logoUrl: "",
docsUrl: "",
company: "http://localhost:10000",
}
export function createOrganisationStore() { export function createOrganisationStore() {
const { subscribe, set } = writable({}) const store = writable({})
const { subscribe, set } = store
async function init() { async function init() {
try { const res = await api.get(`/api/admin/configs/settings`)
const response = await api.get(`/api/admin/configs/settings`) const json = await res.json()
const json = await response.json()
set(json) if (json.status === 400) {
} catch (error) { set(FALLBACK_CONFIG)
set({ } else {
platformUrl: "", set({ ...json.config, _rev: json._rev })
logoUrl: "",
docsUrl: "",
company: "",
})
} }
} }
async function save(config) {
const res = await api.post("/api/admin/configs", {
type: "settings",
config,
_rev: get(store)._rev,
})
const json = await res.json()
if (json.status) {
return json
}
await init()
return { status: 200 }
}
return { return {
subscribe, subscribe,
save: async config => { set,
try { save,
await api.post("/api/admin/configs", { type: "settings", config })
await init()
return { status: 200 }
} catch (error) {
return { error }
}
},
init, init,
} }
} }

View File

@ -0,0 +1,65 @@
import { writable } from "svelte/store"
import api, { post } from "builderStore/api"
import { update } from "lodash"
export function createUsersStore() {
const { subscribe, set } = writable([])
async function init() {
const response = await api.get(`/api/admin/users`)
const json = await response.json()
set(json)
}
async function invite(email) {
const response = await api.post(`/api/admin/users/invite`, { email })
return await response.json()
}
async function acceptInvite(inviteCode, password) {
const response = await api.post("/api/admin/users/invite/accept", {
inviteCode,
password,
})
return await response.json()
}
async function create({ email, password }) {
const response = await api.post("/api/admin/users", {
email,
password,
builder: { global: true },
roles: {},
})
init()
return await response.json()
}
async function del(id) {
const response = await api.delete(`/api/admin/users/${id}`)
update(users => users.filter(user => user._id !== id))
return await response.json()
}
async function updateRoles(data) {
try {
const res = await post(`/api/admin/users`, data)
const json = await res.json()
return json
} catch (error) {
console.log(error)
return error
}
}
return {
subscribe,
init,
invite,
acceptInvite,
create,
updateRoles,
del,
}
}
export const users = createUsersStore()

View File

@ -2873,7 +2873,7 @@ svelte-spa-router@^3.0.5:
dependencies: dependencies:
regexparam "1.3.0" regexparam "1.3.0"
svelte@^3.37.0: svelte@^3.38.2:
version "3.38.2" version "3.38.2"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg== integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==

View File

@ -6,6 +6,7 @@
*/ */
const CouchDB = require("../src/db") const CouchDB = require("../src/db")
const { DocumentTypes } = require("../src/db/utils")
const appName = process.argv[2].toLowerCase() const appName = process.argv[2].toLowerCase()
const remoteUrl = process.argv[3] const remoteUrl = process.argv[3]
@ -18,7 +19,7 @@ const run = async () => {
let apps = [] let apps = []
for (let dbName of appDbNames) { for (let dbName of appDbNames) {
const db = new CouchDB(dbName) const db = new CouchDB(dbName)
apps.push(db.get(dbName)) apps.push(db.get(DocumentTypes.APP_METADATA))
} }
apps = await Promise.all(apps) apps = await Promise.all(apps)
const app = apps.find( const app = apps.find(
@ -32,7 +33,7 @@ const run = async () => {
return return
} }
const instanceDb = new CouchDB(app._id) const instanceDb = new CouchDB(app.appId)
const remoteDb = new CouchDB(`${remoteUrl}/${appName}`) const remoteDb = new CouchDB(`${remoteUrl}/${appName}`)
instanceDb.replicate instanceDb.replicate

View File

@ -16,11 +16,11 @@ const {
getLayoutParams, getLayoutParams,
getScreenParams, getScreenParams,
generateScreenID, generateScreenID,
generateDevAppID,
DocumentTypes,
AppStatus,
} = require("../../db/utils") } = require("../../db/utils")
const { const { BUILTIN_ROLE_IDS, AccessController } = require("@budibase/auth/roles")
BUILTIN_ROLE_IDS,
AccessController,
} = require("../../utilities/security/roles")
const { BASE_LAYOUTS } = require("../../constants/layouts") const { BASE_LAYOUTS } = require("../../constants/layouts")
const { const {
createHomeScreen, createHomeScreen,
@ -30,8 +30,9 @@ const { cloneDeep } = require("lodash/fp")
const { processObject } = require("@budibase/string-templates") const { processObject } = require("@budibase/string-templates")
const { getAllApps } = require("../../utilities") const { getAllApps } = require("../../utilities")
const { USERS_TABLE_SCHEMA } = require("../../constants") const { USERS_TABLE_SCHEMA } = require("../../constants")
const { getDeployedApps } = require("../../utilities/builder/hosting") const { getDeployedApps } = require("../../utilities/workerRequests")
const { clientLibraryPath } = require("../../utilities") const { clientLibraryPath } = require("../../utilities")
const { getAllLocks } = require("../../utilities/redis")
const URL_REGEX_SLASH = /\/|\\/g const URL_REGEX_SLASH = /\/|\\/g
@ -84,7 +85,9 @@ async function getAppUrlIfNotInUse(ctx) {
} }
async function createInstance(template) { async function createInstance(template) {
const appId = generateAppID() const baseAppId = generateAppID()
const appId = generateDevAppID(baseAppId)
const db = new CouchDB(appId) const db = new CouchDB(appId)
await db.put({ await db.put({
_id: "_design/database", _id: "_design/database",
@ -114,7 +117,24 @@ async function createInstance(template) {
} }
exports.fetch = async function (ctx) { exports.fetch = async function (ctx) {
ctx.body = await getAllApps() const isDev = ctx.query && ctx.query.status === AppStatus.DEV
const apps = await getAllApps(isDev)
// get the locks for all the dev apps
if (isDev) {
const locks = await getAllLocks()
for (let app of apps) {
const lock = locks.find(lock => lock.appId === app.appId)
if (lock) {
app.lockedBy = lock.user
} else {
// make sure its definitely not present
delete app.lockedBy
}
}
}
ctx.body = apps
} }
exports.fetchAppDefinition = async function (ctx) { exports.fetchAppDefinition = async function (ctx) {
@ -135,7 +155,7 @@ exports.fetchAppDefinition = async function (ctx) {
exports.fetchAppPackage = async function (ctx) { exports.fetchAppPackage = async function (ctx) {
const db = new CouchDB(ctx.params.appId) const db = new CouchDB(ctx.params.appId)
const application = await db.get(ctx.params.appId) const application = await db.get(DocumentTypes.APP_METADATA)
const [layouts, screens] = await Promise.all([getLayouts(db), getScreens(db)]) const [layouts, screens] = await Promise.all([getLayouts(db), getScreens(db)])
ctx.body = { ctx.body = {
@ -160,7 +180,8 @@ exports.create = async function (ctx) {
const url = await getAppUrlIfNotInUse(ctx) const url = await getAppUrlIfNotInUse(ctx)
const appId = instance._id const appId = instance._id
const newApplication = { const newApplication = {
_id: appId, _id: DocumentTypes.APP_METADATA,
appId: instance._id,
type: "app", type: "app",
version: packageJson.version, version: packageJson.version,
componentLibraries: ["@budibase/standard-components"], componentLibraries: ["@budibase/standard-components"],
@ -194,6 +215,12 @@ exports.update = async function (ctx) {
const data = ctx.request.body const data = ctx.request.body
const newData = { ...application, ...data, url } const newData = { ...application, ...data, url }
// the locked by property is attached by server but generated from
// Redis, shouldn't ever store it
if (newData.lockedBy) {
delete newData.lockedBy
}
const response = await db.put(newData) const response = await db.put(newData)
data._rev = response.rev data._rev = response.rev
@ -217,7 +244,7 @@ exports.delete = async function (ctx) {
} }
const createEmptyAppPackage = async (ctx, app) => { const createEmptyAppPackage = async (ctx, app) => {
const db = new CouchDB(app._id) const db = new CouchDB(app.appId)
let screensAndLayouts = [] let screensAndLayouts = []
for (let layout of BASE_LAYOUTS) { for (let layout of BASE_LAYOUTS) {

View File

@ -1,10 +1,11 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { DocumentTypes } = require("../../db/utils")
const { getComponentLibraryManifest } = require("../../utilities/fileSystem") const { getComponentLibraryManifest } = require("../../utilities/fileSystem")
exports.fetchAppComponentDefinitions = async function (ctx) { exports.fetchAppComponentDefinitions = async function (ctx) {
const appId = ctx.params.appId || ctx.appId const appId = ctx.params.appId || ctx.appId
const db = new CouchDB(appId) const db = new CouchDB(appId)
const app = await db.get(appId) const app = await db.get(DocumentTypes.APP_METADATA)
let componentManifests = await Promise.all( let componentManifests = await Promise.all(
app.componentLibraries.map(async library => { app.componentLibraries.map(async library => {

View File

@ -1,5 +1,3 @@
const { getAppQuota } = require("./quota")
const env = require("../../../environment")
const newid = require("../../../db/newid") const newid = require("../../../db/newid")
/** /**
@ -11,24 +9,6 @@ class Deployment {
this._id = id || newid() this._id = id || newid()
} }
// purely so that we can do quota stuff outside the main deployment context
async init() {
if (!env.SELF_HOSTED) {
this.setQuota(await getAppQuota(this.appId))
}
}
setQuota(quota) {
if (!quota) {
return
}
this.quota = quota
}
getQuota() {
return this.quota
}
getAppId() { getAppId() {
return this.appId return this.appId
} }
@ -38,9 +18,6 @@ class Deployment {
return return
} }
this.verification = verification this.verification = verification
if (this.verification.quota) {
this.quota = this.verification.quota
}
} }
getVerification() { getVerification() {
@ -58,9 +35,6 @@ class Deployment {
if (json.verification) { if (json.verification) {
this.setVerification(json.verification) this.setVerification(json.verification)
} }
if (json.quota) {
this.setQuota(json.quota)
}
if (json.status) { if (json.status) {
this.setStatus(json.status, json.err) this.setStatus(json.status, json.err)
} }
@ -78,9 +52,6 @@ class Deployment {
if (this.verification && this.verification.cfDistribution) { if (this.verification && this.verification.cfDistribution) {
obj.cfDistribution = this.verification.cfDistribution obj.cfDistribution = this.verification.cfDistribution
} }
if (this.quota) {
obj.quota = this.quota
}
return obj return obj
} }
} }

View File

@ -1,80 +0,0 @@
const AWS = require("aws-sdk")
const fetch = require("node-fetch")
const env = require("../../../environment")
const {
deployToObjectStore,
performReplication,
fetchCredentials,
} = require("./utils")
/**
* Verifies the users API key and
* Verifies that the deployment fits within the quota of the user
* Links to the "check-api-key" lambda.
* @param {object} deployment - information about the active deployment, including the appId and quota.
*/
exports.preDeployment = async function (deployment) {
const json = await fetchCredentials(env.DEPLOYMENT_CREDENTIALS_URL, {
apiKey: env.BUDIBASE_API_KEY,
appId: deployment.getAppId(),
quota: deployment.getQuota(),
})
// set credentials here, means any time we're verified we're ready to go
if (json.credentials) {
AWS.config.update({
accessKeyId: json.credentials.AccessKeyId,
secretAccessKey: json.credentials.SecretAccessKey,
sessionToken: json.credentials.SessionToken,
})
}
return json
}
/**
* Finalises the deployment, updating the quota for the user API key
* The verification process returns the levels to update to.
* Calls the "deployment-success" lambda.
* @param {object} deployment information about the active deployment, including the quota info.
* @returns {Promise<object>} The usage has been updated against the user API key.
*/
exports.postDeployment = async function (deployment) {
const DEPLOYMENT_SUCCESS_URL =
env.DEPLOYMENT_CREDENTIALS_URL + "deploy/success"
const response = await fetch(DEPLOYMENT_SUCCESS_URL, {
method: "POST",
body: JSON.stringify({
apiKey: env.BUDIBASE_API_KEY,
quota: deployment.getQuota(),
}),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
})
if (response.status !== 200) {
throw new Error(`Error updating deployment quota for API Key`)
}
return await response.json()
}
exports.deploy = async function (deployment) {
const appId = deployment.getAppId()
const { bucket, accountId } = deployment.getVerification()
const metadata = { accountId }
await deployToObjectStore(appId, bucket, metadata)
}
exports.replicateDb = async function (deployment) {
const appId = deployment.getAppId()
const verification = deployment.getVerification()
return performReplication(
appId,
verification.couchDbSession,
env.DEPLOYMENT_DB_URL
)
}

View File

@ -1,9 +1,8 @@
const PouchDB = require("../../../db") const PouchDB = require("../../../db")
const Deployment = require("./Deployment") const Deployment = require("./Deployment")
const { const { Replication, StaticDatabases } = require("@budibase/auth/db")
getHostingInfo, const { DocumentTypes } = require("../../../db/utils")
HostingTypes,
} = require("../../../utilities/builder/hosting")
// the max time we can wait for an invalidation to complete before considering it failed // the max time we can wait for an invalidation to complete before considering it failed
const MAX_PENDING_TIME_MS = 30 * 60000 const MAX_PENDING_TIME_MS = 30 * 60000
const DeploymentStatus = { const DeploymentStatus = {
@ -12,9 +11,6 @@ const DeploymentStatus = {
FAILURE: "FAILURE", FAILURE: "FAILURE",
} }
// default to AWS deployment, this will be updated before use (if required)
let deploymentService = require("./awsDeploy")
// checks that deployments are in a good state, any pending will be updated // checks that deployments are in a good state, any pending will be updated
async function checkAllDeployments(deployments) { async function checkAllDeployments(deployments) {
let updated = false let updated = false
@ -32,16 +28,16 @@ async function checkAllDeployments(deployments) {
return { updated, deployments } return { updated, deployments }
} }
async function storeLocalDeploymentHistory(deployment) { async function storeDeploymentHistory(deployment) {
const appId = deployment.getAppId() const appId = deployment.getAppId()
const deploymentJSON = deployment.getJSON() const deploymentJSON = deployment.getJSON()
const db = new PouchDB(appId) const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
let deploymentDoc let deploymentDoc
try { try {
deploymentDoc = await db.get("_local/deployments") deploymentDoc = await db.get(appId)
} catch (err) { } catch (err) {
deploymentDoc = { _id: "_local/deployments", history: {} } deploymentDoc = { _id: appId, history: {} }
} }
const deploymentId = deploymentJSON._id const deploymentId = deploymentJSON._id
@ -62,28 +58,37 @@ async function storeLocalDeploymentHistory(deployment) {
} }
async function deployApp(deployment) { async function deployApp(deployment) {
const appId = deployment.getAppId()
try { try {
await deployment.init() const productionAppId = deployment.appId.replace("_dev", "")
deployment.setVerification(
await deploymentService.preDeployment(deployment)
)
console.log(`Uploading assets for appID ${appId}..`) const replication = new Replication({
source: deployment.appId,
target: productionAppId,
})
await deploymentService.deploy(deployment) await replication.replicate()
const db = new PouchDB(productionAppId)
const appDoc = await db.get(DocumentTypes.APP_METADATA)
appDoc.appId = productionAppId
appDoc.instance._id = productionAppId
await db.put(appDoc)
// replicate the DB to the main couchDB cluster // Set up live sync between the live and dev instances
console.log("Replicating local PouchDB to CouchDB..") const liveReplication = new Replication({
await deploymentService.replicateDb(deployment) source: productionAppId,
target: deployment.appId,
await deploymentService.postDeployment(deployment) })
liveReplication.subscribe({
filter: function (doc) {
return doc._id !== DocumentTypes.APP_METADATA
},
})
deployment.setStatus(DeploymentStatus.SUCCESS) deployment.setStatus(DeploymentStatus.SUCCESS)
await storeLocalDeploymentHistory(deployment) await storeDeploymentHistory(deployment)
} catch (err) { } catch (err) {
deployment.setStatus(DeploymentStatus.FAILURE, err.message) deployment.setStatus(DeploymentStatus.FAILURE, err.message)
await storeLocalDeploymentHistory(deployment) await storeDeploymentHistory(deployment)
throw { throw {
...err, ...err,
message: `Deployment Failed: ${err.message}`, message: `Deployment Failed: ${err.message}`,
@ -93,8 +98,8 @@ async function deployApp(deployment) {
exports.fetchDeployments = async function (ctx) { exports.fetchDeployments = async function (ctx) {
try { try {
const db = new PouchDB(ctx.appId) const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
const deploymentDoc = await db.get("_local/deployments") const deploymentDoc = await db.get(ctx.appId)
const { updated, deployments } = await checkAllDeployments( const { updated, deployments } = await checkAllDeployments(
deploymentDoc, deploymentDoc,
ctx.user ctx.user
@ -110,8 +115,8 @@ exports.fetchDeployments = async function (ctx) {
exports.deploymentProgress = async function (ctx) { exports.deploymentProgress = async function (ctx) {
try { try {
const db = new PouchDB(ctx.appId) const db = new PouchDB(StaticDatabases.DEPLOYMENTS.name)
const deploymentDoc = await db.get("_local/deployments") const deploymentDoc = await db.get(ctx.appId)
ctx.body = deploymentDoc[ctx.params.deploymentId] ctx.body = deploymentDoc[ctx.params.deploymentId]
} catch (err) { } catch (err) {
ctx.throw( ctx.throw(
@ -122,15 +127,9 @@ exports.deploymentProgress = async function (ctx) {
} }
exports.deployApp = async function (ctx) { exports.deployApp = async function (ctx) {
// start by checking whether to deploy local or to cloud
const hostingInfo = await getHostingInfo()
deploymentService =
hostingInfo.type === HostingTypes.CLOUD
? require("./awsDeploy")
: require("./selfDeploy")
let deployment = new Deployment(ctx.appId) let deployment = new Deployment(ctx.appId)
deployment.setStatus(DeploymentStatus.PENDING) deployment.setStatus(DeploymentStatus.PENDING)
deployment = await storeLocalDeploymentHistory(deployment) deployment = await storeDeploymentHistory(deployment)
await deployApp(deployment) await deployApp(deployment)

View File

@ -1,39 +0,0 @@
const PouchDB = require("../../../db")
const {
DocumentTypes,
SEPARATOR,
UNICODE_MAX,
ViewNames,
} = require("../../../db/utils")
exports.getAppQuota = async function (appId) {
const db = new PouchDB(appId)
const rows = await db.allDocs({
startkey: DocumentTypes.ROW + SEPARATOR,
endkey: DocumentTypes.ROW + SEPARATOR + UNICODE_MAX,
})
const users = await db.allDocs({
startkey: DocumentTypes.USER + SEPARATOR,
endkey: DocumentTypes.USER + SEPARATOR + UNICODE_MAX,
})
const existingRows = rows.rows.length
const existingUsers = users.rows.length
const designDoc = await db.get("_design/database")
let views = 0
for (let viewName of Object.keys(designDoc.views)) {
if (Object.values(ViewNames).indexOf(viewName) === -1) {
views++
}
}
return {
rows: existingRows,
users: existingUsers,
views: views,
}
}

View File

@ -1,60 +0,0 @@
const AWS = require("aws-sdk")
const {
deployToObjectStore,
performReplication,
fetchCredentials,
} = require("./utils")
const {
getWorkerUrl,
getCouchUrl,
getSelfHostKey,
} = require("../../../utilities/builder/hosting")
exports.preDeployment = async function () {
const url = `${await getWorkerUrl()}/api/deploy`
try {
const json = await fetchCredentials(url, {
selfHostKey: await getSelfHostKey(),
})
// response contains:
// couchDbSession, bucket, objectStoreSession
// set credentials here, means any time we're verified we're ready to go
if (json.objectStoreSession) {
AWS.config.update({
accessKeyId: json.objectStoreSession.accessKeyId,
secretAccessKey: json.objectStoreSession.secretAccessKey,
})
}
return json
} catch (err) {
throw {
message: "Unauthorised to deploy, check self hosting key",
status: 401,
}
}
}
exports.postDeployment = async function () {
// we don't actively need to do anything after deployment in self hosting
}
exports.deploy = async function (deployment) {
const appId = deployment.getAppId()
const verification = deployment.getVerification()
// no metadata, aws has account ID in metadata
const metadata = {}
await deployToObjectStore(appId, verification.bucket, metadata)
}
exports.replicateDb = async function (deployment) {
const appId = deployment.getAppId()
const verification = deployment.getVerification()
return performReplication(
appId,
verification.couchDbSession,
await getCouchUrl()
)
}

View File

@ -1,136 +0,0 @@
const { join } = require("../../../utilities/centralPath")
const fs = require("fs")
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
const fetch = require("node-fetch")
const PouchDB = require("../../../db")
const CouchDB = require("pouchdb")
const { upload } = require("../../../utilities/fileSystem")
const { attachmentsRelativeURL } = require("../../../utilities")
// TODO: everything in this file is to be removed
function walkDir(dirPath, callback) {
for (let filename of fs.readdirSync(dirPath)) {
const filePath = `${dirPath}/${filename}`
const stat = fs.lstatSync(filePath)
if (stat.isFile()) {
callback(filePath)
} else {
walkDir(filePath, callback)
}
}
}
exports.fetchCredentials = async function (url, body) {
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(body),
headers: { "Content-Type": "application/json" },
})
const json = await response.json()
if (json.errors) {
throw new Error(json.errors)
}
if (response.status !== 200) {
throw new Error(
`Error fetching temporary credentials: ${JSON.stringify(json)}`
)
}
return json
}
exports.prepareUpload = async function ({ s3Key, bucket, metadata, file }) {
const response = await upload({
bucket,
metadata,
filename: s3Key,
path: file.path,
type: file.type,
})
// don't store a URL, work this out on the way out as the URL could change
return {
size: file.size,
name: file.name,
url: attachmentsRelativeURL(response.Key),
extension: [...file.name.split(".")].pop(),
key: response.Key,
}
}
exports.deployToObjectStore = async function (appId, bucket, metadata) {
const appAssetsPath = join(budibaseAppsDir(), appId, "public")
let uploads = []
// Upload HTML, CSS and JS for each page of the web app
walkDir(appAssetsPath, function (filePath) {
const filePathParts = filePath.split("/")
const appAssetUpload = exports.prepareUpload({
bucket,
file: {
path: filePath,
name: filePathParts.pop(),
},
s3Key: filePath.replace(appAssetsPath, `assets/${appId}`),
metadata,
})
uploads.push(appAssetUpload)
})
// Upload file attachments
const db = new PouchDB(appId)
let fileUploads
try {
fileUploads = await db.get("_local/fileuploads")
} catch (err) {
fileUploads = { _id: "_local/fileuploads", uploads: [] }
}
for (let file of fileUploads.uploads) {
if (file.uploaded) continue
const attachmentUpload = exports.prepareUpload({
file,
s3Key: `assets/${appId}/attachments/${file.processedFileName}`,
bucket,
metadata,
})
uploads.push(attachmentUpload)
// mark file as uploaded
file.uploaded = true
}
db.put(fileUploads)
try {
return await Promise.all(uploads)
} catch (err) {
console.error("Error uploading budibase app assets to s3", err)
throw err
}
}
exports.performReplication = (appId, session, dbUrl) => {
return new Promise((resolve, reject) => {
const local = new PouchDB(appId)
const remote = new CouchDB(`${dbUrl}/${appId}`, {
fetch: function (url, opts) {
opts.headers.set("Cookie", `${session};`)
return CouchDB.fetch(url, opts)
},
})
const replication = local.sync(remote)
replication.on("complete", () => resolve())
replication.on("error", err => reject(err))
})
}

View File

@ -1,7 +1,11 @@
const fetch = require("node-fetch") const fetch = require("node-fetch")
const CouchDB = require("../../db")
const env = require("../../environment") const env = require("../../environment")
const { checkSlashesInUrl } = require("../../utilities") const { checkSlashesInUrl } = require("../../utilities")
const { request } = require("../../utilities/workerRequests") const { request } = require("../../utilities/workerRequests")
const { clearLock } = require("../../utilities/redis")
const { Replication } = require("@budibase/auth").db
const { DocumentTypes } = require("../../db/utils")
async function redirect(ctx, method) { async function redirect(ctx, method) {
const { devPath } = ctx.params const { devPath } = ctx.params
@ -32,3 +36,49 @@ exports.redirectPost = async ctx => {
exports.redirectDelete = async ctx => { exports.redirectDelete = async ctx => {
await redirect(ctx, "DELETE") await redirect(ctx, "DELETE")
} }
exports.clearLock = async ctx => {
const { appId } = ctx.params
try {
await clearLock(appId, ctx.user)
} catch (err) {
ctx.throw(400, `Unable to remove lock. ${err}`)
}
ctx.body = {
message: "Lock released successfully.",
}
}
exports.revert = async ctx => {
const { appId } = ctx.params
const productionAppId = appId.replace("_dev", "")
// App must have been deployed first
try {
const db = new CouchDB(productionAppId, { skip_setup: true })
const info = await db.info()
if (info.error) throw info.error
} catch (err) {
return ctx.throw(400, "App has not yet been deployed")
}
try {
const replication = new Replication({
source: productionAppId,
target: appId,
})
await replication.rollback()
// update appID in reverted app to be dev version again
const db = new CouchDB(appId)
const appDoc = await db.get(DocumentTypes.APP_METADATA)
appDoc.appId = appId
appDoc.instance._id = appId
await db.put(appDoc)
ctx.body = {
message: "Reverted changes successfully.",
}
} catch (err) {
ctx.throw(400, `Unable to revert. ${err}`)
}
}

View File

@ -1,41 +1,19 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { const { getDeployedApps } = require("../../utilities/workerRequests")
getHostingInfo, const { getScopedConfig } = require("@budibase/auth/db")
getDeployedApps, const { Configs } = require("@budibase/auth").constants
HostingTypes, const { checkSlashesInUrl } = require("../../utilities")
getAppUrl,
} = require("../../utilities/builder/hosting")
const { StaticDatabases } = require("../../db/utils")
exports.fetchInfo = async ctx => {
ctx.body = {
types: Object.values(HostingTypes),
}
}
exports.save = async ctx => {
const db = new CouchDB(StaticDatabases.BUILDER_HOSTING.name)
const { type } = ctx.request.body
if (type === HostingTypes.CLOUD && ctx.request.body._rev) {
ctx.body = await db.remove({
...ctx.request.body,
_id: StaticDatabases.BUILDER_HOSTING.baseDoc,
})
} else {
ctx.body = await db.put({
...ctx.request.body,
_id: StaticDatabases.BUILDER_HOSTING.baseDoc,
})
}
}
exports.fetch = async ctx => {
ctx.body = await getHostingInfo()
}
exports.fetchUrls = async ctx => { exports.fetchUrls = async ctx => {
const appId = ctx.appId
const db = new CouchDB(appId)
const settings = await getScopedConfig(db, { type: Configs.SETTINGS })
let appUrl = "http://localhost:10000/app"
if (settings && settings["platformUrl"]) {
appUrl = checkSlashesInUrl(`${settings["platformUrl"]}/app`)
}
ctx.body = { ctx.body = {
app: await getAppUrl(ctx.appId), app: appUrl,
} }
} }

View File

@ -3,19 +3,19 @@ const {
PermissionLevels, PermissionLevels,
isPermissionLevelHigherThanRead, isPermissionLevelHigherThanRead,
higherPermission, higherPermission,
} = require("../../utilities/security/permissions") } = require("@budibase/auth/permissions")
const { const {
isBuiltin, isBuiltin,
getDBRoleID, getDBRoleID,
getExternalRoleID, getExternalRoleID,
getBuiltinRoles, getBuiltinRoles,
} = require("../../utilities/security/roles") } = require("@budibase/auth/roles")
const { getRoleParams } = require("../../db/utils") const { getRoleParams } = require("../../db/utils")
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { const {
CURRENTLY_SUPPORTED_LEVELS, CURRENTLY_SUPPORTED_LEVELS,
getBasePermissions, getBasePermissions,
} = require("../../utilities/security/utilities") } = require("../../utilities/security")
const PermissionUpdateType = { const PermissionUpdateType = {
REMOVE: "remove", REMOVE: "remove",

View File

@ -58,6 +58,9 @@ async function enrichQueryFields(fields, parameters) {
// enrich the fields with dynamic parameters // enrich the fields with dynamic parameters
for (let key of Object.keys(fields)) { for (let key of Object.keys(fields)) {
if (fields[key] == null) {
continue
}
if (typeof fields[key] === "object") { if (typeof fields[key] === "object") {
// enrich nested fields object // enrich nested fields object
enrichedQuery[key] = await enrichQueryFields(fields[key], parameters) enrichedQuery[key] = await enrichQueryFields(fields[key], parameters)

View File

@ -1,15 +1,12 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { const {
getBuiltinRoles,
BUILTIN_ROLE_IDS,
Role, Role,
getRole, getRole,
isBuiltin, isBuiltin,
getExternalRoleID, getAllRoles,
} = require("../../utilities/security/roles") } = require("@budibase/auth/roles")
const { const {
generateRoleID, generateRoleID,
getRoleParams,
getUserMetadataParams, getUserMetadataParams,
InternalTables, InternalTables,
} = require("../../db/utils") } = require("../../db/utils")
@ -19,14 +16,6 @@ const UpdateRolesOptions = {
REMOVED: "removed", REMOVED: "removed",
} }
// exclude internal roles like builder
const EXTERNAL_BUILTIN_ROLE_IDS = [
BUILTIN_ROLE_IDS.ADMIN,
BUILTIN_ROLE_IDS.POWER,
BUILTIN_ROLE_IDS.BASIC,
BUILTIN_ROLE_IDS.PUBLIC,
]
async function updateRolesOnUserTable(db, roleId, updateOption) { async function updateRolesOnUserTable(db, roleId, updateOption) {
const table = await db.get(InternalTables.USER_METADATA) const table = await db.get(InternalTables.USER_METADATA)
const schema = table.schema const schema = table.schema
@ -51,31 +40,7 @@ async function updateRolesOnUserTable(db, roleId, updateOption) {
} }
exports.fetch = async function (ctx) { exports.fetch = async function (ctx) {
const db = new CouchDB(ctx.appId) ctx.body = await getAllRoles(ctx.appId)
const body = await db.allDocs(
getRoleParams(null, {
include_docs: true,
})
)
let roles = body.rows.map(row => row.doc)
const builtinRoles = getBuiltinRoles()
// need to combine builtin with any DB record of them (for sake of permissions)
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
const builtinRole = builtinRoles[builtinRoleId]
const dbBuiltin = roles.filter(
dbRole => getExternalRoleID(dbRole._id) === builtinRoleId
)[0]
if (dbBuiltin == null) {
roles.push(builtinRole)
} else {
// remove role and all back after combining with the builtin
roles = roles.filter(role => role._id !== dbBuiltin._id)
dbBuiltin._id = getExternalRoleID(dbBuiltin._id)
roles.push(Object.assign(builtinRole, dbBuiltin))
}
}
ctx.body = roles
} }
exports.find = async function (ctx) { exports.find = async function (ctx) {

View File

@ -2,7 +2,7 @@ const { getRoutingInfo } = require("../../utilities/routing")
const { const {
getUserRoleHierarchy, getUserRoleHierarchy,
BUILTIN_ROLE_IDS, BUILTIN_ROLE_IDS,
} = require("../../utilities/security/roles") } = require("@budibase/auth/roles")
const URL_SEPARATOR = "/" const URL_SEPARATOR = "/"

View File

@ -1,6 +1,6 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { getScreenParams, generateScreenID } = require("../../db/utils") const { getScreenParams, generateScreenID } = require("../../db/utils")
const { AccessController } = require("../../utilities/security/roles") const { AccessController } = require("@budibase/auth/roles")
exports.fetch = async ctx => { exports.fetch = async ctx => {
const appId = ctx.appId const appId = ctx.appId

View File

@ -9,14 +9,16 @@ class ScriptExecutor {
} }
execute() { execute() {
const returnValue = this.script.runInContext(this.context) return this.script.runInContext(this.context)
return returnValue
} }
} }
exports.execute = async function (ctx) { exports.execute = async function (ctx) {
const executor = new ScriptExecutor(ctx.request.body) const executor = new ScriptExecutor(ctx.request.body)
const result = executor.execute() ctx.body = executor.execute()
ctx.body = result }
exports.save = async function (ctx) {
ctx.throw(501, "Not currently implemented")
} }

View File

@ -1,5 +1,4 @@
const { SearchIndexes } = require("../../../db/utils") const { SearchIndexes } = require("../../../db/utils")
const { checkSlashesInUrl } = require("../../../utilities")
const env = require("../../../environment") const env = require("../../../environment")
const fetch = require("node-fetch") const fetch = require("node-fetch")
@ -10,7 +9,7 @@ const fetch = require("node-fetch")
* @returns {string} * @returns {string}
*/ */
const luceneEscape = value => { const luceneEscape = value => {
return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&") return `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
} }
/** /**

View File

@ -5,10 +5,9 @@ const { resolve, join } = require("../../../utilities/centralPath")
const fetch = require("node-fetch") const fetch = require("node-fetch")
const uuid = require("uuid") const uuid = require("uuid")
const { ObjectStoreBuckets } = require("../../../constants") const { ObjectStoreBuckets } = require("../../../constants")
const { prepareUpload } = require("../deploy/utils")
const { processString } = require("@budibase/string-templates") const { processString } = require("@budibase/string-templates")
const { budibaseTempDir } = require("../../../utilities/budibaseDir") const { budibaseTempDir } = require("../../../utilities/budibaseDir")
const { getDeployedApps } = require("../../../utilities/builder/hosting") const { getDeployedApps } = require("../../../utilities/workerRequests")
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const { const {
loadHandlebarsFile, loadHandlebarsFile,
@ -17,6 +16,28 @@ const {
} = require("../../../utilities/fileSystem") } = require("../../../utilities/fileSystem")
const env = require("../../../environment") const env = require("../../../environment")
const { objectStoreUrl, clientLibraryPath } = require("../../../utilities") const { objectStoreUrl, clientLibraryPath } = require("../../../utilities")
const { upload } = require("../../../utilities/fileSystem")
const { attachmentsRelativeURL } = require("../../../utilities")
const { DocumentTypes } = require("../../../db/utils")
async function prepareUpload({ s3Key, bucket, metadata, file }) {
const response = await upload({
bucket,
metadata,
filename: s3Key,
path: file.path,
type: file.type,
})
// don't store a URL, work this out on the way out as the URL could change
return {
size: file.size,
name: file.name,
url: attachmentsRelativeURL(response.Key),
extension: [...file.name.split(".")].pop(),
key: response.Key,
}
}
async function checkForSelfHostedURL(ctx) { async function checkForSelfHostedURL(ctx) {
// the "appId" component of the URL may actually be a specific self hosted URL // the "appId" component of the URL may actually be a specific self hosted URL
@ -65,7 +86,7 @@ exports.serveApp = async function (ctx) {
} }
const App = require("./templates/BudibaseApp.svelte").default const App = require("./templates/BudibaseApp.svelte").default
const db = new CouchDB(appId, { skip_setup: true }) const db = new CouchDB(appId, { skip_setup: true })
const appInfo = await db.get(appId) const appInfo = await db.get(DocumentTypes.APP_METADATA)
const { head, html, css } = App.render({ const { head, html, css } = App.render({
title: appInfo.name, title: appInfo.name,
@ -95,13 +116,17 @@ exports.serveComponentLibrary = async function (ctx) {
if (env.isDev() || env.isTest()) { if (env.isDev() || env.isTest()) {
const componentLibraryPath = join( const componentLibraryPath = join(
budibaseTempDir(), budibaseTempDir(),
decodeURI(ctx.query.library), appId,
"node_modules",
"@budibase",
"standard-components",
"package",
"dist" "dist"
) )
return send(ctx, "/awsDeploy.js", { root: componentLibraryPath }) return send(ctx, "/index.js", { root: componentLibraryPath })
} }
const db = new CouchDB(appId) const db = new CouchDB(appId)
const appInfo = await db.get(appId) const appInfo = await db.get(DocumentTypes.APP_METADATA)
let componentLib = "componentlibrary" let componentLib = "componentlibrary"
if (appInfo && appInfo.version) { if (appInfo && appInfo.version) {

View File

@ -5,7 +5,7 @@ const {
getGlobalIDFromUserMetadataID, getGlobalIDFromUserMetadataID,
} = require("../../db/utils") } = require("../../db/utils")
const { InternalTables } = require("../../db/utils") const { InternalTables } = require("../../db/utils")
const { getRole } = require("../../utilities/security/roles") const { getRole, BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { const {
getGlobalUsers, getGlobalUsers,
saveGlobalUser, saveGlobalUser,
@ -73,6 +73,9 @@ exports.createMetadata = async function (ctx) {
exports.updateSelfMetadata = async function (ctx) { exports.updateSelfMetadata = async function (ctx) {
// overwrite the ID with current users // overwrite the ID with current users
ctx.request.body._id = ctx.user._id ctx.request.body._id = ctx.user._id
if (ctx.user.builder && ctx.user.builder.global) {
ctx.request.body.roleId = BUILTIN_ROLE_IDS.ADMIN
}
// make sure no stale rev // make sure no stale rev
delete ctx.request.body._rev delete ctx.request.body._rev
await exports.updateMetadata(ctx) await exports.updateMetadata(ctx)

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const controller = require("../controllers/analytics") const controller = require("../controllers/analytics")
const { BUILDER } = require("../../utilities/security/permissions") const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/apikeys") const controller = require("../controllers/apikeys")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions") const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/application") const controller = require("../controllers/application")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions") const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()

View File

@ -6,7 +6,7 @@ const {
BUILDER, BUILDER,
PermissionLevels, PermissionLevels,
PermissionTypes, PermissionTypes,
} = require("../../utilities/security/permissions") } = require("@budibase/auth/permissions")
const Joi = require("joi") const Joi = require("joi")
const { bodyResource, paramResource } = require("../../middleware/resourceId") const { bodyResource, paramResource } = require("../../middleware/resourceId")

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/backup") const controller = require("../controllers/backup")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions") const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/component") const controller = require("../controllers/component")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions") const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()

View File

@ -5,7 +5,7 @@ const {
BUILDER, BUILDER,
PermissionLevels, PermissionLevels,
PermissionTypes, PermissionTypes,
} = require("../../utilities/security/permissions") } = require("@budibase/auth/permissions")
const router = Router() const router = Router()

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/deploy") const controller = require("../controllers/deploy")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions") const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()

View File

@ -1,6 +1,8 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/dev") const controller = require("../controllers/dev")
const env = require("../../environment") const env = require("../../environment")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()
@ -11,4 +13,8 @@ if (env.isDev() || env.isTest()) {
.delete("/api/admin/:devPath(.*)", controller.redirectDelete) .delete("/api/admin/:devPath(.*)", controller.redirectDelete)
} }
router
.delete("/api/dev/:appId/lock", authorized(BUILDER), controller.clearLock)
.post("/api/dev/:appId/revert", authorized(BUILDER), controller.revert)
module.exports = router module.exports = router

View File

@ -2,15 +2,12 @@ const Router = require("@koa/router")
const controller = require("../controllers/hosting") const controller = require("../controllers/hosting")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const selfhost = require("../../middleware/selfhost") const selfhost = require("../../middleware/selfhost")
const { BUILDER } = require("../../utilities/security/permissions") const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()
router router
.get("/api/hosting/info", authorized(BUILDER), controller.fetchInfo)
.get("/api/hosting/urls", authorized(BUILDER), controller.fetchUrls) .get("/api/hosting/urls", authorized(BUILDER), controller.fetchUrls)
.get("/api/hosting", authorized(BUILDER), controller.fetch)
.post("/api/hosting", authorized(BUILDER), controller.save)
// this isn't risky, doesn't return anything about apps other than names and URLs // this isn't risky, doesn't return anything about apps other than names and URLs
.get("/api/hosting/apps", selfhost, controller.getDeployedApps) .get("/api/hosting/apps", selfhost, controller.getDeployedApps)

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/integration") const controller = require("../controllers/integration")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions") const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()

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