Merge branch 'master' of github.com:Budibase/budibase into ak-fixes

This commit is contained in:
Andrew Kingston 2021-05-21 13:42:43 +01:00
commit a9c3194eba
23 changed files with 482 additions and 392 deletions

View File

@ -41,4 +41,7 @@
align-items: center;
grid-template-columns: 200px 20px;
}
.icon {
cursor: pointer;
}
</style>

View File

@ -1,62 +0,0 @@
<script>
import {
notifications,
Input,
Button,
Layout,
Body,
Heading,
} from "@budibase/bbui"
import { organisation, auth } from "stores/portal"
let email = ""
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">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
</Layout>
<Layout gap="XS" noPadding>
<Heading textAlign="center">Forgotten your password?</Heading>
<Body size="S" textAlign="center">
No problem! Just enter your account's email address and we'll send you
a link to reset it.
</Body>
<Input label="Email" bind:value={email} />
</Layout>
<Button cta on:click={forgot} disabled={!email}>
Reset your password
</Button>
</Layout>
</div>
</div>
<style>
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
img {
width: 48px;
}
</style>

View File

@ -1,91 +0,0 @@
<script>
import { goto, params } from "@roxi/routify"
import {
notifications,
Input,
Button,
Divider,
ActionButton,
Layout,
Body,
Heading,
} from "@budibase/bbui"
import GoogleButton from "./GoogleButton.svelte"
import { organisation, auth } from "stores/portal"
let username = ""
let password = ""
async function login() {
try {
await auth.login({
username,
password,
})
if ($params["?returnUrl"]) {
window.location = decodeURIComponent($params["?returnUrl"])
} else {
notifications.success("Logged in successfully")
$goto("../portal")
}
} catch (err) {
console.error(err)
notifications.error("Invalid credentials")
}
}
const submitOnEnter = e => {
if (e.key === "Enter") {
login()
}
}
</script>
<div class="login">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
<Heading>Sign in to Budibase</Heading>
</Layout>
<GoogleButton />
<Divider noGrid />
<Layout gap="XS" noPadding>
<Body size="S" textAlign="center">Sign in with email</Body>
<Input label="Email" bind:value={username} on:keyup={submitOnEnter} />
<Input
label="Password"
type="password"
on:change
bind:value={password}
on:keyup={submitOnEnter}
/>
</Layout>
<Layout gap="XS" noPadding>
<Button cta on:click={login}>Sign in to Budibase</Button>
<ActionButton quiet on:click={() => $goto("./forgot")}>
Forgot password?
</ActionButton>
</Layout>
</Layout>
</div>
</div>
<style>
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
img {
width: 48px;
}
</style>

View File

@ -1,59 +0,0 @@
<script>
import { notifications, Button, Layout, Body, Heading } from "@budibase/bbui"
import { organisation, auth } from "stores/portal"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
import { params, goto } from "@roxi/routify"
const resetCode = $params["?code"]
let password, error
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">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
</Layout>
<Layout gap="XS" noPadding>
<Heading textAlign="center">Reset your password</Heading>
<Body size="S" textAlign="center">
Please enter the new password you'd like to use.
</Body>
<PasswordRepeatInput bind:password bind:error />
</Layout>
<Button cta on:click={reset} disabled={error || !resetCode}>
Reset your password
</Button>
</Layout>
</div>
</div>
<style>
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 260px;
}
img {
width: 48px;
}
</style>

View File

@ -1 +0,0 @@
export { LoginForm } from "./LoginForm.svelte"

View File

@ -1,7 +1,7 @@
<script>
import { onMount } from "svelte"
import { isActive, redirect } from "@roxi/routify"
import { admin, auth } from "stores/portal"
import { onMount } from "svelte"
let loaded = false
$: hasAdminUser = !!$admin?.checklist?.adminUser
@ -30,6 +30,8 @@
) {
const returnUrl = encodeURIComponent(window.location.pathname)
$redirect("./auth/login?", { returnUrl })
} else if ($auth?.user?.forceResetPassword) {
$redirect("./auth/reset")
}
}
</script>

View File

@ -1,5 +1,62 @@
<script>
import ForgotForm from "components/login/ForgotForm.svelte"
import {
notifications,
Input,
Button,
Layout,
Body,
Heading,
} from "@budibase/bbui"
import { organisation, auth } from "stores/portal"
let email = ""
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>
<ForgotForm />
<div class="login">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
</Layout>
<Layout gap="XS" noPadding>
<Heading textAlign="center">Forgotten your password?</Heading>
<Body size="S" textAlign="center">
No problem! Just enter your account's email address and we'll send you
a link to reset it.
</Body>
<Input label="Email" bind:value={email} />
</Layout>
<Button cta on:click={forgot} disabled={!email}>
Reset your password
</Button>
</Layout>
</div>
</div>
<style>
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
img {
width: 48px;
}
</style>

View File

@ -1,5 +1,96 @@
<script>
import LoginForm from "components/login/LoginForm.svelte"
import {
ActionButton,
Body,
Button,
Divider,
Heading,
Input,
Layout,
notifications,
} from "@budibase/bbui"
import { goto, params } from "@roxi/routify"
import { auth, organisation } from "stores/portal"
import GoogleButton from "./_components/GoogleButton.svelte"
let username = ""
let password = ""
async function login() {
try {
await auth.login({
username,
password,
})
notifications.success("Logged in successfully")
if ($auth?.user?.forceResetPassword) {
$goto("./reset")
} else {
if ($params["?returnUrl"]) {
window.location = decodeURIComponent($params["?returnUrl"])
} else {
notifications.success("Logged in successfully")
$goto("../portal")
}
}
} catch (err) {
console.error(err)
notifications.error("Invalid credentials")
}
}
const submitOnEnter = e => {
if (e.key === "Enter") {
login()
}
}
</script>
<LoginForm />
<div class="login">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
<Heading>Sign in to Budibase</Heading>
</Layout>
<GoogleButton />
<Divider noGrid />
<Layout gap="XS" noPadding>
<Body size="S" textAlign="center">Sign in with email</Body>
<Input label="Email" bind:value={username} on:keyup={submitOnEnter} />
<Input
label="Password"
type="password"
on:change
bind:value={password}
on:keyup={submitOnEnter}
/>
</Layout>
<Layout gap="XS" noPadding>
<Button cta on:click={login}>Sign in to Budibase</Button>
<ActionButton quiet on:click={() => $goto("./forgot")}>
Forgot password?
</ActionButton>
</Layout>
</Layout>
</div>
</div>
<style>
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
img {
width: 48px;
}
</style>

View File

@ -1,5 +1,76 @@
<script>
import ResetForm from "components/login/ResetForm.svelte"
import { Body, Button, Heading, Layout, notifications } from "@budibase/bbui"
import { goto, params } from "@roxi/routify"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
import { auth, organisation } from "stores/portal"
const resetCode = $params["?code"]
let password, error
$: forceResetPassword = $auth?.user?.forceResetPassword
async function reset() {
try {
if (forceResetPassword) {
await auth.updateSelf({
...$auth.user,
password,
forceResetPassword: false,
})
$goto("../portal/")
} else {
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>
<ResetForm />
<div class="login">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img
src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"}
alt="Organisation logo"
/>
</Layout>
<Layout gap="XS" noPadding>
<Heading textAlign="center">Reset your password</Heading>
<Body size="S" textAlign="center">
Please enter the new password you'd like to use.
</Body>
<PasswordRepeatInput bind:password bind:error />
</Layout>
<Button
cta
on:click={reset}
disabled={error || (forceResetPassword ? false : !resetCode)}
>Reset your password</Button
>
</Layout>
</div>
</div>
<style>
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 260px;
}
img {
width: 48px;
}
</style>

View File

@ -1,35 +1,32 @@
<script>
import { onMount, tick } from "svelte"
import {
Button,
Detail,
Heading,
notifications,
Icon,
ActionButton,
Body,
Page,
Layout,
notifications,
Tabs,
Tab,
} from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { fade } from "svelte/transition"
import { email } from "stores/portal"
import Editor from "components/integration/QueryEditor.svelte"
import TemplateBindings from "./_components/TemplateBindings.svelte"
const ConfigTypes = {
SMTP: "smtp",
}
export let template
let selected = "Edit"
let selectedBindingTab = "Template"
let htmlEditor
let mounted = false
$: selectedTemplate = $email.templates.find(
$: selectedTemplate = $email?.templates?.find(
({ purpose }) => purpose === template
)
$: templateBindings =
$email.definitions?.bindings[selectedTemplate.purpose] || []
$email.definitions?.bindings?.[selectedTemplate.purpose] || []
async function saveTemplate() {
try {
@ -44,12 +41,32 @@
function setTemplateBinding(binding) {
htmlEditor.update((selectedTemplate.contents += `{{ ${binding.name} }}`))
}
onMount(() => {
mounted = true
})
async function fixMountBug({ detail }) {
console.log(detail)
if (detail === "Edit") {
await tick()
mounted = true
} else {
mounted = false
}
}
</script>
<Page wide gap="L">
<div class="backbutton" on:click={() => $goto("./")}>
<Icon name="BackAndroid" />
<span>Back</span>
<Page wide>
<Layout gap="XS" noPadding>
<div class="back">
<ActionButton
on:click={() => $goto("./")}
quiet
size="S"
icon="BackAndroid"
>
Back to email settings
</ActionButton>
</div>
<header>
<Heading>
@ -57,7 +74,12 @@
</Heading>
<Button cta on:click={saveTemplate}>Save</Button>
</header>
<Tabs {selected}>
<Body
>Change the email template here. Add dynamic content by using the bindings
menu on the right.</Body
>
</Layout>
<Tabs selected="Edit" on:select={fixMountBug}>
<Tab title="Edit">
<div class="template-editor">
<Editor
@ -67,11 +89,12 @@
on:change={e => {
selectedTemplate.contents = e.detail.value
}}
value={selectedTemplate.contents}
value={selectedTemplate?.contents}
/>
<div class="bindings-editor">
<Detail size="L">Bindings</Detail>
<Tabs selected={selectedBindingTab}>
{#if mounted}
<Tabs selected="Template">
<Tab title="Template">
<TemplateBindings
title="Template Bindings"
@ -82,17 +105,18 @@
<Tab title="Common">
<TemplateBindings
title="Common Bindings"
bindings={$email.definitions.bindings.common}
bindings={$email?.definitions?.bindings?.common}
onBindingClick={setTemplateBinding}
/>
</Tab>
</Tabs>
{/if}
</div>
</div></Tab
>
<Tab title="Preview">
<div class="preview" transition:fade>
{@html selectedTemplate.contents}
<div class="preview">
{@html selectedTemplate?.contents}
</div>
</Tab>
</Tabs>
@ -101,7 +125,7 @@
<style>
.template-editor {
display: grid;
grid-template-columns: 1fr 20%;
grid-template-columns: 1fr minmax(250px, 20%);
grid-gap: var(--spacing-xl);
margin-top: var(--spacing-xl);
}
@ -110,7 +134,6 @@
display: flex;
width: 100%;
justify-content: space-between;
margin-bottom: var(--spacing-l);
margin-top: var(--spacing-l);
}
@ -119,11 +142,4 @@
height: 800px;
padding: var(--spacing-xl);
}
.backbutton {
display: flex;
gap: var(--spacing-m);
margin-bottom: var(--spacing-xl);
cursor: pointer;
}
</style>

View File

@ -1,12 +1,5 @@
<script>
import {
Body,
Menu,
MenuItem,
Detail,
MenuSection,
DetailSummary,
} from "@budibase/bbui"
import { Body, Menu, MenuItem, Detail } from "@budibase/bbui"
export let bindings
export let onBindingClick = () => {}

View File

@ -0,0 +1,6 @@
<script>
import { email } from "stores/portal"
email.templates.fetch()
</script>
<slot />

View File

@ -1,29 +1,18 @@
<script>
import { goto } from "@roxi/routify"
import {
Menu,
MenuItem,
Button,
Heading,
Divider,
Label,
Modal,
ModalContent,
Page,
notifications,
Layout,
Input,
TextArea,
Body,
Page,
Select,
MenuSection,
MenuSeparator,
Table,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { email } from "stores/portal"
import Editor from "components/integration/QueryEditor.svelte"
import TemplateBindings from "./_components/TemplateBindings.svelte"
import TemplateLink from "./_components/TemplateLink.svelte"
import api from "builderStore/api"
@ -46,9 +35,6 @@
]
let smtpConfig
let bindingsOpen = false
let htmlModal
let htmlEditor
let loading
async function saveSmtp() {
@ -66,16 +52,8 @@
}
}
async function saveTemplate() {
try {
await email.templates.save(selectedTemplate)
notifications.success(`Template saved.`)
} catch (err) {
notifications.error(`Failed to update template settings. ${err}`)
}
}
async function fetchSmtp() {
loading = true
// fetch the configs for smtp
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`)
const smtpDoc = await smtpResponse.json()
@ -92,17 +70,14 @@
} else {
smtpConfig = smtpDoc
}
loading = false
}
onMount(async () => {
loading = true
await fetchSmtp()
await email.templates.fetch()
loading = false
})
fetchSmtp()
</script>
<Layout>
<Page>
<Layout>
<Layout noPadding gap="XS">
<Heading size="M">Email</Heading>
<Body>
@ -158,12 +133,14 @@
data={$email.templates}
schema={templateSchema}
{loading}
on:click={({ detail }) => $goto(`./${detail.purpose}`)}
allowEditRows={false}
allowSelectRows={false}
allowEditColumns={false}
/>
{/if}
</Layout>
</Layout>
</Page>
<style>
.form-row {

View File

@ -9,6 +9,8 @@
Divider,
Label,
Input,
Select,
Toggle,
Modal,
Table,
ModalContent,
@ -19,10 +21,12 @@
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
export let userId
let deleteUserModal
let editRolesModal
let resetPasswordModal
const roleSchema = {
name: { displayName: "App" },
@ -33,23 +37,32 @@
$: appList = Object.keys($apps?.data).map(id => ({
...$apps?.data?.[id],
_id: id,
role: [$roleFetch?.data?.roles?.[id]],
role: [$userFetch?.data?.roles?.[id]],
}))
let selectedApp
const roleFetch = fetchData(`/api/admin/users/${userId}`)
const userFetch = fetchData(`/api/admin/users/${userId}`)
const apps = fetchData(`/api/admin/roles`)
async function deleteUser() {
const res = await users.del(userId)
const res = await users.delete(userId)
if (res.message) {
notifications.success(`User ${$roleFetch?.data?.email} deleted.`)
notifications.success(`User ${$userFetch?.data?.email} deleted.`)
$goto("./")
} else {
notifications.error("Failed to delete user.")
}
}
let toggleDisabled = false
async function toggleBuilderAccess({ detail }) {
toggleDisabled = true
await users.save({ ...$userFetch?.data, builder: { global: detail } })
await userFetch.refresh()
toggleDisabled = false
}
async function openUpdateRolesModal({ detail }) {
selectedApp = detail
editRolesModal.show()
@ -68,11 +81,10 @@
Back to users
</ActionButton>
</div>
<Heading>User: {$roleFetch?.data?.email}</Heading>
<Heading>User: {$userFetch?.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.
Change user settings and update their app roles. Also contains the ability
to delete the user as well as force reset their password..
</Body>
</Layout>
<Divider size="S" />
@ -81,21 +93,37 @@
<div class="fields">
<div class="field">
<Label size="L">Email</Label>
<Input disabled thin value={$roleFetch?.data?.email} />
<Input disabled thin value={$userFetch?.data?.email} />
</div>
<div class="field">
<Label size="L">Group(s)</Label>
<Select disabled options={["All users"]} value="All users" />
</div>
<div class="field">
<Label size="L">First name</Label>
<Input disabled thin value={$roleFetch?.data?.firstName} />
<Input disabled thin value={$userFetch?.data?.firstName} />
</div>
<div class="field">
<Label size="L">Last name</Label>
<Input disabled thin value={$roleFetch?.data?.lastName} />
<Input disabled thin value={$userFetch?.data?.lastName} />
</div>
<div class="field">
<Label size="L">Development access?</Label>
<Toggle
text=""
value={$userFetch?.data?.builder?.global}
on:change={toggleBuilderAccess}
disabled={toggleDisabled}
/>
</div>
</div>
<div class="regenerate">
<ActionButton size="S" icon="Refresh" quiet>
Regenerate password
</ActionButton>
<ActionButton
size="S"
icon="Refresh"
quiet
on:click={resetPasswordModal.show}>Force password reset</ActionButton
>
</div>
</Layout>
<Divider size="S" />
@ -131,15 +159,21 @@
showCloseIcon={false}
>
<Body>
Are you sure you want to delete <strong>{$roleFetch?.data?.email}</strong>
Are you sure you want to delete <strong>{$userFetch?.data?.email}</strong>
</Body>
</ModalContent>
</Modal>
<Modal bind:this={editRolesModal}>
<UpdateRolesModal
app={selectedApp}
user={$roleFetch.data}
on:update={roleFetch.refresh}
user={$userFetch.data}
on:update={userFetch.refresh}
/>
</Modal>
<Modal bind:this={resetPasswordModal}>
<ForceResetPasswordModal
user={$userFetch.data}
on:update={userFetch.refresh}
/>
</Modal>

View File

@ -0,0 +1,41 @@
<script>
import { createEventDispatcher } from "svelte"
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import { users } from "stores/portal"
const dispatch = createEventDispatcher()
export let user
const password = Math.random().toString(36).substr(2, 20)
async function resetPassword() {
const res = await users.save({
...user,
password,
forceResetPassword: true,
})
if (res.status) {
notifications.error(res.message)
} else {
notifications.success("Password reset.")
dispatch("update")
}
}
</script>
<ModalContent
onConfirm={resetPassword}
size="M"
title="Force Reset User Password"
confirmText="Reset password"
cancelText="Cancel"
showCloseIcon={false}
>
<Body noPadding
>Before you reset the users password, do not forget to copy the new
password. The user will need this to login. Once the user has logged in they
will be asked to create a new password that is more secure.</Body
>
<Input disabled label="Password" value={password} />
</ModalContent>

View File

@ -11,7 +11,7 @@
<Tags>
{#each tags as tag}
<Tag>
<Tag disabled>
{tag}
</Tag>
{/each}

View File

@ -13,7 +13,7 @@
let selectedRole = user?.roles?.[app?._id]
async function updateUserRoles() {
const res = await users.updateRoles({
const res = await users.save({
...user,
roles: {
...user.roles,

View File

@ -0,0 +1,7 @@
<script>
import { Page } from "@budibase/bbui"
</script>
<Page>
<slot />
</Page>

View File

@ -21,7 +21,7 @@
const schema = {
email: {},
status: { displayName: "Development Access", type: "boolean" },
developmentAccess: { displayName: "Development Access", type: "boolean" },
// role: { type: "options" },
group: {},
// access: {},
@ -32,7 +32,11 @@
let email
$: filteredUsers = $users
.filter(user => user.email.includes(search || ""))
.map(user => ({ ...user, group: ["All"] }))
.map(user => ({
...user,
group: ["All users"],
developmentAccess: user.builder.global,
}))
let createUserModal
let basicOnboardingModal

View File

@ -40,7 +40,7 @@ export function createUsersStore() {
return await response.json()
}
async function updateRoles(data) {
async function save(data) {
try {
const res = await post(`/api/admin/users`, data)
const json = await res.json()
@ -57,8 +57,8 @@ export function createUsersStore() {
invite,
acceptInvite,
create,
updateRoles,
del,
save,
delete: del,
}
}

View File

@ -10,6 +10,7 @@ function buildUserSaveValidation(isSelf = false) {
let schema = {
email: Joi.string().allow(null, ""),
password: Joi.string().allow(null, ""),
forceResetPassword: Joi.boolean().optional(),
firstName: Joi.string().allow(null, ""),
lastName: Joi.string().allow(null, ""),
builder: Joi.object({