Merge pull request #9467 from Budibase/feature/auth-screens-redesign
Selfhost onboarding UX/UI updates
This commit is contained in:
commit
7540cad45c
|
@ -4,37 +4,45 @@
|
|||
Heading,
|
||||
notifications,
|
||||
Layout,
|
||||
Input,
|
||||
Body,
|
||||
ActionButton,
|
||||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { API } from "api"
|
||||
import { admin, auth } from "stores/portal"
|
||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
import { onMount } from "svelte"
|
||||
import { FancyForm, FancyInput, ActionButton } from "@budibase/bbui"
|
||||
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||
import { passwordsMatch, handleError } from "../auth/_components/utils"
|
||||
|
||||
let adminUser = {}
|
||||
let error
|
||||
let modal
|
||||
let form
|
||||
let errors = {}
|
||||
let formData = {}
|
||||
let submitted = false
|
||||
|
||||
$: tenantId = $auth.tenantId
|
||||
$: multiTenancyEnabled = $admin.multiTenancy
|
||||
$: cloud = $admin.cloud
|
||||
$: imported = $admin.importComplete
|
||||
|
||||
async function save() {
|
||||
form.validate()
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return
|
||||
}
|
||||
submitted = true
|
||||
try {
|
||||
adminUser.tenantId = tenantId
|
||||
let adminUser = { ...formData, tenantId }
|
||||
delete adminUser.confirmationPassword
|
||||
// Save the admin user
|
||||
await API.createAdminUser(adminUser)
|
||||
notifications.success("Admin user created")
|
||||
await admin.init()
|
||||
$goto("../portal")
|
||||
} catch (error) {
|
||||
submitted = false
|
||||
notifications.error("Failed to create admin user")
|
||||
}
|
||||
}
|
||||
|
@ -53,35 +61,103 @@
|
|||
<Modal bind:this={modal} padding={false} width="600px">
|
||||
<ImportAppsModal />
|
||||
</Modal>
|
||||
<section>
|
||||
<div class="container">
|
||||
<Layout>
|
||||
|
||||
<TestimonialPage>
|
||||
<Layout gap="M" noPadding>
|
||||
<Layout justifyItems="center" noPadding>
|
||||
<img alt="logo" src={Logo} />
|
||||
<Layout gap="XS" justifyItems="center" noPadding>
|
||||
<Heading size="M">Create an admin user</Heading>
|
||||
<Body size="M" textAlign="center">
|
||||
The admin user has access to everything in Budibase.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Input label="Email" bind:value={adminUser.email} />
|
||||
<PasswordRepeatInput bind:password={adminUser.password} bind:error />
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Button cta disabled={error} on:click={save}>
|
||||
Create super admin user
|
||||
</Button>
|
||||
{#if multiTenancyEnabled}
|
||||
<ActionButton
|
||||
quiet
|
||||
on:click={() => {
|
||||
admin.unload()
|
||||
$goto("../auth/org")
|
||||
}}
|
||||
>
|
||||
Change organisation
|
||||
</ActionButton>
|
||||
{:else if !cloud && !imported}
|
||||
<Heading size="M">Create an admin user</Heading>
|
||||
<Body>The admin user has access to everything in Budibase.</Body>
|
||||
</Layout>
|
||||
<Layout gap="S" noPadding>
|
||||
<FancyForm bind:this={form}>
|
||||
<FancyInput
|
||||
label="Email"
|
||||
value={formData.email}
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
email: e.detail,
|
||||
}
|
||||
}}
|
||||
validate={() => {
|
||||
let fieldError = {
|
||||
email: !formData.email ? "Please enter a valid email" : undefined,
|
||||
}
|
||||
errors = handleError({ ...errors, ...fieldError })
|
||||
}}
|
||||
disabled={submitted}
|
||||
error={errors.email}
|
||||
/>
|
||||
<FancyInput
|
||||
label="Password"
|
||||
value={formData.password}
|
||||
type="password"
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
password: e.detail,
|
||||
}
|
||||
}}
|
||||
validate={() => {
|
||||
let fieldError = {}
|
||||
|
||||
fieldError["password"] = !formData.password
|
||||
? "Please enter a password"
|
||||
: undefined
|
||||
|
||||
fieldError["confirmationPassword"] =
|
||||
!passwordsMatch(
|
||||
formData.password,
|
||||
formData.confirmationPassword
|
||||
) && formData.confirmationPassword
|
||||
? "Passwords must match"
|
||||
: undefined
|
||||
|
||||
errors = handleError({ ...errors, ...fieldError })
|
||||
}}
|
||||
error={errors.password}
|
||||
disabled={submitted}
|
||||
/>
|
||||
<FancyInput
|
||||
label="Repeat Password"
|
||||
value={formData.confirmationPassword}
|
||||
type="password"
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
confirmationPassword: e.detail,
|
||||
}
|
||||
}}
|
||||
validate={() => {
|
||||
let fieldError = {
|
||||
confirmationPassword:
|
||||
!passwordsMatch(
|
||||
formData.password,
|
||||
formData.confirmationPassword
|
||||
) && formData.password
|
||||
? "Passwords must match"
|
||||
: undefined,
|
||||
}
|
||||
errors = handleError({ ...errors, ...fieldError })
|
||||
}}
|
||||
error={errors.confirmationPassword}
|
||||
disabled={submitted}
|
||||
/>
|
||||
</FancyForm>
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding justifyItems="center">
|
||||
<Button
|
||||
cta
|
||||
disabled={Object.keys(errors).length > 0 || submitted}
|
||||
on:click={save}
|
||||
>
|
||||
Create super admin user
|
||||
</Button>
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding justifyItems="center">
|
||||
<div class="user-actions">
|
||||
{#if !cloud && !imported}
|
||||
<ActionButton
|
||||
quiet
|
||||
on:click={() => {
|
||||
|
@ -91,28 +167,13 @@
|
|||
Import from cloud
|
||||
</ActionButton>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
</TestimonialPage>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
width: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
img {
|
||||
width: 48px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
import { FancyButton } from "@budibase/bbui"
|
||||
import GoogleLogo from "assets/google-logo.png"
|
||||
import { auth, organisation } from "stores/portal"
|
||||
|
||||
|
@ -10,31 +10,11 @@
|
|||
</script>
|
||||
|
||||
{#if show}
|
||||
<ActionButton
|
||||
<FancyButton
|
||||
icon={GoogleLogo}
|
||||
on:click={() =>
|
||||
window.open(`/api/global/auth/${tenantId}/google`, "_blank")}
|
||||
>
|
||||
<div class="inner">
|
||||
<img src={GoogleLogo} alt="google icon" />
|
||||
<p>Sign in with Google</p>
|
||||
</div>
|
||||
</ActionButton>
|
||||
Log in with Google
|
||||
</FancyButton>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
.inner img {
|
||||
width: 18px;
|
||||
margin: 3px 10px 3px 3px;
|
||||
}
|
||||
.inner p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { ActionButton, notifications } from "@budibase/bbui"
|
||||
import { notifications, FancyButton } from "@budibase/bbui"
|
||||
import OidcLogo from "assets/oidc-logo.png"
|
||||
import Auth0Logo from "assets/auth0-logo.png"
|
||||
import MicrosoftLogo from "assets/microsoft-logo.png"
|
||||
|
@ -33,34 +33,14 @@
|
|||
</script>
|
||||
|
||||
{#if show}
|
||||
<ActionButton
|
||||
<FancyButton
|
||||
icon={src}
|
||||
on:click={() =>
|
||||
window.open(
|
||||
`/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`,
|
||||
"_blank"
|
||||
)}
|
||||
>
|
||||
<div class="inner">
|
||||
<img {src} alt="oidc icon" />
|
||||
<p>{`Sign in with ${$oidc.name || "OIDC"}`}</p>
|
||||
</div>
|
||||
</ActionButton>
|
||||
{`Log in with ${$oidc.name || "OIDC"}`}
|
||||
</FancyButton>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
.inner img {
|
||||
width: 18px;
|
||||
margin: 3px 10px 3px 3px;
|
||||
}
|
||||
.inner p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
export const handleError = err => {
|
||||
let update = { ...err }
|
||||
return Object.keys(update).reduce((acc, key) => {
|
||||
if (update[key]) {
|
||||
acc[key] = update[key]
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const passwordsMatch = (password, confirmation) => {
|
||||
let confirm = confirmation?.trim()
|
||||
let pwd = password?.trim()
|
||||
return (
|
||||
typeof confirm === "string" && typeof pwd === "string" && confirm == pwd
|
||||
)
|
||||
}
|
|
@ -1,25 +1,35 @@
|
|||
<script>
|
||||
import {
|
||||
notifications,
|
||||
Input,
|
||||
Button,
|
||||
Layout,
|
||||
Body,
|
||||
Heading,
|
||||
ActionButton,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { organisation, auth } from "stores/portal"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
import { onMount } from "svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||
|
||||
let email = ""
|
||||
let form
|
||||
let error
|
||||
let submitted = false
|
||||
|
||||
async function forgot() {
|
||||
form.validate()
|
||||
if (error) {
|
||||
return
|
||||
}
|
||||
submitted = true
|
||||
try {
|
||||
await auth.forgotPassword(email)
|
||||
notifications.success("Email sent - please check your inbox")
|
||||
} catch (err) {
|
||||
submitted = false
|
||||
notifications.error("Unable to send reset password link")
|
||||
}
|
||||
}
|
||||
|
@ -33,45 +43,64 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="login">
|
||||
<div class="main">
|
||||
<Layout>
|
||||
<Layout noPadding justifyItems="center">
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
</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>
|
||||
<Layout gap="XS" nopadding>
|
||||
<Button cta on:click={forgot} disabled={!email}>
|
||||
Reset your password
|
||||
</Button>
|
||||
<ActionButton quiet on:click={() => $goto("../")}>Back</ActionButton>
|
||||
</Layout>
|
||||
<TestimonialPage>
|
||||
<Layout gap="S" noPadding>
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
<span class="heading-wrap">
|
||||
<Heading size="M">
|
||||
<div class="heading-content">
|
||||
<span class="back-chev" on:click={() => $goto("../")}>
|
||||
<Icon name="ChevronLeft" size="XL" />
|
||||
</span>
|
||||
Forgotten your password?
|
||||
</div>
|
||||
</Heading>
|
||||
</span>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Body size="M">
|
||||
No problem! Just enter your account's email address and we'll send you a
|
||||
link to reset it.
|
||||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Layout gap="S" noPadding>
|
||||
<FancyForm bind:this={form}>
|
||||
<FancyInput
|
||||
label="Email"
|
||||
value={email}
|
||||
on:change={e => {
|
||||
email = e.detail
|
||||
}}
|
||||
validate={() => {
|
||||
if (!email) {
|
||||
return "Please enter your email"
|
||||
}
|
||||
return null
|
||||
}}
|
||||
{error}
|
||||
disabled={submitted}
|
||||
/>
|
||||
</FancyForm>
|
||||
</Layout>
|
||||
<div>
|
||||
<Button disabled={!email || error || submitted} cta on:click={forgot}>
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</TestimonialPage>
|
||||
|
||||
<style>
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
}
|
||||
.back-chev {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-left: -5px;
|
||||
}
|
||||
.heading-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
Button,
|
||||
Divider,
|
||||
Heading,
|
||||
Input,
|
||||
Layout,
|
||||
notifications,
|
||||
Link,
|
||||
|
@ -14,22 +13,30 @@
|
|||
import { auth, organisation, oidc, admin } from "stores/portal"
|
||||
import GoogleButton from "./_components/GoogleButton.svelte"
|
||||
import OIDCButton from "./_components/OIDCButton.svelte"
|
||||
import { handleError } from "./_components/utils"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let username = ""
|
||||
let password = ""
|
||||
let loaded = false
|
||||
let form
|
||||
let errors = {}
|
||||
let formData = {}
|
||||
|
||||
$: company = $organisation.company || "Budibase"
|
||||
$: multiTenancyEnabled = $admin.multiTenancy
|
||||
$: cloud = $admin.cloud
|
||||
|
||||
async function login() {
|
||||
form.validate()
|
||||
if (Object.keys(errors).length > 0) {
|
||||
console.log("errors")
|
||||
return
|
||||
}
|
||||
try {
|
||||
await auth.login({
|
||||
username: username.trim(),
|
||||
password,
|
||||
username: formData?.username.trim(),
|
||||
password: formData?.password,
|
||||
})
|
||||
if ($auth?.user?.forceResetPassword) {
|
||||
$goto("./reset")
|
||||
|
@ -57,75 +64,96 @@
|
|||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
<div class="login">
|
||||
<div class="main">
|
||||
<Layout>
|
||||
<Layout noPadding justifyItems="center">
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
<Heading textAlign="center">Sign in to {company}</Heading>
|
||||
</Layout>
|
||||
|
||||
<TestimonialPage>
|
||||
<Layout gap="S" noPadding>
|
||||
<Layout justifyItems="center" noPadding>
|
||||
{#if loaded}
|
||||
<GoogleButton />
|
||||
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
||||
{/if}
|
||||
<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 disabled={!username && !password} on:click={login}
|
||||
>Sign in to {company}</Button
|
||||
>
|
||||
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
||||
Forgot password?
|
||||
</ActionButton>
|
||||
{#if multiTenancyEnabled && !cloud}
|
||||
<ActionButton
|
||||
quiet
|
||||
on:click={() => {
|
||||
admin.unload()
|
||||
$goto("./org")
|
||||
}}
|
||||
>
|
||||
Change organisation
|
||||
</ActionButton>
|
||||
{/if}
|
||||
</Layout>
|
||||
{#if cloud}
|
||||
<Body size="xs" textAlign="center">
|
||||
By using Budibase Cloud
|
||||
<br />
|
||||
you are agreeing to our
|
||||
<Link href="https://budibase.com/eula" target="_blank"
|
||||
>License Agreement</Link
|
||||
>
|
||||
</Body>
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
{/if}
|
||||
<Heading size="M">Log in to Budibase</Heading>
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
<Layout gap="S" noPadding>
|
||||
{#if loaded && ($organisation.google || $organisation.oidc)}
|
||||
<FancyForm>
|
||||
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
||||
<GoogleButton />
|
||||
</FancyForm>
|
||||
<Divider />
|
||||
{/if}
|
||||
<FancyForm bind:this={form}>
|
||||
<FancyInput
|
||||
label="Your work email"
|
||||
value={formData.username}
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
username: e.detail,
|
||||
}
|
||||
}}
|
||||
validate={() => {
|
||||
let fieldError = {
|
||||
username: !formData.username
|
||||
? "Please enter a valid email"
|
||||
: undefined,
|
||||
}
|
||||
errors = handleError({ ...errors, ...fieldError })
|
||||
}}
|
||||
error={errors.username}
|
||||
/>
|
||||
<FancyInput
|
||||
label="Password"
|
||||
value={formData.password}
|
||||
type="password"
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
password: e.detail,
|
||||
}
|
||||
}}
|
||||
validate={() => {
|
||||
let fieldError = {
|
||||
password: !formData.password
|
||||
? "Please enter your password"
|
||||
: undefined,
|
||||
}
|
||||
errors = handleError({ ...errors, ...fieldError })
|
||||
}}
|
||||
error={errors.password}
|
||||
/>
|
||||
</FancyForm>
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding justifyItems="center">
|
||||
<Button cta disabled={Object.keys(errors).length > 0} on:click={login}>
|
||||
Log in to {company}
|
||||
</Button>
|
||||
</Layout>
|
||||
<Layout gap="XS" noPadding justifyItems="center">
|
||||
<div class="user-actions">
|
||||
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
||||
Forgot password
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
{#if cloud}
|
||||
<Body size="xs" textAlign="center">
|
||||
By using Budibase Cloud
|
||||
<br />
|
||||
you are agreeing to our
|
||||
<Link href="https://budibase.com/eula" target="_blank" secondary={true}>
|
||||
License Agreement
|
||||
</Link>
|
||||
</Body>
|
||||
{/if}
|
||||
</Layout>
|
||||
</TestimonialPage>
|
||||
|
||||
<style>
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.user-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
}
|
||||
|
|
|
@ -1,31 +1,43 @@
|
|||
<script>
|
||||
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"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { handleError, passwordsMatch } from "./_components/utils"
|
||||
|
||||
const resetCode = $params["?code"]
|
||||
let password, error
|
||||
let form
|
||||
let formData = {}
|
||||
let errors = {}
|
||||
let loaded = false
|
||||
|
||||
$: submitted = false
|
||||
$: forceResetPassword = $auth?.user?.forceResetPassword
|
||||
|
||||
async function reset() {
|
||||
form.validate()
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return
|
||||
}
|
||||
submitted = true
|
||||
try {
|
||||
if (forceResetPassword) {
|
||||
await auth.updateSelf({
|
||||
password,
|
||||
password: formData.password,
|
||||
forceResetPassword: false,
|
||||
})
|
||||
$goto("../portal/")
|
||||
} else {
|
||||
await auth.resetPassword(password, resetCode)
|
||||
await auth.resetPassword(formData.password, resetCode)
|
||||
notifications.success("Password reset successfully")
|
||||
// send them to login if reset successful
|
||||
$goto("./login")
|
||||
}
|
||||
} catch (err) {
|
||||
submitted = false
|
||||
notifications.error("Unable to reset password")
|
||||
}
|
||||
}
|
||||
|
@ -37,47 +49,92 @@
|
|||
} catch (error) {
|
||||
notifications.error("Error getting org config")
|
||||
}
|
||||
loaded = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="login">
|
||||
<div class="main">
|
||||
<Layout>
|
||||
<Layout noPadding justifyItems="center">
|
||||
<img src={$organisation.logoUrl || Logo} 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>
|
||||
<TestimonialPage>
|
||||
<Layout gap="S" noPadding>
|
||||
{#if loaded}
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
{/if}
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="M">Reset your password</Heading>
|
||||
<Body size="M">Please enter the new password you'd like to use.</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Layout gap="S" noPadding>
|
||||
<FancyForm bind:this={form}>
|
||||
<FancyInput
|
||||
label="Password"
|
||||
value={formData.password}
|
||||
type="password"
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
password: e.detail,
|
||||
}
|
||||
}}
|
||||
validate={() => {
|
||||
let fieldError = {}
|
||||
|
||||
fieldError["password"] = !formData.password
|
||||
? "Please enter a password"
|
||||
: undefined
|
||||
|
||||
fieldError["confirmationPassword"] =
|
||||
!passwordsMatch(
|
||||
formData.password,
|
||||
formData.confirmationPassword
|
||||
) && formData.confirmationPassword
|
||||
? "Passwords must match"
|
||||
: undefined
|
||||
|
||||
errors = handleError({ ...errors, ...fieldError })
|
||||
}}
|
||||
error={errors.password}
|
||||
disabled={submitted}
|
||||
/>
|
||||
<FancyInput
|
||||
label="Repeat Password"
|
||||
value={formData.confirmationPassword}
|
||||
type="password"
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
confirmationPassword: e.detail,
|
||||
}
|
||||
}}
|
||||
validate={() => {
|
||||
const isValid =
|
||||
!passwordsMatch(
|
||||
formData.password,
|
||||
formData.confirmationPassword
|
||||
) && formData.password
|
||||
|
||||
let fieldError = {
|
||||
confirmationPassword: isValid ? "Passwords must match" : null,
|
||||
}
|
||||
|
||||
errors = handleError({ ...errors, ...fieldError })
|
||||
}}
|
||||
error={errors.confirmationPassword}
|
||||
disabled={submitted}
|
||||
/>
|
||||
</FancyForm>
|
||||
</Layout>
|
||||
<div>
|
||||
<Button
|
||||
disabled={Object.keys(errors).length > 0 ||
|
||||
(forceResetPassword ? false : !resetCode)}
|
||||
cta
|
||||
on:click={reset}>Reset your password</Button
|
||||
>
|
||||
</div>
|
||||
</Layout>
|
||||
</TestimonialPage>
|
||||
|
||||
<style>
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
}
|
||||
|
|
|
@ -1,70 +1,192 @@
|
|||
<script>
|
||||
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { users, organisation } from "stores/portal"
|
||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||
import { users, organisation, auth } from "stores/portal"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { handleError, passwordsMatch } from "../auth/_components/utils"
|
||||
|
||||
const inviteCode = $params["?code"]
|
||||
let password, error
|
||||
let form
|
||||
let formData = {}
|
||||
let onboarding = false
|
||||
let errors = {}
|
||||
|
||||
$: company = $organisation.company || "Budibase"
|
||||
|
||||
async function acceptInvite() {
|
||||
form.validate()
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return
|
||||
}
|
||||
onboarding = true
|
||||
try {
|
||||
await users.acceptInvite(inviteCode, password)
|
||||
const { password, firstName, lastName } = formData
|
||||
await users.acceptInvite(inviteCode, password, firstName, lastName)
|
||||
notifications.success("Invitation accepted successfully")
|
||||
$goto("../auth/login")
|
||||
await login()
|
||||
} catch (error) {
|
||||
notifications.error(error.message)
|
||||
onboarding = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getInvite() {
|
||||
try {
|
||||
const invite = await users.fetchInvite(inviteCode)
|
||||
if (invite?.email) {
|
||||
formData.email = invite?.email
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function login() {
|
||||
try {
|
||||
await auth.login({
|
||||
username: formData.email.trim(),
|
||||
password: formData.password.trim(),
|
||||
})
|
||||
notifications.success("Logged in successfully")
|
||||
$goto("../portal")
|
||||
} catch (err) {
|
||||
notifications.error(err.message ? err.message : "Invalid credentials") //not likely, considering.
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await organisation.init()
|
||||
await getInvite()
|
||||
} catch (error) {
|
||||
notifications.error("Error getting org config")
|
||||
notifications.error("Error getting invite config")
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<Layout>
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
<Layout gap="XS" justifyItems="center" noPadding>
|
||||
<Heading size="M">Invitation to {company}</Heading>
|
||||
<Body textAlign="center" size="M">
|
||||
Please enter a password to get started.
|
||||
</Body>
|
||||
</Layout>
|
||||
<PasswordRepeatInput bind:error bind:password />
|
||||
<Button disabled={error} cta on:click={acceptInvite}>
|
||||
Accept invite
|
||||
</Button>
|
||||
<TestimonialPage>
|
||||
<Layout gap="S" noPadding>
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="M">Join {company}</Heading>
|
||||
<Body size="M">Create your account to access your budibase apps!</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Layout gap="S" noPadding>
|
||||
<FancyForm bind:this={form}>
|
||||
<FancyInput
|
||||
label="Email"
|
||||
value={formData.email}
|
||||
disabled={true}
|
||||
error={errors.email}
|
||||
/>
|
||||
<FancyInput
|
||||
label="First name"
|
||||
value={formData.firstName}
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
firstName: e.detail,
|
||||
}
|
||||
}}
|
||||
validate={() => {
|
||||
let fieldError = {
|
||||
firstName: !formData.firstName
|
||||
? "Please enter your first name"
|
||||
: undefined,
|
||||
}
|
||||
|
||||
errors = handleError({ ...errors, ...fieldError })
|
||||
}}
|
||||
error={errors.firstName}
|
||||
disabled={onboarding}
|
||||
/>
|
||||
<FancyInput
|
||||
label="Last name (optional)"
|
||||
value={formData.lastName}
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
lastName: e.detail,
|
||||
}
|
||||
}}
|
||||
disabled={onboarding}
|
||||
/>
|
||||
<FancyInput
|
||||
label="Password"
|
||||
value={formData.password}
|
||||
type="password"
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
password: e.detail,
|
||||
}
|
||||
}}
|
||||
validate={() => {
|
||||
let fieldError = {}
|
||||
|
||||
fieldError["password"] = !formData.password
|
||||
? "Please enter a password"
|
||||
: undefined
|
||||
|
||||
fieldError["confirmationPassword"] =
|
||||
!passwordsMatch(
|
||||
formData.password,
|
||||
formData.confirmationPassword
|
||||
) && formData.confirmationPassword
|
||||
? "Passwords must match"
|
||||
: undefined
|
||||
|
||||
errors = handleError({ ...errors, ...fieldError })
|
||||
}}
|
||||
error={errors.password}
|
||||
disabled={onboarding}
|
||||
/>
|
||||
<FancyInput
|
||||
label="Repeat password"
|
||||
value={formData.confirmationPassword}
|
||||
type="password"
|
||||
on:change={e => {
|
||||
formData = {
|
||||
...formData,
|
||||
confirmationPassword: e.detail,
|
||||
}
|
||||
}}
|
||||
validate={() => {
|
||||
let fieldError = {
|
||||
confirmationPassword:
|
||||
!passwordsMatch(
|
||||
formData.password,
|
||||
formData.confirmationPassword
|
||||
) && formData.password
|
||||
? "Passwords must match"
|
||||
: undefined,
|
||||
}
|
||||
|
||||
errors = handleError({ ...errors, ...fieldError })
|
||||
}}
|
||||
error={errors.confirmationPassword}
|
||||
disabled={onboarding}
|
||||
/>
|
||||
</FancyForm>
|
||||
</Layout>
|
||||
<div>
|
||||
<Button
|
||||
disabled={Object.keys(errors).length > 0 || onboarding}
|
||||
cta
|
||||
on:click={acceptInvite}
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</TestimonialPage>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
img {
|
||||
width: 40px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,13 +29,19 @@ export function createUsersStore() {
|
|||
async function invite(payload) {
|
||||
return API.inviteUsers(payload)
|
||||
}
|
||||
async function acceptInvite(inviteCode, password) {
|
||||
async function acceptInvite(inviteCode, password, firstName, lastName) {
|
||||
return API.acceptInvite({
|
||||
inviteCode,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchInvite(inviteCode) {
|
||||
return API.getUserInvite(inviteCode)
|
||||
}
|
||||
|
||||
async function create(data) {
|
||||
let mappedUsers = data.users.map(user => {
|
||||
const body = {
|
||||
|
@ -101,6 +107,7 @@ export function createUsersStore() {
|
|||
fetch,
|
||||
invite,
|
||||
acceptInvite,
|
||||
fetchInvite,
|
||||
create,
|
||||
save,
|
||||
bulkDelete,
|
||||
|
|
|
@ -146,6 +146,16 @@ export const buildUserEndpoints = API => ({
|
|||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the invitation associated with a provided code.
|
||||
* @param code The unique code for the target invite
|
||||
*/
|
||||
getUserInvite: async code => {
|
||||
return await API.get({
|
||||
url: `/api/global/users/invite/${code}`,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Invites multiple users to the current tenant.
|
||||
* @param users An array of users to invite
|
||||
|
@ -168,13 +178,17 @@ export const buildUserEndpoints = API => ({
|
|||
* Accepts an invite to join the platform and creates a user.
|
||||
* @param inviteCode the invite code sent in the email
|
||||
* @param password the password for the newly created user
|
||||
* @param firstName the first name of the new user
|
||||
* @param lastName the last name of the new user
|
||||
*/
|
||||
acceptInvite: async ({ inviteCode, password }) => {
|
||||
acceptInvite: async ({ inviteCode, password, firstName, lastName }) => {
|
||||
return await API.post({
|
||||
url: "/api/global/users/invite/accept",
|
||||
body: {
|
||||
inviteCode,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
|
|
@ -86,7 +86,7 @@ export async function importApps(ctx: Ctx) {
|
|||
if (Array.isArray(file)) {
|
||||
ctx.throw(400, "Single file is required")
|
||||
}
|
||||
if (file.type !== "application/gzip") {
|
||||
if (file.type !== "application/gzip" && file.type !== "application/x-gzip") {
|
||||
ctx.throw(400, "Import file must be a gzipped tarball.")
|
||||
}
|
||||
|
||||
|
|
|
@ -210,6 +210,19 @@ export const inviteMultiple = async (ctx: any) => {
|
|||
ctx.body = await sdk.users.invite(request)
|
||||
}
|
||||
|
||||
export const checkInvite = async (ctx: any) => {
|
||||
const { code } = ctx.params
|
||||
let invite
|
||||
try {
|
||||
invite = await checkInviteCode(code, false)
|
||||
} catch (e) {
|
||||
ctx.throw(400, "There was a problem with the invite")
|
||||
}
|
||||
ctx.body = {
|
||||
email: invite.email,
|
||||
}
|
||||
}
|
||||
|
||||
export const inviteAccept = async (ctx: any) => {
|
||||
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
||||
try {
|
||||
|
|
|
@ -62,6 +62,10 @@ const PUBLIC_ENDPOINTS = [
|
|||
route: "/api/system/restored",
|
||||
method: "POST",
|
||||
},
|
||||
{
|
||||
route: "/api/global/users/invite",
|
||||
method: "GET",
|
||||
},
|
||||
]
|
||||
|
||||
const NO_TENANCY_ENDPOINTS = [
|
||||
|
|
|
@ -38,6 +38,13 @@ function buildInviteMultipleValidation() {
|
|||
))
|
||||
}
|
||||
|
||||
function buildInviteLookupValidation() {
|
||||
// prettier-ignore
|
||||
return auth.joiValidator.params(Joi.object({
|
||||
code: Joi.string().required()
|
||||
}).unknown(true))
|
||||
}
|
||||
|
||||
const createUserAdminOnly = (ctx: any, next: any) => {
|
||||
if (!ctx.request.body._id) {
|
||||
return auth.adminOnly(ctx, next)
|
||||
|
@ -51,6 +58,8 @@ function buildInviteAcceptValidation() {
|
|||
return auth.joiValidator.body(Joi.object({
|
||||
inviteCode: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
firstName: Joi.string().required(),
|
||||
lastName: Joi.string().optional(),
|
||||
}).required().unknown(true))
|
||||
}
|
||||
|
||||
|
@ -91,6 +100,11 @@ router
|
|||
)
|
||||
|
||||
// non-global endpoints
|
||||
.get(
|
||||
"/api/global/users/invite/:code",
|
||||
buildInviteLookupValidation(),
|
||||
controller.checkInvite
|
||||
)
|
||||
.post(
|
||||
"/api/global/users/invite/accept",
|
||||
buildInviteAcceptValidation(),
|
||||
|
|
|
@ -48,6 +48,7 @@ export class UserAPI extends TestAPI {
|
|||
.send({
|
||||
password: "newpassword",
|
||||
inviteCode: code,
|
||||
firstName: "Ted",
|
||||
})
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
|
Loading…
Reference in New Issue