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 db3497c83a
36 changed files with 439 additions and 742 deletions

View File

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

View File

@ -3,27 +3,17 @@
export let size = "M" export let size = "M"
export let serif = false export let serif = false
export let noPadding = false export let weight = null
export let weight = 400 export let textAlign = null
export let textAlign = "left"
</script> </script>
<p <p
style="font-weight:{weight};text-align:{textAlign};" style={`
class:noPadding ${weight ? `font-weight:${weight};` : ""}
${textAlign ? `text-align:${textAlign};` : ""}
`}
class="spectrum-Body spectrum-Body--size{size}" class="spectrum-Body spectrum-Body--size{size}"
class:spectrum-Body--serif={serif} class:spectrum-Body--serif={serif}
> >
<slot /> <slot />
</p> </p>
<style>
p {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
.noPadding {
padding: 0;
margin: 0;
}
</style>

View File

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

View File

@ -47,6 +47,8 @@
}) })
schema.email.displayName = "Email" schema.email.displayName = "Email"
schema.roleId.displayName = "Role" schema.roleId.displayName = "Role"
schema.firstName.displayName = "First Name"
schema.lastName.displayName = "Last Name"
if (schema.status) { if (schema.status) {
schema.status.displayName = "Status" schema.status.displayName = "Status"
} }

View File

@ -32,6 +32,8 @@
delete customSchema["email"] delete customSchema["email"]
delete customSchema["roleId"] delete customSchema["roleId"]
delete customSchema["status"] delete customSchema["status"]
delete customSchema["firstName"]
delete customSchema["lastName"]
return Object.entries(customSchema) return Object.entries(customSchema)
} }
@ -87,6 +89,14 @@
meta={{ name: "password", type: "password" }} meta={{ name: "password", type: "password" }}
bind:value={row.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 <!-- Defer rendering this select until roles load, otherwise the initial
selection is always undefined --> selection is always undefined -->
<Select <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> <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 { organisation } from "stores/portal"
import { auth } from "stores/backend"
let username = "" let email = ""
let password = ""
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> </script>
<div class="login"> <div class="login">
@ -20,9 +34,11 @@
No problem! Just enter your account's email address and we'll send you No problem! Just enter your account's email address and we'll send you
a link to reset it. a link to reset it.
</Body> </Body>
<Input label="Email" bind:value={username} /> <Input label="Email" bind:value={email} />
</Layout> </Layout>
<Button cta on:click={reset}>Reset your password</Button> <Button cta on:click={forgot} disabled={!email}>
Reset your password
</Button>
</Layout> </Layout>
</div> </div>
</div> </div>

View File

@ -1,17 +1,16 @@
<script> <script>
import { Link } from "@budibase/bbui" import { ActionButton } from "@budibase/bbui"
import GoogleLogo from "/assets/google-logo.png" import GoogleLogo from "/assets/google-logo.png"
</script> </script>
<div class="outer"> <ActionButton>
<Link target="_blank" href="/api/admin/auth/google"> <a target="_blank" href="/api/admin/auth/google">
<div class="inner"> <div class="inner">
<img src={GoogleLogo} alt="google icon" /> <img src={GoogleLogo} alt="google icon" />
<p>Sign in with Google</p> <p>Sign in with Google</p>
</div> </div>
</Link> </a>
</div> </ActionButton>
<style> <style>
.outer { .outer {
@ -35,10 +34,8 @@
.inner p { .inner p {
margin: 0; margin: 0;
} }
.outer :global(a) { a {
text-decoration: none; text-decoration: none;
font-weight: 500; color: inherit;
font-size: var(--font-size-m);
color: #fff;
} }
</style> </style>

View File

@ -40,8 +40,8 @@
<Heading>Sign in to Budibase</Heading> <Heading>Sign in to Budibase</Heading>
</Layout> </Layout>
<GoogleButton /> <GoogleButton />
<Layout gap="XS" noPadding>
<Divider noGrid /> <Divider noGrid />
<Layout gap="XS" noPadding>
<Body size="S" textAlign="center">Sign in with email</Body> <Body size="S" textAlign="center">Sign in with email</Body>
<Input label="Email" bind:value={username} /> <Input label="Email" bind:value={username} />
<Input <Input
@ -51,7 +51,7 @@
bind:value={password} bind:value={password}
/> />
</Layout> </Layout>
<Layout gap="S" noPadding> <Layout gap="XS" noPadding>
<Button cta on:click={login}>Sign in to Budibase</Button> <Button cta on:click={login}>Sign in to Budibase</Button>
<ActionButton quiet on:click={() => $goto("./forgot")}> <ActionButton quiet on:click={() => $goto("./forgot")}>
Forgot password? Forgot password?

View File

@ -1,12 +1,23 @@
<script> <script>
import { Input, Button, Layout, Body, Heading } from "@budibase/bbui" import { notifications, Button, Layout, Body, Heading } from "@budibase/bbui"
import { params } from "@roxi/routify"
import { organisation } from "stores/portal" 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"] 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> </script>
<div class="login"> <div class="login">
@ -20,9 +31,11 @@
<Body size="S" textAlign="center"> <Body size="S" textAlign="center">
Please enter the new password you'd like to use. Please enter the new password you'd like to use.
</Body> </Body>
<Input label="Password" bind:value={password} /> <PasswordRepeatInput bind:password bind:error />
</Layout> </Layout>
<Button cta on:click={reset}>Reset your password</Button> <Button cta on:click={reset} disabled={error || !resetCode}>
Reset your password
</Button>
</Layout> </Layout>
</div> </div>
</div> </div>

View File

@ -48,7 +48,7 @@
</ActionMenu> </ActionMenu>
</div> </div>
<div class="status"> <div class="status">
<Body noPadding size="S"> <Body size="S">
Edited {Math.floor(1 + Math.random() * 10)} months ago Edited {Math.floor(1 + Math.random() * 10)} months ago
</Body> </Body>
{#if app.lockedBy} {#if app.lockedBy}

View File

@ -22,7 +22,13 @@
// 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 && !$isActive("./auth")) { if (
loaded &&
hasAdminUser &&
!$auth.user &&
!$isActive("./auth") &&
!$isActive("./invite")
) {
$redirect("./auth/login") $redirect("./auth/login")
} }
} }

View File

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

View File

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

View File

@ -59,44 +59,42 @@
}) })
</script> </script>
<Page> <Layout>
<Layout noPadding> <Layout gap="XS" noPadding>
<div>
<Heading size="M">OAuth</Heading> <Heading size="M">OAuth</Heading>
<Body> <Body>
Every budibase app comes with basic authentication (email/password) Every budibase app comes with basic authentication (email/password)
included. You can add additional authentication methods from the options included. You can add additional authentication methods from the options
below. below.
</Body> </Body>
</div> </Layout>
<Divider />
{#if google} {#if google}
<div> <Divider />
<Layout gap="XS" noPadding>
<Heading size="S"> <Heading size="S">
<span> <span>
<GoogleLogo /> <GoogleLogo />
Google Google
</span> </span>
</Heading> </Heading>
<Body> <Body size="S">
To allow users to authenticate using their Google accounts, fill out To allow users to authenticate using their Google accounts, fill out the
the fields below. fields below.
</Body> </Body>
</div> </Layout>
<Layout gap="XS" noPadding>
{#each ConfigFields.Google as field} {#each ConfigFields.Google as field}
<div class="form-row"> <div class="form-row">
<Label size="L">{field}</Label> <Label size="L">{field}</Label>
<Input bind:value={google.config[field]} /> <Input bind:value={google.config[field]} />
</div> </div>
{/each} {/each}
</Layout>
<div> <div>
<Button primary on:click={() => save(google)}>Save</Button> <Button cta on:click={() => save(google)}>Save</Button>
</div> </div>
<Divider />
{/if} {/if}
</Layout> </Layout>
</Page>
<style> <style>
.form-row { .form-row {

View File

@ -16,7 +16,7 @@
{#each bindings as binding} {#each bindings as binding}
<MenuItem on:click={() => onBindingClick(binding)}> <MenuItem on:click={() => onBindingClick(binding)}>
<Detail size="M">{binding.name}</Detail> <Detail size="M">{binding.name}</Detail>
<Body size="XS" noPadding>{binding.description}</Body> <Body size="XS">{binding.description}</Body>
</MenuItem> </MenuItem>
{/each} {/each}
</Menu> </Menu>

View File

@ -102,59 +102,57 @@
}) })
</script> </script>
<Page> <Layout>
<header> <Layout noPadding gap="XS">
<Heading size="M">Email</Heading> <Heading size="M">Email</Heading>
<Body size="S"> <Body>
Sending email is not required, but highly recommended for processes such Sending email is not required, but highly recommended for processes such
as password recovery. To setup automated auth emails, simply add the as password recovery. To setup automated auth emails, simply add the
values below and click activate. values below and click activate.
</Body> </Body>
</header> </Layout>
<Divider /> <Divider />
{#if smtpConfig} {#if smtpConfig}
<div class="config-form"> <Layout gap="XS" noPadding>
<Heading size="S">SMTP</Heading> <Heading size="S">SMTP</Heading>
<Body size="S"> <Body size="S">
To allow your app to benefit from automated auth emails, add your SMTP To allow your app to benefit from automated auth emails, add your SMTP
details below. details below.
</Body> </Body>
<Layout gap="S"> </Layout>
<Heading size="S"> <Layout gap="XS" noPadding>
<span />
</Heading>
<div class="form-row"> <div class="form-row">
<Label>Host</Label> <Label size="L">Host</Label>
<Input bind:value={smtpConfig.config.host} /> <Input bind:value={smtpConfig.config.host} />
</div> </div>
<div class="form-row"> <div class="form-row">
<Label>Port</Label> <Label size="L">Port</Label>
<Input type="number" bind:value={smtpConfig.config.port} /> <Input type="number" bind:value={smtpConfig.config.port} />
</div> </div>
<div class="form-row"> <div class="form-row">
<Label>User</Label> <Label size="L">User</Label>
<Input bind:value={smtpConfig.config.auth.user} /> <Input bind:value={smtpConfig.config.auth.user} />
</div> </div>
<div class="form-row"> <div class="form-row">
<Label>Password</Label> <Label size="L">Password</Label>
<Input type="password" bind:value={smtpConfig.config.auth.pass} /> <Input type="password" bind:value={smtpConfig.config.auth.pass} />
</div> </div>
<div class="form-row"> <div class="form-row">
<Label>From email address</Label> <Label size="L">From email address</Label>
<Input type="email" bind:value={smtpConfig.config.from} /> <Input type="email" bind:value={smtpConfig.config.from} />
</div> </div>
</Layout> </Layout>
<div>
<Button cta on:click={saveSmtp}>Save</Button> <Button cta on:click={saveSmtp}>Save</Button>
</div> </div>
<Divider /> <Divider />
<Layout gap="XS" noPadding>
<div class="config-form">
<Heading size="S">Templates</Heading> <Heading size="S">Templates</Heading>
<Body size="S"> <Body size="S">
Budibase comes out of the box with ready-made email templates to help Budibase comes out of the box with ready-made email templates to help
with user onboarding. Please refrain from changing the links. with user onboarding. Please refrain from changing the links.
</Body> </Body>
</div> </Layout>
<Table <Table
{customRenderers} {customRenderers}
data={$email.templates} data={$email.templates}
@ -165,27 +163,13 @@
allowEditColumns={false} allowEditColumns={false}
/> />
{/if} {/if}
</Page> </Layout>
<style> <style>
.config-form {
margin-top: 42px;
margin-bottom: 42px;
}
.form-row { .form-row {
display: grid; display: grid;
grid-template-columns: 20% 1fr; grid-template-columns: 25% 1fr;
grid-gap: var(--spacing-l); grid-gap: var(--spacing-l);
}
span {
display: flex;
align-items: center; align-items: center;
gap: var(--spacing-s);
}
header {
margin-bottom: 42px;
} }
</style> </style>

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 }) { async function openUpdateRolesModal({ detail }) {
console.log(detail)
selectedApp = detail selectedApp = detail
editRolesModal.show() editRolesModal.show()
} }
</script> </script>
<Layout noPadding gap="XS"> <Layout noPadding>
<Layout gap="XS" noPadding>
<div class="back"> <div class="back">
<ActionButton on:click={() => $goto("./")} quiet size="S" icon="BackAndroid" <ActionButton
>Back to users</ActionButton on:click={() => $goto("./")}
quiet
size="S"
icon="BackAndroid"
> >
Back to users
</ActionButton>
</div> </div>
<div class="heading">
<Layout noPadding gap="XS">
<Heading>User: {$roleFetch?.data?.email}</Heading> <Heading>User: {$roleFetch?.data?.email}</Heading>
<Body <Body>
>Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis porro Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis porro ut
ut nesciunt ipsam perspiciatis aliquam et hic minus alias beatae. Odit nesciunt ipsam perspiciatis aliquam et hic minus alias beatae. Odit
veritatis quos quas laborum magnam tenetur perspiciatis ex hic. veritatis quos quas laborum magnam tenetur perspiciatis ex hic.
</Body> </Body>
</Layout> </Layout>
</div>
<Divider size="S" /> <Divider size="S" />
<div class="general"> <Layout gap="S" noPadding>
<Heading size="S">General</Heading> <Heading size="S">General</Heading>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label size="L">Email</Label> <Label size="L">Email</Label>
<Input disabled thin value={$roleFetch?.data?.email} /> <Input disabled thin value={$roleFetch?.data?.email} />
</div> </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>
<div class="regenerate"> <div class="regenerate">
<ActionButton size="S" icon="Refresh" quiet <ActionButton size="S" icon="Refresh" quiet>
>Regenerate password</ActionButton Regenerate password
> </ActionButton>
</div>
</div> </div>
</Layout>
<Divider size="S" /> <Divider size="S" />
<div class="roles"> <Layout gap="S" noPadding>
<Heading size="S">Configure roles</Heading> <Heading size="S">Configure roles</Heading>
<Table <Table
on:click={openUpdateRolesModal} on:click={openUpdateRolesModal}
@ -100,16 +110,14 @@
allowSelectRows={false} allowSelectRows={false}
customRenderers={[{ column: "role", component: TagsRenderer }]} customRenderers={[{ column: "role", component: TagsRenderer }]}
/> />
</div> </Layout>
<Divider size="S" /> <Divider size="S" />
<div class="delete"> <Layout gap="XS" noPadding>
<Layout gap="S" noPadding <Heading size="S">Delete user</Heading>
><Heading size="S">Delete user</Heading>
<Body>Deleting a user completely removes them from your account.</Body> <Body>Deleting a user completely removes them from your account.</Body>
</Layout>
<div class="delete-button"> <div class="delete-button">
<Button warning on:click={deleteUserModal.show}>Delete user</Button> <Button warning on:click={deleteUserModal.show}>Delete user</Button>
</div></Layout
>
</div> </div>
</Layout> </Layout>
@ -122,10 +130,9 @@
cancelText="Cancel" cancelText="Cancel"
showCloseIcon={false} showCloseIcon={false}
> >
<Body <Body>
>Are you sure you want to delete <strong>{$roleFetch?.data?.email}</strong Are you sure you want to delete <strong>{$roleFetch?.data?.email}</strong>
></Body </Body>
>
</ModalContent> </ModalContent>
</Modal> </Modal>
<Modal bind:this={editRolesModal}> <Modal bind:this={editRolesModal}>
@ -140,26 +147,12 @@
.fields { .fields {
display: grid; display: grid;
grid-gap: var(--spacing-m); grid-gap: var(--spacing-m);
margin-top: var(--spacing-xl);
} }
.field { .field {
display: grid; display: grid;
grid-template-columns: 32% 1fr; grid-template-columns: 32% 1fr;
align-items: center; align-items: center;
} }
.heading {
margin-bottom: var(--spacing-xl);
}
.general {
position: relative;
margin: var(--spacing-xl) 0;
}
.roles {
margin: var(--spacing-xl) 0;
}
.delete {
margin-top: var(--spacing-xl);
}
.regenerate { .regenerate {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -30,18 +30,18 @@
<ModalContent <ModalContent
onConfirm={createUserFlow} onConfirm={createUserFlow}
size="M" size="M"
title="Add new user options" title="Add new user"
confirmText="Add user" confirmText="Add user"
confirmDisabled={disabled} confirmDisabled={disabled}
cancelText="Cancel" cancelText="Cancel"
disabled={$error} disabled={$error}
showCloseIcon={false} showCloseIcon={false}
> >
<Body noPadding <Body size="S">
>If you have SMTP configured and an email for the new user, you can use the If you have SMTP configured and an email for the new user, you can use the
automated email onboarding flow. Otherwise, use our basic onboarding process automated email onboarding flow. Otherwise, use our basic onboarding process
with autogenerated passwords.</Body with autogenerated passwords.
> </Body>
<Select <Select
placeholder={null} placeholder={null}
bind:value={selected} bind:value={selected}

View File

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

View File

@ -4,8 +4,9 @@
const displayLimit = 5 const displayLimit = 5
$: tags = value?.slice(0, displayLimit) ?? [] $: roles = value?.filter(role => role != null) ?? []
$: leftover = (value?.length ?? 0) - tags.length $: tags = roles.slice(0, displayLimit)
$: leftover = roles.length - tags.length
</script> </script>
<Tags> <Tags>

View File

@ -1,7 +1,6 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { Body, Select, ModalContent, notifications } from "@budibase/bbui" import { Body, Select, ModalContent, notifications } from "@budibase/bbui"
import { fetchData } from "helpers"
import { users } from "stores/portal" import { users } from "stores/portal"
export let app export let app
@ -11,7 +10,7 @@
const roles = app.roles const roles = app.roles
let options = roles.map(role => role._id) let options = roles.map(role => role._id)
let selectedRole let selectedRole = user?.roles?.[app?._id]
async function updateUserRoles() { async function updateUserRoles() {
const res = await users.updateRoles({ const res = await users.updateRoles({
@ -24,7 +23,7 @@
if (res.status === 400) { if (res.status === 400) {
notifications.error("Failed to update role") notifications.error("Failed to update role")
} else { } else {
notifications.success("Roles updated") notifications.success("Role updated")
dispatch("update") dispatch("update")
} }
} }
@ -32,20 +31,20 @@
<ModalContent <ModalContent
onConfirm={updateUserRoles} onConfirm={updateUserRoles}
title="Update App Roles" title="Update App Role"
confirmText="Update roles" confirmText="Update role"
cancelText="Cancel" cancelText="Cancel"
size="M" size="M"
showCloseIcon={false} showCloseIcon={false}
> >
<Body noPadding <Body>
>Update {user.email}'s roles for <strong>{app.name}</strong>.</Body Update {user.email}'s role for <strong>{app.name}</strong>.
> </Body>
<Select <Select
placeholder={null} placeholder={null}
bind:value={selectedRole} bind:value={selectedRole}
on:change on:change
{options} {options}
label="Select roles:" label="Role"
/> />
</ModalContent> </ModalContent>

View File

@ -44,28 +44,27 @@
</script> </script>
<Layout> <Layout>
<div class="heading"> <Layout gap="XS" noPadding>
<Heading>Users</Heading> <Heading>Users</Heading>
<Body <Body>
>Users are the common denominator in Budibase. Each user is assigned to a Users are the common denominator in Budibase. Each user is assigned to a
group that contains apps and permissions. In this section, you can add group that contains apps and permissions. In this section, you can add
users, or edit and delete an existing user.</Body users, or edit and delete an existing user.
> </Body>
</div> </Layout>
<Divider size="S" /> <Divider size="S" />
<Layout gap="S" noPadding>
<div class="users"> <div class="users-heading">
<Heading size="S">Users</Heading> <Heading size="S">Users</Heading>
<div class="field">
<Label size="L">Search / filter</Label>
<Search bind:value={search} placeholder="" />
</div>
<div class="buttons">
<ButtonGroup> <ButtonGroup>
<Button disabled secondary>Import users</Button> <Button disabled secondary>Import users</Button>
<Button primary on:click={createUserModal.show}>Add user</Button> <Button primary on:click={createUserModal.show}>Add user</Button>
</ButtonGroup> </ButtonGroup>
</div> </div>
<div class="field">
<Label size="L">Search / filter</Label>
<Search bind:value={search} placeholder="" />
</div>
<Table <Table
on:click={({ detail }) => $goto(`./${detail._id}`)} on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema} {schema}
@ -75,31 +74,28 @@
allowSelectRows={false} allowSelectRows={false}
customRenderers={[{ column: "group", component: TagsRenderer }]} customRenderers={[{ column: "group", component: TagsRenderer }]}
/> />
</div> </Layout>
</Layout> </Layout>
<Modal bind:this={createUserModal} <Modal bind:this={createUserModal}>
><AddUserModal on:change={openBasicOnoboardingModal} /></Modal <AddUserModal on:change={openBasicOnoboardingModal} />
> </Modal>
<Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal> <Modal bind:this={basicOnboardingModal}><BasicOnboardingModal {email} /></Modal>
<style> <style>
.users {
position: relative;
}
.field { .field {
display: flex; display: flex;
align-items: center; align-items: center;
flex-direction: row; flex-direction: row;
grid-gap: var(--spacing-m); grid-gap: var(--spacing-m);
margin: var(--spacing-xl) 0;
} }
.field > :global(*) + :global(*) { .field > :global(*) + :global(*) {
margin-left: var(--spacing-m); margin-left: var(--spacing-m);
} }
.buttons { .users-heading {
position: absolute; display: flex;
top: 0; flex-direction: row;
right: 0; justify-content: space-between;
align-items: center;
} }
</style> </style>

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,20 +57,19 @@
} }
</script> </script>
<div class="container"> <Layout>
<Layout noPadding> <Layout gap="XS" noPadding>
<div class="intro">
<Heading size="M">General</Heading> <Heading size="M">General</Heading>
<Body> <Body>
General is the place where you edit your organisation name, logo. You General is the place where you edit your organisation name, logo. You can
can also configure your platform URL as well as turn on or off also configure your platform URL as well as turn on or off analytics.
analytics.
</Body> </Body>
</div> </Layout>
<Divider size="S" /> <Divider size="S" />
<div class="information"> <Layout gap="XS" noPadding>
<Heading size="S">Information</Heading> <Heading size="S">Information</Heading>
<Body>Here you can update your logo and organization name.</Body> <Body size="S">Here you can update your logo and organization name.</Body>
</Layout>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label size="L">Organization name</Label> <Label size="L">Organization name</Label>
@ -88,47 +87,46 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<Divider size="S" /> <Divider size="S" />
<div class="analytics"> <Layout gap="XS" noPadding>
<Heading size="S">Platform</Heading> <Heading size="S">Platform</Heading>
<Body>Here you can set up general platform settings.</Body> <Body size="S">Here you can set up general platform settings.</Body>
</Layout>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label size="L">Platform URL</Label> <Label size="L">Platform URL</Label>
<Input thin bind:value={$organisation.platformUrl} /> <Input thin bind:value={$organisation.platformUrl} />
</div> </div>
</div> </div>
</div>
<Divider size="S" /> <Divider size="S" />
<div class="analytics"> <Layout gap="S" noPadding>
<Layout gap="XS" noPadding>
<Heading size="S">Analytics</Heading> <Heading size="S">Analytics</Heading>
<Body> <Body size="S">
If you would like to send analytics that help us make Budibase better, If you would like to send analytics that help us make Budibase better,
please let us know below. please let us know below.
</Body> </Body>
</Layout>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label size="L">Send Analytics to Budibase</Label> <Label size="L">Send Analytics to Budibase</Label>
<Toggle text="" value={!analyticsDisabled} /> <Toggle text="" value={!analyticsDisabled} />
</div> </div>
</div> </div>
</div> </Layout>
<div class="save"> <div>
<Button disabled={loading} on:click={saveConfig} cta>Save</Button> <Button disabled={loading} on:click={saveConfig} cta>Save</Button>
</div> </div>
</Layout> </Layout>
</div>
<style> <style>
.fields { .fields {
display: grid; display: grid;
grid-gap: var(--spacing-m); grid-gap: var(--spacing-m);
margin-top: var(--spacing-xl);
} }
.field { .field {
display: grid; display: grid;
grid-template-columns: 32% 1fr; grid-template-columns: 33% 1fr;
align-items: center; align-items: center;
} }
.file { .file {
@ -137,10 +135,4 @@
.logo { .logo {
align-items: start; align-items: start;
} }
.intro {
display: grid;
}
.save {
margin-left: auto;
}
</style> </style>

View File

@ -33,6 +33,25 @@ export function createAuthStore() {
await response.json() await response.json()
store.update(state => ({ ...state, user: null })) 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 => { createUser: async user => {
const response = await api.post(`/api/admin/users`, user) const response = await api.post(`/api/admin/users`, user)
if (response.status !== 200) { if (response.status !== 200) {

View File

@ -50,6 +50,24 @@ exports.USERS_TABLE_SCHEMA = {
fieldName: "email", fieldName: "email",
name: "email", name: "email",
}, },
firstName: {
name: "firstName",
fieldName: "firstName",
type: exports.FieldTypes.STRING,
constraints: {
type: exports.FieldTypes.STRING,
presence: false,
},
},
lastName: {
name: "lastName",
fieldName: "lastName",
type: exports.FieldTypes.STRING,
constraints: {
type: exports.FieldTypes.STRING,
presence: false,
},
},
roleId: { roleId: {
fieldName: "roleId", fieldName: "roleId",
name: "roleId", name: "roleId",

View File

@ -54,10 +54,13 @@ exports.reset = async ctx => {
} }
try { try {
const user = await getGlobalUserByEmail(email) const user = await getGlobalUserByEmail(email)
// only if user exists, don't error though if they don't
if (user) {
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
user, user,
subject: "{{ company }} platform password reset", subject: "{{ company }} platform password reset",
}) })
}
} catch (err) { } catch (err) {
// don't throw any kind of error to the user, this might give away something // 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. * @return {Promise<boolean>} returns true if there is a configuration that can be used.
*/ */
exports.isEmailConfigured = async (groupId = null) => { exports.isEmailConfigured = async (groupId = null) => {
// when "testing" simply return true
if (TEST_MODE) {
return true
}
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const config = await getSmtpConfiguration(db, groupId) const config = await getSmtpConfiguration(db, groupId)
return config != null return config != null

View File

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