Merge branch 'user-app-list' of github.com:Budibase/budibase into user-app-list
This commit is contained in:
commit
8e10b42f9c
|
@ -22,6 +22,7 @@
|
|||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
position: relative;
|
||||
}
|
||||
.paddingX-S {
|
||||
padding-left: var(--spacing-s);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue