Browse Source

Merging master

pull/286/head
Conor_Mack 6 years ago
parent
commit
976c188f1e
  1. 3
      packages/builder/package.json
  2. 4
      packages/builder/src/App.svelte
  3. 10
      packages/builder/src/components/common/Icons/Close.svelte
  4. 6
      packages/builder/src/components/common/Icons/Info.svelte
  5. 2
      packages/builder/src/components/common/Icons/index.js
  6. 12
      packages/builder/src/components/common/Input.svelte
  7. 13
      packages/builder/src/components/common/Inputs/InputGroup.svelte
  8. 6
      packages/builder/src/components/database/ModelDataTable/api.js
  9. 3
      packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte
  10. 2
      packages/builder/src/components/nav/SchemaManagementDrawer.svelte
  11. 68
      packages/builder/src/components/start/AppCard.svelte
  12. 83
      packages/builder/src/components/start/AppList.svelte
  13. 197
      packages/builder/src/components/start/CreateAppModal.svelte
  14. 22
      packages/builder/src/components/userInterface/CategoryTab.svelte
  15. 4
      packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte
  16. 29
      packages/builder/src/components/userInterface/ComponentSelectionList.svelte
  17. 6
      packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte
  18. 16
      packages/builder/src/components/userInterface/FlatButton.svelte
  19. 3
      packages/builder/src/components/userInterface/FlatButtonGroup.svelte
  20. 46
      packages/builder/src/components/userInterface/ItemTab/Item.svelte
  21. 12
      packages/builder/src/components/userInterface/PropertyControl.svelte
  22. 11
      packages/builder/src/components/userInterface/PropertyGroup.svelte
  23. 144
      packages/builder/src/components/userInterface/propertyCategories.js
  24. 81
      packages/builder/src/components/userInterface/temporaryPanelStructure.js
  25. 5
      packages/builder/src/global.css
  26. 95
      packages/builder/src/pages/[application]/_reset.svelte
  27. 2
      packages/builder/src/pages/[application]/frontend/_layout.svelte
  28. 217
      packages/builder/src/pages/_layout.svelte
  29. 271
      packages/builder/src/pages/index.svelte
  30. 9
      packages/cli/src/commands/init/index.js
  31. 26
      packages/cli/src/commands/init/initHandler.js
  32. 13
      packages/server/.vscode/launch.json
  33. 2
      packages/server/package.json
  34. 108
      packages/server/src/api/controllers/accesslevel.js
  35. 71
      packages/server/src/api/controllers/application.js
  36. 2
      packages/server/src/api/controllers/auth.js
  37. 30
      packages/server/src/api/controllers/record.js
  38. 39
      packages/server/src/api/controllers/user.js
  39. 64
      packages/server/src/api/controllers/workflow.js
  40. 12
      packages/server/src/api/index.js
  41. 14
      packages/server/src/api/routes/accesslevel.js
  42. 12
      packages/server/src/api/routes/application.js
  43. 4
      packages/server/src/api/routes/client.js
  44. 3
      packages/server/src/api/routes/component.js
  45. 6
      packages/server/src/api/routes/index.js
  46. 6
      packages/server/src/api/routes/instance.js
  47. 45
      packages/server/src/api/routes/model.js
  48. 120
      packages/server/src/api/routes/pages.js
  49. 12
      packages/server/src/api/routes/record.js
  50. 12
      packages/server/src/api/routes/screen.js
  51. 184
      packages/server/src/api/routes/tests/accesslevel.spec.js
  52. 33
      packages/server/src/api/routes/tests/couchTestUtils.js
  53. 9
      packages/server/src/api/routes/tests/model.spec.js
  54. 23
      packages/server/src/api/routes/tests/record.spec.js
  55. 3
      packages/server/src/api/routes/tests/user.spec.js
  56. 114
      packages/server/src/api/routes/tests/workflow.spec.js
  57. 22
      packages/server/src/api/routes/user.js
  58. 14
      packages/server/src/api/routes/view.js
  59. 13
      packages/server/src/api/routes/workflow.js
  60. 34
      packages/server/src/middleware/authenticated.js
  61. 58
      packages/server/src/middleware/authorized.js
  62. 38
      packages/server/src/schemas/index.js
  63. 64
      packages/server/src/utilities/accessLevels.js
  64. 1
      packages/server/src/utilities/appDirectoryTemplate/.gitignore
  65. 11
      packages/server/src/utilities/appDirectoryTemplate/package.json
  66. 19
      packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json
  67. 0
      packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/.gitkeep
  68. 19
      packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json
  69. 0
      packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/.gitkeep
  70. 1
      packages/server/src/utilities/appDirectoryTemplate/plugins.js

3
packages/builder/package.json

@ -38,6 +38,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.3.5",
"@budibase/client": "^0.0.32", "@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0", "@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0", "codemirror": "^5.51.0",
@ -79,7 +80,7 @@
"rollup-plugin-svelte": "^5.0.3", "rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^4.0.4", "rollup-plugin-terser": "^4.0.4",
"rollup-plugin-url": "^2.2.2", "rollup-plugin-url": "^2.2.2",
"svelte": "^3.0.0" "svelte": "3.23.x"
}, },
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072" "gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
} }

4
packages/builder/src/App.svelte

@ -26,6 +26,4 @@
<AppNotification /> <AppNotification />
<Modal> <Router {routes} />
<Router {routes} />
</Modal>

10
packages/builder/src/components/common/Icons/Close.svelte

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414
1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
</svg>

After

Width:  |  Height:  |  Size: 292 B

6
packages/builder/src/components/common/Icons/Info.svelte

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10
10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z" />
</svg>

After

Width:  |  Height:  |  Size: 271 B

2
packages/builder/src/components/common/Icons/index.js

@ -29,3 +29,5 @@ export { default as ContributionIcon } from "./Contribution.svelte"
export { default as BugIcon } from "./Bug.svelte" export { default as BugIcon } from "./Bug.svelte"
export { default as EmailIcon } from "./Email.svelte" export { default as EmailIcon } from "./Email.svelte"
export { default as TwitterIcon } from "./Twitter.svelte" export { default as TwitterIcon } from "./Twitter.svelte"
export { default as InfoIcon } from "./Info.svelte"
export { default as CloseIcon } from "./Close.svelte"

12
packages/builder/src/components/common/Input.svelte

@ -11,12 +11,12 @@
input { input {
display: block; display: block;
font-size: 14px; font-size: 14px;
font-family: sans-serif;
font-weight: 500; font-weight: 500;
color: #163057; color: var(--ink);
line-height: 1.3; line-height: 1.3;
padding: 1em 2.6em 0.9em 1.4em; padding: 12px;
width: 100%; width: 164px;
float: right;
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@ -24,7 +24,7 @@
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
background: #fff; background: #fff;
border: 1px solid #ccc; border: 1px solid var(--grey-dark);
height: 35px; height: 32px;
} }
</style> </style>

13
packages/builder/src/components/common/Inputs/InputGroup.svelte

@ -29,7 +29,6 @@
<style> <style>
.input-container { .input-container {
display: flex;
} }
.label { .label {
@ -43,16 +42,18 @@
input { input {
width: 40px; width: 40px;
height: 32px; height: 32px;
font-size: 13px; font-size: 12px;
font-weight: 700; font-weight: 700;
margin: 0px 5px; margin: 0px 0px 0px 1px;
color: #163057; text-align: center;
color: var(--ink);
opacity: 0.7; opacity: 0.7;
padding: 5px 10px; padding: 0px 4px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #dbdbdb; border: 1px solid var(--grey);
border-radius: 2px; border-radius: 2px;
outline: none; outline: none;
float: right;
} }
input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-inner-spin-button,

6
packages/builder/src/components/database/ModelDataTable/api.js

@ -15,13 +15,13 @@ export async function createDatabase(appname, instanceName) {
} }
export async function deleteRecord(record, instanceId) { export async function deleteRecord(record, instanceId) {
const DELETE_RECORDS_URL = `/api/${instanceId}/records/${record._id}/${record._rev}` const DELETE_RECORDS_URL = `/api/${instanceId}/${record._modelId}/records/${record._id}/${record._rev}`
const response = await api.delete(DELETE_RECORDS_URL) const response = await api.delete(DELETE_RECORDS_URL)
return response return response
} }
export async function saveRecord(record, instanceId) { export async function saveRecord(record, instanceId, modelId) {
const SAVE_RECORDS_URL = `/api/${instanceId}/records` const SAVE_RECORDS_URL = `/api/${instanceId}/${modelId}/records`
const response = await api.post(SAVE_RECORDS_URL, record) const response = await api.post(SAVE_RECORDS_URL, record)
return await response.json() return await response.json()

3
packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte

@ -42,7 +42,8 @@
...record, ...record,
modelId: $backendUiStore.selectedModel._id, modelId: $backendUiStore.selectedModel._id,
}, },
instanceId instanceId,
$backendUiStore.selectedModel._id
) )
if (recordResponse.errors) { if (recordResponse.errors) {
errors = recordResponse.errors errors = recordResponse.errors

2
packages/builder/src/components/nav/SchemaManagementDrawer.svelte

@ -46,7 +46,7 @@
function selectModel(model) { function selectModel(model) {
backendUiStore.update(state => { backendUiStore.update(state => {
state.selectedModel = model state.selectedModel = model
state.selectedView = `all_${model._id}` state.selectedView = `${model._id}`
return state return state
}) })
} }

68
packages/builder/src/components/start/AppCard.svelte

@ -0,0 +1,68 @@
<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>

83
packages/builder/src/components/start/AppList.svelte

@ -1,5 +1,5 @@
<script> <script>
import Button from "components/common/Button.svelte" import AppCard from "./AppCard.svelte"
export let apps export let apps
function myFunction() { function myFunction() {
@ -13,27 +13,23 @@
<div> <div>
<div> <div>
<div class="app-section-title">Your Web Apps</div> <div class="app-section-title">Your Web Apps</div>
{#each apps as app} <div class="apps">
<div class="apps-card"> {#each apps as app}
<h3 class="app-title">{app.name}</h3> <AppCard {...app} />
<p class="app-desc"> {/each}
A minimalist CRM which removes the noise and allows you to focus </div>
on your business.
</p>
<div class="card-footer">
<div class="modified-date">Last Edited - 25th May 2020</div>
<a href={`/_builder/${app._id}`} class="app-button">
Open Web App
</a>
</div>
</div>
{/each}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<style> <style>
.apps {
display: grid;
grid-template-columns: repeat(auto-fill, 400px);
grid-gap: 40px 85px;
justify-content: start;
}
.root { .root {
margin: 40px 80px; margin: 40px 80px;
} }
@ -44,59 +40,4 @@
font-weight: 700; font-weight: 700;
margin-bottom: 20px; margin-bottom: 20px;
} }
.apps {
display: flex;
flex-wrap: wrap;
gap: 40px;
}
.apps-card {
background-color: var(--white);
padding: 20px;
max-width: 400px;
max-height: 150px;
border-radius: 5px;
border: 1px solid var(--grey-dark);
}
.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> </style>

197
packages/builder/src/components/start/CreateAppModal.svelte

@ -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>

22
packages/builder/src/components/userInterface/CategoryTab.svelte

@ -4,7 +4,7 @@
export let onClick = category => {} export let onClick = category => {}
</script> </script>
<ul class="tabs"> <div class="tabs">
{#each categories as category} {#each categories as category}
<li <li
on:click={() => onClick(category)} on:click={() => onClick(category)}
@ -13,31 +13,23 @@
</li> </li>
{/each} {/each}
</ul> </div>
<style> <style>
.tabs { .tabs {
display: flex; display: flex;
justify-content: center;
list-style: none; list-style: none;
margin: 0 auto; font-size: 18px;
padding: 0 30px; font-weight: 700;
border-bottom: 1px solid #d8d8d8;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.14px;
} }
li { li {
color: #808192; color: var(--ink-lighter);
margin: 0 5px;
padding: 0 8px;
cursor: pointer; cursor: pointer;
margin-right: 20px;
} }
.active { .active {
border-bottom: solid 3px #0055ff; color: var(--ink);
color: #393c44;
} }
</style> </style>

4
packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte

@ -109,7 +109,7 @@
.title > div:nth-child(1) { .title > div:nth-child(1) {
grid-column-start: name; grid-column-start: name;
color: var(--secondary100); color: var(--ink);
} }
.title > div:nth-child(2) { .title > div:nth-child(2) {
@ -117,7 +117,7 @@
} }
.component-props-container { .component-props-container {
margin-top: 10px; margin-top: 20px;
flex: 1 1 auto; flex: 1 1 auto;
} }
</style> </style>

29
packages/builder/src/components/userInterface/ComponentSelectionList.svelte

@ -52,32 +52,9 @@
</div> </div>
<style> <style>
.tabs {
display: flex;
justify-content: center;
list-style: none;
margin: 0 auto;
padding: 0 30px;
border-bottom: 1px solid #d8d8d8;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.14px;
}
li {
color: #808192;
margin: 0 5px;
padding: 0 8px;
cursor: pointer;
}
.panel { .panel {
padding: 20px; padding: 20px 0px;
} display: flex;
flex-wrap: wrap;
.active {
border-bottom: solid 3px #0055ff;
color: #393c44;
} }
</style> </style>

6
packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte

@ -54,13 +54,13 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 20px 0; padding: 20px 20px;
border-left: solid 1px #e8e8ef; border-left: solid 1px var(--grey);
} }
.switcher { .switcher {
display: flex; display: flex;
margin: 0px 20px 20px 20px; margin: 0px 20px 20px 0px;
} }
.switcher > button { .switcher > button {

16
packages/builder/src/components/userInterface/FlatButton.svelte

@ -19,20 +19,20 @@
<style> <style>
.flatbutton { .flatbutton {
cursor: pointer; cursor: pointer;
padding: 5px; padding: 8px 2px;
text-align: center; text-align: center;
background: #ffffff; background: #ffffff;
color: #808192; color: var(--ink-light);
border-radius: 4px; border-radius: 5px;
font-family: Roboto; font-family: Roboto;
font-size: 11px; font-size: 14px;
font-weight: 500; font-weight: 400;
letter-spacing: 0.11px; transition: all 0.3s;
transition: background 0.5s, color 0.5s ease; text-rendering: optimizeLegibility;
} }
.selected { .selected {
background: #808192; background: var(--ink-light);
color: #ffffff; color: #ffffff;
} }
</style> </style>

3
packages/builder/src/components/userInterface/FlatButtonGroup.svelte

@ -1,6 +1,5 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import FlatButton from "./FlatButton.svelte" import FlatButton from "./FlatButton.svelte"
export let buttonProps = [] export let buttonProps = []
export let isMultiSelect = false export let isMultiSelect = false
@ -44,11 +43,9 @@
<style> <style>
.flatbutton-group { .flatbutton-group {
display: flex; display: flex;
flex-flow: row nowrap;
} }
.button-container { .button-container {
flex: 1; flex: 1;
margin: 5px;
} }
</style> </style>

46
packages/builder/src/components/userInterface/ItemTab/Item.svelte

@ -1,66 +1,56 @@
<script> <script>
import { fly } from "svelte/transition"
export let item export let item
</script> </script>
<div class="item-item" on:click> <div class="item-item" transition:fly={{ y: 100, duration: 1000 }} on:click>
<div class="item-icon"> <div class="item-icon">
<i class={item.icon} /> <i class={item.icon} />
</div> </div>
<div class="item-text"> <div class="item-text">
<div class="item-name">{item.name}</div> <div class="item-name">{item.name}</div>
<div class="item-description">
<p>{item.description}</p>
</div>
</div> </div>
</div> </div>
<style> <style>
.item-item { .item-item {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
padding: 10px 0px 8px 10px;
align-items: center;
cursor: pointer; cursor: pointer;
margin-bottom: 8px;
padding: 8px 0px 16px 0px;
width: 120px;
height: 80px;
justify-content: center;
align-items: center;
margin-right: 8px;
background-color: var(--grey-light);
border-radius: 3px;
} }
.item-item:hover { .item-item:hover {
background: #fbfbfb; background: var(--grey);
border-radius: 5px; border-radius: 3px;
transition: all 0.2s;
} }
.item-icon { .item-icon {
flex: 0 0 40px; border-radius: 3px;
background: #f1f4fc;
height: 40px;
border-radius: 5px;
display: flex; display: flex;
justify-content: center;
align-items: center;
} }
.item-text { .item-text {
display: flex; display: flex;
padding-left: 16px;
padding-top: 8px;
flex-direction: column; flex-direction: column;
} }
.item-name { .item-name {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 400;
}
.item-description {
font-size: 12px;
color: #808192;
}
p {
line-height: 15px;
} }
i { i {
font-size: 24px; font-size: 24px;
color: #808192; color: var(--ink-light);
} }
</style> </style>

12
packages/builder/src/components/userInterface/PropertyControl.svelte

@ -43,8 +43,9 @@
<style> <style>
.property-control { .property-control {
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row;
margin: 8px 0px; margin: 8px 0px;
align-items: center;
} }
.label { .label {
@ -53,13 +54,16 @@
align-items: center; align-items: center;
padding: 0px 5px; padding: 0px 5px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 400;
letter-spacing: 0.12px;
text-align: left; text-align: left;
color: var(--ink);
margin-right: auto;
text-transform: capitalize;
} }
.control { .control {
flex: 1; flex: 1;
padding-left: 5px; padding-left: 2px;
max-width: 164px;
} }
</style> </style>

11
packages/builder/src/components/userInterface/PropertyGroup.svelte

@ -42,14 +42,15 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: auto; height: auto;
background: #fbfbfb; background: var(--grey-light);
margin: 5px; margin: 0px 0px 4px 0px;
padding: 5px; padding: 8px 12px;
justify-content: center;
border-radius: 4px;
} }
.property-group-name { .property-group-name {
cursor: pointer; cursor: pointer;
flex: 0 0 20px;
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row nowrap;
} }
@ -61,7 +62,7 @@
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.14px; letter-spacing: 0.14px;
color: #393c44; color: var(--ink);
} }
.icon { .icon {

144
packages/builder/src/components/userInterface/propertyCategories.js

@ -7,20 +7,55 @@ import InputGroup from "../common/Inputs/InputGroup.svelte"
*/ */
export const layout = [ export const layout = [
{
label: "Display",
key: "display",
control: OptionSelect,
initialValue: "Flex",
options: [
{ label: "Flex", value: "flex" },
{ label: "Inline Flex", value: "inline-flex" },
],
},
{ {
label: "Direction", label: "Direction",
key: "flex-direction", key: "flex-direction",
control: OptionSelect, control: OptionSelect,
initialValue: "columnReverse", initialValue: "Row",
options: [ options: [
{ label: "row", value: "row" }, { label: "Row", value: "row" },
{ label: "row-reverse", value: "rowReverse" }, { label: "Row Reverse", value: "rowReverse" },
{ label: "column", value: "column" }, { label: "column", value: "column" },
{ label: "column-reverse", value: "columnReverse" }, { label: "Column Reverse", value: "columnReverse" },
],
},
{
label: "Justify",
key: "justify-content",
control: OptionSelect,
initialValue: "Flex Start",
options: [
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
{ label: "Space Between", value: "space-between" },
{ label: "Space Around", value: "space-around" },
{ label: "Space Evenly", value: "space-evenly" },
],
},
{
label: "Align",
key: "align-items",
control: OptionSelect,
initialValue: "Flex Start",
options: [
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
{ label: "Baseline", value: "baseline" },
{ label: "Stretch", value: "stretch" },
], ],
}, },
{ label: "Justify", key: "justify-content", control: Input },
{ label: "Align", key: "align-items", control: Input },
{ {
label: "Wrap", label: "Wrap",
key: "flex-wrap", key: "flex-wrap",
@ -33,19 +68,20 @@ export const layout = [
] ]
const spacingMeta = [ const spacingMeta = [
{ placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" }, { placeholder: "L" },
{ placeholder: "B" },
{ placeholder: "R" },
{ placeholder: "T" },
] ]
export const spacing = [ export const spacing = [
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
{ {
label: "Padding", label: "Padding",
key: "padding", key: "padding",
control: InputGroup, control: InputGroup,
meta: spacingMeta, meta: spacingMeta,
}, },
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
] ]
export const size = [ export const size = [
@ -62,7 +98,39 @@ export const position = [
label: "Position", label: "Position",
key: "position", key: "position",
control: OptionSelect, control: OptionSelect,
options: ["static", "relative", "fixed", "absolute", "sticky"], initialValue: "Wrap",
options: [
{ label: "Static", value: "static" },
{ label: "Relative", value: "relative" },
{ label: "Fixed", value: "fixed" },
{ label: "Absolute", value: "absolute" },
{ label: "Sticky", value: "sticky" },
],
},
{
label: "Top",
key: "top",
control: Input,
},
{
label: "Right",
key: "right",
control: Input,
},
{
label: "Bottom",
key: "bottom",
control: Input,
},
{
label: "Left",
key: "left",
control: Input,
},
{
label: "Z-index",
key: "z-index",
control: Input,
}, },
] ]
@ -74,13 +142,21 @@ export const typography = [
defaultValue: "initial", defaultValue: "initial",
options: [ options: [
"initial", "initial",
"Times New Roman",
"Georgia",
"Arial", "Arial",
"Arial Black", "Arial Black",
"Cursive",
"Courier",
"Comic Sans MS", "Comic Sans MS",
"Helvetica",
"Impact", "Impact",
"Inter",
"Lucida Sans Unicode", "Lucida Sans Unicode",
"Open Sans",
"Playfair",
"Roboto",
"Roboto Mono",
"Times New Roman",
"Verdana",
], ],
styleBindingProperty: "font-family", styleBindingProperty: "font-family",
}, },
@ -95,8 +171,7 @@ export const typography = [
{ {
label: "Color", label: "Color",
key: "color", key: "color",
control: OptionSelect, control: Input,
options: ["black", "white", "red", "blue", "green"],
}, },
{ {
label: "align", label: "align",
@ -112,8 +187,7 @@ export const background = [
{ {
label: "Background", label: "Background",
key: "background", key: "background",
control: OptionSelect, control: Input,
options: ["black", "white", "red", "blue", "green"],
}, },
{ label: "Image", key: "image", control: Input }, //custom { label: "Image", key: "image", control: Input }, //custom
] ]
@ -124,15 +198,45 @@ export const border = [
{ {
label: "Color", label: "Color",
key: "border-color", key: "border-color",
control: Input,
},
{
label: "Style",
key: "border-style",
control: OptionSelect, control: OptionSelect,
options: ["black", "white", "red", "blue", "green"], options: [
"none",
"hidden",
"dotted",
"dashed",
"solid",
"double",
"groove",
"ridge",
"inset",
"outset",
],
}, },
{ label: "Style", key: "border-style", control: Input },
] ]
export const effects = [ export const effects = [
{ label: "Opacity", key: "opacity", control: Input }, { label: "Opacity", key: "opacity", control: Input },
{ label: "Rotate", key: "transform", control: Input }, //needs special control {
label: "Rotate",
key: "transform",
control: OptionSelect,
options: [
{ label: "None", value: "rotate(0deg)" },
{ label: "45 degrees", value: "rotate(45deg)" },
{ label: "90 degrees", value: "rotate(90deg)" },
{ label: "135 degrees", value: "rotate(135deg)" },
{ label: "180 degrees", value: "rotate(180deg)" },
{ label: "225 degrees", value: "rotate(225deg)" },
{ label: "270 degrees", value: "rotate(270deg)" },
{ label: "315 degrees", value: "rotate(315deg)" },
{ label: "360 degrees", value: "rotate(360deg)" },
],
}, //needs special control
{ label: "Shadow", key: "box-shadow", control: Input }, { label: "Shadow", key: "box-shadow", control: Input },
] ]

81
packages/builder/src/components/userInterface/temporaryPanelStructure.js

@ -10,15 +10,6 @@ export default {
name: "Basic", name: "Basic",
isCategory: true, isCategory: true,
children: [ children: [
{
_component: "##builtin/screenslot",
name: "Screenslot",
description:
"This component is a placeholder for the rendering of a screen within a page.",
icon: "ri-crop-2-line",
commonProps: {},
children: [],
},
{ {
_component: "@budibase/standard-components/container", _component: "@budibase/standard-components/container",
name: "Container", name: "Container",
@ -119,7 +110,7 @@ export default {
{ {
name: "Input", name: "Input",
description: "These components handle user input.", description: "These components handle user input.",
icon: "ri-edit-box-line", icon: "ri-edit-box-fill",
commonProps: {}, commonProps: {},
children: [ children: [
{ {
@ -127,7 +118,7 @@ export default {
name: "Textfield", name: "Textfield",
description: description:
"A textfield component that allows the user to input text.", "A textfield component that allows the user to input text.",
icon: "ri-edit-box-line", icon: "ri-edit-box-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [ settings: [
@ -145,7 +136,7 @@ export default {
_component: "@budibase/standard-components/checkbox", _component: "@budibase/standard-components/checkbox",
name: "Checkbox", name: "Checkbox",
description: "A selectable checkbox component", description: "A selectable checkbox component",
icon: "ri-checkbox-line", icon: "ri-checkbox-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [{ label: "Label", key: "label", control: Input }], settings: [{ label: "Label", key: "label", control: Input }],
@ -166,7 +157,7 @@ export default {
name: "Select", name: "Select",
description: description:
"A select component for choosing from different options", "A select component for choosing from different options",
icon: "ri-file-list-line", icon: "ri-file-list-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [], settings: [],
@ -236,7 +227,7 @@ export default {
name: "Card", name: "Card",
description: description:
"A basic card component that can contain content and actions.", "A basic card component that can contain content and actions.",
icon: "ri-layout-bottom-line", icon: "ri-layout-bottom-fill",
children: [], children: [],
properties: { design: { ...all } }, properties: { design: { ...all } },
}, },
@ -248,21 +239,6 @@ export default {
children: [], children: [],
properties: { design: { ...all } }, properties: { design: { ...all } },
}, },
{
name: "Navigation Bar",
_component: "@budibase/standard-components/Navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
children: [],
properties: { design: { ...all } },
},
],
},
{
name: "Data",
isCategory: true,
children: [
{ {
name: "Table", name: "Table",
description: "A component that generates a table from your data.", description: "A component that generates a table from your data.",
@ -283,27 +259,11 @@ export default {
}, },
children: [], children: [],
}, },
{
_component: "@budibase/standard-components/datatable",
name: "DataTable",
description: "A table for displaying data from the backend.",
icon: "ri-archive-drawer-fill",
properties: { design: { ...all } },
children: [],
},
{
_component: "@budibase/standard-components/dataform",
name: "DataForm",
description: "Form stuff",
icon: "ri-file-edit-fill",
properties: { design: { ...all } },
children: [],
},
{ {
name: "Chart", name: "Chart",
_component: "@budibase/standard-components/datachart", _component: "@budibase/standard-components/datachart",
description: "Shiny chart", description: "Shiny chart",
icon: "ri-bar-chart-line", icon: "ri-bar-chart-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
children: [], children: [],
}, },
@ -311,7 +271,7 @@ export default {
name: "List", name: "List",
_component: "@budibase/standard-components/datalist", _component: "@budibase/standard-components/datalist",
description: "Shiny list", description: "Shiny list",
icon: "ri-file-list-line", icon: "ri-file-list-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
children: [], children: [],
}, },
@ -319,10 +279,35 @@ export default {
name: "Map", name: "Map",
_component: "@budibase/standard-components/datamap", _component: "@budibase/standard-components/datamap",
description: "Shiny map", description: "Shiny map",
icon: "ri-map-pin-line", icon: "ri-map-pin-fill",
properties: { design: { ...all } },
children: [],
},
],
},
{
name: "Layouts",
isCategory: true,
children: [
{
_component: "##builtin/screenslot",
name: "Screenslot",
description:
"This component is a placeholder for the rendering of a screen within a page.",
icon: "ri-crop-2-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
commonProps: {},
children: [], children: [],
}, },
{
name: "Nav Bar",
_component: "@budibase/standard-components/Navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
children: [],
properties: { design: { ...all } },
},
], ],
}, },
], ],

5
packages/builder/src/global.css

@ -13,6 +13,7 @@
--grey: #F2F2F2; --grey: #F2F2F2;
--grey-light: #FBFBFB; --grey-light: #FBFBFB;
--grey-medium: #e8e8ef;
--grey-dark: #E6E6E6; --grey-dark: #E6E6E6;
--primary100: #0055ff; --primary100: #0055ff;
@ -125,6 +126,10 @@ h5 {
color: var(--darkslate); color: var(--darkslate);
} }
textarea {
font-family: var(--fontnormal);
}
.hoverable:hover { .hoverable:hover {
cursor: pointer; cursor: pointer;
} }

95
packages/builder/src/pages/[application]/_layout.svelte → packages/builder/src/pages/[application]/_reset.svelte

@ -1,4 +1,5 @@
<script> <script>
import Modal from "svelte-simple-modal"
import { store } from "builderStore" import { store } from "builderStore"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
@ -25,56 +26,58 @@
} }
</script> </script>
<div class="root"> <Modal>
<div class="root">
<div class="top-nav">
<div class="topleftnav"> <div class="top-nav">
<button class="home-logo"> <div class="topleftnav">
<img <button class="home-logo">
src="/_builder/assets/bb-logo.svg" <img
alt="budibase icon" src="/_builder/assets/bb-logo.svg"
on:click={() => $goto(`/`)} /> alt="budibase icon"
</button> on:click={() => $goto(`/`)} />
</button>
<!-- This gets all indexable subroutes and sticks them in the top nav. -->
{#each $layout.children as { path, title }} <!-- This gets all indexable subroutes and sticks them in the top nav. -->
<span {#each $layout.children as { path, title }}
class:active={$isActive(path)} <span
class="topnavitem" class:active={$isActive(path)}
on:click={() => $goto(path)}> class="topnavitem"
{title} on:click={() => $goto(path)}>
</span> {title}
{/each} </span>
<!-- <IconButton icon="home" {/each}
<!-- <IconButton icon="home"
color="var(--slate)" color="var(--slate)"
hoverColor="var(--secondary75)"/> --> hoverColor="var(--secondary75)"/> -->
</div>
<div class="toprightnav">
<span
class:active={$isActive(`/settings`)}
class="topnavitemright"
on:click={() => $goto(`/settings`)}>
<SettingsIcon />
</span>
<span
class:active={false}
class="topnavitemright"
on:click={() => (location = `/${application}`)}>
<PreviewIcon />
</span>
</div>
</div> </div>
<div class="toprightnav">
<span
class:active={$isActive(`/settings`)}
class="topnavitemright"
on:click={() => $goto(`/settings`)}>
<SettingsIcon />
</span>
<span
class:active={false}
class="topnavitemright"
on:click={() => (location = `/${application}`)}>
<PreviewIcon />
</span>
</div>
</div>
{#await promise} {#await promise}
<!-- This should probably be some kind of loading state? --> <!-- This should probably be some kind of loading state? -->
<div /> <div />
{:then} {:then}
<slot /> <slot />
{:catch error} {:catch error}
<p>Something went wrong: {error.message}</p> <p>Something went wrong: {error.message}</p>
{/await} {/await}
</div> </div>
</Modal>
<style> <style>
.root { .root {
@ -147,7 +150,7 @@
} }
.topnavitemright:hover { .topnavitemright:hover {
color: rgb(255, 255, 255, 0.8); color: var(--ink);
font-weight: 500; font-weight: 500;
} }

2
packages/builder/src/pages/[application]/frontend/_layout.svelte

@ -127,7 +127,6 @@
.root { .root {
display: grid; display: grid;
grid-template-columns: 275px 1fr 275px; grid-template-columns: 275px 1fr 275px;
height: 100%;
width: 100%; width: 100%;
background: var(--grey-light); background: var(--grey-light);
} }
@ -136,7 +135,6 @@
.root { .root {
display: grid; display: grid;
grid-template-columns: 300px 1fr 300px; grid-template-columns: 300px 1fr 300px;
height: 100%;
width: 100%; width: 100%;
background: var(--grey-light); background: var(--grey-light);
} }

217
packages/builder/src/pages/_layout.svelte

@ -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>

271
packages/builder/src/pages/index.svelte

@ -1,23 +1,13 @@
<script> <script>
import { getContext } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
import AppList from "components/start/AppList.svelte" import AppList from "components/start/AppList.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import IconButton from "components/common/IconButton.svelte" import IconButton from "components/common/IconButton.svelte"
import {
SettingsIcon,
AppsIcon,
UpdatesIcon,
HostingIcon,
DocumentationIcon,
TutorialsIcon,
CommunityIcon,
ContributionIcon,
BugIcon,
EmailIcon,
TwitterIcon,
} from "components/common/Icons/"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
let promise = getApps() let promise = getApps()
@ -31,229 +21,54 @@
throw new Error(json) throw new Error(json)
} }
} }
</script>
<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"> // Handle create app modal
<div class="nav-section-title">Build</div> const { open } = getContext("simple-modal")
<div class="nav-item-home">
<span class="nav-item-icon"> const showCreateAppModal = () => {
<AppsIcon /> open(
</span> CreateAppModal,
<div class="nav-item-title">Apps</div> {
</div> message: "What is your name?",
<div class="nav-item"> hasForm: true,
<span class="nav-item-icon"> },
<SettingsIcon /> {
</span> closeButton: false,
<div class="nav-item-title">Settings</div> closeOnEsc: false,
</div> closeOnOuterClick: false,
<a href="https://budibase.con/login" target="_blank" class="nav-item"> styleContent: { padding: 0 },
<span class="nav-item-icon"> closeOnOuterClick: true,
<UpdatesIcon /> }
</span> )
<div class="nav-item-title">Updates</div> }
</a> </script>
<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="welcome">Welcome to Budibase</div>
<div class="nav-section-title">Contact</div> <div class="banner">
<a <div class="banner-content">
href="https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md" <div class="banner-header">
target="_blank" Every accomplishment starts with a decision to try.
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>
<button class="banner-button" type="button" on:click={showCreateAppModal}>
<i class="ri-add-circle-fill" />
Create New Web App
</button>
</div> </div>
<div class="banner-image">
<div class="main"> <img src="/_builder/assets/banner-image.png" alt="Bannerimage" />
<div class="welcome">Welcome to Budibase</div>
<div class="banner">
<div class="banner-content">
<div class="banner-header">
Every accomplishment starts with a decision to try.
</div>
<button class="banner-button" type="button">
<i class="ri-add-circle-fill" />
Create New Web App
</button>
</div>
<div class="banner-image">
<img src="/_builder/assets/banner-image.png" alt="Bannerimage" />
</div>
</div>
{#await promise}
<div class="spinner-container">
<Spinner />
</div>
{:then result}
<AppList apps={result} />
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
</div> </div>
</div> </div>
{#await promise}
<div class="spinner-container">
<Spinner />
</div>
{:then result}
<AppList apps={result} />
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
<style> <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-dark);
}
.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);
}
.welcome { .welcome {
margin: 60px 80px 0px 80px; margin: 60px 80px 0px 80px;
font-size: 42px; font-size: 42px;

9
packages/cli/src/commands/init/index.js

@ -11,13 +11,6 @@ module.exports = {
default: "~/.budibase", default: "~/.budibase",
alias: "d", 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", { yargs.positional("clientId", {
type: "string", type: "string",
describe: "used to determine the name of the global databse", describe: "used to determine the name of the global databse",
@ -28,7 +21,7 @@ module.exports = {
type: "string", type: "string",
describe: describe:
"connection string for couch db, format: https://username:password@localhost:5984", "connection string for couch db, format: https://username:password@localhost:5984",
alias: "x", alias: "u",
default: "", default: "",
}) })
yargs.positional("quiet", { yargs.positional("quiet", {

26
packages/cli/src/commands/init/initHandler.js

@ -14,7 +14,6 @@ const run = async opts => {
try { try {
await ensureAppDir(opts) await ensureAppDir(opts)
await setEnvironmentVariables(opts) await setEnvironmentVariables(opts)
await prompts(opts)
await createClientDatabase(opts) await createClientDatabase(opts)
await createDevEnvFile(opts) await createDevEnvFile(opts)
console.log(chalk.green("Budibase successfully initialised.")) console.log(chalk.green("Budibase successfully initialised."))
@ -24,13 +23,13 @@ const run = async opts => {
} }
const setEnvironmentVariables = 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") const dataDir = join(opts.dir, ".data")
await ensureDir(dataDir) await ensureDir(dataDir)
process.env.COUCH_DB_URL = process.env.COUCH_DB_URL =
dataDir + (dataDir.endsWith("/") || dataDir.endsWith("\\") ? "" : "/") 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) 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 => { const createClientDatabase = async opts => {
// cannot be a top level require as it // cannot be a top level require as it
// will cause environment module to be loaded prematurely // will cause environment module to be loaded prematurely

13
packages/server/.vscode/launch.json

@ -49,6 +49,19 @@
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", "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", "type": "node",
"request": "launch", "request": "launch",

2
packages/server/package.json

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

108
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
}

71
packages/server/src/api/controllers/application.js

@ -3,6 +3,10 @@ const ClientDb = require("../../db/clientDb")
const { getPackageForBuilder } = require("../../utilities/builder") const { getPackageForBuilder } = require("../../utilities/builder")
const newid = require("../../db/newid") const newid = require("../../db/newid")
const env = require("../../environment") 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) { exports.fetch = async function(ctx) {
const db = new CouchDB(ClientDb.name(env.CLIENT_ID)) const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
@ -32,12 +36,77 @@ exports.create = async function(ctx) {
"@budibase/standard-components", "@budibase/standard-components",
"@budibase/materialdesign-components", "@budibase/materialdesign-components",
], ],
...ctx.request.body, name: ctx.request.body.name,
description: ctx.request.body.description,
} }
const { rev } = await db.post(newApplication) const { rev } = await db.post(newApplication)
newApplication._rev = rev 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 (ctx.isDev) {
const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
await runNpmInstall(newAppFolder)
}
ctx.body = newApplication ctx.body = newApplication
ctx.message = `Application ${ctx.request.body.name} created successfully` 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()

2
packages/server/src/api/controllers/auth.js

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

30
packages/server/src/api/controllers/record.js

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

39
packages/server/src/api/controllers/user.js

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

64
packages/server/src/api/controllers/workflow.js

@ -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)
}

12
packages/server/src/api/index.js

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

14
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

12
packages/server/src/api/routes/application.js

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

4
packages/server/src/api/routes/client.js

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

3
packages/server/src/api/routes/component.js

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

6
packages/server/src/api/routes/index.js

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

6
packages/server/src/api/routes/instance.js

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

45
packages/server/src/api/routes/model.js

@ -1,12 +1,49 @@
const Router = require("@koa/router") 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() 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 router
.get("/api/:instanceId/models", controller.fetch) .get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch)
.post("/api/:instanceId/models", controller.create) .post("/api/:instanceId/models", authorized(BUILDER), modelController.create)
// .patch("/api/:instanceId/models", controller.update) // .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 module.exports = router

120
packages/server/src/api/routes/pages.js

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

12
packages/server/src/api/routes/record.js

@ -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

12
packages/server/src/api/routes/screen.js

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

184
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)
})
})
});

33
packages/server/src/api/routes/tests/couchTestUtils.js

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

9
packages/server/src/api/routes/tests/model.spec.js

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

23
packages/server/src/api/routes/tests/record.spec.js

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

3
packages/server/src/api/routes/tests/user.spec.js

@ -6,6 +6,7 @@ const {
defaultHeaders, defaultHeaders,
createUser, createUser,
} = require("./couchTestUtils") } = require("./couchTestUtils")
const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels")
describe("/users", () => { describe("/users", () => {
let request let request
@ -51,7 +52,7 @@ describe("/users", () => {
const res = await request const res = await request
.post(`/api/${instance._id}/users`) .post(`/api/${instance._id}/users`)
.set(defaultHeaders) .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(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)

114
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);
})
})
});

22
packages/server/src/api/routes/user.js

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

14
packages/server/src/api/routes/view.js

@ -1,12 +1,20 @@
const Router = require("@koa/router") 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() const router = 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); // .patch("/api/:databaseId/views", controller.update);
// .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy); // .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 module.exports = router

13
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

34
packages/server/src/middleware/authenticated.js

@ -1,6 +1,11 @@
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes") const STATUS_CODES = require("../utilities/statusCodes")
const env = require("../environment") 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) => { module.exports = async (ctx, next) => {
if (ctx.path === "/_builder") { if (ctx.path === "/_builder") {
@ -8,8 +13,9 @@ module.exports = async (ctx, next) => {
return 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.isAuthenticated = true
ctx.isBuilder = true
await next() await next()
return return
} }
@ -23,7 +29,12 @@ module.exports = async (ctx, next) => {
} }
try { 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 ctx.isAuthenticated = true
} catch (err) { } catch (err) {
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text) ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
@ -31,3 +42,22 @@ module.exports = async (ctx, next) => {
await 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
}

58
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")
}

38
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,
}

64
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 },
]
}

1
packages/server/src/utilities/appDirectoryTemplate/.gitignore

@ -0,0 +1 @@
dist/

11
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"
}
}

19
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": ""
}

0
packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/.gitkeep

19
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": ""
}

0
packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/.gitkeep

1
packages/server/src/utilities/appDirectoryTemplate/plugins.js

@ -0,0 +1 @@
module.exports = () => ({})
Loading…
Cancel
Save