Merge pull request #9467 from Budibase/feature/auth-screens-redesign

Selfhost onboarding UX/UI updates
This commit is contained in:
deanhannigan 2023-01-31 16:13:46 +00:00 committed by GitHub
commit 7540cad45c
15 changed files with 613 additions and 286 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
)
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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,

View File

@ -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,
},
})
},

View File

@ -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.")
}

View File

@ -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 {

View File

@ -62,6 +62,10 @@ const PUBLIC_ENDPOINTS = [
route: "/api/system/restored",
method: "POST",
},
{
route: "/api/global/users/invite",
method: "GET",
},
]
const NO_TENANCY_ENDPOINTS = [

View File

@ -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(),

View File

@ -48,6 +48,7 @@ export class UserAPI extends TestAPI {
.send({
password: "newpassword",
inviteCode: code,
firstName: "Ted",
})
.expect("Content-Type", /json/)
.expect(200)