UI Onboarding UI/UX auth refactoring
This commit is contained in:
parent
ac520e3a1e
commit
d37c0e4b5d
|
@ -4,37 +4,45 @@
|
||||||
Heading,
|
Heading,
|
||||||
notifications,
|
notifications,
|
||||||
Layout,
|
Layout,
|
||||||
Input,
|
|
||||||
Body,
|
Body,
|
||||||
ActionButton,
|
|
||||||
Modal,
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { admin, auth } from "stores/portal"
|
import { admin, auth } from "stores/portal"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
|
||||||
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
|
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { FancyForm, FancyInput, ActionButton } from "@budibase/bbui"
|
||||||
|
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||||
|
import { handleError, passwordsMatch } from "../auth/_components/utils"
|
||||||
|
|
||||||
let adminUser = {}
|
|
||||||
let error
|
|
||||||
let modal
|
let modal
|
||||||
|
let form
|
||||||
|
let errors = {}
|
||||||
|
let formData = {}
|
||||||
|
let submitted = false
|
||||||
|
|
||||||
$: tenantId = $auth.tenantId
|
$: tenantId = $auth.tenantId
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
|
||||||
$: cloud = $admin.cloud
|
$: cloud = $admin.cloud
|
||||||
$: imported = $admin.importComplete
|
$: imported = $admin.importComplete
|
||||||
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
form.validate()
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitted = true
|
||||||
try {
|
try {
|
||||||
adminUser.tenantId = tenantId
|
const adminUser = { ...formData, tenantId }
|
||||||
// Save the admin user
|
// Save the admin user
|
||||||
await API.createAdminUser(adminUser)
|
await API.createAdminUser(adminUser)
|
||||||
notifications.success("Admin user created")
|
notifications.success("Admin user created")
|
||||||
await admin.init()
|
await admin.init()
|
||||||
$goto("../portal")
|
$goto("../portal")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
submitted = false
|
||||||
notifications.error("Failed to create admin user")
|
notifications.error("Failed to create admin user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,35 +61,109 @@
|
||||||
<Modal bind:this={modal} padding={false} width="600px">
|
<Modal bind:this={modal} padding={false} width="600px">
|
||||||
<ImportAppsModal />
|
<ImportAppsModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
<section>
|
|
||||||
<div class="container">
|
<TestimonialPage>
|
||||||
<Layout>
|
<Layout gap="M" noPadding>
|
||||||
|
<Layout justifyItems="center" noPadding>
|
||||||
<img alt="logo" src={Logo} />
|
<img alt="logo" src={Logo} />
|
||||||
<Layout gap="XS" justifyItems="center" noPadding>
|
<Heading size="M">Create an admin user</Heading>
|
||||||
<Heading size="M">Create an admin user</Heading>
|
<Body>The admin user has access to everything in Budibase.</Body>
|
||||||
<Body size="M" textAlign="center">
|
</Layout>
|
||||||
The admin user has access to everything in Budibase.
|
<Layout gap="S" noPadding>
|
||||||
</Body>
|
<FancyForm bind:this={form}>
|
||||||
</Layout>
|
<FancyInput
|
||||||
<Layout gap="XS" noPadding>
|
label="Email"
|
||||||
<Input label="Email" bind:value={adminUser.email} />
|
value={formData.email}
|
||||||
<PasswordRepeatInput bind:password={adminUser.password} bind:error />
|
on:change={e => {
|
||||||
</Layout>
|
formData = {
|
||||||
<Layout gap="XS" noPadding>
|
...formData,
|
||||||
<Button cta disabled={error} on:click={save}>
|
email: e.detail,
|
||||||
Create super admin user
|
}
|
||||||
</Button>
|
}}
|
||||||
{#if multiTenancyEnabled}
|
validate={() => {
|
||||||
<ActionButton
|
handleError(() => {
|
||||||
quiet
|
return {
|
||||||
on:click={() => {
|
email: !formData.email
|
||||||
admin.unload()
|
? "Please enter a valid email"
|
||||||
$goto("../auth/org")
|
: undefined,
|
||||||
}}
|
}
|
||||||
>
|
}, errors)
|
||||||
Change organisation
|
}}
|
||||||
</ActionButton>
|
disabled={submitted}
|
||||||
{:else if !cloud && !imported}
|
error={errors.email}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Password"
|
||||||
|
value={formData.password}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
password: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
handleError(() => {
|
||||||
|
let err = {}
|
||||||
|
|
||||||
|
err["password"] = !formData.password
|
||||||
|
? "Please enter a password"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
err["confirmationPassword"] =
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.confirmationPassword
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return err
|
||||||
|
}, errors)
|
||||||
|
}}
|
||||||
|
error={errors.password}
|
||||||
|
disabled={submitted}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Repeat Password"
|
||||||
|
value={formData.confirmationPassword}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
confirmationPassword: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
handleError(() => {
|
||||||
|
return {
|
||||||
|
confirmationPassword:
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.password
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}, errors)
|
||||||
|
}}
|
||||||
|
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 && !multiTenancyEnabled}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
quiet
|
quiet
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -91,28 +173,13 @@
|
||||||
Import from cloud
|
Import from cloud
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</Layout>
|
||||||
</section>
|
</TestimonialPage>
|
||||||
|
|
||||||
<style>
|
<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 {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { FancyButton } from "@budibase/bbui"
|
||||||
import GoogleLogo from "assets/google-logo.png"
|
import GoogleLogo from "assets/google-logo.png"
|
||||||
import { auth, organisation } from "stores/portal"
|
import { auth, organisation } from "stores/portal"
|
||||||
|
|
||||||
|
@ -10,31 +10,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<ActionButton
|
<FancyButton
|
||||||
|
icon={GoogleLogo}
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
window.open(`/api/global/auth/${tenantId}/google`, "_blank")}
|
window.open(`/api/global/auth/${tenantId}/google`, "_blank")}
|
||||||
>
|
>
|
||||||
<div class="inner">
|
Log in with Google
|
||||||
<img src={GoogleLogo} alt="google icon" />
|
</FancyButton>
|
||||||
<p>Sign in with Google</p>
|
|
||||||
</div>
|
|
||||||
</ActionButton>
|
|
||||||
{/if}
|
{/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>
|
<script>
|
||||||
import { ActionButton, notifications } from "@budibase/bbui"
|
import { notifications, FancyButton } from "@budibase/bbui"
|
||||||
import OidcLogo from "assets/oidc-logo.png"
|
import OidcLogo from "assets/oidc-logo.png"
|
||||||
import Auth0Logo from "assets/auth0-logo.png"
|
import Auth0Logo from "assets/auth0-logo.png"
|
||||||
import MicrosoftLogo from "assets/microsoft-logo.png"
|
import MicrosoftLogo from "assets/microsoft-logo.png"
|
||||||
|
@ -33,34 +33,14 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<ActionButton
|
<FancyButton
|
||||||
|
icon={src}
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
window.open(
|
window.open(
|
||||||
`/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`,
|
`/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`,
|
||||||
"_blank"
|
"_blank"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="inner">
|
{`Log in with ${$oidc.name || "OIDC"}`}
|
||||||
<img {src} alt="oidc icon" />
|
</FancyButton>
|
||||||
<p>{`Sign in with ${$oidc.name || "OIDC"}`}</p>
|
|
||||||
</div>
|
|
||||||
</ActionButton>
|
|
||||||
{/if}
|
{/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,18 @@
|
||||||
|
exports.handleError = (validate, errors) => {
|
||||||
|
const err = validate()
|
||||||
|
let update = { ...errors, ...err }
|
||||||
|
errors = Object.keys(update).reduce((acc, key) => {
|
||||||
|
if (update[key]) {
|
||||||
|
acc[key] = update[key]
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.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>
|
<script>
|
||||||
import {
|
import {
|
||||||
notifications,
|
notifications,
|
||||||
Input,
|
|
||||||
Button,
|
Button,
|
||||||
Layout,
|
Layout,
|
||||||
Body,
|
Body,
|
||||||
Heading,
|
Heading,
|
||||||
ActionButton,
|
Icon,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { organisation, auth } from "stores/portal"
|
import { organisation, auth } from "stores/portal"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||||
|
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||||
|
|
||||||
let email = ""
|
let email = ""
|
||||||
|
let form
|
||||||
|
let error
|
||||||
|
let submitted = false
|
||||||
|
|
||||||
async function forgot() {
|
async function forgot() {
|
||||||
|
form.validate()
|
||||||
|
if (error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitted = true
|
||||||
try {
|
try {
|
||||||
await auth.forgotPassword(email)
|
await auth.forgotPassword(email)
|
||||||
notifications.success("Email sent - please check your inbox")
|
notifications.success("Email sent - please check your inbox")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
submitted = false
|
||||||
notifications.error("Unable to send reset password link")
|
notifications.error("Unable to send reset password link")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,45 +43,64 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="login">
|
<TestimonialPage>
|
||||||
<div class="main">
|
<Layout gap="M" noPadding>
|
||||||
<Layout>
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<Layout noPadding justifyItems="center">
|
<span class="heading-wrap">
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<Heading size="M">
|
||||||
</Layout>
|
<div class="heading-content">
|
||||||
<Layout gap="XS" noPadding>
|
<span class="back-chev" on:click={() => $goto("../")}>
|
||||||
<Heading textAlign="center">Forgotten your password?</Heading>
|
<Icon name="ChevronLeft" size="XL" />
|
||||||
<Body size="S" textAlign="center">
|
</span>
|
||||||
No problem! Just enter your account's email address and we'll send you
|
Forgotten your password?
|
||||||
a link to reset it.
|
</div>
|
||||||
</Body>
|
</Heading>
|
||||||
<Input label="Email" bind:value={email} />
|
</span>
|
||||||
</Layout>
|
<Layout gap="XS" noPadding>
|
||||||
<Layout gap="XS" nopadding>
|
<Body size="M">
|
||||||
<Button cta on:click={forgot} disabled={!email}>
|
No problem! Just enter your account's email address and we'll send you a
|
||||||
Reset your password
|
link to reset it.
|
||||||
</Button>
|
</Body>
|
||||||
<ActionButton quiet on:click={() => $goto("../")}>Back</ActionButton>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
</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>
|
<style>
|
||||||
.login {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
}
|
}
|
||||||
|
.back-chev {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
.heading-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
Button,
|
Button,
|
||||||
Divider,
|
Divider,
|
||||||
Heading,
|
Heading,
|
||||||
Input,
|
|
||||||
Layout,
|
Layout,
|
||||||
notifications,
|
notifications,
|
||||||
Link,
|
Link,
|
||||||
|
@ -14,22 +13,30 @@
|
||||||
import { auth, organisation, oidc, admin } from "stores/portal"
|
import { auth, organisation, oidc, admin } from "stores/portal"
|
||||||
import GoogleButton from "./_components/GoogleButton.svelte"
|
import GoogleButton from "./_components/GoogleButton.svelte"
|
||||||
import OIDCButton from "./_components/OIDCButton.svelte"
|
import OIDCButton from "./_components/OIDCButton.svelte"
|
||||||
|
import { handleError } from "./_components/utils"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
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 { onMount } from "svelte"
|
||||||
|
|
||||||
let username = ""
|
|
||||||
let password = ""
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let form
|
||||||
|
let errors = {}
|
||||||
|
let formData = {}
|
||||||
|
|
||||||
$: company = $organisation.company || "Budibase"
|
$: company = $organisation.company || "Budibase"
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
|
||||||
$: cloud = $admin.cloud
|
$: cloud = $admin.cloud
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
|
form.validate()
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
console.log("errors")
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await auth.login({
|
await auth.login({
|
||||||
username: username.trim(),
|
username: formData?.username.trim(),
|
||||||
password,
|
password: formData?.password,
|
||||||
})
|
})
|
||||||
if ($auth?.user?.forceResetPassword) {
|
if ($auth?.user?.forceResetPassword) {
|
||||||
$goto("./reset")
|
$goto("./reset")
|
||||||
|
@ -57,75 +64,98 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeydown} />
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
<div class="login">
|
|
||||||
<div class="main">
|
<TestimonialPage>
|
||||||
<Layout>
|
<Layout gap="M" noPadding>
|
||||||
<Layout noPadding justifyItems="center">
|
<Layout justifyItems="center" noPadding>
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
|
||||||
<Heading textAlign="center">Sign in to {company}</Heading>
|
|
||||||
</Layout>
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<GoogleButton />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<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>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<Heading size="M">Log in to Budibase</Heading>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
<Layout gap="S" noPadding>
|
||||||
</div>
|
{#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={() => {
|
||||||
|
handleError(() => {
|
||||||
|
return {
|
||||||
|
username: !formData.username
|
||||||
|
? "Please enter a valid email"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}, errors)
|
||||||
|
}}
|
||||||
|
error={errors.username}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Password"
|
||||||
|
value={formData.password}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
password: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
handleError(() => {
|
||||||
|
return {
|
||||||
|
password: !formData.password
|
||||||
|
? "Please enter your password"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}, errors)
|
||||||
|
}}
|
||||||
|
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>
|
<style>
|
||||||
.login {
|
.user-actions {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,44 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body, Button, Heading, Layout, notifications } from "@budibase/bbui"
|
import { Body, Button, Heading, Layout, notifications } from "@budibase/bbui"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
|
||||||
import { auth, organisation } from "stores/portal"
|
import { auth, organisation } from "stores/portal"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
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 { onMount } from "svelte"
|
||||||
|
import { handleError, passwordsMatch } from "./_components/utils"
|
||||||
|
|
||||||
const resetCode = $params["?code"]
|
const resetCode = $params["?code"]
|
||||||
let password, error
|
let form
|
||||||
|
|
||||||
|
let formData = {}
|
||||||
|
let errors = {}
|
||||||
|
$: console.log(errors)
|
||||||
|
|
||||||
|
$: submitted = false
|
||||||
$: forceResetPassword = $auth?.user?.forceResetPassword
|
$: forceResetPassword = $auth?.user?.forceResetPassword
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
|
form.validate()
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitted = true
|
||||||
try {
|
try {
|
||||||
if (forceResetPassword) {
|
if (forceResetPassword) {
|
||||||
await auth.updateSelf({
|
await auth.updateSelf({
|
||||||
password,
|
password: formData.password,
|
||||||
forceResetPassword: false,
|
forceResetPassword: false,
|
||||||
})
|
})
|
||||||
$goto("../portal/")
|
$goto("../portal/")
|
||||||
} else {
|
} else {
|
||||||
await auth.resetPassword(password, resetCode)
|
await auth.resetPassword(formData.password, resetCode)
|
||||||
notifications.success("Password reset successfully")
|
notifications.success("Password reset successfully")
|
||||||
// send them to login if reset successful
|
// send them to login if reset successful
|
||||||
$goto("./login")
|
$goto("./login")
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
submitted = false
|
||||||
notifications.error("Unable to reset password")
|
notifications.error("Unable to reset password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,44 +53,88 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="login">
|
<TestimonialPage>
|
||||||
<div class="main">
|
<Layout gap="M" noPadding>
|
||||||
<Layout>
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<Layout noPadding justifyItems="center">
|
<Layout gap="XS" noPadding>
|
||||||
<img src={$organisation.logoUrl || Logo} alt="Organisation logo" />
|
<Heading size="M">Reset your password</Heading>
|
||||||
</Layout>
|
<Body size="M">Please enter the new password you'd like to use.</Body>
|
||||||
<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>
|
</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={() => {
|
||||||
|
handleError(() => {
|
||||||
|
let err = {}
|
||||||
|
|
||||||
|
err["password"] = !formData.password
|
||||||
|
? "Please enter a password"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
err["confirmationPassword"] =
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.confirmationPassword
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return err
|
||||||
|
}, errors)
|
||||||
|
}}
|
||||||
|
error={errors.password}
|
||||||
|
disabled={submitted}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Repeat Password"
|
||||||
|
value={formData.confirmationPassword}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
confirmationPassword: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
handleError(() => {
|
||||||
|
return {
|
||||||
|
confirmationPassword:
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.password
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}, errors)
|
||||||
|
}}
|
||||||
|
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>
|
<style>
|
||||||
.login {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +1,194 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
|
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { users, organisation } from "stores/portal"
|
import { users, organisation, auth } from "stores/portal"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
|
||||||
import Logo from "assets/bb-emblem.svg"
|
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 { onMount } from "svelte"
|
||||||
|
import { handleError, passwordsMatch } from "../auth/_components/utils"
|
||||||
|
|
||||||
const inviteCode = $params["?code"]
|
const inviteCode = $params["?code"]
|
||||||
let password, error
|
let form
|
||||||
|
let formData = {}
|
||||||
|
let onboarding = false
|
||||||
|
let errors = {}
|
||||||
|
|
||||||
$: company = $organisation.company || "Budibase"
|
$: company = $organisation.company || "Budibase"
|
||||||
|
|
||||||
async function acceptInvite() {
|
async function acceptInvite() {
|
||||||
|
form.validate()
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onboarding = true
|
||||||
try {
|
try {
|
||||||
await users.acceptInvite(inviteCode, password)
|
const { password, firstName, lastName } = formData
|
||||||
|
await users.acceptInvite(inviteCode, password, firstName, lastName)
|
||||||
notifications.success("Invitation accepted successfully")
|
notifications.success("Invitation accepted successfully")
|
||||||
$goto("../auth/login")
|
await login()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(error.message)
|
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 () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
|
await getInvite()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting org config")
|
notifications.error("Error getting invite config")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<TestimonialPage>
|
||||||
<div class="container">
|
<Layout gap="M" noPadding>
|
||||||
<Layout>
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<Layout gap="XS" noPadding>
|
||||||
<Layout gap="XS" justifyItems="center" noPadding>
|
<Heading size="M">Join {company}</Heading>
|
||||||
<Heading size="M">Invitation to {company}</Heading>
|
<Body size="M">Create your account to access your budibase apps!</Body>
|
||||||
<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>
|
|
||||||
</Layout>
|
</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={() => {
|
||||||
|
handleError(() => {
|
||||||
|
return {
|
||||||
|
firstName: !formData.firstName
|
||||||
|
? "Please enter your first name"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}, errors)
|
||||||
|
}}
|
||||||
|
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={() => {
|
||||||
|
handleError(() => {
|
||||||
|
let err = {}
|
||||||
|
|
||||||
|
err["password"] = !formData.password
|
||||||
|
? "Please enter a password"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
err["confirmationPassword"] =
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.confirmationPassword
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return err
|
||||||
|
}, errors)
|
||||||
|
}}
|
||||||
|
error={errors.password}
|
||||||
|
disabled={onboarding}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Repeat Password"
|
||||||
|
value={formData.confirmationPassword}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
confirmationPassword: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
handleError(() => {
|
||||||
|
return {
|
||||||
|
confirmationPassword:
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.password
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}, errors)
|
||||||
|
}}
|
||||||
|
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>
|
<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 {
|
img {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -29,13 +29,19 @@ export function createUsersStore() {
|
||||||
async function invite(payload) {
|
async function invite(payload) {
|
||||||
return API.inviteUsers(payload)
|
return API.inviteUsers(payload)
|
||||||
}
|
}
|
||||||
async function acceptInvite(inviteCode, password) {
|
async function acceptInvite(inviteCode, password, firstName, lastName) {
|
||||||
return API.acceptInvite({
|
return API.acceptInvite({
|
||||||
inviteCode,
|
inviteCode,
|
||||||
password,
|
password,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchInvite(inviteCode) {
|
||||||
|
return API.getUserInvite(inviteCode)
|
||||||
|
}
|
||||||
|
|
||||||
async function create(data) {
|
async function create(data) {
|
||||||
let mappedUsers = data.users.map(user => {
|
let mappedUsers = data.users.map(user => {
|
||||||
const body = {
|
const body = {
|
||||||
|
@ -101,6 +107,7 @@ export function createUsersStore() {
|
||||||
fetch,
|
fetch,
|
||||||
invite,
|
invite,
|
||||||
acceptInvite,
|
acceptInvite,
|
||||||
|
fetchInvite,
|
||||||
create,
|
create,
|
||||||
save,
|
save,
|
||||||
bulkDelete,
|
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.
|
* Invites multiple users to the current tenant.
|
||||||
* @param users An array of users to invite
|
* @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.
|
* Accepts an invite to join the platform and creates a user.
|
||||||
* @param inviteCode the invite code sent in the email
|
* @param inviteCode the invite code sent in the email
|
||||||
* @param password the password for the newly created user
|
* @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({
|
return await API.post({
|
||||||
url: "/api/global/users/invite/accept",
|
url: "/api/global/users/invite/accept",
|
||||||
body: {
|
body: {
|
||||||
inviteCode,
|
inviteCode,
|
||||||
password,
|
password,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -210,6 +210,19 @@ export const inviteMultiple = async (ctx: any) => {
|
||||||
ctx.body = await sdk.users.invite(request)
|
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) => {
|
export const inviteAccept = async (ctx: any) => {
|
||||||
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -62,6 +62,10 @@ const PUBLIC_ENDPOINTS = [
|
||||||
route: "/api/system/restored",
|
route: "/api/system/restored",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: "/api/global/users/invite",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const NO_TENANCY_ENDPOINTS = [
|
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) => {
|
const createUserAdminOnly = (ctx: any, next: any) => {
|
||||||
if (!ctx.request.body._id) {
|
if (!ctx.request.body._id) {
|
||||||
return auth.adminOnly(ctx, next)
|
return auth.adminOnly(ctx, next)
|
||||||
|
@ -51,6 +58,8 @@ function buildInviteAcceptValidation() {
|
||||||
return auth.joiValidator.body(Joi.object({
|
return auth.joiValidator.body(Joi.object({
|
||||||
inviteCode: Joi.string().required(),
|
inviteCode: Joi.string().required(),
|
||||||
password: Joi.string().required(),
|
password: Joi.string().required(),
|
||||||
|
firstName: Joi.string().required(),
|
||||||
|
lastName: Joi.string().optional(),
|
||||||
}).required().unknown(true))
|
}).required().unknown(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +100,11 @@ router
|
||||||
)
|
)
|
||||||
|
|
||||||
// non-global endpoints
|
// non-global endpoints
|
||||||
|
.get(
|
||||||
|
"/api/global/users/invite/:code",
|
||||||
|
buildInviteLookupValidation(),
|
||||||
|
controller.checkInvite
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/global/users/invite/accept",
|
"/api/global/users/invite/accept",
|
||||||
buildInviteAcceptValidation(),
|
buildInviteAcceptValidation(),
|
||||||
|
|
Loading…
Reference in New Issue