Merge branch 'create-new-app' into feature/create-app-builder-ui

This commit is contained in:
kevmodrome 2020-05-26 17:37:41 +02:00
commit a028e2e3aa
41 changed files with 1113 additions and 163 deletions

View File

@ -11,13 +11,6 @@ module.exports = {
default: "~/.budibase",
alias: "d",
})
yargs.positional("database", {
type: "string",
describe: "use a local (PouchDB) or remote (CouchDB) database",
alias: "b",
default: "local",
choices: ["local", "remote"],
})
yargs.positional("clientId", {
type: "string",
describe: "used to determine the name of the global databse",
@ -28,7 +21,7 @@ module.exports = {
type: "string",
describe:
"connection string for couch db, format: https://username:password@localhost:5984",
alias: "x",
alias: "u",
default: "",
})
yargs.positional("quiet", {

View File

@ -14,7 +14,6 @@ const run = async opts => {
try {
await ensureAppDir(opts)
await setEnvironmentVariables(opts)
await prompts(opts)
await createClientDatabase(opts)
await createDevEnvFile(opts)
console.log(chalk.green("Budibase successfully initialised."))
@ -24,13 +23,13 @@ const run = async opts => {
}
const setEnvironmentVariables = async opts => {
if (opts.database === "local") {
if (opts.couchDbUrl) {
process.env.COUCH_DB_URL = opts.couchDbUrl
} else {
const dataDir = join(opts.dir, ".data")
await ensureDir(dataDir)
process.env.COUCH_DB_URL =
dataDir + (dataDir.endsWith("/") || dataDir.endsWith("\\") ? "" : "/")
} else {
process.env.COUCH_DB_URL = opts.couchDbUrl
}
}
@ -39,25 +38,6 @@ const ensureAppDir = async opts => {
await ensureDir(opts.dir)
}
const prompts = async opts => {
const questions = [
{
type: "input",
name: "couchDbUrl",
message:
"CouchDB Connection String (e.g. https://user:password@localhost:5984): ",
validate: function(value) {
return !!value || "Please enter connection string"
},
},
]
if (opts.database === "remote" && !opts.couchDbUrl) {
const answers = await inquirer.prompt(questions)
opts.couchDbUrl = answers.couchDbUrl
}
}
const createClientDatabase = async opts => {
// cannot be a top level require as it
// will cause environment module to be loaded prematurely

View File

@ -49,6 +49,19 @@
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest - Access Levels",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["accesslevel.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",

View File

@ -24,7 +24,7 @@
},
"scripts": {
"test": "jest routes --runInBand",
"test:integration": "jest routes --runInBand",
"test:integration": "jest workflow --runInBand",
"test:watch": "jest -w",
"initialise": "node ../cli/bin/budi init -b local -q",
"budi": "node ../cli/bin/budi",

View File

@ -0,0 +1,108 @@
const CouchDB = require("../../db")
const newid = require("../../db/newid")
const {
generateAdminPermissions,
generatePowerUserPermissions,
POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID,
} = require("../../utilities/accessLevels")
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const body = await db.query("database/by_type", {
include_docs: true,
key: ["accesslevel"],
})
const customAccessLevels = body.rows.map(row => row.doc)
const staticAccessLevels = [
{
_id: ADMIN_LEVEL_ID,
name: "Admin",
permissions: await generateAdminPermissions(ctx.params.instanceId),
},
{
_id: POWERUSER_LEVEL_ID,
name: "Power User",
permissions: await generatePowerUserPermissions(ctx.params.instanceId),
},
]
ctx.body = [...staticAccessLevels, ...customAccessLevels]
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.get(ctx.params.levelId)
}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const level = await db.get(ctx.params.levelId)
level.name = ctx.body.name
level.permissions = ctx.request.body.permissions
const result = await db.put(level)
level._rev = result.rev
ctx.body = level
ctx.message = `Level ${level.name} updated successfully.`
}
exports.patch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const level = await db.get(ctx.params.levelId)
const { removedPermissions, addedPermissions, _rev } = ctx.request.body
if (!_rev) throw new Error("Must supply a _rev to update an access level")
level._rev = _rev
if (removedPermissions) {
level.permissions = level.permissions.filter(
p =>
!removedPermissions.some(
rem => rem.name === p.name && rem.itemId === p.itemId
)
)
}
if (addedPermissions) {
level.permissions = [
...level.permissions.filter(
p =>
!addedPermissions.some(
add => add.name === p.name && add.itemId === p.itemId
)
),
...addedPermissions,
]
}
const result = await db.put(level)
level._rev = result.rev
ctx.body = level
ctx.message = `Access Level ${level.name} updated successfully.`
}
exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const level = {
name: ctx.request.body.name,
_rev: ctx.request.body._rev,
permissions: ctx.request.body.permissions || [],
_id: newid(),
type: "accesslevel",
}
const result = await db.put(level)
level._rev = result.rev
ctx.body = level
ctx.message = `Access Level '${level.name}' created successfully.`
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
await db.remove(ctx.params.levelId, ctx.params.rev)
ctx.message = `Access Level ${ctx.params.id} deleted successfully`
ctx.status = 200
}

View File

@ -3,6 +3,10 @@ const ClientDb = require("../../db/clientDb")
const { getPackageForBuilder } = require("../../utilities/builder")
const newid = require("../../db/newid")
const env = require("../../environment")
const instanceController = require("./instance")
const { resolve, join } = require("path")
const { copy, readJSON, writeJSON, exists } = require("fs-extra")
const { exec } = require("child_process")
exports.fetch = async function(ctx) {
const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
@ -32,12 +36,77 @@ exports.create = async function(ctx) {
"@budibase/standard-components",
"@budibase/materialdesign-components",
],
...ctx.request.body,
name: ctx.request.body.name,
description: ctx.request.body.description,
}
const { rev } = await db.post(newApplication)
newApplication._rev = rev
const createInstCtx = {
params: {
clientId: env.CLIENT_ID,
applicationId: newApplication._id,
},
request: {
body: { name: `dev-${env.CLIENT_ID}` },
},
}
await instanceController.create(createInstCtx)
if (env.NODE_ENV === "production") {
const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
await runNpmInstall(newAppFolder)
}
ctx.body = newApplication
ctx.message = `Application ${ctx.request.body.name} created successfully`
}
const createEmptyAppPackage = async (ctx, app) => {
const templateFolder = resolve(
__dirname,
"..",
"..",
"utilities",
"appDirectoryTemplate"
)
const appsFolder = env.BUDIBASE_DIR
const newAppFolder = resolve(appsFolder, app._id)
if (await exists(newAppFolder)) {
ctx.throw(400, "App folder already exists for this application")
return
}
await copy(templateFolder, newAppFolder)
const packageJsonPath = join(appsFolder, app._id, "package.json")
const packageJson = await readJSON(packageJsonPath)
packageJson.name = npmFriendlyAppName(app.name)
await writeJSON(packageJsonPath, packageJson)
return newAppFolder
}
const runNpmInstall = async newAppFolder => {
return new Promise((resolve, reject) => {
const cmd = `cd ${newAppFolder} && npm install`
exec(cmd, (error, stdout, stderr) => {
if (error) {
reject(error)
}
resolve(stdout ? stdout : stderr)
})
})
}
const npmFriendlyAppName = name =>
name
.replace(/_/g, "")
.replace(/./g, "")
.replace(/ /g, "")
.toLowerCase()

View File

@ -37,7 +37,7 @@ exports.authenticate = async ctx => {
if (await bcrypt.compare(password, dbUser.password)) {
const payload = {
userId: dbUser._id,
accessLevel: "",
accessLevelId: dbUser.accessLevelId,
instanceId: instanceId,
}

View File

@ -7,6 +7,7 @@ const ajv = new Ajv()
exports.save = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const record = ctx.request.body
record.modelId = ctx.params.modelId
if (!record._rev && !record._id) {
record._id = newid()
@ -43,16 +44,12 @@ exports.save = async function(ctx) {
record.type = "record"
const response = await db.post(record)
record._rev = response.rev
// await ctx.publish(events.recordApi.save.onRecordCreated, {
// record: record,
// })
ctx.body = record
ctx.status = 200
ctx.message = `${model.name} created successfully`
}
exports.fetch = async function(ctx) {
exports.fetchView = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const response = await db.query(`database/${ctx.params.viewName}`, {
include_docs: true,
@ -60,13 +57,30 @@ exports.fetch = async function(ctx) {
ctx.body = response.rows.map(row => row.doc)
}
exports.fetchModel = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const response = await db.query(`database/all_${ctx.params.modelId}`, {
include_docs: true,
})
ctx.body = response.rows.map(row => row.doc)
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.get(ctx.params.recordId)
const record = await db.get(ctx.params.recordId)
if (record.modelId !== ctx.params.modelId) {
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
return
}
ctx.body = record
}
exports.destroy = async function(ctx) {
const databaseId = ctx.params.instanceId
const db = new CouchDB(databaseId)
const db = new CouchDB(ctx.params.instanceId)
const record = await db.get(ctx.params.recordId)
if (record.modelId !== ctx.params.modelId) {
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
return
}
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
}

View File

@ -2,8 +2,11 @@ const CouchDB = require("../../db")
const clientDb = require("../../db/clientDb")
const bcrypt = require("../../utilities/bcrypt")
const env = require("../../environment")
const getUserId = userName => `user_${userName}`
const {
POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID,
} = require("../../utilities/accessLevels")
exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId)
@ -18,17 +21,26 @@ exports.fetch = async function(ctx) {
exports.create = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId)
const appId = (await database.get("_design/database")).metadata.applicationId
const { username, password, name } = ctx.request.body
const { username, password, name, accessLevelId } = ctx.request.body
if (!username || !password) ctx.throw(400, "Username and Password Required.")
if (!username || !password) {
ctx.throw(400, "Username and Password Required.")
}
const response = await database.post({
const accessLevel = await checkAccessLevel(database, accessLevelId)
if (!accessLevel) ctx.throw(400, "Invalid Access Level")
const user = {
_id: getUserId(username),
username,
password: await bcrypt.hash(password),
name: name || username,
type: "user",
})
accessLevelId,
}
const response = await database.post(user)
// the clientDB needs to store a map of users against the app
const db = new CouchDB(clientDb.name(env.CLIENT_ID))
@ -49,6 +61,8 @@ exports.create = async function(ctx) {
}
}
exports.update = async function(ctx) {}
exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId)
await database.destroy(getUserId(ctx.params.username))
@ -65,3 +79,18 @@ exports.find = async function(ctx) {
_rev: user._rev,
}
}
const checkAccessLevel = async (db, accessLevelId) => {
if (!accessLevelId) return
if (
accessLevelId === POWERUSER_LEVEL_ID ||
accessLevelId === ADMIN_LEVEL_ID
) {
return {
_id: accessLevelId,
name: accessLevelId,
permissions: [],
}
}
return await db.get(accessLevelId)
}

View File

@ -0,0 +1,68 @@
const CouchDB = require("../../db")
const Ajv = require("ajv")
const newid = require("../../db/newid")
const ajv = new Ajv()
exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const workflow = ctx.request.body
workflow._id = newid()
// TODO: Possibly validate the workflow against a schema
// // validation with ajv
// const model = await db.get(record.modelId)
// const validate = ajv.compile({
// properties: model.schema,
// })
// const valid = validate(record)
// if (!valid) {
// ctx.status = 400
// ctx.body = {
// status: 400,
// errors: validate.errors,
// }
// return
// }
workflow.type = "workflow"
const response = await db.post(workflow)
workflow._rev = response.rev
ctx.status = 200
ctx.body = {
message: "Workflow created successfully",
workflow: {
...workflow,
...response
}
};
}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.get(ctx.params.recordId)
}
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const response = await db.query(`database/by_type`, {
type: "workflow",
include_docs: true,
})
ctx.body = response.rows.map(row => row.doc)
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.get(ctx.params.id)
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
}

View File

@ -7,7 +7,6 @@ const {
authRoutes,
pageRoutes,
userRoutes,
recordRoutes,
instanceRoutes,
clientRoutes,
applicationRoutes,
@ -15,6 +14,8 @@ const {
viewRoutes,
staticRoutes,
componentRoutes,
workflowRoutes,
accesslevelRoutes,
} = require("./routes")
const router = new Router()
@ -70,11 +71,11 @@ router.use(modelRoutes.allowedMethods())
router.use(userRoutes.routes())
router.use(userRoutes.allowedMethods())
router.use(recordRoutes.routes())
router.use(recordRoutes.allowedMethods())
router.use(instanceRoutes.routes())
router.use(instanceRoutes.allowedMethods())
router.use(workflowRoutes.routes())
router.use(workflowRoutes.allowedMethods())
// end auth routes
router.use(pageRoutes.routes())
@ -89,6 +90,9 @@ router.use(componentRoutes.allowedMethods())
router.use(clientRoutes.routes())
router.use(clientRoutes.allowedMethods())
router.use(accesslevelRoutes.routes())
router.use(accesslevelRoutes.allowedMethods())
router.use(staticRoutes.routes())
router.use(staticRoutes.allowedMethods())

View File

@ -0,0 +1,14 @@
const Router = require("@koa/router")
const controller = require("../controllers/accesslevel")
const router = Router()
router
.post("/api/:instanceId/accesslevels", controller.create)
.put("/api/:instanceId/accesslevels", controller.update)
.get("/api/:instanceId/accesslevels", controller.fetch)
.get("/api/:instanceId/accesslevels/:levelId", controller.find)
.delete("/api/:instanceId/accesslevels/:levelId/:rev", controller.destroy)
.patch("/api/:instanceId/accesslevels/:levelId", controller.patch)
module.exports = router

View File

@ -1,11 +1,17 @@
const Router = require("@koa/router")
const controller = require("../controllers/application")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router()
router
.get("/api/applications", controller.fetch)
.get("/api/:applicationId/appPackage", controller.fetchAppPackage)
.post("/api/applications", controller.create)
.get("/api/applications", authorized(BUILDER), controller.fetch)
.get(
"/api/:applicationId/appPackage",
authorized(BUILDER),
controller.fetchAppPackage
)
.post("/api/applications", authorized(BUILDER), controller.create)
module.exports = router

View File

@ -1,8 +1,10 @@
const Router = require("@koa/router")
const controller = require("../controllers/client")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router()
router.get("/api/client/id", controller.getClientId)
router.get("/api/client/id", authorized(BUILDER), controller.getClientId)
module.exports = router

View File

@ -1,10 +1,13 @@
const Router = require("@koa/router")
const controller = require("../controllers/component")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router()
router.get(
"/:appId/components/definitions",
authorized(BUILDER),
controller.fetchAppComponentDefinitions
)

View File

@ -1,7 +1,6 @@
const authRoutes = require("./auth")
const pageRoutes = require("./pages")
const userRoutes = require("./user")
const recordRoutes = require("./record")
const instanceRoutes = require("./instance")
const clientRoutes = require("./client")
const applicationRoutes = require("./application")
@ -9,12 +8,13 @@ const modelRoutes = require("./model")
const viewRoutes = require("./view")
const staticRoutes = require("./static")
const componentRoutes = require("./component")
const workflowRoutes = require("./workflow")
const accesslevelRoutes = require("./accesslevel")
module.exports = {
authRoutes,
pageRoutes,
userRoutes,
recordRoutes,
instanceRoutes,
clientRoutes,
applicationRoutes,
@ -22,4 +22,6 @@ module.exports = {
viewRoutes,
staticRoutes,
componentRoutes,
workflowRoutes,
accesslevelRoutes,
}

View File

@ -1,10 +1,12 @@
const Router = require("@koa/router")
const controller = require("../controllers/instance")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router()
router
.post("/api/:applicationId/instances", controller.create)
.delete("/api/instances/:instanceId", controller.destroy)
.post("/api/:applicationId/instances", authorized(BUILDER), controller.create)
.delete("/api/instances/:instanceId", authorized(BUILDER), controller.destroy)
module.exports = router

View File

@ -1,12 +1,49 @@
const Router = require("@koa/router")
const controller = require("../controllers/model")
const modelController = require("../controllers/model")
const recordController = require("../controllers/record")
const authorized = require("../../middleware/authorized")
const {
READ_MODEL,
WRITE_MODEL,
BUILDER,
} = require("../../utilities/accessLevels")
const router = Router()
// records
router
.get("/api/:instanceId/models", controller.fetch)
.post("/api/:instanceId/models", controller.create)
.get(
"/api/:instanceId/:modelId/records",
authorized(READ_MODEL, ctx => ctx.params.modelId),
recordController.fetchModel
)
.get(
"/api/:instanceId/:modelId/records/:recordId",
authorized(READ_MODEL, ctx => ctx.params.modelId),
recordController.find
)
.post(
"/api/:instanceId/:modelId/records",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.save
)
.delete(
"/api/:instanceId/:modelId/records/:recordId/:revId",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.destroy
)
// models
router
.get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch)
.post("/api/:instanceId/models", authorized(BUILDER), modelController.create)
// .patch("/api/:instanceId/models", controller.update)
.delete("/api/:instanceId/models/:modelId/:revId", controller.destroy)
.delete(
"/api/:instanceId/models/:modelId/:revId",
authorized(BUILDER),
modelController.destroy
)
module.exports = router

View File

@ -7,63 +7,85 @@ const {
renameScreen,
deleteScreen,
} = require("../../utilities/builder")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router()
router.post("/_builder/api/:appId/pages/:pageName", async ctx => {
await buildPage(
ctx.config,
ctx.params.appId,
ctx.params.pageName,
ctx.request.body
)
ctx.response.status = StatusCodes.OK
})
router.post(
"/_builder/api/:appId/pages/:pageName",
authorized(BUILDER),
async ctx => {
await buildPage(
ctx.config,
ctx.params.appId,
ctx.params.pageName,
ctx.request.body
)
ctx.response.status = StatusCodes.OK
}
)
router.get("/_builder/api/:appId/pages/:pagename/screens", async ctx => {
ctx.body = await listScreens(
ctx.config,
ctx.params.appId,
ctx.params.pagename
)
ctx.response.status = StatusCodes.OK
})
router.get(
"/_builder/api/:appId/pages/:pagename/screens",
authorized(BUILDER),
async ctx => {
ctx.body = await listScreens(
ctx.config,
ctx.params.appId,
ctx.params.pagename
)
ctx.response.status = StatusCodes.OK
}
)
router.post("/_builder/api/:appId/pages/:pagename/screen", async ctx => {
ctx.body = await saveScreen(
ctx.config,
ctx.params.appId,
ctx.params.pagename,
ctx.request.body
)
ctx.response.status = StatusCodes.OK
})
router.post(
"/_builder/api/:appId/pages/:pagename/screen",
authorized(BUILDER),
async ctx => {
ctx.body = await saveScreen(
ctx.config,
ctx.params.appId,
ctx.params.pagename,
ctx.request.body
)
ctx.response.status = StatusCodes.OK
}
)
router.patch("/_builder/api/:appname/pages/:pagename/screen", async ctx => {
await renameScreen(
ctx.config,
ctx.params.appname,
ctx.params.pagename,
ctx.request.body.oldname,
ctx.request.body.newname
)
ctx.response.status = StatusCodes.OK
})
router.patch(
"/_builder/api/:appname/pages/:pagename/screen",
authorized(BUILDER),
async ctx => {
await renameScreen(
ctx.config,
ctx.params.appname,
ctx.params.pagename,
ctx.request.body.oldname,
ctx.request.body.newname
)
ctx.response.status = StatusCodes.OK
}
)
router.delete("/_builder/api/:appname/pages/:pagename/screen/*", async ctx => {
const name = ctx.request.path.replace(
`/_builder/api/${ctx.params.appname}/pages/${ctx.params.pagename}/screen/`,
""
)
router.delete(
"/_builder/api/:appname/pages/:pagename/screen/*",
authorized(BUILDER),
async ctx => {
const name = ctx.request.path.replace(
`/_builder/api/${ctx.params.appname}/pages/${ctx.params.pagename}/screen/`,
""
)
await deleteScreen(
ctx.config,
ctx.params.appname,
ctx.params.pagename,
decodeURI(name)
)
await deleteScreen(
ctx.config,
ctx.params.appname,
ctx.params.pagename,
decodeURI(name)
)
ctx.response.status = StatusCodes.OK
})
ctx.response.status = StatusCodes.OK
}
)
module.exports = router

View File

@ -1,12 +0,0 @@
const Router = require("@koa/router")
const controller = require("../controllers/record")
const router = Router()
router
.get("/api/:instanceId/:viewName/records", controller.fetch)
.get("/api/:instanceId/records/:recordId", controller.find)
.post("/api/:instanceId/records", controller.save)
.delete("/api/:instanceId/records/:recordId/:revId", controller.destroy)
module.exports = router

View File

@ -1,11 +1,17 @@
const Router = require("@koa/router")
const controller = require("../controllers/screen")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router()
router
.get("/api/:instanceId/screens", controller.fetch)
.post("/api/:instanceId/screens", controller.save)
.delete("/api/:instanceId/:screenId/:revId", controller.destroy)
.get("/api/:instanceId/screens", authorized(BUILDER), controller.fetch)
.post("/api/:instanceId/screens", authorized(BUILDER), controller.save)
.delete(
"/api/:instanceId/:screenId/:revId",
authorized(BUILDER),
controller.destroy
)
module.exports = router

View File

@ -0,0 +1,184 @@
const {
createInstance,
createClientDatabase,
createApplication,
createModel,
createView,
supertest,
defaultHeaders
} = require("./couchTestUtils")
const {
generateAdminPermissions,
generatePowerUserPermissions,
POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID,
READ_MODEL,
WRITE_MODEL,
} = require("../../../utilities/accessLevels")
describe("/accesslevels", () => {
let appId
let server
let request
let instanceId
let model
let view
beforeAll(async () => {
({ request, server } = await supertest())
await createClientDatabase(request);
appId = (await createApplication(request))._id
});
afterAll(async () => {
server.close();
})
beforeEach(async () => {
instanceId = (await createInstance(request, appId))._id
model = await createModel(request, instanceId)
view = await createView(request, instanceId)
})
describe("create", () => {
it("returns a success message when level is successfully created", async () => {
const res = await request
.post(`/api/${instanceId}/accesslevels`)
.send({ name: "user" })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(res.res.statusMessage).toEqual("Access Level 'user' created successfully.")
expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined()
expect(res.body.permissions).toEqual([])
})
});
describe("fetch", () => {
it("should list custom levels, plus 2 default levels", async () => {
const createRes = await request
.post(`/api/${instanceId}/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
const res = await request
.get(`/api/${instanceId}/accesslevels`)
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.length).toBe(3)
const adminLevel = res.body.find(r => r._id === ADMIN_LEVEL_ID)
expect(adminLevel).toBeDefined()
expect(adminLevel.permissions).toEqual(await generateAdminPermissions(instanceId))
const powerUserLevel = res.body.find(r => r._id === POWERUSER_LEVEL_ID)
expect(powerUserLevel).toBeDefined()
expect(powerUserLevel.permissions).toEqual(await generatePowerUserPermissions(instanceId))
const customLevelFetched = res.body.find(r => r._id === customLevel._id)
expect(customLevelFetched.permissions).toEqual(customLevel.permissions)
})
});
describe("destroy", () => {
it("should delete custom access level", async () => {
const createRes = await request
.post(`/api/${instanceId}/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL } ] })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
await request
.delete(`/api/${instanceId}/accesslevels/${customLevel._id}/${customLevel._rev}`)
.set(defaultHeaders)
.expect(200)
await request
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
.set(defaultHeaders)
.expect(404)
})
})
describe("patch", () => {
it("should add given permissions", async () => {
const createRes = await request
.post(`/api/${instanceId}/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
await request
.patch(`/api/${instanceId}/accesslevels/${customLevel._id}`)
.send({
_rev: customLevel._rev,
addedPermissions: [ { itemId: model._id, name: WRITE_MODEL } ]
})
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const finalRes = await request
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
.set(defaultHeaders)
.expect(200)
expect(finalRes.body.permissions.length).toBe(2)
expect(finalRes.body.permissions.some(p => p.name === WRITE_MODEL)).toBe(true)
expect(finalRes.body.permissions.some(p => p.name === READ_MODEL)).toBe(true)
})
it("should remove given permissions", async () => {
const createRes = await request
.post(`/api/${instanceId}/accesslevels`)
.send({
name: "user",
permissions: [
{ itemId: model._id, name: READ_MODEL },
{ itemId: model._id, name: WRITE_MODEL },
]
})
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
await request
.patch(`/api/${instanceId}/accesslevels/${customLevel._id}`)
.send({
_rev: customLevel._rev,
removedPermissions: [ { itemId: model._id, name: WRITE_MODEL }]
})
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const finalRes = await request
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
.set(defaultHeaders)
.expect(200)
expect(finalRes.body.permissions.length).toBe(1)
expect(finalRes.body.permissions.some(p => p.name === READ_MODEL)).toBe(true)
})
})
});

View File

@ -2,6 +2,7 @@ const CouchDB = require("../../../db")
const { create, destroy } = require("../../../db/clientDb")
const supertest = require("supertest")
const app = require("../../../app")
const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels")
const TEST_CLIENT_ID = "test-client-id"
@ -17,7 +18,7 @@ exports.supertest = async () => {
exports.defaultHeaders = {
Accept: "application/json",
Authorization: "Basic test-admin-secret",
Cookie: ["builder:token=test-admin-secret"],
}
exports.createModel = async (request, instanceId, model) => {
@ -37,6 +38,18 @@ exports.createModel = async (request, instanceId, model) => {
return res.body
}
exports.createView = async (request, instanceId, view) => {
view = view || {
map: "function(doc) { emit(doc[doc.key], doc._id); } ",
}
const res = await request
.post(`/api/${instanceId}/views`)
.set(exports.defaultHeaders)
.send(view)
return res.body
}
exports.createClientDatabase = async () => await create(TEST_CLIENT_ID)
exports.createApplication = async (request, name = "test_application") => {
@ -70,20 +83,20 @@ exports.createUser = async (
const res = await request
.post(`/api/${instanceId}/users`)
.set(exports.defaultHeaders)
.send({ name: "Bill", username, password })
.send({
name: "Bill",
username,
password,
accessLevelId: POWERUSER_LEVEL_ID,
})
return res.body
}
exports.insertDocument = async (databaseId, document) => {
const { id, ...documentFields } = document
await new CouchDB(databaseId).put({ _id: id, ...documentFields })
return await new CouchDB(databaseId).put({ _id: id, ...documentFields })
}
exports.createSchema = async (request, instanceId, schema) => {
for (let model of schema.models) {
await request.post(`/api/${instanceId}/models`).send(model)
}
for (let view of schema.views) {
await request.post(`/api/${instanceId}/views`).send(view)
}
}
exports.destroyDocument = async (databaseId, documentId) => {
return await new CouchDB(databaseId).destroy(documentId);
}

View File

@ -3,7 +3,8 @@ const {
createModel,
supertest,
createClientDatabase,
createApplication
createApplication ,
defaultHeaders
} = require("./couchTestUtils")
describe("/models", () => {
@ -38,7 +39,7 @@ describe("/models", () => {
name: { type: "string" }
}
})
.set("Accept", "application/json")
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
.end(async (err, res) => {
@ -60,7 +61,7 @@ describe("/models", () => {
it("returns all the models for that instance in the response body", done => {
request
.get(`/api/${instance._id}/models`)
.set("Accept", "application/json")
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
.end(async (_, res) => {
@ -83,7 +84,7 @@ describe("/models", () => {
it("returns a success response when a model is deleted.", done => {
request
.delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`)
.set("Accept", "application/json")
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
.end(async (_, res) => {

View File

@ -3,7 +3,8 @@ const {
createClientDatabase,
createInstance,
createModel,
supertest
supertest,
defaultHeaders,
} = require("./couchTestUtils");
describe("/records", () => {
@ -38,9 +39,9 @@ describe("/records", () => {
const createRecord = async r =>
await request
.post(`/api/${instance._id}/records`)
.post(`/api/${instance._id}/${model._id}/records`)
.send(r || record)
.set("Accept", "application/json")
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
@ -56,14 +57,14 @@ describe("/records", () => {
const existing = rec.body
const res = await request
.post(`/api/${instance._id}/records`)
.post(`/api/${instance._id}/${model._id}/records`)
.send({
_id: existing._id,
_rev: existing._rev,
modelId: model._id,
name: "Updated Name",
})
.set("Accept", "application/json")
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
@ -76,8 +77,8 @@ describe("/records", () => {
const existing = rec.body
const res = await request
.get(`/api/${instance._id}/records/${existing._id}`)
.set("Accept", "application/json")
.get(`/api/${instance._id}/${model._id}/records/${existing._id}`)
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
@ -99,8 +100,8 @@ describe("/records", () => {
await createRecord(newRecord)
const res = await request
.get(`/api/${instance._id}/all_${newRecord.modelId}/records`)
.set("Accept", "application/json")
.get(`/api/${instance._id}/${model._id}/records`)
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
@ -112,8 +113,8 @@ describe("/records", () => {
it("load should return 404 when record does not exist", async () => {
await createRecord()
await request
.get(`/api/${instance._id}/records/not-a-valid-id`)
.set("Accept", "application/json")
.get(`/api/${instance._id}/${model._id}/records/not-a-valid-id`)
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(404)
})

View File

@ -6,6 +6,7 @@ const {
defaultHeaders,
createUser,
} = require("./couchTestUtils")
const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels")
describe("/users", () => {
let request
@ -51,7 +52,7 @@ describe("/users", () => {
const res = await request
.post(`/api/${instance._id}/users`)
.set(defaultHeaders)
.send({ name: "Bill", username: "bill", password: "bills_password" })
.send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID })
.expect(200)
.expect('Content-Type', /json/)

View File

@ -0,0 +1,114 @@
const {
createClientDatabase,
createApplication,
createInstance,
defaultHeaders,
supertest,
insertDocument,
destroyDocument
} = require("./couchTestUtils")
const TEST_WORKFLOW = {
_id: "Test Workflow",
name: "My Workflow",
pageId: "123123123",
screenId: "kasdkfldsafkl",
live: true,
uiTree: {
},
definition: {
triggers: [
],
next: {
actionId: "abc123",
type: "SERVER",
conditions: {
}
}
}
}
describe("/workflows", () => {
let request
let server
let app
let instance
let workflow
beforeAll(async () => {
({ request, server } = await supertest())
await createClientDatabase(request)
app = await createApplication(request)
})
beforeEach(async () => {
instance = await createInstance(request, app._id)
if (workflow) await destroyDocument(workflow.id);
})
afterAll(async () => {
server.close()
})
const createWorkflow = async () => {
workflow = await insertDocument(instance._id, {
type: "workflow",
...TEST_WORKFLOW
});
}
describe("create", () => {
it("returns a success message when the workflow is successfully created", async () => {
const res = await request
.post(`/api/${instance._id}/workflows`)
.set(defaultHeaders)
.send(TEST_WORKFLOW)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.message).toEqual("Workflow created successfully");
expect(res.body.workflow.name).toEqual("My Workflow");
})
})
describe("fetch", () => {
it("return all the workflows for an instance", async () => {
await createWorkflow();
const res = await request
.get(`/api/${instance._id}/workflows`)
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body[0]).toEqual(expect.objectContaining(TEST_WORKFLOW));
})
})
describe("find", () => {
it("returns a workflow when queried by ID", async () => {
await createWorkflow();
const res = await request
.get(`/api/${instance._id}/workflows/${workflow.id}`)
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body).toEqual(expect.objectContaining(TEST_WORKFLOW));
})
})
describe("destroy", () => {
it("deletes a workflow by its ID", async () => {
await createWorkflow();
const res = await request
.delete(`/api/${instance._id}/workflows/${workflow.id}/${workflow.rev}`)
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.id).toEqual(TEST_WORKFLOW._id);
})
})
});

View File

@ -1,12 +1,26 @@
const Router = require("@koa/router")
const controller = require("../controllers/user")
const authorized = require("../../middleware/authorized")
const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels")
const router = Router()
router
.get("/api/:instanceId/users", controller.fetch)
.get("/api/:instanceId/users/:username", controller.find)
.post("/api/:instanceId/users", controller.create)
.delete("/api/:instanceId/users/:username", controller.destroy)
.get("/api/:instanceId/users", authorized(LIST_USERS), controller.fetch)
.get(
"/api/:instanceId/users/:username",
authorized(USER_MANAGEMENT),
controller.find
)
.post(
"/api/:instanceId/users",
authorized(USER_MANAGEMENT),
controller.create
)
.delete(
"/api/:instanceId/users/:username",
authorized(USER_MANAGEMENT),
controller.destroy
)
module.exports = router

View File

@ -1,12 +1,20 @@
const Router = require("@koa/router")
const controller = require("../controllers/view")
const viewController = require("../controllers/view")
const recordController = require("../controllers/record")
const authorized = require("../../middleware/authorized")
const { BUILDER, READ_VIEW } = require("../../utilities/accessLevels")
const router = Router()
router
.get("/api/:instanceId/views", controller.fetch)
.get(
"/api/:instanceId/view/:viewName",
authorized(READ_VIEW, ctx => ctx.params.viewName),
recordController.fetchView
)
.get("/api/:instanceId/views", authorized(BUILDER), viewController.fetch)
// .patch("/api/:databaseId/views", controller.update);
// .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy);
.post("/api/:instanceId/views", controller.create)
.post("/api/:instanceId/views", authorized(BUILDER), viewController.create)
module.exports = router

View File

@ -0,0 +1,13 @@
const Router = require("@koa/router")
const controller = require("../controllers/workflow")
const router = Router()
router
.get("/api/:instanceId/workflows", controller.fetch)
.get("/api/:instanceId/workflows/:id", controller.find)
.post("/api/:instanceId/workflows", controller.create)
.put("/api/:instanceId/workflows/:id", controller.update)
.delete("/api/:instanceId/workflows/:id/:rev", controller.destroy)
module.exports = router

View File

@ -1,6 +1,11 @@
const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes")
const env = require("../environment")
const accessLevelController = require("../api/controllers/accesslevel")
const {
ADMIN_LEVEL_ID,
POWERUSER_LEVEL_ID,
} = require("../utilities/accessLevels")
module.exports = async (ctx, next) => {
if (ctx.path === "/_builder") {
@ -8,8 +13,9 @@ module.exports = async (ctx, next) => {
return
}
if (ctx.isDev && ctx.cookies.get("builder:token") === env.ADMIN_SECRET) {
if (ctx.cookies.get("builder:token") === env.ADMIN_SECRET) {
ctx.isAuthenticated = true
ctx.isBuilder = true
await next()
return
}
@ -23,7 +29,12 @@ module.exports = async (ctx, next) => {
}
try {
ctx.jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
const jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
ctx.user = {
...jwtPayload,
accessLevel: await getAccessLevel(jwtPayload.accessLevelId),
}
ctx.isAuthenticated = true
} catch (err) {
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
@ -31,3 +42,22 @@ module.exports = async (ctx, next) => {
await next()
}
const getAccessLevel = async accessLevelId => {
if (
accessLevelId === POWERUSER_LEVEL_ID ||
accessLevelId === ADMIN_LEVEL_ID
) {
return {
_id: accessLevelId,
name: accessLevelId,
permissions: [],
}
}
const findAccessContext = {
params: { levelId: accessLevelId },
}
await accessLevelController.find(findAccessContext)
return findAccessContext.body
}

View File

@ -0,0 +1,58 @@
const {
adminPermissions,
ADMIN_LEVEL_ID,
POWERUSER_LEVEL_ID,
BUILDER,
} = require("../utilities/accessLevels")
module.exports = (permName, getItemId) => async (ctx, next) => {
if (!ctx.isAuthenticated) {
ctx.throw(403, "Session not authenticated")
}
if (ctx.isBuilder) {
await next()
return
}
if (permName === BUILDER) {
ctx.throw(403, "Not Authorized")
return
}
if (!ctx.user) {
ctx.throw(403, "User not found")
}
const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "")
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
await next()
return
}
const thisPermissionId = {
name: permName,
itemId: getItemId && getItemId(ctx),
}
// power user has everything, except the admin specific perms
if (
ctx.user.accessLevel._id === POWERUSER_LEVEL_ID &&
!adminPermissions.map(permissionId).includes(thisPermissionId)
) {
await next()
return
}
if (
ctx.user.accessLevel.permissions
.map(permissionId)
.includes(thisPermissionId)
) {
await next()
return
}
ctx.throw(403, "Not Authorized")
}

View File

@ -0,0 +1,38 @@
const WORKFLOW_SCHEMA = {
properties: {
type: "workflow",
pageId: {
type: "string"
},
screenId: {
type: "string"
},
live: {
type: "boolean"
},
uiTree: {
type: "object"
},
definition: {
type: "object",
properties: {
triggers: { type: "array" },
next: {
type: "object",
properties: {
type: { type: "string" },
actionId: { type: "string" },
args: { type: "object" },
conditions: { type: "array" },
errorHandling: { type: "object" },
next: { type: "object" }
}
},
}
}
}
};
module.exports = {
WORKFLOW_SCHEMA
};

View File

@ -0,0 +1,64 @@
const viewController = require("../api/controllers/view")
const modelController = require("../api/controllers/model")
exports.ADMIN_LEVEL_ID = "ADMIN"
exports.POWERUSER_LEVEL_ID = "POWER_USER"
exports.READ_MODEL = "read-model"
exports.WRITE_MODEL = "write-model"
exports.READ_VIEW = "read-view"
exports.EXECUTE_WORKFLOW = "execute-workflow"
exports.USER_MANAGEMENT = "user-management"
exports.BUILDER = "builder"
exports.LIST_USERS = "list-users"
exports.adminPermissions = [
{
name: exports.USER_MANAGEMENT,
},
]
exports.generateAdminPermissions = async instanceId => [
...exports.adminPermissions,
...(await exports.generatePowerUserPermissions(instanceId)),
]
exports.generatePowerUserPermissions = async instanceId => {
const fetchModelsCtx = {
params: {
instanceId,
},
}
await modelController.fetch(fetchModelsCtx)
const models = fetchModelsCtx.body
const fetchViewsCtx = {
params: {
instanceId,
},
}
await viewController.fetch(fetchViewsCtx)
const views = fetchViewsCtx.body
const readModelPermissions = models.map(m => ({
itemId: m._id,
name: exports.READ_MODEL,
}))
const writeModelPermissions = models.map(m => ({
itemId: m._id,
name: exports.WRITE_MODEL,
}))
const viewPermissions = views.map(v => ({
itemId: v.name,
name: exports.READ_VIEW,
}))
return [
...readModelPermissions,
...writeModelPermissions,
...viewPermissions,
{ name: exports.LIST_USERS },
]
}

View File

@ -0,0 +1 @@
dist/

View File

@ -0,0 +1,11 @@
{
"name": "name",
"version": "1.0.0",
"description": "",
"author": "",
"license": "ISC",
"dependencies": {
"@budibase/standard-components": "0.x",
"@budibase/materialdesign-components": "0.x"
}
}

View File

@ -0,0 +1,19 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
"props" : {
"_component": "@budibase/standard-components/container",
"_children": [],
"_id": 0,
"type": "div",
"_styles": {
"layout": {},
"position": {}
},
"_code": ""
},
"_css": "",
"uiFunctions": ""
}

View File

@ -0,0 +1,19 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
"props" : {
"_component": "@budibase/standard-components/container",
"_children": [],
"_id": 1,
"type": "div",
"_styles": {
"layout": {},
"position": {}
},
"_code": ""
},
"_css": "",
"uiFunctions": ""
}

View File

@ -0,0 +1 @@
module.exports = () => ({})