Fix/verify sso bug (#14253)
* Delete associated SSO user * Update reset password screen * Partial type removed * lint * Split getFirstPlatformUser from getPlatformUsers * lint * Use correct method * Code review comments * lint
This commit is contained in:
parent
cf31c7ba3f
commit
60f7c03e17
|
@ -199,9 +199,8 @@ export const createPlatformUserView = async () => {
|
||||||
|
|
||||||
export const queryPlatformView = async <T extends Document>(
|
export const queryPlatformView = async <T extends Document>(
|
||||||
viewName: ViewName,
|
viewName: ViewName,
|
||||||
params: DatabaseQueryOpts,
|
params: DatabaseQueryOpts
|
||||||
opts?: QueryViewOptions
|
): Promise<T[]> => {
|
||||||
): Promise<T[] | T> => {
|
|
||||||
const CreateFuncByName: any = {
|
const CreateFuncByName: any = {
|
||||||
[ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView,
|
[ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView,
|
||||||
[ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView,
|
[ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView,
|
||||||
|
@ -209,7 +208,9 @@ export const queryPlatformView = async <T extends Document>(
|
||||||
|
|
||||||
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => {
|
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => {
|
||||||
const createFn = CreateFuncByName[viewName]
|
const createFn = CreateFuncByName[viewName]
|
||||||
return queryView(viewName, params, db, createFn, opts)
|
return queryView(viewName, params, db, createFn, {
|
||||||
|
arrayResponse: true,
|
||||||
|
}) as Promise<T[]>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,11 @@ export async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
|
||||||
return db.get(emailOrId)
|
return db.get(emailOrId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateUserDoc(platformUser: PlatformUserById) {
|
||||||
|
const db = getPlatformDB()
|
||||||
|
await db.put(platformUser)
|
||||||
|
}
|
||||||
|
|
||||||
// CREATE
|
// CREATE
|
||||||
|
|
||||||
function newUserIdDoc(id: string, tenantId: string): PlatformUserById {
|
function newUserIdDoc(id: string, tenantId: string): PlatformUserById {
|
||||||
|
|
|
@ -18,6 +18,9 @@ import {
|
||||||
User,
|
User,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
UserGroup,
|
UserGroup,
|
||||||
|
PlatformUserBySsoId,
|
||||||
|
PlatformUserById,
|
||||||
|
AnyDocument,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
getAccountHolderFromUserIds,
|
getAccountHolderFromUserIds,
|
||||||
|
@ -25,7 +28,11 @@ import {
|
||||||
isCreator,
|
isCreator,
|
||||||
validateUniqueUser,
|
validateUniqueUser,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import { searchExistingEmails } from "./lookup"
|
import {
|
||||||
|
getFirstPlatformUser,
|
||||||
|
getPlatformUsers,
|
||||||
|
searchExistingEmails,
|
||||||
|
} from "./lookup"
|
||||||
import { hash } from "../utils"
|
import { hash } from "../utils"
|
||||||
import { validatePassword } from "../security"
|
import { validatePassword } from "../security"
|
||||||
|
|
||||||
|
@ -446,9 +453,32 @@ export class UserDB {
|
||||||
creator => !!creator
|
creator => !!creator
|
||||||
).length
|
).length
|
||||||
|
|
||||||
|
const ssoUsersToDelete: AnyDocument[] = []
|
||||||
for (let user of usersToDelete) {
|
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)
|
await bulkDeleteProcessing(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete any associated SSO user docs
|
||||||
|
await platform.getPlatformDB().bulkDocs(ssoUsersToDelete)
|
||||||
|
|
||||||
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
|
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
|
||||||
|
|
||||||
// Build Response
|
// Build Response
|
||||||
|
|
|
@ -34,15 +34,22 @@ export async function searchExistingEmails(emails: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup, could be email or userId, either will return a doc
|
// lookup, could be email or userId, either will return a doc
|
||||||
export async function getPlatformUser(
|
export async function getPlatformUsers(
|
||||||
identifier: string
|
identifier: string
|
||||||
): Promise<PlatformUser | null> {
|
): Promise<PlatformUser[]> {
|
||||||
// use the view here and allow to find anyone regardless of casing
|
// use the view here and allow to find anyone regardless of casing
|
||||||
// Use lowercase to ensure email login is case insensitive
|
// 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()],
|
keys: [identifier.toLowerCase()],
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})) as PlatformUser
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFirstPlatformUser(
|
||||||
|
identifier: string
|
||||||
|
): Promise<PlatformUser | null> {
|
||||||
|
const platformUserDocs = await getPlatformUsers(identifier)
|
||||||
|
return platformUserDocs[0] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExistingTenantUsers(
|
export async function getExistingTenantUsers(
|
||||||
|
@ -74,15 +81,10 @@ export async function getExistingPlatformUsers(
|
||||||
keys: lcEmails,
|
keys: lcEmails,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
}
|
}
|
||||||
|
return await dbUtils.queryPlatformView(
|
||||||
const opts = {
|
|
||||||
arrayResponse: true,
|
|
||||||
}
|
|
||||||
return (await dbUtils.queryPlatformView(
|
|
||||||
ViewName.PLATFORM_USERS_LOWERCASE,
|
ViewName.PLATFORM_USERS_LOWERCASE,
|
||||||
params,
|
params
|
||||||
opts
|
)
|
||||||
)) as PlatformUserByEmail[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExistingAccounts(
|
export async function getExistingAccounts(
|
||||||
|
@ -93,14 +95,5 @@ export async function getExistingAccounts(
|
||||||
keys: lcEmails,
|
keys: lcEmails,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
}
|
}
|
||||||
|
return await dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, params)
|
||||||
const opts = {
|
|
||||||
arrayResponse: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await dbUtils.queryPlatformView(
|
|
||||||
ViewName.ACCOUNT_BY_EMAIL,
|
|
||||||
params,
|
|
||||||
opts
|
|
||||||
)) as AccountMetadata[]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types"
|
import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types"
|
||||||
import * as accountSdk from "../accounts"
|
import * as accountSdk from "../accounts"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { getPlatformUser } from "./lookup"
|
import { getFirstPlatformUser } from "./lookup"
|
||||||
import { EmailUnavailableError } from "../errors"
|
import { EmailUnavailableError } from "../errors"
|
||||||
import { getTenantId } from "../context"
|
import { getTenantId } from "../context"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
@ -51,7 +51,7 @@ async function isCreatorByGroupMembership(user?: User | ContextUser) {
|
||||||
export async function validateUniqueUser(email: string, tenantId: string) {
|
export async function validateUniqueUser(email: string, tenantId: string) {
|
||||||
// check budibase users in other tenants
|
// check budibase users in other tenants
|
||||||
if (env.MULTI_TENANCY) {
|
if (env.MULTI_TENANCY) {
|
||||||
const tenantUser = await getPlatformUser(email)
|
const tenantUser = await getFirstPlatformUser(email)
|
||||||
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
||||||
throw new EmailUnavailableError(email)
|
throw new EmailUnavailableError(email)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,32 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Input } from "@budibase/bbui"
|
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||||
import { createValidationStore, requiredValidator } from "helpers/validation"
|
import { createValidationStore, requiredValidator } from "helpers/validation"
|
||||||
|
|
||||||
export let password
|
export let password
|
||||||
|
export let passwordForm
|
||||||
export let error
|
export let error
|
||||||
|
|
||||||
|
const validatePassword = value => {
|
||||||
|
if (!value || value.length < 12) {
|
||||||
|
return "Please enter at least 12 characters. We recommend using machine generated or random passwords."
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const [firstPassword, passwordError, firstTouched] = createValidationStore(
|
const [firstPassword, passwordError, firstTouched] = createValidationStore(
|
||||||
"",
|
"",
|
||||||
requiredValidator
|
requiredValidator
|
||||||
)
|
)
|
||||||
const [repeatPassword, _, repeatTouched] = createValidationStore(
|
const [repeatPassword, _, repeatTouched] = createValidationStore(
|
||||||
"",
|
"",
|
||||||
requiredValidator
|
requiredValidator,
|
||||||
|
validatePassword
|
||||||
)
|
)
|
||||||
|
|
||||||
$: password = $firstPassword
|
$: password = $firstPassword
|
||||||
|
$: firstPasswordError =
|
||||||
|
($firstTouched && $passwordError) ||
|
||||||
|
($repeatTouched && validatePassword(password))
|
||||||
$: error =
|
$: error =
|
||||||
!$firstPassword ||
|
!$firstPassword ||
|
||||||
!$firstTouched ||
|
!$firstTouched ||
|
||||||
|
@ -22,19 +34,19 @@
|
||||||
$firstPassword !== $repeatPassword
|
$firstPassword !== $repeatPassword
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout gap="XS" noPadding>
|
<FancyForm bind:this={passwordForm}>
|
||||||
<Input
|
<FancyInput
|
||||||
label="Password"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
error={$firstTouched && $passwordError}
|
error={firstPasswordError}
|
||||||
bind:value={$firstPassword}
|
bind:value={$firstPassword}
|
||||||
/>
|
/>
|
||||||
<Input
|
<FancyInput
|
||||||
label="Repeat Password"
|
label="Repeat password"
|
||||||
type="password"
|
type="password"
|
||||||
error={$repeatTouched &&
|
error={$repeatTouched &&
|
||||||
$firstPassword !== $repeatPassword &&
|
$firstPassword !== $repeatPassword &&
|
||||||
"Passwords must match"}
|
"Passwords must match"}
|
||||||
bind:value={$repeatPassword}
|
bind:value={$repeatPassword}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</FancyForm>
|
||||||
|
|
|
@ -4,47 +4,45 @@
|
||||||
Button,
|
Button,
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
|
ProgressCircle,
|
||||||
notifications,
|
notifications,
|
||||||
FancyForm,
|
|
||||||
FancyInput,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
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 { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { handleError, passwordsMatch } from "./_components/utils"
|
import PasswordRepeatInput from "../../../components/common/users/PasswordRepeatInput.svelte"
|
||||||
|
|
||||||
const resetCode = $params["?code"]
|
const resetCode = $params["?code"]
|
||||||
let form
|
let form
|
||||||
let formData = {}
|
|
||||||
let errors = {}
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let loading = false
|
||||||
|
let password
|
||||||
|
let passwordError
|
||||||
|
|
||||||
$: submitted = false
|
|
||||||
$: forceResetPassword = $auth?.user?.forceResetPassword
|
$: forceResetPassword = $auth?.user?.forceResetPassword
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
form.validate()
|
if (!form.validate() || passwordError) {
|
||||||
if (Object.keys(errors).length > 0) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
submitted = true
|
|
||||||
try {
|
try {
|
||||||
|
loading = true
|
||||||
if (forceResetPassword) {
|
if (forceResetPassword) {
|
||||||
await auth.updateSelf({
|
await auth.updateSelf({
|
||||||
password: formData.password,
|
password,
|
||||||
forceResetPassword: false,
|
forceResetPassword: false,
|
||||||
})
|
})
|
||||||
$goto("../portal/")
|
$goto("../portal/")
|
||||||
} else {
|
} else {
|
||||||
await auth.resetPassword(formData.password, resetCode)
|
await auth.resetPassword(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
|
loading = false
|
||||||
notifications.error(err.message || "Unable to reset password")
|
notifications.error(err.message || "Unable to reset password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,86 +56,37 @@
|
||||||
}
|
}
|
||||||
loaded = true
|
loaded = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleKeydown = evt => {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
{/if}
|
{/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>
|
|
||||||
|
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
<FancyForm bind:this={form}>
|
<Heading size="M">Reset your password</Heading>
|
||||||
<FancyInput
|
<Body size="M">Must contain at least 12 characters</Body>
|
||||||
label="Password"
|
<PasswordRepeatInput
|
||||||
value={formData.password}
|
bind:passwordForm={form}
|
||||||
type="password"
|
bind:password
|
||||||
on:change={e => {
|
bind:error={passwordError}
|
||||||
formData = {
|
/>
|
||||||
...formData,
|
<Button secondary cta on:click={reset}>
|
||||||
password: e.detail,
|
{#if loading}
|
||||||
}
|
<ProgressCircle overBackground={true} size="S" />
|
||||||
}}
|
{:else}
|
||||||
validate={() => {
|
Reset
|
||||||
let fieldError = {}
|
{/if}
|
||||||
|
</Button>
|
||||||
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>
|
</Layout>
|
||||||
<div>
|
<div />
|
||||||
<Button
|
|
||||||
disabled={Object.keys(errors).length > 0 ||
|
|
||||||
(forceResetPassword ? false : !resetCode)}
|
|
||||||
cta
|
|
||||||
on:click={reset}>Reset your password</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</TestimonialPage>
|
</TestimonialPage>
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ export interface PlatformUserByEmail extends Document {
|
||||||
*/
|
*/
|
||||||
export interface PlatformUserById extends Document {
|
export interface PlatformUserById extends Document {
|
||||||
tenantId: string
|
tenantId: string
|
||||||
|
email?: string
|
||||||
|
ssoId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,6 +24,7 @@ export interface PlatformUserBySsoId extends Document {
|
||||||
tenantId: string
|
tenantId: string
|
||||||
userId: string
|
userId: string
|
||||||
email: string
|
email: string
|
||||||
|
ssoId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlatformUser =
|
export type PlatformUser =
|
||||||
|
|
|
@ -62,7 +62,7 @@ export const addSsoSupport = async (ctx: Ctx<AddSSoUserRequest>) => {
|
||||||
const { email, ssoId } = ctx.request.body
|
const { email, ssoId } = ctx.request.body
|
||||||
try {
|
try {
|
||||||
// Status is changed to 404 from getUserDoc if user is not found
|
// 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
|
email
|
||||||
)) as PlatformUserByEmail
|
)) as PlatformUserByEmail
|
||||||
await platform.users.addSsoUser(
|
await platform.users.addSsoUser(
|
||||||
|
@ -71,6 +71,13 @@ export const addSsoSupport = async (ctx: Ctx<AddSSoUserRequest>) => {
|
||||||
userByEmail.userId,
|
userByEmail.userId,
|
||||||
userByEmail.tenantId
|
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
|
ctx.status = 200
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.throw(err.status || 400, err)
|
ctx.throw(err.status || 400, err)
|
||||||
|
@ -268,7 +275,7 @@ export const find = async (ctx: any) => {
|
||||||
|
|
||||||
export const tenantUserLookup = async (ctx: any) => {
|
export const tenantUserLookup = async (ctx: any) => {
|
||||||
const id = ctx.params.id
|
const id = ctx.params.id
|
||||||
const user = await userSdk.core.getPlatformUser(id)
|
const user = await userSdk.core.getFirstPlatformUser(id)
|
||||||
if (user) {
|
if (user) {
|
||||||
ctx.body = user
|
ctx.body = user
|
||||||
} else {
|
} else {
|
||||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -8005,7 +8005,20 @@ caseless@~0.12.0:
|
||||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||||
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
|
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"
|
version "4.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1"
|
resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1"
|
||||||
integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==
|
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"
|
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||||
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
|
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:
|
type-fest@^0.13.1:
|
||||||
version "0.13.1"
|
version "0.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
|
||||||
|
|
Loading…
Reference in New Issue