Merge branch 'master' of github.com:Budibase/budibase into feature/app-updated-at

This commit is contained in:
mike12345567 2021-05-21 14:57:41 +01:00
commit ede7e38442
25 changed files with 497 additions and 390 deletions

View File

@ -41,4 +41,7 @@
align-items: center; align-items: center;
grid-template-columns: 200px 20px; grid-template-columns: 200px 20px;
} }
.icon {
cursor: pointer;
}
</style> </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,84 +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")
}
}
</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} />
<Input
label="Password"
type="password"
on:change
bind:value={password}
/>
</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

@ -8,7 +8,6 @@
MenuItem, MenuItem,
StatusLight, StatusLight,
} from "@budibase/bbui" } from "@budibase/bbui"
import { auth } from "stores/portal"
export let app export let app
export let exportApp export let exportApp

View File

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

View File

@ -1,5 +1,62 @@
<script> <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> </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,89 @@
<script> <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")
}
}
</script> </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} />
<Input
label="Password"
type="password"
on:change
bind:value={password}
/>
</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> <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> </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,34 @@
<script> <script>
import { onMount, tick } from "svelte"
import { import {
Button, Button,
Detail, Detail,
Heading, Heading,
notifications, ActionButton,
Icon, Body,
Page, Page,
Layout,
notifications,
Tabs, Tabs,
Tab, Tab,
} from "@budibase/bbui" } from "@budibase/bbui"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { fade } from "svelte/transition"
import { email } from "stores/portal" import { email } from "stores/portal"
import Editor from "components/integration/QueryEditor.svelte" import Editor from "components/integration/QueryEditor.svelte"
import TemplateBindings from "./_components/TemplateBindings.svelte" import TemplateBindings from "./_components/TemplateBindings.svelte"
const ConfigTypes = {
SMTP: "smtp",
}
export let template export let template
let selected = "Edit"
let selectedBindingTab = "Template"
let htmlEditor let htmlEditor
let mounted = false
$: selectedTemplate = $email.templates.find( $: selectedTemplate = $email.templates?.find(
({ purpose }) => purpose === template ({ purpose }) => purpose === template
) )
$: baseTemplate = $email.templates?.find(({ purpose }) => purpose === "base")
$: templateBindings = $: templateBindings =
$email.definitions?.bindings[selectedTemplate.purpose] || [] $email.definitions?.bindings?.[selectedTemplate.purpose] || []
$: previewContent = makePreviewContent(baseTemplate, selectedTemplate)
async function saveTemplate() { async function saveTemplate() {
try { try {
@ -44,20 +43,56 @@
function setTemplateBinding(binding) { function setTemplateBinding(binding) {
htmlEditor.update((selectedTemplate.contents += `{{ ${binding.name} }}`)) htmlEditor.update((selectedTemplate.contents += `{{ ${binding.name} }}`))
} }
const makePreviewContent = (baseTemplate, selectedTemplate) => {
if (!selectedTemplate) {
return ""
}
if (selectedTemplate.purpose === "base") {
return selectedTemplate.contents
}
const base = baseTemplate?.contents ?? ""
return base.replace("{{ body }}", selectedTemplate?.contents ?? "")
}
onMount(() => {
mounted = true
})
async function fixMountBug({ detail }) {
if (detail === "Edit") {
await tick()
mounted = true
} else {
mounted = false
}
}
</script> </script>
<Page wide gap="L"> <Page wide>
<div class="backbutton" on:click={() => $goto("./")}> <Layout gap="XS" noPadding>
<Icon name="BackAndroid" /> <div class="back">
<span>Back</span> <ActionButton
</div> on:click={() => $goto("./")}
<header> quiet
<Heading> size="S"
Email Template: {template} icon="BackAndroid"
</Heading> >
<Button cta on:click={saveTemplate}>Save</Button> Back to email settings
</header> </ActionButton>
<Tabs {selected}> </div>
<header>
<Heading>
Email Template: {template}
</Heading>
<Button cta on:click={saveTemplate}>Save</Button>
</header>
<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"> <Tab title="Edit">
<div class="template-editor"> <div class="template-editor">
<Editor <Editor
@ -67,32 +102,34 @@
on:change={e => { on:change={e => {
selectedTemplate.contents = e.detail.value selectedTemplate.contents = e.detail.value
}} }}
value={selectedTemplate.contents} value={selectedTemplate?.contents}
/> />
<div class="bindings-editor"> <div class="bindings-editor">
<Detail size="L">Bindings</Detail> <Detail size="L">Bindings</Detail>
<Tabs selected={selectedBindingTab}> {#if mounted}
<Tab title="Template"> <Tabs selected="Template">
<TemplateBindings <Tab title="Template">
title="Template Bindings" <TemplateBindings
bindings={templateBindings} title="Template Bindings"
onBindingClick={setTemplateBinding} bindings={templateBindings}
/> onBindingClick={setTemplateBinding}
</Tab> />
<Tab title="Common"> </Tab>
<TemplateBindings <Tab title="Common">
title="Common Bindings" <TemplateBindings
bindings={$email.definitions.bindings.common} title="Common Bindings"
onBindingClick={setTemplateBinding} bindings={$email?.definitions?.bindings?.common}
/> onBindingClick={setTemplateBinding}
</Tab> />
</Tabs> </Tab>
</Tabs>
{/if}
</div> </div>
</div></Tab </div>
> </Tab>
<Tab title="Preview"> <Tab title="Preview">
<div class="preview" transition:fade> <div class="preview">
{@html selectedTemplate.contents} <iframe srcdoc={previewContent} />
</div> </div>
</Tab> </Tab>
</Tabs> </Tabs>
@ -101,7 +138,7 @@
<style> <style>
.template-editor { .template-editor {
display: grid; display: grid;
grid-template-columns: 1fr 20%; grid-template-columns: 1fr minmax(250px, 20%);
grid-gap: var(--spacing-xl); grid-gap: var(--spacing-xl);
margin-top: var(--spacing-xl); margin-top: var(--spacing-xl);
} }
@ -110,20 +147,17 @@
display: flex; display: flex;
width: 100%; width: 100%;
justify-content: space-between; justify-content: space-between;
margin-bottom: var(--spacing-l);
margin-top: var(--spacing-l); margin-top: var(--spacing-l);
} }
.preview { .preview {
background: white;
height: 800px; height: 800px;
padding: var(--spacing-xl); padding: var(--spacing-xl) 0;
position: relative;
} }
iframe {
.backbutton { width: 100%;
display: flex; height: 100%;
gap: var(--spacing-m); border: none;
margin-bottom: var(--spacing-xl);
cursor: pointer;
} }
</style> </style>

View File

@ -1,12 +1,5 @@
<script> <script>
import { import { Body, Menu, MenuItem, Detail } from "@budibase/bbui"
Body,
Menu,
MenuItem,
Detail,
MenuSection,
DetailSummary,
} from "@budibase/bbui"
export let bindings export let bindings
export let onBindingClick = () => {} 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> <script>
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { import {
Menu,
MenuItem,
Button, Button,
Heading, Heading,
Divider, Divider,
Label, Label,
Modal, Page,
ModalContent,
notifications, notifications,
Layout, Layout,
Input, Input,
TextArea,
Body, Body,
Page,
Select,
MenuSection,
MenuSeparator,
Table, Table,
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte"
import { email } from "stores/portal" 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 TemplateLink from "./_components/TemplateLink.svelte"
import api from "builderStore/api" import api from "builderStore/api"
@ -46,9 +35,6 @@
] ]
let smtpConfig let smtpConfig
let bindingsOpen = false
let htmlModal
let htmlEditor
let loading let loading
async function saveSmtp() { 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() { async function fetchSmtp() {
loading = true
// fetch the configs for smtp // fetch the configs for smtp
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`) const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`)
const smtpDoc = await smtpResponse.json() const smtpDoc = await smtpResponse.json()
@ -92,78 +70,77 @@
} else { } else {
smtpConfig = smtpDoc smtpConfig = smtpDoc
} }
loading = false
} }
onMount(async () => { fetchSmtp()
loading = true
await fetchSmtp()
await email.templates.fetch()
loading = false
})
</script> </script>
<Layout> <Page>
<Layout noPadding gap="XS"> <Layout>
<Heading size="M">Email</Heading> <Layout noPadding gap="XS">
<Body> <Heading size="M">Email</Heading>
Sending email is not required, but highly recommended for processes such <Body>
as password recovery. To setup automated auth emails, simply add the Sending email is not required, but highly recommended for processes such
values below and click activate. as password recovery. To setup automated auth emails, simply add the
</Body> values below and click activate.
</Layout>
<Divider />
{#if smtpConfig}
<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> </Body>
</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 /> <Divider />
<Layout gap="XS" noPadding> {#if smtpConfig}
<Heading size="S">Templates</Heading> <Layout gap="XS" noPadding>
<Body size="S"> <Heading size="S">SMTP</Heading>
Budibase comes out of the box with ready-made email templates to help <Body size="S">
with user onboarding. Please refrain from changing the links. To allow your app to benefit from automated auth emails, add your SMTP
</Body> details below.
</Layout> </Body>
<Table </Layout>
{customRenderers} <Layout gap="XS" noPadding>
data={$email.templates} <div class="form-row">
schema={templateSchema} <Label size="L">Host</Label>
{loading} <Input bind:value={smtpConfig.config.host} />
allowEditRows={false} </div>
allowSelectRows={false} <div class="form-row">
allowEditColumns={false} <Label size="L">Port</Label>
/> <Input type="number" bind:value={smtpConfig.config.port} />
{/if} </div>
</Layout> <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 />
<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>
</Layout>
<Table
{customRenderers}
data={$email.templates}
schema={templateSchema}
{loading}
on:click={({ detail }) => $goto(`./${detail.purpose}`)}
allowEditRows={false}
allowSelectRows={false}
allowEditColumns={false}
/>
{/if}
</Layout>
</Page>
<style> <style>
.form-row { .form-row {

View File

@ -9,6 +9,8 @@
Divider, Divider,
Label, Label,
Input, Input,
Select,
Toggle,
Modal, Modal,
Table, Table,
ModalContent, ModalContent,
@ -19,10 +21,12 @@
import TagsRenderer from "./_components/TagsTableRenderer.svelte" import TagsRenderer from "./_components/TagsTableRenderer.svelte"
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte" import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
export let userId export let userId
let deleteUserModal let deleteUserModal
let editRolesModal let editRolesModal
let resetPasswordModal
const roleSchema = { const roleSchema = {
name: { displayName: "App" }, name: { displayName: "App" },
@ -33,23 +37,32 @@
$: appList = Object.keys($apps?.data).map(id => ({ $: appList = Object.keys($apps?.data).map(id => ({
...$apps?.data?.[id], ...$apps?.data?.[id],
_id: id, _id: id,
role: [$roleFetch?.data?.roles?.[id]], role: [$userFetch?.data?.roles?.[id]],
})) }))
let selectedApp let selectedApp
const roleFetch = fetchData(`/api/admin/users/${userId}`) const userFetch = fetchData(`/api/admin/users/${userId}`)
const apps = fetchData(`/api/admin/roles`) const apps = fetchData(`/api/admin/roles`)
async function deleteUser() { async function deleteUser() {
const res = await users.del(userId) const res = await users.delete(userId)
if (res.message) { if (res.message) {
notifications.success(`User ${$roleFetch?.data?.email} deleted.`) notifications.success(`User ${$userFetch?.data?.email} deleted.`)
$goto("./") $goto("./")
} else { } else {
notifications.error("Failed to delete user.") 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 }) { async function openUpdateRolesModal({ detail }) {
selectedApp = detail selectedApp = detail
editRolesModal.show() editRolesModal.show()
@ -68,11 +81,10 @@
Back to users Back to users
</ActionButton> </ActionButton>
</div> </div>
<Heading>User: {$roleFetch?.data?.email}</Heading> <Heading>User: {$userFetch?.data?.email}</Heading>
<Body> <Body>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis porro ut Change user settings and update their app roles. Also contains the ability
nesciunt ipsam perspiciatis aliquam et hic minus alias beatae. Odit to delete the user as well as force reset their password..
veritatis quos quas laborum magnam tenetur perspiciatis ex hic.
</Body> </Body>
</Layout> </Layout>
<Divider size="S" /> <Divider size="S" />
@ -81,21 +93,37 @@
<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={$userFetch?.data?.email} />
</div>
<div class="field">
<Label size="L">Group(s)</Label>
<Select disabled options={["All users"]} value="All users" />
</div> </div>
<div class="field"> <div class="field">
<Label size="L">First name</Label> <Label size="L">First name</Label>
<Input disabled thin value={$roleFetch?.data?.firstName} /> <Input disabled thin value={$userFetch?.data?.firstName} />
</div> </div>
<div class="field"> <div class="field">
<Label size="L">Last name</Label> <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> </div>
<div class="regenerate"> <div class="regenerate">
<ActionButton size="S" icon="Refresh" quiet> <ActionButton
Regenerate password size="S"
</ActionButton> icon="Refresh"
quiet
on:click={resetPasswordModal.show}>Force password reset</ActionButton
>
</div> </div>
</Layout> </Layout>
<Divider size="S" /> <Divider size="S" />
@ -131,15 +159,21 @@
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>{$userFetch?.data?.email}</strong>
</Body> </Body>
</ModalContent> </ModalContent>
</Modal> </Modal>
<Modal bind:this={editRolesModal}> <Modal bind:this={editRolesModal}>
<UpdateRolesModal <UpdateRolesModal
app={selectedApp} app={selectedApp}
user={$roleFetch.data} user={$userFetch.data}
on:update={roleFetch.refresh} on:update={userFetch.refresh}
/>
</Modal>
<Modal bind:this={resetPasswordModal}>
<ForceResetPasswordModal
user={$userFetch.data}
on:update={userFetch.refresh}
/> />
</Modal> </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> <Tags>
{#each tags as tag} {#each tags as tag}
<Tag> <Tag disabled>
{tag} {tag}
</Tag> </Tag>
{/each} {/each}

View File

@ -13,7 +13,7 @@
let selectedRole = user?.roles?.[app?._id] let selectedRole = user?.roles?.[app?._id]
async function updateUserRoles() { async function updateUserRoles() {
const res = await users.updateRoles({ const res = await users.save({
...user, ...user,
roles: { roles: {
...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 = { const schema = {
email: {}, email: {},
status: { displayName: "Development Access", type: "boolean" }, developmentAccess: { displayName: "Development Access", type: "boolean" },
// role: { type: "options" }, // role: { type: "options" },
group: {}, group: {},
// access: {}, // access: {},
@ -32,7 +32,11 @@
let email let email
$: filteredUsers = $users $: filteredUsers = $users
.filter(user => user.email.includes(search || "")) .filter(user => user.email.includes(search || ""))
.map(user => ({ ...user, group: ["All"] })) .map(user => ({
...user,
group: ["All users"],
developmentAccess: user.builder.global,
}))
let createUserModal let createUserModal
let basicOnboardingModal let basicOnboardingModal

View File

@ -2,7 +2,7 @@ import { writable } from "svelte/store"
import api from "builderStore/api" import api from "builderStore/api"
export function createEmailStore() { export function createEmailStore() {
const store = writable([]) const store = writable({})
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,

View File

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

View File

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