diff --git a/packages/cli/src/commands/init/index.js b/packages/cli/src/commands/init/index.js index 4a25fbd8b2..f387a8d203 100644 --- a/packages/cli/src/commands/init/index.js +++ b/packages/cli/src/commands/init/index.js @@ -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", { diff --git a/packages/cli/src/commands/init/initHandler.js b/packages/cli/src/commands/init/initHandler.js index 0467633ee8..c0c4ad1b9e 100644 --- a/packages/cli/src/commands/init/initHandler.js +++ b/packages/cli/src/commands/init/initHandler.js @@ -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 diff --git a/packages/server/.vscode/launch.json b/packages/server/.vscode/launch.json index 964e9297f4..4dc6b653cb 100644 --- a/packages/server/.vscode/launch.json +++ b/packages/server/.vscode/launch.json @@ -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", diff --git a/packages/server/package.json b/packages/server/package.json index 95990ac88f..156a25aa0b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -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", diff --git a/packages/server/src/api/controllers/accesslevel.js b/packages/server/src/api/controllers/accesslevel.js new file mode 100644 index 0000000000..9c9c7ff727 --- /dev/null +++ b/packages/server/src/api/controllers/accesslevel.js @@ -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 +} diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index c4b49d1f74..c9c072fc2b 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -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() diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js index 4cfc2f438a..b9e1150648 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.js @@ -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, } diff --git a/packages/server/src/api/controllers/record.js b/packages/server/src/api/controllers/record.js index 2cd648b6c3..7a5b045136 100644 --- a/packages/server/src/api/controllers/record.js +++ b/packages/server/src/api/controllers/record.js @@ -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) } diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 2be0132a47..f654caa4c0 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -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) +} diff --git a/packages/server/src/api/controllers/workflow.js b/packages/server/src/api/controllers/workflow.js new file mode 100644 index 0000000000..216ca6afd2 --- /dev/null +++ b/packages/server/src/api/controllers/workflow.js @@ -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) +} diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 176eaaff96..66c3168f23 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -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()) diff --git a/packages/server/src/api/routes/accesslevel.js b/packages/server/src/api/routes/accesslevel.js new file mode 100644 index 0000000000..d34acab02c --- /dev/null +++ b/packages/server/src/api/routes/accesslevel.js @@ -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 diff --git a/packages/server/src/api/routes/application.js b/packages/server/src/api/routes/application.js index dddf64a710..60cc781ac6 100644 --- a/packages/server/src/api/routes/application.js +++ b/packages/server/src/api/routes/application.js @@ -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 diff --git a/packages/server/src/api/routes/client.js b/packages/server/src/api/routes/client.js index ff87b82e22..16acf1b7a3 100644 --- a/packages/server/src/api/routes/client.js +++ b/packages/server/src/api/routes/client.js @@ -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 diff --git a/packages/server/src/api/routes/component.js b/packages/server/src/api/routes/component.js index 5df34381fa..8fbe7ac41a 100644 --- a/packages/server/src/api/routes/component.js +++ b/packages/server/src/api/routes/component.js @@ -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 ) diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js index a8f57c0e14..c515d5f437 100644 --- a/packages/server/src/api/routes/index.js +++ b/packages/server/src/api/routes/index.js @@ -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, } diff --git a/packages/server/src/api/routes/instance.js b/packages/server/src/api/routes/instance.js index fd74a98bf1..9b7b3db511 100644 --- a/packages/server/src/api/routes/instance.js +++ b/packages/server/src/api/routes/instance.js @@ -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 diff --git a/packages/server/src/api/routes/model.js b/packages/server/src/api/routes/model.js index d25d0d17cb..d9eb5cf798 100644 --- a/packages/server/src/api/routes/model.js +++ b/packages/server/src/api/routes/model.js @@ -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 diff --git a/packages/server/src/api/routes/pages.js b/packages/server/src/api/routes/pages.js index 88fd1239f4..98f0f08b92 100644 --- a/packages/server/src/api/routes/pages.js +++ b/packages/server/src/api/routes/pages.js @@ -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 diff --git a/packages/server/src/api/routes/record.js b/packages/server/src/api/routes/record.js deleted file mode 100644 index fb0b543dc1..0000000000 --- a/packages/server/src/api/routes/record.js +++ /dev/null @@ -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 diff --git a/packages/server/src/api/routes/screen.js b/packages/server/src/api/routes/screen.js index 3a167b6ef6..19823aab68 100644 --- a/packages/server/src/api/routes/screen.js +++ b/packages/server/src/api/routes/screen.js @@ -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 diff --git a/packages/server/src/api/routes/tests/accesslevel.spec.js b/packages/server/src/api/routes/tests/accesslevel.spec.js new file mode 100644 index 0000000000..ef2d1575cd --- /dev/null +++ b/packages/server/src/api/routes/tests/accesslevel.spec.js @@ -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) + }) + }) +}); diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js index 7018ec24b2..aa7a765d25 100644 --- a/packages/server/src/api/routes/tests/couchTestUtils.js +++ b/packages/server/src/api/routes/tests/couchTestUtils.js @@ -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); +} \ No newline at end of file diff --git a/packages/server/src/api/routes/tests/model.spec.js b/packages/server/src/api/routes/tests/model.spec.js index e32c94224f..3e224536ed 100644 --- a/packages/server/src/api/routes/tests/model.spec.js +++ b/packages/server/src/api/routes/tests/model.spec.js @@ -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) => { diff --git a/packages/server/src/api/routes/tests/record.spec.js b/packages/server/src/api/routes/tests/record.spec.js index b75b245135..16af0b6031 100644 --- a/packages/server/src/api/routes/tests/record.spec.js +++ b/packages/server/src/api/routes/tests/record.spec.js @@ -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) }) diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index bba748daff..f994798c2e 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -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/) diff --git a/packages/server/src/api/routes/tests/workflow.spec.js b/packages/server/src/api/routes/tests/workflow.spec.js new file mode 100644 index 0000000000..5b09009479 --- /dev/null +++ b/packages/server/src/api/routes/tests/workflow.spec.js @@ -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); + }) + }) +}); diff --git a/packages/server/src/api/routes/user.js b/packages/server/src/api/routes/user.js index 61f4a096fb..20e17c7473 100644 --- a/packages/server/src/api/routes/user.js +++ b/packages/server/src/api/routes/user.js @@ -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 diff --git a/packages/server/src/api/routes/view.js b/packages/server/src/api/routes/view.js index f7ce9b41b5..3bae02c4b3 100644 --- a/packages/server/src/api/routes/view.js +++ b/packages/server/src/api/routes/view.js @@ -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 diff --git a/packages/server/src/api/routes/workflow.js b/packages/server/src/api/routes/workflow.js new file mode 100644 index 0000000000..86332e89aa --- /dev/null +++ b/packages/server/src/api/routes/workflow.js @@ -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 diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js index 1bcc5575c3..1a28e6a418 100644 --- a/packages/server/src/middleware/authenticated.js +++ b/packages/server/src/middleware/authenticated.js @@ -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 +} diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js new file mode 100644 index 0000000000..15a7b25458 --- /dev/null +++ b/packages/server/src/middleware/authorized.js @@ -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") +} diff --git a/packages/server/src/schemas/index.js b/packages/server/src/schemas/index.js new file mode 100644 index 0000000000..23a839bf97 --- /dev/null +++ b/packages/server/src/schemas/index.js @@ -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 +}; \ No newline at end of file diff --git a/packages/server/src/utilities/accessLevels.js b/packages/server/src/utilities/accessLevels.js new file mode 100644 index 0000000000..25a231ed73 --- /dev/null +++ b/packages/server/src/utilities/accessLevels.js @@ -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 }, + ] +} diff --git a/packages/server/src/utilities/appDirectoryTemplate/.gitignore b/packages/server/src/utilities/appDirectoryTemplate/.gitignore new file mode 100644 index 0000000000..77738287f0 --- /dev/null +++ b/packages/server/src/utilities/appDirectoryTemplate/.gitignore @@ -0,0 +1 @@ +dist/ \ No newline at end of file diff --git a/packages/server/src/utilities/appDirectoryTemplate/package.json b/packages/server/src/utilities/appDirectoryTemplate/package.json new file mode 100644 index 0000000000..f49e35d23f --- /dev/null +++ b/packages/server/src/utilities/appDirectoryTemplate/package.json @@ -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" + } +} diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json b/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json new file mode 100644 index 0000000000..89a23a78e5 --- /dev/null +++ b/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json @@ -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": "" +} diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/.gitkeep b/packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json b/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json new file mode 100644 index 0000000000..14d0301c24 --- /dev/null +++ b/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json @@ -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": "" +} diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/.gitkeep b/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/server/src/utilities/appDirectoryTemplate/plugins.js b/packages/server/src/utilities/appDirectoryTemplate/plugins.js new file mode 100644 index 0000000000..44368bf6ec --- /dev/null +++ b/packages/server/src/utilities/appDirectoryTemplate/plugins.js @@ -0,0 +1 @@ +module.exports = () => ({})