Merge pull request #1513 from Budibase/fixes/user-management
Fixes/user management
This commit is contained in:
commit
e8cef05195
|
@ -41,4 +41,7 @@
|
|||
align-items: center;
|
||||
grid-template-columns: 200px 20px;
|
||||
}
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1 +0,0 @@
|
|||
export { LoginForm } from "./LoginForm.svelte"
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +1,89 @@
|
|||
<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>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,20 +41,45 @@
|
|||
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>
|
||||
</div>
|
||||
<header>
|
||||
<Heading>
|
||||
Email Template: {template}
|
||||
</Heading>
|
||||
<Button cta on:click={saveTemplate}>Save</Button>
|
||||
</header>
|
||||
<Tabs {selected}>
|
||||
<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>
|
||||
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">
|
||||
<div class="template-editor">
|
||||
<Editor
|
||||
|
@ -67,32 +89,34 @@
|
|||
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}>
|
||||
<Tab title="Template">
|
||||
<TemplateBindings
|
||||
title="Template Bindings"
|
||||
bindings={templateBindings}
|
||||
onBindingClick={setTemplateBinding}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Common">
|
||||
<TemplateBindings
|
||||
title="Common Bindings"
|
||||
bindings={$email.definitions.bindings.common}
|
||||
onBindingClick={setTemplateBinding}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
{#if mounted}
|
||||
<Tabs selected="Template">
|
||||
<Tab title="Template">
|
||||
<TemplateBindings
|
||||
title="Template Bindings"
|
||||
bindings={templateBindings}
|
||||
onBindingClick={setTemplateBinding}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Common">
|
||||
<TemplateBindings
|
||||
title="Common Bindings"
|
||||
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>
|
||||
|
|
|
@ -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 = () => {}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<script>
|
||||
import { email } from "stores/portal"
|
||||
email.templates.fetch()
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -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,78 +70,77 @@
|
|||
} else {
|
||||
smtpConfig = smtpDoc
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
loading = true
|
||||
await fetchSmtp()
|
||||
await email.templates.fetch()
|
||||
loading = false
|
||||
})
|
||||
fetchSmtp()
|
||||
</script>
|
||||
|
||||
<Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="M">Email</Heading>
|
||||
<Body>
|
||||
Sending email is not required, but highly recommended for processes such
|
||||
as password recovery. To setup automated auth emails, simply add the
|
||||
values below and click activate.
|
||||
</Body>
|
||||
</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.
|
||||
<Page>
|
||||
<Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="M">Email</Heading>
|
||||
<Body>
|
||||
Sending email is not required, but highly recommended for processes such
|
||||
as password recovery. To setup automated auth emails, simply add the
|
||||
values below and click activate.
|
||||
</Body>
|
||||
</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 />
|
||||
<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}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
allowEditColumns={false}
|
||||
/>
|
||||
{/if}
|
||||
</Layout>
|
||||
{#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>
|
||||
</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 />
|
||||
<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>
|
||||
.form-row {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<Tags>
|
||||
{#each tags as tag}
|
||||
<Tag>
|
||||
<Tag disabled>
|
||||
{tag}
|
||||
</Tag>
|
||||
{/each}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<script>
|
||||
import { Page } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<Page>
|
||||
<slot />
|
||||
</Page>
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Reference in New Issue