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",
|
||||
"repoId": 190729906,
|
||||
"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.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
|
||||
* 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.
|
||||
* @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()
|
||||
let allDbs = await CouchDB.allDbs()
|
||||
const appDbNames = allDbs.filter(dbName =>
|
||||
|
@ -176,12 +180,19 @@ exports.getAllApps = async (devApps = false) => {
|
|||
const apps = response
|
||||
.filter(result => result.status === "fulfilled")
|
||||
.map(({ value }) => value)
|
||||
return apps.filter(app => {
|
||||
if (devApps) {
|
||||
return app.appId.startsWith(exports.APP_DEV_PREFIX)
|
||||
}
|
||||
return !app.appId.startsWith(exports.APP_DEV_PREFIX)
|
||||
})
|
||||
if (!all) {
|
||||
return apps.filter(app => {
|
||||
if (dev) {
|
||||
return isDevApp(app)
|
||||
}
|
||||
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>}
|
||||
*/
|
||||
exports.getGlobalUserByEmail = async email => {
|
||||
if (email == null) {
|
||||
throw "Must supply an email address to view"
|
||||
}
|
||||
const db = getDB(StaticDatabases.GLOBAL.name)
|
||||
try {
|
||||
let users = (
|
||||
|
|
|
@ -45,3 +45,9 @@
|
|||
</span>
|
||||
<span class="spectrum-Checkbox-label">{text || ""}</span>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.spectrum-Checkbox-input {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -37,9 +37,28 @@
|
|||
const fieldId = id || generateID()
|
||||
let selectedImageIdx = 0
|
||||
let fileDragged = false
|
||||
let selectedUrl
|
||||
$: selectedImage = value?.[selectedImageIdx] ?? null
|
||||
$: 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) {
|
||||
if (
|
||||
|
@ -102,11 +121,13 @@
|
|||
<div class="gallery">
|
||||
<div class="title">
|
||||
<div class="filename">{selectedImage.name}</div>
|
||||
<div class="filesize">
|
||||
{#if selectedImage.size <= BYTES_IN_MB}
|
||||
{`${selectedImage.size / BYTES_IN_KB} KB`}
|
||||
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if}
|
||||
</div>
|
||||
{#if selectedImage.size}
|
||||
<div class="filesize">
|
||||
{#if selectedImage.size <= BYTES_IN_MB}
|
||||
{`${selectedImage.size / BYTES_IN_KB} KB`}
|
||||
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !disabled}
|
||||
<div class="delete-button" on:click={removeFile}>
|
||||
<Icon name="Close" />
|
||||
|
@ -114,7 +135,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{#if isImage}
|
||||
<img alt="preview" src={selectedImage.url} />
|
||||
<img alt="preview" src={selectedUrl} />
|
||||
{:else}
|
||||
<div class="placeholder">
|
||||
<div class="extension">{selectedImage.extension}</div>
|
||||
|
@ -142,11 +163,13 @@
|
|||
<div class="gallery">
|
||||
<div class="title">
|
||||
<div class="filename">{file.name}</div>
|
||||
<div class="filesize">
|
||||
{#if file.size <= BYTES_IN_MB}
|
||||
{`${file.size / BYTES_IN_KB} KB`}
|
||||
{:else}{`${file.size / BYTES_IN_MB} MB`}{/if}
|
||||
</div>
|
||||
{#if file.size}
|
||||
<div class="filesize">
|
||||
{#if file.size <= BYTES_IN_MB}
|
||||
{`${file.size / BYTES_IN_KB} KB`}
|
||||
{:else}{`${file.size / BYTES_IN_MB} MB`}{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !disabled}
|
||||
<div class="delete-button" on:click={removeFile}>
|
||||
<Icon name="Close" />
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
import "@spectrum-css/search/dist/index-vars.css"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let value = ""
|
||||
export let value = null
|
||||
export let placeholder = null
|
||||
export let disabled = false
|
||||
export let id = null
|
||||
export let updateOnChange = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let focus = false
|
||||
|
@ -23,6 +24,13 @@
|
|||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const onInput = event => {
|
||||
if (!updateOnChange) {
|
||||
return
|
||||
}
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const updateValueOnEnter = event => {
|
||||
if (event.key === "Enter") {
|
||||
updateValue(event.target.value)
|
||||
|
@ -44,15 +52,18 @@
|
|||
<use xlink:href="#spectrum-icon-18-Magnify" />
|
||||
</svg>
|
||||
<input
|
||||
on:click
|
||||
on:keyup={updateValueOnEnter}
|
||||
{disabled}
|
||||
{id}
|
||||
value={value || ""}
|
||||
placeholder={placeholder || ""}
|
||||
on:click
|
||||
on:blur
|
||||
on:focus
|
||||
on:input
|
||||
on:blur={onBlur}
|
||||
on:focus={onFocus}
|
||||
on:input
|
||||
on:input={onInput}
|
||||
on:keyup={updateValueOnEnter}
|
||||
type="search"
|
||||
class="spectrum-Textfield-input spectrum-Search-input"
|
||||
autocomplete="off"
|
||||
|
|
|
@ -26,3 +26,9 @@
|
|||
<span class="spectrum-Switch-switch" />
|
||||
<label class="spectrum-Switch-label" for={id}>{text}</label>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spectrum-Switch-input {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
import "@spectrum-css/textfield/dist/index-vars.css"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let value = ""
|
||||
export let value = null
|
||||
export let placeholder = null
|
||||
export let type = "text"
|
||||
export let disabled = false
|
||||
export let error = null
|
||||
export let id = null
|
||||
export let readonly = false
|
||||
export let updateOnChange = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let focus = false
|
||||
|
@ -37,7 +38,13 @@
|
|||
}
|
||||
focus = false
|
||||
updateValue(event.target.value)
|
||||
dispatch("blur")
|
||||
}
|
||||
|
||||
const onInput = event => {
|
||||
if (readonly || !updateOnChange) {
|
||||
return
|
||||
}
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const updateValueOnEnter = event => {
|
||||
|
@ -66,16 +73,19 @@
|
|||
</svg>
|
||||
{/if}
|
||||
<input
|
||||
on:click
|
||||
on:keyup={updateValueOnEnter}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{id}
|
||||
value={value || ""}
|
||||
placeholder={placeholder || ""}
|
||||
on:click
|
||||
on:blur
|
||||
on:focus
|
||||
on:input
|
||||
on:blur={onBlur}
|
||||
on:focus={onFocus}
|
||||
on:input
|
||||
on:input={onInput}
|
||||
on:keyup={updateValueOnEnter}
|
||||
{type}
|
||||
class="spectrum-Textfield-input"
|
||||
/>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let error = null
|
||||
export let updateOnChange = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -21,6 +22,7 @@
|
|||
|
||||
<Field {label} {labelPosition} {error}>
|
||||
<TextField
|
||||
{updateOnChange}
|
||||
{error}
|
||||
{disabled}
|
||||
{readonly}
|
||||
|
@ -31,5 +33,6 @@
|
|||
on:click
|
||||
on:input
|
||||
on:blur
|
||||
on:focus
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export let labelPosition = "above"
|
||||
export let placeholder = null
|
||||
export let disabled = false
|
||||
export let updateOnChange = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -18,11 +19,14 @@
|
|||
|
||||
<Field {label} {labelPosition}>
|
||||
<Search
|
||||
{updateOnChange}
|
||||
{disabled}
|
||||
{value}
|
||||
{placeholder}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
on:input
|
||||
on:focus
|
||||
on:blur
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
position: relative;
|
||||
}
|
||||
.paddingX-S {
|
||||
padding-left: var(--spacing-s);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
max-width: 80ch;
|
||||
margin: 0 auto;
|
||||
padding: calc(var(--spacing-xl) * 2);
|
||||
min-height: calc(100% - var(--spacing-xl) * 4);
|
||||
}
|
||||
|
||||
.wide {
|
||||
|
@ -22,5 +23,6 @@
|
|||
margin: 0;
|
||||
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2)
|
||||
calc(var(--spacing-xl) * 2) calc(var(--spacing-xl) * 2);
|
||||
min-height: calc(100% - var(--spacing-xl) * 3);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,27 +3,17 @@
|
|||
|
||||
export let size = "M"
|
||||
export let serif = false
|
||||
export let noPadding = false
|
||||
export let textAlign
|
||||
export let weight = null
|
||||
export let textAlign = null
|
||||
</script>
|
||||
|
||||
<p
|
||||
style="{textAlign ? `text-align:${textAlign}` : ``}"
|
||||
class:noPadding
|
||||
style={`
|
||||
${weight ? `font-weight:${weight};` : ""}
|
||||
${textAlign ? `text-align:${textAlign};` : ""}
|
||||
`}
|
||||
class="spectrum-Body spectrum-Body--size{size}"
|
||||
class:spectrum-Body--serif={serif}
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
|
||||
<style>
|
||||
p {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.noPadding {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</script>
|
||||
|
||||
<h1
|
||||
style="{textAlign ? `text-align:${textAlign}` : ``}"
|
||||
style={textAlign ? `text-align:${textAlign}` : ``}
|
||||
class:noPadding
|
||||
class="spectrum-Heading spectrum-Heading--size{size}"
|
||||
>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||
import QuerySelector from "./QuerySelector.svelte"
|
||||
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||
import CronBuilder from "./CronBuilder.svelte"
|
||||
import Editor from "components/integration/QueryEditor.svelte"
|
||||
|
||||
export let block
|
||||
|
@ -76,6 +77,8 @@
|
|||
/>
|
||||
{:else if value.customType === "query"}
|
||||
<QuerySelector bind:value={block.inputs[key]} />
|
||||
{:else if value.customType === "cron"}
|
||||
<CronBuilder bind:value={block.inputs[key]} />
|
||||
{:else if value.customType === "queryParams"}
|
||||
<QueryParamSelector bind:value={block.inputs[key]} {bindings} />
|
||||
{: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 />
|
||||
{#if schema && Object.keys(schema).length > 0}
|
||||
<CreateRowButton
|
||||
title={isUsersTable ? "Create user" : "Create row"}
|
||||
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow}
|
||||
/>
|
||||
{#if !isUsersTable}
|
||||
<CreateRowButton
|
||||
title={"Create row"}
|
||||
modalContentComponent={CreateEditRow}
|
||||
/>
|
||||
{/if}
|
||||
<CreateViewButton />
|
||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||
{#if isUsersTable}
|
||||
|
|
|
@ -47,6 +47,8 @@
|
|||
})
|
||||
schema.email.displayName = "Email"
|
||||
schema.roleId.displayName = "Role"
|
||||
schema.firstName.displayName = "First Name"
|
||||
schema.lastName.displayName = "Last Name"
|
||||
if (schema.status) {
|
||||
schema.status.displayName = "Status"
|
||||
}
|
||||
|
@ -101,7 +103,7 @@
|
|||
</div>
|
||||
<div class="popovers">
|
||||
<slot />
|
||||
{#if selectedRows.length > 0}
|
||||
{#if !isUsersTable && selectedRows.length > 0}
|
||||
<DeleteRowsButton {selectedRows} {deleteRows} />
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -114,7 +116,7 @@
|
|||
{customRenderers}
|
||||
{rowCount}
|
||||
bind:selectedRows
|
||||
allowSelectRows={allowEditing}
|
||||
allowSelectRows={allowEditing && !isUsersTable}
|
||||
allowEditRows={allowEditing}
|
||||
allowEditColumns={allowEditing}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
delete customSchema["email"]
|
||||
delete customSchema["roleId"]
|
||||
delete customSchema["status"]
|
||||
delete customSchema["firstName"]
|
||||
delete customSchema["lastName"]
|
||||
return Object.entries(customSchema)
|
||||
}
|
||||
|
||||
|
@ -67,7 +69,7 @@
|
|||
return false
|
||||
}
|
||||
|
||||
notifications.success("User saved successfully.")
|
||||
notifications.success("User saved successfully")
|
||||
rows.save(rowResponse)
|
||||
}
|
||||
</script>
|
||||
|
@ -84,8 +86,14 @@
|
|||
readonly={!creating}
|
||||
/>
|
||||
<RowFieldControl
|
||||
meta={{ name: "password", type: "password" }}
|
||||
bind:value={row.password}
|
||||
meta={{ ...tableSchema.firstName, name: "First Name" }}
|
||||
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
|
||||
selection is always undefined -->
|
||||
|
@ -97,16 +105,6 @@
|
|||
getOptionLabel={role => role.name}
|
||||
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]}
|
||||
{#if !meta.autocolumn}
|
||||
<RowFieldControl {meta} bind:value={row[key]} {creating} />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import api from "builderStore/api"
|
||||
|
@ -99,15 +99,19 @@
|
|||
const typeOptions = [
|
||||
{
|
||||
label: "Text",
|
||||
value: "string",
|
||||
value: FIELDS.STRING.type,
|
||||
},
|
||||
{
|
||||
label: "Number",
|
||||
value: "number",
|
||||
value: FIELDS.NUMBER.type,
|
||||
},
|
||||
{
|
||||
label: "Date",
|
||||
value: "datetime",
|
||||
value: FIELDS.DATETIME.type,
|
||||
},
|
||||
{
|
||||
label: "Options",
|
||||
value: FIELDS.OPTIONS.type,
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
|
|
@ -3,7 +3,14 @@
|
|||
import { store } from "builderStore"
|
||||
import { tables } from "stores/backend"
|
||||
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 analytics from "analytics"
|
||||
import screenTemplates from "builderStore/store/screenTemplates"
|
||||
|
@ -123,8 +130,10 @@
|
|||
bind:value={createAutoscreens}
|
||||
/>
|
||||
<div>
|
||||
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
|
||||
<TableDataImport bind:dataImport />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
|
||||
<TableDataImport bind:dataImport />
|
||||
</Layout>
|
||||
</div>
|
||||
</ModalContent>
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
FAILURE: "FAILURE",
|
||||
}
|
||||
|
||||
const POLL_INTERVAL = 1000
|
||||
const POLL_INTERVAL = 10000
|
||||
|
||||
let loading = false
|
||||
let feedbackModal
|
||||
|
@ -61,7 +61,6 @@
|
|||
|
||||
// Required to check any updated deployment statuses between polls
|
||||
function checkIncomingDeploymentStatus(current, incoming) {
|
||||
console.log(current, incoming)
|
||||
for (let incomingDeployment of incoming) {
|
||||
if (
|
||||
incomingDeployment.status === DeploymentStatus.FAILURE ||
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
this={control}
|
||||
{componentInstance}
|
||||
value={safeValue}
|
||||
updateOnChange={false}
|
||||
on:change={handleChange}
|
||||
onChange={handleChange}
|
||||
{type}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<script>
|
||||
import {
|
||||
notifications,
|
||||
Input,
|
||||
Button,
|
||||
Layout,
|
||||
Body,
|
||||
Heading,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { auth } from "stores/backend"
|
||||
import { organisation, auth } from "stores/portal"
|
||||
|
||||
let email = ""
|
||||
|
||||
|
@ -25,17 +25,19 @@
|
|||
<div class="main">
|
||||
<Layout>
|
||||
<Layout noPadding justifyItems="center">
|
||||
<img src="https://i.imgur.com/ZKyklgF.png" />
|
||||
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading textAlign="center">Forgotten your password?</Heading>
|
||||
<Body size="S" textAlign="center">
|
||||
No problem! Just enter your account's email address and we'll send
|
||||
you a link to reset it.
|
||||
No problem! Just enter your account's email address and we'll send you
|
||||
a link to reset it.
|
||||
</Body>
|
||||
<Input label="Email" bind:value={email} />
|
||||
</Layout>
|
||||
<Button cta on:click={forgot} disabled={!email}>Reset your password</Button>
|
||||
<Button cta on:click={forgot} disabled={!email}>
|
||||
Reset your password
|
||||
</Button>
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<script>
|
||||
import { Link } from "@budibase/bbui"
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
import GoogleLogo from "/assets/google-logo.png"
|
||||
</script>
|
||||
|
||||
<div class="outer">
|
||||
<Link target="_blank" href="/api/admin/auth/google">
|
||||
<ActionButton>
|
||||
<a target="_blank" href="/api/admin/auth/google">
|
||||
<div class="inner">
|
||||
<img src={GoogleLogo} alt="google icon" />
|
||||
<p>Sign in with Google</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</a>
|
||||
</ActionButton>
|
||||
|
||||
<style>
|
||||
.outer {
|
||||
|
@ -34,10 +34,8 @@
|
|||
.inner p {
|
||||
margin: 0;
|
||||
}
|
||||
.outer :global(a) {
|
||||
a {
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: var(--font-size-m);
|
||||
color: #fff;
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
Heading,
|
||||
} from "@budibase/bbui"
|
||||
import GoogleButton from "./GoogleButton.svelte"
|
||||
import { auth } from "stores/backend"
|
||||
import { organisation, auth } from "stores/portal"
|
||||
|
||||
let username = ""
|
||||
let password = ""
|
||||
|
@ -35,12 +35,12 @@
|
|||
<div class="main">
|
||||
<Layout>
|
||||
<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>
|
||||
</Layout>
|
||||
<GoogleButton />
|
||||
<Divider noGrid />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Divider noGrid />
|
||||
<Body size="S" textAlign="center">Sign in with email</Body>
|
||||
<Input label="Email" bind:value={username} />
|
||||
<Input
|
||||
|
@ -50,7 +50,7 @@
|
|||
bind:value={password}
|
||||
/>
|
||||
</Layout>
|
||||
<Layout gap="S" noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Button cta on:click={login}>Sign in to Budibase</Button>
|
||||
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
||||
Forgot password?
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<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 { params, goto } from "@roxi/routify"
|
||||
import { auth } from "stores/backend"
|
||||
|
||||
const resetCode = $params["?code"]
|
||||
let password, error
|
||||
|
@ -16,7 +16,6 @@
|
|||
} catch (err) {
|
||||
notifications.error("Unable to reset password")
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -24,7 +23,7 @@
|
|||
<div class="main">
|
||||
<Layout>
|
||||
<Layout noPadding justifyItems="center">
|
||||
<img src="https://i.imgur.com/ZKyklgF.png" />
|
||||
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading textAlign="center">Reset your password</Heading>
|
||||
|
@ -33,7 +32,9 @@
|
|||
</Body>
|
||||
<PasswordRepeatInput bind:password bind:error />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,12 +6,9 @@
|
|||
Layout,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Link,
|
||||
} from "@budibase/bbui"
|
||||
import { gradient } from "actions"
|
||||
import { AppStatus } from "constants"
|
||||
import { url } from "@roxi/routify"
|
||||
import { auth } from "stores/backend"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
export let app
|
||||
export let exportApp
|
||||
|
@ -25,11 +22,11 @@
|
|||
<Layout noPadding gap="XS" alignContent="start">
|
||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||
<div class="title">
|
||||
<Link on:click={() => openApp(app)}>
|
||||
<div class="name" on:click={() => openApp(app)}>
|
||||
<Heading size="XS">
|
||||
{app.name}
|
||||
</Heading>
|
||||
</Link>
|
||||
</div>
|
||||
<ActionMenu align="right">
|
||||
<Icon slot="control" name="More" hoverable />
|
||||
<MenuItem on:click={() => exportApp(app)} icon="Download">
|
||||
|
@ -48,7 +45,7 @@
|
|||
</ActionMenu>
|
||||
</div>
|
||||
<div class="status">
|
||||
<Body noPadding size="S">
|
||||
<Body size="S">
|
||||
Edited {Math.floor(1 + Math.random() * 10)} months ago
|
||||
</Body>
|
||||
{#if app.lockedBy}
|
||||
|
@ -76,7 +73,7 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.title :global(a) {
|
||||
.name {
|
||||
text-decoration: none;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
|
|
|
@ -1,16 +1,7 @@
|
|||
<script>
|
||||
import { gradient } from "actions"
|
||||
import {
|
||||
Heading,
|
||||
Button,
|
||||
Icon,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Link,
|
||||
} from "@budibase/bbui"
|
||||
import { AppStatus } from "constants"
|
||||
import { url } from "@roxi/routify"
|
||||
import { auth } from "stores/backend"
|
||||
import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
export let app
|
||||
export let openApp
|
||||
|
@ -23,19 +14,24 @@
|
|||
|
||||
<div class="title" class:last>
|
||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||
<Link on:click={() => openApp(app)}>
|
||||
<div class="name" on:click={() => openApp(app)}>
|
||||
<Heading size="XS">
|
||||
{app.name}
|
||||
</Heading>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div class:last>
|
||||
Edited {Math.round(Math.random() * 10 + 1)} months ago
|
||||
</div>
|
||||
<div class:last>
|
||||
{#if app.lockedBy}
|
||||
<div class="status status--locked-you" />
|
||||
Locked by {app.lockedBy.email}
|
||||
{#if app.lockedBy.email === $auth.user.email}
|
||||
<div class="status status--locked-you" />
|
||||
Locked by you
|
||||
{:else}
|
||||
<div class="status status--locked-other" />
|
||||
Locked by {app.lockedBy.email}
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="status status--open" />
|
||||
Open
|
||||
|
@ -63,7 +59,7 @@
|
|||
width: 40px;
|
||||
border-radius: var(--border-radius-s);
|
||||
}
|
||||
.title :global(a) {
|
||||
.name {
|
||||
text-decoration: none;
|
||||
}
|
||||
.title :global(h1:hover) {
|
||||
|
|
|
@ -15,7 +15,14 @@ export const AppStatus = {
|
|||
}
|
||||
|
||||
// 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 = {
|
||||
MASTER: {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { goto, isActive } from "@roxi/routify"
|
||||
import { auth } from "stores/backend"
|
||||
import { admin } from "stores/portal"
|
||||
import { isActive, redirect } from "@roxi/routify"
|
||||
import { admin, auth } from "stores/portal"
|
||||
|
||||
let loaded = false
|
||||
$: hasAdminUser = !!$admin?.checklist?.adminUser
|
||||
|
@ -16,7 +15,7 @@
|
|||
// Force creation of an admin user if one doesn't exist
|
||||
$: {
|
||||
if (loaded && !hasAdminUser) {
|
||||
$goto("./admin")
|
||||
$redirect("./admin")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,7 +28,7 @@
|
|||
!$isActive("./auth") &&
|
||||
!$isActive("./invite")
|
||||
) {
|
||||
$goto("./auth/login")
|
||||
$redirect("./auth/login")
|
||||
}
|
||||
}
|
||||
</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"
|
||||
import { goto } from "@roxi/routify"
|
||||
import api from "builderStore/api"
|
||||
import { admin } from "stores/portal"
|
||||
import { admin, organisation } from "stores/portal"
|
||||
|
||||
let adminUser = {}
|
||||
|
||||
|
@ -32,22 +32,22 @@
|
|||
|
||||
<section>
|
||||
<div class="container">
|
||||
<Layout gap="XS">
|
||||
<img src="https://i.imgur.com/ZKyklgF.png" />
|
||||
</Layout>
|
||||
<div class="center">
|
||||
<Layout gap="XS">
|
||||
<Layout>
|
||||
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||
<Layout gap="XS" justifyItems="center" noPadding>
|
||||
<Heading size="M">Create an admin user</Heading>
|
||||
<Body size="M"
|
||||
>The admin user has access to everything in Budibase.</Body
|
||||
>
|
||||
<Body size="M" textAlign="center">
|
||||
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>
|
||||
</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>
|
||||
</Layout>
|
||||
</div>
|
||||
|
@ -68,9 +68,6 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
width: 40px;
|
||||
margin: 0 auto;
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
<script>
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { roles } from "stores/backend"
|
||||
import {
|
||||
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 { Button, Icon, ActionGroup, Tabs, Tab } from "@budibase/bbui"
|
||||
import DeployModal from "components/deploy/DeployModal.svelte"
|
||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||
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>
|
||||
import { goto } from "@roxi/routify"
|
||||
$goto("./login")
|
||||
import { redirect } from "@roxi/routify"
|
||||
$redirect("./login")
|
||||
</script>
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
$goto("./portal")
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
$: {
|
||||
if (!$auth.user) {
|
||||
$redirect("./auth/login")
|
||||
} else if ($auth.user.builder?.global) {
|
||||
$redirect("./portal")
|
||||
} else {
|
||||
$redirect("./apps")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { users, organisation } from "stores/portal"
|
||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||
import { users } from "stores/portal"
|
||||
|
||||
const inviteCode = $params["?code"]
|
||||
let password, error
|
||||
|
@ -23,19 +23,18 @@
|
|||
|
||||
<section>
|
||||
<div class="container">
|
||||
<Layout gap="XS">
|
||||
<img src="https://i.imgur.com/ZKyklgF.png" />
|
||||
</Layout>
|
||||
<Layout gap="XS">
|
||||
<Heading textAlign="center" size="M">Accept Invitation</Heading>
|
||||
<Body textAlign="center" size="S"
|
||||
>Please enter a password to setup your user.</Body
|
||||
>
|
||||
<Layout>
|
||||
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||
<Layout gap="XS" justifyItems="center" noPadding>
|
||||
<Heading size="M">Accept Invitation</Heading>
|
||||
<Body textAlign="center" size="M">
|
||||
Please enter a password to set up your user.
|
||||
</Body>
|
||||
</Layout>
|
||||
<PasswordRepeatInput bind:error bind:password />
|
||||
</Layout>
|
||||
<Layout gap="S">
|
||||
<Button disabled={error} cta on:click={acceptInvite}>Accept invite</Button
|
||||
>
|
||||
<Button disabled={error} cta on:click={acceptInvite}>
|
||||
Accept invite
|
||||
</Button>
|
||||
</Layout>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { isActive, goto } from "@roxi/routify"
|
||||
import { isActive, redirect, goto } from "@roxi/routify"
|
||||
import {
|
||||
Icon,
|
||||
Avatar,
|
||||
|
@ -12,15 +12,14 @@
|
|||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
||||
import { organisation } from "stores/portal"
|
||||
import { auth } from "stores/backend"
|
||||
import { organisation, auth } from "stores/portal"
|
||||
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let oldSettingsModal
|
||||
let loaded = false
|
||||
|
||||
organisation.init()
|
||||
|
||||
let menu = [
|
||||
const menu = [
|
||||
{ title: "Apps", href: "/builder/portal/apps" },
|
||||
{ title: "Drafts", href: "/builder/portal/drafts" },
|
||||
{ title: "Users", href: "/builder/portal/manage/users", heading: "Manage" },
|
||||
|
@ -35,54 +34,69 @@
|
|||
{ title: "Theming", href: "/builder/portal/theming" },
|
||||
{ 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>
|
||||
|
||||
<div class="container">
|
||||
<div class="nav">
|
||||
<Layout paddingX="L" paddingY="L">
|
||||
<div class="branding">
|
||||
<div class="name" on:click={() => $goto("./apps")}>
|
||||
<img
|
||||
src={$organisation?.logoUrl || "https://i.imgur.com/ZKyklgF.png"}
|
||||
alt="Logotype"
|
||||
/>
|
||||
<span>{$organisation?.company || "Budibase"}</span>
|
||||
{#if loaded}
|
||||
<div class="container">
|
||||
<div class="nav">
|
||||
<Layout paddingX="L" paddingY="L">
|
||||
<div class="branding">
|
||||
<div class="name" on:click={() => $goto("./apps")}>
|
||||
<img
|
||||
src={$organisation?.logoUrl || "https://i.imgur.com/ZKyklgF.png"}
|
||||
alt="Logotype"
|
||||
/>
|
||||
<span>{$organisation?.company || "Budibase"}</span>
|
||||
</div>
|
||||
<div class="onboarding">
|
||||
<ConfigChecklist />
|
||||
</div>
|
||||
</div>
|
||||
<div class="onboarding">
|
||||
<ConfigChecklist />
|
||||
<div class="menu">
|
||||
<Navigation>
|
||||
{#each menu as { title, href, heading }}
|
||||
<Item selected={$isActive(href)} {href} {heading}>{title}</Item>
|
||||
{/each}
|
||||
</Navigation>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</Layout>
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
<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="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>
|
||||
<Modal bind:this={oldSettingsModal} width="30%">
|
||||
<BuilderSettingsModal />
|
||||
</Modal>
|
||||
<Modal bind:this={oldSettingsModal} width="30%">
|
||||
<BuilderSettingsModal />
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
import api, { del } from "builderStore/api"
|
||||
import analytics from "analytics"
|
||||
import { onMount } from "svelte"
|
||||
import { apps } from "stores/portal"
|
||||
import { auth } from "stores/backend"
|
||||
import { apps, auth } from "stores/portal"
|
||||
import download from "downloadjs"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
@ -99,8 +98,7 @@
|
|||
if (!appToDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
await del(`/api/applications/${appToDelete?._id}`)
|
||||
await del(`/api/applications/${appToDelete?.appId}`)
|
||||
await apps.load()
|
||||
appToDelete = null
|
||||
notifications.success("App deleted successfully.")
|
||||
|
@ -160,7 +158,7 @@
|
|||
/>
|
||||
</ActionGroup>
|
||||
</div>
|
||||
{#if $apps.length}
|
||||
{#if loaded && $apps.length}
|
||||
<div
|
||||
class:appGrid={layout === "grid"}
|
||||
class:appTable={layout === "table"}
|
||||
|
@ -230,7 +228,7 @@
|
|||
}
|
||||
|
||||
.select {
|
||||
width: 120px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.appGrid {
|
||||
|
@ -248,7 +246,7 @@
|
|||
height: 70px;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
grid-gap: var(--spacing-xl);
|
||||
grid-template-columns: auto 1fr;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
$goto("./apps")
|
||||
import { redirect } from "@roxi/routify"
|
||||
$redirect("./apps")
|
||||
</script>
|
||||
|
|
|
@ -59,44 +59,42 @@
|
|||
})
|
||||
</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>
|
||||
<Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<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>
|
||||
</Layout>
|
||||
{#if google}
|
||||
<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>
|
||||
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">
|
||||
<span>
|
||||
<GoogleLogo />
|
||||
Google
|
||||
</span>
|
||||
</Heading>
|
||||
<Body size="S">
|
||||
To allow users to authenticate using their Google accounts, fill out the
|
||||
fields below.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
{#each ConfigFields.Google as field}
|
||||
<div class="form-row">
|
||||
<Label size="L">{field}</Label>
|
||||
<Input bind:value={google.config[field]} />
|
||||
</div>
|
||||
{/each}
|
||||
<div>
|
||||
<Button primary on:click={() => save(google)}>Save</Button>
|
||||
</div>
|
||||
<Divider />
|
||||
{/if}
|
||||
</Layout>
|
||||
</Page>
|
||||
</Layout>
|
||||
<div>
|
||||
<Button cta on:click={() => save(google)}>Save</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
{#each bindings as binding}
|
||||
<MenuItem on:click={() => onBindingClick(binding)}>
|
||||
<Detail size="M">{binding.name}</Detail>
|
||||
<Body size="XS" noPadding>{binding.description}</Body>
|
||||
<Body size="XS">{binding.description}</Body>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</Menu>
|
||||
|
|
|
@ -102,59 +102,57 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<Page>
|
||||
<header>
|
||||
<Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="M">Email</Heading>
|
||||
<Body size="S">
|
||||
<Body>
|
||||
Sending email is not required, but highly recommended for processes such
|
||||
as password recovery. To setup automated auth emails, simply add the
|
||||
values below and click activate.
|
||||
</Body>
|
||||
</header>
|
||||
</Layout>
|
||||
<Divider />
|
||||
{#if smtpConfig}
|
||||
<div class="config-form">
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">SMTP</Heading>
|
||||
<Body size="S">
|
||||
To allow your app to benefit from automated auth emails, add your SMTP
|
||||
details below.
|
||||
</Body>
|
||||
<Layout gap="S">
|
||||
<Heading size="S">
|
||||
<span />
|
||||
</Heading>
|
||||
<div class="form-row">
|
||||
<Label>Host</Label>
|
||||
<Input bind:value={smtpConfig.config.host} />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<Label>Port</Label>
|
||||
<Input type="number" bind:value={smtpConfig.config.port} />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<Label>User</Label>
|
||||
<Input bind:value={smtpConfig.config.auth.user} />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<Label>Password</Label>
|
||||
<Input type="password" bind:value={smtpConfig.config.auth.pass} />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<Label>From email address</Label>
|
||||
<Input type="email" bind:value={smtpConfig.config.from} />
|
||||
</div>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<div class="form-row">
|
||||
<Label size="L">Host</Label>
|
||||
<Input bind:value={smtpConfig.config.host} />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<Label size="L">Port</Label>
|
||||
<Input type="number" bind:value={smtpConfig.config.port} />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<Label size="L">User</Label>
|
||||
<Input bind:value={smtpConfig.config.auth.user} />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<Label size="L">Password</Label>
|
||||
<Input type="password" bind:value={smtpConfig.config.auth.pass} />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<Label size="L">From email address</Label>
|
||||
<Input type="email" bind:value={smtpConfig.config.from} />
|
||||
</div>
|
||||
</Layout>
|
||||
<div>
|
||||
<Button cta on:click={saveSmtp}>Save</Button>
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
<div class="config-form">
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Templates</Heading>
|
||||
<Body size="S">
|
||||
Budibase comes out of the box with ready-made email templates to help
|
||||
with user onboarding. Please refrain from changing the links.
|
||||
</Body>
|
||||
</div>
|
||||
</Layout>
|
||||
<Table
|
||||
{customRenderers}
|
||||
data={$email.templates}
|
||||
|
@ -165,27 +163,13 @@
|
|||
allowEditColumns={false}
|
||||
/>
|
||||
{/if}
|
||||
</Page>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.config-form {
|
||||
margin-top: 42px;
|
||||
margin-bottom: 42px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-template-columns: 25% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 42px;
|
||||
}
|
||||
</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 }) {
|
||||
console.log(detail)
|
||||
selectedApp = detail
|
||||
editRolesModal.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Layout noPadding gap="XS">
|
||||
<div class="back">
|
||||
<ActionButton on:click={() => $goto("./")} quiet size="S" icon="BackAndroid"
|
||||
>Back to users</ActionButton
|
||||
>
|
||||
</div>
|
||||
<div class="heading">
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading>User: {$userFetch?.data?.email}</Heading>
|
||||
<Body
|
||||
>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>
|
||||
</div>
|
||||
<Layout noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<div class="back">
|
||||
<ActionButton
|
||||
on:click={() => $goto("./")}
|
||||
quiet
|
||||
size="S"
|
||||
icon="BackAndroid"
|
||||
>
|
||||
Back to users
|
||||
</ActionButton>
|
||||
</div>
|
||||
<Heading>User: {$userFetch?.data?.email}</Heading>
|
||||
<Body>
|
||||
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" />
|
||||
<div class="general">
|
||||
<Layout gap="S" noPadding>
|
||||
<Heading size="S">General</Heading>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
|
@ -97,6 +99,14 @@
|
|||
<Label size="L">Group(s)</Label>
|
||||
<Select disabled options={["All users"]} value="All users" />
|
||||
</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">
|
||||
<Label size="L">Development access?</Label>
|
||||
<Toggle
|
||||
|
@ -115,9 +125,9 @@
|
|||
on:click={resetPasswordModal.show}>Force password reset</ActionButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<div class="roles">
|
||||
<Layout gap="S" noPadding>
|
||||
<Heading size="S">Configure roles</Heading>
|
||||
<Table
|
||||
on:click={openUpdateRolesModal}
|
||||
|
@ -128,16 +138,14 @@
|
|||
allowSelectRows={false}
|
||||
customRenderers={[{ column: "role", component: TagsRenderer }]}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<div class="delete">
|
||||
<Layout gap="S" noPadding
|
||||
><Heading size="S">Delete user</Heading>
|
||||
<Body>Deleting a user completely removes them from your account.</Body>
|
||||
<div class="delete-button">
|
||||
<Button warning on:click={deleteUserModal.show}>Delete user</Button>
|
||||
</div></Layout
|
||||
>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Delete user</Heading>
|
||||
<Body>Deleting a user completely removes them from your account.</Body>
|
||||
</Layout>
|
||||
<div class="delete-button">
|
||||
<Button warning on:click={deleteUserModal.show}>Delete user</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
|
@ -150,10 +158,9 @@
|
|||
cancelText="Cancel"
|
||||
showCloseIcon={false}
|
||||
>
|
||||
<Body
|
||||
>Are you sure you want to delete <strong>{$userFetch?.data?.email}</strong
|
||||
></Body
|
||||
>
|
||||
<Body>
|
||||
Are you sure you want to delete <strong>{$userFetch?.data?.email}</strong>
|
||||
</Body>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Modal bind:this={editRolesModal}>
|
||||
|
@ -174,26 +181,12 @@
|
|||
.fields {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-m);
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 32% 1fr;
|
||||
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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
|
@ -30,18 +30,18 @@
|
|||
<ModalContent
|
||||
onConfirm={createUserFlow}
|
||||
size="M"
|
||||
title="Add new user options"
|
||||
title="Add new user"
|
||||
confirmText="Add user"
|
||||
confirmDisabled={disabled}
|
||||
cancelText="Cancel"
|
||||
disabled={$error}
|
||||
showCloseIcon={false}
|
||||
>
|
||||
<Body noPadding
|
||||
>If you have SMTP configured and an email for the new user, you can use the
|
||||
<Body size="S">
|
||||
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
|
||||
with autogenerated passwords.</Body
|
||||
>
|
||||
with autogenerated passwords.
|
||||
</Body>
|
||||
<Select
|
||||
placeholder={null}
|
||||
bind:value={selected}
|
||||
|
|
|
@ -26,10 +26,10 @@
|
|||
error={$touched && $error}
|
||||
showCloseIcon={false}
|
||||
>
|
||||
<Body noPadding
|
||||
>Below you will find the user’s username and password. The password will not
|
||||
be accessible from this point. Please download the credentials.</Body
|
||||
>
|
||||
<Body size="S">
|
||||
Below you will find the user’s username and password. The password will not
|
||||
be accessible from this point. Please save the credentials.
|
||||
</Body>
|
||||
<Input
|
||||
type="email"
|
||||
label="Username"
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
|
||||
const displayLimit = 5
|
||||
|
||||
$: tags = value?.slice(0, displayLimit) ?? []
|
||||
$: leftover = (value?.length ?? 0) - tags.length
|
||||
$: roles = value?.filter(role => role != null) ?? []
|
||||
$: tags = roles.slice(0, displayLimit)
|
||||
$: leftover = roles.length - tags.length
|
||||
</script>
|
||||
|
||||
<Tags>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
const roles = app.roles
|
||||
let options = roles.map(role => role._id)
|
||||
let selectedRole
|
||||
let selectedRole = user?.roles?.[app?._id]
|
||||
|
||||
async function updateUserRoles() {
|
||||
const res = await users.save({
|
||||
|
@ -23,7 +23,7 @@
|
|||
if (res.status === 400) {
|
||||
notifications.error("Failed to update role")
|
||||
} else {
|
||||
notifications.success("Roles updated")
|
||||
notifications.success("Role updated")
|
||||
dispatch("update")
|
||||
}
|
||||
}
|
||||
|
@ -31,20 +31,20 @@
|
|||
|
||||
<ModalContent
|
||||
onConfirm={updateUserRoles}
|
||||
title="Update App Roles"
|
||||
confirmText="Update roles"
|
||||
title="Update App Role"
|
||||
confirmText="Update role"
|
||||
cancelText="Cancel"
|
||||
size="M"
|
||||
showCloseIcon={false}
|
||||
>
|
||||
<Body noPadding
|
||||
>Update {user.email}'s roles for <strong>{app.name}</strong>.</Body
|
||||
>
|
||||
<Body>
|
||||
Update {user.email}'s role for <strong>{app.name}</strong>.
|
||||
</Body>
|
||||
<Select
|
||||
placeholder={null}
|
||||
bind:value={selectedRole}
|
||||
on:change
|
||||
{options}
|
||||
label="Select roles:"
|
||||
label="Role"
|
||||
/>
|
||||
</ModalContent>
|
||||
|
|
|
@ -48,28 +48,27 @@
|
|||
</script>
|
||||
|
||||
<Layout>
|
||||
<div class="heading">
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading>Users</Heading>
|
||||
<Body
|
||||
>Users are the common denominator in Budibase. Each user is assigned to a
|
||||
<Body>
|
||||
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
|
||||
users, or edit and delete an existing user.</Body
|
||||
>
|
||||
</div>
|
||||
users, or edit and delete an existing user.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
|
||||
<div class="users">
|
||||
<Heading size="S">Users</Heading>
|
||||
<Layout gap="S" noPadding>
|
||||
<div class="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">
|
||||
<Label size="L">Search / filter</Label>
|
||||
<Search bind:value={search} placeholder="" />
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<ButtonGroup>
|
||||
<Button disabled secondary>Import users</Button>
|
||||
<Button overBackground on:click={createUserModal.show}>Add user</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<Table
|
||||
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
||||
{schema}
|
||||
|
@ -79,31 +78,28 @@
|
|||
allowSelectRows={false}
|
||||
customRenderers={[{ column: "group", component: TagsRenderer }]}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<Modal bind:this={createUserModal}
|
||||
><AddUserModal on:change={openBasicOnoboardingModal} /></Modal
|
||||
>
|
||||
<Modal bind:this={createUserModal}>
|
||||
<AddUserModal on:change={openBasicOnoboardingModal} />
|
||||
</Modal>
|
||||
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal>
|
||||
|
||||
<style>
|
||||
.users {
|
||||
position: relative;
|
||||
}
|
||||
.field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
grid-gap: var(--spacing-m);
|
||||
margin: var(--spacing-xl) 0;
|
||||
}
|
||||
.field > :global(*) + :global(*) {
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
.buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
.users-heading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</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 { post } from "builderStore/api"
|
||||
import analytics from "analytics"
|
||||
let analyticsDisabled = analytics.disabled()
|
||||
|
||||
function toggleAnalytics() {
|
||||
if (analyticsDisabled) {
|
||||
analytics.optIn()
|
||||
} else {
|
||||
analytics.optOut()
|
||||
}
|
||||
}
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
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 file
|
||||
|
||||
async function uploadLogo() {
|
||||
async function uploadLogo(file) {
|
||||
let data = new FormData()
|
||||
data.append("file", file)
|
||||
|
||||
const res = await post("/api/admin/configs/upload/settings/logo", data, {})
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
loading = true
|
||||
await toggleAnalytics()
|
||||
if (file) {
|
||||
await uploadLogo()
|
||||
|
||||
// Set analytics preference
|
||||
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({
|
||||
company: $organisation.company,
|
||||
platformUrl: $organisation.platformUrl,
|
||||
company: $values.company ?? "",
|
||||
platformUrl: $values.platformUrl ?? "",
|
||||
})
|
||||
if (res.status === 200) {
|
||||
notifications.success("Settings saved.")
|
||||
notifications.success("Settings saved successfully")
|
||||
} else {
|
||||
notifications.error(res.message)
|
||||
}
|
||||
|
||||
loading = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<Layout noPadding>
|
||||
<div class="intro">
|
||||
<Heading size="M">General</Heading>
|
||||
<Body>
|
||||
General is the place where you edit your organisation name, logo. You
|
||||
can also configure your platform URL as well as turn on or off
|
||||
analytics.
|
||||
</Body>
|
||||
<Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="M">General</Heading>
|
||||
<Body>
|
||||
General is the place where you edit your organisation name, logo. You can
|
||||
also configure your platform URL as well as turn on or off analytics.
|
||||
</Body>
|
||||
</Layout>
|
||||
<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>
|
||||
<Divider size="S" />
|
||||
<div class="information">
|
||||
<Heading size="S">Information</Heading>
|
||||
<Body>Here you can update your logo and organization name.</Body>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Organization name</Label>
|
||||
<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 class="field logo">
|
||||
<Label size="L">Logo</Label>
|
||||
<div class="file">
|
||||
<Dropzone
|
||||
value={[$values.logo]}
|
||||
on:change={e => {
|
||||
$values.logo = e.detail?.[0]
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider size="S" />
|
||||
<div class="analytics">
|
||||
<Heading size="S">Platform</Heading>
|
||||
<Body>Here you can set up general platform settings.</Body>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Platform URL</Label>
|
||||
<Input thin bind:value={$organisation.platformUrl} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider size="S" />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Platform</Heading>
|
||||
<Body size="S">Here you can set up general platform settings.</Body>
|
||||
</Layout>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Platform URL</Label>
|
||||
<Input thin bind:value={$values.platformUrl} />
|
||||
</div>
|
||||
<Divider size="S" />
|
||||
<div class="analytics">
|
||||
</div>
|
||||
<Divider size="S" />
|
||||
<Layout gap="S" noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Analytics</Heading>
|
||||
<Body>
|
||||
<Body size="S">
|
||||
If you would like to send analytics that help us make Budibase better,
|
||||
please let us know below.
|
||||
</Body>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Send Analytics to Budibase</Label>
|
||||
<Toggle text="" value={!analyticsDisabled} />
|
||||
</div>
|
||||
</Layout>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Send Analytics to Budibase</Label>
|
||||
<Toggle text="" bind:value={$values.analytics} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="save">
|
||||
<Button disabled={loading} on:click={saveConfig} cta>Save</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
<div>
|
||||
<Button disabled={loading} on:click={saveConfig} cta>Save</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-m);
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 32% 1fr;
|
||||
grid-template-columns: 33% 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
.file {
|
||||
|
@ -134,10 +142,4 @@
|
|||
.logo {
|
||||
align-items: start;
|
||||
}
|
||||
.intro {
|
||||
display: grid;
|
||||
}
|
||||
.save {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
$goto("./builder")
|
||||
import { redirect } from "@roxi/routify"
|
||||
$redirect("./builder")
|
||||
</script>
|
||||
|
|
|
@ -7,4 +7,3 @@ export { roles } from "./roles"
|
|||
export { datasources } from "./datasources"
|
||||
export { integrations } from "./integrations"
|
||||
export { queries } from "./queries"
|
||||
export { auth } from "./auth"
|
||||
|
|
|
@ -7,7 +7,7 @@ export function createAuthStore() {
|
|||
return {
|
||||
subscribe: store.subscribe,
|
||||
checkAuth: async () => {
|
||||
const response = await api.get("/api/self")
|
||||
const response = await api.get("/api/admin/users/self")
|
||||
const user = await response.json()
|
||||
if (response.status === 200) {
|
||||
store.update(state => ({ ...state, user }))
|
||||
|
@ -33,6 +33,14 @@ export function createAuthStore() {
|
|||
await response.json()
|
||||
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 => {
|
||||
const response = await api.post(`/api/admin/auth/reset`, {
|
||||
email,
|
|
@ -3,3 +3,4 @@ export { users } from "./users"
|
|||
export { admin } from "./admin"
|
||||
export { apps } from "./apps"
|
||||
export { email } from "./email"
|
||||
export { auth } from "./auth"
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import api from "builderStore/api"
|
||||
|
||||
const FALLBACK_CONFIG = {
|
||||
platformUrl: "",
|
||||
logoUrl: "",
|
||||
docsUrl: "",
|
||||
company: "http://localhost:10000",
|
||||
const DEFAULT_CONFIG = {
|
||||
platformUrl: "http://localhost:1000",
|
||||
logoUrl: "https://i.imgur.com/ZKyklgF.png",
|
||||
docsUrl: undefined,
|
||||
company: "Budibase",
|
||||
}
|
||||
|
||||
export function createOrganisationStore() {
|
||||
const store = writable({})
|
||||
const store = writable(DEFAULT_CONFIG)
|
||||
const { subscribe, set } = store
|
||||
|
||||
async function init() {
|
||||
|
@ -17,16 +17,16 @@ export function createOrganisationStore() {
|
|||
const json = await res.json()
|
||||
|
||||
if (json.status === 400) {
|
||||
set(FALLBACK_CONFIG)
|
||||
set(DEFAULT_CONFIG)
|
||||
} else {
|
||||
set({ ...json.config, _rev: json._rev })
|
||||
set({ ...DEFAULT_CONFIG, ...json.config, _rev: json._rev })
|
||||
}
|
||||
}
|
||||
|
||||
async function save(config) {
|
||||
const res = await api.post("/api/admin/configs", {
|
||||
type: "settings",
|
||||
config,
|
||||
config: { ...get(store), ...config },
|
||||
_rev: get(store)._rev,
|
||||
})
|
||||
const json = await res.json()
|
||||
|
|
|
@ -117,13 +117,17 @@ async function createInstance(template) {
|
|||
}
|
||||
|
||||
exports.fetch = async function (ctx) {
|
||||
const isDev = ctx.query && ctx.query.status === AppStatus.DEV
|
||||
const apps = await getAllApps(isDev)
|
||||
const dev = ctx.query && ctx.query.status === AppStatus.DEV
|
||||
const all = ctx.query && ctx.query.status === AppStatus.ALL
|
||||
const apps = await getAllApps({ dev, all })
|
||||
|
||||
// get the locks for all the dev apps
|
||||
if (isDev) {
|
||||
if (dev || all) {
|
||||
const locks = await getAllLocks()
|
||||
for (let app of apps) {
|
||||
if (app.status !== "development") {
|
||||
continue
|
||||
}
|
||||
const lock = locks.find(lock => lock.appId === app.appId)
|
||||
if (lock) {
|
||||
app.lockedBy = lock.user
|
||||
|
@ -210,7 +214,7 @@ exports.create = async function (ctx) {
|
|||
exports.update = async function (ctx) {
|
||||
const url = await getAppUrlIfNotInUse(ctx)
|
||||
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 newData = { ...application, ...data, url }
|
||||
|
@ -231,7 +235,7 @@ exports.update = async function (ctx) {
|
|||
|
||||
exports.delete = async function (ctx) {
|
||||
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()
|
||||
/* istanbul ignore next */
|
||||
if (!env.isTest()) {
|
||||
|
|
|
@ -6,6 +6,8 @@ const webhooks = require("./webhook")
|
|||
const { getAutomationParams, generateAutomationID } = require("../../db/utils")
|
||||
|
||||
const WH_STEP_ID = triggers.BUILTIN_DEFINITIONS.WEBHOOK.stepId
|
||||
const CRON_STEP_ID = triggers.BUILTIN_DEFINITIONS.CRON.stepId
|
||||
|
||||
/*************************
|
||||
* *
|
||||
* BUILDER FUNCTIONS *
|
||||
|
@ -32,6 +34,46 @@ function cleanAutomationInputs(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.
|
||||
* @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,
|
||||
newAuto: automation,
|
||||
})
|
||||
automation = await checkForCronTriggers({
|
||||
appId: ctx.appId,
|
||||
newAuto: automation,
|
||||
})
|
||||
const response = await db.put(automation)
|
||||
automation._rev = response.rev
|
||||
|
||||
|
@ -135,6 +181,11 @@ exports.update = async function (ctx) {
|
|||
oldAuto: oldAutomation,
|
||||
newAuto: automation,
|
||||
})
|
||||
automation = await checkForCronTriggers({
|
||||
appId: ctx.appId,
|
||||
oldAuto: oldAutomation,
|
||||
newAuto: automation,
|
||||
})
|
||||
const response = await db.put(automation)
|
||||
automation._rev = response.rev
|
||||
|
||||
|
@ -171,6 +222,10 @@ exports.destroy = async function (ctx) {
|
|||
appId: ctx.appId,
|
||||
oldAuto: oldAutomation,
|
||||
})
|
||||
await checkForCronTriggers({
|
||||
appId: ctx.appId,
|
||||
oldAuto: oldAutomation,
|
||||
})
|
||||
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) {
|
||||
const appId = ctx.appId
|
||||
const db = new CouchDB(appId)
|
||||
let dbRow = await db.get(ctx.params.rowId)
|
||||
let dbTable = await db.get(dbRow.tableId)
|
||||
const patchfields = ctx.request.body
|
||||
const inputs = ctx.request.body
|
||||
const tableId = inputs.tableId
|
||||
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
|
||||
for (let key of Object.keys(patchfields)) {
|
||||
for (let key of Object.keys(inputs)) {
|
||||
if (!dbTable.schema[key]) continue
|
||||
dbRow[key] = patchfields[key]
|
||||
dbRow[key] = inputs[key]
|
||||
}
|
||||
|
||||
// this returns the table and row incase they have been updated
|
||||
|
@ -90,13 +105,9 @@ exports.patch = async function (ctx) {
|
|||
table,
|
||||
})
|
||||
|
||||
// TODO remove special user case in future
|
||||
if (row.tableId === InternalTables.USER_METADATA) {
|
||||
if (isUserTable) {
|
||||
// the row has been updated, need to put it into the ctx
|
||||
ctx.request.body = {
|
||||
...row,
|
||||
password: ctx.request.body.password,
|
||||
}
|
||||
ctx.request.body = row
|
||||
await userController.updateMetadata(ctx)
|
||||
return
|
||||
}
|
||||
|
@ -129,12 +140,9 @@ exports.save = async function (ctx) {
|
|||
|
||||
// if the row obj had an _id then it will have been retrieved
|
||||
if (inputs._id && inputs._rev) {
|
||||
const existingRow = await db.get(inputs._id)
|
||||
if (existingRow) {
|
||||
ctx.params.rowId = inputs._id
|
||||
await exports.patch(ctx)
|
||||
return
|
||||
}
|
||||
ctx.params.rowId = inputs._id
|
||||
await exports.patch(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
if (!inputs._rev && !inputs._id) {
|
||||
|
@ -167,14 +175,6 @@ exports.save = async function (ctx) {
|
|||
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"
|
||||
const response = await db.put(row)
|
||||
// don't worry about rev, tables handle rev/lastID updates
|
||||
|
|
|
@ -6,7 +6,7 @@ const {
|
|||
InternalTables,
|
||||
} = require("../../../db/utils")
|
||||
const { isEqual } = require("lodash/fp")
|
||||
const { AutoFieldSubTypes } = require("../../../constants")
|
||||
const { AutoFieldSubTypes, FieldTypes } = require("../../../constants")
|
||||
const { inputProcessing } = require("../../../utilities/rowProcessor")
|
||||
const { USERS_TABLE_SCHEMA } = require("../../../constants")
|
||||
|
||||
|
@ -72,18 +72,21 @@ exports.handleDataImport = async (appId, user, table, dataImport) => {
|
|||
row._id = generateRowID(table._id)
|
||||
row.tableId = table._id
|
||||
const processed = inputProcessing(user, table, row)
|
||||
table = processed.table
|
||||
row = processed.row
|
||||
// these auto-fields will never actually link anywhere (always builder)
|
||||
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 (
|
||||
schema.autocolumn &&
|
||||
(schema.subtype === AutoFieldSubTypes.CREATED_BY ||
|
||||
schema.subtype === AutoFieldSubTypes.UPDATED_BY)
|
||||
schema.type === FieldTypes.OPTIONS &&
|
||||
(!schema.constraints.inclusion ||
|
||||
schema.constraints.inclusion.indexOf(row[fieldName]) === -1)
|
||||
) {
|
||||
delete row[fieldName]
|
||||
schema.constraints.inclusion = [
|
||||
...schema.constraints.inclusion,
|
||||
row[fieldName],
|
||||
]
|
||||
}
|
||||
}
|
||||
table = processed.table
|
||||
data[i] = row
|
||||
}
|
||||
|
||||
|
|
|
@ -2,17 +2,23 @@ const CouchDB = require("../../db")
|
|||
const {
|
||||
generateUserMetadataID,
|
||||
getUserMetadataParams,
|
||||
getGlobalIDFromUserMetadataID,
|
||||
} = 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 {
|
||||
getGlobalUsers,
|
||||
saveGlobalUser,
|
||||
deleteGlobalUser,
|
||||
addAppRoleToUser,
|
||||
} = require("../../utilities/workerRequests")
|
||||
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) {
|
||||
const database = new CouchDB(ctx.appId)
|
||||
const global = await getGlobalUsers(ctx, ctx.appId)
|
||||
|
@ -38,43 +44,12 @@ exports.fetchMetadata = async function (ctx) {
|
|||
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) {
|
||||
// overwrite the ID with current users
|
||||
ctx.request.body._id = ctx.user._id
|
||||
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
|
||||
delete ctx.request.body._rev
|
||||
|
@ -84,23 +59,19 @@ exports.updateSelfMetadata = async function (ctx) {
|
|||
exports.updateMetadata = async function (ctx) {
|
||||
const appId = ctx.appId
|
||||
const db = new CouchDB(appId)
|
||||
const user = ctx.request.body
|
||||
const globalUser = await saveGlobalUser(ctx, appId, {
|
||||
...user,
|
||||
_id: getGlobalIDFromUserMetadataID(user._id),
|
||||
})
|
||||
const user = removeGlobalProps(ctx.request.body)
|
||||
if (user.roleId) {
|
||||
await addAppRoleToUser(ctx, appId, user.roleId, user._id)
|
||||
}
|
||||
const metadata = {
|
||||
...globalUser,
|
||||
tableId: InternalTables.USER_METADATA,
|
||||
_id: user._id || generateUserMetadataID(globalUser._id),
|
||||
_rev: user._rev,
|
||||
...user,
|
||||
}
|
||||
ctx.body = await db.put(metadata)
|
||||
}
|
||||
|
||||
exports.destroyMetadata = async function (ctx) {
|
||||
const db = new CouchDB(ctx.appId)
|
||||
await deleteGlobalUser(ctx, getGlobalIDFromUserMetadataID(ctx.params.id))
|
||||
try {
|
||||
const dbUser = await db.get(ctx.params.id)
|
||||
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
|
||||
}
|
||||
ctx.body = {
|
||||
message: `User ${ctx.params.id} deleted.`,
|
||||
message: `User metadata ${ctx.params.id} deleted.`,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,6 @@
|
|||
const setup = require("./utilities")
|
||||
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", () => {
|
||||
let request = setup.getRequest()
|
||||
let config = setup.getConfig()
|
||||
|
@ -27,7 +13,7 @@ describe("/authenticate", () => {
|
|||
|
||||
describe("fetch self", () => {
|
||||
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 res = await request
|
||||
.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", () => {
|
||||
it("should be able to fetch tables contents via 'view'", async () => {
|
||||
const row = await config.createRow()
|
||||
|
|
|
@ -8,12 +8,7 @@ jest.mock("../../../utilities/workerRequests", () => ({
|
|||
getGlobalUsers: jest.fn(() => {
|
||||
return {}
|
||||
}),
|
||||
saveGlobalUser: jest.fn(() => {
|
||||
const uuid = require("uuid/v4")
|
||||
return {
|
||||
_id: `us_${uuid()}`
|
||||
}
|
||||
}),
|
||||
addAppRoleToUser: 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", () => {
|
||||
beforeEach(() => {
|
||||
workerRequests.saveGlobalUser.mockImplementationOnce(() => ({
|
||||
_id: "us_test@test.com"
|
||||
}))
|
||||
})
|
||||
|
||||
it("should be able to update the user", async () => {
|
||||
|
@ -144,16 +88,12 @@ describe("/users", () => {
|
|||
.expect(200)
|
||||
.expect("Content-Type", /json/)
|
||||
expect(res.body.message).toBeDefined()
|
||||
expect(workerRequests.deleteGlobalUser).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("find", () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
workerRequests.saveGlobalUser.mockImplementationOnce(() => ({
|
||||
_id: "us_uuid1",
|
||||
}))
|
||||
workerRequests.getGlobalUsers.mockImplementationOnce(() => ({
|
||||
_id: "us_uuid1",
|
||||
roleId: BUILTIN_ROLE_IDS.POWER,
|
||||
|
|
|
@ -3,8 +3,7 @@ const structures = require("../../../../tests/utilities/structures")
|
|||
const env = require("../../../../environment")
|
||||
|
||||
jest.mock("../../../../utilities/workerRequests", () => ({
|
||||
getGlobalUsers: jest.fn(),
|
||||
saveGlobalUser: jest.fn(() => {
|
||||
getGlobalUsers: jest.fn(() => {
|
||||
return {
|
||||
_id: "us_uuid1",
|
||||
}
|
||||
|
|
|
@ -25,12 +25,6 @@ router
|
|||
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
||||
controller.updateMetadata
|
||||
)
|
||||
.post(
|
||||
"/api/users/metadata",
|
||||
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
||||
usage,
|
||||
controller.createMetadata
|
||||
)
|
||||
.post(
|
||||
"/api/users/metadata/self",
|
||||
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
|
||||
|
|
|
@ -3,7 +3,6 @@ const sendSmtpEmail = require("./steps/sendSmtpEmail")
|
|||
const createRow = require("./steps/createRow")
|
||||
const updateRow = require("./steps/updateRow")
|
||||
const deleteRow = require("./steps/deleteRow")
|
||||
const createUser = require("./steps/createUser")
|
||||
const executeScript = require("./steps/executeScript")
|
||||
const executeQuery = require("./steps/executeQuery")
|
||||
const outgoingWebhook = require("./steps/outgoingWebhook")
|
||||
|
@ -20,7 +19,6 @@ const BUILTIN_ACTIONS = {
|
|||
CREATE_ROW: createRow.run,
|
||||
UPDATE_ROW: updateRow.run,
|
||||
DELETE_ROW: deleteRow.run,
|
||||
CREATE_USER: createUser.run,
|
||||
OUTGOING_WEBHOOK: outgoingWebhook.run,
|
||||
EXECUTE_SCRIPT: executeScript.run,
|
||||
EXECUTE_QUERY: executeQuery.run,
|
||||
|
@ -31,7 +29,6 @@ const BUILTIN_DEFINITIONS = {
|
|||
CREATE_ROW: createRow.definition,
|
||||
UPDATE_ROW: updateRow.definition,
|
||||
DELETE_ROW: deleteRow.definition,
|
||||
CREATE_USER: createUser.definition,
|
||||
OUTGOING_WEBHOOK: outgoingWebhook.definition,
|
||||
EXECUTE_SCRIPT: executeScript.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.
|
||||
*/
|
||||
class Orchestrator {
|
||||
constructor(automation, triggerOutput) {
|
||||
constructor(automation, triggerOutput = {}) {
|
||||
this._metadata = triggerOutput.metadata
|
||||
this._chainCount = this._metadata ? this._metadata.automationChainCount : 0
|
||||
this._appId = triggerOutput.appId
|
||||
|
|
|
@ -7,9 +7,10 @@ const Queue = env.isTest()
|
|||
const { getAutomationParams } = require("../db/utils")
|
||||
const { coerce } = require("../utilities/rowProcessor")
|
||||
const { utils } = require("@budibase/auth/redis")
|
||||
const { JobQueues } = require("../constants")
|
||||
|
||||
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_BOOL = false
|
||||
|
@ -196,6 +197,29 @@ const BUILTIN_DEFINITIONS = {
|
|||
},
|
||||
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) {
|
||||
|
|
|
@ -5,6 +5,10 @@ const { ObjectStoreBuckets } = require("@budibase/auth").objectStore
|
|||
exports.LOGO_URL =
|
||||
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
|
||||
|
||||
exports.JobQueues = {
|
||||
AUTOMATIONS: "automationQueue",
|
||||
}
|
||||
|
||||
exports.FieldTypes = {
|
||||
STRING: "string",
|
||||
LONGFORM: "longform",
|
||||
|
@ -50,6 +54,24 @@ exports.USERS_TABLE_SCHEMA = {
|
|||
fieldName: "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: {
|
||||
fieldName: "roleId",
|
||||
name: "roleId",
|
||||
|
|
|
@ -19,6 +19,7 @@ const StaticDatabases = {
|
|||
|
||||
const AppStatus = {
|
||||
DEV: "dev",
|
||||
ALL: "all",
|
||||
DEPLOYED: "PUBLISHED",
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,11 @@ class TestConfiguration {
|
|||
if (builder) {
|
||||
user.builder = { global: true }
|
||||
}
|
||||
await db.put(user)
|
||||
const resp = await db.put(user)
|
||||
return {
|
||||
_rev: resp._rev,
|
||||
...user,
|
||||
}
|
||||
}
|
||||
|
||||
async init(appName = "test_application") {
|
||||
|
@ -308,18 +312,12 @@ class TestConfiguration {
|
|||
roleId = BUILTIN_ROLE_IDS.POWER
|
||||
) {
|
||||
const globalId = `us_${Math.random()}`
|
||||
await this.globalUser(globalId, roleId === BUILTIN_ROLE_IDS.BUILDER)
|
||||
const user = await this._req(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
roleId,
|
||||
},
|
||||
null,
|
||||
controllers.user.createMetadata
|
||||
const resp = await this.globalUser(
|
||||
globalId,
|
||||
roleId === BUILTIN_ROLE_IDS.BUILDER
|
||||
)
|
||||
return {
|
||||
...user,
|
||||
...resp,
|
||||
globalId,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
const csv = require("csvtojson")
|
||||
const { FieldTypes } = require("../constants")
|
||||
|
||||
const VALIDATORS = {
|
||||
string: () => true,
|
||||
number: attribute => !isNaN(Number(attribute)),
|
||||
datetime: attribute => !isNaN(new Date(attribute).getTime()),
|
||||
[FieldTypes.STRING]: () => true,
|
||||
[FieldTypes.OPTIONS]: () => true,
|
||||
[FieldTypes.NUMBER]: attribute => !isNaN(Number(attribute)),
|
||||
[FieldTypes.DATETIME]: attribute => !isNaN(new Date(attribute).getTime()),
|
||||
}
|
||||
|
||||
const PARSERS = {
|
||||
number: attribute => Number(attribute),
|
||||
datetime: attribute => new Date(attribute).toISOString(),
|
||||
[FieldTypes.NUMBER]: attribute => Number(attribute),
|
||||
[FieldTypes.DATETIME]: attribute => new Date(attribute).toISOString(),
|
||||
}
|
||||
|
||||
function parse(csvString, parsers) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const CouchDB = require("../db")
|
||||
const { getGlobalIDFromUserMetadataID } = require("../db/utils")
|
||||
const { getGlobalIDFromUserMetadataID, InternalTables } = require("../db/utils")
|
||||
const { getGlobalUsers } = require("../utilities/workerRequests")
|
||||
|
||||
exports.getFullUser = async (ctx, userId) => {
|
||||
|
@ -21,6 +21,7 @@ exports.getFullUser = async (ctx, userId) => {
|
|||
return {
|
||||
...global,
|
||||
...metadata,
|
||||
tableId: InternalTables.USER_METADATA,
|
||||
// make sure the ID is always a local ID, not a global one
|
||||
_id: userId,
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ const env = require("../environment")
|
|||
const { checkSlashesInUrl } = require("./index")
|
||||
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
|
||||
const { getDeployedAppID } = require("@budibase/auth/db")
|
||||
const { getGlobalIDFromUserMetadataID } = require("../db/utils")
|
||||
|
||||
function getAppRole(appId, user) {
|
||||
if (!user.roles) {
|
||||
|
@ -118,53 +119,50 @@ exports.getGlobalUsers = async (ctx, appId = null, globalId = null) => {
|
|||
return users
|
||||
}
|
||||
|
||||
exports.saveGlobalUser = async (ctx, appId, body) => {
|
||||
const globalUser = body._id
|
||||
? 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
exports.getGlobalSelf = async ctx => {
|
||||
const endpoint = `/api/admin/users/self`
|
||||
const response = await fetch(
|
||||
checkSlashesInUrl(env.WORKER_URL + endpoint),
|
||||
request(ctx, reqCfg)
|
||||
request(ctx, { method: "GET" })
|
||||
)
|
||||
const json = await response.json()
|
||||
if (json.status !== 200 && response.status !== 200) {
|
||||
ctx.throw(400, "Unable to save global user.")
|
||||
}
|
||||
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,
|
||||
ctx.throw(400, "Unable to get self globally.")
|
||||
}
|
||||
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 => {
|
||||
// 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 = []
|
||||
for (let app of apps) {
|
||||
// use dev app IDs
|
||||
|
|
|
@ -16,9 +16,14 @@ exports.save = async ctx => {
|
|||
const { email, password, _id } = ctx.request.body
|
||||
|
||||
// make sure another user isn't using the same email
|
||||
const dbUser = await getGlobalUserByEmail(email)
|
||||
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
|
||||
ctx.throw(400, "Email address already in use.")
|
||||
let dbUser
|
||||
if (email) {
|
||||
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
|
||||
|
@ -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
|
||||
exports.fetch = async ctx => {
|
||||
const db = new CouchDB(GLOBAL_DB)
|
||||
|
@ -145,12 +177,16 @@ exports.invite = async ctx => {
|
|||
}
|
||||
|
||||
exports.inviteAccept = async ctx => {
|
||||
const { inviteCode } = ctx.request.body
|
||||
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
||||
try {
|
||||
const email = await checkInviteCode(inviteCode)
|
||||
// redirect the request
|
||||
delete ctx.request.body.inviteCode
|
||||
ctx.request.body.email = email
|
||||
// only pass through certain props for accepting
|
||||
ctx.request.body = {
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
email,
|
||||
}
|
||||
// this will flesh out the body response
|
||||
await exports.save(ctx)
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../../controllers/admin/configs")
|
||||
const joiValidator = require("../../../middleware/joi-validator")
|
||||
const adminOnly = require("../../../middleware/adminOnly")
|
||||
const Joi = require("joi")
|
||||
const { Configs, ConfigUploads } = require("../../../constants")
|
||||
|
||||
|
@ -77,8 +78,13 @@ function buildConfigGetValidation() {
|
|||
}
|
||||
|
||||
router
|
||||
.post("/api/admin/configs", buildConfigSaveValidation(), controller.save)
|
||||
.delete("/api/admin/configs/:id", controller.destroy)
|
||||
.post(
|
||||
"/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/checklist", controller.configChecklist)
|
||||
.get(
|
||||
|
@ -89,6 +95,7 @@ router
|
|||
.get("/api/admin/configs/:type", buildConfigGetValidation(), controller.find)
|
||||
.post(
|
||||
"/api/admin/configs/upload/:type/:name",
|
||||
adminOnly,
|
||||
buildUploadValidation(),
|
||||
controller.upload
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../../controllers/admin/groups")
|
||||
const joiValidator = require("../../../middleware/joi-validator")
|
||||
const adminOnly = require("../../../middleware/adminOnly")
|
||||
const Joi = require("joi")
|
||||
|
||||
const router = Router()
|
||||
|
@ -24,9 +25,14 @@ function buildGroupSaveValidation() {
|
|||
}
|
||||
|
||||
router
|
||||
.post("/api/admin/groups", buildGroupSaveValidation(), controller.save)
|
||||
.post(
|
||||
"/api/admin/groups",
|
||||
adminOnly,
|
||||
buildGroupSaveValidation(),
|
||||
controller.save
|
||||
)
|
||||
.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)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../../controllers/admin/roles")
|
||||
const adminOnly = require("../../../middleware/adminOnly")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/admin/roles", controller.fetch)
|
||||
.get("/api/admin/roles/:appId", controller.find)
|
||||
.get("/api/admin/roles", adminOnly, controller.fetch)
|
||||
.get("/api/admin/roles/:appId", adminOnly, controller.find)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -3,6 +3,7 @@ const controller = require("../../controllers/admin/templates")
|
|||
const joiValidator = require("../../../middleware/joi-validator")
|
||||
const Joi = require("joi")
|
||||
const { TemplatePurpose, TemplateTypes } = require("../../../constants")
|
||||
const adminOnly = require("../../../middleware/adminOnly")
|
||||
|
||||
const router = Router()
|
||||
|
||||
|
@ -21,11 +22,16 @@ function buildTemplateSaveValidation() {
|
|||
|
||||
router
|
||||
.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/:type", controller.fetchByType)
|
||||
.get("/api/admin/template/:ownerId", controller.fetchByOwner)
|
||||
.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
|
||||
|
|
|
@ -1,28 +1,35 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../../controllers/admin/users")
|
||||
const joiValidator = require("../../../middleware/joi-validator")
|
||||
const adminOnly = require("../../../middleware/adminOnly")
|
||||
const Joi = require("joi")
|
||||
|
||||
const router = Router()
|
||||
|
||||
function buildUserSaveValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
_id: Joi.string(),
|
||||
_rev: Joi.string(),
|
||||
email: Joi.string(),
|
||||
function buildUserSaveValidation(isSelf = false) {
|
||||
let schema = {
|
||||
email: Joi.string().allow(null, ""),
|
||||
password: Joi.string().allow(null, ""),
|
||||
forcePasswordChange: Joi.boolean().optional(),
|
||||
firstName: Joi.string().allow(null, ""),
|
||||
lastName: Joi.string().allow(null, ""),
|
||||
builder: Joi.object({
|
||||
global: Joi.boolean().optional(),
|
||||
apps: Joi.array().optional(),
|
||||
}).unknown(true).optional(),
|
||||
// maps appId -> roleId for the user
|
||||
roles: Joi.object()
|
||||
.pattern(/.*/, Joi.string())
|
||||
.required()
|
||||
})
|
||||
.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() {
|
||||
|
@ -41,13 +48,29 @@ function buildInviteAcceptValidation() {
|
|||
}
|
||||
|
||||
router
|
||||
.post("/api/admin/users", buildUserSaveValidation(), controller.save)
|
||||
.post(
|
||||
"/api/admin/users",
|
||||
adminOnly,
|
||||
buildUserSaveValidation(),
|
||||
controller.save
|
||||
)
|
||||
.get("/api/admin/users", controller.fetch)
|
||||
.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/roles/:appId")
|
||||
.post("/api/admin/users/invite", buildInviteValidation(), controller.invite)
|
||||
.post(
|
||||
"/api/admin/users/invite",
|
||||
adminOnly,
|
||||
buildInviteValidation(),
|
||||
controller.invite
|
||||
)
|
||||
.post(
|
||||
"/api/admin/users/invite/accept",
|
||||
buildInviteAcceptValidation(),
|
||||
|
|
|
@ -30,7 +30,7 @@ describe("/api/admin/auth", () => {
|
|||
expect(sendMailMock).toHaveBeenCalled()
|
||||
const emailCall = sendMailMock.mock.calls[0][0]
|
||||
// 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]
|
||||
expect(code).toBeDefined()
|
||||
})
|
||||
|
|
|
@ -13,7 +13,7 @@ describe("/api/admin/configs/checklist", () => {
|
|||
let config = setup.getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
await config.init(false)
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
|
|
@ -30,7 +30,7 @@ describe("/api/admin/users", () => {
|
|||
expect(sendMailMock).toHaveBeenCalled()
|
||||
const emailCall = sendMailMock.mock.calls[0][0]
|
||||
// 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]
|
||||
expect(code).toBeDefined()
|
||||
})
|
||||
|
|
|
@ -38,20 +38,25 @@ class TestConfiguration {
|
|||
return request.body
|
||||
}
|
||||
|
||||
async init() {
|
||||
// create a test user
|
||||
await this._req(
|
||||
{
|
||||
email: "test@test.com",
|
||||
password: "test",
|
||||
_id: "us_uuid1",
|
||||
builder: {
|
||||
global: true,
|
||||
async init(createUser = true) {
|
||||
if (createUser) {
|
||||
// create a test user
|
||||
await this._req(
|
||||
{
|
||||
email: "test@test.com",
|
||||
password: "test",
|
||||
_id: "us_uuid1",
|
||||
builder: {
|
||||
global: true,
|
||||
},
|
||||
admin: {
|
||||
global: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
controllers.users.save
|
||||
)
|
||||
null,
|
||||
controllers.users.save
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 message = {
|
||||
from: from || config.from,
|
||||
subject: await processString(subject || config.subject, context),
|
||||
to: email,
|
||||
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)
|
||||
if (TEST_MODE) {
|
||||
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
|
||||
|
|
Loading…
Reference in New Issue