Merge pull request #1491 from Budibase/admin/user-management-ui
User Management UI and Settings Fixes
This commit is contained in:
commit
bc94dfa86b
|
@ -16,7 +16,10 @@
|
||||||
|
|
||||||
function getInitials(name) {
|
function getInitials(name) {
|
||||||
let parts = name.split(" ")
|
let parts = name.split(" ")
|
||||||
return parts.map(name => name[0]).join("")
|
if (parts.length > 0) {
|
||||||
|
return parts.map(name => name[0]).join("")
|
||||||
|
}
|
||||||
|
return name
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -282,6 +282,7 @@
|
||||||
{#if sortedRows?.length && fields.length}
|
{#if sortedRows?.length && fields.length}
|
||||||
{#each sortedRows as row, idx}
|
{#each sortedRows as row, idx}
|
||||||
<tr
|
<tr
|
||||||
|
on:click={() => dispatch("click", row)}
|
||||||
on:click={() => toggleSelectRow(row)}
|
on:click={() => toggleSelectRow(row)}
|
||||||
class="spectrum-Table-row"
|
class="spectrum-Table-row"
|
||||||
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow}
|
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
export let selected = false
|
export let selected = false
|
||||||
export let open = false
|
export let open = false
|
||||||
|
export let href = false
|
||||||
export let title
|
export let title
|
||||||
export let icon
|
export let icon
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,7 +11,7 @@
|
||||||
class:is-open={open}
|
class:is-open={open}
|
||||||
class="spectrum-TreeView-item"
|
class="spectrum-TreeView-item"
|
||||||
>
|
>
|
||||||
<a on:click class="spectrum-TreeView-itemLink" href="#">
|
<a on:click class="spectrum-TreeView-itemLink" {href}>
|
||||||
{#if $$slots.default}
|
{#if $$slots.default}
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-UIIcon-ChevronRight100 spectrum-TreeView-itemIndicator"
|
class="spectrum-Icon spectrum-UIIcon-ChevronRight100 spectrum-TreeView-itemIndicator"
|
||||||
|
|
|
@ -4,16 +4,17 @@
|
||||||
import { routes } from "../.routify/routes"
|
import { routes } from "../.routify/routes"
|
||||||
import { initialise } from "builderStore"
|
import { initialise } from "builderStore"
|
||||||
import { NotificationDisplay } from "@budibase/bbui"
|
import { NotificationDisplay } from "@budibase/bbui"
|
||||||
|
import { parse, stringify } from "qs"
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await initialise()
|
await initialise()
|
||||||
})
|
})
|
||||||
|
|
||||||
const config = {}
|
const queryHandler = { parse, stringify }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NotificationDisplay />
|
<NotificationDisplay />
|
||||||
<Router {routes} {config} />
|
<Router {routes} config={{ queryHandler }} />
|
||||||
<div class="modal-container" />
|
<div class="modal-container" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
export default function (url) {
|
||||||
|
const store = writable({ status: "LOADING", data: {}, error: {} })
|
||||||
|
|
||||||
|
async function get() {
|
||||||
|
store.update(u => ({ ...u, status: "LOADING" }))
|
||||||
|
try {
|
||||||
|
const response = await api.get(url)
|
||||||
|
store.set({ data: await response.json(), status: "SUCCESS" })
|
||||||
|
} catch (e) {
|
||||||
|
store.set({ data: {}, error: e, status: "ERROR" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get()
|
||||||
|
|
||||||
|
return { subscribe: store.subscribe, refresh: get }
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
export { default as fetchData } from "./fetchData"
|
||||||
|
export {
|
||||||
|
buildStyle,
|
||||||
|
convertCamel,
|
||||||
|
pipe,
|
||||||
|
capitalise,
|
||||||
|
get_name,
|
||||||
|
get_capitalised_name,
|
||||||
|
} from "./helpers"
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { emailValidator, requiredValidator } from "./validators"
|
||||||
|
export { createValidationStore } from "./validation"
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { writable, derived } from "svelte/store"
|
||||||
|
|
||||||
|
export function createValidationStore(initialValue, ...validators) {
|
||||||
|
let touched = false
|
||||||
|
|
||||||
|
const value = writable(initialValue || "")
|
||||||
|
const error = derived(value, $v => validate($v, validators))
|
||||||
|
const touchedStore = derived(value, () => {
|
||||||
|
if (!touched) {
|
||||||
|
touched = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return touched
|
||||||
|
})
|
||||||
|
|
||||||
|
return [value, error, touchedStore]
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(value, validators) {
|
||||||
|
const failing = validators.find(v => v(value) !== true)
|
||||||
|
|
||||||
|
return failing && failing(value)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
export function emailValidator(value) {
|
||||||
|
return (
|
||||||
|
(value &&
|
||||||
|
!!value.match(
|
||||||
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||||
|
)) ||
|
||||||
|
"Please enter a valid email"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requiredValidator(value) {
|
||||||
|
return (
|
||||||
|
(value !== undefined && value !== null && value !== "") ||
|
||||||
|
"This field is required"
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { page, goto } from "@roxi/routify"
|
||||||
import { auth } from "stores/backend"
|
import { auth } from "stores/backend"
|
||||||
import { admin } from "stores/portal"
|
import { admin } from "stores/portal"
|
||||||
|
|
||||||
|
@ -22,7 +22,12 @@
|
||||||
|
|
||||||
// Redirect to log in at any time if the user isn't authenticated
|
// Redirect to log in at any time if the user isn't authenticated
|
||||||
$: {
|
$: {
|
||||||
if (loaded && hasAdminUser && !$auth.user) {
|
if (
|
||||||
|
!$page.path.includes("/builder/invite") &&
|
||||||
|
loaded &&
|
||||||
|
hasAdminUser &&
|
||||||
|
!$auth.user
|
||||||
|
) {
|
||||||
$goto("./auth/login")
|
$goto("./auth/login")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { goto, params } from "@roxi/routify"
|
||||||
|
import { createValidationStore, requiredValidator } from "helpers/validation"
|
||||||
|
import { users } from "stores/portal"
|
||||||
|
|
||||||
|
const [password, passwordError, passwordTouched] = createValidationStore(
|
||||||
|
"",
|
||||||
|
requiredValidator
|
||||||
|
)
|
||||||
|
const [repeat, _, repeatTouched] = createValidationStore(
|
||||||
|
"",
|
||||||
|
requiredValidator
|
||||||
|
)
|
||||||
|
const inviteCode = $params["?code"]
|
||||||
|
|
||||||
|
async function acceptInvite() {
|
||||||
|
try {
|
||||||
|
const res = await users.acceptInvite(inviteCode, $password)
|
||||||
|
if (!res) {
|
||||||
|
throw new Error(res.message)
|
||||||
|
}
|
||||||
|
notifications.success(`User created.`)
|
||||||
|
$goto("../auth/login")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<Layout gap="XS">
|
||||||
|
<img src="https://i.imgur.com/ZKyklgF.png" />
|
||||||
|
</Layout>
|
||||||
|
<div class="center">
|
||||||
|
<Layout gap="XS">
|
||||||
|
<Heading size="M">Accept Invitation</Heading>
|
||||||
|
<Body size="M">Please enter a password to setup your user.</Body>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
<Layout gap="XS">
|
||||||
|
<Input
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
error={$passwordTouched && $passwordError}
|
||||||
|
bind:value={$password}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Repeat Password"
|
||||||
|
type="password"
|
||||||
|
error={$repeatTouched &&
|
||||||
|
$password !== $repeat &&
|
||||||
|
"Passwords must match"}
|
||||||
|
bind:value={$repeat}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="S">
|
||||||
|
<Button
|
||||||
|
disabled={!$passwordTouched || !$repeatTouched || $password !== $repeat}
|
||||||
|
cta
|
||||||
|
on:click={acceptInvite}>Accept invite</Button
|
||||||
|
>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 260px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 40px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { isActive, goto } from "@roxi/routify"
|
import { isActive, goto } from "@roxi/routify"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
@ -13,34 +12,21 @@
|
||||||
Modal,
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
||||||
import { organisation, apps } from "stores/portal"
|
import { organisation } from "stores/portal"
|
||||||
import { auth } from "stores/backend"
|
import { auth } from "stores/backend"
|
||||||
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
|
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
|
||||||
|
|
||||||
let orgName
|
|
||||||
let orgLogo
|
|
||||||
let user
|
|
||||||
let oldSettingsModal
|
let oldSettingsModal
|
||||||
|
|
||||||
async function getInfo() {
|
organisation.init()
|
||||||
// fetch orgInfo
|
|
||||||
orgName = "ACME Inc."
|
|
||||||
orgLogo = "https://via.placeholder.com/150"
|
|
||||||
user = { name: "John Doe" }
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
organisation.init()
|
|
||||||
getInfo()
|
|
||||||
})
|
|
||||||
|
|
||||||
let menu = [
|
let menu = [
|
||||||
{ title: "Apps", href: "/builder/portal/apps" },
|
{ title: "Apps", href: "/builder/portal/apps" },
|
||||||
{ title: "Drafts", href: "/builder/portal/drafts" },
|
{ title: "Drafts", href: "/builder/portal/drafts" },
|
||||||
{ title: "Users", href: "/builder/portal/users", heading: "Manage" },
|
{ title: "Users", href: "/builder/portal/manage/users", heading: "Manage" },
|
||||||
{ title: "Groups", href: "/builder/portal/groups" },
|
{ title: "Groups", href: "/builder/portal/manage/groups" },
|
||||||
{ title: "Auth", href: "/builder/portal/oauth" },
|
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
||||||
{ title: "Email", href: "/builder/portal/email" },
|
{ title: "Email", href: "/builder/portal/manage/email" },
|
||||||
{
|
{
|
||||||
title: "General",
|
title: "General",
|
||||||
href: "/builder/portal/settings/general",
|
href: "/builder/portal/settings/general",
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
import { Page } from "@budibase/bbui"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Page>
|
||||||
|
<slot />
|
||||||
|
</Page>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,114 @@
|
||||||
|
<script>
|
||||||
|
import GoogleLogo from "./_logos/Google.svelte"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
Page,
|
||||||
|
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 primary 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>
|
|
@ -1,32 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Button,
|
Button,
|
||||||
Detail,
|
Detail,
|
||||||
Heading,
|
Heading,
|
||||||
Divider,
|
|
||||||
Label,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
notifications,
|
notifications,
|
||||||
Layout,
|
|
||||||
Icon,
|
Icon,
|
||||||
Body,
|
|
||||||
Page,
|
Page,
|
||||||
Select,
|
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
MenuSection,
|
|
||||||
MenuSeparator,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { email } from "stores/portal"
|
import { email } from "stores/portal"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
import TemplateBindings from "./TemplateBindings.svelte"
|
import TemplateBindings from "./_components/TemplateBindings.svelte"
|
||||||
import api from "builderStore/api"
|
|
||||||
|
|
||||||
const ConfigTypes = {
|
const ConfigTypes = {
|
||||||
SMTP: "smtp",
|
SMTP: "smtp",
|
|
@ -23,8 +23,8 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { email } from "stores/portal"
|
import { email } from "stores/portal"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
import TemplateBindings from "./TemplateBindings.svelte"
|
import TemplateBindings from "./_components/TemplateBindings.svelte"
|
||||||
import TemplateLink from "./TemplateLink.svelte"
|
import TemplateLink from "./_components/TemplateLink.svelte"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
||||||
const ConfigTypes = {
|
const ConfigTypes = {
|
|
@ -0,0 +1,43 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,115 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,168 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
ActionButton,
|
||||||
|
Button,
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Divider,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Table,
|
||||||
|
ModalContent,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { fetchData } from "helpers"
|
||||||
|
import { users } from "stores/portal"
|
||||||
|
|
||||||
|
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
|
||||||
|
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
|
||||||
|
|
||||||
|
export let userId
|
||||||
|
let deleteUserModal
|
||||||
|
let editRolesModal
|
||||||
|
|
||||||
|
const roleSchema = {
|
||||||
|
name: { displayName: "App" },
|
||||||
|
role: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the Apps list and the roles response to get something that makes sense for the table
|
||||||
|
$: appList = Object.keys($apps?.data).map(id => ({
|
||||||
|
...$apps?.data?.[id],
|
||||||
|
_id: id,
|
||||||
|
role: [$roleFetch?.data?.roles?.[id]],
|
||||||
|
}))
|
||||||
|
let selectedApp
|
||||||
|
|
||||||
|
const roleFetch = fetchData(`/api/admin/users/${userId}`)
|
||||||
|
const apps = fetchData(`/api/admin/roles`)
|
||||||
|
|
||||||
|
async function deleteUser() {
|
||||||
|
const res = await users.del(userId)
|
||||||
|
if (res.message) {
|
||||||
|
notifications.success(`User ${$roleFetch?.data?.email} deleted.`)
|
||||||
|
$goto("./")
|
||||||
|
} else {
|
||||||
|
notifications.error("Failed to delete user.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: {$roleFetch?.data?.email}</Heading>
|
||||||
|
<Body
|
||||||
|
>Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis porro
|
||||||
|
ut nesciunt ipsam perspiciatis aliquam et hic minus alias beatae. Odit
|
||||||
|
veritatis quos quas laborum magnam tenetur perspiciatis ex hic.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
<Divider size="S" />
|
||||||
|
<div class="general">
|
||||||
|
<Heading size="S">General</Heading>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Email</Label>
|
||||||
|
<Input disabled thin value={$roleFetch?.data?.email} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="regenerate">
|
||||||
|
<ActionButton size="S" icon="Refresh" quiet
|
||||||
|
>Regenerate password</ActionButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider size="S" />
|
||||||
|
<div class="roles">
|
||||||
|
<Heading size="S">Configure roles</Heading>
|
||||||
|
<Table
|
||||||
|
on:click={openUpdateRolesModal}
|
||||||
|
schema={roleSchema}
|
||||||
|
data={appList}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
customRenderers={[{ column: "role", component: TagsRenderer }]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<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
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<Modal bind:this={deleteUserModal}>
|
||||||
|
<ModalContent
|
||||||
|
warning
|
||||||
|
onConfirm={deleteUser}
|
||||||
|
title="Delete User"
|
||||||
|
confirmText="Delete user"
|
||||||
|
cancelText="Cancel"
|
||||||
|
showCloseIcon={false}
|
||||||
|
>
|
||||||
|
<Body
|
||||||
|
>Are you sure you want to delete <strong>{$roleFetch?.data?.email}</strong
|
||||||
|
></Body
|
||||||
|
>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<Modal bind:this={editRolesModal}>
|
||||||
|
<UpdateRolesModal
|
||||||
|
app={selectedApp}
|
||||||
|
user={$roleFetch.data}
|
||||||
|
on:update={roleFetch.refresh}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.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;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
ModalContent,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { createValidationStore, emailValidator } from "helpers/validation"
|
||||||
|
import { users } from "stores/portal"
|
||||||
|
|
||||||
|
export let disabled
|
||||||
|
|
||||||
|
const options = ["Email onboarding", "Basic onboarding"]
|
||||||
|
let selected = options[0]
|
||||||
|
|
||||||
|
const [email, error, touched] = createValidationStore("", emailValidator)
|
||||||
|
|
||||||
|
async function createUserFlow() {
|
||||||
|
const res = await users.invite($email)
|
||||||
|
console.log(res)
|
||||||
|
if (res.status) {
|
||||||
|
notifications.error(res.message)
|
||||||
|
} else {
|
||||||
|
notifications.success(res.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
onConfirm={createUserFlow}
|
||||||
|
size="M"
|
||||||
|
title="Add new user options"
|
||||||
|
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
|
||||||
|
automated email onboarding flow. Otherwise, use our basic onboarding process
|
||||||
|
with autogenerated passwords.</Body
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder={null}
|
||||||
|
bind:value={selected}
|
||||||
|
on:change
|
||||||
|
{options}
|
||||||
|
label="Add new user via:"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
bind:value={$email}
|
||||||
|
error={$touched && $error}
|
||||||
|
placeholder="john@doe.com"
|
||||||
|
label="Email"
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
|
||||||
|
import { createValidationStore, emailValidator } from "helpers/validation"
|
||||||
|
import { users } from "stores/portal"
|
||||||
|
|
||||||
|
const [email, error, touched] = createValidationStore("", emailValidator)
|
||||||
|
const password = Math.random().toString(36).substr(2, 20)
|
||||||
|
|
||||||
|
async function createUser() {
|
||||||
|
const res = await users.create({ email: $email, password })
|
||||||
|
if (res.status) {
|
||||||
|
notifications.error(res.message)
|
||||||
|
} else {
|
||||||
|
notifications.success("Succesfully created user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
onConfirm={createUser}
|
||||||
|
size="M"
|
||||||
|
title="Basic user onboarding"
|
||||||
|
confirmText="Continue"
|
||||||
|
cancelText="Cancel"
|
||||||
|
disabled={$error}
|
||||||
|
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
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
label="Username"
|
||||||
|
bind:value={$email}
|
||||||
|
error={$touched && $error}
|
||||||
|
/>
|
||||||
|
<Input disabled label="Password" value={password} />
|
||||||
|
</ModalContent>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script>
|
||||||
|
import { Tag, Tags } from "@budibase/bbui"
|
||||||
|
export let value
|
||||||
|
|
||||||
|
const displayLimit = 5
|
||||||
|
|
||||||
|
$: tags = value?.slice(0, displayLimit) ?? []
|
||||||
|
$: leftover = (value?.length ?? 0) - tags.length
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tags>
|
||||||
|
{#each tags as tag}
|
||||||
|
<Tag>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
{/each}
|
||||||
|
{#if leftover}
|
||||||
|
<Tag>+{leftover} more</Tag>
|
||||||
|
{/if}
|
||||||
|
</Tags>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { Body, Select, ModalContent, notifications } from "@budibase/bbui"
|
||||||
|
import { fetchData } from "helpers"
|
||||||
|
import { users } from "stores/portal"
|
||||||
|
|
||||||
|
export let app
|
||||||
|
export let user
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const roles = app.roles
|
||||||
|
let options = roles.map(role => role._id)
|
||||||
|
let selectedRole
|
||||||
|
|
||||||
|
async function updateUserRoles() {
|
||||||
|
const res = await users.updateRoles({
|
||||||
|
...user,
|
||||||
|
roles: {
|
||||||
|
...user.roles,
|
||||||
|
[app._id]: selectedRole,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (res.status === 400) {
|
||||||
|
notifications.error("Failed to update role")
|
||||||
|
} else {
|
||||||
|
notifications.success("Roles updated")
|
||||||
|
dispatch("update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
onConfirm={updateUserRoles}
|
||||||
|
title="Update App Roles"
|
||||||
|
confirmText="Update roles"
|
||||||
|
cancelText="Cancel"
|
||||||
|
size="M"
|
||||||
|
showCloseIcon={false}
|
||||||
|
>
|
||||||
|
<Body noPadding
|
||||||
|
>Update {user.email}'s roles for <strong>{app.name}</strong>.</Body
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder={null}
|
||||||
|
bind:value={selectedRole}
|
||||||
|
on:change
|
||||||
|
{options}
|
||||||
|
label="Select roles:"
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
|
@ -0,0 +1,105 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Divider,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Search,
|
||||||
|
Table,
|
||||||
|
Label,
|
||||||
|
Layout,
|
||||||
|
Modal,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
|
||||||
|
import AddUserModal from "./_components/AddUserModal.svelte"
|
||||||
|
import BasicOnboardingModal from "./_components/BasicOnboardingModal.svelte"
|
||||||
|
import { users } from "stores/portal"
|
||||||
|
|
||||||
|
users.init()
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
email: {},
|
||||||
|
status: { displayName: "Development Access", type: "boolean" },
|
||||||
|
// role: { type: "options" },
|
||||||
|
group: {},
|
||||||
|
// access: {},
|
||||||
|
// group: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let search
|
||||||
|
let email
|
||||||
|
$: filteredUsers = $users
|
||||||
|
.filter(user => user.email.includes(search || ""))
|
||||||
|
.map(user => ({ ...user, group: ["All"] }))
|
||||||
|
|
||||||
|
let createUserModal
|
||||||
|
let basicOnboardingModal
|
||||||
|
|
||||||
|
function openBasicOnoboardingModal() {
|
||||||
|
createUserModal.hide()
|
||||||
|
basicOnboardingModal.show()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<div class="heading">
|
||||||
|
<Heading>Users</Heading>
|
||||||
|
<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>
|
||||||
|
<Divider size="S" />
|
||||||
|
|
||||||
|
<div class="users">
|
||||||
|
<Heading size="S">Users</Heading>
|
||||||
|
<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}
|
||||||
|
data={filteredUsers || $users}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
customRenderers={[{ column: "group", component: TagsRenderer }]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -1,7 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import GoogleLogo from "./logos/Google.svelte"
|
import GoogleLogo from "./_logos/Google.svelte"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Page,
|
||||||
Heading,
|
Heading,
|
||||||
Divider,
|
Divider,
|
||||||
Label,
|
Label,
|
||||||
|
@ -9,7 +10,6 @@
|
||||||
Layout,
|
Layout,
|
||||||
Input,
|
Input,
|
||||||
Body,
|
Body,
|
||||||
Page,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { organisation } from "stores/portal"
|
import { organisation } from "stores/portal"
|
||||||
|
import { post } from "builderStore/api"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
let analyticsDisabled = analytics.disabled()
|
let analyticsDisabled = analytics.disabled()
|
||||||
|
|
||||||
|
@ -24,18 +25,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading = false
|
let loading = false
|
||||||
|
let file
|
||||||
|
|
||||||
$: company = $organisation?.company
|
async function uploadLogo() {
|
||||||
$: logoUrl = $organisation.logoUrl
|
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() {
|
async function saveConfig() {
|
||||||
loading = true
|
loading = true
|
||||||
await toggleAnalytics()
|
await toggleAnalytics()
|
||||||
const res = await organisation.save({ ...$organisation, company })
|
if (file) {
|
||||||
|
await uploadLogo()
|
||||||
|
}
|
||||||
|
const res = await organisation.save({
|
||||||
|
company: $organisation.company,
|
||||||
|
platformUrl: $organisation.platformUrl,
|
||||||
|
})
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
notifications.success("General settings saved.")
|
notifications.success("Settings saved.")
|
||||||
} else {
|
} else {
|
||||||
notifications.danger("Error when saving settings.")
|
notifications.error(res.message)
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
@ -46,10 +59,9 @@
|
||||||
<div class="intro">
|
<div class="intro">
|
||||||
<Heading size="M">General</Heading>
|
<Heading size="M">General</Heading>
|
||||||
<Body>
|
<Body>
|
||||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Hic vero, aut
|
General is the place where you edit your organisation name, logo. You
|
||||||
culpa provident sunt ratione! Voluptas doloremque, dicta nisi velit
|
can also configure your platform URL as well as turn on or off
|
||||||
perspiciatis, ratione vel blanditiis totam, nam voluptate repellat
|
analytics.
|
||||||
aperiam fuga!
|
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
|
@ -59,14 +71,30 @@
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Organization name</Label>
|
<Label size="L">Organization name</Label>
|
||||||
<Input thin bind:value={company} />
|
<Input thin bind:value={$organisation.company} />
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="field">
|
<div class="field logo">
|
||||||
<Label>Logo</Label>
|
<Label size="L">Logo</Label>
|
||||||
<div class="file">
|
<div class="file">
|
||||||
<Dropzone />
|
<Dropzone
|
||||||
|
value={[file]}
|
||||||
|
on:change={e => {
|
||||||
|
file = e.detail?.[0]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div>
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
|
@ -103,6 +131,9 @@
|
||||||
.file {
|
.file {
|
||||||
max-width: 30ch;
|
max-width: 30ch;
|
||||||
}
|
}
|
||||||
|
.logo {
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
.intro {
|
.intro {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export { organisation } from "./organisation"
|
export { organisation } from "./organisation"
|
||||||
|
export { users } from "./users"
|
||||||
export { admin } from "./admin"
|
export { admin } from "./admin"
|
||||||
export { apps } from "./apps"
|
export { apps } from "./apps"
|
||||||
export { email } from "./email"
|
export { email } from "./email"
|
||||||
|
|
|
@ -1,35 +1,46 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
const FALLBACK_CONFIG = {
|
||||||
|
platformUrl: "",
|
||||||
|
logoUrl: "",
|
||||||
|
docsUrl: "",
|
||||||
|
company: "http://localhost:10000",
|
||||||
|
}
|
||||||
|
|
||||||
export function createOrganisationStore() {
|
export function createOrganisationStore() {
|
||||||
const { subscribe, set } = writable({})
|
const store = writable({})
|
||||||
|
const { subscribe, set } = store
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
const res = await api.get(`/api/admin/configs/settings`)
|
||||||
const response = await api.get(`/api/admin/configs/settings`)
|
const json = await res.json()
|
||||||
const json = await response.json()
|
|
||||||
set(json)
|
if (json.status === 400) {
|
||||||
} catch (error) {
|
set(FALLBACK_CONFIG)
|
||||||
set({
|
} else {
|
||||||
platformUrl: "",
|
set({ ...json.config, _rev: json._rev })
|
||||||
logoUrl: "",
|
|
||||||
docsUrl: "",
|
|
||||||
company: "",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function save(config) {
|
||||||
|
const res = await api.post("/api/admin/configs", {
|
||||||
|
type: "settings",
|
||||||
|
config,
|
||||||
|
_rev: get(store)._rev,
|
||||||
|
})
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.status) {
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
await init()
|
||||||
|
return { status: 200 }
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
save: async config => {
|
set,
|
||||||
try {
|
save,
|
||||||
await api.post("/api/admin/configs", { type: "settings", config })
|
|
||||||
await init()
|
|
||||||
return { status: 200 }
|
|
||||||
} catch (error) {
|
|
||||||
return { error }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
init,
|
init,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import api, { post } from "builderStore/api"
|
||||||
|
import { update } from "lodash"
|
||||||
|
|
||||||
|
export function createUsersStore() {
|
||||||
|
const { subscribe, set } = writable([])
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const response = await api.get(`/api/admin/users`)
|
||||||
|
const json = await response.json()
|
||||||
|
set(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invite(email) {
|
||||||
|
const response = await api.post(`/api/admin/users/invite`, { email })
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
async function acceptInvite(inviteCode, password) {
|
||||||
|
const response = await api.post("/api/admin/users/invite/accept", {
|
||||||
|
inviteCode,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create({ email, password }) {
|
||||||
|
const response = await api.post("/api/admin/users", {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
builder: { global: true },
|
||||||
|
roles: {},
|
||||||
|
})
|
||||||
|
init()
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function del(id) {
|
||||||
|
const response = await api.delete(`/api/admin/users/${id}`)
|
||||||
|
update(users => users.filter(user => user._id !== id))
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRoles(data) {
|
||||||
|
try {
|
||||||
|
const res = await post(`/api/admin/users`, data)
|
||||||
|
const json = await res.json()
|
||||||
|
return json
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
init,
|
||||||
|
invite,
|
||||||
|
acceptInvite,
|
||||||
|
create,
|
||||||
|
updateRoles,
|
||||||
|
del,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const users = createUsersStore()
|
|
@ -1,5 +1,4 @@
|
||||||
const { SearchIndexes } = require("../../../db/utils")
|
const { SearchIndexes } = require("../../../db/utils")
|
||||||
const { checkSlashesInUrl } = require("../../../utilities")
|
|
||||||
const env = require("../../../environment")
|
const env = require("../../../environment")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
|
|
||||||
|
@ -10,7 +9,7 @@ const fetch = require("node-fetch")
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
const luceneEscape = value => {
|
const luceneEscape = value => {
|
||||||
return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&")
|
return `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -118,16 +118,17 @@ exports.upload = async function (ctx) {
|
||||||
// add to configuration structure
|
// add to configuration structure
|
||||||
// TODO: right now this only does a global level
|
// TODO: right now this only does a global level
|
||||||
const db = new CouchDB(GLOBAL_DB)
|
const db = new CouchDB(GLOBAL_DB)
|
||||||
let config = await getScopedFullConfig(db, { type })
|
let cfgStructure = await getScopedFullConfig(db, { type })
|
||||||
if (!config) {
|
if (!cfgStructure) {
|
||||||
config = {
|
cfgStructure = {
|
||||||
_id: generateConfigID({ type }),
|
_id: generateConfigID({ type }),
|
||||||
|
config: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const url = `/${bucket}/${key}`
|
const url = `/${bucket}/${key}`
|
||||||
config[`${name}Url`] = url
|
cfgStructure.config[`${name}Url`] = url
|
||||||
// write back to db with url updated
|
// write back to db with url updated
|
||||||
await db.put(config)
|
await db.put(cfgStructure)
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "File has been uploaded and url stored to config.",
|
message: "File has been uploaded and url stored to config.",
|
||||||
|
|
|
@ -25,9 +25,9 @@ function smtpValidation() {
|
||||||
function settingValidation() {
|
function settingValidation() {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return Joi.object({
|
return Joi.object({
|
||||||
platformUrl: Joi.string().valid("", null),
|
platformUrl: Joi.string().optional(),
|
||||||
logoUrl: Joi.string().valid("", null),
|
logoUrl: Joi.string().optional(),
|
||||||
docsUrl: Joi.string().valid("", null),
|
docsUrl: Joi.string().optional(),
|
||||||
company: Joi.string().required(),
|
company: Joi.string().required(),
|
||||||
}).unknown(true)
|
}).unknown(true)
|
||||||
}
|
}
|
||||||
|
@ -44,9 +44,9 @@ function googleValidation() {
|
||||||
function buildConfigSaveValidation() {
|
function buildConfigSaveValidation() {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return joiValidator.body(Joi.object({
|
return joiValidator.body(Joi.object({
|
||||||
_id: Joi.string(),
|
_id: Joi.string().optional(),
|
||||||
_rev: Joi.string(),
|
_rev: Joi.string().optional(),
|
||||||
group: Joi.string(),
|
group: Joi.string().optional(),
|
||||||
type: Joi.string().valid(...Object.values(Configs)).required(),
|
type: Joi.string().valid(...Object.values(Configs)).required(),
|
||||||
config: Joi.alternatives()
|
config: Joi.alternatives()
|
||||||
.conditional("type", {
|
.conditional("type", {
|
||||||
|
|
|
@ -7,9 +7,8 @@ const {
|
||||||
EmailTemplatePurpose,
|
EmailTemplatePurpose,
|
||||||
} = require("../constants")
|
} = require("../constants")
|
||||||
const { checkSlashesInUrl } = require("./index")
|
const { checkSlashesInUrl } = require("./index")
|
||||||
const env = require("../environment")
|
|
||||||
|
|
||||||
const LOCAL_URL = `http://localhost:${env.PORT}`
|
const LOCAL_URL = `http://localhost:10000`
|
||||||
const BASE_COMPANY = "Budibase"
|
const BASE_COMPANY = "Budibase"
|
||||||
|
|
||||||
exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
||||||
|
@ -42,7 +41,7 @@ exports.getSettingsTemplateContext = async (purpose, code = null) => {
|
||||||
case EmailTemplatePurpose.INVITATION:
|
case EmailTemplatePurpose.INVITATION:
|
||||||
context[InternalTemplateBindings.INVITE_CODE] = code
|
context[InternalTemplateBindings.INVITE_CODE] = code
|
||||||
context[InternalTemplateBindings.INVITE_URL] = checkSlashesInUrl(
|
context[InternalTemplateBindings.INVITE_URL] = checkSlashesInUrl(
|
||||||
`${URL}/invite?code=${code}`
|
`${URL}/builder/invite?code=${code}`
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue