Merge branch 'master' into WIP---Frontend-Components-Blocks-UI
This commit is contained in:
commit
789e51e123
|
@ -38,6 +38,7 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.3.5",
|
||||
"@budibase/client": "^0.0.32",
|
||||
"@nx-js/compiler-util": "^2.0.0",
|
||||
"codemirror": "^5.51.0",
|
||||
|
@ -79,7 +80,7 @@
|
|||
"rollup-plugin-svelte": "^5.0.3",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"rollup-plugin-url": "^2.2.2",
|
||||
"svelte": "^3.0.0"
|
||||
"svelte": "3.23.x"
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
}
|
||||
|
|
|
@ -26,6 +26,4 @@
|
|||
|
||||
<AppNotification />
|
||||
|
||||
<Modal>
|
||||
<Router {routes} />
|
||||
</Modal>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<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: 266 B |
|
@ -0,0 +1 @@
|
|||
<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: 253 B |
|
@ -29,3 +29,5 @@ export { default as ContributionIcon } from "./Contribution.svelte"
|
|||
export { default as BugIcon } from "./Bug.svelte"
|
||||
export { default as EmailIcon } from "./Email.svelte"
|
||||
export { default as TwitterIcon } from "./Twitter.svelte"
|
||||
export { default as InfoIcon } from "./Info.svelte"
|
||||
export { default as CloseIcon } from "./Close.svelte"
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
<script>
|
||||
import Button from "components/common/Button.svelte"
|
||||
export let name, description =`A minimalist CRM which removes the noise and allows you to focus
|
||||
on your business.`, _id;
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="apps-card">
|
||||
<h3 class="app-title">{name}</h3>
|
||||
<p class="app-desc">
|
||||
{description}
|
||||
</p>
|
||||
<div class="card-footer">
|
||||
<div class="modified-date">Last Edited - 25th May 2020</div>
|
||||
<a href={`/_builder/${_id}`} class="app-button">
|
||||
Open Web App
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-card {
|
||||
background-color: var(--white);
|
||||
padding: 20px;
|
||||
max-width: 400px;
|
||||
max-height: 150px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--grey-medium);
|
||||
}
|
||||
|
||||
.app-button:hover {
|
||||
background-color: var(--grey-light);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
color: var(--ink-light);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modified-date {
|
||||
font-size: 14px;
|
||||
color: var(--ink-light);
|
||||
}
|
||||
|
||||
.app-button {
|
||||
background-color: var(--white);
|
||||
color: var(--ink);
|
||||
padding: 12px 20px;
|
||||
border-radius: 5px;
|
||||
border: 1px var(--grey) solid;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import Button from "components/common/Button.svelte"
|
||||
import AppCard from "./AppCard.svelte"
|
||||
export let apps
|
||||
|
||||
function myFunction() {
|
||||
|
@ -13,27 +13,23 @@
|
|||
<div>
|
||||
<div>
|
||||
<div class="app-section-title">Your Web Apps</div>
|
||||
<div class="apps">
|
||||
{#each apps as app}
|
||||
<div class="apps-card">
|
||||
<h3 class="app-title">{app.name}</h3>
|
||||
<p class="app-desc">
|
||||
A minimalist CRM which removes the noise and allows you to focus
|
||||
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>
|
||||
<AppCard {...app} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 400px);
|
||||
grid-gap: 40px 85px;
|
||||
justify-content: start;
|
||||
}
|
||||
.root {
|
||||
margin: 40px 80px;
|
||||
}
|
||||
|
@ -44,59 +40,4 @@
|
|||
font-weight: 700;
|
||||
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>
|
||||
|
|
|
@ -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>
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
--grey: #F2F2F2;
|
||||
--grey-light: #FBFBFB;
|
||||
--grey-medium: #e8e8ef;
|
||||
--grey-dark: #E6E6E6;
|
||||
|
||||
--primary100: #0055ff;
|
||||
|
@ -125,6 +126,10 @@ h5 {
|
|||
color: var(--darkslate);
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: var(--fontnormal);
|
||||
}
|
||||
|
||||
.hoverable:hover {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import Modal from "svelte-simple-modal"
|
||||
import { store } from "builderStore"
|
||||
|
||||
import { fade } from "svelte/transition"
|
||||
|
@ -25,6 +26,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
<div class="root">
|
||||
|
||||
<div class="top-nav">
|
||||
|
@ -75,6 +77,7 @@
|
|||
{/await}
|
||||
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.root {
|
|
@ -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>
|
|
@ -1,23 +1,13 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { store } from "builderStore"
|
||||
import AppList from "components/start/AppList.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import ActionButton from "components/common/ActionButton.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 CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
|
||||
let promise = getApps()
|
||||
|
||||
|
@ -31,110 +21,35 @@
|
|||
throw new Error(json)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle create app modal
|
||||
const { open } = getContext("simple-modal")
|
||||
|
||||
const showCreateAppModal = () => {
|
||||
open(
|
||||
CreateAppModal,
|
||||
{
|
||||
message: "What is your name?",
|
||||
hasForm: true,
|
||||
},
|
||||
{
|
||||
closeButton: false,
|
||||
closeOnEsc: false,
|
||||
closeOnOuterClick: false,
|
||||
styleContent: { padding: 0 },
|
||||
closeOnOuterClick: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
</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">
|
||||
<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">
|
||||
<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">
|
||||
<button class="banner-button" type="button" on:click={showCreateAppModal}>
|
||||
<i class="ri-add-circle-fill" />
|
||||
Create New Web App
|
||||
</button>
|
||||
|
@ -152,108 +67,8 @@
|
|||
{:catch err}
|
||||
<h1 style="color:red">{err}</h1>
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 {
|
||||
margin: 60px 80px 0px 80px;
|
||||
font-size: 42px;
|
||||
|
|
|
@ -11,13 +11,6 @@ module.exports = {
|
|||
default: "~/.budibase",
|
||||
alias: "d",
|
||||
})
|
||||
yargs.positional("database", {
|
||||
type: "string",
|
||||
describe: "use a local (PouchDB) or remote (CouchDB) database",
|
||||
alias: "b",
|
||||
default: "local",
|
||||
choices: ["local", "remote"],
|
||||
})
|
||||
yargs.positional("clientId", {
|
||||
type: "string",
|
||||
describe: "used to determine the name of the global databse",
|
||||
|
@ -28,7 +21,7 @@ module.exports = {
|
|||
type: "string",
|
||||
describe:
|
||||
"connection string for couch db, format: https://username:password@localhost:5984",
|
||||
alias: "x",
|
||||
alias: "u",
|
||||
default: "",
|
||||
})
|
||||
yargs.positional("quiet", {
|
||||
|
|
|
@ -14,7 +14,6 @@ const run = async opts => {
|
|||
try {
|
||||
await ensureAppDir(opts)
|
||||
await setEnvironmentVariables(opts)
|
||||
await prompts(opts)
|
||||
await createClientDatabase(opts)
|
||||
await createDevEnvFile(opts)
|
||||
console.log(chalk.green("Budibase successfully initialised."))
|
||||
|
@ -24,13 +23,13 @@ const run = async opts => {
|
|||
}
|
||||
|
||||
const setEnvironmentVariables = async opts => {
|
||||
if (opts.database === "local") {
|
||||
if (opts.couchDbUrl) {
|
||||
process.env.COUCH_DB_URL = opts.couchDbUrl
|
||||
} else {
|
||||
const dataDir = join(opts.dir, ".data")
|
||||
await ensureDir(dataDir)
|
||||
process.env.COUCH_DB_URL =
|
||||
dataDir + (dataDir.endsWith("/") || dataDir.endsWith("\\") ? "" : "/")
|
||||
} else {
|
||||
process.env.COUCH_DB_URL = opts.couchDbUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,25 +38,6 @@ const ensureAppDir = async opts => {
|
|||
await ensureDir(opts.dir)
|
||||
}
|
||||
|
||||
const prompts = async opts => {
|
||||
const questions = [
|
||||
{
|
||||
type: "input",
|
||||
name: "couchDbUrl",
|
||||
message:
|
||||
"CouchDB Connection String (e.g. https://user:password@localhost:5984): ",
|
||||
validate: function(value) {
|
||||
return !!value || "Please enter connection string"
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (opts.database === "remote" && !opts.couchDbUrl) {
|
||||
const answers = await inquirer.prompt(questions)
|
||||
opts.couchDbUrl = answers.couchDbUrl
|
||||
}
|
||||
}
|
||||
|
||||
const createClientDatabase = async opts => {
|
||||
// cannot be a top level require as it
|
||||
// will cause environment module to be loaded prematurely
|
||||
|
|
|
@ -49,6 +49,19 @@
|
|||
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Jest - Access Levels",
|
||||
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||
"args": ["accesslevel.spec", "--runInBand"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"disableOptimisticBPs": true,
|
||||
"windows": {
|
||||
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"test": "jest routes --runInBand",
|
||||
"test:integration": "jest routes --runInBand",
|
||||
"test:integration": "jest workflow --runInBand",
|
||||
"test:watch": "jest -w",
|
||||
"initialise": "node ../cli/bin/budi init -b local -q",
|
||||
"budi": "node ../cli/bin/budi",
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -3,6 +3,10 @@ const ClientDb = require("../../db/clientDb")
|
|||
const { getPackageForBuilder } = require("../../utilities/builder")
|
||||
const newid = require("../../db/newid")
|
||||
const env = require("../../environment")
|
||||
const instanceController = require("./instance")
|
||||
const { resolve, join } = require("path")
|
||||
const { copy, readJSON, writeJSON, exists } = require("fs-extra")
|
||||
const { exec } = require("child_process")
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
|
||||
|
@ -32,12 +36,77 @@ exports.create = async function(ctx) {
|
|||
"@budibase/standard-components",
|
||||
"@budibase/materialdesign-components",
|
||||
],
|
||||
...ctx.request.body,
|
||||
name: ctx.request.body.name,
|
||||
description: ctx.request.body.description,
|
||||
}
|
||||
|
||||
const { rev } = await db.post(newApplication)
|
||||
newApplication._rev = rev
|
||||
|
||||
const createInstCtx = {
|
||||
params: {
|
||||
clientId: env.CLIENT_ID,
|
||||
applicationId: newApplication._id,
|
||||
},
|
||||
request: {
|
||||
body: { name: `dev-${env.CLIENT_ID}` },
|
||||
},
|
||||
}
|
||||
await instanceController.create(createInstCtx)
|
||||
|
||||
if (ctx.isDev) {
|
||||
const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
|
||||
await runNpmInstall(newAppFolder)
|
||||
}
|
||||
|
||||
ctx.body = newApplication
|
||||
ctx.message = `Application ${ctx.request.body.name} created successfully`
|
||||
}
|
||||
|
||||
const createEmptyAppPackage = async (ctx, app) => {
|
||||
const templateFolder = resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"utilities",
|
||||
"appDirectoryTemplate"
|
||||
)
|
||||
|
||||
const appsFolder = env.BUDIBASE_DIR
|
||||
const newAppFolder = resolve(appsFolder, app._id)
|
||||
|
||||
if (await exists(newAppFolder)) {
|
||||
ctx.throw(400, "App folder already exists for this application")
|
||||
return
|
||||
}
|
||||
|
||||
await copy(templateFolder, newAppFolder)
|
||||
|
||||
const packageJsonPath = join(appsFolder, app._id, "package.json")
|
||||
const packageJson = await readJSON(packageJsonPath)
|
||||
|
||||
packageJson.name = npmFriendlyAppName(app.name)
|
||||
|
||||
await writeJSON(packageJsonPath, packageJson)
|
||||
|
||||
return newAppFolder
|
||||
}
|
||||
|
||||
const runNpmInstall = async newAppFolder => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cmd = `cd ${newAppFolder} && npm install`
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
}
|
||||
resolve(stdout ? stdout : stderr)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const npmFriendlyAppName = name =>
|
||||
name
|
||||
.replace(/_/g, "")
|
||||
.replace(/./g, "")
|
||||
.replace(/ /g, "")
|
||||
.toLowerCase()
|
||||
|
|
|
@ -37,7 +37,7 @@ exports.authenticate = async ctx => {
|
|||
if (await bcrypt.compare(password, dbUser.password)) {
|
||||
const payload = {
|
||||
userId: dbUser._id,
|
||||
accessLevel: "",
|
||||
accessLevelId: dbUser.accessLevelId,
|
||||
instanceId: instanceId,
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ const ajv = new Ajv()
|
|||
exports.save = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const record = ctx.request.body
|
||||
record.modelId = ctx.params.modelId
|
||||
|
||||
if (!record._rev && !record._id) {
|
||||
record._id = newid()
|
||||
|
@ -43,16 +44,12 @@ exports.save = async function(ctx) {
|
|||
record.type = "record"
|
||||
const response = await db.post(record)
|
||||
record._rev = response.rev
|
||||
// await ctx.publish(events.recordApi.save.onRecordCreated, {
|
||||
// record: record,
|
||||
// })
|
||||
|
||||
ctx.body = record
|
||||
ctx.status = 200
|
||||
ctx.message = `${model.name} created successfully`
|
||||
}
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
exports.fetchView = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const response = await db.query(`database/${ctx.params.viewName}`, {
|
||||
include_docs: true,
|
||||
|
@ -60,13 +57,30 @@ exports.fetch = async function(ctx) {
|
|||
ctx.body = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.fetchModel = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const response = await db.query(`database/all_${ctx.params.modelId}`, {
|
||||
include_docs: true,
|
||||
})
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
ctx.body = await db.get(ctx.params.recordId)
|
||||
const record = await db.get(ctx.params.recordId)
|
||||
if (record.modelId !== ctx.params.modelId) {
|
||||
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
|
||||
return
|
||||
}
|
||||
ctx.body = record
|
||||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const databaseId = ctx.params.instanceId
|
||||
const db = new CouchDB(databaseId)
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const record = await db.get(ctx.params.recordId)
|
||||
if (record.modelId !== ctx.params.modelId) {
|
||||
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
|
||||
return
|
||||
}
|
||||
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,11 @@ const CouchDB = require("../../db")
|
|||
const clientDb = require("../../db/clientDb")
|
||||
const bcrypt = require("../../utilities/bcrypt")
|
||||
const env = require("../../environment")
|
||||
|
||||
const getUserId = userName => `user_${userName}`
|
||||
const {
|
||||
POWERUSER_LEVEL_ID,
|
||||
ADMIN_LEVEL_ID,
|
||||
} = require("../../utilities/accessLevels")
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const database = new CouchDB(ctx.params.instanceId)
|
||||
|
@ -18,17 +21,26 @@ exports.fetch = async function(ctx) {
|
|||
exports.create = async function(ctx) {
|
||||
const database = new CouchDB(ctx.params.instanceId)
|
||||
const appId = (await database.get("_design/database")).metadata.applicationId
|
||||
const { username, password, name } = ctx.request.body
|
||||
const { username, password, name, accessLevelId } = ctx.request.body
|
||||
|
||||
if (!username || !password) ctx.throw(400, "Username and Password Required.")
|
||||
if (!username || !password) {
|
||||
ctx.throw(400, "Username and Password Required.")
|
||||
}
|
||||
|
||||
const response = await database.post({
|
||||
const accessLevel = await checkAccessLevel(database, accessLevelId)
|
||||
|
||||
if (!accessLevel) ctx.throw(400, "Invalid Access Level")
|
||||
|
||||
const user = {
|
||||
_id: getUserId(username),
|
||||
username,
|
||||
password: await bcrypt.hash(password),
|
||||
name: name || username,
|
||||
type: "user",
|
||||
})
|
||||
accessLevelId,
|
||||
}
|
||||
|
||||
const response = await database.post(user)
|
||||
|
||||
// the clientDB needs to store a map of users against the app
|
||||
const db = new CouchDB(clientDb.name(env.CLIENT_ID))
|
||||
|
@ -49,6 +61,8 @@ exports.create = async function(ctx) {
|
|||
}
|
||||
}
|
||||
|
||||
exports.update = async function() {}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const database = new CouchDB(ctx.params.instanceId)
|
||||
await database.destroy(getUserId(ctx.params.username))
|
||||
|
@ -65,3 +79,18 @@ exports.find = async function(ctx) {
|
|||
_rev: user._rev,
|
||||
}
|
||||
}
|
||||
|
||||
const checkAccessLevel = async (db, accessLevelId) => {
|
||||
if (!accessLevelId) return
|
||||
if (
|
||||
accessLevelId === POWERUSER_LEVEL_ID ||
|
||||
accessLevelId === ADMIN_LEVEL_ID
|
||||
) {
|
||||
return {
|
||||
_id: accessLevelId,
|
||||
name: accessLevelId,
|
||||
permissions: [],
|
||||
}
|
||||
}
|
||||
return await db.get(accessLevelId)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -7,7 +7,6 @@ const {
|
|||
authRoutes,
|
||||
pageRoutes,
|
||||
userRoutes,
|
||||
recordRoutes,
|
||||
instanceRoutes,
|
||||
clientRoutes,
|
||||
applicationRoutes,
|
||||
|
@ -15,6 +14,8 @@ const {
|
|||
viewRoutes,
|
||||
staticRoutes,
|
||||
componentRoutes,
|
||||
workflowRoutes,
|
||||
accesslevelRoutes,
|
||||
} = require("./routes")
|
||||
|
||||
const router = new Router()
|
||||
|
@ -70,11 +71,11 @@ router.use(modelRoutes.allowedMethods())
|
|||
router.use(userRoutes.routes())
|
||||
router.use(userRoutes.allowedMethods())
|
||||
|
||||
router.use(recordRoutes.routes())
|
||||
router.use(recordRoutes.allowedMethods())
|
||||
|
||||
router.use(instanceRoutes.routes())
|
||||
router.use(instanceRoutes.allowedMethods())
|
||||
|
||||
router.use(workflowRoutes.routes())
|
||||
router.use(workflowRoutes.allowedMethods())
|
||||
// end auth routes
|
||||
|
||||
router.use(pageRoutes.routes())
|
||||
|
@ -89,6 +90,9 @@ router.use(componentRoutes.allowedMethods())
|
|||
router.use(clientRoutes.routes())
|
||||
router.use(clientRoutes.allowedMethods())
|
||||
|
||||
router.use(accesslevelRoutes.routes())
|
||||
router.use(accesslevelRoutes.allowedMethods())
|
||||
|
||||
router.use(staticRoutes.routes())
|
||||
router.use(staticRoutes.allowedMethods())
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/accesslevel")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.post("/api/:instanceId/accesslevels", controller.create)
|
||||
.put("/api/:instanceId/accesslevels", controller.update)
|
||||
.get("/api/:instanceId/accesslevels", controller.fetch)
|
||||
.get("/api/:instanceId/accesslevels/:levelId", controller.find)
|
||||
.delete("/api/:instanceId/accesslevels/:levelId/:rev", controller.destroy)
|
||||
.patch("/api/:instanceId/accesslevels/:levelId", controller.patch)
|
||||
|
||||
module.exports = router
|
|
@ -1,11 +1,17 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/application")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/applications", controller.fetch)
|
||||
.get("/api/:applicationId/appPackage", controller.fetchAppPackage)
|
||||
.post("/api/applications", controller.create)
|
||||
.get("/api/applications", authorized(BUILDER), controller.fetch)
|
||||
.get(
|
||||
"/api/:applicationId/appPackage",
|
||||
authorized(BUILDER),
|
||||
controller.fetchAppPackage
|
||||
)
|
||||
.post("/api/applications", authorized(BUILDER), controller.create)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/client")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get("/api/client/id", controller.getClientId)
|
||||
router.get("/api/client/id", authorized(BUILDER), controller.getClientId)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/component")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get(
|
||||
"/:appId/components/definitions",
|
||||
authorized(BUILDER),
|
||||
controller.fetchAppComponentDefinitions
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const authRoutes = require("./auth")
|
||||
const pageRoutes = require("./pages")
|
||||
const userRoutes = require("./user")
|
||||
const recordRoutes = require("./record")
|
||||
const instanceRoutes = require("./instance")
|
||||
const clientRoutes = require("./client")
|
||||
const applicationRoutes = require("./application")
|
||||
|
@ -9,12 +8,13 @@ const modelRoutes = require("./model")
|
|||
const viewRoutes = require("./view")
|
||||
const staticRoutes = require("./static")
|
||||
const componentRoutes = require("./component")
|
||||
const workflowRoutes = require("./workflow")
|
||||
const accesslevelRoutes = require("./accesslevel")
|
||||
|
||||
module.exports = {
|
||||
authRoutes,
|
||||
pageRoutes,
|
||||
userRoutes,
|
||||
recordRoutes,
|
||||
instanceRoutes,
|
||||
clientRoutes,
|
||||
applicationRoutes,
|
||||
|
@ -22,4 +22,6 @@ module.exports = {
|
|||
viewRoutes,
|
||||
staticRoutes,
|
||||
componentRoutes,
|
||||
workflowRoutes,
|
||||
accesslevelRoutes,
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/instance")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.post("/api/:applicationId/instances", controller.create)
|
||||
.delete("/api/instances/:instanceId", controller.destroy)
|
||||
.post("/api/:applicationId/instances", authorized(BUILDER), controller.create)
|
||||
.delete("/api/instances/:instanceId", authorized(BUILDER), controller.destroy)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,12 +1,49 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/model")
|
||||
const modelController = require("../controllers/model")
|
||||
const recordController = require("../controllers/record")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const {
|
||||
READ_MODEL,
|
||||
WRITE_MODEL,
|
||||
BUILDER,
|
||||
} = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
// records
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/models", controller.fetch)
|
||||
.post("/api/:instanceId/models", controller.create)
|
||||
.get(
|
||||
"/api/:instanceId/:modelId/records",
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.fetchModel
|
||||
)
|
||||
.get(
|
||||
"/api/:instanceId/:modelId/records/:recordId",
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.find
|
||||
)
|
||||
.post(
|
||||
"/api/:instanceId/:modelId/records",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.save
|
||||
)
|
||||
.delete(
|
||||
"/api/:instanceId/:modelId/records/:recordId/:revId",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.destroy
|
||||
)
|
||||
|
||||
// models
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch)
|
||||
.post("/api/:instanceId/models", authorized(BUILDER), modelController.create)
|
||||
// .patch("/api/:instanceId/models", controller.update)
|
||||
.delete("/api/:instanceId/models/:modelId/:revId", controller.destroy)
|
||||
.delete(
|
||||
"/api/:instanceId/models/:modelId/:revId",
|
||||
authorized(BUILDER),
|
||||
modelController.destroy
|
||||
)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -7,10 +7,15 @@ const {
|
|||
renameScreen,
|
||||
deleteScreen,
|
||||
} = require("../../utilities/builder")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post("/_builder/api/:appId/pages/:pageName", async ctx => {
|
||||
router.post(
|
||||
"/_builder/api/:appId/pages/:pageName",
|
||||
authorized(BUILDER),
|
||||
async ctx => {
|
||||
await buildPage(
|
||||
ctx.config,
|
||||
ctx.params.appId,
|
||||
|
@ -18,18 +23,26 @@ router.post("/_builder/api/:appId/pages/:pageName", async ctx => {
|
|||
ctx.request.body
|
||||
)
|
||||
ctx.response.status = StatusCodes.OK
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
router.get("/_builder/api/:appId/pages/:pagename/screens", async ctx => {
|
||||
router.get(
|
||||
"/_builder/api/:appId/pages/:pagename/screens",
|
||||
authorized(BUILDER),
|
||||
async ctx => {
|
||||
ctx.body = await listScreens(
|
||||
ctx.config,
|
||||
ctx.params.appId,
|
||||
ctx.params.pagename
|
||||
)
|
||||
ctx.response.status = StatusCodes.OK
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
router.post("/_builder/api/:appId/pages/:pagename/screen", async ctx => {
|
||||
router.post(
|
||||
"/_builder/api/:appId/pages/:pagename/screen",
|
||||
authorized(BUILDER),
|
||||
async ctx => {
|
||||
ctx.body = await saveScreen(
|
||||
ctx.config,
|
||||
ctx.params.appId,
|
||||
|
@ -37,9 +50,13 @@ router.post("/_builder/api/:appId/pages/:pagename/screen", async ctx => {
|
|||
ctx.request.body
|
||||
)
|
||||
ctx.response.status = StatusCodes.OK
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
router.patch("/_builder/api/:appname/pages/:pagename/screen", async ctx => {
|
||||
router.patch(
|
||||
"/_builder/api/:appname/pages/:pagename/screen",
|
||||
authorized(BUILDER),
|
||||
async ctx => {
|
||||
await renameScreen(
|
||||
ctx.config,
|
||||
ctx.params.appname,
|
||||
|
@ -48,9 +65,13 @@ router.patch("/_builder/api/:appname/pages/:pagename/screen", async ctx => {
|
|||
ctx.request.body.newname
|
||||
)
|
||||
ctx.response.status = StatusCodes.OK
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
router.delete("/_builder/api/:appname/pages/:pagename/screen/*", async ctx => {
|
||||
router.delete(
|
||||
"/_builder/api/:appname/pages/:pagename/screen/*",
|
||||
authorized(BUILDER),
|
||||
async ctx => {
|
||||
const name = ctx.request.path.replace(
|
||||
`/_builder/api/${ctx.params.appname}/pages/${ctx.params.pagename}/screen/`,
|
||||
""
|
||||
|
@ -64,6 +85,7 @@ router.delete("/_builder/api/:appname/pages/:pagename/screen/*", async ctx => {
|
|||
)
|
||||
|
||||
ctx.response.status = StatusCodes.OK
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/record")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/:viewName/records", controller.fetch)
|
||||
.get("/api/:instanceId/records/:recordId", controller.find)
|
||||
.post("/api/:instanceId/records", controller.save)
|
||||
.delete("/api/:instanceId/records/:recordId/:revId", controller.destroy)
|
||||
|
||||
module.exports = router
|
|
@ -1,11 +1,17 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/screen")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/screens", controller.fetch)
|
||||
.post("/api/:instanceId/screens", controller.save)
|
||||
.delete("/api/:instanceId/:screenId/:revId", controller.destroy)
|
||||
.get("/api/:instanceId/screens", authorized(BUILDER), controller.fetch)
|
||||
.post("/api/:instanceId/screens", authorized(BUILDER), controller.save)
|
||||
.delete(
|
||||
"/api/:instanceId/:screenId/:revId",
|
||||
authorized(BUILDER),
|
||||
controller.destroy
|
||||
)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
const {
|
||||
createInstance,
|
||||
createClientDatabase,
|
||||
createApplication,
|
||||
createModel,
|
||||
createView,
|
||||
supertest,
|
||||
defaultHeaders
|
||||
} = require("./couchTestUtils")
|
||||
const {
|
||||
generateAdminPermissions,
|
||||
generatePowerUserPermissions,
|
||||
POWERUSER_LEVEL_ID,
|
||||
ADMIN_LEVEL_ID,
|
||||
READ_MODEL,
|
||||
WRITE_MODEL,
|
||||
} = require("../../../utilities/accessLevels")
|
||||
|
||||
describe("/accesslevels", () => {
|
||||
let appId
|
||||
let server
|
||||
let request
|
||||
let instanceId
|
||||
let model
|
||||
let view
|
||||
|
||||
beforeAll(async () => {
|
||||
({ request, server } = await supertest())
|
||||
await createClientDatabase(request);
|
||||
appId = (await createApplication(request))._id
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
server.close();
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
instanceId = (await createInstance(request, appId))._id
|
||||
model = await createModel(request, instanceId)
|
||||
view = await createView(request, instanceId)
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
|
||||
it("returns a success message when level is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.send({ name: "user" })
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.res.statusMessage).toEqual("Access Level 'user' created successfully.")
|
||||
expect(res.body._id).toBeDefined()
|
||||
expect(res.body._rev).toBeDefined()
|
||||
expect(res.body.permissions).toEqual([])
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
describe("fetch", () => {
|
||||
|
||||
it("should list custom levels, plus 2 default levels", async () => {
|
||||
const createRes = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] })
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const customLevel = createRes.body
|
||||
|
||||
const res = await request
|
||||
.get(`/api/${instanceId}/accesslevels`)
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.length).toBe(3)
|
||||
|
||||
const adminLevel = res.body.find(r => r._id === ADMIN_LEVEL_ID)
|
||||
expect(adminLevel).toBeDefined()
|
||||
expect(adminLevel.permissions).toEqual(await generateAdminPermissions(instanceId))
|
||||
|
||||
const powerUserLevel = res.body.find(r => r._id === POWERUSER_LEVEL_ID)
|
||||
expect(powerUserLevel).toBeDefined()
|
||||
expect(powerUserLevel.permissions).toEqual(await generatePowerUserPermissions(instanceId))
|
||||
|
||||
const customLevelFetched = res.body.find(r => r._id === customLevel._id)
|
||||
expect(customLevelFetched.permissions).toEqual(customLevel.permissions)
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
describe("destroy", () => {
|
||||
it("should delete custom access level", async () => {
|
||||
const createRes = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL } ] })
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const customLevel = createRes.body
|
||||
|
||||
await request
|
||||
.delete(`/api/${instanceId}/accesslevels/${customLevel._id}/${customLevel._rev}`)
|
||||
.set(defaultHeaders)
|
||||
.expect(200)
|
||||
|
||||
await request
|
||||
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
|
||||
.set(defaultHeaders)
|
||||
.expect(404)
|
||||
})
|
||||
})
|
||||
|
||||
describe("patch", () => {
|
||||
it("should add given permissions", async () => {
|
||||
const createRes = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] })
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const customLevel = createRes.body
|
||||
|
||||
await request
|
||||
.patch(`/api/${instanceId}/accesslevels/${customLevel._id}`)
|
||||
.send({
|
||||
_rev: customLevel._rev,
|
||||
addedPermissions: [ { itemId: model._id, name: WRITE_MODEL } ]
|
||||
})
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const finalRes = await request
|
||||
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
|
||||
.set(defaultHeaders)
|
||||
.expect(200)
|
||||
|
||||
expect(finalRes.body.permissions.length).toBe(2)
|
||||
expect(finalRes.body.permissions.some(p => p.name === WRITE_MODEL)).toBe(true)
|
||||
expect(finalRes.body.permissions.some(p => p.name === READ_MODEL)).toBe(true)
|
||||
})
|
||||
|
||||
it("should remove given permissions", async () => {
|
||||
const createRes = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.send({
|
||||
name: "user",
|
||||
permissions: [
|
||||
{ itemId: model._id, name: READ_MODEL },
|
||||
{ itemId: model._id, name: WRITE_MODEL },
|
||||
]
|
||||
})
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const customLevel = createRes.body
|
||||
|
||||
await request
|
||||
.patch(`/api/${instanceId}/accesslevels/${customLevel._id}`)
|
||||
.send({
|
||||
_rev: customLevel._rev,
|
||||
removedPermissions: [ { itemId: model._id, name: WRITE_MODEL }]
|
||||
})
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const finalRes = await request
|
||||
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
|
||||
.set(defaultHeaders)
|
||||
.expect(200)
|
||||
|
||||
expect(finalRes.body.permissions.length).toBe(1)
|
||||
expect(finalRes.body.permissions.some(p => p.name === READ_MODEL)).toBe(true)
|
||||
})
|
||||
})
|
||||
});
|
|
@ -2,6 +2,7 @@ const CouchDB = require("../../../db")
|
|||
const { create, destroy } = require("../../../db/clientDb")
|
||||
const supertest = require("supertest")
|
||||
const app = require("../../../app")
|
||||
const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels")
|
||||
|
||||
const TEST_CLIENT_ID = "test-client-id"
|
||||
|
||||
|
@ -17,7 +18,7 @@ exports.supertest = async () => {
|
|||
|
||||
exports.defaultHeaders = {
|
||||
Accept: "application/json",
|
||||
Authorization: "Basic test-admin-secret",
|
||||
Cookie: ["builder:token=test-admin-secret"],
|
||||
}
|
||||
|
||||
exports.createModel = async (request, instanceId, model) => {
|
||||
|
@ -37,6 +38,18 @@ exports.createModel = async (request, instanceId, model) => {
|
|||
return res.body
|
||||
}
|
||||
|
||||
exports.createView = async (request, instanceId, view) => {
|
||||
view = view || {
|
||||
map: "function(doc) { emit(doc[doc.key], doc._id); } ",
|
||||
}
|
||||
|
||||
const res = await request
|
||||
.post(`/api/${instanceId}/views`)
|
||||
.set(exports.defaultHeaders)
|
||||
.send(view)
|
||||
return res.body
|
||||
}
|
||||
|
||||
exports.createClientDatabase = async () => await create(TEST_CLIENT_ID)
|
||||
|
||||
exports.createApplication = async (request, name = "test_application") => {
|
||||
|
@ -70,20 +83,20 @@ exports.createUser = async (
|
|||
const res = await request
|
||||
.post(`/api/${instanceId}/users`)
|
||||
.set(exports.defaultHeaders)
|
||||
.send({ name: "Bill", username, password })
|
||||
.send({
|
||||
name: "Bill",
|
||||
username,
|
||||
password,
|
||||
accessLevelId: POWERUSER_LEVEL_ID,
|
||||
})
|
||||
return res.body
|
||||
}
|
||||
|
||||
exports.insertDocument = async (databaseId, document) => {
|
||||
const { id, ...documentFields } = document
|
||||
await new CouchDB(databaseId).put({ _id: id, ...documentFields })
|
||||
return await new CouchDB(databaseId).put({ _id: id, ...documentFields })
|
||||
}
|
||||
|
||||
exports.createSchema = async (request, instanceId, schema) => {
|
||||
for (let model of schema.models) {
|
||||
await request.post(`/api/${instanceId}/models`).send(model)
|
||||
}
|
||||
for (let view of schema.views) {
|
||||
await request.post(`/api/${instanceId}/views`).send(view)
|
||||
}
|
||||
exports.destroyDocument = async (databaseId, documentId) => {
|
||||
return await new CouchDB(databaseId).destroy(documentId)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@ const {
|
|||
createModel,
|
||||
supertest,
|
||||
createClientDatabase,
|
||||
createApplication
|
||||
createApplication ,
|
||||
defaultHeaders
|
||||
} = require("./couchTestUtils")
|
||||
|
||||
describe("/models", () => {
|
||||
|
@ -38,7 +39,7 @@ describe("/models", () => {
|
|||
name: { type: "string" }
|
||||
}
|
||||
})
|
||||
.set("Accept", "application/json")
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (err, res) => {
|
||||
|
@ -60,7 +61,7 @@ describe("/models", () => {
|
|||
it("returns all the models for that instance in the response body", done => {
|
||||
request
|
||||
.get(`/api/${instance._id}/models`)
|
||||
.set("Accept", "application/json")
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (_, res) => {
|
||||
|
@ -83,7 +84,7 @@ describe("/models", () => {
|
|||
it("returns a success response when a model is deleted.", done => {
|
||||
request
|
||||
.delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`)
|
||||
.set("Accept", "application/json")
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (_, res) => {
|
||||
|
|
|
@ -3,7 +3,8 @@ const {
|
|||
createClientDatabase,
|
||||
createInstance,
|
||||
createModel,
|
||||
supertest
|
||||
supertest,
|
||||
defaultHeaders,
|
||||
} = require("./couchTestUtils");
|
||||
|
||||
describe("/records", () => {
|
||||
|
@ -38,9 +39,9 @@ describe("/records", () => {
|
|||
|
||||
const createRecord = async r =>
|
||||
await request
|
||||
.post(`/api/${instance._id}/records`)
|
||||
.post(`/api/${instance._id}/${model._id}/records`)
|
||||
.send(r || record)
|
||||
.set("Accept", "application/json")
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -56,14 +57,14 @@ describe("/records", () => {
|
|||
const existing = rec.body
|
||||
|
||||
const res = await request
|
||||
.post(`/api/${instance._id}/records`)
|
||||
.post(`/api/${instance._id}/${model._id}/records`)
|
||||
.send({
|
||||
_id: existing._id,
|
||||
_rev: existing._rev,
|
||||
modelId: model._id,
|
||||
name: "Updated Name",
|
||||
})
|
||||
.set("Accept", "application/json")
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -76,8 +77,8 @@ describe("/records", () => {
|
|||
const existing = rec.body
|
||||
|
||||
const res = await request
|
||||
.get(`/api/${instance._id}/records/${existing._id}`)
|
||||
.set("Accept", "application/json")
|
||||
.get(`/api/${instance._id}/${model._id}/records/${existing._id}`)
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -99,8 +100,8 @@ describe("/records", () => {
|
|||
await createRecord(newRecord)
|
||||
|
||||
const res = await request
|
||||
.get(`/api/${instance._id}/all_${newRecord.modelId}/records`)
|
||||
.set("Accept", "application/json")
|
||||
.get(`/api/${instance._id}/${model._id}/records`)
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -112,8 +113,8 @@ describe("/records", () => {
|
|||
it("load should return 404 when record does not exist", async () => {
|
||||
await createRecord()
|
||||
await request
|
||||
.get(`/api/${instance._id}/records/not-a-valid-id`)
|
||||
.set("Accept", "application/json")
|
||||
.get(`/api/${instance._id}/${model._id}/records/not-a-valid-id`)
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404)
|
||||
})
|
||||
|
|
|
@ -6,6 +6,7 @@ const {
|
|||
defaultHeaders,
|
||||
createUser,
|
||||
} = require("./couchTestUtils")
|
||||
const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels")
|
||||
|
||||
describe("/users", () => {
|
||||
let request
|
||||
|
@ -51,7 +52,7 @@ describe("/users", () => {
|
|||
const res = await request
|
||||
.post(`/api/${instance._id}/users`)
|
||||
.set(defaultHeaders)
|
||||
.send({ name: "Bill", username: "bill", password: "bills_password" })
|
||||
.send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID })
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
const {
|
||||
createClientDatabase,
|
||||
createApplication,
|
||||
createInstance,
|
||||
defaultHeaders,
|
||||
supertest,
|
||||
insertDocument,
|
||||
destroyDocument
|
||||
} = require("./couchTestUtils")
|
||||
|
||||
const TEST_WORKFLOW = {
|
||||
_id: "Test Workflow",
|
||||
name: "My Workflow",
|
||||
pageId: "123123123",
|
||||
screenId: "kasdkfldsafkl",
|
||||
live: true,
|
||||
uiTree: {
|
||||
|
||||
},
|
||||
definition: {
|
||||
triggers: [
|
||||
|
||||
],
|
||||
next: {
|
||||
actionId: "abc123",
|
||||
type: "SERVER",
|
||||
conditions: {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("/workflows", () => {
|
||||
let request
|
||||
let server
|
||||
let app
|
||||
let instance
|
||||
let workflow
|
||||
|
||||
beforeAll(async () => {
|
||||
({ request, server } = await supertest())
|
||||
await createClientDatabase(request)
|
||||
app = await createApplication(request)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
instance = await createInstance(request, app._id)
|
||||
if (workflow) await destroyDocument(workflow.id);
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
const createWorkflow = async () => {
|
||||
workflow = await insertDocument(instance._id, {
|
||||
type: "workflow",
|
||||
...TEST_WORKFLOW
|
||||
});
|
||||
}
|
||||
|
||||
describe("create", () => {
|
||||
it("returns a success message when the workflow is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/${instance._id}/workflows`)
|
||||
.set(defaultHeaders)
|
||||
.send(TEST_WORKFLOW)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Workflow created successfully");
|
||||
expect(res.body.workflow.name).toEqual("My Workflow");
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
it("return all the workflows for an instance", async () => {
|
||||
await createWorkflow();
|
||||
const res = await request
|
||||
.get(`/api/${instance._id}/workflows`)
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body[0]).toEqual(expect.objectContaining(TEST_WORKFLOW));
|
||||
})
|
||||
})
|
||||
|
||||
describe("find", () => {
|
||||
it("returns a workflow when queried by ID", async () => {
|
||||
await createWorkflow();
|
||||
const res = await request
|
||||
.get(`/api/${instance._id}/workflows/${workflow.id}`)
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual(expect.objectContaining(TEST_WORKFLOW));
|
||||
})
|
||||
})
|
||||
|
||||
describe("destroy", () => {
|
||||
it("deletes a workflow by its ID", async () => {
|
||||
await createWorkflow();
|
||||
const res = await request
|
||||
.delete(`/api/${instance._id}/workflows/${workflow.id}/${workflow.rev}`)
|
||||
.set(defaultHeaders)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.id).toEqual(TEST_WORKFLOW._id);
|
||||
})
|
||||
})
|
||||
});
|
|
@ -1,12 +1,26 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/user")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/users", controller.fetch)
|
||||
.get("/api/:instanceId/users/:username", controller.find)
|
||||
.post("/api/:instanceId/users", controller.create)
|
||||
.delete("/api/:instanceId/users/:username", controller.destroy)
|
||||
.get("/api/:instanceId/users", authorized(LIST_USERS), controller.fetch)
|
||||
.get(
|
||||
"/api/:instanceId/users/:username",
|
||||
authorized(USER_MANAGEMENT),
|
||||
controller.find
|
||||
)
|
||||
.post(
|
||||
"/api/:instanceId/users",
|
||||
authorized(USER_MANAGEMENT),
|
||||
controller.create
|
||||
)
|
||||
.delete(
|
||||
"/api/:instanceId/users/:username",
|
||||
authorized(USER_MANAGEMENT),
|
||||
controller.destroy
|
||||
)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/view")
|
||||
const viewController = require("../controllers/view")
|
||||
const recordController = require("../controllers/record")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER, READ_VIEW } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/views", controller.fetch)
|
||||
.get(
|
||||
"/api/:instanceId/view/:viewName",
|
||||
authorized(READ_VIEW, ctx => ctx.params.viewName),
|
||||
recordController.fetchView
|
||||
)
|
||||
.get("/api/:instanceId/views", authorized(BUILDER), viewController.fetch)
|
||||
// .patch("/api/:databaseId/views", controller.update);
|
||||
// .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy);
|
||||
.post("/api/:instanceId/views", controller.create)
|
||||
.post("/api/:instanceId/views", authorized(BUILDER), viewController.create)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/workflow")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/workflows", controller.fetch)
|
||||
.get("/api/:instanceId/workflows/:id", controller.find)
|
||||
.post("/api/:instanceId/workflows", controller.create)
|
||||
.put("/api/:instanceId/workflows/:id", controller.update)
|
||||
.delete("/api/:instanceId/workflows/:id/:rev", controller.destroy)
|
||||
|
||||
module.exports = router
|
|
@ -1,6 +1,11 @@
|
|||
const jwt = require("jsonwebtoken")
|
||||
const STATUS_CODES = require("../utilities/statusCodes")
|
||||
const env = require("../environment")
|
||||
const accessLevelController = require("../api/controllers/accesslevel")
|
||||
const {
|
||||
ADMIN_LEVEL_ID,
|
||||
POWERUSER_LEVEL_ID,
|
||||
} = require("../utilities/accessLevels")
|
||||
|
||||
module.exports = async (ctx, next) => {
|
||||
if (ctx.path === "/_builder") {
|
||||
|
@ -8,8 +13,9 @@ module.exports = async (ctx, next) => {
|
|||
return
|
||||
}
|
||||
|
||||
if (ctx.isDev && ctx.cookies.get("builder:token") === env.ADMIN_SECRET) {
|
||||
if (ctx.cookies.get("builder:token") === env.ADMIN_SECRET) {
|
||||
ctx.isAuthenticated = true
|
||||
ctx.isBuilder = true
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
@ -23,7 +29,12 @@ module.exports = async (ctx, next) => {
|
|||
}
|
||||
|
||||
try {
|
||||
ctx.jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
|
||||
const jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
|
||||
|
||||
ctx.user = {
|
||||
...jwtPayload,
|
||||
accessLevel: await getAccessLevel(jwtPayload.accessLevelId),
|
||||
}
|
||||
ctx.isAuthenticated = true
|
||||
} catch (err) {
|
||||
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
|
||||
|
@ -31,3 +42,22 @@ module.exports = async (ctx, next) => {
|
|||
|
||||
await next()
|
||||
}
|
||||
|
||||
const getAccessLevel = async accessLevelId => {
|
||||
if (
|
||||
accessLevelId === POWERUSER_LEVEL_ID ||
|
||||
accessLevelId === ADMIN_LEVEL_ID
|
||||
) {
|
||||
return {
|
||||
_id: accessLevelId,
|
||||
name: accessLevelId,
|
||||
permissions: [],
|
||||
}
|
||||
}
|
||||
|
||||
const findAccessContext = {
|
||||
params: { levelId: accessLevelId },
|
||||
}
|
||||
await accessLevelController.find(findAccessContext)
|
||||
return findAccessContext.body
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
const {
|
||||
adminPermissions,
|
||||
ADMIN_LEVEL_ID,
|
||||
POWERUSER_LEVEL_ID,
|
||||
BUILDER,
|
||||
} = require("../utilities/accessLevels")
|
||||
|
||||
module.exports = (permName, getItemId) => async (ctx, next) => {
|
||||
if (!ctx.isAuthenticated) {
|
||||
ctx.throw(403, "Session not authenticated")
|
||||
}
|
||||
|
||||
if (ctx.isBuilder) {
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
||||
if (permName === BUILDER) {
|
||||
ctx.throw(403, "Not Authorized")
|
||||
return
|
||||
}
|
||||
|
||||
if (!ctx.user) {
|
||||
ctx.throw(403, "User not found")
|
||||
}
|
||||
|
||||
const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "")
|
||||
|
||||
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
||||
const thisPermissionId = {
|
||||
name: permName,
|
||||
itemId: getItemId && getItemId(ctx),
|
||||
}
|
||||
|
||||
// power user has everything, except the admin specific perms
|
||||
if (
|
||||
ctx.user.accessLevel._id === POWERUSER_LEVEL_ID &&
|
||||
!adminPermissions.map(permissionId).includes(thisPermissionId)
|
||||
) {
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
ctx.user.accessLevel.permissions
|
||||
.map(permissionId)
|
||||
.includes(thisPermissionId)
|
||||
) {
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
||||
ctx.throw(403, "Not Authorized")
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
const WORKFLOW_SCHEMA = {
|
||||
properties: {
|
||||
type: "workflow",
|
||||
pageId: {
|
||||
type: "string",
|
||||
},
|
||||
screenId: {
|
||||
type: "string",
|
||||
},
|
||||
live: {
|
||||
type: "boolean",
|
||||
},
|
||||
uiTree: {
|
||||
type: "object",
|
||||
},
|
||||
definition: {
|
||||
type: "object",
|
||||
properties: {
|
||||
triggers: { type: "array" },
|
||||
next: {
|
||||
type: "object",
|
||||
properties: {
|
||||
type: { type: "string" },
|
||||
actionId: { type: "string" },
|
||||
args: { type: "object" },
|
||||
conditions: { type: "array" },
|
||||
errorHandling: { type: "object" },
|
||||
next: { type: "object" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
WORKFLOW_SCHEMA,
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
const viewController = require("../api/controllers/view")
|
||||
const modelController = require("../api/controllers/model")
|
||||
|
||||
exports.ADMIN_LEVEL_ID = "ADMIN"
|
||||
exports.POWERUSER_LEVEL_ID = "POWER_USER"
|
||||
|
||||
exports.READ_MODEL = "read-model"
|
||||
exports.WRITE_MODEL = "write-model"
|
||||
exports.READ_VIEW = "read-view"
|
||||
exports.EXECUTE_WORKFLOW = "execute-workflow"
|
||||
exports.USER_MANAGEMENT = "user-management"
|
||||
exports.BUILDER = "builder"
|
||||
exports.LIST_USERS = "list-users"
|
||||
|
||||
exports.adminPermissions = [
|
||||
{
|
||||
name: exports.USER_MANAGEMENT,
|
||||
},
|
||||
]
|
||||
|
||||
exports.generateAdminPermissions = async instanceId => [
|
||||
...exports.adminPermissions,
|
||||
...(await exports.generatePowerUserPermissions(instanceId)),
|
||||
]
|
||||
|
||||
exports.generatePowerUserPermissions = async instanceId => {
|
||||
const fetchModelsCtx = {
|
||||
params: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await modelController.fetch(fetchModelsCtx)
|
||||
const models = fetchModelsCtx.body
|
||||
|
||||
const fetchViewsCtx = {
|
||||
params: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await viewController.fetch(fetchViewsCtx)
|
||||
const views = fetchViewsCtx.body
|
||||
|
||||
const readModelPermissions = models.map(m => ({
|
||||
itemId: m._id,
|
||||
name: exports.READ_MODEL,
|
||||
}))
|
||||
|
||||
const writeModelPermissions = models.map(m => ({
|
||||
itemId: m._id,
|
||||
name: exports.WRITE_MODEL,
|
||||
}))
|
||||
|
||||
const viewPermissions = views.map(v => ({
|
||||
itemId: v.name,
|
||||
name: exports.READ_VIEW,
|
||||
}))
|
||||
|
||||
return [
|
||||
...readModelPermissions,
|
||||
...writeModelPermissions,
|
||||
...viewPermissions,
|
||||
{ name: exports.LIST_USERS },
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
dist/
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "name",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@budibase/standard-components": "0.x",
|
||||
"@budibase/materialdesign-components": "0.x"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"title": "Test App",
|
||||
"favicon": "./_shared/favicon.png",
|
||||
"stylesheets": [],
|
||||
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
|
||||
"props" : {
|
||||
"_component": "@budibase/standard-components/container",
|
||||
"_children": [],
|
||||
"_id": 0,
|
||||
"type": "div",
|
||||
"_styles": {
|
||||
"layout": {},
|
||||
"position": {}
|
||||
},
|
||||
"_code": ""
|
||||
},
|
||||
"_css": "",
|
||||
"uiFunctions": ""
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"title": "Test App",
|
||||
"favicon": "./_shared/favicon.png",
|
||||
"stylesheets": [],
|
||||
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
|
||||
"props" : {
|
||||
"_component": "@budibase/standard-components/container",
|
||||
"_children": [],
|
||||
"_id": 1,
|
||||
"type": "div",
|
||||
"_styles": {
|
||||
"layout": {},
|
||||
"position": {}
|
||||
},
|
||||
"_code": ""
|
||||
},
|
||||
"_css": "",
|
||||
"uiFunctions": ""
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
module.exports = () => ({})
|
Loading…
Reference in New Issue