diff --git a/packages/builder/src/pages/builder/admin/index.svelte b/packages/builder/src/pages/builder/admin/index.svelte index 99731b8285..7ef2d6d290 100644 --- a/packages/builder/src/pages/builder/admin/index.svelte +++ b/packages/builder/src/pages/builder/admin/index.svelte @@ -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 { handleError, passwordsMatch } 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 + $: multiTenancyEnabled = $admin.multiTenancy async function save() { + form.validate() + if (Object.keys(errors).length > 0) { + return + } + submitted = true try { - adminUser.tenantId = tenantId + const adminUser = { ...formData, tenantId } // 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,109 @@ -
-
- + + + + logo - - Create an admin user - - The admin user has access to everything in Budibase. - - - - - - - - - {#if multiTenancyEnabled} - { - admin.unload() - $goto("../auth/org") - }} - > - Change organisation - - {:else if !cloud && !imported} + Create an admin user + The admin user has access to everything in Budibase. + + + + { + formData = { + ...formData, + email: e.detail, + } + }} + validate={() => { + handleError(() => { + return { + email: !formData.email + ? "Please enter a valid email" + : undefined, + } + }, errors) + }} + disabled={submitted} + error={errors.email} + /> + { + 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} + /> + { + 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} + /> + + + + + + + -
-
+ + diff --git a/packages/builder/src/pages/builder/auth/_components/GoogleButton.svelte b/packages/builder/src/pages/builder/auth/_components/GoogleButton.svelte index 0acaa127cc..d8e1da7072 100644 --- a/packages/builder/src/pages/builder/auth/_components/GoogleButton.svelte +++ b/packages/builder/src/pages/builder/auth/_components/GoogleButton.svelte @@ -1,5 +1,5 @@ {#if show} - window.open(`/api/global/auth/${tenantId}/google`, "_blank")} > -
- google icon -

Sign in with Google

-
-
+ Log in with Google + {/if} - - diff --git a/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte b/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte index 27f5bde186..396bde3cb0 100644 --- a/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte +++ b/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte @@ -1,5 +1,5 @@ {#if show} - window.open( `/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`, "_blank" )} > -
- oidc icon -

{`Sign in with ${$oidc.name || "OIDC"}`}

-
-
+ {`Log in with ${$oidc.name || "OIDC"}`} + {/if} - - diff --git a/packages/builder/src/pages/builder/auth/_components/utils.js b/packages/builder/src/pages/builder/auth/_components/utils.js new file mode 100644 index 0000000000..0ca42292b9 --- /dev/null +++ b/packages/builder/src/pages/builder/auth/_components/utils.js @@ -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 + ) +} diff --git a/packages/builder/src/pages/builder/auth/forgot.svelte b/packages/builder/src/pages/builder/auth/forgot.svelte index 7227fd6377..13c8331c2d 100644 --- a/packages/builder/src/pages/builder/auth/forgot.svelte +++ b/packages/builder/src/pages/builder/auth/forgot.svelte @@ -1,25 +1,35 @@ -
-
- - - logo - - - Forgotten your password? - - No problem! Just enter your account's email address and we'll send you - a link to reset it. - - - - - - $goto("../")}>Back - + + + logo + + +
+ $goto("../")}> + + + Forgotten your password? +
+
+
+ + + No problem! Just enter your account's email address and we'll send you a + link to reset it. + -
-
+ + + + { + email = e.detail + }} + validate={() => { + if (!email) { + return "Please enter your email" + } + return null + }} + {error} + disabled={submitted} + /> + + +
+ +
+ + diff --git a/packages/builder/src/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte index d8633a4fbc..b80dac44e7 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -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,98 @@ -
-
- - - logo - Sign in to {company} - + + + + {#if loaded} - - - {/if} - - - Sign in with email - - - - - - $goto("./forgot")}> - Forgot password? - - {#if multiTenancyEnabled && !cloud} - { - admin.unload() - $goto("./org") - }} - > - Change organisation - - {/if} - - {#if cloud} - - By using Budibase Cloud -
- you are agreeing to our - License Agreement - + logo {/if} + Log in to Budibase
-
-
+ + {#if loaded && ($organisation.google || $organisation.oidc)} + + + + + + {/if} + + { + formData = { + ...formData, + username: e.detail, + } + }} + validate={() => { + handleError(() => { + return { + username: !formData.username + ? "Please enter a valid email" + : undefined, + } + }, errors) + }} + error={errors.username} + /> + { + formData = { + ...formData, + password: e.detail, + } + }} + validate={() => { + handleError(() => { + return { + password: !formData.password + ? "Please enter your password" + : undefined, + } + }, errors) + }} + error={errors.password} + /> + + + + + + + + + + {#if cloud} + + By using Budibase Cloud +
+ you are agreeing to our + + License Agreement + + + {/if} + + diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.js index e9bdf790f2..1510207604 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.js @@ -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, diff --git a/packages/frontend-core/src/api/user.js b/packages/frontend-core/src/api/user.js index 5c4f070802..9875605ce0 100644 --- a/packages/frontend-core/src/api/user.js +++ b/packages/frontend-core/src/api/user.js @@ -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, }, }) }, diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 10741f3725..817480151d 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -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 { diff --git a/packages/worker/src/api/index.ts b/packages/worker/src/api/index.ts index e37c6c2d94..d8df62f532 100644 --- a/packages/worker/src/api/index.ts +++ b/packages/worker/src/api/index.ts @@ -62,6 +62,10 @@ const PUBLIC_ENDPOINTS = [ route: "/api/system/restored", method: "POST", }, + { + route: "/api/global/users/invite", + method: "GET", + }, ] const NO_TENANCY_ENDPOINTS = [ diff --git a/packages/worker/src/api/routes/global/users.ts b/packages/worker/src/api/routes/global/users.ts index 26cba8e1ab..a73462b235 100644 --- a/packages/worker/src/api/routes/global/users.ts +++ b/packages/worker/src/api/routes/global/users.ts @@ -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(),