Merge pull request #903 from Budibase/feature/security-update

Security Update & Role-Based Screens
This commit is contained in:
Andrew Kingston 2020-12-15 09:52:18 +00:00 committed by GitHub
commit b582b7ac22
73 changed files with 1443 additions and 850 deletions

View File

@ -115,22 +115,22 @@ Cypress.Commands.add("createUser", (email, password, role) => {
// Create User
cy.contains("Users").click()
cy.contains("Create New Row").click()
cy.contains("Create New User").click()
cy.get(".modal").within(() => {
cy.get("input")
.first()
.type(password)
.type(email)
cy.get("input")
.eq(1)
.type(email)
.type(password)
cy.get("select")
.first()
.select(role)
// Save
cy.get(".buttons")
.contains("Create Row")
.contains("Create User")
.click()
})
})

View File

@ -2,9 +2,9 @@ import { getFrontendStore } from "./store/frontend"
import { getBackendUiStore } from "./store/backend"
import { getAutomationStore } from "./store/automation/"
import { getThemeStore } from "./store/theme"
import { derived } from "svelte/store"
import { derived, writable } from "svelte/store"
import analytics from "analytics"
import { LAYOUT_NAMES } from "../constants"
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { makePropsSafe } from "components/userInterface/assetParsing/createProps"
export const store = getFrontendStore()
@ -13,18 +13,12 @@ export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
export const currentAsset = derived(store, $store => {
const layout = $store.layouts
? $store.layouts.find(layout => layout._id === $store.currentAssetId)
: null
if (layout) return layout
const screen = $store.screens
? $store.screens.find(screen => screen._id === $store.currentAssetId)
: null
if (screen) return screen
const type = $store.currentFrontEndType
if (type === FrontendTypes.SCREEN) {
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
} else if (type === FrontendTypes.LAYOUT) {
return $store.layouts.find(layout => layout._id === $store.selectedLayoutId)
}
return null
})
@ -59,8 +53,14 @@ export const selectedComponent = derived(
}
)
export const currentAssetName = derived(store, () => {
return currentAsset.name
export const currentAssetId = derived(store, $store => {
return $store.currentFrontEndType === FrontendTypes.SCREEN
? $store.selectedScreenId
: $store.selectedLayoutId
})
export const currentAssetName = derived(currentAsset, $currentAsset => {
return $currentAsset?.name
})
// leave this as before for consistency
@ -74,6 +74,8 @@ export const mainLayout = derived(store, $store => {
)
})
export const selectedAccessRole = writable("BASIC")
export const initialise = async () => {
try {
await analytics.activate()

View File

@ -6,6 +6,7 @@ const INITIAL_BACKEND_UI_STATE = {
tables: [],
views: [],
users: [],
roles: [],
selectedDatabase: {},
selectedTable: {},
draftTable: {},
@ -177,6 +178,26 @@ export const getBackendUiStore = () => {
return state
}),
},
roles: {
fetch: async () => {
const response = await api.get("/api/roles")
const roles = await response.json()
store.update(state => {
state.roles = roles
return state
})
},
delete: async role => {
const response = await api.delete(`/api/roles/${role._id}/${role._rev}`)
await store.actions.roles.fetch()
return response
},
save: async role => {
const response = await api.post("/api/roles", role)
await store.actions.roles.fetch()
return response
},
},
}
return store

View File

@ -3,7 +3,6 @@ import { cloneDeep } from "lodash/fp"
import {
createProps,
getBuiltin,
makePropsSafe,
} from "components/userInterface/assetParsing/createProps"
import {
allScreens,
@ -11,6 +10,7 @@ import {
currentAsset,
mainLayout,
selectedComponent,
selectedAccessRole,
} from "builderStore"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
@ -32,7 +32,8 @@ const INITIAL_FRONTEND_STATE = {
screens: [],
components: [],
currentFrontEndType: "none",
currentAssetId: "",
selectedScreenId: "",
selectedLayoutId: "",
selectedComponentId: "",
errors: [],
hasAppPackage: false,
@ -83,28 +84,31 @@ export const getFrontendStore = () => {
},
},
screens: {
select: async screenId => {
let promise
select: screenId => {
store.update(state => {
const screen = get(allScreens).find(screen => screen._id === screenId)
let screens = get(allScreens)
let screen =
screens.find(screen => screen._id === screenId) || screens[0]
if (!screen) return state
state.currentFrontEndType = FrontendTypes.SCREEN
state.currentAssetId = screenId
state.currentView = "detail"
// Update role to the screen's role setting so that it will always
// be visible
selectedAccessRole.set(screen.routing.roleId)
promise = store.actions.screens.regenerateCss(screen)
state.currentFrontEndType = FrontendTypes.SCREEN
state.selectedScreenId = screen._id
state.currentView = "detail"
state.selectedComponentId = screen.props?._id
return state
})
await promise
},
create: async screen => {
screen = await store.actions.screens.save(screen)
store.update(state => {
state.currentAssetId = screen._id
state.selectedScreenId = screen._id
state.selectedComponentId = screen.props._id
state.currentFrontEndType = FrontendTypes.SCREEN
selectedAccessRole.set(screen.routing.roleId)
return state
})
return screen
@ -113,6 +117,7 @@ export const getFrontendStore = () => {
const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen)
screen = await response.json()
await store.actions.routing.fetch()
store.update(state => {
const foundScreen = state.screens.findIndex(
@ -122,28 +127,14 @@ export const getFrontendStore = () => {
state.screens.splice(foundScreen, 1)
}
state.screens.push(screen)
if (creatingNewScreen) {
const safeProps = makePropsSafe(
state.components[screen.props._component],
screen.props
)
state.selectedComponentId = safeProps._id
screen.props = safeProps
}
return state
})
return screen
},
regenerateCss: async asset => {
const response = await api.post("/api/css/generate", asset)
asset._css = (await response.json())?.css
},
regenerateCssForCurrentScreen: async () => {
const asset = get(currentAsset)
if (asset) {
await store.actions.screens.regenerateCss(asset)
if (creatingNewScreen) {
store.actions.screens.select(screen._id)
}
return screen
},
delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
@ -159,8 +150,8 @@ export const getFrontendStore = () => {
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
)
if (screenToDelete._id === state.currentAssetId) {
state.currentAssetId = ""
if (screenToDelete._id === state.selectedScreenId) {
state.selectedScreenId = null
}
}
return state
@ -181,50 +172,44 @@ export const getFrontendStore = () => {
},
},
layouts: {
select: async layoutId => {
select: layoutId => {
store.update(state => {
const layout = store.actions.layouts.find(layoutId)
const layout =
store.actions.layouts.find(layoutId) || get(store).layouts[0]
if (!layout) return
state.currentFrontEndType = FrontendTypes.LAYOUT
state.currentView = "detail"
state.currentAssetId = layout._id
state.selectedLayoutId = layout._id
state.selectedComponentId = layout.props?._id
return state
})
let cssPromises = []
cssPromises.push(store.actions.screens.regenerateCssForCurrentScreen())
for (let screen of get(allScreens)) {
cssPromises.push(store.actions.screens.regenerateCss(screen))
}
await Promise.all(cssPromises)
},
save: async layout => {
const layoutToSave = cloneDeep(layout)
delete layoutToSave._css
const creatingNewLayout = layoutToSave._id === undefined
const response = await api.post(`/api/layouts`, layoutToSave)
const json = await response.json()
const savedLayout = await response.json()
store.update(state => {
const layoutIdx = state.layouts.findIndex(
stateLayout => stateLayout._id === json._id
stateLayout => stateLayout._id === savedLayout._id
)
if (layoutIdx >= 0) {
// update existing layout
state.layouts.splice(layoutIdx, 1, json)
state.layouts.splice(layoutIdx, 1, savedLayout)
} else {
// save new layout
state.layouts.push(json)
state.layouts.push(savedLayout)
}
state.currentAssetId = json._id
return state
})
// Select layout if creating a new one
if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id)
}
return savedLayout
},
find: layoutId => {
if (!layoutId) {
@ -237,16 +222,17 @@ export const getFrontendStore = () => {
const response = await api.delete(
`/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}`
)
if (response.status !== 200) {
const json = await response.json()
throw new Error(json.message)
}
store.update(state => {
state.layouts = state.layouts.filter(
layout => layout._id !== layoutToDelete._id
)
if (layoutToDelete._id === state.selectedLayoutId) {
state.selectedLayoutId = get(mainLayout)._id
}
return state
})
},
@ -372,7 +358,6 @@ export const getFrontendStore = () => {
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
promises.push(store.actions.screens.regenerateCssForCurrentScreen())
promises.push(store.actions.preview.saveSelected())
store.actions.components.select(componentToPaste)
@ -390,8 +375,6 @@ export const getFrontendStore = () => {
}
selected._styles[type][name] = value
promises.push(store.actions.screens.regenerateCssForCurrentScreen())
// save without messing with the store
promises.push(store.actions.preview.saveSelected())
return state
@ -476,13 +459,8 @@ export const getFrontendStore = () => {
}).props
}
// Save layout and regenerate all CSS because otherwise weird things happen
// Save layout
nav._children = [...nav._children, newLink]
state.currentAssetId = layout._id
promises.push(store.actions.screens.regenerateCss(layout))
for (let screen of get(allScreens)) {
promises.push(store.actions.screens.regenerateCss(screen))
}
promises.push(store.actions.layouts.save(layout))
}
return state

View File

@ -4,12 +4,17 @@
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
import CreateViewButton from "./buttons/CreateViewButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
import EditRolesButton from "./buttons/EditRolesButton.svelte"
import * as api from "./api"
import Table from "./Table.svelte"
import { TableNames } from "constants"
import CreateEditUser from "./modals/CreateEditUser.svelte"
import CreateEditRow from "./modals/CreateEditRow.svelte"
let data = []
let loading = false
$: isUsersTable = $backendUiStore.selectedTable?._id === TableNames.USERS
$: title = $backendUiStore.selectedTable.name
$: schema = $backendUiStore.selectedTable.schema
$: tableView = {
@ -29,11 +34,22 @@
}
</script>
<Table {title} {schema} {data} allowEditing={true} {loading}>
<Table
{title}
{schema}
tableId={$backendUiStore.selectedTable?._id}
{data}
allowEditing={true}
{loading}>
<CreateColumnButton />
{#if schema && Object.keys(schema).length > 0}
<CreateRowButton />
<CreateRowButton
title={isUsersTable ? 'Create New User' : 'Create New Row'}
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} />
<CreateViewButton />
<ExportButton view={tableView} />
{/if}
{#if isUsersTable}
<EditRolesButton />
{/if}
</Table>

View File

@ -7,20 +7,16 @@
Toggle,
RichText,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let meta
export let creating
export let value = meta.type === "boolean" ? false : ""
export let readonly
$: type = meta.type
$: label = capitalise(meta.name)
$: editingUser =
!creating && $backendUiStore.selectedTable?._id === TableNames.USERS
</script>
{#if type === 'options'}
@ -53,5 +49,5 @@
data-cy="{meta.name}-input"
{type}
bind:value
disabled={editingUser} />
disabled={readonly} />
{/if}

View File

@ -7,10 +7,15 @@
import { notifier } from "builderStore/store/notifications"
import Spinner from "components/common/Spinner.svelte"
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
import { getRenderer, editRowRenderer } from "./cells/cellRenderers"
import {
getRenderer,
editRowRenderer,
userRowRenderer,
} from "./cells/cellRenderers"
import TableLoadingOverlay from "./TableLoadingOverlay"
import TableHeader from "./TableHeader"
import "@budibase/svelte-ag-grid/dist/index.css"
import { TableNames } from "constants"
export let schema = {}
export let data = []
@ -42,7 +47,18 @@
animateRows: true,
}
$: isUsersTable = tableId === TableNames.USERS
$: {
if (isUsersTable) {
schema.email.displayFieldName = "Email"
schema.roleId.displayFieldName = "Role"
}
}
$: {
// Reset selection every time data changes
selectedRows = []
let result = []
if (allowEditing) {
result = [
@ -57,23 +73,34 @@
suppressMenu: true,
minWidth: 114,
width: 114,
cellRenderer: editRowRenderer,
cellRenderer: isUsersTable ? userRowRenderer : editRowRenderer,
},
]
}
Object.keys(schema || {}).forEach((key, idx) => {
const canEditColumn = key => {
if (!allowEditing) {
return false
}
return !(isUsersTable && ["email", "roleId"].includes(key))
}
Object.entries(schema || {}).forEach(([key, value]) => {
result.push({
headerCheckboxSelection: false,
headerComponent: TableHeader,
headerComponentParams: {
field: schema[key],
editable: allowEditing,
editable: canEditColumn(key),
},
headerName: key,
headerName: value.displayFieldName || key,
field: key,
sortable: true,
cellRenderer: getRenderer(schema[key], true),
cellRenderer: getRenderer({
schema: schema[key],
editable: true,
isUsersTable,
}),
cellRendererParams: {
selectRelationship,
},

View File

@ -1,6 +1,9 @@
<script>
import { TextButton as Button, Icon, Modal } from "@budibase/bbui"
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
import CreateEditRow from "../modals/CreateEditRow.svelte"
export let modalContentComponent = CreateEditRow
export let title = "Create New Row"
let modal
</script>
@ -8,9 +11,9 @@
<div>
<Button text small on:click={modal.show}>
<Icon name="addrow" />
Create New Row
{title}
</Button>
</div>
<Modal bind:this={modal}>
<CreateEditRowModal />
<svelte:component this={modalContentComponent} />
</Modal>

View File

@ -0,0 +1,23 @@
<script>
import { TextButton as Button, Modal } from "@budibase/bbui"
import EditRolesModal from "../modals/EditRoles.svelte"
let modal
</script>
<div>
<Button text small on:click={modal.show}>
<i class="ri-lock-line" />
Edit Roles
</Button>
</div>
<Modal bind:this={modal}>
<EditRolesModal />
</Modal>
<style>
i {
margin-right: var(--spacing-xs);
font-size: var(--font-size-s);
}
</style>

View File

@ -0,0 +1,10 @@
<script>
import { backendUiStore } from "builderStore"
export let roleId
$: role = $backendUiStore.roles.find(role => role._id === roleId)
$: roleName = role?.name ?? "Unknown role"
</script>
<div>{roleName}</div>

View File

@ -1,16 +1,25 @@
import AttachmentList from "./AttachmentCell.svelte"
import EditRow from "../modals/EditRow.svelte"
import CreateEditUser from "../modals/CreateEditUser.svelte"
import DeleteRow from "../modals/DeleteRow.svelte"
import RelationshipDisplay from "./RelationshipCell.svelte"
import RoleCell from "./RoleCell.svelte"
const renderers = {
attachment: attachmentRenderer,
link: linkedRowRenderer,
}
export function getRenderer(schema, editable) {
export function getRenderer({ schema, editable, isUsersTable }) {
const rendererParams = {
options: schema.options,
constraints: schema.constraints,
editable,
}
if (renderers[schema.type]) {
return renderers[schema.type](schema.options, schema.constraints, editable)
return renderers[schema.type](rendererParams)
} else if (isUsersTable && schema.name === "roleId") {
return roleRenderer(rendererParams)
} else {
return false
}
@ -45,15 +54,31 @@ export function editRowRenderer(params) {
return container
}
/* eslint-disable no-unused-vars */
function attachmentRenderer(options, constraints, editable) {
export function userRowRenderer(params) {
const container = document.createElement("div")
container.style.height = "100%"
container.style.display = "flex"
container.style.alignItems = "center"
new EditRow({
target: container,
props: {
row: params.data,
modalContentComponent: CreateEditUser,
},
})
return container
}
function attachmentRenderer() {
return params => {
const container = document.createElement("div")
container.style.height = "100%"
container.style.display = "flex"
container.style.alignItems = "center"
const attachmentInstance = new AttachmentList({
new AttachmentList({
target: container,
props: {
files: params.value || [],
@ -64,7 +89,6 @@ function attachmentRenderer(options, constraints, editable) {
}
}
/* eslint-disable no-unused-vars */
function linkedRowRenderer() {
return params => {
let container = document.createElement("div")
@ -84,3 +108,21 @@ function linkedRowRenderer() {
return container
}
}
function roleRenderer() {
return params => {
let container = document.createElement("div")
container.style.display = "grid"
container.style.height = "100%"
container.style.alignItems = "center"
new RoleCell({
target: container,
props: {
roleId: params.value,
},
})
return container
}
}

View File

@ -1,6 +1,5 @@
<script>
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import { notifier } from "builderStore/store/notifications"
import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api"
@ -40,15 +39,9 @@
confirmText={creating ? 'Create Row' : 'Save Row'}
onConfirm={saveRow}>
<ErrorsBox {errors} />
{#if creating && table._id === TableNames.USERS}
<RowFieldControl
{creating}
meta={{ name: 'password', type: 'password' }}
bind:value={row.password} />
{/if}
{#each tableSchema as [key, meta]}
<div>
<RowFieldControl {meta} bind:value={row[key]} {creating} />
<RowFieldControl {meta} bind:value={row[key]} />
</div>
{/each}
</ModalContent>

View File

@ -0,0 +1,85 @@
<script>
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import RowFieldControl from "../RowFieldControl.svelte"
import * as backendApi from "../api"
import { ModalContent, Select } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let row = {}
let errors = []
$: creating = row?._id == null
$: table = row.tableId
? $backendUiStore.tables.find(table => table._id === row?.tableId)
: $backendUiStore.selectedTable
$: tableSchema = getUserSchema(table)
$: customSchemaKeys = getCustomSchemaKeys(tableSchema)
const getUserSchema = table => {
let schema = table?.schema ?? {}
if (schema.username) {
schema.username.name = "Username"
}
return schema
}
const getCustomSchemaKeys = schema => {
let customSchema = { ...schema }
delete customSchema["email"]
delete customSchema["roleId"]
return Object.entries(customSchema)
}
const saveRow = async () => {
const rowResponse = await backendApi.saveRow(
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
if (Array.isArray(rowResponse.errors)) {
errors = rowResponse.errors.map(error => ({ message: error }))
} else {
errors = Object.entries(rowResponse.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
}
return false
}
notifier.success("User saved successfully.")
backendUiStore.actions.rows.save(rowResponse)
}
</script>
<ModalContent
title={creating ? 'Create User' : 'Edit User'}
confirmText={creating ? 'Create User' : 'Save User'}
onConfirm={saveRow}>
<ErrorsBox {errors} />
<RowFieldControl
meta={{ ...tableSchema.email, name: 'Email' }}
bind:value={row.email}
readonly={!creating} />
<RowFieldControl
meta={{ name: 'password', type: 'password' }}
bind:value={row.password} />
<!-- Defer rendering this select until roles load, otherwise the initial
selection is always undefined -->
<Select
thin
secondary
label="Role"
data-cy="roleId-select"
bind:value={row.roleId}>
<option value="">Choose an option</option>
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
{#each customSchemaKeys as [key, meta]}
<RowFieldControl {meta} bind:value={row[key]} {creating} />
{/each}
</ModalContent>

View File

@ -0,0 +1,132 @@
<script>
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
import { onMount } from "svelte"
import api from "builderStore/api"
import { notifier } from "builderStore/store/notifications"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { backendUiStore } from "builderStore"
let permissions = []
let selectedRole = {}
let errors = []
$: selectedRoleId = selectedRole._id
$: otherRoles = $backendUiStore.roles.filter(
role => role._id !== selectedRoleId
)
$: isCreating = selectedRoleId == null || selectedRoleId === ""
const fetchPermissions = async () => {
const permissionsResponse = await api.get("/api/permissions")
permissions = await permissionsResponse.json()
}
// Changes the selected role
const changeRole = event => {
const id = event?.target?.value
const role = $backendUiStore.roles.find(role => role._id === id)
if (role) {
selectedRole = {
...role,
inherits: role.inherits ?? "",
permissionId: role.permissionId ?? "",
}
} else {
selectedRole = { _id: "", inherits: "", permissionId: "" }
}
errors = []
}
// Saves or creates the selected role
const saveRole = async () => {
errors = []
// Clean up empty strings
const keys = ["_id", "inherits", "permissionId"]
keys.forEach(key => {
if (selectedRole[key] === "") {
delete selectedRole[key]
}
})
// Validation
if (!selectedRole.name || selectedRole.name.trim() === "") {
errors.push({ message: "Please enter a role name" })
}
if (!selectedRole.permissionId) {
errors.push({ message: "Please choose permissions" })
}
if (errors.length) {
return false
}
// Save/create the role
const response = await backendUiStore.actions.roles.save(selectedRole)
if (response.status === 200) {
notifier.success("Role saved successfully.")
} else {
notifier.danger("Error saving role.")
return false
}
}
// Deletes the selected role
const deleteRole = async () => {
const response = await backendUiStore.actions.roles.delete(selectedRole)
if (response.status === 200) {
changeRole()
notifier.success("Role deleted successfully.")
} else {
notifier.danger("Error deleting role.")
}
}
onMount(fetchPermissions)
</script>
<ModalContent
title="Edit Roles"
confirmText={isCreating ? 'Create' : 'Save'}
onConfirm={saveRole}>
{#if errors.length}
<ErrorsBox {errors} />
{/if}
<Select
thin
secondary
label="Role"
value={selectedRoleId}
on:change={changeRole}>
<option value="">Create new role</option>
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
{#if selectedRole}
<Input label="Name" bind:value={selectedRole.name} thin />
<Select
thin
secondary
label="Inherits Role"
bind:value={selectedRole.inherits}>
<option value="">None</option>
{#each otherRoles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
<Select
thin
secondary
label="Permissions"
bind:value={selectedRole.permissionId}>
<option value="">Choose permissions</option>
{#each permissions as permission}
<option value={permission._id}>{permission.name}</option>
{/each}
</Select>
{/if}
<div slot="footer">
{#if !isCreating}
<Button red on:click={deleteRole}>Delete</Button>
{/if}
</div>
</ModalContent>

View File

@ -1,8 +1,9 @@
<script>
import { Modal, Button } from "@budibase/bbui"
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
import CreateEditRow from "../modals/CreateEditRow.svelte"
export let row
export let modalContentComponent = CreateEditRow
let modal
@ -14,5 +15,5 @@
<Button data-cy="edit-row" secondary small on:click={showModal}>Edit</Button>
<Modal bind:this={modal}>
<CreateEditRowModal {row} />
<svelte:component this={modalContentComponent} {row} />
</Modal>

View File

@ -2,7 +2,6 @@
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import ListItem from "./ListItem.svelte"
import CreateTableModal from "./modals/CreateTableModal.svelte"
import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditViewPopover from "./popovers/EditViewPopover.svelte"
@ -47,7 +46,9 @@
text={table.name}
selected={selectedView === `all_${table._id}`}
on:click={() => selectTable(table)}>
<EditTablePopover {table} />
{#if table._id !== TableNames.USERS}
<EditTablePopover {table} />
{/if}
</NavItem>
{#each Object.keys(table.views || {}) as viewName}
<NavItem

View File

@ -5,9 +5,9 @@
</script>
{#if hasErrors}
<div class="container bb__alert bb__alert--danger">
<div class="container">
{#each errors as error}
<div class="error">{error.dataPath} {error.message}</div>
<div class="error">{error.dataPath || ''} {error.message}</div>
{/each}
</div>
{/if}
@ -17,6 +17,8 @@
border-radius: var(--border-radius-m);
margin: 0;
padding: var(--spacing-m);
background-color: rgba(241, 165, 165, 0.2);
color: var(--red);
}
.error {

View File

@ -61,7 +61,7 @@
}
.nav-item:hover,
.nav-item.selected {
border-radius: var(--border-radius-m);
border-radius: var(--border-radius-s);
}
.content {

View File

@ -84,6 +84,7 @@
overflow: hidden;
margin: auto;
height: 100%;
background-color: white;
}
.component-container iframe {
border: 0;

View File

@ -1,6 +1,6 @@
<script>
import { goto } from "@sveltech/routify"
import { store, currentAsset } from "builderStore"
import { store, currentAssetId } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils"
import { DropEffect, DropPosition } from "./dragDropStore"
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
@ -22,7 +22,7 @@
const path = store.actions.components.findRoute(component)
// Go to correct URL
$goto(`./${$store.currentAssetId}/${path}`)
$goto(`./${$currentAssetId}/${path}`)
}
const dragstart = component => e => {
@ -71,7 +71,7 @@
on:drop={dragDropStore.actions.drop}
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
withArrow
indentLevel={level + 3}
indentLevel={level + 1}
selected={$store.selectedComponentId === component._id}>
<ComponentDropdownMenu {component} />
</NavItem>

View File

@ -1,5 +1,4 @@
<script>
import { writable } from "svelte/store"
import { goto } from "@sveltech/routify"
import { store, selectedComponent, currentAsset } from "builderStore"
import instantiateStore from "./dragDropStore"
@ -20,6 +19,7 @@
export let route
export let path
export let indent
export let border
$: selectedScreen = $currentAsset
@ -34,6 +34,7 @@
icon="ri-folder-line"
text={path}
opened={true}
{border}
withArrow={route.subpaths} />
{#each Object.entries(route.subpaths) as [url, subpath]}
@ -41,8 +42,8 @@
<NavItem
icon="ri-artboard-2-line"
indentLevel={indent || 1}
selected={$store.currentAssetId === screenId}
opened={$store.currentAssetId === screenId}
selected={$store.selectedScreenId === screenId}
opened={$store.selectedScreenId === screenId}
text={ROUTE_NAME_MAP[url]?.[role] || url}
withArrow={route.subpaths}
on:click={() => changeScreen(screenId)}>
@ -50,6 +51,7 @@
</NavItem>
{#if selectedScreen?._id === screenId}
<ComponentTree
level={1}
components={selectedScreen.props._children}
currentComponent={$selectedComponent}
{dragDropStore} />

View File

@ -1,11 +1,76 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { store, selectedAccessRole } from "builderStore"
import PathTree from "./PathTree.svelte"
let routes = {}
$: paths = Object.keys(routes || {}).sort()
$: {
const allRoutes = $store.routes
const sortedPaths = Object.keys(allRoutes || {}).sort()
const selectedRoleId = $selectedAccessRole
const selectedScreenId = $store.selectedScreenId
let found = false
let firstValidScreenId
let filteredRoutes = {}
let screenRoleId
// Filter all routes down to only those which match the current role
sortedPaths.forEach(path => {
const config = allRoutes[path]
Object.entries(config.subpaths).forEach(([subpath, pathConfig]) => {
Object.entries(pathConfig.screens).forEach(([roleId, screenId]) => {
if (screenId === selectedScreenId) {
screenRoleId = roleId
found = roleId === selectedRoleId
}
if (roleId === selectedRoleId) {
if (!firstValidScreenId) {
firstValidScreenId = screenId
}
if (!filteredRoutes[path]) {
filteredRoutes[path] = { subpaths: {} }
}
filteredRoutes[path].subpaths[subpath] = {
screens: {
[selectedRoleId]: screenId,
},
}
}
})
})
})
routes = filteredRoutes
// Select the correct role for the current screen ID
if (!found && screenRoleId) {
selectedAccessRole.set(screenRoleId)
}
// If the selected screen isn't in this filtered list, select the first one
else if (!found && firstValidScreenId) {
store.actions.screens.select(firstValidScreenId)
}
}
</script>
<div class="root">
{#each Object.keys($store.routes || {}) as path}
<PathTree {path} route={$store.routes[path]} />
{#each paths as path, idx}
<PathTree border={idx > 0} {path} route={routes[path]} />
{/each}
{#if !paths.length}
<div class="empty">
There aren't any screens configured with this access role.
</div>
{/if}
</div>
<style>
div.empty {
font-size: var(--font-size-xs);
color: var(--grey-5);
padding-top: var(--spacing-xs);
}
</style>

View File

@ -6,6 +6,7 @@
import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte"
import SettingsView from "./SettingsView.svelte"
import { setWith } from "lodash"
let flattenedPanel = flattenComponents(panelStructure.categories)
let categories = [
@ -69,7 +70,7 @@
) {
selectedAsset.props._instanceName = value
} else {
selectedAsset[name] = value
setWith(selectedAsset, name.split("."), value, Object)
}
return state
})

View File

@ -1,6 +1,11 @@
<script>
import { goto, url } from "@sveltech/routify"
import { store, currentAssetName, selectedComponent } from "builderStore"
import { goto } from "@sveltech/routify"
import {
store,
currentAssetName,
selectedComponent,
currentAssetId,
} from "builderStore"
import components from "./temporaryPanelStructure.js"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
@ -27,7 +32,7 @@
const onComponentChosen = component => {
store.actions.components.create(component._component, component.presetProps)
const path = store.actions.components.findRoute($selectedComponent)
$goto(`./${$store.currentAssetId}/${path}`)
$goto(`./${$currentAssetId}/${path}`)
close()
}
</script>

View File

@ -1,13 +1,19 @@
<script>
import { onMount } from "svelte"
import { goto, params, url } from "@sveltech/routify"
import { store, currentAsset, selectedComponent } from "builderStore"
import {
store,
allScreens,
currentAsset,
backendUiStore,
selectedAccessRole,
} from "builderStore"
import { FrontendTypes } from "constants"
import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte"
import Layout from "components/userInterface/Layout.svelte"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import NewLayoutModal from "components/userInterface/NewLayoutModal.svelte"
import { Modal, Switcher } from "@budibase/bbui"
import { Modal, Switcher, Select } from "@budibase/bbui"
const tabs = [
{
@ -24,11 +30,38 @@
let routes = {}
let tab = $params.assetType
function navigate({ detail }) {
if (!detail) return
const navigate = ({ detail }) => {
if (!detail) {
return
}
$goto(`../${detail.heading.key}`)
}
const updateAccessRole = event => {
const role = event.target.value
// Select a valid screen with this new role - otherwise we'll not be
// able to change role at all because ComponentNavigationTree will kick us
// back the current role again because the same screen ID is still selected
const firstValidScreenId = $allScreens.find(
screen => screen.routing.roleId === role
)?._id
if (firstValidScreenId) {
store.actions.screens.select(firstValidScreenId)
}
// Otherwise clear the selected screen ID so that the first new valid screen
// can be selected by ComponentNavigationTree
else {
store.update(state => {
state.selectedScreenId = null
return state
})
}
selectedAccessRole.set(role)
}
onMount(() => {
store.actions.routing.fetch()
})
@ -41,11 +74,21 @@
on:click={modal.show}
data-cy="new-screen"
class="ri-add-circle-fill" />
{#if $currentAsset}
<div class="nav-items-container">
<ComponentNavigationTree />
</div>
{/if}
<div class="role-select">
<Select
extraThin
secondary
on:change={updateAccessRole}
value={$selectedAccessRole}
label="Filter by Access">
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
</div>
<div class="nav-items-container">
<ComponentNavigationTree />
</div>
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>
@ -54,8 +97,8 @@
on:click={modal.show}
data-cy="new-layout"
class="ri-add-circle-fill" />
{#each $store.layouts as layout (layout._id)}
<Layout {layout} />
{#each $store.layouts as layout, idx (layout._id)}
<Layout {layout} border={idx > 0} />
{/each}
<Modal bind:this={modal}>
<NewLayoutModal />
@ -82,4 +125,8 @@
cursor: pointer;
color: var(--blue);
}
.role-select {
margin-bottom: var(--spacing-m);
}
</style>

View File

@ -10,6 +10,7 @@
import { writable } from "svelte/store"
export let layout
export let border
let confirmDeleteDialog
let componentToDelete = ""
@ -23,17 +24,17 @@
</script>
<NavItem
border={false}
{border}
icon="ri-layout-3-line"
text={layout.name}
withArrow
selected={$store.currentAssetId === layout._id}
opened={$store.currentAssetId === layout._id}
selected={$store.selectedLayoutId === layout._id}
opened={$store.selectedLayoutId === layout._id}
on:click={selectLayout}>
<LayoutDropdownMenu {layout} />
</NavItem>
{#if $store.currentAssetId === layout._id && layout.props?._children}
{#if $store.selectedLayoutId === layout._id && layout.props?._children}
<ComponentTree
components={layout.props._children}
currentComponent={$selectedComponent}

View File

@ -1,19 +1,15 @@
<script>
import { goto } from "@sveltech/routify"
import api from "builderStore/api"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore, allScreens } from "builderStore"
import { store } from "builderStore"
import { Input, ModalContent } from "@budibase/bbui"
import analytics from "analytics"
const CONTAINER = "@budibase/standard-components/container"
let name = ""
async function save() {
try {
await store.actions.layouts.save({ name })
$goto(`./${$store.currentAssetId}`)
const layout = await store.actions.layouts.save({ name })
$goto(`./${layout._id}`)
notifier.success(`Layout ${name} created successfully`)
} catch (err) {
notifier.danger(`Error creating layout ${name}.`)

View File

@ -1,17 +1,11 @@
<script>
import { goto } from "@sveltech/routify"
import { store, backendUiStore, allScreens } from "builderStore"
import {
Input,
Button,
Spacer,
Select,
ModalContent,
Toggle,
} from "@budibase/bbui"
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates"
import { some } from "lodash/fp"
import analytics from "analytics"
import { onMount } from "svelte"
import api from "builderStore/api"
const CONTAINER = "@budibase/standard-components/container"
@ -21,15 +15,13 @@
let templateIndex
let draftScreen
let createLink = true
let roleId = "BASIC"
$: templates = getTemplates($store, $backendUiStore.tables)
$: route = !route && $allScreens.length === 0 ? "*" : route
$: baseComponents = Object.values($store.components)
.filter(componentDefinition => componentDefinition.baseComponent)
.map(c => c._component)
$: {
if (templates && templateIndex === undefined) {
templateIndex = 0
@ -56,10 +48,10 @@
const save = async () => {
if (!route) {
routeError = "Url is required"
routeError = "URL is required"
} else {
if (routeNameExists(route)) {
routeError = "This url is already taken"
if (routeExists(route, roleId)) {
routeError = "This URL is already taken for this access role"
} else {
routeError = ""
}
@ -69,8 +61,7 @@
draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent
// TODO: need to fix this up correctly
draftScreen.routing = { route, roleId: "ADMIN" }
draftScreen.routing = { route, roleId }
const createdScreen = await store.actions.screens.create(draftScreen)
if (createLink) {
@ -85,12 +76,14 @@
})
}
$goto(`./screen/${createdScreen._id}`)
$goto(`./${createdScreen._id}`)
}
const routeNameExists = route => {
const routeExists = (route, roleId) => {
return $allScreens.some(
screen => screen.routing.route.toLowerCase() === route.toLowerCase()
screen =>
screen.routing.route.toLowerCase() === route.toLowerCase() &&
screen.routing.roleId === roleId
)
}
@ -113,14 +106,16 @@
{/each}
{/if}
</Select>
<Input label="Name" bind:value={name} />
<Input
label="Url"
error={routeError}
bind:value={route}
on:change={routeChanged} />
<Select label="Access" bind:value={roleId} secondary>
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
<Toggle text="Create link in navigation bar" bind:checked={createLink} />
</ModalContent>

View File

@ -0,0 +1,15 @@
<script>
import { Select } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
export let value
let roles = []
</script>
<Select bind:value extraThin secondary on:change>
<option value="">Choose an option</option>
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>

View File

@ -1,8 +1,10 @@
<script>
import { get } from "lodash"
import { isEmpty } from "lodash/fp"
import { FrontendTypes } from "constants"
import PropertyControl from "./PropertyControl.svelte"
import LayoutSelect from "./LayoutSelect.svelte"
import RoleSelect from "./RoleSelect.svelte"
import Input from "./PropertyPanelControls/Input.svelte"
import { excludeProps } from "./propertyCategories.js"
import { store, allScreens, currentAsset } from "builderStore"
@ -16,7 +18,13 @@
export let displayNameField = false
export let assetInstance
let assetProps = ["title", "description", "route", "layoutId"]
let assetProps = [
"title",
"description",
"routing.route",
"layoutId",
"routing.roleId",
]
let duplicateName = false
const propExistsOnComponentDef = prop =>
@ -28,7 +36,8 @@
const screenDefinition = [
{ key: "description", label: "Description", control: Input },
{ key: "route", label: "Route", control: Input },
{ key: "routing.route", label: "Route", control: Input },
{ key: "routing.roleId", label: "Access", control: RoleSelect },
{ key: "layoutId", label: "Layout", control: LayoutSelect },
]
@ -92,7 +101,7 @@
control={def.control}
label={def.label}
key={def.key}
value={assetInstance[def.key]}
value={get(assetInstance, def.key)}
onChange={onScreenPropChange}
props={{ ...excludeProps(def, ['control', 'label']) }} />
{/each}

View File

@ -20,6 +20,7 @@
backendUiStore.actions.reset()
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
await backendUiStore.actions.roles.fetch()
return pkg
} else {
throw new Error(pkg)
@ -217,5 +218,6 @@
position: absolute;
bottom: var(--spacing-m);
left: var(--spacing-m);
z-index: 1;
}
</style>

View File

@ -26,11 +26,12 @@
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
// Get the correct screen children.
const assetChildren = assetList.find(
asset =>
asset._id === $params.asset ||
asset._id === decodeURIComponent($params.asset)
).props._children
const assetChildren =
assetList.find(
asset =>
asset._id === $params.asset ||
asset._id === decodeURIComponent($params.asset)
)?.props._children ?? []
findComponent(componentIds, assetChildren)
}
// }

View File

@ -1,11 +1,10 @@
<script>
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import { onMount } from "svelte"
import { FrontendTypes } from "constants"
import CurrentItemPreview from "components/userInterface/AppPreview"
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
import { last } from "lodash/fp"
import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte"
$: instance = $store.appInstance
@ -36,7 +35,7 @@
</div>
<div class="preview-pane">
{#if $store.currentAssetId && $store.currentAssetId.length > 0}
{#if $currentAsset}
<ComponentSelectionList />
<div class="preview-content">
<CurrentItemPreview />

View File

@ -5,12 +5,32 @@
// Go to first layout
if ($params.assetType === FrontendTypes.LAYOUT) {
$goto(`../${$store.layouts[0]?._id}`)
// Try to use previously selected layout first
let id
if (
$store.selectedLayoutId &&
$store.layouts.find(layout => layout._id === $store.selectedLayoutId)
) {
id = $store.selectedLayoutId
} else {
id = $store.layouts[0]?._id
}
$goto(`../${id}`)
}
// Go to first screen
if ($params.assetType === FrontendTypes.SCREEN) {
$goto(`../${$allScreens[0]?._id}`)
// Try to use previously selected layout first
let id
if (
$store.selectedScreenId &&
$allScreens.find(screen => screen._id === $store.selectedScreenId)
) {
id = $store.selectedScreenId
} else {
id = $allScreens[0]?._id
}
$goto(`../${id}`)
}
</script>

View File

@ -3,7 +3,7 @@
import { setContext, onMount } from "svelte"
import Component from "./Component.svelte"
import SDK from "../sdk"
import { createDataStore, routeStore, screenStore } from "../store"
import { createDataStore, initialise, screenStore } from "../store"
// Provide contexts
setContext("sdk", SDK)
@ -14,13 +14,11 @@
// Load app config
onMount(async () => {
await routeStore.actions.fetchRoutes()
await screenStore.actions.fetchScreens()
await initialise()
loaded = true
})
</script>
{#if loaded && $screenStore.activeLayout}
<!-- // TODO: need to get the active screen as well -->
<Component definition={$screenStore.activeLayout.props} />
{/if}

View File

@ -7,7 +7,15 @@
const { styleable } = getContext("sdk")
const component = getContext("component")
$: routerConfig = getRouterConfig($routeStore.routes)
// Only wrap this as an array to take advantage of svelte keying,
// to ensure the svelte-spa-router is fully remounted when route config
// changes
$: configs = [
{
routes: getRouterConfig($routeStore.routes),
id: $routeStore.routeSessionId,
},
]
const getRouterConfig = routes => {
let config = {}
@ -25,11 +33,11 @@
}
</script>
{#if routerConfig}
{#each configs as config (config.id)}
<div use:styleable={$component.styles}>
<Router on:routeLoading={onRouteLoading} routes={routerConfig} />
<Router on:routeLoading={onRouteLoading} routes={config.routes} />
</div>
{/if}
{/each}
<style>
div {

View File

@ -1,18 +1,29 @@
import * as API from "../api"
import { getAppId } from "../utils/getAppId"
import { writable } from "svelte/store"
import { initialise } from "./initialise"
import { routeStore } from "./routes"
const createAuthStore = () => {
const store = writable("")
const goToDefaultRoute = () => {
// Setting the active route forces an update of the active screen ID,
// even if we're on the same URL
routeStore.actions.setActiveRoute("/")
// Navigating updates the URL to reflect this route
routeStore.actions.navigate("/")
}
const logIn = async ({ email, password }) => {
const user = await API.logIn({ email, password })
if (!user.error) {
store.set(user.token)
location.reload()
await initialise()
goToDefaultRoute()
}
}
const logOut = () => {
const logOut = async () => {
store.set("")
const appId = getAppId()
if (appId) {
@ -20,7 +31,8 @@ const createAuthStore = () => {
window.document.cookie = `budibase:${appId}:${environment}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`
}
}
location.reload()
await initialise()
goToDefaultRoute()
}
return {

View File

@ -6,3 +6,6 @@ export { bindingStore } from "./binding"
// Data stores are layered and duplicated, so it is not a singleton
export { createDataStore, dataStore } from "./data"
// Initialises an app by loading screens and routes
export { initialise } from "./initialise"

View File

@ -0,0 +1,7 @@
import { routeStore } from "./routes"
import { screenStore } from "./screens"
export async function initialise() {
await routeStore.actions.fetchRoutes()
await screenStore.actions.fetchScreens()
}

View File

@ -7,6 +7,7 @@ const createRouteStore = () => {
routes: [],
routeParams: {},
activeRoute: null,
routeSessionId: Math.random(),
}
const store = writable(initialState)
@ -21,8 +22,15 @@ const createRouteStore = () => {
})
})
})
// Sort route by paths so that the router matches correctly
routes.sort((a, b) => {
return a.path > b.path ? -1 : 1
})
store.update(state => {
state.routes = routes
state.routeSessionId = Math.random()
return state
})
}

View File

@ -32,7 +32,6 @@ const {
} = require("../../constants/screens")
const { cloneDeep } = require("lodash/fp")
const { recurseMustache } = require("../../utilities/mustache")
const { generateAssetCss } = require("../../utilities/builder/generateCss")
const { USERS_TABLE_SCHEMA } = require("../../constants")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -131,12 +130,6 @@ exports.fetchAppPackage = async function(ctx) {
const application = await db.get(ctx.params.appId)
const [layouts, screens] = await Promise.all([getLayouts(db), getScreens(db)])
for (let layout of layouts) {
layout._css = generateAssetCss([layout.props])
}
for (let screen of screens) {
screen._css = generateAssetCss([screen.props])
}
ctx.body = {
application,
screens,
@ -230,10 +223,6 @@ const createEmptyAppPackage = async (ctx, app) => {
screensAndLayouts.push(loginScreen)
await db.bulkDocs(screensAndLayouts)
// at the end add CSS to all the structures
for (let asset of screensAndLayouts) {
asset._css = generateAssetCss([asset.props])
}
await compileStaticAssets(app._id, screensAndLayouts)
await compileStaticAssets(app._id)
return newAppFolder
}

View File

@ -34,7 +34,6 @@ exports.authenticate = async ctx => {
userId: dbUser._id,
roleId: dbUser.roleId,
version: app.version,
permissions: dbUser.permissions || [],
}
// if in cloud add the user api key
if (env.CLOUD) {

View File

@ -7,8 +7,6 @@ const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
const PouchDB = require("../../../db")
const env = require("../../../environment")
const EXCLUDED_DIRECTORIES = ["css"]
/**
* Finalises the deployment, updating the quota for the user API key
* The verification process returns the levels to update to.
@ -140,15 +138,9 @@ exports.uploadAppAssets = async function({ appId, bucket, accountId }) {
let uploads = []
// Upload HTML, CSS and JS of the web app
// Upload HTML and JS of the web app
walkDir(appAssetsPath, function(filePath) {
const filePathParts = filePath.split("/")
const publicIndex = filePathParts.indexOf("public")
const directory = filePathParts[publicIndex + 1]
// don't include these top level directories
if (EXCLUDED_DIRECTORIES.indexOf(directory) !== -1) {
return
}
const appAssetUpload = prepareUploadForS3({
file: {
path: filePath,

View File

@ -0,0 +1,6 @@
const { BUILTIN_PERMISSIONS } = require("../../utilities/security/permissions")
exports.fetch = async function(ctx) {
// TODO: need to build out custom permissions
ctx.body = Object.values(BUILTIN_PERMISSIONS)
}

View File

@ -4,7 +4,40 @@ const {
Role,
getRole,
} = require("../../utilities/security/roles")
const { generateRoleID, getRoleParams } = require("../../db/utils")
const {
generateRoleID,
getRoleParams,
getUserParams,
ViewNames,
} = require("../../db/utils")
const UpdateRolesOptions = {
CREATED: "created",
REMOVED: "removed",
}
async function updateRolesOnUserTable(db, roleId, updateOption) {
const table = await db.get(ViewNames.USERS)
const schema = table.schema
const remove = updateOption === UpdateRolesOptions.REMOVED
let updated = false
for (let prop of Object.keys(schema)) {
if (prop === "roleId") {
updated = true
const constraints = schema[prop].constraints
const indexOf = constraints.inclusion.indexOf(roleId)
if (remove && indexOf !== -1) {
constraints.inclusion.splice(indexOf, 1)
} else if (!remove && indexOf === -1) {
constraints.inclusion.push(roleId)
}
break
}
}
if (updated) {
await db.put(table)
}
}
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
@ -15,7 +48,13 @@ exports.fetch = async function(ctx) {
)
const customRoles = body.rows.map(row => row.doc)
const staticRoles = [BUILTIN_ROLES.ADMIN, BUILTIN_ROLES.POWER]
// exclude internal roles like builder
const staticRoles = [
BUILTIN_ROLES.ADMIN,
BUILTIN_ROLES.POWER,
BUILTIN_ROLES.BASIC,
BUILTIN_ROLES.PUBLIC,
]
ctx.body = [...staticRoles, ...customRoles]
}
@ -25,13 +64,18 @@ exports.find = async function(ctx) {
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
let id = ctx.request.body._id || generateRoleID()
const role = new Role(id, ctx.request.body.name, ctx.request.body.inherits)
let { _id, name, inherits, permissionId } = ctx.request.body
if (!_id) {
_id = generateRoleID()
}
const role = new Role(_id, name)
.addPermission(permissionId)
.addInheritance(inherits)
if (ctx.request.body._rev) {
role._rev = ctx.request.body._rev
}
const result = await db.put(role)
await updateRolesOnUserTable(db, _id, UpdateRolesOptions.CREATED)
role._rev = result.rev
ctx.body = role
ctx.message = `Role '${role.name}' created successfully.`
@ -39,7 +83,26 @@ exports.save = async function(ctx) {
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
await db.remove(ctx.params.roleId, ctx.params.rev)
ctx.message = `Role ${ctx.params.id} deleted successfully`
const roleId = ctx.params.roleId
// first check no users actively attached to role
const users = (
await db.allDocs(
getUserParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
const usersWithRole = users.filter(user => user.roleId === roleId)
if (usersWithRole.length !== 0) {
ctx.throw("Cannot delete role when it is in use.")
}
await db.remove(roleId, ctx.params.rev)
await updateRolesOnUserTable(
db,
ctx.params.roleId,
UpdateRolesOptions.REMOVED
)
ctx.message = `Role ${ctx.params.roleId} deleted successfully`
ctx.status = 200
}

View File

@ -9,7 +9,7 @@ const {
ViewNames,
} = require("../../db/utils")
const usersController = require("./user")
const { cloneDeep } = require("lodash")
const { coerceRowValues } = require("../../utilities")
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
@ -29,6 +29,28 @@ validateJs.extend(validateJs.validators.datetime, {
},
})
async function findRow(db, appId, tableId, rowId) {
let row
if (tableId === ViewNames.USERS) {
let ctx = {
params: {
userId: rowId,
},
user: {
appId,
},
}
await usersController.find(ctx)
row = ctx.body
} else {
row = await db.get(rowId)
}
if (row.tableId !== tableId) {
throw "Supplied tableId does not match the rows tableId"
}
return row
}
exports.patch = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
@ -64,6 +86,13 @@ exports.patch = async function(ctx) {
tableId: row.tableId,
table,
})
// Creation of a new user goes to the user controller
if (row.tableId === ViewNames.USERS) {
await usersController.update(ctx)
return
}
const response = await db.put(row)
row._rev = response.rev
row.type = "row"
@ -80,19 +109,25 @@ exports.save = async function(ctx) {
let row = ctx.request.body
row.tableId = ctx.params.tableId
// TODO: find usage of this and break out into own endpoint
if (ctx.request.body.type === "delete") {
await bulkDelete(ctx)
ctx.body = ctx.request.body.rows
return
}
// if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting
if (existingRow) {
ctx.params.id = row._id
await exports.patch(ctx)
return
}
if (!row._rev && !row._id) {
row._id = generateRowID(row.tableId)
}
// if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting
const table = await db.get(row.tableId)
row = coerceRowValues(row, table)
@ -121,39 +156,22 @@ exports.save = async function(ctx) {
})
// Creation of a new user goes to the user controller
if (!existingRow && row.tableId === ViewNames.USERS) {
try {
await usersController.create(ctx)
} catch (err) {
ctx.body = { errors: [err.message] }
}
return
}
if (existingRow) {
const response = await db.put(row)
row._rev = response.rev
row.type = "row"
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} updated successfully.`
if (row.tableId === ViewNames.USERS) {
await usersController.create(ctx)
return
}
row.type = "row"
const response = await db.post(row)
const response = await db.put(row)
row._rev = response.rev
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} created successfully`
ctx.message = `${table.name} saved successfully`
}
exports.fetchView = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const { calculation, group, field } = ctx.query
const viewName = ctx.params.viewName
// if this is a table view being looked for just transfer to that
@ -163,6 +181,8 @@ exports.fetchView = async function(ctx) {
return
}
const db = new CouchDB(appId)
const { calculation, group, field } = ctx.query
const response = await db.query(`database/${viewName}`, {
include_docs: !calculation,
group,
@ -197,41 +217,32 @@ exports.fetchView = async function(ctx) {
exports.fetchTableRows = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const response = await db.allDocs(
getRowParams(ctx.params.tableId, null, {
include_docs: true,
})
)
ctx.body = response.rows.map(row => row.doc)
ctx.body = await linkRows.attachLinkInfo(
appId,
response.rows.map(row => row.doc)
)
}
exports.search = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const response = await db.allDocs({
include_docs: true,
...ctx.request.body,
})
ctx.body = await linkRows.attachLinkInfo(
appId,
response.rows.map(row => row.doc)
)
// special case for users, fetch through the user controller
let rows
if (ctx.params.tableId === ViewNames.USERS) {
await usersController.fetch(ctx)
rows = ctx.body
} else {
const db = new CouchDB(appId)
const response = await db.allDocs(
getRowParams(ctx.params.tableId, null, {
include_docs: true,
})
)
rows = response.rows.map(row => row.doc)
}
ctx.body = await linkRows.attachLinkInfo(appId, rows)
}
exports.find = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const row = await db.get(ctx.params.rowId)
if (row.tableId !== ctx.params.tableId) {
ctx.throw(400, "Supplied tableId does not match the rows tableId")
return
try {
const row = await findRow(db, appId, ctx.params.tableId, ctx.params.rowId)
ctx.body = await linkRows.attachLinkInfo(appId, row)
} catch (err) {
ctx.throw(400, err)
}
ctx.body = await linkRows.attachLinkInfo(appId, row)
}
exports.destroy = async function(ctx) {
@ -297,7 +308,10 @@ exports.fetchEnrichedRow = async function(ctx) {
return
}
// need table to work out where links go in row
const [table, row] = await Promise.all([db.get(tableId), db.get(rowId)])
let [table, row] = await Promise.all([
db.get(tableId),
findRow(db, appId, tableId, rowId),
])
// get the link docs
const linkVals = await linkRows.getLinkDocuments({
appId,
@ -327,68 +341,6 @@ exports.fetchEnrichedRow = async function(ctx) {
ctx.status = 200
}
function coerceRowValues(record, table) {
const row = cloneDeep(record)
for (let [key, value] of Object.entries(row)) {
const field = table.schema[key]
if (!field) continue
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
row[key] = TYPE_TRANSFORM_MAP[field.type][value]
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
row[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
}
}
return row
}
const TYPE_TRANSFORM_MAP = {
link: {
"": [],
[null]: [],
[undefined]: undefined,
},
options: {
"": "",
[null]: "",
[undefined]: undefined,
},
string: {
"": "",
[null]: "",
[undefined]: undefined,
},
longform: {
"": "",
[null]: "",
[undefined]: undefined,
},
number: {
"": null,
[null]: null,
[undefined]: undefined,
parse: n => parseFloat(n),
},
datetime: {
"": null,
[undefined]: undefined,
[null]: null,
},
attachment: {
"": [],
[null]: [],
[undefined]: undefined,
},
boolean: {
"": null,
[null]: null,
[undefined]: undefined,
true: true,
false: false,
},
}
async function bulkDelete(ctx) {
const appId = ctx.user.appId
const { rows } = ctx.request.body

View File

@ -1,8 +1,6 @@
const CouchDB = require("../../db")
const { getScreenParams, generateScreenID } = require("../../db/utils")
const { AccessController } = require("../../utilities/security/roles")
const { generateAssetCss } = require("../../utilities/builder/generateCss")
const compileStaticAssets = require("../../utilities/builder/compileStaticAssets")
exports.fetch = async ctx => {
const appId = ctx.user.appId
@ -32,10 +30,6 @@ exports.save = async ctx => {
}
const response = await db.put(screen)
// update CSS so client doesn't need to make a call directly after
screen._css = generateAssetCss([screen.props])
await compileStaticAssets(appId, screen)
ctx.message = `Screen ${screen.name} saved.`
ctx.body = {
...screen,

View File

@ -16,19 +16,10 @@ const CouchDB = require("../../../db")
const setBuilderToken = require("../../../utilities/builder/setBuilderToken")
const fileProcessor = require("../../../utilities/fileProcessor")
const env = require("../../../environment")
const { generateAssetCss } = require("../../../utilities/builder/generateCss")
const compileStaticAssets = require("../../../utilities/builder/compileStaticAssets")
// this was the version before we started versioning the component library
const COMP_LIB_BASE_APP_VERSION = "0.2.5"
exports.generateCss = async function(ctx) {
const structure = ctx.request.body
structure._css = generateAssetCss([structure.props])
await compileStaticAssets(ctx.appId, structure)
ctx.body = { css: structure._css }
}
exports.serveBuilder = async function(ctx) {
let builderPath = resolve(__dirname, "../../../../builder")
if (ctx.file === "index.html") {

View File

@ -22,13 +22,7 @@
<title>{title}</title>
<link rel="icon" type="image/png" href={favicon} />
<link rel="stylesheet" href={publicPath('bundle.css')} />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto+Mono" />
<style>
html,
body {

View File

@ -1,40 +1,45 @@
const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const { generateUserID, getUserParams, ViewNames } = require("../../db/utils")
const { BUILTIN_ROLE_ID_ARRAY } = require("../../utilities/security/roles")
const {
BUILTIN_PERMISSION_NAMES,
} = require("../../utilities/security/permissions")
const { getRole } = require("../../utilities/security/roles")
exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const data = await database.allDocs(
getUserParams("", {
include_docs: true,
})
)
ctx.body = data.rows.map(row => row.doc)
const users = (
await database.allDocs(
getUserParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
// user hashed password shouldn't ever be returned
for (let user of users) {
delete user.password
}
ctx.body = users
}
exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const { email, password, roleId, permissions } = ctx.request.body
const { email, password, roleId } = ctx.request.body
if (!email || !password) {
ctx.throw(400, "email and Password Required.")
}
const role = await checkRole(db, roleId)
const role = await getRole(ctx.user.appId, roleId)
if (!role) ctx.throw(400, "Invalid Role")
const hashedPassword = await bcrypt.hash(password)
const user = {
...ctx.request.body,
// these must all be after the object spread, make sure
// any values are overwritten, generateUserID will always
// generate the same ID for the user as it is not UUID based
_id: generateUserID(email),
email,
password: await bcrypt.hash(password),
type: "user",
roleId,
permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER],
password: hashedPassword,
tableId: ViewNames.USERS,
}
@ -59,7 +64,12 @@ exports.create = async function(ctx) {
exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const user = ctx.request.body
const dbUser = db.get(ctx.request.body._id)
const dbUser = await db.get(ctx.request.body._id)
if (user.password) {
user.password = await bcrypt.hash(user.password)
} else {
delete user.password
}
const newData = { ...dbUser, ...user }
const response = await db.put(newData)
@ -79,22 +89,12 @@ exports.destroy = async function(ctx) {
exports.find = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const user = await database.get(generateUserID(ctx.params.email))
ctx.body = {
email: user.email,
name: user.name,
_rev: user._rev,
let lookup = ctx.params.email
? generateUserID(ctx.params.email)
: ctx.params.userId
const user = await database.get(lookup)
if (user) {
delete user.password
}
}
const checkRole = async (db, roleId) => {
if (!roleId) return
if (BUILTIN_ROLE_ID_ARRAY.indexOf(roleId) !== -1) {
return {
_id: roleId,
name: roleId,
permissions: [],
}
}
return await db.get(roleId)
ctx.body = user
}

View File

@ -16,6 +16,7 @@ const apiKeysRoutes = require("./apikeys")
const templatesRoutes = require("./templates")
const analyticsRoutes = require("./analytics")
const routingRoutes = require("./routing")
const permissionRoutes = require("./permission")
exports.mainRoutes = [
deployRoutes,
@ -32,6 +33,7 @@ exports.mainRoutes = [
analyticsRoutes,
webhookRoutes,
routingRoutes,
permissionRoutes,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this
tableRoutes,

View File

@ -0,0 +1,10 @@
const Router = require("@koa/router")
const controller = require("../controllers/permission")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()
router.get("/api/permissions", authorized(BUILDER), controller.fetch)
module.exports = router

View File

@ -2,11 +2,27 @@ const Router = require("@koa/router")
const controller = require("../controllers/role")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const Joi = require("joi")
const joiValidator = require("../../middleware/joi-validator")
const {
BUILTIN_PERMISSION_IDS,
} = require("../../utilities/security/permissions")
const router = Router()
function generateValidator() {
// prettier-ignore
return joiValidator.body(Joi.object({
_id: Joi.string().optional(),
_rev: Joi.string().optional(),
name: Joi.string().required(),
permissionId: Joi.string().valid(...Object.values(BUILTIN_PERMISSION_IDS)).required(),
inherits: Joi.string().optional(),
}).unknown(true))
}
router
.post("/api/roles", authorized(BUILDER), controller.save)
.post("/api/roles", authorized(BUILDER), generateValidator(), controller.save)
.get("/api/roles", authorized(BUILDER), controller.fetch)
.get("/api/roles/:roleId", authorized(BUILDER), controller.find)
.delete("/api/roles/:roleId/:rev", authorized(BUILDER), controller.destroy)

View File

@ -25,7 +25,6 @@ router
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.find
)
.post("/api/rows/search", rowController.search)
.post(
"/api/:tableId/rows",
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),

View File

@ -10,7 +10,6 @@ const router = Router()
function generateSaveValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
_css: Joi.string().allow(""),
name: Joi.string().required(),
routing: Joi.object({
route: Joi.string().required(),

View File

@ -5,22 +5,6 @@ const env = require("../../environment")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const usage = require("../../middleware/usageQuota")
const joiValidator = require("../../middleware/joi-validator")
const Joi = require("joi")
function generateCssValidator() {
return joiValidator.body(
Joi.object({
_id: Joi.string().required(),
_rev: Joi.string().required(),
props: Joi.object()
.required()
.unknown(true),
})
.required()
.unknown(true)
)
}
const router = Router()
@ -40,12 +24,6 @@ if (env.NODE_ENV !== "production") {
}
router
.post(
"/api/css/generate",
authorized(BUILDER),
generateCssValidator(),
controller.generateCss
)
.post(
"/api/attachments/process",
authorized(BUILDER),

View File

@ -1,9 +1,6 @@
const CouchDB = require("../../../db")
const supertest = require("supertest")
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const {
BUILTIN_PERMISSION_NAMES,
} = require("../../../utilities/security/permissions")
const packageJson = require("../../../../package")
const jwt = require("jsonwebtoken")
const env = require("../../../environment")
@ -131,49 +128,7 @@ exports.createUser = async (
return res.body
}
const createUserWithOnePermission = async (request, appId, permName) => {
let permissions = [permName]
return await createUserWithPermissions(
request,
appId,
permissions,
"onePermOnlyUser"
)
}
const createUserWithAdminPermissions = async (request, appId) => {
let permissions = [BUILTIN_PERMISSION_NAMES.ADMIN]
return await createUserWithPermissions(
request,
appId,
permissions,
"adminUser"
)
}
const createUserWithAllPermissionExceptOne = async (
request,
appId,
permName
) => {
let permissions = [permName]
return await createUserWithPermissions(
request,
appId,
permissions,
"allPermsExceptOneUser"
)
}
const createUserWithPermissions = async (
request,
appId,
permissions,
email
) => {
const createUserWithRole = async (request, appId, roleId, email) => {
const password = `password_${email}`
await request
.post(`/api/users`)
@ -181,8 +136,7 @@ const createUserWithPermissions = async (
.send({
email,
password,
roleId: BUILTIN_ROLE_IDS.POWER,
permissions,
roleId,
})
const anonUser = {
@ -215,23 +169,29 @@ exports.testPermissionsForEndpoint = async ({
url,
body,
appId,
permName1,
permName2,
passRole,
failRole,
}) => {
const headers = await createUserWithOnePermission(request, appId, permName1)
await createRequest(request, method, url, body)
.set(headers)
.expect(200)
const noPermsHeaders = await createUserWithAllPermissionExceptOne(
const passHeader = await createUserWithRole(
request,
appId,
permName2
passRole,
"passUser@budibase.com"
)
await createRequest(request, method, url, body)
.set(noPermsHeaders)
.set(passHeader)
.expect(200)
const failHeader = await createUserWithRole(
request,
appId,
failRole,
"failUser@budibase.com"
)
await createRequest(request, method, url, body)
.set(failHeader)
.expect(403)
}
@ -242,7 +202,12 @@ exports.builderEndpointShouldBlockNormalUsers = async ({
body,
appId,
}) => {
const headers = await createUserWithAdminPermissions(request, appId)
const headers = await createUserWithRole(
request,
appId,
BUILTIN_ROLE_IDS.BASIC,
"basicUser@budibase.com"
)
await createRequest(request, method, url, body)
.set(headers)

View File

@ -1,46 +0,0 @@
const { generateAssetCss, generateCss } = require("../../../utilities/builder/generateCss")
describe("generate_css", () => {
it("Check how array styles are output", () => {
expect(generateCss({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0 10 0 15;")
})
it("Check handling of an array with empty string values", () => {
expect(generateCss({ padding: ["", "", "", ""] })).toBe("")
})
it("Check handling of an empty array", () => {
expect(generateCss({ margin: [] })).toBe("")
})
it("Check handling of valid font property", () => {
expect(generateCss({ "font-size": "10px" })).toBe("font-size: 10px;")
})
})
describe("generate_screen_css", () => {
const normalComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: { "font-size": "16px" }, hover: {}, active: {}, selected: {} } }
it("Test generation of normal css styles", () => {
expect(generateAssetCss([normalComponent])).toBe(".header-123-456 {\nfont-size: 16px;\n}")
})
const hoverComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {"font-size": "16px"}, active: {}, selected: {} } }
it("Test generation of hover css styles", () => {
expect(generateAssetCss([hoverComponent])).toBe(".header-123-456:hover {\nfont-size: 16px;\n}")
})
const selectedComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: { "font-size": "16px" } } }
it("Test generation of selection css styles", () => {
expect(generateAssetCss([selectedComponent])).toBe(".header-123-456::selection {\nfont-size: 16px;\n}")
})
const emptyComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: {} } }
it("Testing handling of empty component styles", () => {
expect(generateAssetCss([emptyComponent])).toBe("")
})
})

View File

@ -1,15 +1,20 @@
const {
const {
createApplication,
createTable,
createView,
supertest,
defaultHeaders
defaultHeaders,
} = require("./couchTestUtils")
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const {
BUILTIN_ROLE_IDS,
} = require("../../../utilities/security/roles")
BUILTIN_PERMISSION_IDS,
} = require("../../../utilities/security/permissions")
const roleBody = { name: "user", inherits: BUILTIN_ROLE_IDS.BASIC }
const roleBody = {
name: "NewRole",
inherits: BUILTIN_ROLE_IDS.BASIC,
permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY,
}
describe("/roles", () => {
let server
@ -19,8 +24,8 @@ describe("/roles", () => {
let view
beforeAll(async () => {
({ request, server } = await supertest())
});
;({ request, server } = await supertest())
})
afterAll(() => {
server.close()
@ -34,30 +39,29 @@ describe("/roles", () => {
})
describe("create", () => {
it("returns a success message when role is successfully created", async () => {
const res = await request
.post(`/api/roles`)
.send(roleBody)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
expect(res.res.statusMessage).toEqual("Role 'user' created successfully.")
expect(res.res.statusMessage).toEqual(
"Role 'NewRole' created successfully."
)
expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined()
})
});
})
describe("fetch", () => {
it("should list custom roles, plus 2 default roles", async () => {
const createRes = await request
.post(`/api/roles`)
.send(roleBody)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
const customRole = createRes.body
@ -65,33 +69,37 @@ describe("/roles", () => {
const res = await request
.get(`/api/roles`)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.length).toBe(3)
expect(res.body.length).toBe(5)
const adminRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.ADMIN)
expect(adminRole.inherits).toEqual(BUILTIN_ROLE_IDS.POWER)
expect(adminRole).toBeDefined()
expect(adminRole.inherits).toEqual(BUILTIN_ROLE_IDS.POWER)
expect(adminRole.permissionId).toEqual(BUILTIN_PERMISSION_IDS.ADMIN)
const powerUserRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.POWER)
expect(powerUserRole.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(powerUserRole).toBeDefined()
expect(powerUserRole.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(powerUserRole.permissionId).toEqual(BUILTIN_PERMISSION_IDS.POWER)
const customRoleFetched = res.body.find(r => r._id === customRole._id)
expect(customRoleFetched.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(customRoleFetched).toBeDefined()
expect(customRoleFetched.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(customRoleFetched.permissionId).toEqual(
BUILTIN_PERMISSION_IDS.READ_ONLY
)
})
});
})
describe("destroy", () => {
it("should delete custom roles", async () => {
const createRes = await request
.post(`/api/roles`)
.send({ name: "user" })
.send({ name: "user", permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY })
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
const customRole = createRes.body
@ -107,4 +115,4 @@ describe("/roles", () => {
.expect(404)
})
})
});
})

View File

@ -51,11 +51,9 @@ describe("/rows", () => {
describe("save, load, update, delete", () => {
it("returns a success message when the row is created", async () => {
const res = await createRow()
expect(res.res.statusMessage).toEqual(`${table.name} created successfully`)
expect(res.res.statusMessage).toEqual(`${table.name} saved successfully`)
expect(res.body.name).toEqual("Test Contact")
expect(res.body._rev).toBeDefined()
})
@ -118,30 +116,6 @@ describe("/rows", () => {
expect(res.body.find(r => r.name === row.name)).toBeDefined()
})
it("lists rows when queried by their ID", async () => {
const newRow = {
tableId: table._id,
name: "Second Contact",
status: "new"
}
const row = await createRow()
const secondRow = await createRow(newRow)
const rowIds = [row.body._id, secondRow.body._id]
const res = await request
.post(`/api/rows/search`)
.set(defaultHeaders(appId))
.send({
keys: rowIds
})
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.length).toBe(2)
expect(res.body.map(response => response._id)).toEqual(expect.arrayContaining(rowIds))
})
it("load should return 404 when row does not exist", async () => {
await createRow()
await request

View File

@ -5,12 +5,14 @@ const {
createUser,
testPermissionsForEndpoint,
} = require("./couchTestUtils")
const {
BUILTIN_PERMISSION_NAMES,
} = require("../../../utilities/security/permissions")
const {
BUILTIN_ROLE_IDS,
} = require("../../../utilities/security/roles")
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const { cloneDeep } = require("lodash/fp")
const baseBody = {
email: "bill@bill.com",
password: "yeeooo",
roleId: BUILTIN_ROLE_IDS.POWER,
}
describe("/users", () => {
let request
@ -19,13 +21,13 @@ describe("/users", () => {
let appId
beforeAll(async () => {
({ request, server } = await supertest(server))
});
;({ request, server } = await supertest(server))
})
beforeEach(async () => {
app = await createApplication(request)
appId = app.instance._id
});
})
afterAll(() => {
server.close()
@ -39,9 +41,9 @@ describe("/users", () => {
const res = await request
.get(`/api/users`)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.length).toBe(2)
expect(res.body.find(u => u.email === "brenda@brenda.com")).toBeDefined()
expect(res.body.find(u => u.email === "pam@pam.com")).toBeDefined()
@ -54,37 +56,39 @@ describe("/users", () => {
method: "GET",
url: `/api/users`,
appId: appId,
permName1: BUILTIN_PERMISSION_NAMES.POWER,
permName2: BUILTIN_PERMISSION_NAMES.WRITE,
passRole: BUILTIN_ROLE_IDS.ADMIN,
failRole: BUILTIN_ROLE_IDS.PUBLIC,
})
})
})
describe("create", () => {
it("returns a success message when a user is successfully created", async () => {
const body = cloneDeep(baseBody)
body.email = "bill@budibase.com"
const res = await request
.post(`/api/users`)
.set(defaultHeaders(appId))
.send({ email: "bill@bill.com", password: "bills_password", roleId: BUILTIN_ROLE_IDS.POWER })
.send(body)
.expect(200)
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
expect(res.res.statusMessage).toEqual("User created successfully.");
expect(res.res.statusMessage).toEqual("User created successfully.")
expect(res.body._id).toBeUndefined()
})
it("should apply authorization to endpoint", async () => {
const body = cloneDeep(baseBody)
body.email = "brandNewUser@user.com"
await testPermissionsForEndpoint({
request,
method: "POST",
body: { email: "brandNewUser@user.com", password: "yeeooo", roleId: BUILTIN_ROLE_IDS.POWER },
body,
url: `/api/users`,
appId: appId,
permName1: BUILTIN_PERMISSION_NAMES.ADMIN,
permName2: BUILTIN_PERMISSION_NAMES.POWER,
passRole: BUILTIN_ROLE_IDS.ADMIN,
failRole: BUILTIN_ROLE_IDS.PUBLIC,
})
})
});
})
})

View File

@ -32,7 +32,7 @@ const USERS_TABLE_SCHEMA = {
constraints: {
type: "string",
presence: false,
inclusion: Object.keys(BUILTIN_ROLE_IDS),
inclusion: Object.values(BUILTIN_ROLE_IDS),
},
},
},

View File

@ -102,15 +102,11 @@ exports.generateRowID = tableId => {
* Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/
exports.getUserParams = (email = "", otherProps = {}) => {
return getDocParams(
DocumentTypes.ROW,
`${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${email}`,
otherProps
)
return exports.getRowParams(ViewNames.USERS, email, otherProps)
}
/**
* Generates a new user ID based on the passed in username.
* Generates a new user ID based on the passed in email.
* @param {string} email The email which the ID is going to be built up of.
* @returns {string} The new user ID which the user doc can be stored under.
*/

View File

@ -1,4 +1,7 @@
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
const {
BUILTIN_ROLE_IDS,
getUserPermissionIds,
} = require("../utilities/security/roles")
const {
PermissionTypes,
doesHavePermission,
@ -48,7 +51,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
}
const role = ctx.user.role
const permissions = ctx.user.permissions
const permissions = await getUserPermissionIds(ctx.appId, role._id)
if (ADMIN_ROLES.indexOf(role._id) !== -1) {
return next()
}

View File

@ -1,66 +1,18 @@
const {
ensureDir,
constants,
copyFile,
writeFile,
readdir,
readFile,
existsSync,
} = require("fs-extra")
const { ensureDir, constants, copyFile } = require("fs-extra")
const { join } = require("../centralPath")
const { budibaseAppsDir } = require("../budibaseDir")
const CSS_DIRECTORY = "css"
/**
* Compile all the non-db static web assets that are required for the running of
* a budibase application. This includes CSS, the JSON structure of the DOM and
* a budibase application. This includes the JSON structure of the DOM and
* the client library, a script responsible for reading the JSON structure
* and rendering the application.
* @param {string} appId id of the application we want to compile static assets for
* @param {array|object} assets a list of screens or screen layouts for which the CSS should be extracted and stored.
*/
module.exports = async (appId, assets) => {
module.exports = async appId => {
const publicPath = join(budibaseAppsDir(), appId, "public")
await ensureDir(publicPath)
for (let asset of Array.isArray(assets) ? assets : [assets]) {
await buildCssBundle(publicPath, asset)
await copyClientLib(publicPath)
// remove props that shouldn't be present when written to DB
if (asset._css) {
delete asset._css
}
}
return assets
}
/**
* Reads the _css property of all screens and the screen layouts, and creates a singular CSS
* bundle for the app at <appId>/public/bundle.css
* @param {String} publicPath - path to the public assets directory of the budibase application
* @param {Object} asset a single screen or screen layout which is being updated
*/
const buildCssBundle = async (publicPath, asset) => {
const cssPath = join(publicPath, CSS_DIRECTORY)
let cssString = ""
// create a singular CSS file for this asset
const assetCss = asset._css ? asset._css.trim() : ""
if (assetCss.length !== 0) {
await ensureDir(cssPath)
await writeFile(join(cssPath, asset._id), assetCss)
}
// bundle up all the CSS in the directory into one top level CSS file
if (existsSync(cssPath)) {
const cssFiles = await readdir(cssPath)
for (let filename of cssFiles) {
const css = await readFile(join(cssPath, filename))
cssString += css
}
}
await writeFile(join(publicPath, "bundle.css"), cssString)
await copyClientLib(publicPath)
}
/**

View File

@ -1,43 +0,0 @@
exports.generateAssetCss = component_arr => {
let styles = ""
for (const { _styles, _id, _children, _component } of component_arr) {
let [componentName] = _component.match(/[a-z]*$/)
Object.keys(_styles).forEach(selector => {
const cssString = exports.generateCss(_styles[selector])
if (cssString) {
styles += exports.applyClass(_id, componentName, cssString, selector)
}
})
if (_children && _children.length) {
styles += exports.generateAssetCss(_children) + "\n"
}
}
return styles.trim()
}
exports.generateCss = style => {
let cssString = Object.entries(style).reduce((str, [key, value]) => {
if (typeof value === "string") {
if (value) {
return (str += `${key}: ${value};\n`)
}
} else if (Array.isArray(value)) {
if (value.length > 0 && !value.every(v => v === "")) {
return (str += `${key}: ${value.join(" ")};\n`)
}
}
return str
}, "")
return (cssString || "").trim()
}
exports.applyClass = (id, name = "element", styles, selector) => {
if (selector === "normal") {
return `.${name}-${id} {\n${styles}\n}`
} else {
let sel = selector === "selected" ? "::selection" : `:${selector}`
return `.${name}-${id}${sel} {\n${styles}\n}`
}
}

View File

@ -1,5 +1,4 @@
const { BUILTIN_ROLE_IDS } = require("../security/roles")
const { BUILTIN_PERMISSION_NAMES } = require("../security/permissions")
const env = require("../../environment")
const CouchDB = require("../../db")
const jwt = require("jsonwebtoken")
@ -11,7 +10,6 @@ module.exports = async (ctx, appId, version) => {
const builderUser = {
userId: "BUILDER",
roleId: BUILTIN_ROLE_IDS.BUILDER,
permissions: [BUILTIN_PERMISSION_NAMES.ADMIN],
version,
}
if (env.BUDIBASE_API_KEY) {

View File

@ -1,8 +1,59 @@
const env = require("../environment")
const { DocumentTypes, SEPARATOR } = require("../db/utils")
const fs = require("fs")
const { cloneDeep } = require("lodash/fp")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
/**
* A map of how we convert various properties in rows to each other based on the row type.
*/
const TYPE_TRANSFORM_MAP = {
link: {
"": [],
[null]: [],
[undefined]: undefined,
},
options: {
"": "",
[null]: "",
[undefined]: undefined,
},
string: {
"": "",
[null]: "",
[undefined]: undefined,
},
longform: {
"": "",
[null]: "",
[undefined]: undefined,
},
number: {
"": null,
[null]: null,
[undefined]: undefined,
parse: n => parseFloat(n),
},
datetime: {
"": null,
[undefined]: undefined,
[null]: null,
},
attachment: {
"": [],
[null]: [],
[undefined]: undefined,
},
boolean: {
"": null,
[null]: null,
[undefined]: undefined,
true: true,
false: false,
},
}
function confirmAppId(possibleAppId) {
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
? possibleAppId
@ -74,3 +125,46 @@ exports.setCookie = (ctx, name, value) => {
exports.isClient = ctx => {
return ctx.headers["x-budibase-type"] === "client"
}
/**
* Recursively walk a directory tree and execute a callback on all files.
* @param {String} dirPath - Directory to traverse
* @param {Function} callback - callback to execute on files
*/
exports.walkDir = (dirPath, callback) => {
for (let filename of fs.readdirSync(dirPath)) {
const filePath = `${dirPath}/${filename}`
const stat = fs.lstatSync(filePath)
if (stat.isFile()) {
callback(filePath)
} else {
exports.walkDir(filePath, callback)
}
}
}
/**
* This will coerce the values in a row to the correct types based on the type transform map and the
* table schema.
* @param {object} row The row which is to be coerced to correct values based on schema, this input
* row will not be updated.
* @param {object} table The table that has been retrieved from DB, this must contain the expected
* schema for the rows.
* @returns {object} The updated row will be returned with all values coerced.
*/
exports.coerceRowValues = (row, table) => {
const clonedRow = cloneDeep(row)
for (let [key, value] of Object.entries(clonedRow)) {
const field = table.schema[key]
if (!field) continue
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
clonedRow[key] = TYPE_TRANSFORM_MAP[field.type][value]
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
clonedRow[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
}
}
return clonedRow
}

View File

@ -45,7 +45,7 @@ function getAllowedLevels(userPermLevel) {
}
}
exports.BUILTIN_PERMISSION_NAMES = {
exports.BUILTIN_PERMISSION_IDS = {
READ_ONLY: "read_only",
WRITE: "write",
ADMIN: "admin",
@ -54,21 +54,24 @@ exports.BUILTIN_PERMISSION_NAMES = {
exports.BUILTIN_PERMISSIONS = {
READ_ONLY: {
name: exports.BUILTIN_PERMISSION_NAMES.READ_ONLY,
_id: exports.BUILTIN_PERMISSION_IDS.READ_ONLY,
name: "Read only",
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.READ),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
],
},
WRITE: {
name: exports.BUILTIN_PERMISSION_NAMES.WRITE,
_id: exports.BUILTIN_PERMISSION_IDS.WRITE,
name: "Read/Write",
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
],
},
POWER: {
name: exports.BUILTIN_PERMISSION_NAMES.POWER,
_id: exports.BUILTIN_PERMISSION_IDS.POWER,
name: "Power",
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE),
new Permission(PermissionTypes.USER, PermissionLevels.READ),
@ -78,7 +81,8 @@ exports.BUILTIN_PERMISSIONS = {
],
},
ADMIN: {
name: exports.BUILTIN_PERMISSION_NAMES.ADMIN,
_id: exports.BUILTIN_PERMISSION_IDS.ADMIN,
name: "Admin",
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.ADMIN),
new Permission(PermissionTypes.USER, PermissionLevels.ADMIN),
@ -89,11 +93,11 @@ exports.BUILTIN_PERMISSIONS = {
},
}
exports.doesHavePermission = (permType, permLevel, userPermissionNames) => {
exports.doesHavePermission = (permType, permLevel, permissionIds) => {
const builtins = Object.values(exports.BUILTIN_PERMISSIONS)
let permissions = flatten(
builtins
.filter(builtin => userPermissionNames.indexOf(builtin.name) !== -1)
.filter(builtin => permissionIds.indexOf(builtin._id) !== -1)
.map(builtin => builtin.permissions)
)
for (let permission of permissions) {

View File

@ -1,28 +1,46 @@
const CouchDB = require("../../db")
const { cloneDeep } = require("lodash/fp")
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
const BUILTIN_IDS = {
ADMIN: "ADMIN",
POWER: "POWER_USER",
POWER: "POWER",
BASIC: "BASIC",
PUBLIC: "PUBLIC",
BUILDER: "BUILDER",
}
function Role(id, name, inherits) {
function Role(id, name) {
this._id = id
this.name = name
if (inherits) {
this.inherits = inherits
}
}
Role.prototype.addPermission = function(permissionId) {
this.permissionId = permissionId
return this
}
Role.prototype.addInheritance = function(inherits) {
this.inherits = inherits
return this
}
exports.BUILTIN_ROLES = {
ADMIN: new Role(BUILTIN_IDS.ADMIN, "Admin", BUILTIN_IDS.POWER),
POWER: new Role(BUILTIN_IDS.POWER, "Power", BUILTIN_IDS.BASIC),
BASIC: new Role(BUILTIN_IDS.BASIC, "Basic", BUILTIN_IDS.PUBLIC),
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public"),
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder"),
ADMIN: new Role(BUILTIN_IDS.ADMIN, "Admin")
.addPermission(BUILTIN_PERMISSION_IDS.ADMIN)
.addInheritance(BUILTIN_IDS.POWER),
POWER: new Role(BUILTIN_IDS.POWER, "Power")
.addPermission(BUILTIN_PERMISSION_IDS.POWER)
.addInheritance(BUILTIN_IDS.BASIC),
BASIC: new Role(BUILTIN_IDS.BASIC, "Basic")
.addPermission(BUILTIN_PERMISSION_IDS.WRITE)
.addInheritance(BUILTIN_IDS.PUBLIC),
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public").addPermission(
BUILTIN_PERMISSION_IDS.READ_ONLY
),
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission(
BUILTIN_PERMISSION_IDS.ADMIN
),
}
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
@ -60,6 +78,29 @@ exports.getRole = async (appId, roleId) => {
return role
}
/**
* Simple function to get all the roles based on the top level user role ID.
*/
async function getAllUserRoles(appId, userRoleId) {
if (!userRoleId) {
return [BUILTIN_IDS.PUBLIC]
}
let currentRole = await exports.getRole(appId, userRoleId)
let roles = currentRole ? [currentRole] : []
let roleIds = [userRoleId]
// get all the inherited roles
while (
currentRole &&
currentRole.inherits &&
roleIds.indexOf(currentRole.inherits) === -1
) {
roleIds.push(currentRole.inherits)
currentRole = await exports.getRole(appId, currentRole.inherits)
roles.push(currentRole)
}
return roles
}
/**
* Returns an ordered array of the user's inherited role IDs, this can be used
* to determine if a user can access something that requires a specific role.
@ -70,22 +111,21 @@ exports.getRole = async (appId, roleId) => {
*/
exports.getUserRoleHierarchy = async (appId, userRoleId) => {
// special case, if they don't have a role then they are a public user
if (!userRoleId) {
return [BUILTIN_IDS.PUBLIC]
}
let roleIds = [userRoleId]
let userRole = await exports.getRole(appId, userRoleId)
// check if inherited makes it possible
while (
userRole &&
userRole.inherits &&
roleIds.indexOf(userRole.inherits) === -1
) {
roleIds.push(userRole.inherits)
// go to get the inherited incase it inherits anything
userRole = await exports.getRole(appId, userRole.inherits)
}
return roleIds
return (await getAllUserRoles(appId, userRoleId)).map(role => role._id)
}
/**
* Get all of the user permissions which could be found across the role hierarchy
* @param appId The ID of the application from which roles should be obtained.
* @param userRoleId The user's role ID, this can be found in their access token.
* @returns {Promise<string[]>} A list of permission IDs these should all be unique.
*/
exports.getUserPermissionIds = async (appId, userRoleId) => {
return [
...new Set(
(await getAllUserRoles(appId, userRoleId)).map(role => role.permissionId)
),
]
}
class AccessController {

View File

@ -11,7 +11,7 @@
$: target = openInNewTab ? "_blank" : "_self"
</script>
<a href={url} use:linkable {target} use:styleable={$component.styles}>
<a href={url || '/'} use:linkable {target} use:styleable={$component.styles}>
{text}
<slot />
</a>

View File

@ -6,8 +6,8 @@
export let logoUrl
const logOut = () => {
authStore.actions.logOut()
const logOut = async () => {
await authStore.actions.logOut()
}
</script>

325
yarn.lock
View File

@ -2,40 +2,64 @@
# yarn lockfile v1
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
"@babel/code-frame@^7.0.0":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
dependencies:
"@babel/highlight" "^7.8.3"
"@babel/generator@^7.8.4":
version "7.8.4"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.4.tgz#35bbc74486956fe4251829f9f6c48330e8d0985e"
"@babel/code-frame@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
dependencies:
"@babel/types" "^7.8.3"
"@babel/highlight" "^7.10.4"
"@babel/generator@^7.12.5":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de"
integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==
dependencies:
"@babel/types" "^7.12.5"
jsesc "^2.5.1"
lodash "^4.17.13"
source-map "^0.5.0"
"@babel/helper-function-name@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca"
"@babel/helper-function-name@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a"
integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==
dependencies:
"@babel/helper-get-function-arity" "^7.8.3"
"@babel/template" "^7.8.3"
"@babel/types" "^7.8.3"
"@babel/helper-get-function-arity" "^7.10.4"
"@babel/template" "^7.10.4"
"@babel/types" "^7.10.4"
"@babel/helper-get-function-arity@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5"
"@babel/helper-get-function-arity@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==
dependencies:
"@babel/types" "^7.8.3"
"@babel/types" "^7.10.4"
"@babel/helper-split-export-declaration@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9"
"@babel/helper-split-export-declaration@^7.11.0":
version "7.11.0"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f"
integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==
dependencies:
"@babel/types" "^7.8.3"
"@babel/types" "^7.11.0"
"@babel/helper-validator-identifier@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
"@babel/highlight@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143"
integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==
dependencies:
"@babel/helper-validator-identifier" "^7.10.4"
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/highlight@^7.8.3":
version "7.8.3"
@ -45,38 +69,42 @@
esutils "^2.0.2"
js-tokens "^4.0.0"
"@babel/parser@^7.0.0", "@babel/parser@^7.8.3", "@babel/parser@^7.8.4":
version "7.8.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.4.tgz#d1dbe64691d60358a974295fa53da074dd2ce8e8"
"@babel/parser@^7.12.7", "@babel/parser@^7.7.0":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056"
integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==
"@babel/template@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8"
"@babel/template@^7.10.4":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==
dependencies:
"@babel/code-frame" "^7.8.3"
"@babel/parser" "^7.8.3"
"@babel/types" "^7.8.3"
"@babel/code-frame" "^7.10.4"
"@babel/parser" "^7.12.7"
"@babel/types" "^7.12.7"
"@babel/traverse@^7.0.0":
version "7.8.4"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.4.tgz#f0845822365f9d5b0e312ed3959d3f827f869e3c"
"@babel/traverse@^7.7.0":
version "7.12.9"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.9.tgz#fad26c972eabbc11350e0b695978de6cc8e8596f"
integrity sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==
dependencies:
"@babel/code-frame" "^7.8.3"
"@babel/generator" "^7.8.4"
"@babel/helper-function-name" "^7.8.3"
"@babel/helper-split-export-declaration" "^7.8.3"
"@babel/parser" "^7.8.4"
"@babel/types" "^7.8.3"
"@babel/code-frame" "^7.10.4"
"@babel/generator" "^7.12.5"
"@babel/helper-function-name" "^7.10.4"
"@babel/helper-split-export-declaration" "^7.11.0"
"@babel/parser" "^7.12.7"
"@babel/types" "^7.12.7"
debug "^4.1.0"
globals "^11.1.0"
lodash "^4.17.13"
lodash "^4.17.19"
"@babel/types@^7.0.0", "@babel/types@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c"
"@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.7.0":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.7.tgz#6039ff1e242640a29452c9ae572162ec9a8f5d13"
integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==
dependencies:
esutils "^2.0.2"
lodash "^4.17.13"
"@babel/helper-validator-identifier" "^7.10.4"
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@fortawesome/fontawesome-common-types@^0.1.7":
@ -825,13 +853,15 @@ abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
acorn-jsx@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384"
acorn-jsx@^5.2.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
acorn@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c"
acorn@^7.1.1:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
agent-base@4, agent-base@^4.3.0:
version "4.3.0"
@ -865,10 +895,11 @@ ansi-escapes@^3.2.0:
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
ansi-escapes@^4.2.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d"
version "4.3.1"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61"
integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==
dependencies:
type-fest "^0.8.1"
type-fest "^0.11.0"
ansi-regex@^2.0.0:
version "2.1.1"
@ -892,6 +923,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
aproba@^1.0.3, aproba@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@ -998,13 +1036,14 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
babel-eslint@^10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
version "10.1.0"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==
dependencies:
"@babel/code-frame" "^7.0.0"
"@babel/parser" "^7.0.0"
"@babel/traverse" "^7.0.0"
"@babel/types" "^7.0.0"
"@babel/parser" "^7.7.0"
"@babel/traverse" "^7.7.0"
"@babel/types" "^7.7.0"
eslint-visitor-keys "^1.0.0"
resolve "^1.12.0"
@ -1202,6 +1241,14 @@ chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@ -1232,6 +1279,7 @@ cli-cursor@^2.1.0:
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
dependencies:
restore-cursor "^3.1.0"
@ -1239,6 +1287,11 @@ cli-width@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
cli-width@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
cliui@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
@ -1275,10 +1328,22 @@ color-convert@^1.9.0:
dependencies:
color-name "1.1.3"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
columnify@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb"
@ -1447,6 +1512,7 @@ cosmiconfig@^5.1.0:
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
dependencies:
nice-try "^1.0.4"
path-key "^2.0.1"
@ -1530,6 +1596,7 @@ dedent@^0.7.0:
deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
defaults@^1.0.3:
version "1.0.3"
@ -1637,6 +1704,7 @@ emoji-regex@^7.0.1:
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
encoding@^0.1.11:
version "0.1.12"
@ -1699,14 +1767,16 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
eslint-plugin-cypress@^2.11.1:
version "2.11.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.1.tgz#a945e2774b88211e2c706a059d431e262b5c2862"
version "2.11.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.2.tgz#a8f3fe7ec840f55e4cea37671f93293e6c3e76a0"
integrity sha512-1SergF1sGbVhsf7MYfOLiBhdOg6wqyeV9pXUAIDIffYTGMN3dTBQS9nFAzhLsHhO+Bn0GaVM1Ecm71XUidQ7VA==
dependencies:
globals "^11.12.0"
eslint-plugin-prettier@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba"
version "3.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.2.0.tgz#af391b2226fa0e15c96f36c733f6e9035dbd952c"
integrity sha512-kOUSJnFjAUFKwVxuzy6sA5yyMx6+o9ino4gCdShzBNx4eyFRudWRYKCFolKjoM40PEiuU6Cn7wBLfq3WsGg7qg==
dependencies:
prettier-linter-helpers "^1.0.0"
@ -1715,15 +1785,17 @@ eslint-plugin-svelte3@^2.7.3:
resolved "https://registry.yarnpkg.com/eslint-plugin-svelte3/-/eslint-plugin-svelte3-2.7.3.tgz#e793b646b848e717674fe668c21b909cfa025eb3"
eslint-scope@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9"
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
dependencies:
esrecurse "^4.1.0"
esrecurse "^4.3.0"
estraverse "^4.1.1"
eslint-utils@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
dependencies:
eslint-visitor-keys "^1.1.0"
@ -1734,6 +1806,7 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
eslint@^6.8.0:
version "6.8.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.10.0"
@ -1774,11 +1847,12 @@ eslint@^6.8.0:
v8-compile-cache "^2.0.3"
espree@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d"
version "6.2.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==
dependencies:
acorn "^7.1.0"
acorn-jsx "^5.1.0"
acorn "^7.1.1"
acorn-jsx "^5.2.0"
eslint-visitor-keys "^1.1.0"
esprima@^4.0.0:
@ -1786,21 +1860,28 @@ esprima@^4.0.0:
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
esquery@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
version "1.3.1"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==
dependencies:
estraverse "^4.0.0"
estraverse "^5.1.0"
esrecurse@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
esrecurse@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
dependencies:
estraverse "^4.1.0"
estraverse "^5.2.0"
estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1:
estraverse@^4.1.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
estraverse@^5.1.0, estraverse@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
estree-walker@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362"
@ -1913,6 +1994,7 @@ fast-json-stable-stringify@^2.0.0:
fast-levenshtein@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
figgy-pudding@^3.4.1, figgy-pudding@^3.5.1:
version "3.5.1"
@ -1925,8 +2007,9 @@ figures@^2.0.0:
escape-string-regexp "^1.0.5"
figures@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec"
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
dependencies:
escape-string-regexp "^1.0.5"
@ -2235,6 +2318,11 @@ has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-symbols@^1.0.0, has-symbols@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
@ -2422,21 +2510,22 @@ inquirer@^6.2.0:
through "^2.3.6"
inquirer@^7.0.0:
version "7.0.4"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.4.tgz#99af5bde47153abca23f5c7fc30db247f39da703"
version "7.3.3"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
dependencies:
ansi-escapes "^4.2.1"
chalk "^2.4.2"
chalk "^4.1.0"
cli-cursor "^3.1.0"
cli-width "^2.0.0"
cli-width "^3.0.0"
external-editor "^3.0.3"
figures "^3.0.0"
lodash "^4.17.15"
lodash "^4.17.19"
mute-stream "0.0.8"
run-async "^2.2.0"
rxjs "^6.5.3"
run-async "^2.4.0"
rxjs "^6.6.0"
string-width "^4.1.0"
strip-ansi "^5.1.0"
strip-ansi "^6.0.0"
through "^2.3.6"
invert-kv@^2.0.0:
@ -2546,6 +2635,7 @@ is-fullwidth-code-point@^2.0.0:
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-glob@^3.1.0:
version "3.1.0"
@ -2776,6 +2866,7 @@ lerna@3.14.1:
levn@^0.3.0, levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
dependencies:
prelude-ls "~1.1.2"
type-check "~0.3.2"
@ -2877,10 +2968,15 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5, lodash@^4.2.1:
lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5, lodash@^4.2.1:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
lodash@^4.17.19:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
loud-rejection@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
@ -3063,6 +3159,7 @@ mimic-fn@^1.0.0:
mimic-fn@^2.0.0, mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimatch@^3.0.0, minimatch@^3.0.4:
version "3.0.4"
@ -3169,6 +3266,7 @@ mute-stream@0.0.7:
mute-stream@0.0.8, mute-stream@~0.0.4:
version "0.0.8"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
nanomatch@^1.2.9:
version "1.2.13"
@ -3404,8 +3502,9 @@ onetime@^2.0.0:
mimic-fn "^1.0.0"
onetime@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5"
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
dependencies:
mimic-fn "^2.1.0"
@ -3419,6 +3518,7 @@ optimist@^0.6.1:
optionator@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
dependencies:
deep-is "~0.1.3"
fast-levenshtein "~2.0.6"
@ -3696,6 +3796,7 @@ posix-character-classes@^0.1.0:
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
prettier-linter-helpers@^1.0.0:
version "1.0.0"
@ -3704,13 +3805,14 @@ prettier-linter-helpers@^1.0.0:
fast-diff "^1.1.2"
prettier-plugin-svelte@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-1.4.0.tgz#bb992759fb77ec2c3545d454a7c60f7a258cb745"
integrity sha512-KXO2He7Kql0Lz4DdlzVli1j2JTDUR9jPV/DqyfnJmY1pCeSV1qZkxgdsyYma35W6OLrCAr/G6yKdmzo+75u2Ng==
version "1.4.1"
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-1.4.1.tgz#2f0f7a149190f476dc9b4ba9da8d482bd196f1e2"
integrity sha512-6y0m37Xw01GRf/WIHau+Kp3uXj2JB1agtEmNVKb9opMy34A6OMOYhfneVpNIlrghQSw/jIV+t3e5Ngt4up2CMA==
prettier@^1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
process-nextick-args@~2.0.0:
version "2.0.1"
@ -3907,6 +4009,7 @@ regex-not@^1.0.0, regex-not@^1.0.2:
regexpp@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
repeat-element@^1.1.2:
version "1.1.3"
@ -3995,6 +4098,7 @@ restore-cursor@^2.0.0:
restore-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
dependencies:
onetime "^5.1.0"
signal-exit "^3.0.2"
@ -4045,18 +4149,30 @@ run-async@^2.2.0:
dependencies:
is-promise "^2.1.0"
run-async@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
run-queue@^1.0.0, run-queue@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
dependencies:
aproba "^1.1.1"
rxjs@^6.4.0, rxjs@^6.5.3:
rxjs@^6.4.0:
version "6.5.4"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
dependencies:
tslib "^1.9.0"
rxjs@^6.6.0:
version "6.6.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==
dependencies:
tslib "^1.9.0"
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
@ -4082,6 +4198,7 @@ safe-regex@^1.1.0:
semver@^6.0.0, semver@^6.1.2:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@~5.3.0:
version "5.3.0"
@ -4320,6 +4437,7 @@ string-width@^3.0.0:
string-width@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
@ -4400,8 +4518,9 @@ strip-indent@^2.0.0:
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
strip-json-comments@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7"
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
strong-log-transformer@^2.0.0:
version "2.1.0"
@ -4417,6 +4536,13 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
svelte@^3.30.0:
version "3.30.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.30.0.tgz#cbde341e96bf34f4ac73c8f14f8a014e03bfb7d6"
@ -4563,9 +4689,15 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
type-check@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
dependencies:
prelude-ls "~1.1.2"
type-fest@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
type-fest@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
@ -4723,6 +4855,7 @@ windows-release@^3.1.0:
word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
wordwrap@~0.0.2:
version "0.0.3"