Merge branch 'next' of github.com:Budibase/budibase into user-app-list
This commit is contained in:
commit
0251ee366f
|
@ -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.
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require("./src/db/utils")
|
|
@ -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",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require("./src/security/permissions")
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
Client: require("./src/redis"),
|
||||||
|
utils: require("./src/redis/utils"),
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require("./src/security/roles")
|
|
@ -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",
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
on:click
|
||||||
{href}
|
{href}
|
||||||
{target}
|
{target}
|
||||||
class:spectrum-Link--primary={primary}
|
class:spectrum-Link--primary={primary}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
@ -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) =>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
export { default as fetchData } from "./fetchData"
|
||||||
|
export {
|
||||||
|
buildStyle,
|
||||||
|
convertCamel,
|
||||||
|
pipe,
|
||||||
|
capitalise,
|
||||||
|
get_name,
|
||||||
|
get_capitalised_name,
|
||||||
|
} from "./helpers"
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { emailValidator, requiredValidator } from "./validators"
|
||||||
|
export { createValidationStore } from "./validation"
|
|
@ -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)
|
||||||
|
}
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
<!-- routify:options index=4 -->
|
|
||||||
<slot />
|
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
import { Page } from "@budibase/bbui"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Page>
|
||||||
|
<slot />
|
||||||
|
</Page>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -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>
|
|
@ -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",
|
|
@ -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 = {
|
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 user’s 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 |
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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([])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
@ -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==
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 = "/"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, "\\$&")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue