mirror of https://github.com/Budibase/budibase.git
53 changed files with 1708 additions and 509 deletions
|
After Width: | Height: | Size: 266 B |
|
After Width: | Height: | Size: 253 B |
@ -0,0 +1,72 @@ |
|||
<script> |
|||
import Button from "components/common/Button.svelte" |
|||
export let name, description =`A minimalist CRM which removes the noise and allows you to focus |
|||
on your business.`, _id; |
|||
|
|||
|
|||
</script> |
|||
|
|||
<div class="apps-card"> |
|||
<h3 class="app-title">{name}</h3> |
|||
<p class="app-desc"> |
|||
{description} |
|||
</p> |
|||
<div class="card-footer"> |
|||
<div class="modified-date">Last Edited - 25th May 2020</div> |
|||
<a href={`/_builder/${_id}`} class="app-button"> |
|||
Open Web App |
|||
</a> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.apps-card { |
|||
background-color: var(--white); |
|||
padding: 20px; |
|||
max-width: 400px; |
|||
max-height: 150px; |
|||
border-radius: 5px; |
|||
border: 1px solid var(--grey-medium); |
|||
} |
|||
|
|||
.app-button:hover { |
|||
background-color: var(--grey-light); |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.app-title { |
|||
font-size: 18px; |
|||
font-weight: 700; |
|||
color: var(--ink); |
|||
text-transform: capitalize; |
|||
} |
|||
|
|||
.app-desc { |
|||
color: var(--ink-light); |
|||
} |
|||
|
|||
.card-footer { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: baseline; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.modified-date { |
|||
font-size: 14px; |
|||
color: var(--ink-light); |
|||
} |
|||
|
|||
.app-button { |
|||
background-color: var(--white); |
|||
color: var(--ink); |
|||
padding: 12px 20px; |
|||
border-radius: 5px; |
|||
border: 1px var(--grey) solid; |
|||
font-size: 14px; |
|||
font-weight: 400; |
|||
cursor: pointer; |
|||
transition: all 0.2s; |
|||
box-sizing: border-box; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,197 @@ |
|||
<script> |
|||
import Spinner from "components/common/Spinner.svelte" |
|||
import { Input, TextArea, Button } from "@budibase/bbui" |
|||
import { goto } from "@sveltech/routify" |
|||
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/" |
|||
import { getContext } from "svelte" |
|||
import { fade } from "svelte/transition" |
|||
|
|||
const { open, close } = getContext("simple-modal") |
|||
|
|||
let name = "" |
|||
let description = "" |
|||
let loading = false |
|||
let error = {} |
|||
|
|||
const createNewApp = async () => { |
|||
if ((name.length > 100 || name.length < 1) && description.length < 1) { |
|||
error = { |
|||
name: true, |
|||
description: true, |
|||
} |
|||
} else if (description.length < 1) { |
|||
error = { |
|||
name: false, |
|||
description: true, |
|||
} |
|||
} else if (name.length > 100 || name.length < 1) { |
|||
error = { |
|||
name: true, |
|||
} |
|||
} else { |
|||
error = {} |
|||
const data = { name, description } |
|||
loading = true |
|||
try { |
|||
const response = await fetch("/api/applications", { |
|||
method: "POST", // *GET, POST, PUT, DELETE, etc. |
|||
credentials: "same-origin", // include, *same-origin, omit |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
// 'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
body: JSON.stringify(data), // body data type must match "Content-Type" header |
|||
}) |
|||
|
|||
const res = await response.json() |
|||
|
|||
$goto(`./${res._id}`) |
|||
} catch (error) { |
|||
console.error(error) |
|||
} |
|||
} |
|||
} |
|||
|
|||
let value |
|||
let onChange = () => {} |
|||
|
|||
function _onCancel() { |
|||
close() |
|||
} |
|||
|
|||
async function _onOkay() { |
|||
await createNewApp() |
|||
} |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="body"> |
|||
<div class="heading"> |
|||
<span class="icon"> |
|||
<AppsIcon /> |
|||
</span> |
|||
<h3>Create new web app</h3> |
|||
</div> |
|||
<Input |
|||
name="name" |
|||
label="Name" |
|||
placeholder="Enter application name" |
|||
on:change={e => (name = e.target.value)} |
|||
on:input={e => (name = e.target.value)} /> |
|||
{#if error.name} |
|||
<span class="error">You need to enter a name for your application.</span> |
|||
{/if} |
|||
<TextArea |
|||
bind:value={description} |
|||
name="description" |
|||
label="Description" |
|||
placeholder="Describe your application" /> |
|||
{#if error.description} |
|||
<span class="error"> |
|||
Please enter a short description of your application |
|||
</span> |
|||
{/if} |
|||
</div> |
|||
<div class="footer"> |
|||
<a href="./#" class="info"> |
|||
<InfoIcon /> |
|||
How to get started |
|||
</a> |
|||
<Button outline thin on:click={_onCancel}>Cancel</Button> |
|||
<Button primary thin on:click={_onOkay}>Save</Button> |
|||
</div> |
|||
<div class="close-button" on:click={_onCancel}> |
|||
<CloseIcon /> |
|||
</div> |
|||
{#if loading} |
|||
<div in:fade class="spinner-container"> |
|||
<Spinner /> |
|||
<span class="spinner-text">Creating your app...</span> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
position: relative; |
|||
} |
|||
|
|||
.close-button { |
|||
cursor: pointer; |
|||
position: absolute; |
|||
top: 20px; |
|||
right: 20px; |
|||
} |
|||
.close-button :global(svg) { |
|||
width: 24px; |
|||
height: 24px; |
|||
} |
|||
.heading { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
margin-bottom: 20px; |
|||
} |
|||
h3 { |
|||
margin: 0; |
|||
font-size: 24px; |
|||
font-weight: bold; |
|||
} |
|||
.icon { |
|||
display: grid; |
|||
border-radius: 3px; |
|||
align-content: center; |
|||
justify-content: center; |
|||
margin-right: 12px; |
|||
height: 20px; |
|||
width: 20px; |
|||
padding: 10px; |
|||
background-color: var(--blue-light); |
|||
} |
|||
.info { |
|||
color: var(--primary100); |
|||
text-decoration-color: var(--primary100); |
|||
} |
|||
.info :global(svg) { |
|||
fill: var(--primary100); |
|||
margin-right: 8px; |
|||
width: 24px; |
|||
height: 24px; |
|||
} |
|||
.body { |
|||
padding: 40px 40px 80px 40px; |
|||
display: grid; |
|||
grid-gap: 20px; |
|||
} |
|||
.footer { |
|||
display: grid; |
|||
grid-gap: 20px; |
|||
align-items: center; |
|||
grid-template-columns: 1fr auto auto; |
|||
padding: 30px 40px; |
|||
border-bottom-left-radius: 5px; |
|||
border-bottom-right-radius: 50px; |
|||
background-color: var(--grey-light); |
|||
} |
|||
.spinner-container { |
|||
background: white; |
|||
position: absolute; |
|||
border-radius: 5px; |
|||
left: 0; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
display: grid; |
|||
justify-items: center; |
|||
align-content: center; |
|||
grid-gap: 50px; |
|||
} |
|||
.spinner-text { |
|||
font-size: 2em; |
|||
} |
|||
.error { |
|||
color: var(--deletion100); |
|||
font-weight: bold; |
|||
font-size: 0.8em; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,217 @@ |
|||
<script> |
|||
import Modal from "svelte-simple-modal" |
|||
import { |
|||
SettingsIcon, |
|||
AppsIcon, |
|||
UpdatesIcon, |
|||
HostingIcon, |
|||
DocumentationIcon, |
|||
TutorialsIcon, |
|||
CommunityIcon, |
|||
ContributionIcon, |
|||
BugIcon, |
|||
EmailIcon, |
|||
TwitterIcon, |
|||
} from "components/common/Icons/" |
|||
</script> |
|||
|
|||
<Modal> |
|||
<div class="root"> |
|||
<div class="ui-nav"> |
|||
<div class="home-logo"> |
|||
<img src="/_builder/assets/bb-logo.svg" alt="Budibase icon" /> |
|||
</div> |
|||
|
|||
<div class="nav-section"> |
|||
<div class="nav-section-title">Build</div> |
|||
<div class="nav-item-home"> |
|||
<span class="nav-item-icon"> |
|||
<AppsIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Apps</div> |
|||
</div> |
|||
<div class="nav-item"> |
|||
<span class="nav-item-icon"> |
|||
<SettingsIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Settings</div> |
|||
</div> |
|||
<a href="https://budibase.con/login" target="_blank" class="nav-item"> |
|||
<span class="nav-item-icon"> |
|||
<UpdatesIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Updates</div> |
|||
</a> |
|||
<a href="https://budibase.con/login" target="_blank" class="nav-item"> |
|||
<span class="nav-item-icon"> |
|||
<HostingIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Hosting</div> |
|||
</a> |
|||
</div> |
|||
|
|||
<div class="nav-section"> |
|||
<div class="nav-section-title">Learn</div> |
|||
<a href="https://docs.budibase.com/" target="_blank" class="nav-item"> |
|||
<span class="nav-item-icon"> |
|||
<DocumentationIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Documentation</div> |
|||
</a> |
|||
<a |
|||
href="https://docs.budibase.com/tutorial/quick-start" |
|||
target="_blank" |
|||
class="nav-item"> |
|||
<span class="nav-item-icon"> |
|||
<TutorialsIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Tutorials</div> |
|||
</a> |
|||
<a href="https://forum.budibase.com/" target="_blank" class="nav-item"> |
|||
<span class="nav-item-icon"> |
|||
<CommunityIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Community</div> |
|||
</a> |
|||
</div> |
|||
|
|||
<div class="nav-section"> |
|||
<div class="nav-section-title">Contact</div> |
|||
<a |
|||
href="https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md" |
|||
target="_blank" |
|||
class="nav-item"> |
|||
<span class="nav-item-icon"> |
|||
<ContributionIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Contribute to our product</div> |
|||
</a> |
|||
<a |
|||
href="https://github.com/Budibase/budibase/issues" |
|||
target="_blank" |
|||
class="nav-item"> |
|||
<span class="nav-item-icon"> |
|||
<BugIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Report bug</div> |
|||
</a> |
|||
<a href="mailto:support@budibase.com" target="_blank" class="nav-item"> |
|||
<span class="nav-item-icon"> |
|||
<EmailIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Email</div> |
|||
</a> |
|||
<a href="https://twitter.com/budibase" target="_blank" class="nav-item"> |
|||
<span class="nav-item-icon"> |
|||
<TwitterIcon /> |
|||
</span> |
|||
<div class="nav-item-title">Twitter</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="main"> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.root { |
|||
display: grid; |
|||
grid-template-columns: 275px 1fr; |
|||
height: 100%; |
|||
width: 100%; |
|||
background: var(--grey-light); |
|||
} |
|||
|
|||
@media only screen and (min-width: 1800px) { |
|||
.root { |
|||
display: grid; |
|||
grid-template-columns: 300px 1fr; |
|||
height: 100%; |
|||
width: 100%; |
|||
background: var(--grey-light); |
|||
} |
|||
} |
|||
|
|||
.main { |
|||
grid-column: 2; |
|||
} |
|||
|
|||
.ui-nav { |
|||
grid-column: 1; |
|||
background-color: var(--white); |
|||
padding: 20px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
border-right: 1px solid var(--grey-medium); |
|||
} |
|||
|
|||
.home-logo { |
|||
cursor: pointer; |
|||
height: 40px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.home-logo img { |
|||
height: 40px; |
|||
} |
|||
|
|||
.nav-section { |
|||
margin: 20px 0px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.nav-section-title { |
|||
font-size: 20px; |
|||
color: var(--ink); |
|||
font-weight: 700; |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
.nav-item { |
|||
cursor: pointer; |
|||
margin: 0px 0px 4px 0px; |
|||
padding: 0px 0px 0px 12px; |
|||
height: 40px; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.nav-item-home { |
|||
cursor: pointer; |
|||
margin: 0px 0px 4px 0px; |
|||
padding: 0px 0px 0px 12px; |
|||
height: 40px; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
box-sizing: border-box; |
|||
background-color: var(--blue-light); |
|||
} |
|||
|
|||
.nav-item:hover { |
|||
background-color: var(--grey-light); |
|||
border-radius: 3px; |
|||
} |
|||
|
|||
.nav-item::selection { |
|||
background-color: var(--blue-light); |
|||
border-radius: 3px; |
|||
} |
|||
|
|||
.nav-item-title { |
|||
font-size: 14px; |
|||
color: var(--ink); |
|||
font-weight: 500; |
|||
margin-left: 12px; |
|||
} |
|||
|
|||
.nav-item-icon { |
|||
color: var(--ink-light); |
|||
} |
|||
</style> |
|||
@ -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 |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
const CouchDB = require("../../db") |
|||
const newid = require("../../db/newid") |
|||
|
|||
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) |
|||
} |
|||
@ -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 |
|||
@ -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 |
|||
|
|||
@ -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 |
|||
|
|||
@ -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 |
|||
|
|||
@ -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/: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", controller.fetch) |
|||
.post("/api/:instanceId/models", controller.create) |
|||
.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 |
|||
|
|||
@ -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 |
|||
@ -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 |
|||
|
|||
@ -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) |
|||
}) |
|||
}) |
|||
}); |
|||
@ -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); |
|||
}) |
|||
}) |
|||
}); |
|||
@ -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 |
|||
|
|||
@ -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 |
|||
|
|||
@ -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 |
|||
@ -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") |
|||
} |
|||
@ -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, |
|||
} |
|||
@ -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 }, |
|||
] |
|||
} |
|||
@ -0,0 +1 @@ |
|||
dist/ |
|||
@ -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" |
|||
} |
|||
} |
|||
@ -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": "" |
|||
} |
|||
@ -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": "" |
|||
} |
|||
@ -0,0 +1 @@ |
|||
module.exports = () => ({}) |
|||
Loading…
Reference in new issue