Merge pull request #903 from Budibase/feature/security-update
Security Update & Role-Based Screens
This commit is contained in:
commit
afe309d731
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
}
|
||||
.nav-item:hover,
|
||||
.nav-item.selected {
|
||||
border-radius: var(--border-radius-m);
|
||||
border-radius: var(--border-radius-s);
|
||||
}
|
||||
|
||||
.content {
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
overflow: hidden;
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
.component-container iframe {
|
||||
border: 0;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}.`)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
// }
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { routeStore } from "./routes"
|
||||
import { screenStore } from "./screens"
|
||||
|
||||
export async function initialise() {
|
||||
await routeStore.actions.fetchRoutes()
|
||||
await screenStore.actions.fetchScreens()
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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("")
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
});
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
});
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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}`
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
export let logoUrl
|
||||
|
||||
const logOut = () => {
|
||||
authStore.actions.logOut()
|
||||
const logOut = async () => {
|
||||
await authStore.actions.logOut()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
325
yarn.lock
325
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue