Merge branch 'user-app-list' of github.com:Budibase/budibase into user-app-list

This commit is contained in:
mike12345567 2021-05-19 13:18:38 +01:00
commit 8e10b42f9c
36 changed files with 439 additions and 742 deletions

View File

@ -22,6 +22,7 @@
.container {
display: grid;
grid-template-columns: 1fr;
position: relative;
}
.paddingX-S {
padding-left: var(--spacing-s);

View File

@ -3,27 +3,17 @@
export let size = "M"
export let serif = false
export let noPadding = false
export let weight = 400
export let textAlign = "left"
export let weight = null
export let textAlign = null
</script>
<p
style="font-weight:{weight};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>

View File

@ -3,12 +3,14 @@
// Sizes
export let size = "M"
export let textAlign = "left"
export let textAlign
export let noPadding = false
</script>
<h1 style="text-align:{textAlign};"
class:noPadding
class="spectrum-Heading spectrum-Heading--size{size}">
<h1
style="{textAlign ? `text-align:${textAlign}` : ``}"
class:noPadding
class="spectrum-Heading spectrum-Heading--size{size}"
>
<slot />
</h1>

View File

@ -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"
}

View File

@ -32,6 +32,8 @@
delete customSchema["email"]
delete customSchema["roleId"]
delete customSchema["status"]
delete customSchema["firstName"]
delete customSchema["lastName"]
return Object.entries(customSchema)
}
@ -87,6 +89,14 @@
meta={{ name: "password", type: "password" }}
bind:value={row.password}
/>
<RowFieldControl
meta={{ ...tableSchema.firstName, name: "First Name" }}
bind:value={row.firstName}
/>
<RowFieldControl
meta={{ ...tableSchema.lastName, name: "Last Name" }}
bind:value={row.lastName}
/>
<!-- Defer rendering this select until roles load, otherwise the initial
selection is always undefined -->
<Select

View File

@ -0,0 +1,43 @@
<script>
import { Layout, Input } from "@budibase/bbui"
import {
createValidationStore,
requiredValidator,
} from "../../../helpers/validation"
export let password
export let error
const [firstPassword, passwordError, firstTouched] = createValidationStore(
"",
requiredValidator
)
const [repeatPassword, _, repeatTouched] = createValidationStore(
"",
requiredValidator
)
$: password = $firstPassword
$: error =
!$firstPassword ||
!$firstTouched ||
!$repeatTouched ||
$firstPassword !== $repeatPassword
</script>
<Layout gap="XS" noPadding>
<Input
label="Password"
type="password"
error={$firstTouched && $passwordError}
bind:value={$firstPassword}
/>
<Input
label="Repeat Password"
type="password"
error={$repeatTouched &&
$firstPassword !== $repeatPassword &&
"Passwords must match"}
bind:value={$repeatPassword}
/>
</Layout>

View File

@ -1,11 +1,25 @@
<script>
import { Input, Button, Layout, Body, Heading } from "@budibase/bbui"
import {
notifications,
Input,
Button,
Layout,
Body,
Heading,
} from "@budibase/bbui"
import { organisation } from "stores/portal"
import { auth } from "stores/backend"
let username = ""
let password = ""
let email = ""
async function reset() {}
async function forgot() {
try {
await auth.forgotPassword(email)
notifications.success("Email sent - please check your inbox")
} catch (err) {
notifications.error("Unable to send reset password link")
}
}
</script>
<div class="login">
@ -20,9 +34,11 @@
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={username} />
<Input label="Email" bind:value={email} />
</Layout>
<Button cta on:click={reset}>Reset your password</Button>
<Button cta on:click={forgot} disabled={!email}>
Reset your password
</Button>
</Layout>
</div>
</div>

View File

@ -1,17 +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 {
@ -35,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>

View File

@ -40,8 +40,8 @@
<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
@ -51,7 +51,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?

View File

@ -1,12 +1,23 @@
<script>
import { Input, Button, Layout, Body, Heading } from "@budibase/bbui"
import { params } from "@roxi/routify"
import { notifications, Button, Layout, Body, Heading } from "@budibase/bbui"
import { organisation } 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 = ""
let password, error
async function reset() {}
async function reset() {
try {
await auth.resetPassword(password, resetCode)
notifications.success("Password reset successfully")
// send them to login if reset successful
$goto("./login")
} catch (err) {
notifications.error("Unable to reset password")
}
}
</script>
<div class="login">
@ -20,9 +31,11 @@
<Body size="S" textAlign="center">
Please enter the new password you'd like to use.
</Body>
<Input label="Password" bind:value={password} />
<PasswordRepeatInput bind:password bind:error />
</Layout>
<Button cta on:click={reset}>Reset your password</Button>
<Button cta on:click={reset} disabled={error || !resetCode}>
Reset your password
</Button>
</Layout>
</div>
</div>

View File

@ -48,7 +48,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}

View File

@ -22,7 +22,13 @@
// Redirect to log in at any time if the user isn't authenticated
$: {
if (loaded && hasAdminUser && !$auth.user && !$isActive("./auth")) {
if (
loaded &&
hasAdminUser &&
!$auth.user &&
!$isActive("./auth") &&
!$isActive("./invite")
) {
$redirect("./auth/login")
}
}

View File

@ -36,7 +36,7 @@
<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" textAlign="center" noPadding>
<Body size="M" textAlign="center">
The admin user has access to everything in Budibase.
</Body>
</Layout>

View File

@ -0,0 +1,40 @@
<script>
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import { writable } from "svelte/store"
import { auth } from "stores/backend"
import { saveRow } from "components/backend/DataTable/api"
import { TableNames } from "constants"
const values = writable({
firstName: $auth.user.firstName,
lastName: $auth.user.lastName,
})
const updateInfo = async () => {
const newUser = {
...$auth.user,
firstName: $values.firstName,
lastName: $values.lastName,
}
console.log(newUser)
const response = await saveRow(newUser, TableNames.USERS)
if (response.ok) {
await auth.checkAuth()
notifications.success("Information updated successfully")
} else {
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>

View File

@ -10,6 +10,7 @@
Page,
Icon,
Body,
Modal,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { apps, organisation } from "stores/portal"
@ -17,8 +18,11 @@
import { goto } from "@roxi/routify"
import { AppStatus } from "constants"
import { gradient } from "actions"
import UpdateUserInfoModal from "./_components/UpdateUserInfoModal.svelte"
let loaded = false
let userInfoModal
let userPasswordModal
onMount(async () => {
await organisation.init()
@ -35,8 +39,10 @@
<img src={$organisation.logoUrl} />
<div class="info-title">
<Layout noPadding gap="XS">
<Heading size="L">Hey {$auth.user.email}</Heading>
<Body noPadding>
<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, as well as company news
and the employee handbook.
@ -47,7 +53,9 @@
<Avatar size="M" name="John Doe" />
<Icon size="XL" name="ChevronDown" />
</div>
<MenuItem icon="UserEdit">Update user information</MenuItem>
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
Update user information
</MenuItem>
<MenuItem icon="LockClosed">Update password</MenuItem>
<MenuItem
icon="UserDeveloper"
@ -66,7 +74,7 @@
<div class="group">
<Layout gap="S" noPadding>
<div class="group-title">
<Body weight="500" noPadding size="XS">GROUP</Body>
<Body weight="500" size="XS">GROUP</Body>
{#if $auth.user?.builder?.global}
<Icon name="Settings" hoverable />
{/if}
@ -76,7 +84,7 @@
<div class="preview" use:gradient={{ seed: app.name }} />
<div class="app-info">
<Heading size="XS">{app.name}</Heading>
<Body noPadding size="S">
<Body size="S">
Edited {Math.round(Math.random() * 10 + 1)} months ago
</Body>
</div>
@ -89,6 +97,9 @@
</div>
</Page>
</div>
<Modal bind:this={userInfoModal}>
<UpdateUserInfoModal />
</Modal>
{/if}
<style>
@ -150,7 +161,7 @@
}
.preview {
height: 40px;
width: 40px;
width: 60px;
border-radius: var(--border-radius-s);
}
</style>

View File

@ -1,29 +1,15 @@
<script>
import {
Layout,
Heading,
Body,
Input,
Button,
notifications,
} from "@budibase/bbui"
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
import { goto, params } from "@roxi/routify"
import { createValidationStore, requiredValidator } from "helpers/validation"
import { users, organisation } from "stores/portal"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
const [password, passwordError, passwordTouched] = createValidationStore(
"",
requiredValidator
)
const [repeat, _, repeatTouched] = createValidationStore(
"",
requiredValidator
)
const inviteCode = $params["?code"]
let password, error
async function acceptInvite() {
try {
const res = await users.acceptInvite(inviteCode, $password)
const res = await users.acceptInvite(inviteCode, password)
if (!res) {
throw new Error(res.message)
}
@ -41,31 +27,12 @@
<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" noPadding>
<Body textAlign="center" size="M">
Please enter a password to set up your user.
</Body>
</Layout>
<Layout gap="XS" noPadding>
<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>
<Button
disabled={!$passwordTouched || !$repeatTouched || $password !== $repeat}
cta
on:click={acceptInvite}
>
<PasswordRepeatInput bind:error bind:password />
<Button disabled={error} cta on:click={acceptInvite}>
Accept invite
</Button>
</Layout>

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -51,45 +51,55 @@
}
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>
<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: {$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>
<Divider size="S" />
<div class="general">
<Layout gap="S" noPadding>
<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 class="field">
<Label size="L">First name</Label>
<Input disabled thin value={$roleFetch?.data?.firstName} />
</div>
<div class="field">
<Label size="L">Last name</Label>
<Input disabled thin value={$roleFetch?.data?.lastName} />
</div>
</div>
<div class="regenerate">
<ActionButton size="S" icon="Refresh" quiet
>Regenerate password</ActionButton
>
<ActionButton size="S" icon="Refresh" quiet>
Regenerate password
</ActionButton>
</div>
</div>
</Layout>
<Divider size="S" />
<div class="roles">
<Layout gap="S" noPadding>
<Heading size="S">Configure roles</Heading>
<Table
on:click={openUpdateRolesModal}
@ -100,16 +110,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>
@ -122,10 +130,9 @@
cancelText="Cancel"
showCloseIcon={false}
>
<Body
>Are you sure you want to delete <strong>{$roleFetch?.data?.email}</strong
></Body
>
<Body>
Are you sure you want to delete <strong>{$roleFetch?.data?.email}</strong>
</Body>
</ModalContent>
</Modal>
<Modal bind:this={editRolesModal}>
@ -140,26 +147,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;

View File

@ -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}

View File

@ -26,10 +26,10 @@
error={$touched && $error}
showCloseIcon={false}
>
<Body noPadding
>Below you will find the users 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 users username and password. The password will not
be accessible from this point. Please save the credentials.
</Body>
<Input
type="email"
label="Username"

View File

@ -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>

View File

@ -1,7 +1,6 @@
<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
@ -11,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.updateRoles({
@ -24,7 +23,7 @@
if (res.status === 400) {
notifications.error("Failed to update role")
} else {
notifications.success("Roles updated")
notifications.success("Role updated")
dispatch("update")
}
}
@ -32,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>

View File

@ -44,28 +44,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>
<div class="field">
<Label size="L">Search / filter</Label>
<Search bind:value={search} placeholder="" />
</div>
<div class="buttons">
<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>
<Table
on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema}
@ -75,31 +74,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>

View File

@ -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

View File

@ -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>

View File

@ -57,78 +57,76 @@
}
</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={$organisation.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={[file]}
on:change={e => {
file = 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={$organisation.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="" value={!analyticsDisabled} />
</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 {
@ -137,10 +135,4 @@
.logo {
align-items: start;
}
.intro {
display: grid;
}
.save {
margin-left: auto;
}
</style>

View File

@ -33,6 +33,25 @@ export function createAuthStore() {
await response.json()
store.update(state => ({ ...state, user: null }))
},
forgotPassword: async email => {
const response = await api.post(`/api/admin/auth/reset`, {
email,
})
if (response.status !== 200) {
throw "Unable to send email with reset link"
}
await response.json()
},
resetPassword: async (password, code) => {
const response = await api.post(`/api/admin/auth/reset/update`, {
password,
resetCode: code,
})
if (response.status !== 200) {
throw "Unable to reset password"
}
await response.json()
},
createUser: async user => {
const response = await api.post(`/api/admin/users`, user)
if (response.status !== 200) {

View File

@ -50,6 +50,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",

View File

@ -54,10 +54,13 @@ exports.reset = async ctx => {
}
try {
const user = await getGlobalUserByEmail(email)
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
user,
subject: "{{ company }} platform password reset",
})
// only if user exists, don't error though if they don't
if (user) {
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
user,
subject: "{{ company }} platform password reset",
})
}
} catch (err) {
// don't throw any kind of error to the user, this might give away something
}

View File

@ -1,93 +0,0 @@
const authPkg = require("@budibase/auth")
const { google } = require("@budibase/auth/src/middleware")
const { Configs } = require("../../constants")
const CouchDB = require("../../db")
const { clearCookie } = authPkg.utils
const { Cookies } = authPkg.constants
const { passport } = authPkg.auth
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
exports.authenticate = async (ctx, next) => {
return passport.authenticate("local", async (err, user) => {
if (err) {
return ctx.throw(403, "Unauthorized")
}
const expires = new Date()
expires.setDate(expires.getDate() + 1)
if (!user) {
return ctx.throw(403, "Unauthorized")
}
ctx.cookies.set(Cookies.Auth, user.token, {
expires,
path: "/",
httpOnly: false,
overwrite: true,
})
delete user.token
ctx.body = { user }
})(ctx, next)
}
exports.logout = async ctx => {
clearCookie(ctx, Cookies.Auth)
ctx.body = { message: "User logged out" }
}
/**
* The initial call that google authentication makes to take you to the google login screen.
* On a successful login, you will be redirected to the googleAuth callback route.
*/
exports.googlePreAuth = async (ctx, next) => {
const db = new CouchDB(GLOBAL_DB)
const config = await authPkg.db.getScopedFullConfig(db, {
type: Configs.GOOGLE,
group: ctx.query.group,
})
const strategy = await google.strategyFactory(config)
return passport.authenticate(strategy, {
scope: ["profile", "email"],
})(ctx, next)
}
exports.googleAuth = async (ctx, next) => {
const db = new CouchDB(GLOBAL_DB)
const config = await authPkg.db.getScopedFullConfig(db, {
type: Configs.GOOGLE,
group: ctx.query.group,
})
const strategy = await google.strategyFactory(config)
return passport.authenticate(
strategy,
{ successRedirect: "/", failureRedirect: "/error" },
async (err, user) => {
if (err) {
return ctx.throw(403, "Unauthorized")
}
const expires = new Date()
expires.setDate(expires.getDate() + 1)
if (!user) {
return ctx.throw(403, "Unauthorized")
}
ctx.cookies.set(Cookies.Auth, user.token, {
expires,
path: "/",
httpOnly: false,
overwrite: true,
})
ctx.redirect("/")
}
)(ctx, next)
}

View File

@ -117,6 +117,10 @@ async function getSmtpConfiguration(db, groupId = null) {
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
*/
exports.isEmailConfigured = async (groupId = null) => {
// when "testing" simply return true
if (TEST_MODE) {
return true
}
const db = new CouchDB(GLOBAL_DB)
const config = await getSmtpConfiguration(db, groupId)
return config != null

View File

@ -35,7 +35,7 @@ exports.getSettingsTemplateContext = async (purpose, code = null) => {
case EmailTemplatePurpose.PASSWORD_RECOVERY:
context[InternalTemplateBindings.RESET_CODE] = code
context[InternalTemplateBindings.RESET_URL] = checkSlashesInUrl(
`${URL}/reset?code=${code}`
`${URL}/builder/auth/reset?code=${code}`
)
break
case EmailTemplatePurpose.INVITATION: