Merge branch 'next' into fixes/user-management
This commit is contained in:
commit
0346ef9bb1
|
@ -31,6 +31,14 @@
|
||||||
"created_at": "2021-05-01T05:27:53Z",
|
"created_at": "2021-05-01T05:27:53Z",
|
||||||
"repoId": 190729906,
|
"repoId": 190729906,
|
||||||
"pullRequestNo": 1431
|
"pullRequestNo": 1431
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mjashanks",
|
||||||
|
"id": 3524181,
|
||||||
|
"comment_id": 844846454,
|
||||||
|
"created_at": "2021-05-20T08:14:04Z",
|
||||||
|
"repoId": 190729906,
|
||||||
|
"pullRequestNo": 1510
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -34,6 +34,10 @@ exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||||
exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
|
exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
|
|
||||||
|
function isDevApp(app) {
|
||||||
|
return app.appId.startsWith(exports.APP_DEV_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
||||||
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
|
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
|
||||||
|
@ -160,7 +164,7 @@ exports.getDeployedAppID = appId => {
|
||||||
* different users/companies apps as there is no security around it - all apps are returned.
|
* different users/companies apps as there is no security around it - all apps are returned.
|
||||||
* @return {Promise<object[]>} returns the app information document stored in each app database.
|
* @return {Promise<object[]>} returns the app information document stored in each app database.
|
||||||
*/
|
*/
|
||||||
exports.getAllApps = async (devApps = false) => {
|
exports.getAllApps = async ({ dev, all } = {}) => {
|
||||||
const CouchDB = getCouch()
|
const CouchDB = getCouch()
|
||||||
let allDbs = await CouchDB.allDbs()
|
let allDbs = await CouchDB.allDbs()
|
||||||
const appDbNames = allDbs.filter(dbName =>
|
const appDbNames = allDbs.filter(dbName =>
|
||||||
|
@ -176,12 +180,19 @@ exports.getAllApps = async (devApps = false) => {
|
||||||
const apps = response
|
const apps = response
|
||||||
.filter(result => result.status === "fulfilled")
|
.filter(result => result.status === "fulfilled")
|
||||||
.map(({ value }) => value)
|
.map(({ value }) => value)
|
||||||
return apps.filter(app => {
|
if (!all) {
|
||||||
if (devApps) {
|
return apps.filter(app => {
|
||||||
return app.appId.startsWith(exports.APP_DEV_PREFIX)
|
if (dev) {
|
||||||
}
|
return isDevApp(app)
|
||||||
return !app.appId.startsWith(exports.APP_DEV_PREFIX)
|
}
|
||||||
})
|
return !isDevApp(app)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return apps.map(app => ({
|
||||||
|
...app,
|
||||||
|
status: isDevApp(app) ? "development" : "published",
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,9 @@ exports.isClient = ctx => {
|
||||||
* @return {Promise<object|null>}
|
* @return {Promise<object|null>}
|
||||||
*/
|
*/
|
||||||
exports.getGlobalUserByEmail = async email => {
|
exports.getGlobalUserByEmail = async email => {
|
||||||
|
if (email == null) {
|
||||||
|
throw "Must supply an email address to view"
|
||||||
|
}
|
||||||
const db = getDB(StaticDatabases.GLOBAL.name)
|
const db = getDB(StaticDatabases.GLOBAL.name)
|
||||||
try {
|
try {
|
||||||
let users = (
|
let users = (
|
||||||
|
|
|
@ -45,3 +45,9 @@
|
||||||
</span>
|
</span>
|
||||||
<span class="spectrum-Checkbox-label">{text || ""}</span>
|
<span class="spectrum-Checkbox-label">{text || ""}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Checkbox-input {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -37,9 +37,28 @@
|
||||||
const fieldId = id || generateID()
|
const fieldId = id || generateID()
|
||||||
let selectedImageIdx = 0
|
let selectedImageIdx = 0
|
||||||
let fileDragged = false
|
let fileDragged = false
|
||||||
|
let selectedUrl
|
||||||
$: selectedImage = value?.[selectedImageIdx] ?? null
|
$: selectedImage = value?.[selectedImageIdx] ?? null
|
||||||
$: fileCount = value?.length ?? 0
|
$: fileCount = value?.length ?? 0
|
||||||
$: isImage = imageExtensions.includes(selectedImage?.extension?.toLowerCase())
|
$: isImage =
|
||||||
|
imageExtensions.includes(selectedImage?.extension?.toLowerCase()) ||
|
||||||
|
selectedImage?.type?.startsWith("image")
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (selectedImage?.url) {
|
||||||
|
selectedUrl = selectedImage?.url
|
||||||
|
} else if (selectedImage) {
|
||||||
|
try {
|
||||||
|
let reader = new FileReader()
|
||||||
|
reader.readAsDataURL(selectedImage)
|
||||||
|
reader.onload = e => {
|
||||||
|
selectedUrl = e.target.result
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
selectedUrl = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function processFileList(fileList) {
|
async function processFileList(fileList) {
|
||||||
if (
|
if (
|
||||||
|
@ -102,11 +121,13 @@
|
||||||
<div class="gallery">
|
<div class="gallery">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="filename">{selectedImage.name}</div>
|
<div class="filename">{selectedImage.name}</div>
|
||||||
<div class="filesize">
|
{#if selectedImage.size}
|
||||||
{#if selectedImage.size <= BYTES_IN_MB}
|
<div class="filesize">
|
||||||
{`${selectedImage.size / BYTES_IN_KB} KB`}
|
{#if selectedImage.size <= BYTES_IN_MB}
|
||||||
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if}
|
{`${selectedImage.size / BYTES_IN_KB} KB`}
|
||||||
</div>
|
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if !disabled}
|
{#if !disabled}
|
||||||
<div class="delete-button" on:click={removeFile}>
|
<div class="delete-button" on:click={removeFile}>
|
||||||
<Icon name="Close" />
|
<Icon name="Close" />
|
||||||
|
@ -114,7 +135,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if isImage}
|
{#if isImage}
|
||||||
<img alt="preview" src={selectedImage.url} />
|
<img alt="preview" src={selectedUrl} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="placeholder">
|
<div class="placeholder">
|
||||||
<div class="extension">{selectedImage.extension}</div>
|
<div class="extension">{selectedImage.extension}</div>
|
||||||
|
@ -142,11 +163,13 @@
|
||||||
<div class="gallery">
|
<div class="gallery">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="filename">{file.name}</div>
|
<div class="filename">{file.name}</div>
|
||||||
<div class="filesize">
|
{#if file.size}
|
||||||
{#if file.size <= BYTES_IN_MB}
|
<div class="filesize">
|
||||||
{`${file.size / BYTES_IN_KB} KB`}
|
{#if file.size <= BYTES_IN_MB}
|
||||||
{:else}{`${file.size / BYTES_IN_MB} MB`}{/if}
|
{`${file.size / BYTES_IN_KB} KB`}
|
||||||
</div>
|
{:else}{`${file.size / BYTES_IN_MB} MB`}{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if !disabled}
|
{#if !disabled}
|
||||||
<div class="delete-button" on:click={removeFile}>
|
<div class="delete-button" on:click={removeFile}>
|
||||||
<Icon name="Close" />
|
<Icon name="Close" />
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
import "@spectrum-css/search/dist/index-vars.css"
|
import "@spectrum-css/search/dist/index-vars.css"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let value = ""
|
export let value = null
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let id = null
|
export let id = null
|
||||||
|
export let updateOnChange = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let focus = false
|
let focus = false
|
||||||
|
@ -23,6 +24,13 @@
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onInput = event => {
|
||||||
|
if (!updateOnChange) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
const updateValueOnEnter = event => {
|
const updateValueOnEnter = event => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
|
@ -44,15 +52,18 @@
|
||||||
<use xlink:href="#spectrum-icon-18-Magnify" />
|
<use xlink:href="#spectrum-icon-18-Magnify" />
|
||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
on:click
|
|
||||||
on:keyup={updateValueOnEnter}
|
|
||||||
{disabled}
|
{disabled}
|
||||||
{id}
|
{id}
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
placeholder={placeholder || ""}
|
placeholder={placeholder || ""}
|
||||||
|
on:click
|
||||||
|
on:blur
|
||||||
|
on:focus
|
||||||
|
on:input
|
||||||
on:blur={onBlur}
|
on:blur={onBlur}
|
||||||
on:focus={onFocus}
|
on:focus={onFocus}
|
||||||
on:input
|
on:input={onInput}
|
||||||
|
on:keyup={updateValueOnEnter}
|
||||||
type="search"
|
type="search"
|
||||||
class="spectrum-Textfield-input spectrum-Search-input"
|
class="spectrum-Textfield-input spectrum-Search-input"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
|
@ -26,3 +26,9 @@
|
||||||
<span class="spectrum-Switch-switch" />
|
<span class="spectrum-Switch-switch" />
|
||||||
<label class="spectrum-Switch-label" for={id}>{text}</label>
|
<label class="spectrum-Switch-label" for={id}>{text}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Switch-input {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
import "@spectrum-css/textfield/dist/index-vars.css"
|
import "@spectrum-css/textfield/dist/index-vars.css"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let value = ""
|
export let value = null
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let type = "text"
|
export let type = "text"
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
export let id = null
|
export let id = null
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
|
export let updateOnChange = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let focus = false
|
let focus = false
|
||||||
|
@ -37,7 +38,13 @@
|
||||||
}
|
}
|
||||||
focus = false
|
focus = false
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
dispatch("blur")
|
}
|
||||||
|
|
||||||
|
const onInput = event => {
|
||||||
|
if (readonly || !updateOnChange) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValueOnEnter = event => {
|
const updateValueOnEnter = event => {
|
||||||
|
@ -66,16 +73,19 @@
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
<input
|
<input
|
||||||
on:click
|
|
||||||
on:keyup={updateValueOnEnter}
|
|
||||||
{disabled}
|
{disabled}
|
||||||
{readonly}
|
{readonly}
|
||||||
{id}
|
{id}
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
placeholder={placeholder || ""}
|
placeholder={placeholder || ""}
|
||||||
|
on:click
|
||||||
|
on:blur
|
||||||
|
on:focus
|
||||||
|
on:input
|
||||||
on:blur={onBlur}
|
on:blur={onBlur}
|
||||||
on:focus={onFocus}
|
on:focus={onFocus}
|
||||||
on:input
|
on:input={onInput}
|
||||||
|
on:keyup={updateValueOnEnter}
|
||||||
{type}
|
{type}
|
||||||
class="spectrum-Textfield-input"
|
class="spectrum-Textfield-input"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let error = null
|
export let error = null
|
||||||
|
export let updateOnChange = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
|
|
||||||
<Field {label} {labelPosition} {error}>
|
<Field {label} {labelPosition} {error}>
|
||||||
<TextField
|
<TextField
|
||||||
|
{updateOnChange}
|
||||||
{error}
|
{error}
|
||||||
{disabled}
|
{disabled}
|
||||||
{readonly}
|
{readonly}
|
||||||
|
@ -31,5 +33,6 @@
|
||||||
on:click
|
on:click
|
||||||
on:input
|
on:input
|
||||||
on:blur
|
on:blur
|
||||||
|
on:focus
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
export let labelPosition = "above"
|
export let labelPosition = "above"
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let updateOnChange = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -18,11 +19,14 @@
|
||||||
|
|
||||||
<Field {label} {labelPosition}>
|
<Field {label} {labelPosition}>
|
||||||
<Search
|
<Search
|
||||||
|
{updateOnChange}
|
||||||
{disabled}
|
{disabled}
|
||||||
{value}
|
{value}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
on:input
|
on:input
|
||||||
|
on:focus
|
||||||
|
on:blur
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
.container {
|
.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.paddingX-S {
|
.paddingX-S {
|
||||||
padding-left: var(--spacing-s);
|
padding-left: var(--spacing-s);
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
max-width: 80ch;
|
max-width: 80ch;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: calc(var(--spacing-xl) * 2);
|
padding: calc(var(--spacing-xl) * 2);
|
||||||
|
min-height: calc(100% - var(--spacing-xl) * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wide {
|
.wide {
|
||||||
|
@ -22,5 +23,6 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2)
|
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2)
|
||||||
calc(var(--spacing-xl) * 2) calc(var(--spacing-xl) * 2);
|
calc(var(--spacing-xl) * 2) calc(var(--spacing-xl) * 2);
|
||||||
|
min-height: calc(100% - var(--spacing-xl) * 3);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,27 +3,17 @@
|
||||||
|
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let serif = false
|
export let serif = false
|
||||||
export let noPadding = false
|
export let weight = null
|
||||||
export let textAlign
|
export let textAlign = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
style="{textAlign ? `text-align:${textAlign}` : ``}"
|
style={`
|
||||||
class:noPadding
|
${weight ? `font-weight:${weight};` : ""}
|
||||||
|
${textAlign ? `text-align:${textAlign};` : ""}
|
||||||
|
`}
|
||||||
class="spectrum-Body spectrum-Body--size{size}"
|
class="spectrum-Body spectrum-Body--size{size}"
|
||||||
class:spectrum-Body--serif={serif}
|
class:spectrum-Body--serif={serif}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<style>
|
|
||||||
p {
|
|
||||||
margin-top: 0.75em;
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noPadding {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
style="{textAlign ? `text-align:${textAlign}` : ``}"
|
style={textAlign ? `text-align:${textAlign}` : ``}
|
||||||
class:noPadding
|
class:noPadding
|
||||||
class="spectrum-Heading spectrum-Heading--size{size}"
|
class="spectrum-Heading spectrum-Heading--size{size}"
|
||||||
>
|
>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import CodeEditorModal from "./CodeEditorModal.svelte"
|
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||||
import QuerySelector from "./QuerySelector.svelte"
|
import QuerySelector from "./QuerySelector.svelte"
|
||||||
import QueryParamSelector from "./QueryParamSelector.svelte"
|
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||||
|
import CronBuilder from "./CronBuilder.svelte"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
|
@ -76,6 +77,8 @@
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "query"}
|
{:else if value.customType === "query"}
|
||||||
<QuerySelector bind:value={block.inputs[key]} />
|
<QuerySelector bind:value={block.inputs[key]} />
|
||||||
|
{:else if value.customType === "cron"}
|
||||||
|
<CronBuilder bind:value={block.inputs[key]} />
|
||||||
{:else if value.customType === "queryParams"}
|
{:else if value.customType === "queryParams"}
|
||||||
<QueryParamSelector bind:value={block.inputs[key]} {bindings} />
|
<QueryParamSelector bind:value={block.inputs[key]} {bindings} />
|
||||||
{:else if value.customType === "table"}
|
{:else if value.customType === "table"}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script>
|
||||||
|
import { Button, Select, Label, Heading, Input } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
|
||||||
|
let presets = false
|
||||||
|
|
||||||
|
const CRON_EXPRESSIONS = [
|
||||||
|
{
|
||||||
|
label: "Every Minute",
|
||||||
|
value: "* * * * *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Every Hour",
|
||||||
|
value: "0 * * * *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Every Morning at 8AM",
|
||||||
|
value: "0 8 * * *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Every Night at Midnight",
|
||||||
|
value: "0 0 * * *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Every Budibase Reboot",
|
||||||
|
value: "@reboot",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="block-field">
|
||||||
|
<Input bind:value />
|
||||||
|
|
||||||
|
<div class="presets">
|
||||||
|
<Button on:click={() => (presets = !presets)}
|
||||||
|
>{presets ? "Hide" : "Show"} Presets</Button
|
||||||
|
>
|
||||||
|
{#if presets}
|
||||||
|
<Select
|
||||||
|
bind:value
|
||||||
|
secondary
|
||||||
|
extraThin
|
||||||
|
label="Presets"
|
||||||
|
options={CRON_EXPRESSIONS}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.presets {
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.block-field {
|
||||||
|
padding-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -57,10 +57,12 @@
|
||||||
>
|
>
|
||||||
<CreateColumnButton />
|
<CreateColumnButton />
|
||||||
{#if schema && Object.keys(schema).length > 0}
|
{#if schema && Object.keys(schema).length > 0}
|
||||||
<CreateRowButton
|
{#if !isUsersTable}
|
||||||
title={isUsersTable ? "Create user" : "Create row"}
|
<CreateRowButton
|
||||||
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow}
|
title={"Create row"}
|
||||||
/>
|
modalContentComponent={CreateEditRow}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<CreateViewButton />
|
<CreateViewButton />
|
||||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||||
{#if isUsersTable}
|
{#if isUsersTable}
|
||||||
|
|
|
@ -47,6 +47,8 @@
|
||||||
})
|
})
|
||||||
schema.email.displayName = "Email"
|
schema.email.displayName = "Email"
|
||||||
schema.roleId.displayName = "Role"
|
schema.roleId.displayName = "Role"
|
||||||
|
schema.firstName.displayName = "First Name"
|
||||||
|
schema.lastName.displayName = "Last Name"
|
||||||
if (schema.status) {
|
if (schema.status) {
|
||||||
schema.status.displayName = "Status"
|
schema.status.displayName = "Status"
|
||||||
}
|
}
|
||||||
|
@ -101,7 +103,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="popovers">
|
<div class="popovers">
|
||||||
<slot />
|
<slot />
|
||||||
{#if selectedRows.length > 0}
|
{#if !isUsersTable && selectedRows.length > 0}
|
||||||
<DeleteRowsButton {selectedRows} {deleteRows} />
|
<DeleteRowsButton {selectedRows} {deleteRows} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -114,7 +116,7 @@
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
{rowCount}
|
{rowCount}
|
||||||
bind:selectedRows
|
bind:selectedRows
|
||||||
allowSelectRows={allowEditing}
|
allowSelectRows={allowEditing && !isUsersTable}
|
||||||
allowEditRows={allowEditing}
|
allowEditRows={allowEditing}
|
||||||
allowEditColumns={allowEditing}
|
allowEditColumns={allowEditing}
|
||||||
showAutoColumns={!hideAutocolumns}
|
showAutoColumns={!hideAutocolumns}
|
||||||
|
|
|
@ -32,6 +32,8 @@
|
||||||
delete customSchema["email"]
|
delete customSchema["email"]
|
||||||
delete customSchema["roleId"]
|
delete customSchema["roleId"]
|
||||||
delete customSchema["status"]
|
delete customSchema["status"]
|
||||||
|
delete customSchema["firstName"]
|
||||||
|
delete customSchema["lastName"]
|
||||||
return Object.entries(customSchema)
|
return Object.entries(customSchema)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +69,7 @@
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.success("User saved successfully.")
|
notifications.success("User saved successfully")
|
||||||
rows.save(rowResponse)
|
rows.save(rowResponse)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -84,8 +86,14 @@
|
||||||
readonly={!creating}
|
readonly={!creating}
|
||||||
/>
|
/>
|
||||||
<RowFieldControl
|
<RowFieldControl
|
||||||
meta={{ name: "password", type: "password" }}
|
meta={{ ...tableSchema.firstName, name: "First Name" }}
|
||||||
bind:value={row.password}
|
bind:value={row.firstName}
|
||||||
|
readonly={!creating}
|
||||||
|
/>
|
||||||
|
<RowFieldControl
|
||||||
|
meta={{ ...tableSchema.lastName, name: "Last Name" }}
|
||||||
|
bind:value={row.lastName}
|
||||||
|
readonly={!creating}
|
||||||
/>
|
/>
|
||||||
<!-- Defer rendering this select until roles load, otherwise the initial
|
<!-- Defer rendering this select until roles load, otherwise the initial
|
||||||
selection is always undefined -->
|
selection is always undefined -->
|
||||||
|
@ -97,16 +105,6 @@
|
||||||
getOptionLabel={role => role.name}
|
getOptionLabel={role => role.name}
|
||||||
getOptionValue={role => role._id}
|
getOptionValue={role => role._id}
|
||||||
/>
|
/>
|
||||||
<Select
|
|
||||||
label="Status"
|
|
||||||
bind:value={row.status}
|
|
||||||
options={[
|
|
||||||
{ label: "Active", value: "active" },
|
|
||||||
{ label: "Inactive", value: "inactive" },
|
|
||||||
]}
|
|
||||||
getOptionLabel={status => status.label}
|
|
||||||
getOptionValue={status => status.value}
|
|
||||||
/>
|
|
||||||
{#each customSchemaKeys as [key, meta]}
|
{#each customSchemaKeys as [key, meta]}
|
||||||
{#if !meta.autocolumn}
|
{#if !meta.autocolumn}
|
||||||
<RowFieldControl {meta} bind:value={row[key]} {creating} />
|
<RowFieldControl {meta} bind:value={row[key]} {creating} />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
@ -99,15 +99,19 @@
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{
|
{
|
||||||
label: "Text",
|
label: "Text",
|
||||||
value: "string",
|
value: FIELDS.STRING.type,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Number",
|
label: "Number",
|
||||||
value: "number",
|
value: FIELDS.NUMBER.type,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Date",
|
label: "Date",
|
||||||
value: "datetime",
|
value: FIELDS.DATETIME.type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Options",
|
||||||
|
value: FIELDS.OPTIONS.type,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,7 +3,14 @@
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import { Input, Label, ModalContent, Toggle, Divider } from "@budibase/bbui"
|
import {
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
ModalContent,
|
||||||
|
Toggle,
|
||||||
|
Divider,
|
||||||
|
Layout,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import TableDataImport from "../TableDataImport.svelte"
|
import TableDataImport from "../TableDataImport.svelte"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
import screenTemplates from "builderStore/store/screenTemplates"
|
import screenTemplates from "builderStore/store/screenTemplates"
|
||||||
|
@ -123,8 +130,10 @@
|
||||||
bind:value={createAutoscreens}
|
bind:value={createAutoscreens}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
|
<Layout gap="XS" noPadding>
|
||||||
<TableDataImport bind:dataImport />
|
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
|
||||||
|
<TableDataImport bind:dataImport />
|
||||||
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
FAILURE: "FAILURE",
|
FAILURE: "FAILURE",
|
||||||
}
|
}
|
||||||
|
|
||||||
const POLL_INTERVAL = 1000
|
const POLL_INTERVAL = 10000
|
||||||
|
|
||||||
let loading = false
|
let loading = false
|
||||||
let feedbackModal
|
let feedbackModal
|
||||||
|
@ -61,7 +61,6 @@
|
||||||
|
|
||||||
// Required to check any updated deployment statuses between polls
|
// Required to check any updated deployment statuses between polls
|
||||||
function checkIncomingDeploymentStatus(current, incoming) {
|
function checkIncomingDeploymentStatus(current, incoming) {
|
||||||
console.log(current, incoming)
|
|
||||||
for (let incomingDeployment of incoming) {
|
for (let incomingDeployment of incoming) {
|
||||||
if (
|
if (
|
||||||
incomingDeployment.status === DeploymentStatus.FAILURE ||
|
incomingDeployment.status === DeploymentStatus.FAILURE ||
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
this={control}
|
this={control}
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
value={safeValue}
|
value={safeValue}
|
||||||
|
updateOnChange={false}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
{type}
|
{type}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
|
notifications,
|
||||||
Input,
|
Input,
|
||||||
Button,
|
Button,
|
||||||
Layout,
|
Layout,
|
||||||
Body,
|
Body,
|
||||||
Heading,
|
Heading,
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { auth } from "stores/backend"
|
import { organisation, auth } from "stores/portal"
|
||||||
|
|
||||||
let email = ""
|
let email = ""
|
||||||
|
|
||||||
|
@ -25,17 +25,19 @@
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout noPadding justifyItems="center">
|
<Layout noPadding justifyItems="center">
|
||||||
<img src="https://i.imgur.com/ZKyklgF.png" />
|
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading textAlign="center">Forgotten your password?</Heading>
|
<Heading textAlign="center">Forgotten your password?</Heading>
|
||||||
<Body size="S" textAlign="center">
|
<Body size="S" textAlign="center">
|
||||||
No problem! Just enter your account's email address and we'll send
|
No problem! Just enter your account's email address and we'll send you
|
||||||
you a link to reset it.
|
a link to reset it.
|
||||||
</Body>
|
</Body>
|
||||||
<Input label="Email" bind:value={email} />
|
<Input label="Email" bind:value={email} />
|
||||||
</Layout>
|
</Layout>
|
||||||
<Button cta on:click={forgot} disabled={!email}>Reset your password</Button>
|
<Button cta on:click={forgot} disabled={!email}>
|
||||||
|
Reset your password
|
||||||
|
</Button>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { Link } from "@budibase/bbui"
|
import { ActionButton } from "@budibase/bbui"
|
||||||
import GoogleLogo from "/assets/google-logo.png"
|
import GoogleLogo from "/assets/google-logo.png"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="outer">
|
<ActionButton>
|
||||||
<Link target="_blank" href="/api/admin/auth/google">
|
<a target="_blank" href="/api/admin/auth/google">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<img src={GoogleLogo} alt="google icon" />
|
<img src={GoogleLogo} alt="google icon" />
|
||||||
<p>Sign in with Google</p>
|
<p>Sign in with Google</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</a>
|
||||||
</div>
|
</ActionButton>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.outer {
|
.outer {
|
||||||
|
@ -34,10 +34,8 @@
|
||||||
.inner p {
|
.inner p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.outer :global(a) {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 500;
|
color: inherit;
|
||||||
font-size: var(--font-size-m);
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
Heading,
|
Heading,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import GoogleButton from "./GoogleButton.svelte"
|
import GoogleButton from "./GoogleButton.svelte"
|
||||||
import { auth } from "stores/backend"
|
import { organisation, auth } from "stores/portal"
|
||||||
|
|
||||||
let username = ""
|
let username = ""
|
||||||
let password = ""
|
let password = ""
|
||||||
|
@ -35,12 +35,12 @@
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout noPadding justifyItems="center">
|
<Layout noPadding justifyItems="center">
|
||||||
<img src="https://i.imgur.com/ZKyklgF.png" />
|
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||||
<Heading>Sign in to Budibase</Heading>
|
<Heading>Sign in to Budibase</Heading>
|
||||||
</Layout>
|
</Layout>
|
||||||
<GoogleButton />
|
<GoogleButton />
|
||||||
|
<Divider noGrid />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Divider noGrid />
|
|
||||||
<Body size="S" textAlign="center">Sign in with email</Body>
|
<Body size="S" textAlign="center">Sign in with email</Body>
|
||||||
<Input label="Email" bind:value={username} />
|
<Input label="Email" bind:value={username} />
|
||||||
<Input
|
<Input
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
bind:value={password}
|
bind:value={password}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Button cta on:click={login}>Sign in to Budibase</Button>
|
<Button cta on:click={login}>Sign in to Budibase</Button>
|
||||||
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
||||||
Forgot password?
|
Forgot password?
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Layout, Body, Heading, notifications } from "@budibase/bbui"
|
import { notifications, Button, Layout, Body, Heading } from "@budibase/bbui"
|
||||||
|
import { organisation, auth } from "stores/portal"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||||
import { params, goto } from "@roxi/routify"
|
import { params, goto } from "@roxi/routify"
|
||||||
import { auth } from "stores/backend"
|
|
||||||
|
|
||||||
const resetCode = $params["?code"]
|
const resetCode = $params["?code"]
|
||||||
let password, error
|
let password, error
|
||||||
|
@ -16,7 +16,6 @@
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Unable to reset password")
|
notifications.error("Unable to reset password")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -24,7 +23,7 @@
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout noPadding justifyItems="center">
|
<Layout noPadding justifyItems="center">
|
||||||
<img src="https://i.imgur.com/ZKyklgF.png" />
|
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading textAlign="center">Reset your password</Heading>
|
<Heading textAlign="center">Reset your password</Heading>
|
||||||
|
@ -33,7 +32,9 @@
|
||||||
</Body>
|
</Body>
|
||||||
<PasswordRepeatInput bind:password bind:error />
|
<PasswordRepeatInput bind:password bind:error />
|
||||||
</Layout>
|
</Layout>
|
||||||
<Button cta on:click={reset} disabled={error || !resetCode}>Reset your password</Button>
|
<Button cta on:click={reset} disabled={error || !resetCode}>
|
||||||
|
Reset your password
|
||||||
|
</Button>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,12 +6,9 @@
|
||||||
Layout,
|
Layout,
|
||||||
ActionMenu,
|
ActionMenu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Link,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { gradient } from "actions"
|
import { gradient } from "actions"
|
||||||
import { AppStatus } from "constants"
|
import { auth } from "stores/portal"
|
||||||
import { url } from "@roxi/routify"
|
|
||||||
import { auth } from "stores/backend"
|
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let exportApp
|
export let exportApp
|
||||||
|
@ -25,11 +22,11 @@
|
||||||
<Layout noPadding gap="XS" alignContent="start">
|
<Layout noPadding gap="XS" alignContent="start">
|
||||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Link on:click={() => openApp(app)}>
|
<div class="name" on:click={() => openApp(app)}>
|
||||||
<Heading size="XS">
|
<Heading size="XS">
|
||||||
{app.name}
|
{app.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
</Link>
|
</div>
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right">
|
||||||
<Icon slot="control" name="More" hoverable />
|
<Icon slot="control" name="More" hoverable />
|
||||||
<MenuItem on:click={() => exportApp(app)} icon="Download">
|
<MenuItem on:click={() => exportApp(app)} icon="Download">
|
||||||
|
@ -48,7 +45,7 @@
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
</div>
|
</div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<Body noPadding size="S">
|
<Body size="S">
|
||||||
Edited {Math.floor(1 + Math.random() * 10)} months ago
|
Edited {Math.floor(1 + Math.random() * 10)} months ago
|
||||||
</Body>
|
</Body>
|
||||||
{#if app.lockedBy}
|
{#if app.lockedBy}
|
||||||
|
@ -76,7 +73,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title :global(a) {
|
.name {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
|
@ -1,16 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { gradient } from "actions"
|
import { gradient } from "actions"
|
||||||
import {
|
import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||||
Heading,
|
import { auth } from "stores/portal"
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
ActionMenu,
|
|
||||||
MenuItem,
|
|
||||||
Link,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { AppStatus } from "constants"
|
|
||||||
import { url } from "@roxi/routify"
|
|
||||||
import { auth } from "stores/backend"
|
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let openApp
|
export let openApp
|
||||||
|
@ -23,19 +14,24 @@
|
||||||
|
|
||||||
<div class="title" class:last>
|
<div class="title" class:last>
|
||||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||||
<Link on:click={() => openApp(app)}>
|
<div class="name" on:click={() => openApp(app)}>
|
||||||
<Heading size="XS">
|
<Heading size="XS">
|
||||||
{app.name}
|
{app.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class:last>
|
<div class:last>
|
||||||
Edited {Math.round(Math.random() * 10 + 1)} months ago
|
Edited {Math.round(Math.random() * 10 + 1)} months ago
|
||||||
</div>
|
</div>
|
||||||
<div class:last>
|
<div class:last>
|
||||||
{#if app.lockedBy}
|
{#if app.lockedBy}
|
||||||
<div class="status status--locked-you" />
|
{#if app.lockedBy.email === $auth.user.email}
|
||||||
Locked by {app.lockedBy.email}
|
<div class="status status--locked-you" />
|
||||||
|
Locked by you
|
||||||
|
{:else}
|
||||||
|
<div class="status status--locked-other" />
|
||||||
|
Locked by {app.lockedBy.email}
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="status status--open" />
|
<div class="status status--open" />
|
||||||
Open
|
Open
|
||||||
|
@ -63,7 +59,7 @@
|
||||||
width: 40px;
|
width: 40px;
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
}
|
}
|
||||||
.title :global(a) {
|
.name {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.title :global(h1:hover) {
|
.title :global(h1:hover) {
|
||||||
|
|
|
@ -15,7 +15,14 @@ export const AppStatus = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// fields on the user table that cannot be edited
|
// fields on the user table that cannot be edited
|
||||||
export const UNEDITABLE_USER_FIELDS = ["email", "password", "roleId", "status"]
|
export const UNEDITABLE_USER_FIELDS = [
|
||||||
|
"email",
|
||||||
|
"password",
|
||||||
|
"roleId",
|
||||||
|
"status",
|
||||||
|
"firstName",
|
||||||
|
"lastName",
|
||||||
|
]
|
||||||
|
|
||||||
export const LAYOUT_NAMES = {
|
export const LAYOUT_NAMES = {
|
||||||
MASTER: {
|
MASTER: {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { goto, isActive } from "@roxi/routify"
|
import { isActive, redirect } from "@roxi/routify"
|
||||||
import { auth } from "stores/backend"
|
import { admin, auth } from "stores/portal"
|
||||||
import { admin } from "stores/portal"
|
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
$: hasAdminUser = !!$admin?.checklist?.adminUser
|
$: hasAdminUser = !!$admin?.checklist?.adminUser
|
||||||
|
@ -16,7 +15,7 @@
|
||||||
// Force creation of an admin user if one doesn't exist
|
// Force creation of an admin user if one doesn't exist
|
||||||
$: {
|
$: {
|
||||||
if (loaded && !hasAdminUser) {
|
if (loaded && !hasAdminUser) {
|
||||||
$goto("./admin")
|
$redirect("./admin")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +28,7 @@
|
||||||
!$isActive("./auth") &&
|
!$isActive("./auth") &&
|
||||||
!$isActive("./invite")
|
!$isActive("./invite")
|
||||||
) {
|
) {
|
||||||
$goto("./auth/login")
|
$redirect("./auth/login")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script>
|
||||||
|
import { admin } from "stores/portal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if ($admin?.checklist?.adminUser) {
|
||||||
|
$redirect("../")
|
||||||
|
} else {
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loaded}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
|
@ -9,7 +9,7 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { admin } from "stores/portal"
|
import { admin, organisation } from "stores/portal"
|
||||||
|
|
||||||
let adminUser = {}
|
let adminUser = {}
|
||||||
|
|
||||||
|
@ -32,22 +32,22 @@
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Layout gap="XS">
|
<Layout>
|
||||||
<img src="https://i.imgur.com/ZKyklgF.png" />
|
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||||
</Layout>
|
<Layout gap="XS" justifyItems="center" noPadding>
|
||||||
<div class="center">
|
|
||||||
<Layout gap="XS">
|
|
||||||
<Heading size="M">Create an admin user</Heading>
|
<Heading size="M">Create an admin user</Heading>
|
||||||
<Body size="M"
|
<Body size="M" textAlign="center">
|
||||||
>The admin user has access to everything in Budibase.</Body
|
The admin user has access to everything in Budibase.
|
||||||
>
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Input label="Email" bind:value={adminUser.email} />
|
||||||
|
<Input
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
bind:value={adminUser.password}
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
|
||||||
<Layout gap="XS">
|
|
||||||
<Input label="Email" bind:value={adminUser.email} />
|
|
||||||
<Input label="Password" type="password" bind:value={adminUser.password} />
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="S">
|
|
||||||
<Button cta on:click={save}>Create super admin user</Button>
|
<Button cta on:click={save}>Create super admin user</Button>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,9 +68,6 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
img {
|
img {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
@ -1,19 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
import {
|
import { Button, Icon, ActionGroup, Tabs, Tab } from "@budibase/bbui"
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
ActionGroup,
|
|
||||||
ActionButton,
|
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import SettingsLink from "components/settings/Link.svelte"
|
|
||||||
import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte"
|
|
||||||
import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte"
|
|
||||||
import DeployModal from "components/deploy/DeployModal.svelte"
|
import DeployModal from "components/deploy/DeployModal.svelte"
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import { get } from "builderStore/api"
|
import { get } from "builderStore/api"
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $auth.user}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Body, notifications } from "@budibase/bbui"
|
||||||
|
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
|
let password
|
||||||
|
let error
|
||||||
|
|
||||||
|
const updatePassword = async () => {
|
||||||
|
try {
|
||||||
|
await auth.updateSelf({ ...$auth.user, password })
|
||||||
|
notifications.success("Information updated successfully")
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Failed to update password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
title="Update password"
|
||||||
|
confirmText="Update password"
|
||||||
|
onConfirm={updatePassword}
|
||||||
|
disabled={error || !password}
|
||||||
|
>
|
||||||
|
<Body size="S">Enter your new password below.</Body>
|
||||||
|
<PasswordRepeatInput bind:password bind:error />
|
||||||
|
</ModalContent>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
|
const values = writable({
|
||||||
|
firstName: $auth.user.firstName,
|
||||||
|
lastName: $auth.user.lastName,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateInfo = async () => {
|
||||||
|
try {
|
||||||
|
await auth.updateSelf({ ...$auth.user, ...$values })
|
||||||
|
notifications.success("Information updated successfully")
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Failed to update information")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
title="Update user information"
|
||||||
|
confirmText="Update information"
|
||||||
|
onConfirm={updateInfo}
|
||||||
|
>
|
||||||
|
<Body size="S">
|
||||||
|
Personalise the platform by adding your first name and last name.
|
||||||
|
</Body>
|
||||||
|
<Input bind:value={$values.firstName} label="First name" />
|
||||||
|
<Input bind:value={$values.lastName} label="Last name" />
|
||||||
|
</ModalContent>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $auth.user}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
|
@ -0,0 +1,163 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Heading,
|
||||||
|
Layout,
|
||||||
|
Select,
|
||||||
|
Divider,
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Avatar,
|
||||||
|
Page,
|
||||||
|
Icon,
|
||||||
|
Body,
|
||||||
|
Modal,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { apps, organisation, auth } from "stores/portal"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import { gradient } from "actions"
|
||||||
|
import UpdateUserInfoModal from "./_components/UpdateUserInfoModal.svelte"
|
||||||
|
import ChangePasswordModal from "./_components/ChangePasswordModal.svelte"
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
let userInfoModal
|
||||||
|
let changePasswordModal
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await organisation.init()
|
||||||
|
await apps.load(AppStatus.DEV)
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loaded}
|
||||||
|
<div class="container">
|
||||||
|
<Page>
|
||||||
|
<div class="content">
|
||||||
|
<Layout noPadding>
|
||||||
|
<img src={$organisation.logoUrl} />
|
||||||
|
<div class="info-title">
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Heading size="L">
|
||||||
|
Hey {$auth.user.firstName || $auth.user.email}
|
||||||
|
</Heading>
|
||||||
|
<Body>
|
||||||
|
Welcome to the {$organisation.company} portal. Below you'll find
|
||||||
|
the list of apps that you have access to.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
<ActionMenu align="right">
|
||||||
|
<div slot="control" class="avatar">
|
||||||
|
<Avatar size="M" name="John Doe" />
|
||||||
|
<Icon size="XL" name="ChevronDown" />
|
||||||
|
</div>
|
||||||
|
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
|
||||||
|
Update user information
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon="LockClosed"
|
||||||
|
on:click={() => changePasswordModal.show()}
|
||||||
|
>
|
||||||
|
Update password
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon="UserDeveloper"
|
||||||
|
on:click={() => $goto("../portal")}
|
||||||
|
>
|
||||||
|
Open developer mode
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon="LogOut" on:click={auth.logout}>Log out</MenuItem>
|
||||||
|
</ActionMenu>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
{#if $apps.length}
|
||||||
|
<Heading>Apps</Heading>
|
||||||
|
<div class="group">
|
||||||
|
<Layout gap="S" noPadding>
|
||||||
|
{#each $apps as app, idx (app.appId)}
|
||||||
|
<a class="app" target="_blank" href={`/${app.appId}`}>
|
||||||
|
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||||
|
<div class="app-info">
|
||||||
|
<Heading size="XS">{app.name}</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Edited {Math.round(Math.random() * 10 + 1)} months ago
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<Icon name="ChevronRight" />
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">You don't have access to any apps yet.</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
The apps you have access to will be listed here.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</div>
|
||||||
|
<Modal bind:this={userInfoModal}>
|
||||||
|
<UpdateUserInfoModal />
|
||||||
|
</Modal>
|
||||||
|
<Modal bind:this={changePasswordModal}>
|
||||||
|
<ChangePasswordModal />
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 60px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 40px;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
}
|
||||||
|
.info-title {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
place-items: center;
|
||||||
|
grid-gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
.avatar:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
filter: brightness(110%);
|
||||||
|
}
|
||||||
|
.group {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.app {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
background-color: var(--background);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
align-items: center;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.app:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
transition: background-color 130ms ease-in-out;
|
||||||
|
}
|
||||||
|
.preview {
|
||||||
|
height: 40px;
|
||||||
|
width: 60px;
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,4 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
$goto("./login")
|
$redirect("./login")
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,4 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
$goto("./portal")
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (!$auth.user) {
|
||||||
|
$redirect("./auth/login")
|
||||||
|
} else if ($auth.user.builder?.global) {
|
||||||
|
$redirect("./portal")
|
||||||
|
} else {
|
||||||
|
$redirect("./apps")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
|
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
|
import { users, organisation } from "stores/portal"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||||
import { users } from "stores/portal"
|
|
||||||
|
|
||||||
const inviteCode = $params["?code"]
|
const inviteCode = $params["?code"]
|
||||||
let password, error
|
let password, error
|
||||||
|
@ -23,19 +23,18 @@
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Layout gap="XS">
|
<Layout>
|
||||||
<img src="https://i.imgur.com/ZKyklgF.png" />
|
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||||
</Layout>
|
<Layout gap="XS" justifyItems="center" noPadding>
|
||||||
<Layout gap="XS">
|
<Heading size="M">Accept Invitation</Heading>
|
||||||
<Heading textAlign="center" size="M">Accept Invitation</Heading>
|
<Body textAlign="center" size="M">
|
||||||
<Body textAlign="center" size="S"
|
Please enter a password to set up your user.
|
||||||
>Please enter a password to setup your user.</Body
|
</Body>
|
||||||
>
|
</Layout>
|
||||||
<PasswordRepeatInput bind:error bind:password />
|
<PasswordRepeatInput bind:error bind:password />
|
||||||
</Layout>
|
<Button disabled={error} cta on:click={acceptInvite}>
|
||||||
<Layout gap="S">
|
Accept invite
|
||||||
<Button disabled={error} cta on:click={acceptInvite}>Accept invite</Button
|
</Button>
|
||||||
>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { isActive, goto } from "@roxi/routify"
|
import { isActive, redirect, goto } from "@roxi/routify"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
@ -12,15 +12,14 @@
|
||||||
Modal,
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
||||||
import { organisation } from "stores/portal"
|
import { organisation, auth } from "stores/portal"
|
||||||
import { auth } from "stores/backend"
|
|
||||||
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
|
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let oldSettingsModal
|
let oldSettingsModal
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
organisation.init()
|
const menu = [
|
||||||
|
|
||||||
let menu = [
|
|
||||||
{ title: "Apps", href: "/builder/portal/apps" },
|
{ title: "Apps", href: "/builder/portal/apps" },
|
||||||
{ title: "Drafts", href: "/builder/portal/drafts" },
|
{ title: "Drafts", href: "/builder/portal/drafts" },
|
||||||
{ title: "Users", href: "/builder/portal/manage/users", heading: "Manage" },
|
{ title: "Users", href: "/builder/portal/manage/users", heading: "Manage" },
|
||||||
|
@ -35,54 +34,69 @@
|
||||||
{ title: "Theming", href: "/builder/portal/theming" },
|
{ title: "Theming", href: "/builder/portal/theming" },
|
||||||
{ title: "Account", href: "/builder/portal/account" },
|
{ title: "Account", href: "/builder/portal/account" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
// Prevent non-builders from accessing the portal
|
||||||
|
if (!$auth.user?.builder?.global) {
|
||||||
|
$redirect("../")
|
||||||
|
} else {
|
||||||
|
await organisation.init()
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
{#if loaded}
|
||||||
<div class="nav">
|
<div class="container">
|
||||||
<Layout paddingX="L" paddingY="L">
|
<div class="nav">
|
||||||
<div class="branding">
|
<Layout paddingX="L" paddingY="L">
|
||||||
<div class="name" on:click={() => $goto("./apps")}>
|
<div class="branding">
|
||||||
<img
|
<div class="name" on:click={() => $goto("./apps")}>
|
||||||
src={$organisation?.logoUrl || "https://i.imgur.com/ZKyklgF.png"}
|
<img
|
||||||
alt="Logotype"
|
src={$organisation?.logoUrl || "https://i.imgur.com/ZKyklgF.png"}
|
||||||
/>
|
alt="Logotype"
|
||||||
<span>{$organisation?.company || "Budibase"}</span>
|
/>
|
||||||
|
<span>{$organisation?.company || "Budibase"}</span>
|
||||||
|
</div>
|
||||||
|
<div class="onboarding">
|
||||||
|
<ConfigChecklist />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="onboarding">
|
<div class="menu">
|
||||||
<ConfigChecklist />
|
<Navigation>
|
||||||
|
{#each menu as { title, href, heading }}
|
||||||
|
<Item selected={$isActive(href)} {href} {heading}>{title}</Item>
|
||||||
|
{/each}
|
||||||
|
</Navigation>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
<div class="menu">
|
|
||||||
<Navigation>
|
|
||||||
{#each menu as { title, href, heading }}
|
|
||||||
<Item selected={$isActive(href)} {href} {heading}>{title}</Item>
|
|
||||||
{/each}
|
|
||||||
</Navigation>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
<div class="main">
|
|
||||||
<div class="toolbar">
|
|
||||||
<Search placeholder="Global search" />
|
|
||||||
<ActionMenu align="right">
|
|
||||||
<div slot="control" class="avatar">
|
|
||||||
<Avatar size="M" name="John Doe" />
|
|
||||||
<Icon size="XL" name="ChevronDown" />
|
|
||||||
</div>
|
|
||||||
<MenuItem icon="Settings" on:click={oldSettingsModal.show}>
|
|
||||||
Old settings
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem icon="LogOut" on:click={auth.logout}>Log out</MenuItem>
|
|
||||||
</ActionMenu>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="main">
|
||||||
<slot />
|
<div class="toolbar">
|
||||||
|
<Search placeholder="Global search" />
|
||||||
|
<ActionMenu align="right">
|
||||||
|
<div slot="control" class="avatar">
|
||||||
|
<Avatar size="M" name="John Doe" />
|
||||||
|
<Icon size="XL" name="ChevronDown" />
|
||||||
|
</div>
|
||||||
|
<MenuItem icon="Settings" on:click={oldSettingsModal.show}>
|
||||||
|
Old settings
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
|
||||||
|
Close developer mode
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon="LogOut" on:click={auth.logout}>Log out</MenuItem>
|
||||||
|
</ActionMenu>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Modal bind:this={oldSettingsModal} width="30%">
|
||||||
<Modal bind:this={oldSettingsModal} width="30%">
|
<BuilderSettingsModal />
|
||||||
<BuilderSettingsModal />
|
</Modal>
|
||||||
</Modal>
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
import api, { del } from "builderStore/api"
|
import api, { del } from "builderStore/api"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { apps } from "stores/portal"
|
import { apps, auth } from "stores/portal"
|
||||||
import { auth } from "stores/backend"
|
|
||||||
import download from "downloadjs"
|
import download from "downloadjs"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
@ -99,8 +98,7 @@
|
||||||
if (!appToDelete) {
|
if (!appToDelete) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await del(`/api/applications/${appToDelete?.appId}`)
|
||||||
await del(`/api/applications/${appToDelete?._id}`)
|
|
||||||
await apps.load()
|
await apps.load()
|
||||||
appToDelete = null
|
appToDelete = null
|
||||||
notifications.success("App deleted successfully.")
|
notifications.success("App deleted successfully.")
|
||||||
|
@ -160,7 +158,7 @@
|
||||||
/>
|
/>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
</div>
|
</div>
|
||||||
{#if $apps.length}
|
{#if loaded && $apps.length}
|
||||||
<div
|
<div
|
||||||
class:appGrid={layout === "grid"}
|
class:appGrid={layout === "grid"}
|
||||||
class:appTable={layout === "table"}
|
class:appTable={layout === "table"}
|
||||||
|
@ -230,7 +228,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.select {
|
.select {
|
||||||
width: 120px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appGrid {
|
.appGrid {
|
||||||
|
@ -248,7 +246,7 @@
|
||||||
height: 70px;
|
height: 70px;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xl);
|
grid-gap: var(--spacing-xl);
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
$goto("./apps")
|
$redirect("./apps")
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -59,44 +59,42 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page>
|
<Layout>
|
||||||
<Layout noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<div>
|
<Heading size="M">OAuth</Heading>
|
||||||
<Heading size="M">OAuth</Heading>
|
<Body>
|
||||||
<Body>
|
Every budibase app comes with basic authentication (email/password)
|
||||||
Every budibase app comes with basic authentication (email/password)
|
included. You can add additional authentication methods from the options
|
||||||
included. You can add additional authentication methods from the options
|
below.
|
||||||
below.
|
</Body>
|
||||||
</Body>
|
</Layout>
|
||||||
</div>
|
{#if google}
|
||||||
<Divider />
|
<Divider />
|
||||||
{#if google}
|
<Layout gap="XS" noPadding>
|
||||||
<div>
|
<Heading size="S">
|
||||||
<Heading size="S">
|
<span>
|
||||||
<span>
|
<GoogleLogo />
|
||||||
<GoogleLogo />
|
Google
|
||||||
Google
|
</span>
|
||||||
</span>
|
</Heading>
|
||||||
</Heading>
|
<Body size="S">
|
||||||
<Body>
|
To allow users to authenticate using their Google accounts, fill out the
|
||||||
To allow users to authenticate using their Google accounts, fill out
|
fields below.
|
||||||
the fields below.
|
</Body>
|
||||||
</Body>
|
</Layout>
|
||||||
</div>
|
<Layout gap="XS" noPadding>
|
||||||
|
|
||||||
{#each ConfigFields.Google as field}
|
{#each ConfigFields.Google as field}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label size="L">{field}</Label>
|
<Label size="L">{field}</Label>
|
||||||
<Input bind:value={google.config[field]} />
|
<Input bind:value={google.config[field]} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div>
|
</Layout>
|
||||||
<Button primary on:click={() => save(google)}>Save</Button>
|
<div>
|
||||||
</div>
|
<Button cta on:click={() => save(google)}>Save</Button>
|
||||||
<Divider />
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</Page>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.form-row {
|
.form-row {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
{#each bindings as binding}
|
{#each bindings as binding}
|
||||||
<MenuItem on:click={() => onBindingClick(binding)}>
|
<MenuItem on:click={() => onBindingClick(binding)}>
|
||||||
<Detail size="M">{binding.name}</Detail>
|
<Detail size="M">{binding.name}</Detail>
|
||||||
<Body size="XS" noPadding>{binding.description}</Body>
|
<Body size="XS">{binding.description}</Body>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{/each}
|
{/each}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
|
@ -102,59 +102,57 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page>
|
<Layout>
|
||||||
<header>
|
<Layout noPadding gap="XS">
|
||||||
<Heading size="M">Email</Heading>
|
<Heading size="M">Email</Heading>
|
||||||
<Body size="S">
|
<Body>
|
||||||
Sending email is not required, but highly recommended for processes such
|
Sending email is not required, but highly recommended for processes such
|
||||||
as password recovery. To setup automated auth emails, simply add the
|
as password recovery. To setup automated auth emails, simply add the
|
||||||
values below and click activate.
|
values below and click activate.
|
||||||
</Body>
|
</Body>
|
||||||
</header>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#if smtpConfig}
|
{#if smtpConfig}
|
||||||
<div class="config-form">
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">SMTP</Heading>
|
<Heading size="S">SMTP</Heading>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
To allow your app to benefit from automated auth emails, add your SMTP
|
To allow your app to benefit from automated auth emails, add your SMTP
|
||||||
details below.
|
details below.
|
||||||
</Body>
|
</Body>
|
||||||
<Layout gap="S">
|
</Layout>
|
||||||
<Heading size="S">
|
<Layout gap="XS" noPadding>
|
||||||
<span />
|
<div class="form-row">
|
||||||
</Heading>
|
<Label size="L">Host</Label>
|
||||||
<div class="form-row">
|
<Input bind:value={smtpConfig.config.host} />
|
||||||
<Label>Host</Label>
|
</div>
|
||||||
<Input bind:value={smtpConfig.config.host} />
|
<div class="form-row">
|
||||||
</div>
|
<Label size="L">Port</Label>
|
||||||
<div class="form-row">
|
<Input type="number" bind:value={smtpConfig.config.port} />
|
||||||
<Label>Port</Label>
|
</div>
|
||||||
<Input type="number" bind:value={smtpConfig.config.port} />
|
<div class="form-row">
|
||||||
</div>
|
<Label size="L">User</Label>
|
||||||
<div class="form-row">
|
<Input bind:value={smtpConfig.config.auth.user} />
|
||||||
<Label>User</Label>
|
</div>
|
||||||
<Input bind:value={smtpConfig.config.auth.user} />
|
<div class="form-row">
|
||||||
</div>
|
<Label size="L">Password</Label>
|
||||||
<div class="form-row">
|
<Input type="password" bind:value={smtpConfig.config.auth.pass} />
|
||||||
<Label>Password</Label>
|
</div>
|
||||||
<Input type="password" bind:value={smtpConfig.config.auth.pass} />
|
<div class="form-row">
|
||||||
</div>
|
<Label size="L">From email address</Label>
|
||||||
<div class="form-row">
|
<Input type="email" bind:value={smtpConfig.config.from} />
|
||||||
<Label>From email address</Label>
|
</div>
|
||||||
<Input type="email" bind:value={smtpConfig.config.from} />
|
</Layout>
|
||||||
</div>
|
<div>
|
||||||
</Layout>
|
|
||||||
<Button cta on:click={saveSmtp}>Save</Button>
|
<Button cta on:click={saveSmtp}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
<div class="config-form">
|
|
||||||
<Heading size="S">Templates</Heading>
|
<Heading size="S">Templates</Heading>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Budibase comes out of the box with ready-made email templates to help
|
Budibase comes out of the box with ready-made email templates to help
|
||||||
with user onboarding. Please refrain from changing the links.
|
with user onboarding. Please refrain from changing the links.
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</Layout>
|
||||||
<Table
|
<Table
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
data={$email.templates}
|
data={$email.templates}
|
||||||
|
@ -165,27 +163,13 @@
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Page>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.config-form {
|
|
||||||
margin-top: 42px;
|
|
||||||
margin-bottom: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
.form-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20% 1fr;
|
grid-template-columns: 25% 1fr;
|
||||||
grid-gap: var(--spacing-l);
|
grid-gap: var(--spacing-l);
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
margin-bottom: 42px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 268 268"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<g clip-path="url(#clip0)">
|
|
||||||
<path
|
|
||||||
d="M58.8037 109.043C64.0284 93.2355 74.1116 79.4822 87.615 69.7447C101.118
|
|
||||||
60.0073 117.352 54.783 134 54.8172C152.872 54.8172 169.934 61.5172 183.334
|
|
||||||
72.4828L222.328 33.5C198.566 12.7858 168.114 0 134 0C81.1817 0 35.711
|
|
||||||
30.1277 13.8467 74.2583L58.8037 109.043Z"
|
|
||||||
fill="#EA4335"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M179.113 201.145C166.942 208.995 151.487 213.183 134 213.183C117.418
|
|
||||||
213.217 101.246 208.034 87.7727 198.369C74.2993 188.703 64.2077 175.044
|
|
||||||
58.9265 159.326L13.8132 193.574C24.8821 215.978 42.012 234.828 63.2572
|
|
||||||
247.984C84.5024 261.14 109.011 268.075 134 268C166.752 268 198.041 256.353
|
|
||||||
221.48 234.5L179.125 201.145H179.113Z"
|
|
||||||
fill="#34A853"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M221.48 234.5C245.991 211.631 261.903 177.595 261.903 134C261.903
|
|
||||||
126.072 260.686 117.552 258.866 109.634H134V161.414H205.869C202.329
|
|
||||||
178.823 192.804 192.301 179.125 201.145L221.48 234.5Z"
|
|
||||||
fill="#4A90E2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M58.9265 159.326C56.1947 151.162 54.8068 142.609 54.8172 134C54.8172
|
|
||||||
125.268 56.213 116.882 58.8037 109.043L13.8467 74.2584C4.64957 92.825
|
|
||||||
-0.0915078 113.28 1.86708e-05 134C1.86708e-05 155.44 4.96919 175.652
|
|
||||||
13.8132 193.574L58.9265 159.326Z"
|
|
||||||
fill="#FBBC05"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0">
|
|
||||||
<rect width="268" height="268" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,115 +0,0 @@
|
||||||
<script>
|
|
||||||
import GoogleLogo from "./_logos/Google.svelte"
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Heading,
|
|
||||||
Divider,
|
|
||||||
Label,
|
|
||||||
notifications,
|
|
||||||
Layout,
|
|
||||||
Input,
|
|
||||||
Body,
|
|
||||||
Page,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import api from "builderStore/api"
|
|
||||||
|
|
||||||
const ConfigTypes = {
|
|
||||||
Google: "google",
|
|
||||||
// Github: "github",
|
|
||||||
// AzureAD: "ad",
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfigFields = {
|
|
||||||
Google: ["clientID", "clientSecret", "callbackURL"],
|
|
||||||
}
|
|
||||||
|
|
||||||
let google
|
|
||||||
|
|
||||||
async function save(doc) {
|
|
||||||
try {
|
|
||||||
// Save an oauth config
|
|
||||||
const response = await api.post(`/api/admin/configs`, doc)
|
|
||||||
const json = await response.json()
|
|
||||||
if (response.status !== 200) throw new Error(json.message)
|
|
||||||
google._rev = json._rev
|
|
||||||
google._id = json._id
|
|
||||||
|
|
||||||
notifications.success(`Settings saved.`)
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error(`Failed to update OAuth settings. ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// fetch the configs for oauth
|
|
||||||
const googleResponse = await api.get(
|
|
||||||
`/api/admin/configs/${ConfigTypes.Google}`
|
|
||||||
)
|
|
||||||
const googleDoc = await googleResponse.json()
|
|
||||||
|
|
||||||
if (!googleDoc._id) {
|
|
||||||
google = {
|
|
||||||
type: ConfigTypes.Google,
|
|
||||||
config: {},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
google = googleDoc
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Page>
|
|
||||||
<header>
|
|
||||||
<Heading size="M">OAuth</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
Every budibase app comes with basic authentication (email/password)
|
|
||||||
included. You can add additional authentication methods from the options
|
|
||||||
below.
|
|
||||||
</Body>
|
|
||||||
</header>
|
|
||||||
<Divider />
|
|
||||||
{#if google}
|
|
||||||
<div class="config-form">
|
|
||||||
<Layout gap="S">
|
|
||||||
<Heading size="S">
|
|
||||||
<span>
|
|
||||||
<GoogleLogo />
|
|
||||||
Google
|
|
||||||
</span>
|
|
||||||
</Heading>
|
|
||||||
{#each ConfigFields.Google as field}
|
|
||||||
<div class="form-row">
|
|
||||||
<Label>{field}</Label>
|
|
||||||
<Input bind:value={google.config[field]} />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</Layout>
|
|
||||||
<Button primary on:click={() => save(google)}>Save</Button>
|
|
||||||
</div>
|
|
||||||
<Divider />
|
|
||||||
{/if}
|
|
||||||
</Page>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.config-form {
|
|
||||||
margin-top: 42px;
|
|
||||||
margin-bottom: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 20% 1fr;
|
|
||||||
grid-gap: var(--spacing-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
margin-bottom: 42px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -64,29 +64,31 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openUpdateRolesModal({ detail }) {
|
async function openUpdateRolesModal({ detail }) {
|
||||||
console.log(detail)
|
|
||||||
selectedApp = detail
|
selectedApp = detail
|
||||||
editRolesModal.show()
|
editRolesModal.show()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding>
|
||||||
<div class="back">
|
<Layout gap="XS" noPadding>
|
||||||
<ActionButton on:click={() => $goto("./")} quiet size="S" icon="BackAndroid"
|
<div class="back">
|
||||||
>Back to users</ActionButton
|
<ActionButton
|
||||||
>
|
on:click={() => $goto("./")}
|
||||||
</div>
|
quiet
|
||||||
<div class="heading">
|
size="S"
|
||||||
<Layout noPadding gap="XS">
|
icon="BackAndroid"
|
||||||
<Heading>User: {$userFetch?.data?.email}</Heading>
|
>
|
||||||
<Body
|
Back to users
|
||||||
>Change user settings and update their app roles. Also contains the
|
</ActionButton>
|
||||||
ability to delete the user as well as force reset their password.
|
</div>
|
||||||
</Body>
|
<Heading>User: {$userFetch?.data?.email}</Heading>
|
||||||
</Layout>
|
<Body>
|
||||||
</div>
|
Change user settings and update their app roles. Also contains the ability
|
||||||
|
to delete the user as well as force reset their password..
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
<div class="general">
|
<Layout gap="S" noPadding>
|
||||||
<Heading size="S">General</Heading>
|
<Heading size="S">General</Heading>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
@ -97,6 +99,14 @@
|
||||||
<Label size="L">Group(s)</Label>
|
<Label size="L">Group(s)</Label>
|
||||||
<Select disabled options={["All users"]} value="All users" />
|
<Select disabled options={["All users"]} value="All users" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">First name</Label>
|
||||||
|
<Input disabled thin value={$userFetch?.data?.firstName} />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Last name</Label>
|
||||||
|
<Input disabled thin value={$userFetch?.data?.lastName} />
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Development access?</Label>
|
<Label size="L">Development access?</Label>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
@ -115,9 +125,9 @@
|
||||||
on:click={resetPasswordModal.show}>Force password reset</ActionButton
|
on:click={resetPasswordModal.show}>Force password reset</ActionButton
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
<div class="roles">
|
<Layout gap="S" noPadding>
|
||||||
<Heading size="S">Configure roles</Heading>
|
<Heading size="S">Configure roles</Heading>
|
||||||
<Table
|
<Table
|
||||||
on:click={openUpdateRolesModal}
|
on:click={openUpdateRolesModal}
|
||||||
|
@ -128,16 +138,14 @@
|
||||||
allowSelectRows={false}
|
allowSelectRows={false}
|
||||||
customRenderers={[{ column: "role", component: TagsRenderer }]}
|
customRenderers={[{ column: "role", component: TagsRenderer }]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Layout>
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
<div class="delete">
|
<Layout gap="XS" noPadding>
|
||||||
<Layout gap="S" noPadding
|
<Heading size="S">Delete user</Heading>
|
||||||
><Heading size="S">Delete user</Heading>
|
<Body>Deleting a user completely removes them from your account.</Body>
|
||||||
<Body>Deleting a user completely removes them from your account.</Body>
|
</Layout>
|
||||||
<div class="delete-button">
|
<div class="delete-button">
|
||||||
<Button warning on:click={deleteUserModal.show}>Delete user</Button>
|
<Button warning on:click={deleteUserModal.show}>Delete user</Button>
|
||||||
</div></Layout
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
@ -150,10 +158,9 @@
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
>
|
>
|
||||||
<Body
|
<Body>
|
||||||
>Are you sure you want to delete <strong>{$userFetch?.data?.email}</strong
|
Are you sure you want to delete <strong>{$userFetch?.data?.email}</strong>
|
||||||
></Body
|
</Body>
|
||||||
>
|
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal bind:this={editRolesModal}>
|
<Modal bind:this={editRolesModal}>
|
||||||
|
@ -174,26 +181,12 @@
|
||||||
.fields {
|
.fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-m);
|
grid-gap: var(--spacing-m);
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
}
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32% 1fr;
|
grid-template-columns: 32% 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.heading {
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.general {
|
|
||||||
position: relative;
|
|
||||||
margin: var(--spacing-xl) 0;
|
|
||||||
}
|
|
||||||
.roles {
|
|
||||||
margin: var(--spacing-xl) 0;
|
|
||||||
}
|
|
||||||
.delete {
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.regenerate {
|
.regenerate {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
|
@ -30,18 +30,18 @@
|
||||||
<ModalContent
|
<ModalContent
|
||||||
onConfirm={createUserFlow}
|
onConfirm={createUserFlow}
|
||||||
size="M"
|
size="M"
|
||||||
title="Add new user options"
|
title="Add new user"
|
||||||
confirmText="Add user"
|
confirmText="Add user"
|
||||||
confirmDisabled={disabled}
|
confirmDisabled={disabled}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
disabled={$error}
|
disabled={$error}
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
>
|
>
|
||||||
<Body noPadding
|
<Body size="S">
|
||||||
>If you have SMTP configured and an email for the new user, you can use the
|
If you have SMTP configured and an email for the new user, you can use the
|
||||||
automated email onboarding flow. Otherwise, use our basic onboarding process
|
automated email onboarding flow. Otherwise, use our basic onboarding process
|
||||||
with autogenerated passwords.</Body
|
with autogenerated passwords.
|
||||||
>
|
</Body>
|
||||||
<Select
|
<Select
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
bind:value={selected}
|
bind:value={selected}
|
||||||
|
|
|
@ -26,10 +26,10 @@
|
||||||
error={$touched && $error}
|
error={$touched && $error}
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
>
|
>
|
||||||
<Body noPadding
|
<Body size="S">
|
||||||
>Below you will find the user’s username and password. The password will not
|
Below you will find the user’s username and password. The password will not
|
||||||
be accessible from this point. Please download the credentials.</Body
|
be accessible from this point. Please save the credentials.
|
||||||
>
|
</Body>
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
label="Username"
|
label="Username"
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
|
|
||||||
const displayLimit = 5
|
const displayLimit = 5
|
||||||
|
|
||||||
$: tags = value?.slice(0, displayLimit) ?? []
|
$: roles = value?.filter(role => role != null) ?? []
|
||||||
$: leftover = (value?.length ?? 0) - tags.length
|
$: tags = roles.slice(0, displayLimit)
|
||||||
|
$: leftover = roles.length - tags.length
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tags>
|
<Tags>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
const roles = app.roles
|
const roles = app.roles
|
||||||
let options = roles.map(role => role._id)
|
let options = roles.map(role => role._id)
|
||||||
let selectedRole
|
let selectedRole = user?.roles?.[app?._id]
|
||||||
|
|
||||||
async function updateUserRoles() {
|
async function updateUserRoles() {
|
||||||
const res = await users.save({
|
const res = await users.save({
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
if (res.status === 400) {
|
if (res.status === 400) {
|
||||||
notifications.error("Failed to update role")
|
notifications.error("Failed to update role")
|
||||||
} else {
|
} else {
|
||||||
notifications.success("Roles updated")
|
notifications.success("Role updated")
|
||||||
dispatch("update")
|
dispatch("update")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,20 +31,20 @@
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
onConfirm={updateUserRoles}
|
onConfirm={updateUserRoles}
|
||||||
title="Update App Roles"
|
title="Update App Role"
|
||||||
confirmText="Update roles"
|
confirmText="Update role"
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
size="M"
|
size="M"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
>
|
>
|
||||||
<Body noPadding
|
<Body>
|
||||||
>Update {user.email}'s roles for <strong>{app.name}</strong>.</Body
|
Update {user.email}'s role for <strong>{app.name}</strong>.
|
||||||
>
|
</Body>
|
||||||
<Select
|
<Select
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
bind:value={selectedRole}
|
bind:value={selectedRole}
|
||||||
on:change
|
on:change
|
||||||
{options}
|
{options}
|
||||||
label="Select roles:"
|
label="Role"
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -48,28 +48,27 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<div class="heading">
|
<Layout gap="XS" noPadding>
|
||||||
<Heading>Users</Heading>
|
<Heading>Users</Heading>
|
||||||
<Body
|
<Body>
|
||||||
>Users are the common denominator in Budibase. Each user is assigned to a
|
Users are the common denominator in Budibase. Each user is assigned to a
|
||||||
group that contains apps and permissions. In this section, you can add
|
group that contains apps and permissions. In this section, you can add
|
||||||
users, or edit and delete an existing user.</Body
|
users, or edit and delete an existing user.
|
||||||
>
|
</Body>
|
||||||
</div>
|
</Layout>
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
|
<Layout gap="S" noPadding>
|
||||||
<div class="users">
|
<div class="users-heading">
|
||||||
<Heading size="S">Users</Heading>
|
<Heading size="S">Users</Heading>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button disabled secondary>Import users</Button>
|
||||||
|
<Button primary on:click={createUserModal.show}>Add user</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Search / filter</Label>
|
<Label size="L">Search / filter</Label>
|
||||||
<Search bind:value={search} placeholder="" />
|
<Search bind:value={search} placeholder="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
|
||||||
<ButtonGroup>
|
|
||||||
<Button disabled secondary>Import users</Button>
|
|
||||||
<Button overBackground on:click={createUserModal.show}>Add user</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</div>
|
|
||||||
<Table
|
<Table
|
||||||
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
||||||
{schema}
|
{schema}
|
||||||
|
@ -79,31 +78,28 @@
|
||||||
allowSelectRows={false}
|
allowSelectRows={false}
|
||||||
customRenderers={[{ column: "group", component: TagsRenderer }]}
|
customRenderers={[{ column: "group", component: TagsRenderer }]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={createUserModal}
|
<Modal bind:this={createUserModal}>
|
||||||
><AddUserModal on:change={openBasicOnoboardingModal} /></Modal
|
<AddUserModal on:change={openBasicOnoboardingModal} />
|
||||||
>
|
</Modal>
|
||||||
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal>
|
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.users {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.field {
|
.field {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
grid-gap: var(--spacing-m);
|
grid-gap: var(--spacing-m);
|
||||||
margin: var(--spacing-xl) 0;
|
|
||||||
}
|
}
|
||||||
.field > :global(*) + :global(*) {
|
.field > :global(*) + :global(*) {
|
||||||
margin-left: var(--spacing-m);
|
margin-left: var(--spacing-m);
|
||||||
}
|
}
|
||||||
.buttons {
|
.users-heading {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: 0;
|
flex-direction: row;
|
||||||
right: 0;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 268 268"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<g clip-path="url(#clip0)">
|
|
||||||
<path
|
|
||||||
d="M58.8037 109.043C64.0284 93.2355 74.1116 79.4822 87.615 69.7447C101.118
|
|
||||||
60.0073 117.352 54.783 134 54.8172C152.872 54.8172 169.934 61.5172 183.334
|
|
||||||
72.4828L222.328 33.5C198.566 12.7858 168.114 0 134 0C81.1817 0 35.711
|
|
||||||
30.1277 13.8467 74.2583L58.8037 109.043Z"
|
|
||||||
fill="#EA4335"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M179.113 201.145C166.942 208.995 151.487 213.183 134 213.183C117.418
|
|
||||||
213.217 101.246 208.034 87.7727 198.369C74.2993 188.703 64.2077 175.044
|
|
||||||
58.9265 159.326L13.8132 193.574C24.8821 215.978 42.012 234.828 63.2572
|
|
||||||
247.984C84.5024 261.14 109.011 268.075 134 268C166.752 268 198.041 256.353
|
|
||||||
221.48 234.5L179.125 201.145H179.113Z"
|
|
||||||
fill="#34A853"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M221.48 234.5C245.991 211.631 261.903 177.595 261.903 134C261.903
|
|
||||||
126.072 260.686 117.552 258.866 109.634H134V161.414H205.869C202.329
|
|
||||||
178.823 192.804 192.301 179.125 201.145L221.48 234.5Z"
|
|
||||||
fill="#4A90E2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M58.9265 159.326C56.1947 151.162 54.8068 142.609 54.8172 134C54.8172
|
|
||||||
125.268 56.213 116.882 58.8037 109.043L13.8467 74.2584C4.64957 92.825
|
|
||||||
-0.0915078 113.28 1.86708e-05 134C1.86708e-05 155.44 4.96919 175.652
|
|
||||||
13.8132 193.574L58.9265 159.326Z"
|
|
||||||
fill="#FBBC05"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0">
|
|
||||||
<rect width="268" height="268" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,114 +0,0 @@
|
||||||
<script>
|
|
||||||
import GoogleLogo from "./_logos/Google.svelte"
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Page,
|
|
||||||
Heading,
|
|
||||||
Divider,
|
|
||||||
Label,
|
|
||||||
notifications,
|
|
||||||
Layout,
|
|
||||||
Input,
|
|
||||||
Body,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import api from "builderStore/api"
|
|
||||||
|
|
||||||
const ConfigTypes = {
|
|
||||||
Google: "google",
|
|
||||||
// Github: "github",
|
|
||||||
// AzureAD: "ad",
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfigFields = {
|
|
||||||
Google: ["clientID", "clientSecret", "callbackURL"],
|
|
||||||
}
|
|
||||||
|
|
||||||
let google
|
|
||||||
|
|
||||||
async function save(doc) {
|
|
||||||
try {
|
|
||||||
// Save an oauth config
|
|
||||||
const response = await api.post(`/api/admin/configs`, doc)
|
|
||||||
const json = await response.json()
|
|
||||||
if (response.status !== 200) throw new Error(json.message)
|
|
||||||
google._rev = json._rev
|
|
||||||
google._id = json._id
|
|
||||||
|
|
||||||
notifications.success(`Settings saved.`)
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error(`Failed to update OAuth settings. ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// fetch the configs for oauth
|
|
||||||
const googleResponse = await api.get(
|
|
||||||
`/api/admin/configs/${ConfigTypes.Google}`
|
|
||||||
)
|
|
||||||
const googleDoc = await googleResponse.json()
|
|
||||||
|
|
||||||
if (!googleDoc._id) {
|
|
||||||
google = {
|
|
||||||
type: ConfigTypes.Google,
|
|
||||||
config: {},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
google = googleDoc
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Page>
|
|
||||||
<Layout noPadding>
|
|
||||||
<div>
|
|
||||||
<Heading size="M">OAuth</Heading>
|
|
||||||
<Body>
|
|
||||||
Every budibase app comes with basic authentication (email/password)
|
|
||||||
included. You can add additional authentication methods from the options
|
|
||||||
below.
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
<Divider />
|
|
||||||
{#if google}
|
|
||||||
<div>
|
|
||||||
<Heading size="S">
|
|
||||||
<span>
|
|
||||||
<GoogleLogo />
|
|
||||||
Google
|
|
||||||
</span>
|
|
||||||
</Heading>
|
|
||||||
<Body>
|
|
||||||
To allow users to authenticate using their Google accounts, fill out
|
|
||||||
the fields below.
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#each ConfigFields.Google as field}
|
|
||||||
<div class="form-row">
|
|
||||||
<Label size="L">{field}</Label>
|
|
||||||
<Input bind:value={google.config[field]} />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<div>
|
|
||||||
<Button cta on:click={() => save(google)}>Save</Button>
|
|
||||||
</div>
|
|
||||||
<Divider />
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
</Page>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.form-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 20% 1fr;
|
|
||||||
grid-gap: var(--spacing-l);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -14,118 +14,126 @@
|
||||||
import { organisation } from "stores/portal"
|
import { organisation } from "stores/portal"
|
||||||
import { post } from "builderStore/api"
|
import { post } from "builderStore/api"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
let analyticsDisabled = analytics.disabled()
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
function toggleAnalytics() {
|
|
||||||
if (analyticsDisabled) {
|
|
||||||
analytics.optIn()
|
|
||||||
} else {
|
|
||||||
analytics.optOut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const values = writable({
|
||||||
|
analytics: !analytics.disabled(),
|
||||||
|
company: $organisation.company,
|
||||||
|
platformUrl: $organisation.platformUrl,
|
||||||
|
logo: $organisation.logoUrl
|
||||||
|
? { url: $organisation.logoUrl, type: "image", name: "Logo" }
|
||||||
|
: null,
|
||||||
|
})
|
||||||
let loading = false
|
let loading = false
|
||||||
let file
|
|
||||||
|
|
||||||
async function uploadLogo() {
|
async function uploadLogo(file) {
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
data.append("file", file)
|
data.append("file", file)
|
||||||
|
|
||||||
const res = await post("/api/admin/configs/upload/settings/logo", data, {})
|
const res = await post("/api/admin/configs/upload/settings/logo", data, {})
|
||||||
return await res.json()
|
return await res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
loading = true
|
loading = true
|
||||||
await toggleAnalytics()
|
|
||||||
if (file) {
|
// Set analytics preference
|
||||||
await uploadLogo()
|
if ($values.analytics) {
|
||||||
|
analytics.optIn()
|
||||||
|
} else {
|
||||||
|
analytics.optOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload logo if required
|
||||||
|
if ($values.logo && !$values.logo.url) {
|
||||||
|
await uploadLogo($values.logo)
|
||||||
|
await organisation.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update settings
|
||||||
const res = await organisation.save({
|
const res = await organisation.save({
|
||||||
company: $organisation.company,
|
company: $values.company ?? "",
|
||||||
platformUrl: $organisation.platformUrl,
|
platformUrl: $values.platformUrl ?? "",
|
||||||
})
|
})
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
notifications.success("Settings saved.")
|
notifications.success("Settings saved successfully")
|
||||||
} else {
|
} else {
|
||||||
notifications.error(res.message)
|
notifications.error(res.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<Layout>
|
||||||
<Layout noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<div class="intro">
|
<Heading size="M">General</Heading>
|
||||||
<Heading size="M">General</Heading>
|
<Body>
|
||||||
<Body>
|
General is the place where you edit your organisation name, logo. You can
|
||||||
General is the place where you edit your organisation name, logo. You
|
also configure your platform URL as well as turn on or off analytics.
|
||||||
can also configure your platform URL as well as turn on or off
|
</Body>
|
||||||
analytics.
|
</Layout>
|
||||||
</Body>
|
<Divider size="S" />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Information</Heading>
|
||||||
|
<Body size="S">Here you can update your logo and organization name.</Body>
|
||||||
|
</Layout>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Organization name</Label>
|
||||||
|
<Input thin bind:value={$values.company} />
|
||||||
</div>
|
</div>
|
||||||
<Divider size="S" />
|
<div class="field logo">
|
||||||
<div class="information">
|
<Label size="L">Logo</Label>
|
||||||
<Heading size="S">Information</Heading>
|
<div class="file">
|
||||||
<Body>Here you can update your logo and organization name.</Body>
|
<Dropzone
|
||||||
<div class="fields">
|
value={[$values.logo]}
|
||||||
<div class="field">
|
on:change={e => {
|
||||||
<Label size="L">Organization name</Label>
|
$values.logo = e.detail?.[0]
|
||||||
<Input thin bind:value={$organisation.company} />
|
}}
|
||||||
</div>
|
/>
|
||||||
<div class="field logo">
|
|
||||||
<Label size="L">Logo</Label>
|
|
||||||
<div class="file">
|
|
||||||
<Dropzone
|
|
||||||
value={[file]}
|
|
||||||
on:change={e => {
|
|
||||||
file = e.detail?.[0]
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider size="S" />
|
</div>
|
||||||
<div class="analytics">
|
<Divider size="S" />
|
||||||
<Heading size="S">Platform</Heading>
|
<Layout gap="XS" noPadding>
|
||||||
<Body>Here you can set up general platform settings.</Body>
|
<Heading size="S">Platform</Heading>
|
||||||
<div class="fields">
|
<Body size="S">Here you can set up general platform settings.</Body>
|
||||||
<div class="field">
|
</Layout>
|
||||||
<Label size="L">Platform URL</Label>
|
<div class="fields">
|
||||||
<Input thin bind:value={$organisation.platformUrl} />
|
<div class="field">
|
||||||
</div>
|
<Label size="L">Platform URL</Label>
|
||||||
</div>
|
<Input thin bind:value={$values.platformUrl} />
|
||||||
</div>
|
</div>
|
||||||
<Divider size="S" />
|
</div>
|
||||||
<div class="analytics">
|
<Divider size="S" />
|
||||||
|
<Layout gap="S" noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">Analytics</Heading>
|
<Heading size="S">Analytics</Heading>
|
||||||
<Body>
|
<Body size="S">
|
||||||
If you would like to send analytics that help us make Budibase better,
|
If you would like to send analytics that help us make Budibase better,
|
||||||
please let us know below.
|
please let us know below.
|
||||||
</Body>
|
</Body>
|
||||||
<div class="fields">
|
</Layout>
|
||||||
<div class="field">
|
<div class="fields">
|
||||||
<Label size="L">Send Analytics to Budibase</Label>
|
<div class="field">
|
||||||
<Toggle text="" value={!analyticsDisabled} />
|
<Label size="L">Send Analytics to Budibase</Label>
|
||||||
</div>
|
<Toggle text="" bind:value={$values.analytics} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="save">
|
|
||||||
<Button disabled={loading} on:click={saveConfig} cta>Save</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
<div>
|
||||||
|
<Button disabled={loading} on:click={saveConfig} cta>Save</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.fields {
|
.fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-m);
|
grid-gap: var(--spacing-m);
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
}
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32% 1fr;
|
grid-template-columns: 33% 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.file {
|
.file {
|
||||||
|
@ -134,10 +142,4 @@
|
||||||
.logo {
|
.logo {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
.intro {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
.save {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
$goto("./builder")
|
$redirect("./builder")
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,4 +7,3 @@ export { roles } from "./roles"
|
||||||
export { datasources } from "./datasources"
|
export { datasources } from "./datasources"
|
||||||
export { integrations } from "./integrations"
|
export { integrations } from "./integrations"
|
||||||
export { queries } from "./queries"
|
export { queries } from "./queries"
|
||||||
export { auth } from "./auth"
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ export function createAuthStore() {
|
||||||
return {
|
return {
|
||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
checkAuth: async () => {
|
checkAuth: async () => {
|
||||||
const response = await api.get("/api/self")
|
const response = await api.get("/api/admin/users/self")
|
||||||
const user = await response.json()
|
const user = await response.json()
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
store.update(state => ({ ...state, user }))
|
store.update(state => ({ ...state, user }))
|
||||||
|
@ -33,6 +33,14 @@ export function createAuthStore() {
|
||||||
await response.json()
|
await response.json()
|
||||||
store.update(state => ({ ...state, user: null }))
|
store.update(state => ({ ...state, user: null }))
|
||||||
},
|
},
|
||||||
|
updateSelf: async user => {
|
||||||
|
const response = await api.post("/api/admin/users/self", user)
|
||||||
|
if (response.status === 200) {
|
||||||
|
store.update(state => ({ ...state, user: { ...state.user, ...user } }))
|
||||||
|
} else {
|
||||||
|
throw "Unable to update user details"
|
||||||
|
}
|
||||||
|
},
|
||||||
forgotPassword: async email => {
|
forgotPassword: async email => {
|
||||||
const response = await api.post(`/api/admin/auth/reset`, {
|
const response = await api.post(`/api/admin/auth/reset`, {
|
||||||
email,
|
email,
|
|
@ -3,3 +3,4 @@ export { users } from "./users"
|
||||||
export { admin } from "./admin"
|
export { admin } from "./admin"
|
||||||
export { apps } from "./apps"
|
export { apps } from "./apps"
|
||||||
export { email } from "./email"
|
export { email } from "./email"
|
||||||
|
export { auth } from "./auth"
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
||||||
const FALLBACK_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
platformUrl: "",
|
platformUrl: "http://localhost:1000",
|
||||||
logoUrl: "",
|
logoUrl: "https://i.imgur.com/ZKyklgF.png",
|
||||||
docsUrl: "",
|
docsUrl: undefined,
|
||||||
company: "http://localhost:10000",
|
company: "Budibase",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOrganisationStore() {
|
export function createOrganisationStore() {
|
||||||
const store = writable({})
|
const store = writable(DEFAULT_CONFIG)
|
||||||
const { subscribe, set } = store
|
const { subscribe, set } = store
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
@ -17,16 +17,16 @@ export function createOrganisationStore() {
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
|
||||||
if (json.status === 400) {
|
if (json.status === 400) {
|
||||||
set(FALLBACK_CONFIG)
|
set(DEFAULT_CONFIG)
|
||||||
} else {
|
} else {
|
||||||
set({ ...json.config, _rev: json._rev })
|
set({ ...DEFAULT_CONFIG, ...json.config, _rev: json._rev })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(config) {
|
async function save(config) {
|
||||||
const res = await api.post("/api/admin/configs", {
|
const res = await api.post("/api/admin/configs", {
|
||||||
type: "settings",
|
type: "settings",
|
||||||
config,
|
config: { ...get(store), ...config },
|
||||||
_rev: get(store)._rev,
|
_rev: get(store)._rev,
|
||||||
})
|
})
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
|
|
@ -117,13 +117,17 @@ async function createInstance(template) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetch = async function (ctx) {
|
exports.fetch = async function (ctx) {
|
||||||
const isDev = ctx.query && ctx.query.status === AppStatus.DEV
|
const dev = ctx.query && ctx.query.status === AppStatus.DEV
|
||||||
const apps = await getAllApps(isDev)
|
const all = ctx.query && ctx.query.status === AppStatus.ALL
|
||||||
|
const apps = await getAllApps({ dev, all })
|
||||||
|
|
||||||
// get the locks for all the dev apps
|
// get the locks for all the dev apps
|
||||||
if (isDev) {
|
if (dev || all) {
|
||||||
const locks = await getAllLocks()
|
const locks = await getAllLocks()
|
||||||
for (let app of apps) {
|
for (let app of apps) {
|
||||||
|
if (app.status !== "development") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const lock = locks.find(lock => lock.appId === app.appId)
|
const lock = locks.find(lock => lock.appId === app.appId)
|
||||||
if (lock) {
|
if (lock) {
|
||||||
app.lockedBy = lock.user
|
app.lockedBy = lock.user
|
||||||
|
@ -210,7 +214,7 @@ exports.create = async function (ctx) {
|
||||||
exports.update = async function (ctx) {
|
exports.update = async function (ctx) {
|
||||||
const url = await getAppUrlIfNotInUse(ctx)
|
const url = await getAppUrlIfNotInUse(ctx)
|
||||||
const db = new CouchDB(ctx.params.appId)
|
const db = new CouchDB(ctx.params.appId)
|
||||||
const application = await db.get(ctx.params.appId)
|
const application = await db.get(DocumentTypes.APP_METADATA)
|
||||||
|
|
||||||
const data = ctx.request.body
|
const data = ctx.request.body
|
||||||
const newData = { ...application, ...data, url }
|
const newData = { ...application, ...data, url }
|
||||||
|
@ -231,7 +235,7 @@ exports.update = async function (ctx) {
|
||||||
|
|
||||||
exports.delete = async function (ctx) {
|
exports.delete = async function (ctx) {
|
||||||
const db = new CouchDB(ctx.params.appId)
|
const db = new CouchDB(ctx.params.appId)
|
||||||
const app = await db.get(ctx.params.appId)
|
const app = await db.get(DocumentTypes.APP_METADATA)
|
||||||
const result = await db.destroy()
|
const result = await db.destroy()
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
|
|
|
@ -6,6 +6,8 @@ const webhooks = require("./webhook")
|
||||||
const { getAutomationParams, generateAutomationID } = require("../../db/utils")
|
const { getAutomationParams, generateAutomationID } = require("../../db/utils")
|
||||||
|
|
||||||
const WH_STEP_ID = triggers.BUILTIN_DEFINITIONS.WEBHOOK.stepId
|
const WH_STEP_ID = triggers.BUILTIN_DEFINITIONS.WEBHOOK.stepId
|
||||||
|
const CRON_STEP_ID = triggers.BUILTIN_DEFINITIONS.CRON.stepId
|
||||||
|
|
||||||
/*************************
|
/*************************
|
||||||
* *
|
* *
|
||||||
* BUILDER FUNCTIONS *
|
* BUILDER FUNCTIONS *
|
||||||
|
@ -32,6 +34,46 @@ function cleanAutomationInputs(automation) {
|
||||||
return automation
|
return automation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function handles checking of any cron jobs need to be created or deleted for automations.
|
||||||
|
* @param {string} appId The ID of the app in which we are checking for webhooks
|
||||||
|
* @param {object|undefined} oldAuto The old automation object if updating/deleting
|
||||||
|
* @param {object|undefined} newAuto The new automation object if creating/updating
|
||||||
|
*/
|
||||||
|
async function checkForCronTriggers({ appId, oldAuto, newAuto }) {
|
||||||
|
const oldTrigger = oldAuto ? oldAuto.definition.trigger : null
|
||||||
|
const newTrigger = newAuto ? newAuto.definition.trigger : null
|
||||||
|
function isCronTrigger(auto) {
|
||||||
|
return (
|
||||||
|
auto &&
|
||||||
|
auto.definition.trigger &&
|
||||||
|
auto.definition.trigger.stepId === CRON_STEP_ID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLive = auto => auto && auto.live
|
||||||
|
|
||||||
|
const cronTriggerRemoved =
|
||||||
|
isCronTrigger(oldAuto) && !isCronTrigger(newAuto) && oldTrigger.cronJobId
|
||||||
|
const cronTriggerDeactivated = !isLive(newAuto) && isLive(oldAuto)
|
||||||
|
|
||||||
|
const cronTriggerActivated = isLive(newAuto) && !isLive(oldAuto)
|
||||||
|
|
||||||
|
if (cronTriggerRemoved || cronTriggerDeactivated) {
|
||||||
|
await triggers.automationQueue.removeRepeatableByKey(oldTrigger.cronJobId)
|
||||||
|
}
|
||||||
|
// need to create cron job
|
||||||
|
else if (isCronTrigger(newAuto) && cronTriggerActivated) {
|
||||||
|
const job = await triggers.automationQueue.add(
|
||||||
|
{ automation: newAuto, event: { appId } },
|
||||||
|
{ repeat: { cron: newTrigger.inputs.cron } }
|
||||||
|
)
|
||||||
|
// Assign cron job ID from bull so we can remove it later if the cron trigger is removed
|
||||||
|
newTrigger.cronJobId = job.id
|
||||||
|
}
|
||||||
|
return newAuto
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function handles checking if any webhooks need to be created or deleted for automations.
|
* This function handles checking if any webhooks need to be created or deleted for automations.
|
||||||
* @param {string} appId The ID of the app in which we are checking for webhooks
|
* @param {string} appId The ID of the app in which we are checking for webhooks
|
||||||
|
@ -111,6 +153,10 @@ exports.create = async function (ctx) {
|
||||||
appId: ctx.appId,
|
appId: ctx.appId,
|
||||||
newAuto: automation,
|
newAuto: automation,
|
||||||
})
|
})
|
||||||
|
automation = await checkForCronTriggers({
|
||||||
|
appId: ctx.appId,
|
||||||
|
newAuto: automation,
|
||||||
|
})
|
||||||
const response = await db.put(automation)
|
const response = await db.put(automation)
|
||||||
automation._rev = response.rev
|
automation._rev = response.rev
|
||||||
|
|
||||||
|
@ -135,6 +181,11 @@ exports.update = async function (ctx) {
|
||||||
oldAuto: oldAutomation,
|
oldAuto: oldAutomation,
|
||||||
newAuto: automation,
|
newAuto: automation,
|
||||||
})
|
})
|
||||||
|
automation = await checkForCronTriggers({
|
||||||
|
appId: ctx.appId,
|
||||||
|
oldAuto: oldAutomation,
|
||||||
|
newAuto: automation,
|
||||||
|
})
|
||||||
const response = await db.put(automation)
|
const response = await db.put(automation)
|
||||||
automation._rev = response.rev
|
automation._rev = response.rev
|
||||||
|
|
||||||
|
@ -171,6 +222,10 @@ exports.destroy = async function (ctx) {
|
||||||
appId: ctx.appId,
|
appId: ctx.appId,
|
||||||
oldAuto: oldAutomation,
|
oldAuto: oldAutomation,
|
||||||
})
|
})
|
||||||
|
await checkForCronTriggers({
|
||||||
|
appId: ctx.appId,
|
||||||
|
oldAuto: oldAutomation,
|
||||||
|
})
|
||||||
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
|
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,13 +56,28 @@ async function findRow(ctx, db, tableId, rowId) {
|
||||||
exports.patch = async function (ctx) {
|
exports.patch = async function (ctx) {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
let dbRow = await db.get(ctx.params.rowId)
|
const inputs = ctx.request.body
|
||||||
let dbTable = await db.get(dbRow.tableId)
|
const tableId = inputs.tableId
|
||||||
const patchfields = ctx.request.body
|
const isUserTable = tableId === InternalTables.USER_METADATA
|
||||||
|
let dbRow
|
||||||
|
try {
|
||||||
|
dbRow = await db.get(ctx.params.rowId)
|
||||||
|
} catch (err) {
|
||||||
|
if (isUserTable) {
|
||||||
|
// don't include the rev, it'll be the global rev
|
||||||
|
// this time
|
||||||
|
dbRow = {
|
||||||
|
_id: inputs._id,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.throw(400, "Row does not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let dbTable = await db.get(tableId)
|
||||||
// need to build up full patch fields before coerce
|
// need to build up full patch fields before coerce
|
||||||
for (let key of Object.keys(patchfields)) {
|
for (let key of Object.keys(inputs)) {
|
||||||
if (!dbTable.schema[key]) continue
|
if (!dbTable.schema[key]) continue
|
||||||
dbRow[key] = patchfields[key]
|
dbRow[key] = inputs[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
// this returns the table and row incase they have been updated
|
// this returns the table and row incase they have been updated
|
||||||
|
@ -90,13 +105,9 @@ exports.patch = async function (ctx) {
|
||||||
table,
|
table,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO remove special user case in future
|
if (isUserTable) {
|
||||||
if (row.tableId === InternalTables.USER_METADATA) {
|
|
||||||
// the row has been updated, need to put it into the ctx
|
// the row has been updated, need to put it into the ctx
|
||||||
ctx.request.body = {
|
ctx.request.body = row
|
||||||
...row,
|
|
||||||
password: ctx.request.body.password,
|
|
||||||
}
|
|
||||||
await userController.updateMetadata(ctx)
|
await userController.updateMetadata(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -129,12 +140,9 @@ exports.save = async function (ctx) {
|
||||||
|
|
||||||
// if the row obj had an _id then it will have been retrieved
|
// if the row obj had an _id then it will have been retrieved
|
||||||
if (inputs._id && inputs._rev) {
|
if (inputs._id && inputs._rev) {
|
||||||
const existingRow = await db.get(inputs._id)
|
ctx.params.rowId = inputs._id
|
||||||
if (existingRow) {
|
await exports.patch(ctx)
|
||||||
ctx.params.rowId = inputs._id
|
return
|
||||||
await exports.patch(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputs._rev && !inputs._id) {
|
if (!inputs._rev && !inputs._id) {
|
||||||
|
@ -167,14 +175,6 @@ exports.save = async function (ctx) {
|
||||||
table,
|
table,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO remove special user case in future
|
|
||||||
if (row.tableId === InternalTables.USER_METADATA) {
|
|
||||||
// the row has been updated, need to put it into the ctx
|
|
||||||
ctx.request.body = row
|
|
||||||
await userController.createMetadata(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
row.type = "row"
|
row.type = "row"
|
||||||
const response = await db.put(row)
|
const response = await db.put(row)
|
||||||
// don't worry about rev, tables handle rev/lastID updates
|
// don't worry about rev, tables handle rev/lastID updates
|
||||||
|
|
|
@ -6,7 +6,7 @@ const {
|
||||||
InternalTables,
|
InternalTables,
|
||||||
} = require("../../../db/utils")
|
} = require("../../../db/utils")
|
||||||
const { isEqual } = require("lodash/fp")
|
const { isEqual } = require("lodash/fp")
|
||||||
const { AutoFieldSubTypes } = require("../../../constants")
|
const { AutoFieldSubTypes, FieldTypes } = require("../../../constants")
|
||||||
const { inputProcessing } = require("../../../utilities/rowProcessor")
|
const { inputProcessing } = require("../../../utilities/rowProcessor")
|
||||||
const { USERS_TABLE_SCHEMA } = require("../../../constants")
|
const { USERS_TABLE_SCHEMA } = require("../../../constants")
|
||||||
|
|
||||||
|
@ -72,18 +72,21 @@ exports.handleDataImport = async (appId, user, table, dataImport) => {
|
||||||
row._id = generateRowID(table._id)
|
row._id = generateRowID(table._id)
|
||||||
row.tableId = table._id
|
row.tableId = table._id
|
||||||
const processed = inputProcessing(user, table, row)
|
const processed = inputProcessing(user, table, row)
|
||||||
|
table = processed.table
|
||||||
row = processed.row
|
row = processed.row
|
||||||
// these auto-fields will never actually link anywhere (always builder)
|
|
||||||
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
||||||
|
// check whether the options need to be updated for inclusion as part of the data import
|
||||||
if (
|
if (
|
||||||
schema.autocolumn &&
|
schema.type === FieldTypes.OPTIONS &&
|
||||||
(schema.subtype === AutoFieldSubTypes.CREATED_BY ||
|
(!schema.constraints.inclusion ||
|
||||||
schema.subtype === AutoFieldSubTypes.UPDATED_BY)
|
schema.constraints.inclusion.indexOf(row[fieldName]) === -1)
|
||||||
) {
|
) {
|
||||||
delete row[fieldName]
|
schema.constraints.inclusion = [
|
||||||
|
...schema.constraints.inclusion,
|
||||||
|
row[fieldName],
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
table = processed.table
|
|
||||||
data[i] = row
|
data[i] = row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,23 @@ const CouchDB = require("../../db")
|
||||||
const {
|
const {
|
||||||
generateUserMetadataID,
|
generateUserMetadataID,
|
||||||
getUserMetadataParams,
|
getUserMetadataParams,
|
||||||
getGlobalIDFromUserMetadataID,
|
|
||||||
} = require("../../db/utils")
|
} = require("../../db/utils")
|
||||||
const { InternalTables } = require("../../db/utils")
|
const { InternalTables } = require("../../db/utils")
|
||||||
const { getRole, BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
||||||
const {
|
const {
|
||||||
getGlobalUsers,
|
getGlobalUsers,
|
||||||
saveGlobalUser,
|
addAppRoleToUser,
|
||||||
deleteGlobalUser,
|
|
||||||
} = require("../../utilities/workerRequests")
|
} = require("../../utilities/workerRequests")
|
||||||
const { getFullUser } = require("../../utilities/users")
|
const { getFullUser } = require("../../utilities/users")
|
||||||
|
|
||||||
|
function removeGlobalProps(user) {
|
||||||
|
// make sure to always remove some of the global user props
|
||||||
|
delete user.password
|
||||||
|
delete user.roles
|
||||||
|
delete user.builder
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
exports.fetchMetadata = async function (ctx) {
|
exports.fetchMetadata = async function (ctx) {
|
||||||
const database = new CouchDB(ctx.appId)
|
const database = new CouchDB(ctx.appId)
|
||||||
const global = await getGlobalUsers(ctx, ctx.appId)
|
const global = await getGlobalUsers(ctx, ctx.appId)
|
||||||
|
@ -38,43 +44,12 @@ exports.fetchMetadata = async function (ctx) {
|
||||||
ctx.body = users
|
ctx.body = users
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createMetadata = async function (ctx) {
|
|
||||||
const appId = ctx.appId
|
|
||||||
const db = new CouchDB(appId)
|
|
||||||
const { roleId } = ctx.request.body
|
|
||||||
|
|
||||||
if (ctx.request.body._id) {
|
|
||||||
return exports.updateMetadata(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check role valid
|
|
||||||
const role = await getRole(appId, roleId)
|
|
||||||
if (!role) ctx.throw(400, "Invalid Role")
|
|
||||||
|
|
||||||
const globalUser = await saveGlobalUser(ctx, appId, ctx.request.body)
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
...globalUser,
|
|
||||||
_id: generateUserMetadataID(globalUser._id),
|
|
||||||
type: "user",
|
|
||||||
tableId: InternalTables.USER_METADATA,
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await db.post(user)
|
|
||||||
// for automations to make it obvious was successful
|
|
||||||
ctx.status = 200
|
|
||||||
ctx.body = {
|
|
||||||
_id: response.id,
|
|
||||||
_rev: response.rev,
|
|
||||||
email: ctx.request.body.email,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updateSelfMetadata = async function (ctx) {
|
exports.updateSelfMetadata = async function (ctx) {
|
||||||
// overwrite the ID with current users
|
// overwrite the ID with current users
|
||||||
ctx.request.body._id = ctx.user._id
|
ctx.request.body._id = ctx.user._id
|
||||||
if (ctx.user.builder && ctx.user.builder.global) {
|
if (ctx.user.builder && ctx.user.builder.global) {
|
||||||
ctx.request.body.roleId = BUILTIN_ROLE_IDS.ADMIN
|
// specific case, update self role in global user
|
||||||
|
await addAppRoleToUser(ctx, ctx.appId, BUILTIN_ROLE_IDS.ADMIN)
|
||||||
}
|
}
|
||||||
// make sure no stale rev
|
// make sure no stale rev
|
||||||
delete ctx.request.body._rev
|
delete ctx.request.body._rev
|
||||||
|
@ -84,23 +59,19 @@ exports.updateSelfMetadata = async function (ctx) {
|
||||||
exports.updateMetadata = async function (ctx) {
|
exports.updateMetadata = async function (ctx) {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
const user = ctx.request.body
|
const user = removeGlobalProps(ctx.request.body)
|
||||||
const globalUser = await saveGlobalUser(ctx, appId, {
|
if (user.roleId) {
|
||||||
...user,
|
await addAppRoleToUser(ctx, appId, user.roleId, user._id)
|
||||||
_id: getGlobalIDFromUserMetadataID(user._id),
|
}
|
||||||
})
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
...globalUser,
|
|
||||||
tableId: InternalTables.USER_METADATA,
|
tableId: InternalTables.USER_METADATA,
|
||||||
_id: user._id || generateUserMetadataID(globalUser._id),
|
...user,
|
||||||
_rev: user._rev,
|
|
||||||
}
|
}
|
||||||
ctx.body = await db.put(metadata)
|
ctx.body = await db.put(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.destroyMetadata = async function (ctx) {
|
exports.destroyMetadata = async function (ctx) {
|
||||||
const db = new CouchDB(ctx.appId)
|
const db = new CouchDB(ctx.appId)
|
||||||
await deleteGlobalUser(ctx, getGlobalIDFromUserMetadataID(ctx.params.id))
|
|
||||||
try {
|
try {
|
||||||
const dbUser = await db.get(ctx.params.id)
|
const dbUser = await db.get(ctx.params.id)
|
||||||
await db.remove(dbUser._id, dbUser._rev)
|
await db.remove(dbUser._id, dbUser._rev)
|
||||||
|
@ -108,7 +79,7 @@ exports.destroyMetadata = async function (ctx) {
|
||||||
// error just means the global user has no config in this app
|
// error just means the global user has no config in this app
|
||||||
}
|
}
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: `User ${ctx.params.id} deleted.`,
|
message: `User metadata ${ctx.params.id} deleted.`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,6 @@
|
||||||
const setup = require("./utilities")
|
const setup = require("./utilities")
|
||||||
const { generateUserMetadataID } = require("../../../db/utils")
|
const { generateUserMetadataID } = require("../../../db/utils")
|
||||||
|
|
||||||
require("../../../utilities/workerRequests")
|
|
||||||
jest.mock("../../../utilities/workerRequests", () => ({
|
|
||||||
getGlobalUsers: jest.fn(() => {
|
|
||||||
return {
|
|
||||||
_id: "us_uuid1",
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
saveGlobalUser: jest.fn(() => {
|
|
||||||
return {
|
|
||||||
_id: "us_uuid1",
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe("/authenticate", () => {
|
describe("/authenticate", () => {
|
||||||
let request = setup.getRequest()
|
let request = setup.getRequest()
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
@ -27,7 +13,7 @@ describe("/authenticate", () => {
|
||||||
|
|
||||||
describe("fetch self", () => {
|
describe("fetch self", () => {
|
||||||
it("should be able to fetch self", async () => {
|
it("should be able to fetch self", async () => {
|
||||||
const user = await config.createUser("test@test.com", "p4ssw0rd")
|
await config.createUser("test@test.com", "p4ssw0rd")
|
||||||
const headers = await config.login("test@test.com", "p4ssw0rd", { userId: "us_uuid1" })
|
const headers = await config.login("test@test.com", "p4ssw0rd", { userId: "us_uuid1" })
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/self`)
|
.get(`/api/self`)
|
||||||
|
|
|
@ -304,24 +304,6 @@ describe("/rows", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("search", () => {
|
|
||||||
it("should run a search on the table", async () => {
|
|
||||||
const res = await request
|
|
||||||
.post(`/api/${table._id}/rows/search`)
|
|
||||||
.send({
|
|
||||||
query: {
|
|
||||||
name: "Test",
|
|
||||||
},
|
|
||||||
pagination: { pageSize: 25 }
|
|
||||||
})
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.rows.length).toEqual(1)
|
|
||||||
expect(res.body.bookmark).toBeDefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("fetchView", () => {
|
describe("fetchView", () => {
|
||||||
it("should be able to fetch tables contents via 'view'", async () => {
|
it("should be able to fetch tables contents via 'view'", async () => {
|
||||||
const row = await config.createRow()
|
const row = await config.createRow()
|
||||||
|
|
|
@ -8,12 +8,7 @@ jest.mock("../../../utilities/workerRequests", () => ({
|
||||||
getGlobalUsers: jest.fn(() => {
|
getGlobalUsers: jest.fn(() => {
|
||||||
return {}
|
return {}
|
||||||
}),
|
}),
|
||||||
saveGlobalUser: jest.fn(() => {
|
addAppRoleToUser: jest.fn(),
|
||||||
const uuid = require("uuid/v4")
|
|
||||||
return {
|
|
||||||
_id: `us_${uuid()}`
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
deleteGlobalUser: jest.fn(),
|
deleteGlobalUser: jest.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -67,59 +62,8 @@ describe("/users", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("create", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
workerRequests.getGlobalUsers.mockImplementationOnce(() => ([
|
|
||||||
{
|
|
||||||
_id: "us_uuid1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "us_uuid2",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
))
|
|
||||||
})
|
|
||||||
|
|
||||||
async function create(user, status = 200) {
|
|
||||||
return request
|
|
||||||
.post(`/api/users/metadata`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.send(user)
|
|
||||||
.expect(status)
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
}
|
|
||||||
|
|
||||||
it("returns a success message when a user is successfully created", async () => {
|
|
||||||
const body = basicUser(BUILTIN_ROLE_IDS.POWER)
|
|
||||||
const res = await create(body)
|
|
||||||
|
|
||||||
expect(res.res.statusMessage).toEqual("OK")
|
|
||||||
expect(res.body._id).toBeDefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
|
||||||
const body = basicUser(BUILTIN_ROLE_IDS.POWER)
|
|
||||||
await checkPermissionsEndpoint({
|
|
||||||
config,
|
|
||||||
method: "POST",
|
|
||||||
body,
|
|
||||||
url: `/api/users/metadata`,
|
|
||||||
passRole: BUILTIN_ROLE_IDS.ADMIN,
|
|
||||||
failRole: BUILTIN_ROLE_IDS.PUBLIC,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should error if no role provided", async () => {
|
|
||||||
const user = basicUser(null)
|
|
||||||
await create(user, 400)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
workerRequests.saveGlobalUser.mockImplementationOnce(() => ({
|
|
||||||
_id: "us_test@test.com"
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to update the user", async () => {
|
it("should be able to update the user", async () => {
|
||||||
|
@ -144,16 +88,12 @@ describe("/users", () => {
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
expect(res.body.message).toBeDefined()
|
expect(res.body.message).toBeDefined()
|
||||||
expect(workerRequests.deleteGlobalUser).toHaveBeenCalled()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("find", () => {
|
describe("find", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks()
|
jest.resetAllMocks()
|
||||||
workerRequests.saveGlobalUser.mockImplementationOnce(() => ({
|
|
||||||
_id: "us_uuid1",
|
|
||||||
}))
|
|
||||||
workerRequests.getGlobalUsers.mockImplementationOnce(() => ({
|
workerRequests.getGlobalUsers.mockImplementationOnce(() => ({
|
||||||
_id: "us_uuid1",
|
_id: "us_uuid1",
|
||||||
roleId: BUILTIN_ROLE_IDS.POWER,
|
roleId: BUILTIN_ROLE_IDS.POWER,
|
||||||
|
|
|
@ -3,8 +3,7 @@ const structures = require("../../../../tests/utilities/structures")
|
||||||
const env = require("../../../../environment")
|
const env = require("../../../../environment")
|
||||||
|
|
||||||
jest.mock("../../../../utilities/workerRequests", () => ({
|
jest.mock("../../../../utilities/workerRequests", () => ({
|
||||||
getGlobalUsers: jest.fn(),
|
getGlobalUsers: jest.fn(() => {
|
||||||
saveGlobalUser: jest.fn(() => {
|
|
||||||
return {
|
return {
|
||||||
_id: "us_uuid1",
|
_id: "us_uuid1",
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,12 +25,6 @@ router
|
||||||
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
||||||
controller.updateMetadata
|
controller.updateMetadata
|
||||||
)
|
)
|
||||||
.post(
|
|
||||||
"/api/users/metadata",
|
|
||||||
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
|
||||||
usage,
|
|
||||||
controller.createMetadata
|
|
||||||
)
|
|
||||||
.post(
|
.post(
|
||||||
"/api/users/metadata/self",
|
"/api/users/metadata/self",
|
||||||
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
||||||
|
|
|
@ -3,7 +3,6 @@ const sendSmtpEmail = require("./steps/sendSmtpEmail")
|
||||||
const createRow = require("./steps/createRow")
|
const createRow = require("./steps/createRow")
|
||||||
const updateRow = require("./steps/updateRow")
|
const updateRow = require("./steps/updateRow")
|
||||||
const deleteRow = require("./steps/deleteRow")
|
const deleteRow = require("./steps/deleteRow")
|
||||||
const createUser = require("./steps/createUser")
|
|
||||||
const executeScript = require("./steps/executeScript")
|
const executeScript = require("./steps/executeScript")
|
||||||
const executeQuery = require("./steps/executeQuery")
|
const executeQuery = require("./steps/executeQuery")
|
||||||
const outgoingWebhook = require("./steps/outgoingWebhook")
|
const outgoingWebhook = require("./steps/outgoingWebhook")
|
||||||
|
@ -20,7 +19,6 @@ const BUILTIN_ACTIONS = {
|
||||||
CREATE_ROW: createRow.run,
|
CREATE_ROW: createRow.run,
|
||||||
UPDATE_ROW: updateRow.run,
|
UPDATE_ROW: updateRow.run,
|
||||||
DELETE_ROW: deleteRow.run,
|
DELETE_ROW: deleteRow.run,
|
||||||
CREATE_USER: createUser.run,
|
|
||||||
OUTGOING_WEBHOOK: outgoingWebhook.run,
|
OUTGOING_WEBHOOK: outgoingWebhook.run,
|
||||||
EXECUTE_SCRIPT: executeScript.run,
|
EXECUTE_SCRIPT: executeScript.run,
|
||||||
EXECUTE_QUERY: executeQuery.run,
|
EXECUTE_QUERY: executeQuery.run,
|
||||||
|
@ -31,7 +29,6 @@ const BUILTIN_DEFINITIONS = {
|
||||||
CREATE_ROW: createRow.definition,
|
CREATE_ROW: createRow.definition,
|
||||||
UPDATE_ROW: updateRow.definition,
|
UPDATE_ROW: updateRow.definition,
|
||||||
DELETE_ROW: deleteRow.definition,
|
DELETE_ROW: deleteRow.definition,
|
||||||
CREATE_USER: createUser.definition,
|
|
||||||
OUTGOING_WEBHOOK: outgoingWebhook.definition,
|
OUTGOING_WEBHOOK: outgoingWebhook.definition,
|
||||||
EXECUTE_SCRIPT: executeScript.definition,
|
EXECUTE_SCRIPT: executeScript.definition,
|
||||||
EXECUTE_QUERY: executeQuery.definition,
|
EXECUTE_QUERY: executeQuery.definition,
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
const roles = require("@budibase/auth/roles")
|
|
||||||
const userController = require("../../api/controllers/user")
|
|
||||||
const env = require("../../environment")
|
|
||||||
const usage = require("../../utilities/usageQuota")
|
|
||||||
|
|
||||||
module.exports.definition = {
|
|
||||||
description: "Create a new user",
|
|
||||||
tagline: "Create user {{inputs.email}}",
|
|
||||||
icon: "ri-user-add-line",
|
|
||||||
name: "Create User",
|
|
||||||
type: "ACTION",
|
|
||||||
stepId: "CREATE_USER",
|
|
||||||
inputs: {
|
|
||||||
roleId: roles.BUILTIN_ROLE_IDS.POWER,
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
inputs: {
|
|
||||||
properties: {
|
|
||||||
email: {
|
|
||||||
type: "string",
|
|
||||||
customType: "email",
|
|
||||||
title: "Email",
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
type: "string",
|
|
||||||
title: "Password",
|
|
||||||
},
|
|
||||||
roleId: {
|
|
||||||
type: "string",
|
|
||||||
title: "Role",
|
|
||||||
enum: roles.BUILTIN_ROLE_ID_ARRAY,
|
|
||||||
pretty: roles.BUILTIN_ROLE_NAME_ARRAY,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["email", "password", "roleId"],
|
|
||||||
},
|
|
||||||
outputs: {
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: "string",
|
|
||||||
description: "The identifier of the new user",
|
|
||||||
},
|
|
||||||
revision: {
|
|
||||||
type: "string",
|
|
||||||
description: "The revision of the new user",
|
|
||||||
},
|
|
||||||
response: {
|
|
||||||
type: "object",
|
|
||||||
description: "The response from the user table",
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether the action was successful",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["id", "revision", "success"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.run = async function ({ inputs, appId, apiKey, emitter }) {
|
|
||||||
const { email, password, roleId } = inputs
|
|
||||||
const ctx = {
|
|
||||||
appId,
|
|
||||||
request: {
|
|
||||||
body: { email, password, roleId },
|
|
||||||
},
|
|
||||||
eventEmitter: emitter,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (env.isProd()) {
|
|
||||||
await usage.update(apiKey, usage.Properties.USER, 1)
|
|
||||||
}
|
|
||||||
await userController.createMetadata(ctx)
|
|
||||||
return {
|
|
||||||
response: ctx.body,
|
|
||||||
// internal property not returned through the API
|
|
||||||
id: ctx.body._id,
|
|
||||||
revision: ctx.body._rev,
|
|
||||||
success: ctx.status === 200,
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
response: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
const usageQuota = require("../../utilities/usageQuota")
|
|
||||||
const setup = require("./utilities")
|
|
||||||
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
|
||||||
const { InternalTables } = require("../../db/utils")
|
|
||||||
|
|
||||||
jest.mock("../../utilities/usageQuota")
|
|
||||||
|
|
||||||
describe("test the create user action", () => {
|
|
||||||
let config = setup.getConfig()
|
|
||||||
let user
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await config.init()
|
|
||||||
user = {
|
|
||||||
email: "test@test.com",
|
|
||||||
password: "password",
|
|
||||||
roleId: BUILTIN_ROLE_IDS.POWER
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
|
||||||
|
|
||||||
it("should be able to run the action", async () => {
|
|
||||||
const res = await setup.runStep(setup.actions.CREATE_USER.stepId, user)
|
|
||||||
expect(res.id).toBeDefined()
|
|
||||||
expect(res.revision).toBeDefined()
|
|
||||||
const userDoc = await config.getRow(InternalTables.USER_METADATA, res.id)
|
|
||||||
expect(userDoc).toBeDefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return an error if no inputs provided", async () => {
|
|
||||||
const res = await setup.runStep(setup.actions.CREATE_USER.stepId, {})
|
|
||||||
expect(res.success).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("check usage quota attempts", async () => {
|
|
||||||
await setup.runInProd(async () => {
|
|
||||||
await setup.runStep(setup.actions.CREATE_USER.stepId, user)
|
|
||||||
expect(usageQuota.update).toHaveBeenCalledWith(setup.apiKey, "users", 1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -12,7 +12,7 @@ const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
|
||||||
* inputs and handles any outputs.
|
* inputs and handles any outputs.
|
||||||
*/
|
*/
|
||||||
class Orchestrator {
|
class Orchestrator {
|
||||||
constructor(automation, triggerOutput) {
|
constructor(automation, triggerOutput = {}) {
|
||||||
this._metadata = triggerOutput.metadata
|
this._metadata = triggerOutput.metadata
|
||||||
this._chainCount = this._metadata ? this._metadata.automationChainCount : 0
|
this._chainCount = this._metadata ? this._metadata.automationChainCount : 0
|
||||||
this._appId = triggerOutput.appId
|
this._appId = triggerOutput.appId
|
||||||
|
|
|
@ -7,9 +7,10 @@ const Queue = env.isTest()
|
||||||
const { getAutomationParams } = require("../db/utils")
|
const { getAutomationParams } = require("../db/utils")
|
||||||
const { coerce } = require("../utilities/rowProcessor")
|
const { coerce } = require("../utilities/rowProcessor")
|
||||||
const { utils } = require("@budibase/auth/redis")
|
const { utils } = require("@budibase/auth/redis")
|
||||||
|
const { JobQueues } = require("../constants")
|
||||||
|
|
||||||
const { opts } = utils.getRedisOptions()
|
const { opts } = utils.getRedisOptions()
|
||||||
let automationQueue = new Queue("automationQueue", { redis: opts })
|
let automationQueue = new Queue(JobQueues.AUTOMATIONS, { redis: opts })
|
||||||
|
|
||||||
const FAKE_STRING = "TEST"
|
const FAKE_STRING = "TEST"
|
||||||
const FAKE_BOOL = false
|
const FAKE_BOOL = false
|
||||||
|
@ -196,6 +197,29 @@ const BUILTIN_DEFINITIONS = {
|
||||||
},
|
},
|
||||||
type: "TRIGGER",
|
type: "TRIGGER",
|
||||||
},
|
},
|
||||||
|
CRON: {
|
||||||
|
name: "Cron Trigger",
|
||||||
|
event: "cron:trigger",
|
||||||
|
icon: "ri-timer-line",
|
||||||
|
tagline: "Cron Trigger (<b>{{inputs.cron}}</b>)",
|
||||||
|
description: "Triggers automation on a cron schedule.",
|
||||||
|
stepId: "CRON",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
cron: {
|
||||||
|
type: "string",
|
||||||
|
customType: "cron",
|
||||||
|
title: "Expression",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["cron"],
|
||||||
|
},
|
||||||
|
outputs: {},
|
||||||
|
},
|
||||||
|
type: "TRIGGER",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async function queueRelevantRowAutomations(event, eventType) {
|
async function queueRelevantRowAutomations(event, eventType) {
|
||||||
|
|
|
@ -5,6 +5,10 @@ const { ObjectStoreBuckets } = require("@budibase/auth").objectStore
|
||||||
exports.LOGO_URL =
|
exports.LOGO_URL =
|
||||||
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
|
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
|
||||||
|
|
||||||
|
exports.JobQueues = {
|
||||||
|
AUTOMATIONS: "automationQueue",
|
||||||
|
}
|
||||||
|
|
||||||
exports.FieldTypes = {
|
exports.FieldTypes = {
|
||||||
STRING: "string",
|
STRING: "string",
|
||||||
LONGFORM: "longform",
|
LONGFORM: "longform",
|
||||||
|
@ -50,6 +54,24 @@ exports.USERS_TABLE_SCHEMA = {
|
||||||
fieldName: "email",
|
fieldName: "email",
|
||||||
name: "email",
|
name: "email",
|
||||||
},
|
},
|
||||||
|
firstName: {
|
||||||
|
name: "firstName",
|
||||||
|
fieldName: "firstName",
|
||||||
|
type: exports.FieldTypes.STRING,
|
||||||
|
constraints: {
|
||||||
|
type: exports.FieldTypes.STRING,
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
name: "lastName",
|
||||||
|
fieldName: "lastName",
|
||||||
|
type: exports.FieldTypes.STRING,
|
||||||
|
constraints: {
|
||||||
|
type: exports.FieldTypes.STRING,
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
roleId: {
|
roleId: {
|
||||||
fieldName: "roleId",
|
fieldName: "roleId",
|
||||||
name: "roleId",
|
name: "roleId",
|
||||||
|
|
|
@ -19,6 +19,7 @@ const StaticDatabases = {
|
||||||
|
|
||||||
const AppStatus = {
|
const AppStatus = {
|
||||||
DEV: "dev",
|
DEV: "dev",
|
||||||
|
ALL: "all",
|
||||||
DEPLOYED: "PUBLISHED",
|
DEPLOYED: "PUBLISHED",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,11 @@ class TestConfiguration {
|
||||||
if (builder) {
|
if (builder) {
|
||||||
user.builder = { global: true }
|
user.builder = { global: true }
|
||||||
}
|
}
|
||||||
await db.put(user)
|
const resp = await db.put(user)
|
||||||
|
return {
|
||||||
|
_rev: resp._rev,
|
||||||
|
...user,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(appName = "test_application") {
|
async init(appName = "test_application") {
|
||||||
|
@ -308,18 +312,12 @@ class TestConfiguration {
|
||||||
roleId = BUILTIN_ROLE_IDS.POWER
|
roleId = BUILTIN_ROLE_IDS.POWER
|
||||||
) {
|
) {
|
||||||
const globalId = `us_${Math.random()}`
|
const globalId = `us_${Math.random()}`
|
||||||
await this.globalUser(globalId, roleId === BUILTIN_ROLE_IDS.BUILDER)
|
const resp = await this.globalUser(
|
||||||
const user = await this._req(
|
globalId,
|
||||||
{
|
roleId === BUILTIN_ROLE_IDS.BUILDER
|
||||||
email,
|
|
||||||
password,
|
|
||||||
roleId,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
controllers.user.createMetadata
|
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
...user,
|
...resp,
|
||||||
globalId,
|
globalId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
const csv = require("csvtojson")
|
const csv = require("csvtojson")
|
||||||
|
const { FieldTypes } = require("../constants")
|
||||||
|
|
||||||
const VALIDATORS = {
|
const VALIDATORS = {
|
||||||
string: () => true,
|
[FieldTypes.STRING]: () => true,
|
||||||
number: attribute => !isNaN(Number(attribute)),
|
[FieldTypes.OPTIONS]: () => true,
|
||||||
datetime: attribute => !isNaN(new Date(attribute).getTime()),
|
[FieldTypes.NUMBER]: attribute => !isNaN(Number(attribute)),
|
||||||
|
[FieldTypes.DATETIME]: attribute => !isNaN(new Date(attribute).getTime()),
|
||||||
}
|
}
|
||||||
|
|
||||||
const PARSERS = {
|
const PARSERS = {
|
||||||
number: attribute => Number(attribute),
|
[FieldTypes.NUMBER]: attribute => Number(attribute),
|
||||||
datetime: attribute => new Date(attribute).toISOString(),
|
[FieldTypes.DATETIME]: attribute => new Date(attribute).toISOString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
function parse(csvString, parsers) {
|
function parse(csvString, parsers) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const CouchDB = require("../db")
|
const CouchDB = require("../db")
|
||||||
const { getGlobalIDFromUserMetadataID } = require("../db/utils")
|
const { getGlobalIDFromUserMetadataID, InternalTables } = require("../db/utils")
|
||||||
const { getGlobalUsers } = require("../utilities/workerRequests")
|
const { getGlobalUsers } = require("../utilities/workerRequests")
|
||||||
|
|
||||||
exports.getFullUser = async (ctx, userId) => {
|
exports.getFullUser = async (ctx, userId) => {
|
||||||
|
@ -21,6 +21,7 @@ exports.getFullUser = async (ctx, userId) => {
|
||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
...metadata,
|
...metadata,
|
||||||
|
tableId: InternalTables.USER_METADATA,
|
||||||
// make sure the ID is always a local ID, not a global one
|
// make sure the ID is always a local ID, not a global one
|
||||||
_id: userId,
|
_id: userId,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ const env = require("../environment")
|
||||||
const { checkSlashesInUrl } = require("./index")
|
const { checkSlashesInUrl } = require("./index")
|
||||||
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
||||||
const { getDeployedAppID } = require("@budibase/auth/db")
|
const { getDeployedAppID } = require("@budibase/auth/db")
|
||||||
|
const { getGlobalIDFromUserMetadataID } = require("../db/utils")
|
||||||
|
|
||||||
function getAppRole(appId, user) {
|
function getAppRole(appId, user) {
|
||||||
if (!user.roles) {
|
if (!user.roles) {
|
||||||
|
@ -118,53 +119,50 @@ exports.getGlobalUsers = async (ctx, appId = null, globalId = null) => {
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.saveGlobalUser = async (ctx, appId, body) => {
|
exports.getGlobalSelf = async ctx => {
|
||||||
const globalUser = body._id
|
const endpoint = `/api/admin/users/self`
|
||||||
? await exports.getGlobalUsers(ctx, appId, body._id)
|
|
||||||
: {}
|
|
||||||
const preRoles = globalUser.roles || {}
|
|
||||||
if (body.roleId) {
|
|
||||||
preRoles[appId] = body.roleId
|
|
||||||
}
|
|
||||||
// make sure no dev app IDs in roles
|
|
||||||
const roles = {}
|
|
||||||
for (let [appId, roleId] of Object.entries(preRoles)) {
|
|
||||||
roles[getDeployedAppID(appId)] = roleId
|
|
||||||
}
|
|
||||||
const endpoint = `/api/admin/users`
|
|
||||||
const reqCfg = {
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
...globalUser,
|
|
||||||
password: body.password || undefined,
|
|
||||||
status: body.status,
|
|
||||||
email: body.email,
|
|
||||||
roles,
|
|
||||||
builder: {
|
|
||||||
global: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
checkSlashesInUrl(env.WORKER_URL + endpoint),
|
checkSlashesInUrl(env.WORKER_URL + endpoint),
|
||||||
request(ctx, reqCfg)
|
request(ctx, { method: "GET" })
|
||||||
)
|
)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
if (json.status !== 200 && response.status !== 200) {
|
if (json.status !== 200 && response.status !== 200) {
|
||||||
ctx.throw(400, "Unable to save global user.")
|
ctx.throw(400, "Unable to get self globally.")
|
||||||
}
|
|
||||||
delete body.password
|
|
||||||
delete body.roles
|
|
||||||
delete body.builder
|
|
||||||
// TODO: for now these have been left in as they are
|
|
||||||
// TODO: pretty important to keeping relationships working
|
|
||||||
// TODO: however if user metadata is changed this should be removed
|
|
||||||
// delete body.email
|
|
||||||
// delete body.roleId
|
|
||||||
// delete body.status
|
|
||||||
return {
|
|
||||||
...body,
|
|
||||||
_id: json._id,
|
|
||||||
}
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.addAppRoleToUser = async (ctx, appId, roleId, userId = null) => {
|
||||||
|
appId = getDeployedAppID(appId)
|
||||||
|
let user,
|
||||||
|
endpoint,
|
||||||
|
body = {}
|
||||||
|
if (!userId) {
|
||||||
|
user = await exports.getGlobalSelf(ctx)
|
||||||
|
endpoint = `/api/admin/users/self`
|
||||||
|
} else {
|
||||||
|
userId = getGlobalIDFromUserMetadataID(userId)
|
||||||
|
user = await exports.getGlobalUsers(ctx, appId, userId)
|
||||||
|
body._id = userId
|
||||||
|
endpoint = `/api/admin/users`
|
||||||
|
}
|
||||||
|
body = {
|
||||||
|
...body,
|
||||||
|
roles: {
|
||||||
|
...user.roles,
|
||||||
|
[appId]: roleId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const response = await fetch(
|
||||||
|
checkSlashesInUrl(env.WORKER_URL + endpoint),
|
||||||
|
request(ctx, {
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const json = await response.json()
|
||||||
|
if (json.status !== 200 && response.status !== 200) {
|
||||||
|
ctx.throw(400, "Unable to save self globally.")
|
||||||
|
}
|
||||||
|
return json
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -8,7 +8,7 @@ const CouchDB = require("../../../db")
|
||||||
|
|
||||||
exports.fetch = async ctx => {
|
exports.fetch = async ctx => {
|
||||||
// always use the dev apps as they'll be most up to date (true)
|
// always use the dev apps as they'll be most up to date (true)
|
||||||
const apps = await getAllApps(true)
|
const apps = await getAllApps({ dev: true })
|
||||||
const promises = []
|
const promises = []
|
||||||
for (let app of apps) {
|
for (let app of apps) {
|
||||||
// use dev app IDs
|
// use dev app IDs
|
||||||
|
|
|
@ -16,9 +16,14 @@ exports.save = async ctx => {
|
||||||
const { email, password, _id } = ctx.request.body
|
const { email, password, _id } = ctx.request.body
|
||||||
|
|
||||||
// make sure another user isn't using the same email
|
// make sure another user isn't using the same email
|
||||||
const dbUser = await getGlobalUserByEmail(email)
|
let dbUser
|
||||||
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
|
if (email) {
|
||||||
ctx.throw(400, "Email address already in use.")
|
dbUser = await getGlobalUserByEmail(email)
|
||||||
|
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
|
||||||
|
ctx.throw(400, "Email address already in use.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dbUser = await db.get(_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the password, make sure one is defined
|
// get the password, make sure one is defined
|
||||||
|
@ -96,6 +101,33 @@ exports.destroy = async ctx => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getSelf = async ctx => {
|
||||||
|
ctx.params = {
|
||||||
|
id: ctx.user._id,
|
||||||
|
}
|
||||||
|
// this will set the body
|
||||||
|
await exports.find(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updateSelf = async ctx => {
|
||||||
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
const user = await db.get(ctx.user._id)
|
||||||
|
if (ctx.request.body.password) {
|
||||||
|
ctx.request.body.password = await hash(ctx.request.body.password)
|
||||||
|
}
|
||||||
|
// don't allow sending up an ID/Rev, always use the existing one
|
||||||
|
delete ctx.request.body._id
|
||||||
|
delete ctx.request.body._rev
|
||||||
|
const response = await db.put({
|
||||||
|
...user,
|
||||||
|
...ctx.request.body,
|
||||||
|
})
|
||||||
|
ctx.body = {
|
||||||
|
_id: response.id,
|
||||||
|
_rev: response.rev,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// called internally by app server user fetch
|
// called internally by app server user fetch
|
||||||
exports.fetch = async ctx => {
|
exports.fetch = async ctx => {
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
|
@ -145,12 +177,16 @@ exports.invite = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.inviteAccept = async ctx => {
|
exports.inviteAccept = async ctx => {
|
||||||
const { inviteCode } = ctx.request.body
|
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
||||||
try {
|
try {
|
||||||
const email = await checkInviteCode(inviteCode)
|
const email = await checkInviteCode(inviteCode)
|
||||||
// redirect the request
|
// only pass through certain props for accepting
|
||||||
delete ctx.request.body.inviteCode
|
ctx.request.body = {
|
||||||
ctx.request.body.email = email
|
firstName,
|
||||||
|
lastName,
|
||||||
|
password,
|
||||||
|
email,
|
||||||
|
}
|
||||||
// this will flesh out the body response
|
// this will flesh out the body response
|
||||||
await exports.save(ctx)
|
await exports.save(ctx)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../../controllers/admin/configs")
|
const controller = require("../../controllers/admin/configs")
|
||||||
const joiValidator = require("../../../middleware/joi-validator")
|
const joiValidator = require("../../../middleware/joi-validator")
|
||||||
|
const adminOnly = require("../../../middleware/adminOnly")
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
const { Configs, ConfigUploads } = require("../../../constants")
|
const { Configs, ConfigUploads } = require("../../../constants")
|
||||||
|
|
||||||
|
@ -77,8 +78,13 @@ function buildConfigGetValidation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.post("/api/admin/configs", buildConfigSaveValidation(), controller.save)
|
.post(
|
||||||
.delete("/api/admin/configs/:id", controller.destroy)
|
"/api/admin/configs",
|
||||||
|
adminOnly,
|
||||||
|
buildConfigSaveValidation(),
|
||||||
|
controller.save
|
||||||
|
)
|
||||||
|
.delete("/api/admin/configs/:id", adminOnly, controller.destroy)
|
||||||
.get("/api/admin/configs", controller.fetch)
|
.get("/api/admin/configs", controller.fetch)
|
||||||
.get("/api/admin/configs/checklist", controller.configChecklist)
|
.get("/api/admin/configs/checklist", controller.configChecklist)
|
||||||
.get(
|
.get(
|
||||||
|
@ -89,6 +95,7 @@ router
|
||||||
.get("/api/admin/configs/:type", buildConfigGetValidation(), controller.find)
|
.get("/api/admin/configs/:type", buildConfigGetValidation(), controller.find)
|
||||||
.post(
|
.post(
|
||||||
"/api/admin/configs/upload/:type/:name",
|
"/api/admin/configs/upload/:type/:name",
|
||||||
|
adminOnly,
|
||||||
buildUploadValidation(),
|
buildUploadValidation(),
|
||||||
controller.upload
|
controller.upload
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../../controllers/admin/groups")
|
const controller = require("../../controllers/admin/groups")
|
||||||
const joiValidator = require("../../../middleware/joi-validator")
|
const joiValidator = require("../../../middleware/joi-validator")
|
||||||
|
const adminOnly = require("../../../middleware/adminOnly")
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
@ -24,9 +25,14 @@ function buildGroupSaveValidation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.post("/api/admin/groups", buildGroupSaveValidation(), controller.save)
|
.post(
|
||||||
|
"/api/admin/groups",
|
||||||
|
adminOnly,
|
||||||
|
buildGroupSaveValidation(),
|
||||||
|
controller.save
|
||||||
|
)
|
||||||
.get("/api/admin/groups", controller.fetch)
|
.get("/api/admin/groups", controller.fetch)
|
||||||
.delete("/api/admin/groups/:id", controller.destroy)
|
.delete("/api/admin/groups/:id", adminOnly, controller.destroy)
|
||||||
.get("/api/admin/groups/:id", controller.find)
|
.get("/api/admin/groups/:id", controller.find)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../../controllers/admin/roles")
|
const controller = require("../../controllers/admin/roles")
|
||||||
|
const adminOnly = require("../../../middleware/adminOnly")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
.get("/api/admin/roles", controller.fetch)
|
.get("/api/admin/roles", adminOnly, controller.fetch)
|
||||||
.get("/api/admin/roles/:appId", controller.find)
|
.get("/api/admin/roles/:appId", adminOnly, controller.find)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -3,6 +3,7 @@ const controller = require("../../controllers/admin/templates")
|
||||||
const joiValidator = require("../../../middleware/joi-validator")
|
const joiValidator = require("../../../middleware/joi-validator")
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
const { TemplatePurpose, TemplateTypes } = require("../../../constants")
|
const { TemplatePurpose, TemplateTypes } = require("../../../constants")
|
||||||
|
const adminOnly = require("../../../middleware/adminOnly")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
@ -21,11 +22,16 @@ function buildTemplateSaveValidation() {
|
||||||
|
|
||||||
router
|
router
|
||||||
.get("/api/admin/template/definitions", controller.definitions)
|
.get("/api/admin/template/definitions", controller.definitions)
|
||||||
.post("/api/admin/template", buildTemplateSaveValidation(), controller.save)
|
.post(
|
||||||
|
"/api/admin/template",
|
||||||
|
adminOnly,
|
||||||
|
buildTemplateSaveValidation(),
|
||||||
|
controller.save
|
||||||
|
)
|
||||||
.get("/api/admin/template", controller.fetch)
|
.get("/api/admin/template", controller.fetch)
|
||||||
.get("/api/admin/template/:type", controller.fetchByType)
|
.get("/api/admin/template/:type", controller.fetchByType)
|
||||||
.get("/api/admin/template/:ownerId", controller.fetchByOwner)
|
.get("/api/admin/template/:ownerId", controller.fetchByOwner)
|
||||||
.get("/api/admin/template/:id", controller.find)
|
.get("/api/admin/template/:id", controller.find)
|
||||||
.delete("/api/admin/template/:id/:rev", controller.destroy)
|
.delete("/api/admin/template/:id/:rev", adminOnly, controller.destroy)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -1,28 +1,35 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../../controllers/admin/users")
|
const controller = require("../../controllers/admin/users")
|
||||||
const joiValidator = require("../../../middleware/joi-validator")
|
const joiValidator = require("../../../middleware/joi-validator")
|
||||||
|
const adminOnly = require("../../../middleware/adminOnly")
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
function buildUserSaveValidation() {
|
function buildUserSaveValidation(isSelf = false) {
|
||||||
// prettier-ignore
|
let schema = {
|
||||||
return joiValidator.body(Joi.object({
|
email: Joi.string().allow(null, ""),
|
||||||
_id: Joi.string(),
|
|
||||||
_rev: Joi.string(),
|
|
||||||
email: Joi.string(),
|
|
||||||
password: Joi.string().allow(null, ""),
|
password: Joi.string().allow(null, ""),
|
||||||
forcePasswordChange: Joi.boolean().optional(),
|
forcePasswordChange: Joi.boolean().optional(),
|
||||||
|
firstName: Joi.string().allow(null, ""),
|
||||||
|
lastName: Joi.string().allow(null, ""),
|
||||||
builder: Joi.object({
|
builder: Joi.object({
|
||||||
global: Joi.boolean().optional(),
|
global: Joi.boolean().optional(),
|
||||||
apps: Joi.array().optional(),
|
apps: Joi.array().optional(),
|
||||||
}).unknown(true).optional(),
|
})
|
||||||
// maps appId -> roleId for the user
|
|
||||||
roles: Joi.object()
|
|
||||||
.pattern(/.*/, Joi.string())
|
|
||||||
.required()
|
|
||||||
.unknown(true)
|
.unknown(true)
|
||||||
}).required().unknown(true))
|
.optional(),
|
||||||
|
// maps appId -> roleId for the user
|
||||||
|
roles: Joi.object().pattern(/.*/, Joi.string()).required().unknown(true),
|
||||||
|
}
|
||||||
|
if (!isSelf) {
|
||||||
|
schema = {
|
||||||
|
...schema,
|
||||||
|
_id: Joi.string(),
|
||||||
|
_rev: Joi.string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return joiValidator.body(Joi.object(schema).required().unknown(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInviteValidation() {
|
function buildInviteValidation() {
|
||||||
|
@ -41,13 +48,29 @@ function buildInviteAcceptValidation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.post("/api/admin/users", buildUserSaveValidation(), controller.save)
|
.post(
|
||||||
|
"/api/admin/users",
|
||||||
|
adminOnly,
|
||||||
|
buildUserSaveValidation(),
|
||||||
|
controller.save
|
||||||
|
)
|
||||||
.get("/api/admin/users", controller.fetch)
|
.get("/api/admin/users", controller.fetch)
|
||||||
.post("/api/admin/users/init", controller.adminUser)
|
.post("/api/admin/users/init", controller.adminUser)
|
||||||
.delete("/api/admin/users/:id", controller.destroy)
|
.get("/api/admin/users/self", controller.getSelf)
|
||||||
|
.post(
|
||||||
|
"/api/admin/users/self",
|
||||||
|
buildUserSaveValidation(true),
|
||||||
|
controller.updateSelf
|
||||||
|
)
|
||||||
|
.delete("/api/admin/users/:id", adminOnly, controller.destroy)
|
||||||
.get("/api/admin/users/:id", controller.find)
|
.get("/api/admin/users/:id", controller.find)
|
||||||
.get("/api/admin/roles/:appId")
|
.get("/api/admin/roles/:appId")
|
||||||
.post("/api/admin/users/invite", buildInviteValidation(), controller.invite)
|
.post(
|
||||||
|
"/api/admin/users/invite",
|
||||||
|
adminOnly,
|
||||||
|
buildInviteValidation(),
|
||||||
|
controller.invite
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/admin/users/invite/accept",
|
"/api/admin/users/invite/accept",
|
||||||
buildInviteAcceptValidation(),
|
buildInviteAcceptValidation(),
|
||||||
|
|
|
@ -30,7 +30,7 @@ describe("/api/admin/auth", () => {
|
||||||
expect(sendMailMock).toHaveBeenCalled()
|
expect(sendMailMock).toHaveBeenCalled()
|
||||||
const emailCall = sendMailMock.mock.calls[0][0]
|
const emailCall = sendMailMock.mock.calls[0][0]
|
||||||
// after this URL there should be a code
|
// after this URL there should be a code
|
||||||
const parts = emailCall.html.split("http://localhost:10000/reset?code=")
|
const parts = emailCall.html.split("http://localhost:10000/builder/auth/reset?code=")
|
||||||
code = parts[1].split("\"")[0]
|
code = parts[1].split("\"")[0]
|
||||||
expect(code).toBeDefined()
|
expect(code).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,7 +13,7 @@ describe("/api/admin/configs/checklist", () => {
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.init()
|
await config.init(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(setup.afterAll)
|
||||||
|
|
|
@ -30,7 +30,7 @@ describe("/api/admin/users", () => {
|
||||||
expect(sendMailMock).toHaveBeenCalled()
|
expect(sendMailMock).toHaveBeenCalled()
|
||||||
const emailCall = sendMailMock.mock.calls[0][0]
|
const emailCall = sendMailMock.mock.calls[0][0]
|
||||||
// after this URL there should be a code
|
// after this URL there should be a code
|
||||||
const parts = emailCall.html.split("http://localhost:10000/invite?code=")
|
const parts = emailCall.html.split("http://localhost:10000/builder/invite?code=")
|
||||||
code = parts[1].split("\"")[0]
|
code = parts[1].split("\"")[0]
|
||||||
expect(code).toBeDefined()
|
expect(code).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
|
@ -38,20 +38,25 @@ class TestConfiguration {
|
||||||
return request.body
|
return request.body
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init(createUser = true) {
|
||||||
// create a test user
|
if (createUser) {
|
||||||
await this._req(
|
// create a test user
|
||||||
{
|
await this._req(
|
||||||
email: "test@test.com",
|
{
|
||||||
password: "test",
|
email: "test@test.com",
|
||||||
_id: "us_uuid1",
|
password: "test",
|
||||||
builder: {
|
_id: "us_uuid1",
|
||||||
global: true,
|
builder: {
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
null,
|
||||||
null,
|
controllers.users.save
|
||||||
controllers.users.save
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async end() {
|
async end() {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = async (ctx, next) => {
|
||||||
|
if (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) {
|
||||||
|
ctx.throw(403, "Admin user only endpoint.")
|
||||||
|
}
|
||||||
|
return next()
|
||||||
|
}
|
|
@ -155,10 +155,12 @@ exports.sendEmail = async (
|
||||||
const context = await getSettingsTemplateContext(purpose, code)
|
const context = await getSettingsTemplateContext(purpose, code)
|
||||||
const message = {
|
const message = {
|
||||||
from: from || config.from,
|
from: from || config.from,
|
||||||
subject: await processString(subject || config.subject, context),
|
|
||||||
to: email,
|
to: email,
|
||||||
html: await buildEmail(purpose, email, context, { user, contents }),
|
html: await buildEmail(purpose, email, context, { user, contents }),
|
||||||
}
|
}
|
||||||
|
if (subject || config.subject) {
|
||||||
|
message.subject = await processString(subject || config.subject, context)
|
||||||
|
}
|
||||||
const response = await transport.sendMail(message)
|
const response = await transport.sendMail(message)
|
||||||
if (TEST_MODE) {
|
if (TEST_MODE) {
|
||||||
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
|
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
|
||||||
|
|
Loading…
Reference in New Issue