diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index 5d9c5b74d37..6ee06d12ef0 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -199,9 +199,8 @@ export const createPlatformUserView = async () => { export const queryPlatformView = async ( viewName: ViewName, - params: DatabaseQueryOpts, - opts?: QueryViewOptions -): Promise => { + params: DatabaseQueryOpts +): Promise => { const CreateFuncByName: any = { [ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView, [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, @@ -209,7 +208,9 @@ export const queryPlatformView = async ( return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { const createFn = CreateFuncByName[viewName] - return queryView(viewName, params, db, createFn, opts) + return queryView(viewName, params, db, createFn, { + arrayResponse: true, + }) as Promise }) } diff --git a/packages/backend-core/src/platform/users.ts b/packages/backend-core/src/platform/users.ts index 93e9df8c0e6..9378e237244 100644 --- a/packages/backend-core/src/platform/users.ts +++ b/packages/backend-core/src/platform/users.ts @@ -25,6 +25,11 @@ export async function getUserDoc(emailOrId: string): Promise { return db.get(emailOrId) } +export async function updateUserDoc(platformUser: PlatformUserById) { + const db = getPlatformDB() + await db.put(platformUser) +} + // CREATE function newUserIdDoc(id: string, tenantId: string): PlatformUserById { diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 4865ebb5bc1..c96c615f4b7 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -18,6 +18,9 @@ import { User, UserStatus, UserGroup, + PlatformUserBySsoId, + PlatformUserById, + AnyDocument, } from "@budibase/types" import { getAccountHolderFromUserIds, @@ -25,7 +28,11 @@ import { isCreator, validateUniqueUser, } from "./utils" -import { searchExistingEmails } from "./lookup" +import { + getFirstPlatformUser, + getPlatformUsers, + searchExistingEmails, +} from "./lookup" import { hash } from "../utils" import { validatePassword } from "../security" @@ -446,9 +453,32 @@ export class UserDB { creator => !!creator ).length + const ssoUsersToDelete: AnyDocument[] = [] for (let user of usersToDelete) { + const platformUser = (await getFirstPlatformUser( + user._id! + )) as PlatformUserById + const ssoId = platformUser.ssoId + if (ssoId) { + // Need to get the _rev of the SSO user doc to delete it. The view also returns docs that have the ssoId property, so we need to ignore those. + const ssoUsers = (await getPlatformUsers( + ssoId + )) as PlatformUserBySsoId[] + ssoUsers + .filter(user => user.ssoId == null) + .forEach(user => { + ssoUsersToDelete.push({ + ...user, + _deleted: true, + }) + }) + } await bulkDeleteProcessing(user) } + + // Delete any associated SSO user docs + await platform.getPlatformDB().bulkDocs(ssoUsersToDelete) + await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount) // Build Response diff --git a/packages/backend-core/src/users/lookup.ts b/packages/backend-core/src/users/lookup.ts index 355be74dabc..5324ba950fe 100644 --- a/packages/backend-core/src/users/lookup.ts +++ b/packages/backend-core/src/users/lookup.ts @@ -34,15 +34,22 @@ export async function searchExistingEmails(emails: string[]) { } // lookup, could be email or userId, either will return a doc -export async function getPlatformUser( +export async function getPlatformUsers( identifier: string -): Promise { +): Promise { // use the view here and allow to find anyone regardless of casing // Use lowercase to ensure email login is case insensitive - return (await dbUtils.queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, { + return await dbUtils.queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, { keys: [identifier.toLowerCase()], include_docs: true, - })) as PlatformUser + }) +} + +export async function getFirstPlatformUser( + identifier: string +): Promise { + const platformUserDocs = await getPlatformUsers(identifier) + return platformUserDocs[0] ?? null } export async function getExistingTenantUsers( @@ -74,15 +81,10 @@ export async function getExistingPlatformUsers( keys: lcEmails, include_docs: true, } - - const opts = { - arrayResponse: true, - } - return (await dbUtils.queryPlatformView( + return await dbUtils.queryPlatformView( ViewName.PLATFORM_USERS_LOWERCASE, - params, - opts - )) as PlatformUserByEmail[] + params + ) } export async function getExistingAccounts( @@ -93,14 +95,5 @@ export async function getExistingAccounts( keys: lcEmails, include_docs: true, } - - const opts = { - arrayResponse: true, - } - - return (await dbUtils.queryPlatformView( - ViewName.ACCOUNT_BY_EMAIL, - params, - opts - )) as AccountMetadata[] + return await dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, params) } diff --git a/packages/backend-core/src/users/utils.ts b/packages/backend-core/src/users/utils.ts index 348ad1532f6..e1e3da181d9 100644 --- a/packages/backend-core/src/users/utils.ts +++ b/packages/backend-core/src/users/utils.ts @@ -1,7 +1,7 @@ import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types" import * as accountSdk from "../accounts" import env from "../environment" -import { getPlatformUser } from "./lookup" +import { getFirstPlatformUser } from "./lookup" import { EmailUnavailableError } from "../errors" import { getTenantId } from "../context" import { sdk } from "@budibase/shared-core" @@ -51,7 +51,7 @@ async function isCreatorByGroupMembership(user?: User | ContextUser) { export async function validateUniqueUser(email: string, tenantId: string) { // check budibase users in other tenants if (env.MULTI_TENANCY) { - const tenantUser = await getPlatformUser(email) + const tenantUser = await getFirstPlatformUser(email) if (tenantUser != null && tenantUser.tenantId !== tenantId) { throw new EmailUnavailableError(email) } diff --git a/packages/builder/src/components/common/users/PasswordRepeatInput.svelte b/packages/builder/src/components/common/users/PasswordRepeatInput.svelte index 4a453ef049d..9aa90007205 100644 --- a/packages/builder/src/components/common/users/PasswordRepeatInput.svelte +++ b/packages/builder/src/components/common/users/PasswordRepeatInput.svelte @@ -1,20 +1,32 @@ - - + - - + diff --git a/packages/builder/src/pages/builder/auth/reset.svelte b/packages/builder/src/pages/builder/auth/reset.svelte index 06d5feaa53f..e1464aba772 100644 --- a/packages/builder/src/pages/builder/auth/reset.svelte +++ b/packages/builder/src/pages/builder/auth/reset.svelte @@ -4,47 +4,45 @@ Button, Heading, Layout, + ProgressCircle, notifications, - FancyForm, - FancyInput, } from "@budibase/bbui" import { goto, params } from "@roxi/routify" import { auth, organisation } from "stores/portal" import Logo from "assets/bb-emblem.svg" import { TestimonialPage } from "@budibase/frontend-core/src/components" import { onMount } from "svelte" - import { handleError, passwordsMatch } from "./_components/utils" + import PasswordRepeatInput from "../../../components/common/users/PasswordRepeatInput.svelte" const resetCode = $params["?code"] let form - let formData = {} - let errors = {} let loaded = false + let loading = false + let password + let passwordError - $: submitted = false $: forceResetPassword = $auth?.user?.forceResetPassword async function reset() { - form.validate() - if (Object.keys(errors).length > 0) { + if (!form.validate() || passwordError) { return } - submitted = true try { + loading = true if (forceResetPassword) { await auth.updateSelf({ - password: formData.password, + password, forceResetPassword: false, }) $goto("../portal/") } else { - await auth.resetPassword(formData.password, resetCode) + await auth.resetPassword(password, resetCode) notifications.success("Password reset successfully") // send them to login if reset successful $goto("./login") } } catch (err) { - submitted = false + loading = false notifications.error(err.message || "Unable to reset password") } } @@ -58,86 +56,37 @@ } loaded = true }) + + const handleKeydown = evt => { + if (evt.key === "Enter") { + reset() + } + } + {#if loaded} logo {/if} - - Reset your password - Please enter the new password you'd like to use. - - - - { - 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} - /> - { - 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} - /> - + Reset your password + Must contain at least 12 characters + + -
- -
+
diff --git a/packages/types/src/documents/platform/users.ts b/packages/types/src/documents/platform/users.ts index 8f24329502a..8882baab1c1 100644 --- a/packages/types/src/documents/platform/users.ts +++ b/packages/types/src/documents/platform/users.ts @@ -13,6 +13,8 @@ export interface PlatformUserByEmail extends Document { */ export interface PlatformUserById extends Document { tenantId: string + email?: string + ssoId?: string } /** @@ -22,6 +24,7 @@ export interface PlatformUserBySsoId extends Document { tenantId: string userId: string email: string + ssoId?: string } export type PlatformUser = diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index cd69281f564..273eec279cf 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -62,7 +62,7 @@ export const addSsoSupport = async (ctx: Ctx) => { const { email, ssoId } = ctx.request.body try { // Status is changed to 404 from getUserDoc if user is not found - let userByEmail = (await platform.users.getUserDoc( + const userByEmail = (await platform.users.getUserDoc( email )) as PlatformUserByEmail await platform.users.addSsoUser( @@ -71,6 +71,13 @@ export const addSsoSupport = async (ctx: Ctx) => { userByEmail.userId, userByEmail.tenantId ) + // Need to get the _rev of the user doc to update + const userById = await platform.users.getUserDoc(userByEmail.userId) + await platform.users.updateUserDoc({ + ...userById, + email, + ssoId, + }) ctx.status = 200 } catch (err: any) { ctx.throw(err.status || 400, err) @@ -268,7 +275,7 @@ export const find = async (ctx: any) => { export const tenantUserLookup = async (ctx: any) => { const id = ctx.params.id - const user = await userSdk.core.getPlatformUser(id) + const user = await userSdk.core.getFirstPlatformUser(id) if (user) { ctx.body = user } else { diff --git a/yarn.lock b/yarn.lock index d43d77f6f50..d15f1a2d080 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8005,7 +8005,20 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -chai@^4.3.10, chai@^4.3.7: +chai@^4.3.10: + version "4.5.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" + integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.1.0" + +chai@^4.3.7: version "4.4.1" resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== @@ -21204,6 +21217,11 @@ type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-detect@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" + integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== + type-fest@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"