diff --git a/Makefile b/Makefile index 7e67ce6c74..8485efe660 100644 --- a/Makefile +++ b/Makefile @@ -160,6 +160,8 @@ credentials: NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_FR=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX NOTIFICATION_ORG_INVITE_EN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX NOTIFICATION_ORG_INVITE_FR=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + NOTIFICATION_ORG_INVITE_REQUEST_EN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + NOTIFICATION_ORG_INVITE_REQUEST_FR=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX NOTIFICATION_PASSWORD_RESET_EN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX NOTIFICATION_PASSWORD_RESET_FR=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX NOTIFICATION_TWO_FACTOR_CODE_EN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX diff --git a/api/cloudbuild.yaml b/api/cloudbuild.yaml index ecbb3cd783..bed41c92c0 100644 --- a/api/cloudbuild.yaml +++ b/api/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: [ '-c', - 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb', + 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb/arangodb:3.10.4' ] - name: mikewilliamson/wait-for @@ -47,6 +47,8 @@ steps: - NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_FR=$_NOTIFICATION_TEST_TEMPLATE_ID - NOTIFICATION_ORG_INVITE_EN=$_NOTIFICATION_TEST_TEMPLATE_ID - NOTIFICATION_ORG_INVITE_FR=$_NOTIFICATION_TEST_TEMPLATE_ID + - NOTIFICATION_ORG_INVITE_REQUEST_EN=$_NOTIFICATION_TEST_TEMPLATE_ID + - NOTIFICATION_ORG_INVITE_REQUEST_FR=$_NOTIFICATION_TEST_TEMPLATE_ID - NOTIFICATION_PASSWORD_RESET_EN=$_NOTIFICATION_TEST_TEMPLATE_ID - NOTIFICATION_PASSWORD_RESET_FR=$_NOTIFICATION_TEST_TEMPLATE_ID - NOTIFICATION_TWO_FACTOR_CODE_EN=$_NOTIFICATION_TEST_TEMPLATE_ID diff --git a/api/src/affiliation/loaders/load-affiliation-connections-by-org-id.js b/api/src/affiliation/loaders/load-affiliation-connections-by-org-id.js index fdc47ddcec..fa32893b24 100644 --- a/api/src/affiliation/loaders/load-affiliation-connections-by-org-id.js +++ b/api/src/affiliation/loaders/load-affiliation-connections-by-org-id.js @@ -1,178 +1,168 @@ -import {aql} from 'arangojs' -import {fromGlobalId, toGlobalId} from 'graphql-relay' -import {t} from '@lingui/macro' +import { aql } from 'arangojs' +import { fromGlobalId, toGlobalId } from 'graphql-relay' +import { t } from '@lingui/macro' export const loadAffiliationConnectionsByOrgId = - ({query, userKey, cleanseInput, i18n}) => - async ({orgId, after, before, first, last, orderBy, search}) => { - let afterTemplate = aql`` - if (typeof after !== 'undefined') { - const {id: afterId} = fromGlobalId(cleanseInput(after)) - if (typeof orderBy === 'undefined') { - afterTemplate = aql`FILTER TO_NUMBER(affiliation._key) > TO_NUMBER(${afterId})` + ({ query, userKey, cleanseInput, i18n }) => + async ({ orgId, after, before, first, last, orderBy, search, includePending }) => { + let afterTemplate = aql`` + if (typeof after !== 'undefined') { + const { id: afterId } = fromGlobalId(cleanseInput(after)) + if (typeof orderBy === 'undefined') { + afterTemplate = aql`FILTER TO_NUMBER(affiliation._key) > TO_NUMBER(${afterId})` + } else { + let afterTemplateDirection + if (orderBy.direction === 'ASC') { + afterTemplateDirection = aql`>` } else { - let afterTemplateDirection - if (orderBy.direction === 'ASC') { - afterTemplateDirection = aql`>` - } else { - afterTemplateDirection = aql`<` - } + afterTemplateDirection = aql`<` + } - let affiliationField, documentField - /* istanbul ignore else */ - if (orderBy.field === 'user-username') { - affiliationField = aql`DOCUMENT(users, PARSE_IDENTIFIER(affiliation._to).key).userName` - documentField = aql`DOCUMENT(users, PARSE_IDENTIFIER(DOCUMENT(affiliations, ${afterId})._to).key).userName` - } + let affiliationField, documentField + /* istanbul ignore else */ + if (orderBy.field === 'user-username') { + affiliationField = aql`DOCUMENT(users, PARSE_IDENTIFIER(affiliation._to).key).userName` + documentField = aql`DOCUMENT(users, PARSE_IDENTIFIER(DOCUMENT(affiliations, ${afterId})._to).key).userName` + } - afterTemplate = aql` + afterTemplate = aql` FILTER ${affiliationField} ${afterTemplateDirection} ${documentField} OR (${affiliationField} == ${documentField} AND TO_NUMBER(affiliation._key) > TO_NUMBER(${afterId})) ` - } } + } - let beforeTemplate = aql`` - if (typeof before !== 'undefined') { - const {id: beforeId} = fromGlobalId(cleanseInput(before)) - if (typeof orderBy === 'undefined') { - beforeTemplate = aql`FILTER TO_NUMBER(affiliation._key) < TO_NUMBER(${beforeId})` + let beforeTemplate = aql`` + if (typeof before !== 'undefined') { + const { id: beforeId } = fromGlobalId(cleanseInput(before)) + if (typeof orderBy === 'undefined') { + beforeTemplate = aql`FILTER TO_NUMBER(affiliation._key) < TO_NUMBER(${beforeId})` + } else { + let beforeTemplateDirection = aql`` + if (orderBy.direction === 'ASC') { + beforeTemplateDirection = aql`<` } else { - let beforeTemplateDirection = aql`` - if (orderBy.direction === 'ASC') { - beforeTemplateDirection = aql`<` - } else { - beforeTemplateDirection = aql`>` - } + beforeTemplateDirection = aql`>` + } - let affiliationField, documentField - /* istanbul ignore else */ - if (orderBy.field === 'user-username') { - affiliationField = aql`DOCUMENT(users, PARSE_IDENTIFIER(affiliation._to).key).userName` - documentField = aql`DOCUMENT(users, PARSE_IDENTIFIER(DOCUMENT(affiliations, ${beforeId})._to).key).userName` - } + let affiliationField, documentField + /* istanbul ignore else */ + if (orderBy.field === 'user-username') { + affiliationField = aql`DOCUMENT(users, PARSE_IDENTIFIER(affiliation._to).key).userName` + documentField = aql`DOCUMENT(users, PARSE_IDENTIFIER(DOCUMENT(affiliations, ${beforeId})._to).key).userName` + } - beforeTemplate = aql` + beforeTemplate = aql` FILTER ${affiliationField} ${beforeTemplateDirection} ${documentField} OR (${affiliationField} == ${documentField} AND TO_NUMBER(affiliation._key) < TO_NUMBER(${beforeId})) ` - } } + } - let limitTemplate = aql`` - if (typeof first === 'undefined' && typeof last === 'undefined') { + let limitTemplate = aql`` + if (typeof first === 'undefined' && typeof last === 'undefined') { + console.warn( + `User: ${userKey} did not have either \`first\` or \`last\` arguments set for: loadAffiliationConnectionsByOrgId.`, + ) + throw new Error( + i18n._(t`You must provide a \`first\` or \`last\` value to properly paginate the \`Affiliation\` connection.`), + ) + } else if (typeof first !== 'undefined' && typeof last !== 'undefined') { + console.warn( + `User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadAffiliationConnectionsByOrgId.`, + ) + throw new Error( + i18n._(t`Passing both \`first\` and \`last\` to paginate the \`Affiliation\` connection is not supported.`), + ) + } else if (typeof first === 'number' || typeof last === 'number') { + /* istanbul ignore else */ + if (first < 0 || last < 0) { + const argSet = typeof first !== 'undefined' ? 'first' : 'last' console.warn( - `User: ${userKey} did not have either \`first\` or \`last\` arguments set for: loadAffiliationConnectionsByOrgId.`, + `User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadAffiliationConnectionsByOrgId.`, ) - throw new Error( - i18n._( - t`You must provide a \`first\` or \`last\` value to properly paginate the \`Affiliation\` connection.`, - ), - ) - } else if (typeof first !== 'undefined' && typeof last !== 'undefined') { + throw new Error(i18n._(t`\`${argSet}\` on the \`Affiliation\` connection cannot be less than zero.`)) + } else if (first > 100 || last > 100) { + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + const amount = typeof first !== 'undefined' ? first : last console.warn( - `User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadAffiliationConnectionsByOrgId.`, + `User: ${userKey} attempted to have \`${argSet}\` set to ${amount} for: loadAffiliationConnectionsByOrgId.`, ) throw new Error( i18n._( - t`Passing both \`first\` and \`last\` to paginate the \`Affiliation\` connection is not supported.`, + t`Requesting \`${amount}\` records on the \`Affiliation\` connection exceeds the \`${argSet}\` limit of 100 records.`, ), ) - } else if (typeof first === 'number' || typeof last === 'number') { - /* istanbul ignore else */ - if (first < 0 || last < 0) { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadAffiliationConnectionsByOrgId.`, - ) - throw new Error( - i18n._( - t`\`${argSet}\` on the \`Affiliation\` connection cannot be less than zero.`, - ), - ) - } else if (first > 100 || last > 100) { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - const amount = typeof first !== 'undefined' ? first : last - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set to ${amount} for: loadAffiliationConnectionsByOrgId.`, - ) - throw new Error( - i18n._( - t`Requesting \`${amount}\` records on the \`Affiliation\` connection exceeds the \`${argSet}\` limit of 100 records.`, - ), - ) - } else if (typeof first !== 'undefined' && typeof last === 'undefined') { - limitTemplate = aql`TO_NUMBER(affiliation._key) ASC LIMIT TO_NUMBER(${first})` - } else if (typeof first === 'undefined' && typeof last !== 'undefined') { - limitTemplate = aql`TO_NUMBER(affiliation._key) DESC LIMIT TO_NUMBER(${last})` - } - } else { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - const typeSet = typeof first !== 'undefined' ? typeof first : typeof last - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadAffiliationConnectionsByOrgId.`, - ) - throw new Error( - i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`), - ) + } else if (typeof first !== 'undefined' && typeof last === 'undefined') { + limitTemplate = aql`TO_NUMBER(affiliation._key) ASC LIMIT TO_NUMBER(${first})` + } else if (typeof first === 'undefined' && typeof last !== 'undefined') { + limitTemplate = aql`TO_NUMBER(affiliation._key) DESC LIMIT TO_NUMBER(${last})` } + } else { + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + const typeSet = typeof first !== 'undefined' ? typeof first : typeof last + console.warn( + `User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadAffiliationConnectionsByOrgId.`, + ) + throw new Error(i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`)) + } - let hasNextPageFilter = aql`FILTER TO_NUMBER(affiliation._key) > TO_NUMBER(LAST(retrievedAffiliations)._key)` - let hasPreviousPageFilter = aql`FILTER TO_NUMBER(affiliation._key) < TO_NUMBER(FIRST(retrievedAffiliations)._key)` - if (typeof orderBy !== 'undefined') { - let hasNextPageDirection = aql`` - let hasPreviousPageDirection = aql`` - if (orderBy.direction === 'ASC') { - hasNextPageDirection = aql`>` - hasPreviousPageDirection = aql`<` - } else { - hasNextPageDirection = aql`<` - hasPreviousPageDirection = aql`>` - } + let hasNextPageFilter = aql`FILTER TO_NUMBER(affiliation._key) > TO_NUMBER(LAST(retrievedAffiliations)._key)` + let hasPreviousPageFilter = aql`FILTER TO_NUMBER(affiliation._key) < TO_NUMBER(FIRST(retrievedAffiliations)._key)` + if (typeof orderBy !== 'undefined') { + let hasNextPageDirection = aql`` + let hasPreviousPageDirection = aql`` + if (orderBy.direction === 'ASC') { + hasNextPageDirection = aql`>` + hasPreviousPageDirection = aql`<` + } else { + hasNextPageDirection = aql`<` + hasPreviousPageDirection = aql`>` + } - let affField, hasNextPageDocument, hasPreviousPageDocument - /* istanbul ignore else */ - if (orderBy.field === 'user-username') { - affField = aql`DOCUMENT(users, PARSE_IDENTIFIER(affiliation._to).key).userName` - hasNextPageDocument = aql`DOCUMENT(users, PARSE_IDENTIFIER(LAST(retrievedAffiliations)._to).key).userName` - hasPreviousPageDocument = aql`DOCUMENT(users, PARSE_IDENTIFIER(FIRST(retrievedAffiliations)._to).key).userName` - } + let affField, hasNextPageDocument, hasPreviousPageDocument + /* istanbul ignore else */ + if (orderBy.field === 'user-username') { + affField = aql`DOCUMENT(users, PARSE_IDENTIFIER(affiliation._to).key).userName` + hasNextPageDocument = aql`DOCUMENT(users, PARSE_IDENTIFIER(LAST(retrievedAffiliations)._to).key).userName` + hasPreviousPageDocument = aql`DOCUMENT(users, PARSE_IDENTIFIER(FIRST(retrievedAffiliations)._to).key).userName` + } - hasNextPageFilter = aql` + hasNextPageFilter = aql` FILTER ${affField} ${hasNextPageDirection} ${hasNextPageDocument} OR (${affField} == ${hasNextPageDocument} AND TO_NUMBER(affiliation._key) > TO_NUMBER(LAST(retrievedAffiliations)._key)) ` - hasPreviousPageFilter = aql` + hasPreviousPageFilter = aql` FILTER ${affField} ${hasPreviousPageDirection} ${hasPreviousPageDocument} OR (${affField} == ${hasPreviousPageDocument} AND TO_NUMBER(affiliation._key) < TO_NUMBER(FIRST(retrievedAffiliations)._key)) ` - } + } - let sortByField = aql`` - if (typeof orderBy !== 'undefined') { - /* istanbul ignore else */ - if (orderBy.field === 'user-username') { - sortByField = aql`DOCUMENT(users, PARSE_IDENTIFIER(affiliation._to).key).userName ${orderBy.direction},` - } + let sortByField = aql`` + if (typeof orderBy !== 'undefined') { + /* istanbul ignore else */ + if (orderBy.field === 'user-username') { + sortByField = aql`DOCUMENT(users, PARSE_IDENTIFIER(affiliation._to).key).userName ${orderBy.direction},` } + } - let sortString - if (typeof last !== 'undefined') { - sortString = aql`DESC` - } else { - sortString = aql`ASC` - } + let sortString + if (typeof last !== 'undefined') { + sortString = aql`DESC` + } else { + sortString = aql`ASC` + } - let userSearchQuery = aql`` - let userIdFilter = aql`` - if (typeof search !== 'undefined' && search !== '') { - search = cleanseInput(search) - userSearchQuery = aql` + let userSearchQuery = aql`` + let userIdFilter = aql`` + if (typeof search !== 'undefined' && search !== '') { + search = cleanseInput(search) + userSearchQuery = aql` LET tokenArr = TOKENS(${search}, "text_en") LET userIds = UNIQUE( FOR token IN tokenArr @@ -184,12 +174,17 @@ export const loadAffiliationConnectionsByOrgId = RETURN user._id ) ` - userIdFilter = aql`FILTER e._to IN userIds` - } + userIdFilter = aql`FILTER e._to IN userIds` + } - let filteredAffiliationCursor - try { - filteredAffiliationCursor = await query` + let pendingFilter = aql`FILTER e.permission != "pending"` + if (includePending) { + pendingFilter = aql`` + } + + let filteredAffiliationCursor + try { + filteredAffiliationCursor = await query` WITH affiliations, organizations, users, userSearch ${userSearchQuery} @@ -197,6 +192,7 @@ export const loadAffiliationConnectionsByOrgId = LET affiliationKeys = ( FOR v, e IN 1..1 OUTBOUND ${orgId} affiliations ${userIdFilter} + ${pendingFilter} RETURN e._key ) @@ -246,55 +242,51 @@ export const loadAffiliationConnectionsByOrgId = "endKey": LAST(retrievedAffiliations)._key } ` - } catch (err) { - console.error( - `Database error occurred while user: ${userKey} was trying to query affiliations in loadAffiliationConnectionsByOrgId, error: ${err}`, - ) - throw new Error( - i18n._(t`Unable to query affiliation(s). Please try again.`), - ) - } - - let filteredAffiliations - try { - filteredAffiliations = await filteredAffiliationCursor.next() - } catch (err) { - console.error( - `Cursor error occurred while user: ${userKey} was trying to gather affiliations in loadAffiliationConnectionsByOrgId, error: ${err}`, - ) - throw new Error( - i18n._(t`Unable to load affiliation(s). Please try again.`), - ) - } - - if (filteredAffiliations.affiliations.length === 0) { - return { - edges: [], - totalCount: 0, - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, - } - } + } catch (err) { + console.error( + `Database error occurred while user: ${userKey} was trying to query affiliations in loadAffiliationConnectionsByOrgId, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to query affiliation(s). Please try again.`)) + } - const edges = filteredAffiliations.affiliations.map((affiliation) => { - return { - cursor: toGlobalId('affiliation', affiliation._key), - node: affiliation, - } - }) + let filteredAffiliations + try { + filteredAffiliations = await filteredAffiliationCursor.next() + } catch (err) { + console.error( + `Cursor error occurred while user: ${userKey} was trying to gather affiliations in loadAffiliationConnectionsByOrgId, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to load affiliation(s). Please try again.`)) + } + if (filteredAffiliations.affiliations.length === 0) { return { - edges, - totalCount: filteredAffiliations.totalCount, + edges: [], + totalCount: 0, pageInfo: { - hasNextPage: filteredAffiliations.hasNextPage, - hasPreviousPage: filteredAffiliations.hasPreviousPage, - startCursor: toGlobalId('affiliation', filteredAffiliations.startKey), - endCursor: toGlobalId('affiliation', filteredAffiliations.endKey), + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', }, } } + + const edges = filteredAffiliations.affiliations.map((affiliation) => { + return { + cursor: toGlobalId('affiliation', affiliation._key), + node: affiliation, + } + }) + + return { + edges, + totalCount: filteredAffiliations.totalCount, + pageInfo: { + hasNextPage: filteredAffiliations.hasNextPage, + hasPreviousPage: filteredAffiliations.hasPreviousPage, + startCursor: toGlobalId('affiliation', filteredAffiliations.startKey), + endCursor: toGlobalId('affiliation', filteredAffiliations.endKey), + }, + } + } diff --git a/api/src/affiliation/mutations/__tests__/invite-user-to-org.test.js b/api/src/affiliation/mutations/__tests__/invite-user-to-org.test.js index 255f40bd7c..db734a9620 100644 --- a/api/src/affiliation/mutations/__tests__/invite-user-to-org.test.js +++ b/api/src/affiliation/mutations/__tests__/invite-user-to-org.test.js @@ -1,36 +1,23 @@ -import {ensure, dbNameFromFile} from 'arango-tools' -import {setupI18n} from '@lingui/core' -import {graphql, GraphQLSchema, GraphQLError} from 'graphql' -import {toGlobalId} from 'graphql-relay' +import { ensure, dbNameFromFile } from 'arango-tools' +import { setupI18n } from '@lingui/core' +import { graphql, GraphQLSchema } from 'graphql' +import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import { - checkPermission, - userRequired, - verifiedRequired, - tfaRequired, -} from '../../../auth' -import {createMutationSchema} from '../../../mutation' -import {createQuerySchema} from '../../../query' -import {cleanseInput} from '../../../validators' -import {loadOrgByKey} from '../../../organization/loaders' -import {loadUserByKey, loadUserByUserName} from '../../../user/loaders' +import { checkPermission, userRequired, verifiedRequired, tfaRequired } from '../../../auth' +import { createMutationSchema } from '../../../mutation' +import { createQuerySchema } from '../../../query' +import { cleanseInput } from '../../../validators' +import { loadOrgByKey } from '../../../organization/loaders' +import { loadUserByKey, loadUserByUserName } from '../../../user/loaders' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' -const {DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY} = process.env +const { DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY } = process.env describe('invite user to org', () => { - let query, - drop, - truncate, - schema, - collections, - transaction, - i18n, - tokenize, - user + let query, drop, truncate, schema, collections, transaction, i18n, tokenize, user, org, userToInvite const consoleOutput = [] const mockedInfo = (output) => consoleOutput.push(output) @@ -53,7 +40,7 @@ describe('invite user to org', () => { describe('given a successful invitation', () => { beforeAll(async () => { - ;({query, drop, truncate, collections, transaction} = await ensure({ + ;({ query, drop, truncate, collections, transaction } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -84,8 +71,8 @@ describe('invite user to org', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -96,30 +83,35 @@ describe('invite user to org', () => { }) let org beforeEach(async () => { - org = await collections.organizations.save({ - orgDetails: { - en: { - slug: 'treasury-board-secretariat', - acronym: 'TBS', - name: 'Treasury Board of Canada Secretariat', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - fr: { - slug: 'secretariat-conseil-tresor', - acronym: 'SCT', - name: 'Secrétariat du Conseil Trésor du Canada', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', + org = await ( + await collections.organizations.save( + { + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, }, - }, - }) + { returnNew: true }, + ) + ).new }) describe('users role is super admin', () => { beforeEach(async () => { @@ -151,7 +143,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: SUPER_ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: ENGLISH } ) { result { @@ -184,18 +175,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteEmail: sendOrgInviteEmail}, - validators: {cleanseInput}, + notify: { sendOrgInviteEmail: sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) @@ -203,8 +194,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - 'Successfully invited user to organization, and sent notification email.', + status: 'Successfully invited user to organization, and sent notification email.', }, }, }, @@ -248,7 +238,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: ENGLISH } ) { result { @@ -281,18 +270,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteEmail: sendOrgInviteEmail}, - validators: {cleanseInput}, + notify: { sendOrgInviteEmail: sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) @@ -300,8 +289,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - 'Successfully invited user to organization, and sent notification email.', + status: 'Successfully invited user to organization, and sent notification email.', }, }, }, @@ -345,7 +333,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: ENGLISH } ) { result { @@ -378,18 +365,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteEmail: sendOrgInviteEmail}, - validators: {cleanseInput}, + notify: { sendOrgInviteEmail: sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) @@ -397,8 +384,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - 'Successfully invited user to organization, and sent notification email.', + status: 'Successfully invited user to organization, and sent notification email.', }, }, }, @@ -435,7 +421,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: SUPER_ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: ENGLISH } ) { result { @@ -470,18 +455,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount }, + validators: { cleanseInput }, }, ) @@ -489,8 +474,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - 'Successfully sent invitation to service, and organization email.', + status: 'Successfully sent invitation to service, and organization email.', }, }, }, @@ -511,9 +495,9 @@ describe('invite user to org', () => { expect(sendOrgInviteCreateAccount).toHaveBeenCalledWith({ user: { userName: 'test@email.gc.ca', - preferredLang: 'english', }, - orgName: 'Treasury Board of Canada Secretariat', + orgNameEN: 'Treasury Board of Canada Secretariat', + orgNameFR: 'Secrétariat du Conseil Trésor du Canada', createAccountLink, }) }) @@ -531,7 +515,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: ENGLISH } ) { result { @@ -566,18 +549,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount }, + validators: { cleanseInput }, }, ) @@ -585,8 +568,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - 'Successfully sent invitation to service, and organization email.', + status: 'Successfully sent invitation to service, and organization email.', }, }, }, @@ -608,9 +590,9 @@ describe('invite user to org', () => { expect(sendOrgInviteCreateAccount).toHaveBeenCalledWith({ user: { userName: 'test@email.gc.ca', - preferredLang: 'english', }, - orgName: 'Treasury Board of Canada Secretariat', + orgNameEN: 'Treasury Board of Canada Secretariat', + orgNameFR: 'Secrétariat du Conseil Trésor du Canada', createAccountLink, }) }) @@ -628,7 +610,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: ENGLISH } ) { result { @@ -663,18 +644,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount }, + validators: { cleanseInput }, }, ) @@ -682,8 +663,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - 'Successfully sent invitation to service, and organization email.', + status: 'Successfully sent invitation to service, and organization email.', }, }, }, @@ -705,9 +685,9 @@ describe('invite user to org', () => { expect(sendOrgInviteCreateAccount).toHaveBeenCalledWith({ user: { userName: 'test@email.gc.ca', - preferredLang: 'english', }, - orgName: 'Treasury Board of Canada Secretariat', + orgNameEN: 'Treasury Board of Canada Secretariat', + orgNameFR: 'Secrétariat du Conseil Trésor du Canada', createAccountLink, }) }) @@ -744,7 +724,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: ENGLISH } ) { result { @@ -777,18 +756,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteEmail: sendOrgInviteEmail}, - validators: {cleanseInput}, + notify: { sendOrgInviteEmail: sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) @@ -796,8 +775,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - 'Successfully invited user to organization, and sent notification email.', + status: 'Successfully invited user to organization, and sent notification email.', }, }, }, @@ -841,7 +819,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: ENGLISH } ) { result { @@ -874,18 +851,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteEmail: sendOrgInviteEmail}, - validators: {cleanseInput}, + notify: { sendOrgInviteEmail: sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) @@ -893,8 +870,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - 'Successfully invited user to organization, and sent notification email.', + status: 'Successfully invited user to organization, and sent notification email.', }, }, }, @@ -932,7 +908,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: ENGLISH } ) { result { @@ -967,18 +942,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount }, + validators: { cleanseInput }, }, ) @@ -986,8 +961,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - 'Successfully sent invitation to service, and organization email.', + status: 'Successfully sent invitation to service, and organization email.', }, }, }, @@ -1009,9 +983,9 @@ describe('invite user to org', () => { expect(sendOrgInviteCreateAccount).toHaveBeenCalledWith({ user: { userName: 'test@email.gc.ca', - preferredLang: 'english', }, - orgName: 'Treasury Board of Canada Secretariat', + orgNameEN: 'Treasury Board of Canada Secretariat', + orgNameFR: 'Secrétariat du Conseil Trésor du Canada', createAccountLink, }) }) @@ -1029,7 +1003,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: ENGLISH } ) { result { @@ -1064,18 +1037,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount }, + validators: { cleanseInput }, }, ) @@ -1083,8 +1056,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - 'Successfully sent invitation to service, and organization email.', + status: 'Successfully sent invitation to service, and organization email.', }, }, }, @@ -1106,9 +1078,9 @@ describe('invite user to org', () => { expect(sendOrgInviteCreateAccount).toHaveBeenCalledWith({ user: { userName: 'test@email.gc.ca', - preferredLang: 'english', }, - orgName: 'Treasury Board of Canada Secretariat', + orgNameEN: 'Treasury Board of Canada Secretariat', + orgNameFR: 'Secrétariat du Conseil Trésor du Canada', createAccountLink, }) }) @@ -1121,8 +1093,8 @@ describe('invite user to org', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -1133,30 +1105,35 @@ describe('invite user to org', () => { }) let org beforeEach(async () => { - org = await collections.organizations.save({ - orgDetails: { - en: { - slug: 'treasury-board-secretariat', - acronym: 'TBS', - name: 'Treasury Board of Canada Secretariat', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - fr: { - slug: 'secretariat-conseil-tresor', - acronym: 'SCT', - name: 'Secrétariat du Conseil Trésor du Canada', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', + org = await ( + await collections.organizations.save( + { + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, }, - }, - }) + { returnNew: true }, + ) + ).new }) describe('users role is super admin', () => { beforeEach(async () => { @@ -1188,7 +1165,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: SUPER_ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: FRENCH } ) { result { @@ -1221,18 +1197,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'fr'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'fr' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteEmail: sendOrgInviteEmail}, - validators: {cleanseInput}, + notify: { sendOrgInviteEmail: sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) @@ -1285,7 +1261,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: FRENCH } ) { result { @@ -1318,18 +1293,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'fr'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'fr' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteEmail: sendOrgInviteEmail}, - validators: {cleanseInput}, + notify: { sendOrgInviteEmail: sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) @@ -1382,7 +1357,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: FRENCH } ) { result { @@ -1415,18 +1389,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'fr'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'fr' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteEmail: sendOrgInviteEmail}, - validators: {cleanseInput}, + notify: { sendOrgInviteEmail: sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) @@ -1473,7 +1447,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: SUPER_ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: FRENCH } ) { result { @@ -1508,18 +1481,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'fr'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'fr' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount }, + validators: { cleanseInput }, }, ) @@ -1527,8 +1500,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", + status: "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", }, }, }, @@ -1548,8 +1520,9 @@ describe('invite user to org', () => { `User: ${user._key} successfully invited user: test@email.gc.ca to the service, and org: secretariat-conseil-tresor.`, ]) expect(sendOrgInviteCreateAccount).toHaveBeenCalledWith({ - user: {userName: 'test@email.gc.ca', preferredLang: 'french'}, - orgName: 'Secrétariat du Conseil Trésor du Canada', + user: { userName: 'test@email.gc.ca' }, + orgNameEN: 'Treasury Board of Canada Secretariat', + orgNameFR: 'Secrétariat du Conseil Trésor du Canada', createAccountLink, }) }) @@ -1567,7 +1540,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: FRENCH } ) { result { @@ -1602,18 +1574,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'fr'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'fr' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount }, + validators: { cleanseInput }, }, ) @@ -1621,8 +1593,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", + status: "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", }, }, }, @@ -1642,8 +1613,9 @@ describe('invite user to org', () => { `User: ${user._key} successfully invited user: test@email.gc.ca to the service, and org: secretariat-conseil-tresor.`, ]) expect(sendOrgInviteCreateAccount).toHaveBeenCalledWith({ - user: {userName: 'test@email.gc.ca', preferredLang: 'french'}, - orgName: 'Secrétariat du Conseil Trésor du Canada', + user: { userName: 'test@email.gc.ca' }, + orgNameEN: 'Treasury Board of Canada Secretariat', + orgNameFR: 'Secrétariat du Conseil Trésor du Canada', createAccountLink, }) }) @@ -1661,7 +1633,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: FRENCH } ) { result { @@ -1696,18 +1667,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'fr'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'fr' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount }, + validators: { cleanseInput }, }, ) @@ -1715,8 +1686,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", + status: "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", }, }, }, @@ -1736,8 +1706,9 @@ describe('invite user to org', () => { `User: ${user._key} successfully invited user: test@email.gc.ca to the service, and org: secretariat-conseil-tresor.`, ]) expect(sendOrgInviteCreateAccount).toHaveBeenCalledWith({ - user: {userName: 'test@email.gc.ca', preferredLang: 'french'}, - orgName: 'Secrétariat du Conseil Trésor du Canada', + user: { userName: 'test@email.gc.ca' }, + orgNameEN: 'Treasury Board of Canada Secretariat', + orgNameFR: 'Secrétariat du Conseil Trésor du Canada', createAccountLink, }) }) @@ -1774,7 +1745,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: FRENCH } ) { result { @@ -1807,18 +1777,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'fr'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'fr' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteEmail: sendOrgInviteEmail}, - validators: {cleanseInput}, + notify: { sendOrgInviteEmail: sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) @@ -1871,7 +1841,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: FRENCH } ) { result { @@ -1904,18 +1873,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'fr'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'fr' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteEmail: sendOrgInviteEmail}, - validators: {cleanseInput}, + notify: { sendOrgInviteEmail: sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) @@ -1962,7 +1931,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: ADMIN orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: FRENCH } ) { result { @@ -1997,18 +1965,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'fr'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'fr' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount }, + validators: { cleanseInput }, }, ) @@ -2016,8 +1984,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", + status: "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", }, }, }, @@ -2037,8 +2004,9 @@ describe('invite user to org', () => { `User: ${user._key} successfully invited user: test@email.gc.ca to the service, and org: secretariat-conseil-tresor.`, ]) expect(sendOrgInviteCreateAccount).toHaveBeenCalledWith({ - user: {userName: 'test@email.gc.ca', preferredLang: 'french'}, - orgName: 'Secrétariat du Conseil Trésor du Canada', + user: { userName: 'test@email.gc.ca' }, + orgNameEN: 'Treasury Board of Canada Secretariat', + orgNameFR: 'Secrétariat du Conseil Trésor du Canada', createAccountLink, }) }) @@ -2056,7 +2024,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', org._key)}" - preferredLang: FRENCH } ) { result { @@ -2091,18 +2058,18 @@ describe('invite user to org', () => { tokenize, userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'fr'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'fr' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount }, + validators: { cleanseInput }, }, ) @@ -2110,8 +2077,7 @@ describe('invite user to org', () => { data: { inviteUserToOrg: { result: { - status: - "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", + status: "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", }, }, }, @@ -2131,8 +2097,9 @@ describe('invite user to org', () => { `User: ${user._key} successfully invited user: test@email.gc.ca to the service, and org: secretariat-conseil-tresor.`, ]) expect(sendOrgInviteCreateAccount).toHaveBeenCalledWith({ - user: {userName: 'test@email.gc.ca', preferredLang: 'french'}, - orgName: 'Secrétariat du Conseil Trésor du Canada', + user: { userName: 'test@email.gc.ca' }, + orgNameEN: 'Treasury Board of Canada Secretariat', + orgNameFR: 'Secrétariat du Conseil Trésor du Canada', createAccountLink, }) }) @@ -2142,13 +2109,84 @@ describe('invite user to org', () => { }) }) describe('given an unsuccessful invitation', () => { - describe('users language is set to english', () => { + beforeAll(async () => { + ;({ query, drop, truncate, collections, transaction } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + tokenize = jest.fn().mockReturnValue('token') + }) + beforeEach(async () => { + user = ( + await collections.users.save( + { + userName: 'test.account@istio.actually.exists', + emailValidated: true, + tfaSendMethod: 'email', + }, + { returnNew: true }, + ) + ).new + userToInvite = ( + await collections.users.save( + { + userName: 'usertoinvite@istio.actually.exists', + emailValidated: true, + tfaSendMethod: 'email', + }, + { returnNew: true }, + ) + ).new + org = ( + await collections.organizations.save( + { + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }, + { returnNew: true }, + ) + ).new + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + describe('users language is set to french', () => { beforeAll(() => { i18n = setupI18n({ - locale: 'en', + locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -2165,10 +2203,9 @@ describe('invite user to org', () => { mutation { inviteUserToOrg( input: { - userName: "test.account@istio.actually.exists" + userName: "${user.userName}" requestedRole: USER - orgId: "${toGlobalId('organizations', 1)}" - preferredLang: FRENCH + orgId: "${toGlobalId('organizations', org._key)}" } ) { result { @@ -2205,20 +2242,12 @@ describe('invite user to org', () => { tfaRequired: jest.fn(), }, loaders: { - loaders: { - loadOrgByKey: { - load: jest.fn(), - }, - loadUserByKey: { - load: jest.fn(), - }, - loadUserByUserName: { - load: jest.fn(), - }, - }, + loadOrgByKey: loadOrgByKey({ query, language: i18n.locale }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query, i18n }), }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) @@ -2227,16 +2256,14 @@ describe('invite user to org', () => { inviteUserToOrg: { result: { code: 400, - description: 'Unable to invite yourself to an org.', + description: "Impossible de s'inviter à un org.", }, }, }, } expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to invite themselves to 1.`, - ]) + expect(consoleOutput).toEqual([`User: 123 attempted to invite themselves to ${org._key}.`]) }) }) describe('user attempts to invite to an org that does not exist', () => { @@ -2250,7 +2277,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', 1)}" - preferredLang: FRENCH } ) { result { @@ -2297,8 +2323,8 @@ describe('invite user to org', () => { load: jest.fn(), }, }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) @@ -2307,7 +2333,7 @@ describe('invite user to org', () => { inviteUserToOrg: { result: { code: 400, - description: 'Unable to invite user to unknown organization.', + description: "Impossible d'inviter un utilisateur à une organisation inconnue.", }, }, }, @@ -2330,7 +2356,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', 123)}" - preferredLang: FRENCH } ) { result { @@ -2368,7 +2393,7 @@ describe('invite user to org', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), + load: jest.fn().mockReturnValue({ _key: 123 }), }, loadUserByKey: { load: jest.fn(), @@ -2377,8 +2402,8 @@ describe('invite user to org', () => { load: jest.fn(), }, }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) @@ -2388,7 +2413,7 @@ describe('invite user to org', () => { result: { code: 403, description: - 'Permission Denied: Please contact organization admin for help with user invitations.', + "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide concernant les invitations d'utilisateurs.", }, }, }, @@ -2411,7 +2436,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', 123)}" - preferredLang: FRENCH } ) { result { @@ -2449,7 +2473,7 @@ describe('invite user to org', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), + load: jest.fn().mockReturnValue({ _key: 123 }), }, loadUserByKey: { load: jest.fn(), @@ -2458,8 +2482,8 @@ describe('invite user to org', () => { load: jest.fn(), }, }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) @@ -2469,7 +2493,7 @@ describe('invite user to org', () => { result: { code: 403, description: - 'Permission Denied: Please contact organization admin for help with user invitations.', + "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide concernant les invitations d'utilisateurs.", }, }, }, @@ -2492,7 +2516,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: SUPER_ADMIN orgId: "${toGlobalId('organizations', 123)}" - preferredLang: FRENCH } ) { result { @@ -2530,7 +2553,7 @@ describe('invite user to org', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), + load: jest.fn().mockReturnValue({ _key: 123 }), }, loadUserByKey: { load: jest.fn(), @@ -2539,8 +2562,8 @@ describe('invite user to org', () => { load: jest.fn(), }, }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) @@ -2550,7 +2573,7 @@ describe('invite user to org', () => { result: { code: 403, description: - 'Permission Denied: Please contact organization admin for help with user invitations.', + "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide concernant les invitations d'utilisateurs.", }, }, }, @@ -2571,10 +2594,9 @@ describe('invite user to org', () => { mutation { inviteUserToOrg( input: { - userName: "test@email.gc.ca" + userName: "${userToInvite.userName}" requestedRole: USER - orgId: "${toGlobalId('organizations', 123)}" - preferredLang: FRENCH + orgId: "${toGlobalId('organizations', org._key)}" } ) { result { @@ -2613,30 +2635,29 @@ describe('invite user to org', () => { tfaRequired: jest.fn(), }, loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - _key: 123, - slug: 'secretariat-conseil-tresor', - }), - }, - loadUserByUserName: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, + loadOrgByKey: loadOrgByKey({ query, language: i18n.locale }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query, i18n }), }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError('Unable to invite user. Please try again.'), - ] + const error = { + data: { + inviteUserToOrg: { + result: { + code: 500, + description: "Impossible d'inviter un utilisateur. Veuillez réessayer.", + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `Transaction step error occurred while user: 123 attempted to invite user: 456 to org: secretariat-conseil-tresor, error: trx step err`, + `Transaction step error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: ${org.orgDetails.fr.slug}, error: trx step err`, ]) }) }) @@ -2648,10 +2669,9 @@ describe('invite user to org', () => { mutation { inviteUserToOrg( input: { - userName: "test@email.gc.ca" + userName: "${userToInvite.userName}" requestedRole: USER - orgId: "${toGlobalId('organizations', 123)}" - preferredLang: FRENCH + orgId: "${toGlobalId('organizations', org._key)}" } ) { result { @@ -2691,33 +2711,32 @@ describe('invite user to org', () => { tfaRequired: jest.fn(), }, loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - _key: 123, - slug: 'secretariat-conseil-tresor', - }), - }, - loadUserByUserName: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, + loadOrgByKey: loadOrgByKey({ query, language: i18n.locale }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, notify: { sendOrgInviteCreateAccount: jest.fn(), sendOrgInviteEmail: jest.fn(), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError('Unable to invite user. Please try again.'), - ] + const error = { + data: { + inviteUserToOrg: { + result: { + code: 500, + description: "Impossible d'inviter un utilisateur. Veuillez réessayer.", + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `Transaction commit error occurred while user: 123 attempted to invite user: 456 to org: secretariat-conseil-tresor, error: trx commit err`, + `Transaction commit error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: secretariat-conseil-tresor, error: trx commit err`, ]) }) }) @@ -2726,10 +2745,10 @@ describe('invite user to org', () => { describe('users language is set to english', () => { beforeAll(() => { i18n = setupI18n({ - locale: 'fr', + locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -2749,7 +2768,6 @@ describe('invite user to org', () => { userName: "test.account@istio.actually.exists" requestedRole: USER orgId: "${toGlobalId('organizations', 1)}" - preferredLang: FRENCH } ) { result { @@ -2798,8 +2816,8 @@ describe('invite user to org', () => { }, }, }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) @@ -2808,16 +2826,14 @@ describe('invite user to org', () => { inviteUserToOrg: { result: { code: 400, - description: "Impossible de s'inviter à un org.", + description: 'Unable to invite yourself to an org.', }, }, }, } expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to invite themselves to 1.`, - ]) + expect(consoleOutput).toEqual([`User: 123 attempted to invite themselves to 1.`]) }) }) describe('user attempts to invite to an org that does not exist', () => { @@ -2831,7 +2847,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', 1)}" - preferredLang: FRENCH } ) { result { @@ -2878,8 +2893,8 @@ describe('invite user to org', () => { load: jest.fn(), }, }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) @@ -2888,8 +2903,7 @@ describe('invite user to org', () => { inviteUserToOrg: { result: { code: 400, - description: - "Impossible d'inviter un utilisateur à une organisation inconnue.", + description: 'Unable to invite user to unknown organization.', }, }, }, @@ -2912,7 +2926,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', 123)}" - preferredLang: FRENCH } ) { result { @@ -2950,7 +2963,7 @@ describe('invite user to org', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), + load: jest.fn().mockReturnValue({ _key: 123 }), }, loadUserByKey: { load: jest.fn(), @@ -2959,8 +2972,8 @@ describe('invite user to org', () => { load: jest.fn(), }, }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) @@ -2969,8 +2982,7 @@ describe('invite user to org', () => { inviteUserToOrg: { result: { code: 403, - description: - "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide concernant les invitations d'utilisateurs.", + description: 'Permission Denied: Please contact organization admin for help with user invitations.', }, }, }, @@ -2993,7 +3005,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: USER orgId: "${toGlobalId('organizations', 123)}" - preferredLang: FRENCH } ) { result { @@ -3031,7 +3042,7 @@ describe('invite user to org', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), + load: jest.fn().mockReturnValue({ _key: 123 }), }, loadUserByKey: { load: jest.fn(), @@ -3040,8 +3051,8 @@ describe('invite user to org', () => { load: jest.fn(), }, }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) @@ -3050,8 +3061,7 @@ describe('invite user to org', () => { inviteUserToOrg: { result: { code: 403, - description: - "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide concernant les invitations d'utilisateurs.", + description: 'Permission Denied: Please contact organization admin for help with user invitations.', }, }, }, @@ -3074,7 +3084,6 @@ describe('invite user to org', () => { userName: "test@email.gc.ca" requestedRole: SUPER_ADMIN orgId: "${toGlobalId('organizations', 123)}" - preferredLang: FRENCH } ) { result { @@ -3112,7 +3121,7 @@ describe('invite user to org', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), + load: jest.fn().mockReturnValue({ _key: 123 }), }, loadUserByKey: { load: jest.fn(), @@ -3121,8 +3130,8 @@ describe('invite user to org', () => { load: jest.fn(), }, }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) @@ -3131,8 +3140,7 @@ describe('invite user to org', () => { inviteUserToOrg: { result: { code: 403, - description: - "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide concernant les invitations d'utilisateurs.", + description: 'Permission Denied: Please contact organization admin for help with user invitations.', }, }, }, @@ -3153,10 +3161,9 @@ describe('invite user to org', () => { mutation { inviteUserToOrg( input: { - userName: "test@email.gc.ca" + userName: "${userToInvite.userName}" requestedRole: USER - orgId: "${toGlobalId('organizations', 123)}" - preferredLang: FRENCH + orgId: "${toGlobalId('organizations', org._key)}" } ) { result { @@ -3195,32 +3202,29 @@ describe('invite user to org', () => { tfaRequired: jest.fn(), }, loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - _key: 123, - slug: 'secretariat-conseil-tresor', - }), - }, - loadUserByUserName: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, + loadOrgByKey: loadOrgByKey({ query, language: i18n.locale }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, - notify: {sendOrgInviteCreateAccount: jest.fn()}, - validators: {cleanseInput}, + notify: { sendOrgInviteCreateAccount: jest.fn() }, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - "Impossible d'inviter un utilisateur. Veuillez réessayer.", - ), - ] + const error = { + data: { + inviteUserToOrg: { + result: { + code: 500, + description: 'Unable to invite user. Please try again.', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `Transaction step error occurred while user: 123 attempted to invite user: 456 to org: secretariat-conseil-tresor, error: trx step err`, + `Transaction step error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: treasury-board-secretariat, error: trx step err`, ]) }) }) @@ -3232,10 +3236,9 @@ describe('invite user to org', () => { mutation { inviteUserToOrg( input: { - userName: "test@email.gc.ca" + userName: "${userToInvite.userName}" requestedRole: USER - orgId: "${toGlobalId('organizations', 123)}" - preferredLang: FRENCH + orgId: "${toGlobalId('organizations', org._key)}" } ) { result { @@ -3275,35 +3278,32 @@ describe('invite user to org', () => { tfaRequired: jest.fn(), }, loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - _key: 123, - slug: 'secretariat-conseil-tresor', - }), - }, - loadUserByUserName: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, + loadOrgByKey: loadOrgByKey({ query, language: i18n.locale }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, notify: { sendOrgInviteCreateAccount: jest.fn(), sendOrgInviteEmail: jest.fn(), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - "Impossible d'inviter un utilisateur. Veuillez réessayer.", - ), - ] + const error = { + data: { + inviteUserToOrg: { + result: { + code: 500, + description: 'Unable to invite user. Please try again.', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `Transaction commit error occurred while user: 123 attempted to invite user: 456 to org: secretariat-conseil-tresor, error: trx commit err`, + `Transaction commit error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: treasury-board-secretariat, error: trx commit err`, ]) }) }) diff --git a/api/src/affiliation/mutations/__tests__/leave-organization.test.js b/api/src/affiliation/mutations/__tests__/leave-organization.test.js index 2a639fe7f2..cb455c6d3b 100644 --- a/api/src/affiliation/mutations/__tests__/leave-organization.test.js +++ b/api/src/affiliation/mutations/__tests__/leave-organization.test.js @@ -1,33 +1,23 @@ -import {setupI18n} from '@lingui/core' -import {ensure, dbNameFromFile} from 'arango-tools' -import {graphql, GraphQLSchema, GraphQLError} from 'graphql' -import {toGlobalId} from 'graphql-relay' +import { setupI18n } from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import {checkOrgOwner, userRequired, verifiedRequired} from '../../../auth' -import {loadOrgByKey} from '../../../organization/loaders' -import {loadUserByKey} from '../../../user/loaders' -import {cleanseInput} from '../../../validators' -import {createMutationSchema} from '../../../mutation' -import {createQuerySchema} from '../../../query' +import { checkOrgOwner, userRequired, verifiedRequired } from '../../../auth' +import { loadOrgByKey } from '../../../organization/loaders' +import { loadUserByKey } from '../../../user/loaders' +import { cleanseInput } from '../../../validators' +import { createMutationSchema } from '../../../mutation' +import { createQuerySchema } from '../../../query' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' -const {DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY} = process.env +const { DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY } = process.env describe('given a successful leave', () => { - let query, - drop, - truncate, - schema, - collections, - transaction, - i18n, - user, - org, - domain, - domain2 + let query, drop, truncate, schema, collections, transaction, i18n, user, org, domain, domain2 const consoleOutput = [] const mockedInfo = (output) => consoleOutput.push(output) @@ -45,8 +35,8 @@ describe('given a successful leave', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -56,7 +46,7 @@ describe('given a successful leave', () => { }) }) beforeEach(async () => { - ;({query, drop, truncate, collections, transaction} = await ensure({ + ;({ query, drop, truncate, collections, transaction } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -112,13 +102,13 @@ describe('given a successful leave', () => { _to: domain2._id, }) - const dns = await collections.dns.save({dns: true}) + const dns = await collections.dns.save({ dns: true }) await collections.domainsDNS.save({ _from: domain._id, _to: dns._id, }) - const web = await collections.web.save({web: true}) + const web = await collections.web.save({ web: true }) await collections.domainsWeb.save({ _from: domain._id, _to: web._id, @@ -151,330 +141,17 @@ describe('given a successful leave', () => { await collections.affiliations.save({ _from: org._id, _to: user._id, - permission: 'admin', - owner: true, + permission: 'owner', }) }) describe('only one org claims the domains', () => { - describe('org is owner of dmarc data', () => { - beforeEach(async () => { - await collections.ownership.save({ - _from: org._id, - _to: domain._id, - }) - }) - it('removes all dmarc summary data', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({ - i18n, - query, - userKey: user._key, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - - const testOwnershipCursor = - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - const testOwnership = await testOwnershipCursor.next() - expect(testOwnership).toEqual(undefined) - - const testDmarcSummaryCursor = - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - const testDmarcSummary = await testDmarcSummaryCursor.next() - expect(testDmarcSummary).toEqual(undefined) - - const testDomainsToDmarcSumCursor = - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - const testDomainsToDmarcSum = await testDomainsToDmarcSumCursor.next() - expect(testDomainsToDmarcSum).toEqual(undefined) - }) - }) - describe('org is not the owner of dmarc data', () => { - beforeEach(async () => { - await collections.ownership.save({ - _from: 'organizations/1', - _to: domain._id, - }) - }) - it('does not remove all dmarc summary data', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({ - i18n, - query, - userKey: user._key, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - - const testOwnershipCursor = - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - const testOwnership = await testOwnershipCursor.next() - expect(testOwnership).toBeDefined() - - const testDmarcSummaryCursor = - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - const testDmarcSummary = await testDmarcSummaryCursor.next() - expect(testDmarcSummary).toBeDefined() - - const testDomainsToDmarcSumCursor = - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - const testDomainsToDmarcSum = await testDomainsToDmarcSumCursor.next() - expect(testDomainsToDmarcSum).toBeDefined() - }) - }) - it('removes all scan data', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({i18n, query, userKey: user._key}), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const testWebScanCursor = - await query`FOR wScan IN webScan OPTIONS { waitForSync: true } RETURN wScan.webScan` - const testWebScan = await testWebScanCursor.next() - expect(testWebScan).toEqual(undefined) - - const testDNSCursor = - await query`FOR dnsResult IN dns OPTIONS { waitForSync: true } RETURN dnsResult.dns` - const testDNS = await testDNSCursor.next() - expect(testDNS).toEqual(undefined) - - const testWebCursor = - await query`FOR webResult IN web OPTIONS { waitForSync: true } RETURN webResult.web` - const testWeb = await testWebCursor.next() - expect(testWeb).toEqual(undefined) - }) - it('removes all domain, affiliation, and org data', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({i18n, query, userKey: user._key}), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR org IN organizations OPTIONS { waitForSync: true } RETURN org` - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - - const testOrgCursor = - await query`FOR org IN organizations OPTIONS { waitForSync: true } RETURN org` - const testOrg = await testOrgCursor.next() - expect(testOrg).toEqual(undefined) - - const testDomainCursor = - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - const testDomain = await testDomainCursor.next() - expect(testDomain).toEqual(undefined) - - const testAffiliationCursor = - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toEqual(undefined) - }) describe('users language is set to english', () => { beforeAll(() => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -527,7 +204,7 @@ describe('given a successful leave', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -537,7 +214,7 @@ describe('given a successful leave', () => { userKey: user._key, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -545,17 +222,14 @@ describe('given a successful leave', () => { data: { leaveOrganization: { result: { - status: - 'Successfully left organization: treasury-board-secretariat', + status: 'Successfully left organization: treasury-board-secretariat', }, }, }, } expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully left org: treasury-board-secretariat.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully left org: treasury-board-secretariat.`]) }) }) describe('users language is set to french', () => { @@ -563,8 +237,8 @@ describe('given a successful leave', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -617,7 +291,7 @@ describe('given a successful leave', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -627,7 +301,7 @@ describe('given a successful leave', () => { userKey: user._key, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -635,17 +309,14 @@ describe('given a successful leave', () => { data: { leaveOrganization: { result: { - status: - "L'organisation a été quittée avec succès: treasury-board-secretariat", + status: "L'organisation a été quittée avec succès: treasury-board-secretariat", }, }, }, } expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully left org: treasury-board-secretariat.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully left org: treasury-board-secretariat.`]) }) }) }) @@ -681,35 +352,44 @@ describe('given a successful leave', () => { _to: domain._id, }) }) - describe('org is owner of dmarc data', () => { - beforeEach(async () => { - await collections.ownership.save({ - _from: org._id, - _to: domain._id, + + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, }) }) - it('removes all dmarc summary data', async () => { - await graphql( + it('returns status success message', async () => { + const response = await graphql( schema, ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" + mutation { + leaveOrganization ( + input: { + orgId: "${toGlobalId('organizations', org._key)}" + } + ) { + result { + ... on LeaveOrganizationResult { + status } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } + ... on AffiliationError { + code + description } } } - `, + } + `, null, { i18n, @@ -732,7 +412,7 @@ describe('given a successful leave', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -742,39 +422,41 @@ describe('given a successful leave', () => { userKey: user._key, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - - const testOwnershipCursor = - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - const testOwnership = await testOwnershipCursor.next() - expect(testOwnership).toEqual(undefined) - - const testDmarcSummaryCursor = - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - const testDmarcSummary = await testDmarcSummaryCursor.next() - expect(testDmarcSummary).toEqual(undefined) + const expectedResult = { + data: { + leaveOrganization: { + result: { + status: 'Successfully left organization: treasury-board-secretariat', + }, + }, + }, + } - const testDomainsToDmarcSumCursor = - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - const testDomainsToDmarcSum = await testDomainsToDmarcSumCursor.next() - expect(testDomainsToDmarcSum).toEqual(undefined) + expect(response).toEqual(expectedResult) + expect(consoleOutput).toEqual([`User: ${user._key} successfully left org: treasury-board-secretariat.`]) }) }) - describe('org is not the owner of dmarc data', () => { - beforeEach(async () => { - await collections.ownership.save({ - _from: org2._id, - _to: domain._id, + describe('users language is set to french', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'fr', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, }) }) - it('does not remove all dmarc summary data', async () => { - await graphql( + it('returns status success message', async () => { + const response = await graphql( schema, ` mutation { @@ -817,7 +499,7 @@ describe('given a successful leave', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -827,860 +509,24 @@ describe('given a successful leave', () => { userKey: user._key, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const testOwnershipCursor = - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - const testOwnership = await testOwnershipCursor.next() - expect(testOwnership).toBeDefined() - - const testDmarcSummaryCursor = - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - const testDmarcSummary = await testDmarcSummaryCursor.next() - expect(testDmarcSummary).toBeDefined() + const expectedResult = { + data: { + leaveOrganization: { + result: { + status: "L'organisation a été quittée avec succès: treasury-board-secretariat", + }, + }, + }, + } - const testDomainsToDmarcSumCursor = - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - const testDomainsToDmarcSum = await testDomainsToDmarcSumCursor.next() - expect(testDomainsToDmarcSum).toBeDefined() + expect(response).toEqual(expectedResult) + expect(consoleOutput).toEqual([`User: ${user._key} successfully left org: treasury-board-secretariat.`]) }) }) - it('does not remove all scan data', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({i18n, query, userKey: user._key}), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const testWebScanCursor = - await query`FOR wScan IN webScan OPTIONS { waitForSync: true } RETURN wScan.webScan` - const testWebScan = await testWebScanCursor.next() - expect(testWebScan).toEqual(true) - - const testDNSCursor = - await query`FOR dnsResult IN dns OPTIONS { waitForSync: true } RETURN dnsResult.dns` - const testDNS = await testDNSCursor.next() - expect(testDNS).toEqual(true) - - const testWebCursor = - await query`FOR webResult IN web OPTIONS { waitForSync: true } RETURN webResult.web` - const testWeb = await testWebCursor.next() - expect(testWeb).toEqual(true) - - }) - it('does not remove all domain', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({i18n, query, userKey: user._key}), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const testDomainCursor = - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - const testDomain = await testDomainCursor.next() - expect(testDomain).toBeDefined() - }) - it('removes affiliation, and org data', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({i18n, query, userKey: user._key}), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR org IN organizations OPTIONS { waitForSync: true } RETURN org` - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - - const testOrgCursor = await query` - FOR org IN organizations - OPTIONS { waitForSync: true } - FILTER org.orgDetails.en.slug != "treasury-board-secretariat-2" - RETURN org - ` - const testOrg = await testOrgCursor.next() - expect(testOrg).toEqual(undefined) - - const testAffiliationCursor = - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toEqual(undefined) - }) - describe('users language is set to english', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'en', - localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - it('returns status success message', async () => { - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({ - i18n, - query, - userKey: user._key, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResult = { - data: { - leaveOrganization: { - result: { - status: - 'Successfully left organization: treasury-board-secretariat', - }, - }, - }, - } - - expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully left org: treasury-board-secretariat.`, - ]) - }) - }) - describe('users language is set to french', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'fr', - localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - it('returns status success message', async () => { - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({ - i18n, - query, - userKey: user._key, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResult = { - data: { - leaveOrganization: { - result: { - status: - "L'organisation a été quittée avec succès: treasury-board-secretariat", - }, - }, - }, - } - - expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully left org: treasury-board-secretariat.`, - ]) - }) - }) - }) - }) - describe('user is not an org owner', () => { - beforeEach(async () => { - await collections.affiliations.save({ - _from: org._id, - _to: user._id, - permission: 'admin', - owner: false, - }) - await collections.ownership.save({ - _from: 'organizations/1', - _to: domain._id, - }) - }) - it('does not remove all dmarc summary data', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({ - i18n, - query, - userKey: user._key, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const testOwnershipCursor = - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - const testOwnership = await testOwnershipCursor.next() - expect(testOwnership).toBeDefined() - - const testDmarcSummaryCursor = - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - const testDmarcSummary = await testDmarcSummaryCursor.next() - expect(testDmarcSummary).toBeDefined() - - const testDomainsToDmarcSumCursor = - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - const testDomainsToDmarcSum = await testDomainsToDmarcSumCursor.next() - expect(testDomainsToDmarcSum).toBeDefined() - }) - it('does not remove scan data', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({i18n, query, userKey: user._key}), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const testWebScanCursor = - await query`FOR wScan IN webScan OPTIONS { waitForSync: true } RETURN wScan.webScan` - const testWebScan = await testWebScanCursor.next() - expect(testWebScan).toEqual(true) - - const testDNSCursor = - await query`FOR dnsResult IN dns OPTIONS { waitForSync: true } RETURN dnsResult.dns` - const testDNS = await testDNSCursor.next() - expect(testDNS).toEqual(true) - - const testWebCursor = - await query`FOR webResult IN web OPTIONS { waitForSync: true } RETURN webResult.web` - const testWeb = await testWebCursor.next() - expect(testWeb).toEqual(true) - - }) - it('does not remove org and domain information', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({i18n, query, userKey: user._key}), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const testOrgCursor = - await query`FOR org IN organizations OPTIONS { waitForSync: true } RETURN org` - const testOrg = await testOrgCursor.next() - expect(testOrg).toBeDefined() - - const testDomainCursor = - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - const testDomain = await testDomainCursor.next() - expect(testDomain).toBeDefined() - }) - it('removes affiliation data', async () => { - await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({i18n, query, userKey: user._key}), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - - const testAffiliationCursor = - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toEqual(undefined) - }) - describe('language is set to english', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'en', - localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - it('returns status success message', async () => { - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({i18n, query, userKey: user._key}), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResult = { - data: { - leaveOrganization: { - result: { - status: - 'Successfully left organization: treasury-board-secretariat', - }, - }, - }, - } - - expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully left org: treasury-board-secretariat.`, - ]) - }) - }) - describe('language is set to french', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'fr', - localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - it('returns status success message', async () => { - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({i18n, query, userKey: user._key}), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResult = { - data: { - leaveOrganization: { - result: { - status: - "L'organisation a été quittée avec succès: treasury-board-secretariat", - }, - }, - }, - } - - expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully left org: treasury-board-secretariat.`, - ]) - }) }) }) }) @@ -1699,778 +545,86 @@ describe('given an unsuccessful leave', () => { schema = new GraphQLSchema({ query: createQuerySchema(), mutation: createMutationSchema(), - }) - }) - afterEach(async () => { - consoleOutput.length = 0 - }) - describe('language is set to english', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'en', - localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - describe('org cannot be found', () => { - it('returns an error message', async () => { - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: jest.fn(), - collections: jest.fn(), - transaction: jest.fn(), - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(false), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue(undefined), - }, - }, - validators: {cleanseInput}, - }, - ) - - const expectedResult = { - data: { - leaveOrganization: { - result: { - code: 400, - description: 'Unable to leave undefined organization.', - }, - }, - }, - } - - expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User 123 attempted to leave undefined organization: 123`, - ]) - }) - }) - describe('user is an org owner', () => { - describe('database error occurs', () => { - describe('when querying dmarcSummaryCheck', () => { - it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred.')) - - const mockedTransaction = jest.fn() - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable leave organization. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Database error occurred while checking for dmarc summaries for org: 123, when user: 123 attempted to leave: Error: Database error occurred.`, - ]) - }) - }) - describe('when querying domainInfo', () => { - it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockReturnValueOnce({ - all: jest.fn().mockReturnValue([]), - }) - .mockRejectedValue(new Error('Database error occurred.')) - - const mockedTransaction = jest.fn() - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable leave organization. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Database error occurred while while gathering domainInfo org: 123, when user: 123 attempted to leave: Error: Database error occurred.`, - ]) - }) - }) - }) - describe('cursor error occurs', () => { - describe('when getting dmarc summary info', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockRejectedValue(new Error('Cursor error occurred.')), - }) - - const mockedTransaction = jest.fn() - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable leave organization. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Cursor error occurred when getting dmarc summary info for org: 123, when user: 123 attempted to leave: Error: Cursor error occurred.`, - ]) - }) - }) - describe('when getting domainInfo', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockReturnValueOnce([]) - .mockRejectedValue(new Error('Cursor error occurred.')), - }) - - const mockedTransaction = jest.fn() - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable leave organization. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Cursor error occurred while while gathering domainInfo org: 123, when user: 123 attempted to leave: Error: Cursor error occurred.`, - ]) - }) - }) - }) - describe('transaction step error occurs', () => { - describe('when removing dmarcSummary data', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest.fn().mockReturnValue([{}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable leave organization. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while attempting to remove dmarc summaries for org: 123, when user: 123 attempted to leave: Error: Step error occurred.`, - ]) - }) - }) - describe('when removing ownership data', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest.fn().mockReturnValue([{}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable leave organization. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while attempting to remove ownership for org: 123, when user: 123 attempted to leave: Error: Step error occurred.`, - ]) - }) - }) - describe('when removing web scan result data', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockReturnValueOnce([]) - .mockReturnValue([{count: 1, domain: "test.gc.ca"}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } + }) + }) + afterEach(async () => { + consoleOutput.length = 0 + }) + describe('language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + describe('org cannot be found', () => { + it('returns an error message', async () => { + const response = await graphql( + schema, + ` + mutation { + leaveOrganization ( + input: { + orgId: "${toGlobalId('organizations', 123)}" } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to leave organization. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while user: 123 attempted to remove web data for test.gc.ca in org: undefined, Error: Step error occurred.`, - ]) - }) - }) - describe('when removing scan data', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockReturnValueOnce([]) - .mockReturnValue([{count: 1, domain: "test.gc.ca"}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } + ) { + result { + ... on LeaveOrganizationResult { + status } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to leave organization. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while user: 123 attempted to remove DNS data for test.gc.ca in org: undefined, error: Error: Step error occurred.`, - ]) - }) - }) - describe('when removing domain', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockReturnValueOnce([]) - .mockReturnValue([{count: 1, domain: "test.gc.ca"}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } + ... on AffiliationError { + code + description } } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, + } + } + `, + null, + { + i18n, + query: jest.fn(), + collections: jest.fn(), + transaction: jest.fn(), + userKey: '123', + auth: { + checkOrgOwner: jest.fn().mockReturnValue(false), + userRequired: jest.fn().mockReturnValue({ + _key: '123', + emailValidated: true, + }), + verifiedRequired: verifiedRequired({ i18n }), + }, + loaders: { + loadOrgByKey: { + load: jest.fn().mockReturnValue(undefined), }, - ) - - const error = [ - new GraphQLError('Unable leave organization. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while attempting to remove domains for org: 123, when user: 123 attempted to leave: Error: Step error occurred.`, - ]) - }) - }) - describe('when removing affiliation, and org data', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockReturnValueOnce([]) - .mockReturnValue([{count: 1, domain: "test.gc.ca"}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('Step error occurred.')), - }) + }, + validators: { cleanseInput }, + }, + ) - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, + const expectedResult = { + data: { + leaveOrganization: { + result: { + code: 400, + description: 'Unable to leave undefined organization.', }, - ) - - const error = [ - new GraphQLError('Unable leave organization. Please try again.'), - ] + }, + }, + } - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while attempting to remove affiliations, and the org for org: 123, when user: 123 attempted to leave: Error: Step error occurred.`, - ]) - }) - }) + expect(response).toEqual(expectedResult) + expect(consoleOutput).toEqual([`User 123 attempted to leave undefined organization: 123`]) }) }) describe('user is not an org owner', () => { @@ -2482,9 +636,7 @@ describe('given an unsuccessful leave', () => { }) const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('Step error occurred.')), + step: jest.fn().mockRejectedValue(new Error('Step error occurred.')), }) const response = await graphql( @@ -2512,7 +664,7 @@ describe('given an unsuccessful leave', () => { { i18n, query: mockedQuery, - collections: jest.fn({property: 'string'}), + collections: jest.fn({ property: 'string' }), transaction: mockedTransaction, userKey: '123', auth: { @@ -2521,20 +673,18 @@ describe('given an unsuccessful leave', () => { _key: '123', emailValidated: true, }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), + load: jest.fn().mockReturnValue({ _key: 123 }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError('Unable leave organization. Please try again.'), - ] + const error = [new GraphQLError('Unable leave organization. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2582,7 +732,7 @@ describe('given an unsuccessful leave', () => { { i18n, query: mockedQuery, - collections: jest.fn({property: 'string'}), + collections: jest.fn({ property: 'string' }), transaction: mockedTransaction, userKey: '123', auth: { @@ -2591,20 +741,18 @@ describe('given an unsuccessful leave', () => { _key: '123', emailValidated: true, }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), + load: jest.fn().mockReturnValue({ _key: 123 }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError('Unable leave organization. Please try again.'), - ] + const error = [new GraphQLError('Unable leave organization. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2618,8 +766,8 @@ describe('given an unsuccessful leave', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -2664,14 +812,14 @@ describe('given an unsuccessful leave', () => { _key: '123', emailValidated: true, }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: { load: jest.fn().mockReturnValue(undefined), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -2680,727 +828,14 @@ describe('given an unsuccessful leave', () => { leaveOrganization: { result: { code: 400, - description: - 'Impossible de quitter une organisation non définie.', + description: 'Impossible de quitter une organisation non définie.', }, }, }, } expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User 123 attempted to leave undefined organization: 123`, - ]) - }) - }) - describe('user is an org owner', () => { - describe('database error occurs', () => { - describe('when querying dmarcSummaryCheck', () => { - it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred.')) - - const mockedTransaction = jest.fn() - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Database error occurred while checking for dmarc summaries for org: 123, when user: 123 attempted to leave: Error: Database error occurred.`, - ]) - }) - }) - describe('when querying domainInfo', () => { - it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockReturnValueOnce({ - all: jest.fn().mockReturnValue([]), - }) - .mockRejectedValue(new Error('Database error occurred.')) - - const mockedTransaction = jest.fn() - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Database error occurred while while gathering domainInfo org: 123, when user: 123 attempted to leave: Error: Database error occurred.`, - ]) - }) - }) - }) - describe('cursor error occurs', () => { - describe('when getting dmarc summary info', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockRejectedValue(new Error('Cursor error occurred.')), - }) - - const mockedTransaction = jest.fn() - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Cursor error occurred when getting dmarc summary info for org: 123, when user: 123 attempted to leave: Error: Cursor error occurred.`, - ]) - }) - }) - describe('when getting domainInfo', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockReturnValueOnce([]) - .mockRejectedValue(new Error('Cursor error occurred.')), - }) - - const mockedTransaction = jest.fn() - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Cursor error occurred while while gathering domainInfo org: 123, when user: 123 attempted to leave: Error: Cursor error occurred.`, - ]) - }) - }) - }) - describe('transaction step error occurs', () => { - describe('when removing dmarcSummary data', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest.fn().mockReturnValue([{}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while attempting to remove dmarc summaries for org: 123, when user: 123 attempted to leave: Error: Step error occurred.`, - ]) - }) - }) - describe('when removing ownership data', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest.fn().mockReturnValue([{}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while attempting to remove ownership for org: 123, when user: 123 attempted to leave: Error: Step error occurred.`, - ]) - }) - }) - describe('when removing web result data', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockReturnValueOnce([]) - .mockReturnValue([{count: 1}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while user: 123 attempted to remove web data for undefined in org: undefined, Error: Step error occurred.`, - ]) - }) - }) - describe('when removing scan data', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockReturnValueOnce([]) - .mockReturnValue([{count: 1, domain: "test.gc.ca"}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while user: 123 attempted to remove DNS data for test.gc.ca in org: undefined, error: Error: Step error occurred.`, - ]) - }) - }) - describe('when removing domain', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockReturnValueOnce([]) - .mockReturnValue([{count: 1, domain: "test.gc.ca"}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while attempting to remove domains for org: 123, when user: 123 attempted to leave: Error: Step error occurred.`, - ]) - }) - }) - describe('when removing affiliation, and org data', () => { - it('throws an error', async () => { - const mockedQuery = jest.fn().mockReturnValue({ - all: jest - .fn() - .mockReturnValueOnce([]) - .mockReturnValue([{count: 1, domain: "test.gc.ca"}]), - }) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('Step error occurred.')), - }) - - const response = await graphql( - schema, - ` - mutation { - leaveOrganization ( - input: { - orgId: "${toGlobalId('organizations', 123)}" - } - ) { - result { - ... on LeaveOrganizationResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: jest.fn({property: 'string'}), - transaction: mockedTransaction, - userKey: '123', - auth: { - checkOrgOwner: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - emailValidated: true, - }), - verifiedRequired: verifiedRequired({i18n}), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while attempting to remove affiliations, and the org for org: 123, when user: 123 attempted to leave: Error: Step error occurred.`, - ]) - }) - }) + expect(consoleOutput).toEqual([`User 123 attempted to leave undefined organization: 123`]) }) }) describe('user is not an org owner', () => { @@ -3412,9 +847,7 @@ describe('given an unsuccessful leave', () => { }) const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('Step error occurred.')), + step: jest.fn().mockRejectedValue(new Error('Step error occurred.')), }) const response = await graphql( @@ -3442,7 +875,7 @@ describe('given an unsuccessful leave', () => { { i18n, query: mockedQuery, - collections: jest.fn({property: 'string'}), + collections: jest.fn({ property: 'string' }), transaction: mockedTransaction, userKey: '123', auth: { @@ -3451,22 +884,18 @@ describe('given an unsuccessful leave', () => { _key: '123', emailValidated: true, }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), + load: jest.fn().mockReturnValue({ _key: 123 }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] + const error = [new GraphQLError("Impossible de quitter l'organisation. Veuillez réessayer.")] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -3514,7 +943,7 @@ describe('given an unsuccessful leave', () => { { i18n, query: mockedQuery, - collections: jest.fn({property: 'string'}), + collections: jest.fn({ property: 'string' }), transaction: mockedTransaction, userKey: '123', auth: { @@ -3523,22 +952,18 @@ describe('given an unsuccessful leave', () => { _key: '123', emailValidated: true, }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 123}), + load: jest.fn().mockReturnValue({ _key: 123 }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - "Impossible de quitter l'organisation. Veuillez réessayer.", - ), - ] + const error = [new GraphQLError("Impossible de quitter l'organisation. Veuillez réessayer.")] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ diff --git a/api/src/affiliation/mutations/__tests__/remove-user-from-org.test.js b/api/src/affiliation/mutations/__tests__/remove-user-from-org.test.js index e70015f7a7..04baf71703 100644 --- a/api/src/affiliation/mutations/__tests__/remove-user-from-org.test.js +++ b/api/src/affiliation/mutations/__tests__/remove-user-from-org.test.js @@ -1,26 +1,21 @@ -import {setupI18n} from '@lingui/core' -import {ensure, dbNameFromFile} from 'arango-tools' -import {graphql, GraphQLSchema, GraphQLError} from 'graphql' -import {toGlobalId} from 'graphql-relay' +import { setupI18n } from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import {createQuerySchema} from '../../../query' -import {createMutationSchema} from '../../../mutation' -import {cleanseInput} from '../../../validators' -import { - checkPermission, - userRequired, - verifiedRequired, - tfaRequired, -} from '../../../auth' -import {loadOrgByKey} from '../../../organization/loaders' -import {loadUserByKey} from '../../../user/loaders' -import {loadAffiliationByKey} from '../../loaders' +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import { cleanseInput } from '../../../validators' +import { checkPermission, userRequired, verifiedRequired, tfaRequired } from '../../../auth' +import { loadOrgByKey } from '../../../organization/loaders' +import { loadUserByKey } from '../../../user/loaders' +import { loadAffiliationByKey } from '../../loaders' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env const orgOneData = { verified: true, @@ -117,18 +112,7 @@ const userData = { } describe('given the removeUserFromOrg mutation', () => { - let query, - drop, - truncate, - schema, - collections, - transaction, - i18n, - user, - orgOne, - orgTwo, - admin, - affiliation + let query, drop, truncate, schema, collections, transaction, i18n, user, orgOne, orgTwo, admin, affiliation const consoleOutput = [] const mockedInfo = (output) => consoleOutput.push(output) @@ -146,8 +130,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -162,7 +146,7 @@ describe('given the removeUserFromOrg mutation', () => { describe('given a successful mutation', () => { beforeEach(async () => { - ;({query, drop, truncate, collections, transaction} = await ensure({ + ;({ query, drop, truncate, collections, transaction } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -245,8 +229,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -261,7 +245,7 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -273,18 +257,18 @@ describe('given the removeUserFromOrg mutation', () => { const data = await loader.load(affiliation._key) - expect(data).toEqual(undefined) expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) + expect(data).toEqual(undefined) }) describe('users language is set to english', () => { beforeAll(() => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -342,8 +326,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -358,7 +342,7 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -376,10 +360,10 @@ describe('given the removeUserFromOrg mutation', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) + expect(response).toEqual(expectedResponse) }) }) describe('users language is set to french', () => { @@ -387,8 +371,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -446,8 +430,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -462,7 +446,7 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -470,8 +454,7 @@ describe('given the removeUserFromOrg mutation', () => { data: { removeUserFromOrg: { result: { - status: - "L'utilisateur a été retiré de l'organisation avec succès.", + status: "L'utilisateur a été retiré de l'organisation avec succès.", user: { id: toGlobalId('user', user._key), userName: 'test.account@istio.actually.exists', @@ -481,15 +464,15 @@ describe('given the removeUserFromOrg mutation', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) + expect(response).toEqual(expectedResponse) }) }) }) describe('super admin can remove a user from any org', () => { - it.skip('removes the user from the org', async () => { + it('removes the user from the org', async () => { await graphql( schema, ` @@ -538,8 +521,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -554,7 +537,7 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -566,18 +549,18 @@ describe('given the removeUserFromOrg mutation', () => { const data = await loader.load(affiliation._key) - expect(data).toEqual(undefined) expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) + expect(data).toEqual(undefined) }) describe('users language is set to english', () => { beforeAll(() => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -635,8 +618,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -651,7 +634,7 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -669,10 +652,10 @@ describe('given the removeUserFromOrg mutation', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) + expect(response).toEqual(expectedResponse) }) }) describe('users language is set to french', () => { @@ -680,8 +663,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -739,8 +722,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -755,7 +738,7 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -763,8 +746,7 @@ describe('given the removeUserFromOrg mutation', () => { data: { removeUserFromOrg: { result: { - status: - "L'utilisateur a été retiré de l'organisation avec succès.", + status: "L'utilisateur a été retiré de l'organisation avec succès.", user: { id: toGlobalId('user', user._key), userName: 'test.account@istio.actually.exists', @@ -774,10 +756,10 @@ describe('given the removeUserFromOrg mutation', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) + expect(response).toEqual(expectedResponse) }) }) }) @@ -849,8 +831,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -865,7 +847,7 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -877,18 +859,18 @@ describe('given the removeUserFromOrg mutation', () => { const data = await loader.load(affiliation._key) - expect(data).toEqual(undefined) expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) + expect(data).toEqual(undefined) }) describe('users language is set to english', () => { beforeAll(() => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -946,8 +928,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -962,7 +944,7 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -980,10 +962,10 @@ describe('given the removeUserFromOrg mutation', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) + expect(response).toEqual(expectedResponse) }) }) describe('users language is set to french', () => { @@ -991,8 +973,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -1050,8 +1032,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -1066,7 +1048,7 @@ describe('given the removeUserFromOrg mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -1074,8 +1056,7 @@ describe('given the removeUserFromOrg mutation', () => { data: { removeUserFromOrg: { result: { - status: - "L'utilisateur a été retiré de l'organisation avec succès.", + status: "L'utilisateur a été retiré de l'organisation avec succès.", user: { id: toGlobalId('user', user._key), userName: 'test.account@istio.actually.exists', @@ -1085,10 +1066,10 @@ describe('given the removeUserFromOrg mutation', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) + expect(response).toEqual(expectedResponse) }) }) }) @@ -1099,8 +1080,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -1161,7 +1142,7 @@ describe('given the removeUserFromOrg mutation', () => { load: jest.fn(), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -1170,17 +1151,16 @@ describe('given the removeUserFromOrg mutation', () => { removeUserFromOrg: { result: { code: 400, - description: - 'Unable to remove user from unknown organization.', + description: 'Unable to remove user from unknown organization.', }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to remove user: 456 from org: 12345, however no org with that id could be found.`, ]) + expect(response).toEqual(error) }) }) describe('requesting user is not an admin', () => { @@ -1236,7 +1216,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -1244,7 +1224,7 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -1253,16 +1233,16 @@ describe('given the removeUserFromOrg mutation', () => { removeUserFromOrg: { result: { code: 400, - description: 'Unable to remove user from organization.', + description: 'Permission Denied: Please contact organization admin for help with removing users.', }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `User: 123 attempted to remove user: 456 from org: 12345, however they do not have the permission to remove users.`, + `User: 123 attempted to remove user: 456 from org: 12345, but they do not have the right permission.`, ]) + expect(response).toEqual(error) }) }) describe('requesting user is an admin for another org', () => { @@ -1318,7 +1298,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -1326,7 +1306,7 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -1335,16 +1315,16 @@ describe('given the removeUserFromOrg mutation', () => { removeUserFromOrg: { result: { code: 400, - description: 'Unable to remove user from organization.', + description: 'Permission Denied: Please contact organization admin for help with removing users.', }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `User: 123 attempted to remove user: 456 from org: 12345, however they do not have the permission to remove users.`, + `User: 123 attempted to remove user: 456 from org: 12345, but they do not have the right permission.`, ]) + expect(response).toEqual(error) }) }) describe('requested user is not found', () => { @@ -1400,13 +1380,13 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue(undefined), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -1415,17 +1395,16 @@ describe('given the removeUserFromOrg mutation', () => { removeUserFromOrg: { result: { code: 400, - description: - 'Unable to remove unknown user from organization.', + description: 'Unable to remove unknown user from organization.', }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to remove user: 456 from org: 12345, however no user with that id could be found.`, ]) + expect(response).toEqual(error) }) }) describe('requested user does not belong to this org', () => { @@ -1478,7 +1457,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -1486,7 +1465,7 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -1495,192 +1474,23 @@ describe('given the removeUserFromOrg mutation', () => { removeUserFromOrg: { result: { code: 400, - description: - 'Unable to remove a user that already does not belong to this organization.', + description: 'Unable to remove a user that already does not belong to this organization.', }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to remove user: 456, but they do not have any affiliations to org: 12345.`, ]) - }) - }) - describe('super admin attempts to remove another super admin', () => { - it('returns an error message', async () => { - const mockedCursor = { - count: 1, - next: jest.fn().mockReturnValue({ - permission: 'super_admin', - }), - } - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const response = await graphql( - schema, - ` - mutation { - removeUserFromOrg ( - input: { - userId: "${toGlobalId('users', 456)}" - orgId: "${toGlobalId('organizations', 12345)}" - } - ) { - result { - ... on RemoveUserFromOrgResult { - status - user { - id - userName - } - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction, - userKey: 123, - auth: { - checkPermission: jest.fn().mockReturnValue('super_admin'), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - }), - verifiedRequired: jest.fn(), - tfaRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), - }, - loadUserByKey: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = { - data: { - removeUserFromOrg: { - result: { - code: 400, - description: - 'Permission Denied: Please contact organization admin for help with removing users.', - }, - }, - }, - } - - expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to remove user: 456 from org: 12345, but they do not have the right permission.`, - ]) - }) - }) - describe('admin attempts to remove another admin', () => { - it('returns an error message', async () => { - const mockedCursor = { - count: 1, - next: jest.fn().mockReturnValue({ - permission: 'admin', - }), - } - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const response = await graphql( - schema, - ` - mutation { - removeUserFromOrg ( - input: { - userId: "${toGlobalId('users', 456)}" - orgId: "${toGlobalId('organizations', 12345)}" - } - ) { - result { - ... on RemoveUserFromOrgResult { - status - user { - id - userName - } - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction, - userKey: 123, - auth: { - checkPermission: jest.fn().mockReturnValue('admin'), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - }), - verifiedRequired: jest.fn(), - tfaRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), - }, - loadUserByKey: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = { - data: { - removeUserFromOrg: { - result: { - code: 400, - description: - 'Permission Denied: Please contact organization admin for help with removing users.', - }, - }, - }, - } - expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to remove user: 456 from org: 12345, but they do not have the right permission.`, - ]) }) }) }) describe('database error occurs', () => { describe('when checking requested users permission in requested org', () => { it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('database error')) + const mockedQuery = jest.fn().mockRejectedValue(new Error('database error')) const response = await graphql( schema, @@ -1725,7 +1535,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -1733,20 +1543,16 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Unable to remove user from this organization. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to remove user from this organization. Please try again.')] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Database error occurred when user: 123 attempted to check the current permission of user: 456 to see if they could be removed: Error: database error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -1802,7 +1608,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -1810,20 +1616,16 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Unable to remove user from this organization. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to remove user from this organization. Please try again.')] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Cursor error occurred when user: 123 attempted to check the current permission of user: 456 to see if they could be removed: Error: cursor error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -1832,7 +1634,7 @@ describe('given the removeUserFromOrg mutation', () => { it('throws an error', async () => { const mockedCursor = { count: 1, - next: jest.fn().mockReturnValue({permission: 'user'}), + next: jest.fn().mockReturnValue({ permission: 'user' }), } const mockedQuery = jest.fn().mockReturnValue(mockedCursor) @@ -1883,7 +1685,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -1891,20 +1693,16 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Unable to remove user from this organization. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to remove user from this organization. Please try again.')] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Trx step error occurred when user: 123 attempted to remove user: 456 from org: 12345, error: Error: trx step error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -1912,7 +1710,7 @@ describe('given the removeUserFromOrg mutation', () => { it('throws an error', async () => { const mockedCursor = { count: 1, - next: jest.fn().mockReturnValue({permission: 'user'}), + next: jest.fn().mockReturnValue({ permission: 'user' }), } const mockedQuery = jest.fn().mockReturnValue(mockedCursor) @@ -1964,7 +1762,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -1972,20 +1770,16 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Unable to remove user from this organization. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to remove user from this organization. Please try again.')] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Trx commit error occurred when user: 123 attempted to remove user: 456 from org: 12345, error: Error: trx commit error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -1994,8 +1788,8 @@ describe('given the removeUserFromOrg mutation', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -2056,7 +1850,7 @@ describe('given the removeUserFromOrg mutation', () => { load: jest.fn(), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -2065,17 +1859,16 @@ describe('given the removeUserFromOrg mutation', () => { removeUserFromOrg: { result: { code: 400, - description: - "Impossible de supprimer un utilisateur d'une organisation inconnue.", + description: "Impossible de supprimer un utilisateur d'une organisation inconnue.", }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to remove user: 456 from org: 12345, however no org with that id could be found.`, ]) + expect(response).toEqual(error) }) }) describe('requesting user is not an admin', () => { @@ -2131,7 +1924,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -2139,7 +1932,7 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -2149,16 +1942,16 @@ describe('given the removeUserFromOrg mutation', () => { result: { code: 400, description: - "Impossible de supprimer un utilisateur de l'organisation.", + "Autorisation refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des utilisateurs.", }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `User: 123 attempted to remove user: 456 from org: 12345, however they do not have the permission to remove users.`, + `User: 123 attempted to remove user: 456 from org: 12345, but they do not have the right permission.`, ]) + expect(response).toEqual(error) }) }) describe('requesting user is an admin for another org', () => { @@ -2214,7 +2007,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -2222,7 +2015,7 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -2232,16 +2025,16 @@ describe('given the removeUserFromOrg mutation', () => { result: { code: 400, description: - "Impossible de supprimer un utilisateur de l'organisation.", + "Autorisation refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des utilisateurs.", }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `User: 123 attempted to remove user: 456 from org: 12345, however they do not have the permission to remove users.`, + `User: 123 attempted to remove user: 456 from org: 12345, but they do not have the right permission.`, ]) + expect(response).toEqual(error) }) }) describe('requested user is not found', () => { @@ -2297,13 +2090,13 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue(undefined), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -2312,17 +2105,16 @@ describe('given the removeUserFromOrg mutation', () => { removeUserFromOrg: { result: { code: 400, - description: - "Impossible de supprimer un utilisateur inconnu de l'organisation.", + description: "Impossible de supprimer un utilisateur inconnu de l'organisation.", }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to remove user: 456 from org: 12345, however no user with that id could be found.`, ]) + expect(response).toEqual(error) }) }) describe('requested user does not belong to this org', () => { @@ -2375,7 +2167,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -2383,7 +2175,7 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -2399,185 +2191,17 @@ describe('given the removeUserFromOrg mutation', () => { }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to remove user: 456, but they do not have any affiliations to org: 12345.`, ]) - }) - }) - describe('super admin attempts to remove another super admin', () => { - it('returns an error message', async () => { - const mockedCursor = { - count: 1, - next: jest.fn().mockReturnValue({ - permission: 'super_admin', - }), - } - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const response = await graphql( - schema, - ` - mutation { - removeUserFromOrg ( - input: { - userId: "${toGlobalId('users', 456)}" - orgId: "${toGlobalId('organizations', 12345)}" - } - ) { - result { - ... on RemoveUserFromOrgResult { - status - user { - id - userName - } - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction, - userKey: 123, - auth: { - checkPermission: jest.fn().mockReturnValue('super_admin'), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - }), - verifiedRequired: jest.fn(), - tfaRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), - }, - loadUserByKey: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = { - data: { - removeUserFromOrg: { - result: { - code: 400, - description: - "Autorisation refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des utilisateurs.", - }, - }, - }, - } - - expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to remove user: 456 from org: 12345, but they do not have the right permission.`, - ]) - }) - }) - describe('admin attempts to remove another admin', () => { - it('returns an error message', async () => { - const mockedCursor = { - count: 1, - next: jest.fn().mockReturnValue({ - permission: 'admin', - }), - } - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const response = await graphql( - schema, - ` - mutation { - removeUserFromOrg ( - input: { - userId: "${toGlobalId('users', 456)}" - orgId: "${toGlobalId('organizations', 12345)}" - } - ) { - result { - ... on RemoveUserFromOrgResult { - status - user { - id - userName - } - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction, - userKey: 123, - auth: { - checkPermission: jest.fn().mockReturnValue('admin'), - userRequired: jest.fn().mockReturnValue({ - _key: '123', - }), - verifiedRequired: jest.fn(), - tfaRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), - }, - loadUserByKey: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = { - data: { - removeUserFromOrg: { - result: { - code: 400, - description: - "Autorisation refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des utilisateurs.", - }, - }, - }, - } - expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to remove user: 456 from org: 12345, but they do not have the right permission.`, - ]) }) }) }) describe('database error occurs', () => { describe('when checking requested users permission in requested org', () => { it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('database error')) + const mockedQuery = jest.fn().mockRejectedValue(new Error('database error')) const response = await graphql( schema, @@ -2622,7 +2246,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -2630,20 +2254,18 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) const error = [ - new GraphQLError( - "Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer.", - ), + new GraphQLError("Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer."), ] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Database error occurred when user: 123 attempted to check the current permission of user: 456 to see if they could be removed: Error: database error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -2699,7 +2321,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -2707,20 +2329,18 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) const error = [ - new GraphQLError( - "Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer.", - ), + new GraphQLError("Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer."), ] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Cursor error occurred when user: 123 attempted to check the current permission of user: 456 to see if they could be removed: Error: cursor error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -2729,7 +2349,7 @@ describe('given the removeUserFromOrg mutation', () => { it('throws an error', async () => { const mockedCursor = { count: 1, - next: jest.fn().mockReturnValue({permission: 'user'}), + next: jest.fn().mockReturnValue({ permission: 'user' }), } const mockedQuery = jest.fn().mockReturnValue(mockedCursor) @@ -2780,7 +2400,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -2788,20 +2408,18 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) const error = [ - new GraphQLError( - "Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer.", - ), + new GraphQLError("Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer."), ] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Trx step error occurred when user: 123 attempted to remove user: 456 from org: 12345, error: Error: trx step error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -2809,7 +2427,7 @@ describe('given the removeUserFromOrg mutation', () => { it('throws an error', async () => { const mockedCursor = { count: 1, - next: jest.fn().mockReturnValue({permission: 'user'}), + next: jest.fn().mockReturnValue({ permission: 'user' }), } const mockedQuery = jest.fn().mockReturnValue(mockedCursor) @@ -2861,7 +2479,7 @@ describe('given the removeUserFromOrg mutation', () => { }, loaders: { loadOrgByKey: { - load: jest.fn().mockReturnValue({_key: 12345}), + load: jest.fn().mockReturnValue({ _key: 12345 }), }, loadUserByKey: { load: jest.fn().mockReturnValue({ @@ -2869,20 +2487,18 @@ describe('given the removeUserFromOrg mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) const error = [ - new GraphQLError( - "Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer.", - ), + new GraphQLError("Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer."), ] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Trx commit error occurred when user: 123 attempted to remove user: 456 from org: 12345, error: Error: trx commit error`, ]) + expect(response.errors).toEqual(error) }) }) }) diff --git a/api/src/affiliation/mutations/__tests__/request-org-affiliation.test.js b/api/src/affiliation/mutations/__tests__/request-org-affiliation.test.js new file mode 100644 index 0000000000..9f9bcbe11d --- /dev/null +++ b/api/src/affiliation/mutations/__tests__/request-org-affiliation.test.js @@ -0,0 +1,620 @@ +import { ensure, dbNameFromFile } from 'arango-tools' +import { setupI18n } from '@lingui/core' +import { graphql, GraphQLSchema } from 'graphql' +import { toGlobalId } from 'graphql-relay' + +import englishMessages from '../../../locale/en/messages' +import frenchMessages from '../../../locale/fr/messages' +import { userRequired, verifiedRequired } from '../../../auth' +import { createMutationSchema } from '../../../mutation' +import { createQuerySchema } from '../../../query' +import { cleanseInput } from '../../../validators' +import { loadOrgByKey } from '../../../organization/loaders' +import { loadUserByKey } from '../../../user/loaders' +import dbschema from '../../../../database.json' +import { collectionNames } from '../../../collection-names' + +const { DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY } = process.env + +describe('invite user to org', () => { + let query, drop, truncate, schema, collections, transaction, i18n, tokenize, user, org + + const consoleOutput = [] + const mockedInfo = (output) => consoleOutput.push(output) + const mockedWarn = (output) => consoleOutput.push(output) + const mockedError = (output) => consoleOutput.push(output) + beforeAll(async () => { + console.info = mockedInfo + console.warn = mockedWarn + console.error = mockedError + // Create GQL Schema + schema = new GraphQLSchema({ + query: createQuerySchema(), + mutation: createMutationSchema(), + }) + tokenize = jest.fn().mockReturnValue('token') + }) + afterEach(() => { + consoleOutput.length = 0 + }) + + // given a successful request to join an org + describe('given a successful request to join an org', () => { + beforeAll(async () => { + ;({ query, drop, truncate, collections, transaction } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + tokenize = jest.fn().mockReturnValue('token') + }) + beforeEach(async () => { + user = await collections.users.save({ + userName: 'test.account@istio.actually.exists', + emailValidated: true, + tfaSendMethod: 'email', + }) + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + beforeEach(async () => { + org = await ( + await collections.organizations.save( + { + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }, + { returnNew: true }, + ) + ).new + }) + describe('users role is super admin', () => { + describe('inviting an existing account', () => { + describe('requested role is admin', () => { + let secondaryUser + beforeEach(async () => { + secondaryUser = await collections.users.save({ + displayName: 'Test Account', + userName: 'test@email.gc.ca', + preferredLang: 'english', + }) + await collections.affiliations.save({ + _from: org._id, + _to: secondaryUser._id, + permission: 'admin', + }) + }) + it('returns status message', async () => { + const sendInviteRequestEmail = jest.fn() + + const response = await graphql( + schema, + ` + mutation { + requestOrgAffiliation(input: { orgId: "${toGlobalId('organizations', org._key)}" }) { + result { + ... on InviteUserToOrgResult { + status + } + ... on AffiliationError { + code + description + } + } + } + } + `, + null, + { + i18n, + request: { + language: 'en', + protocol: 'https', + get: (text) => text, + }, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + tokenize, + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({ i18n }), + }, + loaders: { + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + }, + notify: { sendInviteRequestEmail: sendInviteRequestEmail }, + validators: { cleanseInput }, + }, + ) + + const expectedResponse = { + data: { + requestOrgAffiliation: { + result: { + status: 'Successfully requested invite to organization, and sent notification email.', + }, + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${user._key} successfully requested invite to the org: treasury-board-secretariat.`, + ]) + expect(sendInviteRequestEmail).toHaveBeenCalledWith({ + user: { + _type: 'user', + displayName: 'Test Account', + id: secondaryUser._key, + preferredLang: 'english', + userName: 'test@email.gc.ca', + ...secondaryUser, + }, + orgName: 'Treasury Board of Canada Secretariat', + adminLink: 'https://host/admin/organizations', + }) + }) + }) + }) + }) + }) + }) + describe('given an unsuccessful invitation', () => { + beforeAll(async () => { + ;({ query, drop, truncate, collections, transaction } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + tokenize = jest.fn().mockReturnValue('token') + }) + beforeEach(async () => { + user = ( + await collections.users.save( + { + userName: 'test.account@istio.actually.exists', + emailValidated: true, + tfaSendMethod: 'email', + }, + { returnNew: true }, + ) + ).new + org = ( + await collections.organizations.save( + { + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }, + { returnNew: true }, + ) + ).new + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + describe('user attempts to request an invite to an org that does not exist', () => { + it('returns an error message', async () => { + const response = await graphql( + schema, + ` + mutation { + requestOrgAffiliation(input: { orgId: "${toGlobalId('organizations', 1)}" }) { + result { + ... on InviteUserToOrgResult { + status + } + ... on AffiliationError { + code + description + } + } + } + } + `, + null, + { + i18n, + request: { + language: 'fr', + protocol: 'https', + get: (text) => text, + }, + query, + collections: collectionNames, + transaction, + userKey: 123, + auth: { + tokenize, + userRequired: jest.fn().mockReturnValue({ + userName: 'test.account@exists.ca', + }), + verifiedRequired: jest.fn(), + }, + loaders: { + loadOrgByKey: { + load: jest.fn().mockReturnValue(undefined), + }, + loadUserByKey: { + load: jest.fn(), + }, + }, + notify: { sendInviteRequestEmail: jest.fn() }, + validators: { cleanseInput }, + }, + ) + + const error = { + data: { + requestOrgAffiliation: { + result: { + code: 400, + description: 'Unable to request invite to unknown organization.', + }, + }, + }, + } + + expect(response).toEqual(error) + expect(consoleOutput).toEqual([ + `User: 123 attempted to request invite to org: 1 however there is no org associated with that id.`, + ]) + }) + }) + describe('user has already requested to join org', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: org._id, + _to: user._id, + permission: 'pending', + }) + }) + it('returns an error message', async () => { + const response = await graphql( + schema, + ` + mutation { + requestOrgAffiliation(input: { orgId: "${toGlobalId('organizations', org._key)}" }) { + result { + ... on InviteUserToOrgResult { + status + } + ... on AffiliationError { + code + description + } + } + } + } + `, + null, + { + i18n, + request: { + language: 'en', + }, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + tokenize, + userRequired: jest.fn().mockReturnValue({ + _id: user._id, + userName: 'test.account@istio.actually.exists', + }), + verifiedRequired: jest.fn(), + }, + loaders: { + loadOrgByKey: { + load: jest.fn().mockReturnValue({ _id: org._id }), + }, + loadUserByKey: { + load: jest.fn(), + }, + }, + notify: { sendInviteRequestEmail: jest.fn() }, + validators: { cleanseInput }, + }, + ) + + const error = { + data: { + requestOrgAffiliation: { + result: { + code: 400, + description: + 'Unable to request invite to organization with which you have already requested to join.', + }, + }, + }, + } + + expect(response).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${user._key} attempted to request invite to org: ${org._key} however they have already requested to join that org.`, + ]) + }) + }) + describe('user is already a member of org', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: org._id, + _to: user._id, + permission: 'user', + }) + }) + it('returns an error message', async () => { + const response = await graphql( + schema, + ` + mutation { + requestOrgAffiliation(input: { orgId: "${toGlobalId('organizations', org._key)}" }) { + result { + ... on InviteUserToOrgResult { + status + } + ... on AffiliationError { + code + description + } + } + } + } + `, + null, + { + i18n, + request: { + language: 'en', + }, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + tokenize, + userRequired: jest.fn().mockReturnValue({ + _id: user._id, + userName: 'test.account@istio.actually.exists', + }), + verifiedRequired: jest.fn(), + }, + loaders: { + loadOrgByKey: { + load: jest.fn().mockReturnValue({ _id: org._id }), + }, + loadUserByKey: { + load: jest.fn(), + }, + }, + notify: { sendInviteRequestEmail: jest.fn() }, + validators: { cleanseInput }, + }, + ) + + const error = { + data: { + requestOrgAffiliation: { + result: { + code: 400, + description: 'Unable to request invite to organization with which you are already affiliated.', + }, + }, + }, + } + + expect(response).toEqual(error) + expect(consoleOutput).toEqual([ + `User: ${user._key} attempted to request invite to org: ${org._key} however they are already affiliated with that org.`, + ]) + }) + }) + }) + describe('transaction error occurs', () => { + describe('when creating affiliation', () => { + it('returns an error message', async () => { + await graphql( + schema, + ` + mutation { + requestOrgAffiliation(input: { orgId: "${toGlobalId('organizations', org._key)}" }) { + result { + ... on InviteUserToOrgResult { + status + } + ... on AffiliationError { + code + description + } + } + } + } + `, + null, + { + i18n, + request: { + language: 'fr', + protocol: 'https', + get: (text) => text, + }, + query, + collections: collectionNames, + transaction: jest.fn().mockReturnValue({ + step: jest.fn().mockRejectedValue('trx step err'), + }), + userKey: 123, + auth: { + tokenize, + userRequired: jest.fn().mockReturnValue({ + _id: user._id, + userName: 'test.account@istio.actually.exists', + }), + verifiedRequired: jest.fn(), + }, + loaders: { + loadOrgByKey: loadOrgByKey({ query, language: i18n.locale }), + loadUserByKey: loadUserByKey({ query }), + }, + notify: { sendInviteRequestEmail: jest.fn() }, + validators: { cleanseInput }, + }, + ) + + expect(consoleOutput).toEqual([ + `Transaction step error occurred while user: 123 attempted to request invite to org: treasury-board-secretariat, error: trx step err`, + ]) + }) + }) + describe('when committing transaction', () => { + it('returns an error message', async () => { + await graphql( + schema, + ` mutation { + requestOrgAffiliation(input: { orgId: "${toGlobalId('organizations', org._key)}" }) { + result { + ... on InviteUserToOrgResult { + status + } + ... on AffiliationError { + code + description + } + } + } + } + `, + null, + { + i18n, + request: { + language: 'fr', + protocol: 'https', + get: (text) => text, + }, + query, + collections: collectionNames, + transaction: jest.fn().mockReturnValue({ + step: jest.fn().mockRejectedValue('trx commit err'), + }), + userKey: 123, + auth: { + tokenize, + userRequired: jest.fn().mockReturnValue({ + _id: user._id, + userName: 'test.account@istio.actually.exists', + }), + verifiedRequired: jest.fn(), + }, + loaders: { + loadOrgByKey: loadOrgByKey({ query, language: i18n.locale }), + loadUserByKey: loadUserByKey({ query }), + }, + notify: { sendInviteRequestEmail: jest.fn() }, + validators: { cleanseInput }, + }, + ) + + expect(consoleOutput).toEqual([ + `Transaction step error occurred while user: 123 attempted to request invite to org: treasury-board-secretariat, error: trx commit err`, + ]) + }) + }) + }) + }) +}) diff --git a/api/src/affiliation/mutations/__tests__/transfer-org-ownership.test.js b/api/src/affiliation/mutations/__tests__/transfer-org-ownership.test.js index 00db8589b8..bc3cbcf89c 100644 --- a/api/src/affiliation/mutations/__tests__/transfer-org-ownership.test.js +++ b/api/src/affiliation/mutations/__tests__/transfer-org-ownership.test.js @@ -1,32 +1,23 @@ -import {setupI18n} from '@lingui/core' -import {ensure, dbNameFromFile} from 'arango-tools' -import {graphql, GraphQLSchema, GraphQLError} from 'graphql' -import {toGlobalId} from 'graphql-relay' +import { setupI18n } from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import {checkOrgOwner, userRequired, verifiedRequired} from '../../../auth' -import {loadOrgByKey} from '../../../organization/loaders' -import {loadUserByKey} from '../../../user/loaders' -import {cleanseInput} from '../../../validators' -import {createMutationSchema} from '../../../mutation' -import {createQuerySchema} from '../../../query' +import { checkOrgOwner, userRequired, verifiedRequired } from '../../../auth' +import { loadOrgByKey } from '../../../organization/loaders' +import { loadUserByKey } from '../../../user/loaders' +import { cleanseInput } from '../../../validators' +import { createMutationSchema } from '../../../mutation' +import { createQuerySchema } from '../../../query' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' -const {DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY} = process.env +const { DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY } = process.env describe('given the transferOrgOwnership mutation', () => { - let query, - drop, - truncate, - schema, - collections, - transaction, - i18n, - user, - user2, - org + let query, drop, truncate, schema, collections, transaction, i18n, user, user2, org const consoleOutput = [] const mockedInfo = (output) => consoleOutput.push(output) @@ -44,8 +35,8 @@ describe('given the transferOrgOwnership mutation', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -59,7 +50,7 @@ describe('given the transferOrgOwnership mutation', () => { }) describe('given a successful transfer', () => { beforeAll(async () => { - ;({query, drop, truncate, collections, transaction} = await ensure({ + ;({ query, drop, truncate, collections, transaction } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -108,14 +99,12 @@ describe('given the transferOrgOwnership mutation', () => { await collections.affiliations.save({ _from: org._id, _to: user._id, - permission: 'admin', - owner: true, + permission: 'owner', }) await collections.affiliations.save({ _from: org._id, _to: user2._id, permission: 'admin', - owner: false, }) }) afterEach(async () => { @@ -170,7 +159,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -179,9 +168,9 @@ describe('given the transferOrgOwnership mutation', () => { i18n, userKey: user._key, }), - loadUserByKey: loadUserByKey({query, userKey: user._key, i18n}), + loadUserByKey: loadUserByKey({ query, userKey: user._key, i18n }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -191,7 +180,7 @@ describe('given the transferOrgOwnership mutation', () => { RETURN aff ` const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toMatchObject({owner: false}) + expect(testAffiliation).toMatchObject({ permission: 'admin' }) }) it('sets owner field in the requested users to true', async () => { await graphql( @@ -238,7 +227,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -247,9 +236,9 @@ describe('given the transferOrgOwnership mutation', () => { i18n, userKey: user._key, }), - loadUserByKey: loadUserByKey({query, userKey: user._key, i18n}), + loadUserByKey: loadUserByKey({ query, userKey: user._key, i18n }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -259,14 +248,14 @@ describe('given the transferOrgOwnership mutation', () => { RETURN aff ` const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toMatchObject({owner: true}) + expect(testAffiliation).toMatchObject({ permission: 'owner' }) }) describe('users language is set to english', () => { beforeAll(() => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, + en: { plurals: {} }, }, locales: ['en'], messages: { @@ -319,7 +308,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -334,7 +323,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -360,7 +349,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n = setupI18n({ locale: 'fr', localeData: { - fr: {plurals: {}}, + fr: { plurals: {} }, }, locales: ['fr'], messages: { @@ -413,7 +402,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n, }), }), - verifiedRequired: verifiedRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -428,7 +417,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -457,8 +446,8 @@ describe('given the transferOrgOwnership mutation', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -519,7 +508,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -528,94 +517,14 @@ describe('given the transferOrgOwnership mutation', () => { transferOrgOwnership: { result: { code: 400, - description: - 'Unable to transfer ownership of undefined organization.', + description: 'Unable to transfer ownership of undefined organization.', }, }, }, } expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User: 123 attempted to transfer org ownership of an undefined org.`, - ]) - }) - }) - describe('requested org is verified', () => { - it('returns an error', async () => { - const response = await graphql( - schema, - ` - mutation { - transferOrgOwnership ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - userId: "${toGlobalId('users', user2._key)}" - } - ) { - result { - ... on TransferOrgOwnershipResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({ - i18n, - query, - userKey: user._key, - }), - userRequired: jest.fn().mockReturnValue({ - _key: 123, - }), - verifiedRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - verified: true, - slug: 'mocked-org', - }), - }, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResult = { - data: { - transferOrgOwnership: { - result: { - code: 400, - description: - 'Unable to transfer ownership of a verified organization.', - }, - }, - }, - } - - expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User: 123 attempted to transfer ownership of a verified org: mocked-org.`, - ]) + expect(consoleOutput).toEqual([`User: 123 attempted to transfer org ownership of an undefined org.`]) }) }) describe('requesting user is not the org owner', () => { @@ -669,7 +578,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -678,8 +587,7 @@ describe('given the transferOrgOwnership mutation', () => { transferOrgOwnership: { result: { code: 400, - description: - 'Permission Denied: Please contact org owner to transfer ownership.', + description: 'Permission Denied: Please contact org owner to transfer ownership.', }, }, }, @@ -740,7 +648,7 @@ describe('given the transferOrgOwnership mutation', () => { load: jest.fn().mockReturnValue(undefined), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -749,8 +657,7 @@ describe('given the transferOrgOwnership mutation', () => { transferOrgOwnership: { result: { code: 400, - description: - 'Unable to transfer ownership of an org to an undefined user.', + description: 'Unable to transfer ownership of an org to an undefined user.', }, }, }, @@ -789,7 +696,7 @@ describe('given the transferOrgOwnership mutation', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 0}), + query: jest.fn().mockReturnValue({ count: 0 }), collections: collectionNames, transaction, userKey: user._key, @@ -813,7 +720,7 @@ describe('given the transferOrgOwnership mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -887,15 +794,11 @@ describe('given the transferOrgOwnership mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Unable to transfer organization ownership. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to transfer organization ownership. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -936,7 +839,7 @@ describe('given the transferOrgOwnership mutation', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 1}), + query: jest.fn().mockReturnValue({ count: 1 }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -960,15 +863,11 @@ describe('given the transferOrgOwnership mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Unable to transfer organization ownership. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to transfer organization ownership. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -979,10 +878,7 @@ describe('given the transferOrgOwnership mutation', () => { describe('when adding ownership to requested user', () => { it('throws an error', async () => { const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('Step Error')), + step: jest.fn().mockReturnValueOnce().mockRejectedValue(new Error('Step Error')), }) const response = await graphql( @@ -1010,7 +906,7 @@ describe('given the transferOrgOwnership mutation', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 1}), + query: jest.fn().mockReturnValue({ count: 1 }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -1034,15 +930,11 @@ describe('given the transferOrgOwnership mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Unable to transfer organization ownership. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to transfer organization ownership. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -1083,7 +975,7 @@ describe('given the transferOrgOwnership mutation', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 1}), + query: jest.fn().mockReturnValue({ count: 1 }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -1107,15 +999,11 @@ describe('given the transferOrgOwnership mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Unable to transfer organization ownership. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to transfer organization ownership. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -1129,8 +1017,8 @@ describe('given the transferOrgOwnership mutation', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -1191,7 +1079,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -1200,94 +1088,14 @@ describe('given the transferOrgOwnership mutation', () => { transferOrgOwnership: { result: { code: 400, - description: - "Impossible de transférer la propriété d'une organisation non définie.", + description: "Impossible de transférer la propriété d'une organisation non définie.", }, }, }, } expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User: 123 attempted to transfer org ownership of an undefined org.`, - ]) - }) - }) - describe('requested org is verified', () => { - it('returns an error', async () => { - const response = await graphql( - schema, - ` - mutation { - transferOrgOwnership ( - input: { - orgId: "${toGlobalId('organizations', org._key)}" - userId: "${toGlobalId('users', user2._key)}" - } - ) { - result { - ... on TransferOrgOwnershipResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkOrgOwner: checkOrgOwner({ - i18n, - query, - userKey: user._key, - }), - userRequired: jest.fn().mockReturnValue({ - _key: 123, - }), - verifiedRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - verified: true, - slug: 'mocked-org', - }), - }, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResult = { - data: { - transferOrgOwnership: { - result: { - code: 400, - description: - "Impossible de transférer la propriété d'une organisation vérifiée.", - }, - }, - }, - } - - expect(response).toEqual(expectedResult) - expect(consoleOutput).toEqual([ - `User: 123 attempted to transfer ownership of a verified org: mocked-org.`, - ]) + expect(consoleOutput).toEqual([`User: 123 attempted to transfer org ownership of an undefined org.`]) }) }) describe('requesting user is not the org owner', () => { @@ -1341,7 +1149,7 @@ describe('given the transferOrgOwnership mutation', () => { i18n, }), }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -1412,7 +1220,7 @@ describe('given the transferOrgOwnership mutation', () => { load: jest.fn().mockReturnValue(undefined), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -1421,8 +1229,7 @@ describe('given the transferOrgOwnership mutation', () => { transferOrgOwnership: { result: { code: 400, - description: - "Impossible de transférer la propriété d'un org à un utilisateur non défini.", + description: "Impossible de transférer la propriété d'un org à un utilisateur non défini.", }, }, }, @@ -1461,7 +1268,7 @@ describe('given the transferOrgOwnership mutation', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 0}), + query: jest.fn().mockReturnValue({ count: 0 }), collections: collectionNames, transaction, userKey: user._key, @@ -1485,7 +1292,7 @@ describe('given the transferOrgOwnership mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -1559,14 +1366,12 @@ describe('given the transferOrgOwnership mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) const error = [ - new GraphQLError( - "Impossible de transférer la propriété de l'organisation. Veuillez réessayer.", - ), + new GraphQLError("Impossible de transférer la propriété de l'organisation. Veuillez réessayer."), ] expect(response.errors).toEqual(error) @@ -1608,7 +1413,7 @@ describe('given the transferOrgOwnership mutation', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 1}), + query: jest.fn().mockReturnValue({ count: 1 }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -1632,14 +1437,12 @@ describe('given the transferOrgOwnership mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) const error = [ - new GraphQLError( - "Impossible de transférer la propriété de l'organisation. Veuillez réessayer.", - ), + new GraphQLError("Impossible de transférer la propriété de l'organisation. Veuillez réessayer."), ] expect(response.errors).toEqual(error) @@ -1651,10 +1454,7 @@ describe('given the transferOrgOwnership mutation', () => { describe('when adding ownership to requested user', () => { it('throws an error', async () => { const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('Step Error')), + step: jest.fn().mockReturnValueOnce().mockRejectedValue(new Error('Step Error')), }) const response = await graphql( @@ -1682,7 +1482,7 @@ describe('given the transferOrgOwnership mutation', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 1}), + query: jest.fn().mockReturnValue({ count: 1 }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -1706,14 +1506,12 @@ describe('given the transferOrgOwnership mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) const error = [ - new GraphQLError( - "Impossible de transférer la propriété de l'organisation. Veuillez réessayer.", - ), + new GraphQLError("Impossible de transférer la propriété de l'organisation. Veuillez réessayer."), ] expect(response.errors).toEqual(error) @@ -1755,7 +1553,7 @@ describe('given the transferOrgOwnership mutation', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 1}), + query: jest.fn().mockReturnValue({ count: 1 }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -1779,14 +1577,12 @@ describe('given the transferOrgOwnership mutation', () => { }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) const error = [ - new GraphQLError( - "Impossible de transférer la propriété de l'organisation. Veuillez réessayer.", - ), + new GraphQLError("Impossible de transférer la propriété de l'organisation. Veuillez réessayer."), ] expect(response.errors).toEqual(error) diff --git a/api/src/affiliation/mutations/__tests__/update-user-role.test.js b/api/src/affiliation/mutations/__tests__/update-user-role.test.js index e9229c9768..523968a487 100644 --- a/api/src/affiliation/mutations/__tests__/update-user-role.test.js +++ b/api/src/affiliation/mutations/__tests__/update-user-role.test.js @@ -1,25 +1,20 @@ -import {ensure, dbNameFromFile} from 'arango-tools' -import {setupI18n} from '@lingui/core' -import {graphql, GraphQLSchema, GraphQLError} from 'graphql' -import {toGlobalId} from 'graphql-relay' +import { ensure, dbNameFromFile } from 'arango-tools' +import { setupI18n } from '@lingui/core' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import {createQuerySchema} from '../../../query' -import {createMutationSchema} from '../../../mutation' -import {cleanseInput} from '../../../validators' -import { - checkPermission, - userRequired, - verifiedRequired, - tfaRequired, -} from '../../../auth' -import {loadUserByUserName, loadUserByKey} from '../../../user/loaders' -import {loadOrgByKey} from '../../../organization/loaders' +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import { cleanseInput } from '../../../validators' +import { checkPermission, userRequired, verifiedRequired, tfaRequired } from '../../../auth' +import { loadUserByUserName, loadUserByKey } from '../../../user/loaders' +import { loadOrgByKey } from '../../../organization/loaders' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env describe('update a users role', () => { let query, drop, truncate, schema, collections, transaction, i18n, user @@ -46,7 +41,7 @@ describe('update a users role', () => { describe('given a successful role update', () => { beforeAll(async () => { // Generate DB Items - ;({query, drop, truncate, collections, transaction} = await ensure({ + ;({ query, drop, truncate, collections, transaction } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -77,8 +72,8 @@ describe('update a users role', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -177,15 +172,15 @@ describe('update a users role', () => { }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, validators: { cleanseInput, @@ -206,10 +201,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${user._key} successful updated user: ${secondaryUser._key} role to super_admin in org: treasury-board-secretariat.`, ]) + expect(response).toEqual(expectedResponse) }) }) describe('to user', () => { @@ -254,15 +249,15 @@ describe('update a users role', () => { }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, validators: { cleanseInput, @@ -283,10 +278,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${user._key} successful updated user: ${secondaryUser._key} role to user in org: treasury-board-secretariat.`, ]) + expect(response).toEqual(expectedResponse) }) }) }) @@ -340,15 +335,15 @@ describe('update a users role', () => { }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, validators: { cleanseInput, @@ -369,10 +364,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${user._key} successful updated user: ${secondaryUser._key} role to super_admin in org: treasury-board-secretariat.`, ]) + expect(response).toEqual(expectedResponse) }) }) describe('to admin', () => { @@ -417,15 +412,15 @@ describe('update a users role', () => { }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, validators: { cleanseInput, @@ -446,10 +441,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${user._key} successful updated user: ${secondaryUser._key} role to admin in org: treasury-board-secretariat.`, ]) + expect(response).toEqual(expectedResponse) }) }) }) @@ -512,15 +507,15 @@ describe('update a users role', () => { }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, validators: { cleanseInput, @@ -541,10 +536,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${user._key} successful updated user: ${secondaryUser._key} role to admin in org: treasury-board-secretariat.`, ]) + expect(response).toEqual(expectedResponse) }) }) }) @@ -555,8 +550,8 @@ describe('update a users role', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -655,15 +650,15 @@ describe('update a users role', () => { }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, validators: { cleanseInput, @@ -675,8 +670,7 @@ describe('update a users role', () => { data: { updateUserRole: { result: { - status: - "Le rôle de l'utilisateur a été mis à jour avec succès.", + status: "Le rôle de l'utilisateur a été mis à jour avec succès.", user: { displayName: 'Test Account', }, @@ -685,10 +679,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${user._key} successful updated user: ${secondaryUser._key} role to super_admin in org: treasury-board-secretariat.`, ]) + expect(response).toEqual(expectedResponse) }) }) describe('to user', () => { @@ -733,15 +727,15 @@ describe('update a users role', () => { }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, validators: { cleanseInput, @@ -753,8 +747,7 @@ describe('update a users role', () => { data: { updateUserRole: { result: { - status: - "Le rôle de l'utilisateur a été mis à jour avec succès.", + status: "Le rôle de l'utilisateur a été mis à jour avec succès.", user: { displayName: 'Test Account', }, @@ -763,10 +756,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${user._key} successful updated user: ${secondaryUser._key} role to user in org: treasury-board-secretariat.`, ]) + expect(response).toEqual(expectedResponse) }) }) }) @@ -820,15 +813,15 @@ describe('update a users role', () => { }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, validators: { cleanseInput, @@ -840,8 +833,7 @@ describe('update a users role', () => { data: { updateUserRole: { result: { - status: - "Le rôle de l'utilisateur a été mis à jour avec succès.", + status: "Le rôle de l'utilisateur a été mis à jour avec succès.", user: { displayName: 'Test Account', }, @@ -850,10 +842,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${user._key} successful updated user: ${secondaryUser._key} role to super_admin in org: treasury-board-secretariat.`, ]) + expect(response).toEqual(expectedResponse) }) }) describe('to admin', () => { @@ -898,15 +890,15 @@ describe('update a users role', () => { }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, validators: { cleanseInput, @@ -918,8 +910,7 @@ describe('update a users role', () => { data: { updateUserRole: { result: { - status: - "Le rôle de l'utilisateur a été mis à jour avec succès.", + status: "Le rôle de l'utilisateur a été mis à jour avec succès.", user: { displayName: 'Test Account', }, @@ -928,10 +919,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${user._key} successful updated user: ${secondaryUser._key} role to admin in org: treasury-board-secretariat.`, ]) + expect(response).toEqual(expectedResponse) }) }) }) @@ -994,15 +985,15 @@ describe('update a users role', () => { }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - verifiedRequired: verifiedRequired({i18n}), - tfaRequired: tfaRequired({i18n}), + verifiedRequired: verifiedRequired({ i18n }), + tfaRequired: tfaRequired({ i18n }), }, loaders: { - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), - loadUserByUserName: loadUserByUserName({query}), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + loadUserByUserName: loadUserByUserName({ query }), }, validators: { cleanseInput, @@ -1014,8 +1005,7 @@ describe('update a users role', () => { data: { updateUserRole: { result: { - status: - "Le rôle de l'utilisateur a été mis à jour avec succès.", + status: "Le rôle de l'utilisateur a été mis à jour avec succès.", user: { displayName: 'Test Account', }, @@ -1024,10 +1014,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ `User: ${user._key} successful updated user: ${secondaryUser._key} role to admin in org: treasury-board-secretariat.`, ]) + expect(response).toEqual(expectedResponse) }) }) }) @@ -1040,8 +1030,8 @@ describe('update a users role', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -1116,10 +1106,8 @@ describe('update a users role', () => { }, } + expect(consoleOutput).toEqual([`User: 123 attempted to update their own role in org: 123.`]) expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to update their own role in org: 123.`, - ]) }) }) describe('user attempts to update a user that does not exist', () => { @@ -1187,10 +1175,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to update a user: random@email.ca role in org: 123, however there is no user associated with that user name.`, ]) + expect(response).toEqual(error) }) }) describe('user attempts to update a users role in an org that does not exist', () => { @@ -1260,10 +1248,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to update a user: 456 role in org: 1, however there is no org associated with that id.`, ]) + expect(response).toEqual(error) }) }) describe('requesting user permission is user', () => { @@ -1336,10 +1324,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to update a user: 456 role in org: treasury-board-secretariat, however they do not have permission to do so.`, ]) + expect(response).toEqual(error) }) }) describe('user attempts to update a users role in an org that the requesting user does not belong to', () => { @@ -1370,7 +1358,7 @@ describe('update a users role', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 0}), + query: jest.fn().mockReturnValue({ count: 0 }), collections: collectionNames, transaction, userKey: 123, @@ -1412,10 +1400,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to update a user: 456 role in org: treasury-board-secretariat, however they do not have permission to do so.`, ]) + expect(response).toEqual(error) }) }) describe('user attempts to update a user that does not belong to the requested org', () => { @@ -1446,7 +1434,7 @@ describe('update a users role', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 0}), + query: jest.fn().mockReturnValue({ count: 0 }), collections: collectionNames, transaction, userKey: 123, @@ -1481,185 +1469,16 @@ describe('update a users role', () => { updateUserRole: { result: { code: 400, - description: - 'Unable to update role: user does not belong to organization.', + description: 'Unable to update role: user does not belong to organization.', }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to update a user: 456 role in org: treasury-board-secretariat, however that user does not have an affiliation with that organization.`, ]) - }) - }) - describe('requesting users role is super admin', () => { - describe('user attempts to update users role to admin', () => { - describe('requested users current role is super admin', () => { - it('returns an error message', async () => { - const response = await graphql( - schema, - ` - mutation { - updateUserRole ( - input: { - userName: "test@email.gc.ca" - orgId: "${toGlobalId('organizations', 123)}" - role: ADMIN - } - ) { - result { - ... on UpdateUserRoleResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: jest.fn().mockReturnValue({ - count: 1, - next: jest - .fn() - .mockReturnValue({permission: 'super_admin'}), - }), - collections: collectionNames, - transaction: jest.fn(), - userKey: 123, - auth: { - checkPermission: jest.fn().mockReturnValue('super_admin'), - userRequired: jest.fn().mockReturnValue({ - userName: 'test.account@istio.actually.exists', - }), - verifiedRequired: jest.fn(), - tfaRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - slug: 'treasury-board-secretariat', - }), - }, - loadUserByUserName: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, - }, - validators: { - cleanseInput, - }, - }, - ) - - const error = { - data: { - updateUserRole: { - result: { - code: 400, - description: - 'Permission Denied: Please contact organization admin for help with updating user roles.', - }, - }, - }, - } - - expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to lower user: 456 from super_admin to: admin.`, - ]) - }) - }) - }) - describe('user attempts to update users role to user', () => { - describe('requested users current role is super admin', () => { - it('returns an error message', async () => { - const response = await graphql( - schema, - ` - mutation { - updateUserRole ( - input: { - userName: "test@email.gc.ca" - orgId: "${toGlobalId('organizations', 123)}" - role: USER - } - ) { - result { - ... on UpdateUserRoleResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: jest.fn().mockReturnValue({ - count: 1, - next: jest - .fn() - .mockReturnValue({permission: 'super_admin'}), - }), - collections: collectionNames, - transaction: jest.fn(), - userKey: 123, - auth: { - checkPermission: jest.fn().mockReturnValue('super_admin'), - userRequired: jest.fn().mockReturnValue({ - userName: 'test.account@istio.actually.exists', - }), - verifiedRequired: jest.fn(), - tfaRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - slug: 'treasury-board-secretariat', - }), - }, - loadUserByUserName: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, - }, - validators: { - cleanseInput, - }, - }, - ) - - const error = { - data: { - updateUserRole: { - result: { - code: 400, - description: - 'Permission Denied: Please contact organization admin for help with updating user roles.', - }, - }, - }, - } - - expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to lower user: 456 from super_admin to: user.`, - ]) - }) - }) + expect(response).toEqual(error) }) }) describe('requesting users role is admin', () => { @@ -1693,9 +1512,7 @@ describe('update a users role', () => { i18n, query: jest.fn().mockReturnValue({ count: 1, - next: jest - .fn() - .mockReturnValue({permission: 'super_admin'}), + next: jest.fn().mockReturnValue({ permission: 'super_admin' }), }), collections: collectionNames, transaction: jest.fn(), @@ -1732,95 +1549,16 @@ describe('update a users role', () => { result: { code: 400, description: - 'Permission Denied: Please contact organization admin for help with updating user roles.', + 'Permission Denied: Please contact organization admin for help with user role changes.', }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `User: 123 attempted to lower user: 456 from super_admin to: user.`, + `User: 123 attempted to update a user: 456 role in org: treasury-board-secretariat, however they do not have permission to update a super_admin.`, ]) - }) - }) - describe('requested users current role is admin', () => { - it('returns an error message', async () => { - const response = await graphql( - schema, - ` - mutation { - updateUserRole ( - input: { - userName: "test@email.gc.ca" - orgId: "${toGlobalId('organizations', 123)}" - role: USER - } - ) { - result { - ... on UpdateUserRoleResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: jest.fn().mockReturnValue({ - count: 1, - next: jest.fn().mockReturnValue({permission: 'admin'}), - }), - collections: collectionNames, - transaction: jest.fn(), - userKey: 123, - auth: { - checkPermission: jest.fn().mockReturnValue('admin'), - userRequired: jest.fn().mockReturnValue({ - userName: 'test.account@istio.actually.exists', - }), - verifiedRequired: jest.fn(), - tfaRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - slug: 'treasury-board-secretariat', - }), - }, - loadUserByUserName: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, - }, - validators: { - cleanseInput, - }, - }, - ) - - const error = { - data: { - updateUserRole: { - result: { - code: 400, - description: - 'Permission Denied: Please contact organization admin for help with updating user roles.', - }, - }, - }, - } - expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to lower user: 456 from admin to: user.`, - ]) }) }) }) @@ -1884,16 +1622,12 @@ describe('update a users role', () => { }, ) - const error = [ - new GraphQLError( - `Unable to update user's role. Please try again.`, - ), - ] + const error = [new GraphQLError(`Unable to update user's role. Please try again.`)] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Database error occurred when user: 123 attempted to update a user's: 456 role, error: Error: database error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -1959,16 +1693,12 @@ describe('update a users role', () => { }, ) - const error = [ - new GraphQLError( - `Unable to update user's role. Please try again.`, - ), - ] + const error = [new GraphQLError(`Unable to update user's role. Please try again.`)] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Cursor error occurred when user: 123 attempted to update a user's: 456 role, error: Error: cursor error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -2003,7 +1733,7 @@ describe('update a users role', () => { i18n, query: jest.fn().mockReturnValue({ count: 1, - next: jest.fn().mockReturnValue({permission: 'user'}), + next: jest.fn().mockReturnValue({ permission: 'user' }), }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ @@ -2036,16 +1766,12 @@ describe('update a users role', () => { }, ) - const error = [ - new GraphQLError( - `Unable to update user's role. Please try again.`, - ), - ] + const error = [new GraphQLError(`Unable to update user's role. Please try again.`)] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Transaction step error occurred when user: 123 attempted to update a user's: 456 role, error: trx step error`, ]) + expect(response.errors).toEqual(error) }) }) describe('when committing transaction', () => { @@ -2078,7 +1804,7 @@ describe('update a users role', () => { i18n, query: jest.fn().mockReturnValue({ count: 1, - next: jest.fn().mockReturnValue({permission: 'user'}), + next: jest.fn().mockReturnValue({ permission: 'user' }), }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ @@ -2112,16 +1838,12 @@ describe('update a users role', () => { }, ) - const error = [ - new GraphQLError( - `Unable to update user's role. Please try again.`, - ), - ] + const error = [new GraphQLError(`Unable to update user's role. Please try again.`)] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Transaction commit error occurred when user: 123 attempted to update a user's: 456 role, error: trx commit error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -2131,8 +1853,8 @@ describe('update a users role', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -2201,17 +1923,14 @@ describe('update a users role', () => { updateUserRole: { result: { code: 400, - description: - 'Impossible de mettre à jour votre propre rôle.', + description: 'Impossible de mettre à jour votre propre rôle.', }, }, }, } + expect(consoleOutput).toEqual([`User: 123 attempted to update their own role in org: 123.`]) expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to update their own role in org: 123.`, - ]) }) }) describe('user attempts to update a user that does not exist', () => { @@ -2273,17 +1992,16 @@ describe('update a users role', () => { updateUserRole: { result: { code: 400, - description: - 'Impossible de mettre à jour le rôle : utilisateur inconnu.', + description: 'Impossible de mettre à jour le rôle : utilisateur inconnu.', }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to update a user: random@email.ca role in org: 123, however there is no user associated with that user name.`, ]) + expect(response).toEqual(error) }) }) describe('user attempts to update a users role in an org that does not exist', () => { @@ -2347,17 +2065,16 @@ describe('update a users role', () => { updateUserRole: { result: { code: 400, - description: - 'Impossible de mettre à jour le rôle : organisation inconnue.', + description: 'Impossible de mettre à jour le rôle : organisation inconnue.', }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to update a user: 456 role in org: 1, however there is no org associated with that id.`, ]) + expect(response).toEqual(error) }) }) describe('requesting user permission is user', () => { @@ -2430,10 +2147,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to update a user: 456 role in org: treasury-board-secretariat, however they do not have permission to do so.`, ]) + expect(response).toEqual(error) }) }) describe('user attempts to update a users role in an org that the requesting user does not belong to', () => { @@ -2464,7 +2181,7 @@ describe('update a users role', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 0}), + query: jest.fn().mockReturnValue({ count: 0 }), collections: collectionNames, transaction, userKey: 123, @@ -2506,10 +2223,10 @@ describe('update a users role', () => { }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to update a user: 456 role in org: treasury-board-secretariat, however they do not have permission to do so.`, ]) + expect(response).toEqual(error) }) }) describe('user attempts to update a user that does not belong to the requested org', () => { @@ -2540,7 +2257,7 @@ describe('update a users role', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 0}), + query: jest.fn().mockReturnValue({ count: 0 }), collections: collectionNames, transaction, userKey: 123, @@ -2582,180 +2299,13 @@ describe('update a users role', () => { }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ `User: 123 attempted to update a user: 456 role in org: treasury-board-secretariat, however that user does not have an affiliation with that organization.`, ]) + expect(response).toEqual(error) }) }) - describe('requesting users role is super admin', () => { - describe('user attempts to update users role to admin', () => { - describe('requested users current role is super admin', () => { - it('returns an error message', async () => { - const response = await graphql( - schema, - ` - mutation { - updateUserRole ( - input: { - userName: "test@email.gc.ca" - orgId: "${toGlobalId('organizations', 123)}" - role: ADMIN - } - ) { - result { - ... on UpdateUserRoleResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: jest.fn().mockReturnValue({ - count: 1, - next: jest - .fn() - .mockReturnValue({permission: 'super_admin'}), - }), - collections: collectionNames, - transaction: jest.fn(), - userKey: 123, - auth: { - checkPermission: jest.fn().mockReturnValue('super_admin'), - userRequired: jest.fn().mockReturnValue({ - userName: 'test.account@istio.actually.exists', - }), - verifiedRequired: jest.fn(), - tfaRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - slug: 'treasury-board-secretariat', - }), - }, - loadUserByUserName: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, - }, - validators: { - cleanseInput, - }, - }, - ) - - const error = { - data: { - updateUserRole: { - result: { - code: 400, - description: - "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la mise à jour des rôles des utilisateurs.", - }, - }, - }, - } - expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to lower user: 456 from super_admin to: admin.`, - ]) - }) - }) - }) - describe('user attempts to update users role to user', () => { - describe('requested users current role is super admin', () => { - it('returns an error message', async () => { - const response = await graphql( - schema, - ` - mutation { - updateUserRole ( - input: { - userName: "test@email.gc.ca" - orgId: "${toGlobalId('organizations', 123)}" - role: USER - } - ) { - result { - ... on UpdateUserRoleResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: jest.fn().mockReturnValue({ - count: 1, - next: jest - .fn() - .mockReturnValue({permission: 'super_admin'}), - }), - collections: collectionNames, - transaction: jest.fn(), - userKey: 123, - auth: { - checkPermission: jest.fn().mockReturnValue('super_admin'), - userRequired: jest.fn().mockReturnValue({ - userName: 'test.account@istio.actually.exists', - }), - verifiedRequired: jest.fn(), - tfaRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - slug: 'treasury-board-secretariat', - }), - }, - loadUserByUserName: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, - }, - validators: { - cleanseInput, - }, - }, - ) - - const error = { - data: { - updateUserRole: { - result: { - code: 400, - description: - "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la mise à jour des rôles des utilisateurs.", - }, - }, - }, - } - - expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to lower user: 456 from super_admin to: user.`, - ]) - }) - }) - }) - }) describe('requesting users role is admin', () => { describe('requested users role is super admin', () => { it('returns an error message', async () => { @@ -2787,9 +2337,7 @@ describe('update a users role', () => { i18n, query: jest.fn().mockReturnValue({ count: 1, - next: jest - .fn() - .mockReturnValue({permission: 'super_admin'}), + next: jest.fn().mockReturnValue({ permission: 'super_admin' }), }), collections: collectionNames, transaction: jest.fn(), @@ -2826,95 +2374,16 @@ describe('update a users role', () => { result: { code: 400, description: - "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la mise à jour des rôles des utilisateurs.", + "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur les changements de rôle des utilisateurs.", }, }, }, } - expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `User: 123 attempted to lower user: 456 from super_admin to: user.`, + `User: 123 attempted to update a user: 456 role in org: treasury-board-secretariat, however they do not have permission to update a super_admin.`, ]) - }) - }) - describe('requested users current role is admin', () => { - it('returns an error message', async () => { - const response = await graphql( - schema, - ` - mutation { - updateUserRole ( - input: { - userName: "test@email.gc.ca" - orgId: "${toGlobalId('organizations', 123)}" - role: USER - } - ) { - result { - ... on UpdateUserRoleResult { - status - } - ... on AffiliationError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: jest.fn().mockReturnValue({ - count: 1, - next: jest.fn().mockReturnValue({permission: 'admin'}), - }), - collections: collectionNames, - transaction: jest.fn(), - userKey: 123, - auth: { - checkPermission: jest.fn().mockReturnValue('admin'), - userRequired: jest.fn().mockReturnValue({ - userName: 'test.account@istio.actually.exists', - }), - verifiedRequired: jest.fn(), - tfaRequired: jest.fn(), - }, - loaders: { - loadOrgByKey: { - load: jest.fn().mockReturnValue({ - slug: 'treasury-board-secretariat', - }), - }, - loadUserByUserName: { - load: jest.fn().mockReturnValue({ - _key: 456, - }), - }, - }, - validators: { - cleanseInput, - }, - }, - ) - - const error = { - data: { - updateUserRole: { - result: { - code: 400, - description: - "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la mise à jour des rôles des utilisateurs.", - }, - }, - }, - } - expect(response).toEqual(error) - expect(consoleOutput).toEqual([ - `User: 123 attempted to lower user: 456 from admin to: user.`, - ]) }) }) }) @@ -2979,15 +2448,13 @@ describe('update a users role', () => { ) const error = [ - new GraphQLError( - `Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer.`, - ), + new GraphQLError(`Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer.`), ] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Database error occurred when user: 123 attempted to update a user's: 456 role, error: Error: database error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -3054,15 +2521,13 @@ describe('update a users role', () => { ) const error = [ - new GraphQLError( - `Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer.`, - ), + new GraphQLError(`Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer.`), ] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Cursor error occurred when user: 123 attempted to update a user's: 456 role, error: Error: cursor error`, ]) + expect(response.errors).toEqual(error) }) }) }) @@ -3097,7 +2562,7 @@ describe('update a users role', () => { i18n, query: jest.fn().mockReturnValue({ count: 1, - next: jest.fn().mockReturnValue({permission: 'user'}), + next: jest.fn().mockReturnValue({ permission: 'user' }), }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ @@ -3131,15 +2596,13 @@ describe('update a users role', () => { ) const error = [ - new GraphQLError( - `Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer.`, - ), + new GraphQLError(`Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer.`), ] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Transaction step error occurred when user: 123 attempted to update a user's: 456 role, error: trx step error`, ]) + expect(response.errors).toEqual(error) }) }) describe('when committing transaction', () => { @@ -3172,7 +2635,7 @@ describe('update a users role', () => { i18n, query: jest.fn().mockReturnValue({ count: 1, - next: jest.fn().mockReturnValue({permission: 'user'}), + next: jest.fn().mockReturnValue({ permission: 'user' }), }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ @@ -3207,15 +2670,13 @@ describe('update a users role', () => { ) const error = [ - new GraphQLError( - `Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer.`, - ), + new GraphQLError(`Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer.`), ] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Transaction commit error occurred when user: 123 attempted to update a user's: 456 role, error: trx commit error`, ]) + expect(response.errors).toEqual(error) }) }) }) diff --git a/api/src/affiliation/mutations/index.js b/api/src/affiliation/mutations/index.js index 3b3584b898..557b4f8353 100644 --- a/api/src/affiliation/mutations/index.js +++ b/api/src/affiliation/mutations/index.js @@ -1,5 +1,6 @@ export * from './invite-user-to-org' export * from './leave-organization' export * from './remove-user-from-org' +export * from './request-org-affiliation' export * from './transfer-org-ownership' export * from './update-user-role' diff --git a/api/src/affiliation/mutations/invite-user-to-org.js b/api/src/affiliation/mutations/invite-user-to-org.js index 44a347e2a1..6f3ff361e2 100644 --- a/api/src/affiliation/mutations/invite-user-to-org.js +++ b/api/src/affiliation/mutations/invite-user-to-org.js @@ -1,11 +1,11 @@ -import {GraphQLNonNull, GraphQLID} from 'graphql' -import {mutationWithClientMutationId, fromGlobalId} from 'graphql-relay' -import {GraphQLEmailAddress} from 'graphql-scalars' -import {t} from '@lingui/macro' +import { GraphQLNonNull, GraphQLID } from 'graphql' +import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay' +import { GraphQLEmailAddress } from 'graphql-scalars' +import { t } from '@lingui/macro' -import {inviteUserToOrgUnion} from '../unions' -import {LanguageEnums, RoleEnums} from '../../enums' +import { inviteUserToOrgUnion } from '../unions' import { logActivity } from '../../audit-logs/mutations/log-activity' +import { InvitationRoleEnums } from '../../enums' export const inviteUserToOrg = new mutationWithClientMutationId({ name: 'InviteUserToOrg', @@ -18,17 +18,13 @@ able to sign-up and be assigned to that organization in one mutation.`, description: 'Users email that you would like to invite to your org.', }, requestedRole: { - type: GraphQLNonNull(RoleEnums), + type: GraphQLNonNull(InvitationRoleEnums), description: 'The role which you would like this user to have.', }, orgId: { type: GraphQLNonNull(GraphQLID), description: 'The organization you wish to invite the user to.', }, - preferredLang: { - type: GraphQLNonNull(LanguageEnums), - description: 'The language in which the email will be sent in.', - }, }), outputFields: () => ({ result: { @@ -47,34 +43,25 @@ able to sign-up and be assigned to that organization in one mutation.`, collections, transaction, userKey, - auth: { - checkPermission, - tokenize, - userRequired, - verifiedRequired, - tfaRequired, - }, - loaders: {loadOrgByKey, loadUserByUserName}, - notify: {sendOrgInviteCreateAccount, sendOrgInviteEmail}, - validators: {cleanseInput}, + auth: { checkPermission, tokenize, userRequired, verifiedRequired, tfaRequired }, + loaders: { loadOrgByKey, loadUserByUserName }, + notify: { sendOrgInviteCreateAccount, sendOrgInviteEmail }, + validators: { cleanseInput }, }, ) => { const userName = cleanseInput(args.userName).toLowerCase() const requestedRole = cleanseInput(args.requestedRole) - const {id: orgId} = fromGlobalId(cleanseInput(args.orgId)) - const preferredLang = cleanseInput(args.preferredLang) + const { id: orgId } = fromGlobalId(cleanseInput(args.orgId)) // Get requesting user const user = await userRequired() - verifiedRequired({user}) - tfaRequired({user}) + verifiedRequired({ user }) + tfaRequired({ user }) // Make sure user is not inviting themselves if (user.userName === userName) { - console.warn( - `User: ${userKey} attempted to invite themselves to ${orgId}.`, - ) + console.warn(`User: ${userKey} attempted to invite themselves to ${orgId}.`) return { _type: 'error', code: 400, @@ -97,12 +84,11 @@ able to sign-up and be assigned to that organization in one mutation.`, } // Check to see requesting users permission to the org is - const permission = await checkPermission({orgId: org._id}) + const permission = await checkPermission({ orgId: org._id }) if ( - typeof permission === 'undefined' || - permission === 'user' || - (permission === 'admin' && requestedRole === 'super_admin') + (['user', 'admin'].includes(requestedRole) && !['admin', 'owner', 'super_admin'].includes(permission)) || + (requestedRole === 'super_admin' && permission !== 'super_admin') ) { console.warn( `User: ${userKey} attempted to invite user: ${userName} to org: ${org._key} with role: ${requestedRole} but does not have permission to do so.`, @@ -110,34 +96,56 @@ able to sign-up and be assigned to that organization in one mutation.`, return { _type: 'error', code: 403, - description: i18n._( - t`Permission Denied: Please contact organization admin for help with user invitations.`, - ), + description: i18n._(t`Permission Denied: Please contact organization admin for help with user invitations.`), } } + // Get org names to use in email + let orgNamesCursor + try { + orgNamesCursor = await query` + LET org = DOCUMENT(organizations, ${org._id}) + RETURN { + "orgNameEN": org.orgDetails.en.name, + "orgNameFR": org.orgDetails.fr.name, + } + ` + } catch (err) { + console.error( + `Database error occurred when user: ${userKey} attempted to invite user: ${userName} to org: ${org._key}. Error while creating cursor for retrieving organization names. error: ${err}`, + ) + throw new Error(i18n._(t`Unable to invite user to organization. Please try again.`)) + } + + let orgNames + try { + orgNames = await orgNamesCursor.next() + } catch (err) { + console.error( + `Cursor error occurred when user: ${userKey} attempted to invite user: ${userName} to org: ${org._key}. Error while retrieving organization names. error: ${err}`, + ) + throw new Error(i18n._(t`Unable to invite user to organization. Please try again.`)) + } + // Check to see if requested user exists const requestedUser = await loadUserByUserName.load(userName) // If there is not associated account with that user name send invite to org with create account if (typeof requestedUser === 'undefined') { const token = tokenize({ - parameters: {userName, orgKey: org._key, requestedRole}, + parameters: { userName, orgKey: org._key, requestedRole }, expPeriod: 24, }) - const createAccountLink = `https://${request.get( - 'host', - )}/create-user/${token}` + const createAccountLink = `https://${request.get('host')}/create-user/${token}` await sendOrgInviteCreateAccount({ - user: {userName: userName, preferredLang}, - orgName: org.name, + user: { userName: userName }, + orgNameEN: orgNames.orgNameEN, + orgNameFR: orgNames.orgNameFR, createAccountLink, }) - console.info( - `User: ${userKey} successfully invited user: ${userName} to the service, and org: ${org.slug}.`, - ) + console.info(`User: ${userKey} successfully invited user: ${userName} to the service, and org: ${org.slug}.`) await logActivity({ transaction, collections, @@ -155,92 +163,121 @@ able to sign-up and be assigned to that organization in one mutation.`, name: org.name, }, // name of resource being acted upon resourceType: 'user', // user, org, domain - updatedProperties: [ - { name: 'role', oldValue: '', newValue: requestedRole }, - ], + updatedProperties: [{ name: 'role', oldValue: '', newValue: requestedRole }], }, }) return { _type: 'regular', - status: i18n._( - t`Successfully sent invitation to service, and organization email.`, - ), + status: i18n._(t`Successfully sent invitation to service, and organization email.`), + } + } + + // If account is found, check if already affiliated with org + let affiliationCursor + try { + affiliationCursor = await query` + WITH affiliations, organizations, users + FOR v, e IN 1..1 INBOUND ${requestedUser._id} affiliations + FILTER e._from == ${org._id} + RETURN e + ` + } catch (err) { + console.error( + `Database error occurred when user: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug}, error: ${err}`, + ) + return { + _type: 'error', + code: 500, + description: i18n._(t`Unable to invite user to organization. Please try again.`), } } - // If account is found add just add affiliation - else { - // Setup Transaction - const trx = await transaction(collections) - - // Create affiliation - try { - await trx.step( - () => - query` + + if (affiliationCursor.count > 0) { + // If affiliation is found, return error + console.warn( + `User: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug} however they are already affiliated with that org.`, + ) + return { + _type: 'error', + code: 400, + description: i18n._(t`Unable to invite user to organization. User is already affiliated with organization.`), + } + } + + // User is not affiliated with org, create affiliation + + // Setup Transaction + const trx = await transaction(collections) + + // Create affiliation + try { + await trx.step( + () => + query` WITH affiliations, organizations, users INSERT { _from: ${org._id}, _to: ${requestedUser._id}, permission: ${requestedRole}, - owner: false } INTO affiliations `, - ) - } catch (err) { - console.error( - `Transaction step error occurred while user: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug}, error: ${err}`, - ) - throw new Error(i18n._(t`Unable to invite user. Please try again.`)) + ) + } catch (err) { + console.error( + `Transaction step error occurred while user: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug}, error: ${err}`, + ) + return { + _type: 'error', + code: 500, + description: i18n._(t`Unable to invite user. Please try again.`), } + } - await sendOrgInviteEmail({ - user: requestedUser, - orgName: org.name, - }) - - // Commit affiliation - try { - await trx.commit() - } catch (err) { - console.error( - `Transaction commit error occurred while user: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug}, error: ${err}`, - ) - throw new Error(i18n._(t`Unable to invite user. Please try again.`)) - } + await sendOrgInviteEmail({ + user: requestedUser, + orgName: requestedUser.preferredLang === 'english' ? orgNames.orgNameEN : orgNames.orgNameFR, + }) - console.info( - `User: ${userKey} successfully invited user: ${requestedUser._key} to the org: ${org.slug}.`, + // Commit affiliation + try { + await trx.commit() + } catch (err) { + console.error( + `Transaction commit error occurred while user: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug}, error: ${err}`, ) - await logActivity({ - transaction, - collections, - query, - initiatedBy: { - id: user._key, - userName: user.userName, - role: permission, - }, - action: 'add', - target: { - resource: userName, - organization: { - id: org._key, - name: org.name, - }, // name of resource being acted upon - updatedProperties: [ - { name: 'role', oldValue: '', newValue: requestedRole }, - ], - resourceType: 'user', // user, org, domain - }, - }) - return { - _type: 'regular', - status: i18n._( - t`Successfully invited user to organization, and sent notification email.`, - ), + _type: 'error', + code: 500, + description: i18n._(t`Unable to invite user. Please try again.`), } } + + console.info(`User: ${userKey} successfully invited user: ${requestedUser._key} to the org: ${org.slug}.`) + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'add', + target: { + resource: userName, + organization: { + id: org._key, + name: org.name, + }, // name of resource being acted upon + updatedProperties: [{ name: 'role', oldValue: '', newValue: requestedRole }], + resourceType: 'user', // user, org, domain + }, + }) + + return { + _type: 'regular', + status: i18n._(t`Successfully invited user to organization, and sent notification email.`), + } }, }) diff --git a/api/src/affiliation/mutations/leave-organization.js b/api/src/affiliation/mutations/leave-organization.js index b908f5616a..bcbd9aa16e 100644 --- a/api/src/affiliation/mutations/leave-organization.js +++ b/api/src/affiliation/mutations/leave-organization.js @@ -27,7 +27,7 @@ export const leaveOrganization = new mutationWithClientMutationId({ query, collections, transaction, - auth: { checkOrgOwner, userRequired, verifiedRequired }, + auth: { userRequired, verifiedRequired }, loaders: { loadOrgByKey }, validators: { cleanseInput }, }, @@ -49,249 +49,25 @@ export const leaveOrganization = new mutationWithClientMutationId({ } } - // check to see if org owner - const owner = await checkOrgOwner({ orgId: org._id }) - // Setup Trans action const trx = await transaction(collections) - if (owner) { - // check to see if org has any dmarc summaries - let dmarcSummaryCheckCursor - try { - dmarcSummaryCheckCursor = await query` - WITH domains, ownership, dmarcSummaries, organizations - FOR v, e IN 1..1 OUTBOUND ${org._id} ownership - RETURN e - ` - } catch (err) { - console.error( - `Database error occurred while checking for dmarc summaries for org: ${org._key}, when user: ${user._key} attempted to leave: ${err}`, - ) - throw new Error(i18n._(t`Unable leave organization. Please try again.`)) - } - - let dmarcSummaryCheckList - try { - dmarcSummaryCheckList = await dmarcSummaryCheckCursor.all() - } catch (err) { - console.error( - `Cursor error occurred when getting dmarc summary info for org: ${org._key}, when user: ${user._key} attempted to leave: ${err}`, - ) - throw new Error(i18n._(t`Unable leave organization. Please try again.`)) - } - - for (const ownership of dmarcSummaryCheckList) { - try { - await trx.step( - () => query` - WITH ownership, organizations, domains, dmarcSummaries, domainsToDmarcSummaries - LET dmarcSummaryEdges = ( - FOR v, e IN 1..1 OUTBOUND ${ownership._to} domainsToDmarcSummaries - RETURN { edgeKey: e._key, dmarcSummaryId: e._to } - ) - LET removeDmarcSummaryEdges = ( - FOR dmarcSummaryEdge IN dmarcSummaryEdges - REMOVE dmarcSummaryEdge.edgeKey IN domainsToDmarcSummaries - OPTIONS { waitForSync: true } - ) - LET removeDmarcSummary = ( - FOR dmarcSummaryEdge IN dmarcSummaryEdges - LET key = PARSE_IDENTIFIER(dmarcSummaryEdge.dmarcSummaryId).key - REMOVE key IN dmarcSummaries - OPTIONS { waitForSync: true } - ) - RETURN true - `, - ) - } catch (err) { - console.error( - `Trx step error occurred while attempting to remove dmarc summaries for org: ${org._key}, when user: ${user._key} attempted to leave: ${err}`, - ) - throw new Error(i18n._(t`Unable leave organization. Please try again.`)) - } - - try { - await trx.step( - () => query` - WITH ownership, organizations, domains - REMOVE ${ownership._key} IN ownership - OPTIONS { waitForSync: true } - `, - ) - } catch (err) { - console.error( - `Trx step error occurred while attempting to remove ownership for org: ${org._key}, when user: ${user._key} attempted to leave: ${err}`, - ) - throw new Error(i18n._(t`Unable leave organization. Please try again.`)) - } - } - - // check to see if any other orgs are using this domain - let countCursor - try { - countCursor = await query` - WITH claims, domains, organizations - LET domainIds = ( - FOR v, e IN 1..1 OUTBOUND ${org._id} claims - RETURN e._to - ) - FOR domain IN domains - FILTER domain._id IN domainIds - LET count = LENGTH( - FOR v, e IN 1..1 INBOUND domain._id claims - RETURN 1 - ) - RETURN { - _id: domain._id, - _key: domain._key, - domain: domain.domain, - count - } - ` - } catch (err) { - console.error( - `Database error occurred while while gathering domainInfo org: ${org._key}, when user: ${user._key} attempted to leave: ${err}`, - ) - throw new Error(i18n._(t`Unable leave organization. Please try again.`)) - } - - let domainInfo - try { - domainInfo = await countCursor.all() - } catch (err) { - console.error( - `Cursor error occurred while while gathering domainInfo org: ${org._key}, when user: ${user._key} attempted to leave: ${err}`, - ) - throw new Error(i18n._(t`Unable leave organization. Please try again.`)) - } - - for (const domain of domainInfo) { - if (domain.count === 1) { - try { - // Remove web data - await trx.step(async () => { - await query` - WITH web, webScan, domains - FOR webV, domainsWebEdge IN 1..1 OUTBOUND ${domain._id} domainsWeb - FOR webScanV, webToWebScansV In 1..1 ANY webV._id webToWebScans - REMOVE webScanV IN webScan - REMOVE webToWebScansV IN webToWebScans - OPTIONS { waitForSync: true } - REMOVE webV IN web - REMOVE domainsWebEdge IN domainsWeb - OPTIONS { waitForSync: true } - ` - }) - } catch (err) { - console.error( - `Trx step error occurred while user: ${user._key} attempted to remove web data for ${domain.domain} in org: ${org.slug}, ${err}`, - ) - throw new Error(i18n._(t`Unable to leave organization. Please try again.`)) - } - - try { - // Remove DNS data - await trx.step(async () => { - await query` - WITH dns, domains - FOR dnsV, domainsDNSEdge IN 1..1 OUTBOUND ${domain._id} domainsDNS - REMOVE dnsV IN dns - REMOVE domainsDNSEdge IN domainsDNS - OPTIONS { waitForSync: true } - ` - }) - } catch (err) { - console.error( - `Trx step error occurred while user: ${user._key} attempted to remove DNS data for ${domain.domain} in org: ${org.slug}, error: ${err}`, - ) - throw new Error(i18n._(t`Unable to leave organization. Please try again.`)) - } - - try { - await trx.step( - () => - query` - WITH claims, domains, organizations - LET domainEdges = ( - FOR v, e IN 1..1 OUTBOUND ${org._id} claims - FILTER e._to == ${domain._id} - RETURN { edgeKey: e._key, domainId: e._to } - ) - LET removeDomainEdges = ( - FOR domainEdge in domainEdges - REMOVE domainEdge.edgeKey IN claims - OPTIONS { waitForSync: true } - ) - LET removeDomain = ( - FOR domainEdge in domainEdges - LET key = PARSE_IDENTIFIER(domainEdge.domainId).key - REMOVE key IN domains - OPTIONS { waitForSync: true } - ) - RETURN true - `, - ) - } catch (err) { - console.error( - `Trx step error occurred while attempting to remove domains for org: ${org._key}, when user: ${user._key} attempted to leave: ${err}`, - ) - throw new Error(i18n._(t`Unable leave organization. Please try again.`)) - } - } - } - - try { - await Promise.all([ - trx.step( - () => - query` - WITH affiliations, organizations, users - LET userEdges = ( - FOR v, e IN 1..1 OUTBOUND ${org._id} affiliations - RETURN { edgeKey: e._key, userKey: e._to } - ) - LET removeUserEdges = ( - FOR userEdge IN userEdges - REMOVE userEdge.edgeKey IN affiliations - OPTIONS { waitForSync: true } - ) - RETURN true - `, - ), - trx.step( - () => - query` - WITH organizations - REMOVE ${org._key} IN organizations - OPTIONS { waitForSync: true } - `, - ), - ]) - } catch (err) { - console.error( - `Trx step error occurred while attempting to remove affiliations, and the org for org: ${org._key}, when user: ${user._key} attempted to leave: ${err}`, - ) - throw new Error(i18n._(t`Unable leave organization. Please try again.`)) - } - } else { - try { - await trx.step( - () => - query` + try { + await trx.step( + () => + query` WITH affiliations, organizations, users FOR v, e IN 1..1 OUTBOUND ${org._id} affiliations FILTER e._to == ${user._id} REMOVE { _key: e._key } IN affiliations OPTIONS { waitForSync: true } `, - ) - } catch (err) { - console.error( - `Trx step error occurred when removing user: ${user._key} affiliation with org: ${org._key}: ${err}`, - ) - throw new Error(i18n._(t`Unable leave organization. Please try again.`)) - } + ) + } catch (err) { + console.error( + `Trx step error occurred when removing user: ${user._key} affiliation with org: ${org._key}: ${err}`, + ) + throw new Error(i18n._(t`Unable leave organization. Please try again.`)) } try { diff --git a/api/src/affiliation/mutations/remove-user-from-org.js b/api/src/affiliation/mutations/remove-user-from-org.js index 237461d3aa..4450653695 100644 --- a/api/src/affiliation/mutations/remove-user-from-org.js +++ b/api/src/affiliation/mutations/remove-user-from-org.js @@ -1,14 +1,13 @@ -import {GraphQLNonNull, GraphQLID} from 'graphql' -import {mutationWithClientMutationId, fromGlobalId} from 'graphql-relay' -import {t} from '@lingui/macro' +import { GraphQLNonNull, GraphQLID } from 'graphql' +import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay' +import { t } from '@lingui/macro' -import {removeUserFromOrgUnion} from '../unions' +import { removeUserFromOrgUnion } from '../unions' import { logActivity } from '../../audit-logs/mutations/log-activity' export const removeUserFromOrg = new mutationWithClientMutationId({ name: 'RemoveUserFromOrg', - description: - 'This mutation allows admins or higher to remove users from any organizations they belong to.', + description: 'This mutation allows admins or higher to remove users from any organizations they belong to.', inputFields: () => ({ userId: { type: GraphQLNonNull(GraphQLID), @@ -35,20 +34,20 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ collections, transaction, userKey, - auth: {checkPermission, userRequired, verifiedRequired, tfaRequired}, - loaders: {loadOrgByKey, loadUserByKey}, - validators: {cleanseInput}, + auth: { checkPermission, userRequired, verifiedRequired, tfaRequired }, + loaders: { loadOrgByKey, loadUserByKey }, + validators: { cleanseInput }, }, ) => { // Cleanse Input - const {id: requestedUserKey} = fromGlobalId(cleanseInput(args.userId)) - const {id: requestedOrgKey} = fromGlobalId(cleanseInput(args.orgId)) + const { id: requestedUserKey } = fromGlobalId(cleanseInput(args.userId)) + const { id: requestedOrgKey } = fromGlobalId(cleanseInput(args.orgId)) // Get requesting user const user = await userRequired() - verifiedRequired({user}) - tfaRequired({user}) + verifiedRequired({ user }) + tfaRequired({ user }) // Get requested org const requestedOrg = await loadOrgByKey.load(requestedOrgKey) @@ -59,24 +58,12 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ return { _type: 'error', code: 400, - description: i18n._( - t`Unable to remove user from unknown organization.`, - ), + description: i18n._(t`Unable to remove user from unknown organization.`), } } // Check requesting users permission - const permission = await checkPermission({orgId: requestedOrg._id}) - if (permission === 'user' || typeof permission === 'undefined') { - console.warn( - `User: ${userKey} attempted to remove user: ${requestedUserKey} from org: ${requestedOrg._key}, however they do not have the permission to remove users.`, - ) - return { - _type: 'error', - code: 400, - description: i18n._(t`Unable to remove user from organization.`), - } - } + const permission = await checkPermission({ orgId: requestedOrg._id }) // Get requested user const requestedUser = await loadUserByKey.load(requestedUserKey) @@ -87,9 +74,7 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ return { _type: 'error', code: 400, - description: i18n._( - t`Unable to remove unknown user from organization.`, - ), + description: i18n._(t`Unable to remove unknown user from organization.`), } } @@ -106,11 +91,7 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ console.error( `Database error occurred when user: ${userKey} attempted to check the current permission of user: ${requestedUser._key} to see if they could be removed: ${err}`, ) - throw new Error( - i18n._( - t`Unable to remove user from this organization. Please try again.`, - ), - ) + throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`)) } if (affiliationCursor.count < 1) { @@ -120,9 +101,7 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ return { _type: 'error', code: 400, - description: i18n._( - t`Unable to remove a user that already does not belong to this organization.`, - ), + description: i18n._(t`Unable to remove a user that already does not belong to this organization.`), } } @@ -133,106 +112,91 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ console.error( `Cursor error occurred when user: ${userKey} attempted to check the current permission of user: ${requestedUser._key} to see if they could be removed: ${err}`, ) - throw new Error( - i18n._( - t`Unable to remove user from this organization. Please try again.`, - ), + throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`)) + } + + // Only admins, owners, and super admins can remove users + if (['admin', 'owner', 'super_admin'].includes(permission) === false) { + console.warn( + `User: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, but they do not have the right permission.`, ) + return { + _type: 'error', + code: 400, + description: i18n._(t`Permission Denied: Please contact organization admin for help with removing users.`), + } } - let canRemove - if ( - permission === 'super_admin' && - (affiliation.permission === 'admin' || affiliation.permission === 'user') - ) { - canRemove = true - } else if (permission === 'admin' && affiliation.permission === 'user') { - canRemove = true - } else { - canRemove = false + // Only super admins can remove super admins and owners + if (['owner', 'super_admin'].includes(affiliation.permission) && permission !== 'super_admin') { + console.warn( + `User: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, but they do not have the right permission.`, + ) + return { + _type: 'error', + code: 400, + description: i18n._(t`Permission Denied: Please contact organization admin for help with removing users.`), + } } - if (canRemove) { - // Setup Transaction - const trx = await transaction(collections) + // Setup Transaction + const trx = await transaction(collections) - try { - await trx.step(async () => { - query` + try { + await trx.step( () => + query` WITH affiliations, organizations, users FOR aff IN affiliations FILTER aff._from == ${requestedOrg._id} FILTER aff._to == ${requestedUser._id} REMOVE aff IN affiliations RETURN true - ` - }) - } catch (err) { - console.error( - `Trx step error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`, - ) - throw new Error( - i18n._( - t`Unable to remove user from this organization. Please try again.`, - ), - ) - } - - try { - await trx.commit() - } catch (err) { - console.error( - `Trx commit error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`, - ) - throw new Error( - i18n._( - t`Unable to remove user from this organization. Please try again.`, - ), - ) - } - - console.info( - `User: ${userKey} successfully removed user: ${requestedUser._key} from org: ${requestedOrg._key}.`, + `, + ) + } catch (err) { + console.error( + `Trx step error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`, ) - await logActivity({ - transaction, - collections, - query, - initiatedBy: { - id: user._key, - userName: user.userName, - role: permission, - }, - action: 'remove', - target: { - resource: requestedUser.userName, - organization: { - id: requestedOrg._key, - name: requestedOrg.name, - }, // name of resource being acted upon - resourceType: 'user', // user, org, domain - }, - }) + throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`)) + } - return { - _type: 'regular', - status: i18n._(t`Successfully removed user from organization.`), - user: { - id: requestedUser.id, - userName: requestedUser.userName, - }, - } - } else { - console.warn( - `User: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, but they do not have the right permission.`, + try { + await trx.commit() + } catch (err) { + console.error( + `Trx commit error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`, ) - return { - _type: 'error', - code: 400, - description: i18n._( - t`Permission Denied: Please contact organization admin for help with removing users.`, - ), - } + throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`)) + } + + console.info(`User: ${userKey} successfully removed user: ${requestedUser._key} from org: ${requestedOrg._key}.`) + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'remove', + target: { + resource: requestedUser.userName, + organization: { + id: requestedOrg._key, + name: requestedOrg.name, + }, // name of resource being acted upon + resourceType: 'user', // user, org, domain + }, + }) + + return { + _type: 'regular', + status: i18n._(t`Successfully removed user from organization.`), + user: { + id: requestedUser.id, + userName: requestedUser.userName, + }, } }, }) diff --git a/api/src/affiliation/mutations/request-org-affiliation.js b/api/src/affiliation/mutations/request-org-affiliation.js new file mode 100644 index 0000000000..312681475e --- /dev/null +++ b/api/src/affiliation/mutations/request-org-affiliation.js @@ -0,0 +1,200 @@ +import { GraphQLNonNull, GraphQLID } from 'graphql' +import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay' +import { t } from '@lingui/macro' + +import { inviteUserToOrgUnion } from '../unions' +import { logActivity } from '../../audit-logs/mutations/log-activity' + +export const requestOrgAffiliation = new mutationWithClientMutationId({ + name: 'RequestOrgAffiliation', + description: `This mutation allows users to request to join an organization.`, + inputFields: () => ({ + orgId: { + type: GraphQLNonNull(GraphQLID), + description: 'The organization you wish to invite the user to.', + }, + }), + outputFields: () => ({ + result: { + type: inviteUserToOrgUnion, + description: + '`InviteUserToOrgUnion` returning either a `InviteUserToOrgResult`, or `InviteUserToOrgError` object.', + resolve: (payload) => payload, + }, + }), + mutateAndGetPayload: async ( + args, + { + i18n, + query, + request, + collections, + transaction, + userKey, + auth: { userRequired, verifiedRequired }, + loaders: { loadOrgByKey, loadUserByKey }, + notify: { sendInviteRequestEmail }, + validators: { cleanseInput }, + }, + ) => { + const { id: orgId } = fromGlobalId(cleanseInput(args.orgId)) + + // Get requesting user + const user = await userRequired() + verifiedRequired({ user }) + + // Check to see if requested org exists + const org = await loadOrgByKey.load(orgId) + + if (typeof org === 'undefined') { + console.warn( + `User: ${userKey} attempted to request invite to org: ${orgId} however there is no org associated with that id.`, + ) + return { + _type: 'error', + code: 400, + description: i18n._(t`Unable to request invite to unknown organization.`), + } + } + + // Check to see if user is already a member of the org + let affiliationCursor + try { + affiliationCursor = await query` + FOR v, e IN 1..1 OUTBOUND ${org._id} affiliations + FILTER e._to == ${user._id} + RETURN e + ` + } catch (err) { + console.error( + `Database error occurred when user: ${userKey} attempted to request invite to ${orgId}, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to request invite. Please try again.`)) + } + + if (affiliationCursor.count > 0) { + const requestedAffiliation = await affiliationCursor.next() + if (requestedAffiliation.permission === 'pending') { + console.warn( + `User: ${userKey} attempted to request invite to org: ${orgId} however they have already requested to join that org.`, + ) + return { + _type: 'error', + code: 400, + description: i18n._( + t`Unable to request invite to organization with which you have already requested to join.`, + ), + } + } else { + console.warn( + `User: ${userKey} attempted to request invite to org: ${orgId} however they are already affiliated with that org.`, + ) + return { + _type: 'error', + code: 400, + description: i18n._(t`Unable to request invite to organization with which you are already affiliated.`), + } + } + } + + // Setup Transaction + const trx = await transaction(collections) + + // Create pending affiliation + try { + await trx.step( + () => + query` + WITH affiliations, organizations, users + INSERT { + _from: ${org._id}, + _to: ${user._id}, + permission: "pending", + } INTO affiliations + `, + ) + } catch (err) { + console.error( + `Transaction step error occurred while user: ${userKey} attempted to request invite to org: ${org.slug}, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to request invite. Please try again.`)) + } + + // get all org admins + let orgAdminsCursor + try { + orgAdminsCursor = await query` + WITH affiliations, organizations, users + FOR v, e IN 1..1 OUTBOUND ${org._id} affiliations + FILTER e.permission == "admin" || e.permission == "super_admin" || e.permission == "owner" + RETURN v._key + ` + } catch (err) { + console.error( + `Database error occurred when user: ${userKey} attempted to request invite to ${orgId}, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to request invite. Please try again.`)) + } + + let orgAdmins + try { + orgAdmins = await orgAdminsCursor.all() + } catch (err) { + console.error( + `Cursor error occurred when user: ${userKey} attempted to request invite to ${orgId}, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to request invite. Please try again.`)) + } + + if (orgAdmins.length > 0) { + const adminLink = `https://${request.get('host')}/admin/organizations` + // send notification to org admins + for (const userKey of orgAdmins) { + const adminUser = await loadUserByKey.load(userKey) + await sendInviteRequestEmail({ user: adminUser, orgName: org.name, adminLink }) + } + } + + // Commit Transaction + try { + await trx.commit() + } catch (err) { + console.error( + `Transaction commit error occurred while user: ${userKey} attempted to request invite to org: ${org.slug}, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to request invite. Please try again.`)) + } + + console.info(`User: ${userKey} successfully requested invite to the org: ${org.slug}.`) + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + }, + action: 'add', + target: { + resource: user.userName, + organization: { + id: org._key, + name: org.name, + }, // name of resource being acted upon + updatedProperties: [ + { + name: 'permission', + oldValue: null, + newValue: 'pending', + }, + ], + resourceType: 'user', // user, org, domain + }, + }) + + return { + _type: 'regular', + status: i18n._(t`Successfully requested invite to organization, and sent notification email.`), + } + }, +}) diff --git a/api/src/affiliation/mutations/transfer-org-ownership.js b/api/src/affiliation/mutations/transfer-org-ownership.js index 7ed9255e31..866eafded5 100644 --- a/api/src/affiliation/mutations/transfer-org-ownership.js +++ b/api/src/affiliation/mutations/transfer-org-ownership.js @@ -1,23 +1,20 @@ -import {t} from '@lingui/macro' -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {fromGlobalId, mutationWithClientMutationId} from 'graphql-relay' +import { t } from '@lingui/macro' +import { GraphQLID, GraphQLNonNull } from 'graphql' +import { fromGlobalId, mutationWithClientMutationId } from 'graphql-relay' -import {transferOrgOwnershipUnion} from '../unions' +import { transferOrgOwnershipUnion } from '../unions' export const transferOrgOwnership = new mutationWithClientMutationId({ name: 'TransferOrgOwnership', - description: - 'This mutation allows a user to transfer org ownership to another user in the given org.', + description: 'This mutation allows a user to transfer org ownership to another user in the given org.', inputFields: () => ({ orgId: { type: GraphQLNonNull(GraphQLID), - description: - 'Id of the organization the user is looking to transfer ownership of.', + description: 'Id of the organization the user is looking to transfer ownership of.', }, userId: { type: GraphQLNonNull(GraphQLID), - description: - 'Id of the user that the org ownership is being transferred to.', + description: 'Id of the user that the org ownership is being transferred to.', }, }), outputFields: () => ({ @@ -35,53 +32,36 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ query, collections, transaction, - auth: {checkOrgOwner, userRequired, verifiedRequired}, - loaders: {loadOrgByKey, loadUserByKey}, - validators: {cleanseInput}, + auth: { checkOrgOwner, userRequired, verifiedRequired }, + loaders: { loadOrgByKey, loadUserByKey }, + validators: { cleanseInput }, }, ) => { // cleanse inputs - const {id: orgKey} = fromGlobalId(cleanseInput(args.orgId)) - const {id: userTransferKey} = fromGlobalId(cleanseInput(args.userId)) + const { id: orgKey } = fromGlobalId(cleanseInput(args.orgId)) + const { id: userTransferKey } = fromGlobalId(cleanseInput(args.userId)) // protect mutation from un-authed users const requestingUser = await userRequired() // ensure that user has email verified their account - verifiedRequired({user: requestingUser}) + verifiedRequired({ user: requestingUser }) // load the requested org const org = await loadOrgByKey.load(orgKey) // ensure requested org is not undefined if (typeof org === 'undefined') { - console.warn( - `User: ${requestingUser._key} attempted to transfer org ownership of an undefined org.`, - ) + console.warn(`User: ${requestingUser._key} attempted to transfer org ownership of an undefined org.`) return { _type: 'error', code: 400, - description: i18n._( - t`Unable to transfer ownership of undefined organization.`, - ), - } - } - // ensure org is not verified - else if (org.verified) { - console.warn( - `User: ${requestingUser._key} attempted to transfer ownership of a verified org: ${org.slug}.`, - ) - return { - _type: 'error', - code: 400, - description: i18n._( - t`Unable to transfer ownership of a verified organization.`, - ), + description: i18n._(t`Unable to transfer ownership of undefined organization.`), } } // get org owner bool value - const owner = await checkOrgOwner({orgId: org._id}) + const owner = await checkOrgOwner({ orgId: org._id }) // check to see if requesting user is the org owner if (!owner) { @@ -91,9 +71,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ return { _type: 'error', code: 400, - description: i18n._( - t`Permission Denied: Please contact org owner to transfer ownership.`, - ), + description: i18n._(t`Permission Denied: Please contact org owner to transfer ownership.`), } } @@ -108,9 +86,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ return { _type: 'error', code: 400, - description: i18n._( - t`Unable to transfer ownership of an org to an undefined user.`, - ), + description: i18n._(t`Unable to transfer ownership of an org to an undefined user.`), } } @@ -127,9 +103,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ console.error( `Database error occurred for user: ${requestingUser._key} when they were attempting to transfer org: ${org.slug} ownership to user: ${requestedUser._key}: ${err}`, ) - throw new Error( - i18n._(t`Unable to transfer organization ownership. Please try again.`), - ) + throw new Error(i18n._(t`Unable to transfer organization ownership. Please try again.`)) } // check to see if requested user belongs to org @@ -159,7 +133,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ FILTER aff._from == ${org._id} FILTER aff._to == ${requestingUser._id} UPDATE { _key: aff._key } WITH { - owner: false, + permission: "admin", } IN affiliations RETURN aff `, @@ -168,9 +142,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ console.error( `Trx step error occurred for user: ${requestingUser._key} when they were attempting to transfer org: ${org.slug} ownership to user: ${requestedUser._key}: ${err}`, ) - throw new Error( - i18n._(t`Unable to transfer organization ownership. Please try again.`), - ) + throw new Error(i18n._(t`Unable to transfer organization ownership. Please try again.`)) } // set new org owner @@ -183,7 +155,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ FILTER aff._from == ${org._id} FILTER aff._to == ${requestedUser._id} UPDATE { _key: aff._key } WITH { - owner: true, + permission: "owner", } IN affiliations RETURN aff `, @@ -192,9 +164,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ console.error( `Trx step error occurred for user: ${requestingUser._key} when they were attempting to transfer org: ${org.slug} ownership to user: ${requestedUser._key}: ${err}`, ) - throw new Error( - i18n._(t`Unable to transfer organization ownership. Please try again.`), - ) + throw new Error(i18n._(t`Unable to transfer organization ownership. Please try again.`)) } // commit changes to the db @@ -204,9 +174,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ console.error( `Trx commit error occurred for user: ${requestingUser._key} when they were attempting to transfer org: ${org.slug} ownership to user: ${requestedUser._key}: ${err}`, ) - throw new Error( - i18n._(t`Unable to transfer organization ownership. Please try again.`), - ) + throw new Error(i18n._(t`Unable to transfer organization ownership. Please try again.`)) } console.info( @@ -214,9 +182,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ ) return { _type: 'regular', - status: i18n._( - t`Successfully transferred org: ${org.slug} ownership to user: ${requestedUser.userName}`, - ), + status: i18n._(t`Successfully transferred org: ${org.slug} ownership to user: ${requestedUser.userName}`), } }, }) diff --git a/api/src/affiliation/mutations/update-user-role.js b/api/src/affiliation/mutations/update-user-role.js index df985043b9..8bb77b48fd 100644 --- a/api/src/affiliation/mutations/update-user-role.js +++ b/api/src/affiliation/mutations/update-user-role.js @@ -1,10 +1,10 @@ -import {GraphQLNonNull, GraphQLID} from 'graphql' -import {mutationWithClientMutationId, fromGlobalId} from 'graphql-relay' -import {GraphQLEmailAddress} from 'graphql-scalars' -import {t} from '@lingui/macro' +import { GraphQLNonNull, GraphQLID } from 'graphql' +import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay' +import { GraphQLEmailAddress } from 'graphql-scalars' +import { t } from '@lingui/macro' -import {RoleEnums} from '../../enums' -import {updateUserRoleUnion} from '../unions' +import { RoleEnums } from '../../enums' +import { updateUserRoleUnion } from '../unions' import { logActivity } from '../../audit-logs/mutations/log-activity' export const updateUserRole = new mutationWithClientMutationId({ @@ -19,20 +19,17 @@ given organization.`, }, orgId: { type: GraphQLNonNull(GraphQLID), - description: - 'The organization that the admin, and the user both belong to.', + description: 'The organization that the admin, and the user both belong to.', }, role: { type: GraphQLNonNull(RoleEnums), - description: - 'The role that the admin wants to give to the selected user.', + description: 'The role that the admin wants to give to the selected user.', }, }), outputFields: () => ({ result: { type: updateUserRoleUnion, - description: - '`UpdateUserRoleUnion` returning either a `UpdateUserRoleResult`, or `UpdateUserRoleError` object.', + description: '`UpdateUserRoleUnion` returning either a `UpdateUserRoleResult`, or `UpdateUserRoleError` object.', resolve: (payload) => payload, }, }), @@ -44,27 +41,25 @@ given organization.`, collections, transaction, userKey, - auth: {checkPermission, userRequired, verifiedRequired, tfaRequired}, - loaders: {loadOrgByKey, loadUserByUserName}, - validators: {cleanseInput}, + auth: { checkPermission, userRequired, verifiedRequired, tfaRequired }, + loaders: { loadOrgByKey, loadUserByUserName }, + validators: { cleanseInput }, }, ) => { // Cleanse Input const userName = cleanseInput(args.userName).toLowerCase() - const {id: orgId} = fromGlobalId(cleanseInput(args.orgId)) + const { id: orgId } = fromGlobalId(cleanseInput(args.orgId)) const role = cleanseInput(args.role) // Get requesting user from db const user = await userRequired() - verifiedRequired({user}) - tfaRequired({user}) + verifiedRequired({ user }) + tfaRequired({ user }) // Make sure user is not attempting to update their own role if (user.userName === userName) { - console.warn( - `User: ${userKey} attempted to update their own role in org: ${orgId}.`, - ) + console.warn(`User: ${userKey} attempted to update their own role in org: ${orgId}.`) return { _type: 'error', code: 400, @@ -101,18 +96,17 @@ given organization.`, } // Check requesting user's permission - const permission = await checkPermission({orgId: org._id}) + const permission = await checkPermission({ orgId: org._id }) - if (permission === 'user' || typeof permission === 'undefined') { + // Only admins, owners, and super admins can update a user's role + if (['admin', 'owner', 'super_admin'].includes(permission) === false) { console.warn( `User: ${userKey} attempted to update a user: ${requestedUser._key} role in org: ${org.slug}, however they do not have permission to do so.`, ) return { _type: 'error', code: 400, - description: i18n._( - t`Permission Denied: Please contact organization admin for help with user role changes.`, - ), + description: i18n._(t`Permission Denied: Please contact organization admin for help with user role changes.`), } } @@ -129,9 +123,8 @@ given organization.`, console.error( `Database error occurred when user: ${userKey} attempted to update a user's: ${requestedUser._key} role, error: ${err}`, ) - throw new Error( - i18n._(t`Unable to update user's role. Please try again.`), - ) + + throw new Error(i18n._(t`Unable to update user's role. Please try again.`)) } if (affiliationCursor.count < 1) { @@ -141,9 +134,7 @@ given organization.`, return { _type: 'error', code: 400, - description: i18n._( - t`Unable to update role: user does not belong to organization.`, - ), + description: i18n._(t`Unable to update role: user does not belong to organization.`), } } @@ -154,81 +145,44 @@ given organization.`, console.error( `Cursor error occurred when user: ${userKey} attempted to update a user's: ${requestedUser._key} role, error: ${err}`, ) - throw new Error( - i18n._(t`Unable to update user's role. Please try again.`), - ) - } - - // Setup Transaction - const trx = await transaction(collections) - // Only super admins can create new super admins - let edge - if (role === 'super_admin' && permission === 'super_admin') { - edge = { - _from: org._id, - _to: requestedUser._id, - permission: 'super_admin', - } - } else if ( - role === 'admin' && - (permission === 'admin' || permission === 'super_admin') - ) { - // If requested user's permission is super admin, make sure they don't get downgraded - if (affiliation.permission === 'super_admin') { - console.warn( - `User: ${userKey} attempted to lower user: ${requestedUser._key} from ${affiliation.permission} to: admin.`, - ) - return { - _type: 'error', - code: 400, - description: i18n._( - t`Permission Denied: Please contact organization admin for help with updating user roles.`, - ), - } - } + throw new Error(i18n._(t`Unable to update user's role. Please try again.`)) + } - edge = { - _from: org._id, - _to: requestedUser._id, - permission: 'admin', - } - } else if (role === 'user' && permission === 'super_admin') { - // If requested user's permission is super admin or admin, make sure they don't get downgraded - if ( - affiliation.permission === 'super_admin' || - (affiliation.permission === 'admin' && permission !== 'super_admin') - ) { - console.warn( - `User: ${userKey} attempted to lower user: ${requestedUser._key} from ${affiliation.permission} to: user.`, - ) - return { - _type: 'error', - code: 400, - description: i18n._( - t`Permission Denied: Please contact organization admin for help with updating user roles.`, - ), - } + // Only super admins can update other super admins or owners + if (['owner', 'super_admin'].includes(affiliation.permission) && permission !== 'super_admin') { + console.warn( + `User: ${userKey} attempted to update a user: ${requestedUser._key} role in org: ${org.slug}, however they do not have permission to update a ${affiliation.permission}.`, + ) + return { + _type: 'error', + code: 400, + description: i18n._(t`Permission Denied: Please contact organization admin for help with user role changes.`), } + } - edge = { - _from: org._id, - _to: requestedUser._id, - permission: 'user', - } - } else { + // Only super admins can make other users super admins or owners + if (['owner', 'super_admin'].includes(role) && permission !== 'super_admin') { console.warn( - `User: ${userKey} attempted to lower user: ${requestedUser._key} from ${affiliation.permission} to: ${role}.`, + `User: ${userKey} attempted to update a user: ${requestedUser._key} role in org: ${org.slug}, however they do not have permission to make a user a ${role}.`, ) return { _type: 'error', code: 400, - description: i18n._( - t`Permission Denied: Please contact organization admin for help with updating user roles.`, - ), + description: i18n._(t`Permission Denied: Please contact organization admin for help with user role changes.`), } } + // Only super admins can create new super admins + const edge = { + _from: org._id, + _to: requestedUser._id, + permission: role, + } + + // Setup Transaction + const trx = await transaction(collections) + try { await trx.step(async () => { await query` @@ -243,9 +197,8 @@ given organization.`, console.error( `Transaction step error occurred when user: ${userKey} attempted to update a user's: ${requestedUser._key} role, error: ${err}`, ) - throw new Error( - i18n._(t`Unable to update user's role. Please try again.`), - ) + + throw new Error(i18n._(t`Unable to update user's role. Please try again.`)) } try { @@ -254,14 +207,11 @@ given organization.`, console.warn( `Transaction commit error occurred when user: ${userKey} attempted to update a user's: ${requestedUser._key} role, error: ${err}`, ) - throw new Error( - i18n._(t`Unable to update user's role. Please try again.`), - ) + + throw new Error(i18n._(t`Unable to update user's role. Please try again.`)) } - console.info( - `User: ${userKey} successful updated user: ${requestedUser._key} role to ${role} in org: ${org.slug}.`, - ) + console.info(`User: ${userKey} successful updated user: ${requestedUser._key} role to ${role} in org: ${org.slug}.`) await logActivity({ transaction, collections, diff --git a/api/src/audit-logs/loaders/load-audit-logs-by-org-id.js b/api/src/audit-logs/loaders/load-audit-logs-by-org-id.js index c41de02b2c..065157677f 100644 --- a/api/src/audit-logs/loaders/load-audit-logs-by-org-id.js +++ b/api/src/audit-logs/loaders/load-audit-logs-by-org-id.js @@ -97,37 +97,23 @@ export const loadAuditLogsByOrgId = `User: ${userKey} did not have either \`first\` or \`last\` arguments set for: loadAuditLogsByOrgId.`, ) throw new Error( - i18n._( - t`You must provide a \`first\` or \`last\` value to properly paginate the \`Log\` connection.`, - ), + i18n._(t`You must provide a \`first\` or \`last\` value to properly paginate the \`Log\` connection.`), ) } else if (typeof first !== 'undefined' && typeof last !== 'undefined') { - console.warn( - `User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadAuditLogsByOrgId.`, - ) + console.warn(`User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadAuditLogsByOrgId.`) throw new Error( - i18n._( - t`Passing both \`first\` and \`last\` to paginate the \`Log\` connection is not supported.`, - ), + i18n._(t`Passing both \`first\` and \`last\` to paginate the \`Log\` connection is not supported.`), ) } else if (typeof first === 'number' || typeof last === 'number') { /* istanbul ignore else */ if (first < 0 || last < 0) { const argSet = typeof first !== 'undefined' ? 'first' : 'last' - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadAuditLogsByOrgId.`, - ) - throw new Error( - i18n._( - t`\`${argSet}\` on the \`Log\` connection cannot be less than zero.`, - ), - ) + console.warn(`User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadAuditLogsByOrgId.`) + throw new Error(i18n._(t`\`${argSet}\` on the \`Log\` connection cannot be less than zero.`)) } else if (first > 100 || last > 100) { const argSet = typeof first !== 'undefined' ? 'first' : 'last' const amount = typeof first !== 'undefined' ? first : last - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set to ${amount} for: loadAuditLogsByOrgId.`, - ) + console.warn(`User: ${userKey} attempted to have \`${argSet}\` set to ${amount} for: loadAuditLogsByOrgId.`) throw new Error( i18n._( t`Requesting \`${amount}\` records on the \`Log\` connection exceeds the \`${argSet}\` limit of 100 records.`, @@ -141,12 +127,8 @@ export const loadAuditLogsByOrgId = } else { const argSet = typeof first !== 'undefined' ? 'first' : 'last' const typeSet = typeof first !== 'undefined' ? typeof first : typeof last - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadAuditLogsByOrgId.`, - ) - throw new Error( - i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`), - ) + console.warn(`User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadAuditLogsByOrgId.`) + throw new Error(i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`)) } let hasNextPageFilter = aql`FILTER TO_NUMBER(log._key) > TO_NUMBER(LAST(retrievedLogs)._key)` @@ -239,11 +221,7 @@ export const loadAuditLogsByOrgId = ) ` } else { - throw new Error( - i18n._( - t`Cannot query audit logs on organization without admin permission or higher.`, - ), - ) + throw new Error(i18n._(t`Cannot query audit logs on organization without admin permission or higher.`)) } let logQuery = aql`` diff --git a/api/src/audit-logs/queries/find-audit-logs.js b/api/src/audit-logs/queries/find-audit-logs.js index b50d2e0145..838c60a6c1 100644 --- a/api/src/audit-logs/queries/find-audit-logs.js +++ b/api/src/audit-logs/queries/find-audit-logs.js @@ -19,8 +19,7 @@ export const findAuditLogs = { }, search: { type: GraphQLString, - description: - 'String used to search for logs by initiant user or target resource.', + description: 'String used to search for logs by initiant user or target resource.', }, filters: { type: logFilters, @@ -48,19 +47,15 @@ export const findAuditLogs = { // Check to see if user belongs to org const permission = await checkPermission({ orgId: org?._id }) - if (permission === 'admin' || permission === 'super_admin') { - const auditLogCollection = await loadAuditLogsByOrgId({ - ...args, - orgId: org?._key, - permission, - }) - console.info(`User: ${userKey} successfully retrieved audit logs.`) - return auditLogCollection + if (['admin', 'owner', 'super_admin'].includes(permission) === false) { + throw new Error(i18n._(t`Cannot query audit logs on organization without admin permission or higher.`)) } - throw new Error( - i18n._( - t`Cannot query audit logs on organization without admin permission or higher.`, - ), - ) + const auditLogCollection = await loadAuditLogsByOrgId({ + ...args, + orgId: org?._key, + permission, + }) + console.info(`User: ${userKey} successfully retrieved audit logs.`) + return auditLogCollection }, } diff --git a/api/src/auth/__tests__/check-domain-ownership.test.js b/api/src/auth/__tests__/check-domain-ownership.test.js index 870ac2e4d2..122bfd0e42 100644 --- a/api/src/auth/__tests__/check-domain-ownership.test.js +++ b/api/src/auth/__tests__/check-domain-ownership.test.js @@ -1,15 +1,27 @@ -import {ensure, dbNameFromFile} from 'arango-tools' -import {setupI18n} from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { setupI18n } from '@lingui/core' -import {checkDomainOwnership} from '../index' +import { checkDomainOwnership } from '../index' import englishMessages from '../../locale/en/messages' import frenchMessages from '../../locale/fr/messages' import dbschema from '../../../database.json' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env describe('given the check domain ownership function', () => { - let query, drop, truncate, collections, org, domain, i18n, user + let query, drop, truncate, collections, org, verifiedOrg, unverifiedOrg, domain, i18n, user + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) const consoleOutput = [] const mockedError = (output) => consoleOutput.push(output) @@ -20,70 +32,155 @@ describe('given the check domain ownership function', () => { consoleOutput.length = 0 }) - describe('given a successful domain ownership call', () => { - let permitted - beforeAll(async () => { - ;({query, drop, truncate, collections} = await ensure({ - variables: { - dbname: dbNameFromFile(__filename), - username: 'root', - rootPassword: rootPass, - password: rootPass, - url, - }, + beforeAll(async () => { + ;({ query, drop, truncate, collections } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, - schema: dbschema, - })) + schema: dbschema, + })) + }) + beforeEach(async () => { + user = await collections.users.save({ + userName: 'test.account@istio.actually.exists', + displayName: 'Test Account', + preferredLang: 'french', + tfaValidated: false, + emailValidated: false, }) - beforeEach(async () => { - user = await collections.users.save({ - userName: 'test.account@istio.actually.exists', - displayName: 'Test Account', - preferredLang: 'french', - tfaValidated: false, - emailValidated: false, - }) - org = await collections.organizations.save({ - orgDetails: { - en: { - slug: 'treasury-board-secretariat', - acronym: 'TBS', - name: 'Treasury Board of Canada Secretariat', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - fr: { - slug: 'secretariat-conseil-tresor', - acronym: 'SCT', - name: 'Secrétariat du Conseil Trésor du Canada', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, + org = await collections.organizations.save({ + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', }, - }) - domain = await collections.domains.save({ - domain: 'test.gc.ca', - slug: 'test-gc-ca', - lastRan: null, - selectors: ['selector1', 'selector2'], - }) - await collections.ownership.save({ - _to: domain._id, - _from: org._id, - }) + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + verified: true, + }) + verifiedOrg = await collections.organizations.save({ + orgDetails: { + en: { + slug: 'other-org', + acronym: 'OO', + name: 'Other Org', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'autre-org', + acronym: 'AO', + name: 'Autre Org', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + verified: true, + }) + unverifiedOrg = await collections.organizations.save({ + orgDetails: { + en: { + slug: 'unverified-org', + acronym: 'UO', + name: 'Unverified Org', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'unverified-org', + acronym: 'UO', + name: 'Unverified Org', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }) + domain = await collections.domains.save({ + domain: 'test.gc.ca', + slug: 'test-gc-ca', + lastRan: null, + selectors: ['selector1', 'selector2'], + }) + await collections.ownership.save({ + _to: domain._id, + _from: org._id, }) - afterEach(async () => { - await truncate() + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + + describe('given a successful domain ownership call', () => { + let permitted + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, }) - afterAll(async () => { - await drop() + + describe('if the user belongs to an org which is verified and the org in question is also verified', () => { + beforeEach(async () => { + await collections.affiliations.save({ + _from: verifiedOrg._id, + _to: user._id, + permission: 'user', + }) + }) + it('will return true', async () => { + const testCheckDomainOwnerShip = checkDomainOwnership({ + i18n, + query, + userKey: user._key, + }) + permitted = await testCheckDomainOwnerShip({ + domainId: domain._id, + }) + expect(permitted).toEqual(true) + }) }) + describe('if the user belongs to an org which has a ownership for a given organization', () => { afterEach(async () => { await query` @@ -109,7 +206,7 @@ describe('given the check domain ownership function', () => { const testCheckDomainOwnerShip = checkDomainOwnership({ query, userKey: user._key, - auth: {loginRequiredBool: true}, + auth: { loginRequiredBool: true }, }) permitted = await testCheckDomainOwnerShip({ domainId: domain._id, @@ -128,9 +225,10 @@ describe('given the check domain ownership function', () => { }) it('will return true', async () => { const testCheckDomainOwnerShip = checkDomainOwnership({ + i18n, query, userKey: user._key, - auth: {loginRequiredBool: true}, + auth: { loginRequiredBool: true }, }) permitted = await testCheckDomainOwnerShip({ domainId: domain._id, @@ -150,7 +248,7 @@ describe('given the check domain ownership function', () => { const testCheckDomainOwnerShip = checkDomainOwnership({ query, userKey: user._key, - auth: {loginRequiredBool: true}, + auth: { loginRequiredBool: true }, }) permitted = await testCheckDomainOwnerShip({ domainId: domain._id, @@ -166,12 +264,10 @@ describe('given the check domain ownership function', () => { it('will return false', async () => { const testCheckDomainOwnerShip = checkDomainOwnership({ query: jest.fn().mockReturnValue({ - next: jest - .fn() - .mockReturnValue({superAdmin: true, domainOwnership: false}), + next: jest.fn().mockReturnValue({ superAdmin: true, domainOwnership: false }), }), userKey: 123, - auth: {loginRequiredBool: true}, + auth: { loginRequiredBool: true }, }) const permitted = await testCheckDomainOwnerShip({ domainId: 'domains/123', @@ -182,18 +278,18 @@ describe('given the check domain ownership function', () => { }) describe('if the user does not belong to an org which has a ownership for a given domain', () => { let permitted + beforeEach(async () => { + await collections.affiliations.save({ + _from: unverifiedOrg._id, + _to: user._id, + permission: 'user', + }) + }) it('will return false', async () => { const testCheckDomainOwnerShip = checkDomainOwnership({ - query: jest - .fn() - .mockReturnValueOnce({ - next: jest.fn().mockReturnValue({superAdmin: false}), - }) - .mockReturnValue({ - next: jest.fn().mockReturnValue([]), - }), + i18n: i18n, + query: query, userKey: '123', - auth: {loginRequiredBool: true}, }) permitted = await testCheckDomainOwnerShip({ domainId: 'domains/123', @@ -206,8 +302,8 @@ describe('given the check domain ownership function', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -220,15 +316,13 @@ describe('given the check domain ownership function', () => { let mockQuery it('returns an appropriate error message', async () => { const firstCursor = { - next: jest - .fn() - .mockRejectedValue(new Error('Cursor error occurred.')), + next: jest.fn().mockRejectedValue(new Error('Cursor error occurred.')), } mockQuery = jest.fn().mockReturnValueOnce(firstCursor) try { const testCheckDomainOwnerShip = checkDomainOwnership({ i18n, - auth: {loginRequired: true}, + auth: { loginRequired: true }, query: mockQuery, userKey: user._key, }) @@ -236,11 +330,7 @@ describe('given the check domain ownership function', () => { domainId: domain._id, }) } catch (err) { - expect(err).toEqual( - new Error( - 'Ownership check error. Unable to request domain information.', - ), - ) + expect(err).toEqual(new Error('Ownership check error. Unable to request domain information.')) expect(consoleOutput).toEqual([ `Cursor error when retrieving super admin affiliated organization ownership for user: ${user._key} and domain: ${domain._id}: Error: Cursor error occurred.`, ]) @@ -257,18 +347,13 @@ describe('given the check domain ownership function', () => { }), } const secondCursor = { - next: jest - .fn() - .mockRejectedValue(new Error('Cursor error occurred.')), + next: jest.fn().mockRejectedValue(new Error('Cursor error occurred.')), } - mockQuery = jest - .fn() - .mockReturnValueOnce(firstCursor) - .mockReturnValue(secondCursor) + mockQuery = jest.fn().mockReturnValueOnce(firstCursor).mockReturnValue(secondCursor) try { const testCheckDomainOwnerShip = checkDomainOwnership({ i18n, - auth: {loginRequired: true}, + auth: { loginRequired: true }, query: mockQuery, userKey: user._key, }) @@ -276,11 +361,7 @@ describe('given the check domain ownership function', () => { domainId: domain._id, }) } catch (err) { - expect(err).toEqual( - new Error( - 'Ownership check error. Unable to request domain information.', - ), - ) + expect(err).toEqual(new Error('Ownership check error. Unable to request domain information.')) expect(consoleOutput).toEqual([ `Cursor error when retrieving affiliated organization ownership for user: ${user._key} and domain: ${domain._id}: Error: Cursor error occurred.`, ]) @@ -290,13 +371,11 @@ describe('given the check domain ownership function', () => { describe('if a database error is encountered during super admin check', () => { let mockQuery it('returns an appropriate error message', async () => { - mockQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred.')) + mockQuery = jest.fn().mockRejectedValue(new Error('Database error occurred.')) try { const testCheckDomainOwnerShip = checkDomainOwnership({ i18n, - auth: {loginRequired: true}, + auth: { loginRequired: true }, query: mockQuery, userKey: user._key, }) @@ -304,11 +383,7 @@ describe('given the check domain ownership function', () => { domainId: domain._id, }) } catch (err) { - expect(err).toEqual( - new Error( - 'Ownership check error. Unable to request domain information.', - ), - ) + expect(err).toEqual(new Error('Ownership check error. Unable to request domain information.')) expect(consoleOutput).toEqual([ `Database error when retrieving super admin affiliated organization ownership for user: ${user._key} and domain: ${domain._id}: Error: Database error occurred.`, ]) @@ -332,7 +407,7 @@ describe('given the check domain ownership function', () => { try { const testCheckDomainOwnerShip = checkDomainOwnership({ i18n, - auth: {loginRequired: true}, + auth: { loginRequired: true }, query: mockQuery, userKey: user._key, }) @@ -340,11 +415,7 @@ describe('given the check domain ownership function', () => { domainId: domain._id, }) } catch (err) { - expect(err).toEqual( - new Error( - 'Ownership check error. Unable to request domain information.', - ), - ) + expect(err).toEqual(new Error('Ownership check error. Unable to request domain information.')) expect(consoleOutput).toEqual([ `Database error when retrieving affiliated organization ownership for user: ${user._key} and domain: ${domain._id}: Error: Database error occurred.`, ]) @@ -357,8 +428,8 @@ describe('given the check domain ownership function', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -371,15 +442,13 @@ describe('given the check domain ownership function', () => { let mockQuery it('returns an appropriate error message', async () => { const firstCursor = { - next: jest - .fn() - .mockRejectedValue(new Error('Cursor error occurred.')), + next: jest.fn().mockRejectedValue(new Error('Cursor error occurred.')), } mockQuery = jest.fn().mockReturnValueOnce(firstCursor) try { const testCheckDomainOwnerShip = checkDomainOwnership({ i18n, - auth: {loginRequired: true}, + auth: { loginRequired: true }, query: mockQuery, userKey: user._key, }) @@ -408,18 +477,13 @@ describe('given the check domain ownership function', () => { }), } const secondCursor = { - next: jest - .fn() - .mockRejectedValue(new Error('Cursor error occurred.')), + next: jest.fn().mockRejectedValue(new Error('Cursor error occurred.')), } - mockQuery = jest - .fn() - .mockReturnValueOnce(firstCursor) - .mockReturnValue(secondCursor) + mockQuery = jest.fn().mockReturnValueOnce(firstCursor).mockReturnValue(secondCursor) try { const testCheckDomainOwnerShip = checkDomainOwnership({ i18n, - auth: {loginRequired: true}, + auth: { loginRequired: true }, query: mockQuery, userKey: user._key, }) @@ -441,13 +505,11 @@ describe('given the check domain ownership function', () => { describe('if a database error is encountered during super admin check', () => { let mockQuery it('returns an appropriate error message', async () => { - mockQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred.')) + mockQuery = jest.fn().mockRejectedValue(new Error('Database error occurred.')) try { const testCheckDomainOwnerShip = checkDomainOwnership({ i18n, - auth: {loginRequired: true}, + auth: { loginRequired: true }, query: mockQuery, userKey: user._key, }) @@ -483,7 +545,7 @@ describe('given the check domain ownership function', () => { try { const testCheckDomainOwnerShip = checkDomainOwnership({ i18n, - auth: {loginRequired: true}, + auth: { loginRequired: true }, query: mockQuery, userKey: user._key, }) diff --git a/api/src/auth/__tests__/check-domain-permission.test.js b/api/src/auth/__tests__/check-domain-permission.test.js index 931a27295f..d39b081675 100644 --- a/api/src/auth/__tests__/check-domain-permission.test.js +++ b/api/src/auth/__tests__/check-domain-permission.test.js @@ -1,12 +1,12 @@ -import {ensure, dbNameFromFile} from 'arango-tools' -import {setupI18n} from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { setupI18n } from '@lingui/core' -import {checkDomainPermission} from '../index' +import { checkDomainPermission } from '../index' import englishMessages from '../../locale/en/messages' import frenchMessages from '../../locale/fr/messages' import dbschema from '../../../database.json' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env describe('given the check domain permission function', () => { let query, drop, truncate, collections, org, domain, i18n @@ -23,7 +23,7 @@ describe('given the check domain permission function', () => { describe('given a successful domain permission call', () => { let user, permitted beforeAll(async () => { - ;({query, drop, truncate, collections} = await ensure({ + ;({ query, drop, truncate, collections } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -103,7 +103,7 @@ describe('given the check domain permission function', () => { query, userKey: user._key, }) - permitted = await testCheckDomainPermission({domainId: domain._id}) + permitted = await testCheckDomainPermission({ domainId: domain._id }) expect(permitted).toEqual(true) }) }) @@ -121,7 +121,7 @@ describe('given the check domain permission function', () => { query, userKey: user._key, }) - permitted = await testCheckDomainPermission({domainId: domain._id}) + permitted = await testCheckDomainPermission({ domainId: domain._id }) expect(permitted).toEqual(true) }) }) @@ -138,7 +138,7 @@ describe('given the check domain permission function', () => { query, userKey: user._key, }) - permitted = await testCheckDomainPermission({domainId: domain._id}) + permitted = await testCheckDomainPermission({ domainId: domain._id }) expect(permitted).toEqual(true) }) }) @@ -152,11 +152,11 @@ describe('given the check domain permission function', () => { const testCheckDomainPermission = checkDomainPermission({ query: jest .fn() - .mockReturnValueOnce({count: 0}) - .mockReturnValue({next: jest.fn().mockReturnValue([])}), + .mockReturnValueOnce({ count: 0 }) + .mockReturnValue({ next: jest.fn().mockReturnValue(false) }), userKey: 123, }) - permitted = await testCheckDomainPermission({domainId: 'domains/123'}) + permitted = await testCheckDomainPermission({ domainId: 'domains/123' }) expect(permitted).toEqual(false) }) }) @@ -165,8 +165,8 @@ describe('given the check domain permission function', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -178,22 +178,16 @@ describe('given the check domain permission function', () => { describe('if a database error is encountered during super admin permission check', () => { let mockQuery it('returns an appropriate error message', async () => { - mockQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred.')) + mockQuery = jest.fn().mockRejectedValue(new Error('Database error occurred.')) try { const testCheckDomainPermission = checkDomainPermission({ i18n, query: mockQuery, userKey: 123, }) - await testCheckDomainPermission({domainId: 'domains/123'}) + await testCheckDomainPermission({ domainId: 'domains/123' }) } catch (err) { - expect(err).toEqual( - new Error( - 'Permission check error. Unable to request domain information.', - ), - ) + expect(err).toEqual(new Error('Permission check error. Unable to request domain information.')) expect(consoleOutput).toEqual([ `Database error when retrieving super admin claims for user: 123 and domain: domains/123: Error: Database error occurred.`, ]) @@ -205,7 +199,7 @@ describe('given the check domain permission function', () => { it('returns an appropriate error message', async () => { mockQuery = jest .fn() - .mockReturnValueOnce({count: 0}) + .mockReturnValueOnce({ count: 0 }) .mockRejectedValue(new Error('Database error occurred.')) try { const testCheckDomainPermission = checkDomainPermission({ @@ -213,13 +207,9 @@ describe('given the check domain permission function', () => { query: mockQuery, userKey: 123, }) - await testCheckDomainPermission({domainId: 'domains/123'}) + await testCheckDomainPermission({ domainId: 'domains/123' }) } catch (err) { - expect(err).toEqual( - new Error( - 'Permission check error. Unable to request domain information.', - ), - ) + expect(err).toEqual(new Error('Permission check error. Unable to request domain information.')) expect(consoleOutput).toEqual([ `Database error when retrieving affiliated organization claims for user: 123 and domain: domains/123: Error: Database error occurred.`, ]) @@ -234,23 +224,16 @@ describe('given the check domain permission function', () => { throw new Error('Cursor error occurred.') }, } - mockQuery = jest - .fn() - .mockReturnValueOnce({count: 0}) - .mockReturnValue(cursor) + mockQuery = jest.fn().mockReturnValueOnce({ count: 0 }).mockReturnValue(cursor) try { const testCheckDomainPermission = checkDomainPermission({ i18n, query: mockQuery, userKey: 123, }) - await testCheckDomainPermission({domainId: 'domains/123'}) + await testCheckDomainPermission({ domainId: 'domains/123' }) } catch (err) { - expect(err).toEqual( - new Error( - 'Permission check error. Unable to request domain information.', - ), - ) + expect(err).toEqual(new Error('Permission check error. Unable to request domain information.')) expect(consoleOutput).toEqual([ `Cursor error when retrieving affiliated organization claims for user: 123 and domain: domains/123: Error: Cursor error occurred.`, ]) @@ -263,8 +246,8 @@ describe('given the check domain permission function', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -276,16 +259,14 @@ describe('given the check domain permission function', () => { describe('if a database error is encountered during super admin permission check', () => { let mockQuery it('returns an appropriate error message', async () => { - mockQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred.')) + mockQuery = jest.fn().mockRejectedValue(new Error('Database error occurred.')) try { const testCheckDomainPermission = checkDomainPermission({ i18n, query: mockQuery, userKey: 123, }) - await testCheckDomainPermission({domainId: 'domains/123'}) + await testCheckDomainPermission({ domainId: 'domains/123' }) } catch (err) { expect(err).toEqual( new Error( @@ -303,7 +284,7 @@ describe('given the check domain permission function', () => { it('returns an appropriate error message', async () => { mockQuery = jest .fn() - .mockReturnValueOnce({count: 0}) + .mockReturnValueOnce({ count: 0 }) .mockRejectedValue(new Error('Database error occurred.')) try { const testCheckDomainPermission = checkDomainPermission({ @@ -311,7 +292,7 @@ describe('given the check domain permission function', () => { query: mockQuery, userKey: 123, }) - await testCheckDomainPermission({domainId: 'domains/123'}) + await testCheckDomainPermission({ domainId: 'domains/123' }) } catch (err) { expect(err).toEqual( new Error( @@ -332,17 +313,14 @@ describe('given the check domain permission function', () => { throw new Error('Cursor error occurred.') }, } - mockQuery = jest - .fn() - .mockReturnValueOnce({count: 1}) - .mockReturnValue(cursor) + mockQuery = jest.fn().mockReturnValueOnce({ count: 1 }).mockReturnValue(cursor) try { const testCheckDomainPermission = checkDomainPermission({ i18n, query: mockQuery, userKey: 123, }) - await testCheckDomainPermission({domainId: domain._id}) + await testCheckDomainPermission({ domainId: domain._id }) } catch (err) { expect(err).toEqual(new Error('todo')) expect(consoleOutput).toEqual([ diff --git a/api/src/auth/__tests__/check-org-owner.test.js b/api/src/auth/__tests__/check-org-owner.test.js index e4fc8c9b79..ade0b35e4c 100644 --- a/api/src/auth/__tests__/check-org-owner.test.js +++ b/api/src/auth/__tests__/check-org-owner.test.js @@ -1,18 +1,18 @@ -import {ensure, dbNameFromFile} from 'arango-tools' -import {setupI18n} from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { setupI18n } from '@lingui/core' -import {checkOrgOwner} from '../check-org-owner' +import { checkOrgOwner } from '../check-org-owner' import englishMessages from '../../locale/en/messages' import frenchMessages from '../../locale/fr/messages' import dbschema from '../../../database.json' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env describe('given the checkOrgOwner function', () => { describe('given a successful check', () => { let query, drop, truncate, collections, user, org beforeAll(async () => { - ;({query, drop, truncate, collections} = await ensure({ + ;({ query, drop, truncate, collections } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -68,14 +68,13 @@ describe('given the checkOrgOwner function', () => { await collections.affiliations.save({ _from: org._id, _to: user._id, - permission: 'admin', - owner: true, + permission: 'owner', }) }) it('returns true', async () => { - const testCheckOrgOwner = checkOrgOwner({query, userKey: user._key}) + const testCheckOrgOwner = checkOrgOwner({ query, userKey: user._key }) - const result = await testCheckOrgOwner({orgId: org._id}) + const result = await testCheckOrgOwner({ orgId: org._id }) expect(result).toEqual(true) }) @@ -86,13 +85,12 @@ describe('given the checkOrgOwner function', () => { _from: org._id, _to: user._id, permission: 'admin', - owner: false, }) }) it('returns false', async () => { - const testCheckOrgOwner = checkOrgOwner({query, userKey: user._key}) + const testCheckOrgOwner = checkOrgOwner({ query, userKey: user._key }) - const result = await testCheckOrgOwner({orgId: org._id}) + const result = await testCheckOrgOwner({ orgId: org._id }) expect(result).toEqual(false) }) @@ -113,7 +111,7 @@ describe('given the checkOrgOwner function', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, + en: { plurals: {} }, }, locales: ['en'], messages: { @@ -123,9 +121,7 @@ describe('given the checkOrgOwner function', () => { }) describe('database error occurs', () => { it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred')) + const mockedQuery = jest.fn().mockRejectedValue(new Error('Database error occurred')) const testCheckOrgOwner = checkOrgOwner({ i18n, @@ -134,11 +130,9 @@ describe('given the checkOrgOwner function', () => { }) try { - await testCheckOrgOwner({orgId: '123'}) + await testCheckOrgOwner({ orgId: '123' }) } catch (err) { - expect(err).toEqual( - new Error(`Unable to load owner information. Please try again.`), - ) + expect(err).toEqual(new Error(`Unable to load owner information. Please try again.`)) } expect(consoleOutput).toEqual([ `Database error when checking to see if user: 123 is the owner of: 123: Error: Database error occurred`, @@ -148,9 +142,7 @@ describe('given the checkOrgOwner function', () => { describe('cursor error occurs', () => { it('throws an error', async () => { const mockedQuery = jest.fn().mockReturnValue({ - next: jest - .fn() - .mockRejectedValue(new Error('Cursor error occurred')), + next: jest.fn().mockRejectedValue(new Error('Cursor error occurred')), }) const testCheckOrgOwner = checkOrgOwner({ @@ -160,11 +152,9 @@ describe('given the checkOrgOwner function', () => { }) try { - await testCheckOrgOwner({orgId: '123'}) + await testCheckOrgOwner({ orgId: '123' }) } catch (err) { - expect(err).toEqual( - new Error(`Unable to load owner information. Please try again.`), - ) + expect(err).toEqual(new Error(`Unable to load owner information. Please try again.`)) } expect(consoleOutput).toEqual([ `Cursor error when checking to see if user: 123 is the owner of: 123: Error: Cursor error occurred`, @@ -177,7 +167,7 @@ describe('given the checkOrgOwner function', () => { i18n = setupI18n({ locale: 'fr', localeData: { - fr: {plurals: {}}, + fr: { plurals: {} }, }, locales: ['fr'], messages: { @@ -187,9 +177,7 @@ describe('given the checkOrgOwner function', () => { }) describe('database error occurs', () => { it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred')) + const mockedQuery = jest.fn().mockRejectedValue(new Error('Database error occurred')) const testCheckOrgOwner = checkOrgOwner({ i18n, @@ -198,12 +186,10 @@ describe('given the checkOrgOwner function', () => { }) try { - await testCheckOrgOwner({orgId: '123'}) + await testCheckOrgOwner({ orgId: '123' }) } catch (err) { expect(err).toEqual( - new Error( - `Impossible de charger les informations sur le propriétaire. Veuillez réessayer.`, - ), + new Error(`Impossible de charger les informations sur le propriétaire. Veuillez réessayer.`), ) } expect(consoleOutput).toEqual([ @@ -214,9 +200,7 @@ describe('given the checkOrgOwner function', () => { describe('cursor error occurs', () => { it('throws an error', async () => { const mockedQuery = jest.fn().mockReturnValue({ - next: jest - .fn() - .mockRejectedValue(new Error('Cursor error occurred')), + next: jest.fn().mockRejectedValue(new Error('Cursor error occurred')), }) const testCheckOrgOwner = checkOrgOwner({ @@ -226,12 +210,10 @@ describe('given the checkOrgOwner function', () => { }) try { - await testCheckOrgOwner({orgId: '123'}) + await testCheckOrgOwner({ orgId: '123' }) } catch (err) { expect(err).toEqual( - new Error( - `Impossible de charger les informations sur le propriétaire. Veuillez réessayer.`, - ), + new Error(`Impossible de charger les informations sur le propriétaire. Veuillez réessayer.`), ) } expect(consoleOutput).toEqual([ diff --git a/api/src/auth/__tests__/check-permission.test.js b/api/src/auth/__tests__/check-permission.test.js index b60a82ac6a..17c2a52964 100644 --- a/api/src/auth/__tests__/check-permission.test.js +++ b/api/src/auth/__tests__/check-permission.test.js @@ -1,12 +1,12 @@ -import {ensure, dbNameFromFile} from 'arango-tools' -import {setupI18n} from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { setupI18n } from '@lingui/core' -import {checkPermission} from '..' +import { checkPermission } from '..' import englishMessages from '../../locale/en/messages' import frenchMessages from '../../locale/fr/messages' import dbschema from '../../../database.json' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env describe('given the check permission function', () => { let query, drop, truncate, collections, i18n @@ -22,7 +22,7 @@ describe('given the check permission function', () => { describe('given a successful permission call', () => { beforeAll(async () => { - ;({query, drop, truncate, collections} = await ensure({ + ;({ query, drop, truncate, collections } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -102,7 +102,7 @@ describe('given the check permission function', () => { userKey: user._key, query, }) - const permission = await testCheckPermission({orgId: org._id}) + const permission = await testCheckPermission({ orgId: org._id }) expect(permission).toEqual('super_admin') }) }) @@ -135,7 +135,7 @@ describe('given the check permission function', () => { userKey: user._key, query, }) - const permission = await testCheckPermission({orgId: org._id}) + const permission = await testCheckPermission({ orgId: org._id }) expect(permission).toEqual('admin') }) }) @@ -168,7 +168,7 @@ describe('given the check permission function', () => { userKey: user._key, query, }) - const permission = await testCheckPermission({orgId: org._id}) + const permission = await testCheckPermission({ orgId: org._id }) expect(permission).toEqual('user') }) }) @@ -193,8 +193,8 @@ describe('given the check permission function', () => { userKey: user._key, query, }) - const permission = await testCheckPermission({orgId: org._id}) - expect(permission).toEqual(undefined) + const permission = await testCheckPermission({ orgId: org._id }) + expect(permission).toEqual(null) }) }) }) @@ -204,8 +204,8 @@ describe('given the check permission function', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -217,9 +217,7 @@ describe('given the check permission function', () => { describe('database error occurs', () => { describe('when checking if super admin', () => { it('throws an error', async () => { - query = jest - .fn() - .mockRejectedValue(new Error('Database error occurred.')) + query = jest.fn().mockRejectedValue(new Error('Database error occurred.')) try { const testCheckPermission = checkPermission({ @@ -227,11 +225,9 @@ describe('given the check permission function', () => { userKey: '1', query, }) - await testCheckPermission({orgId: 'organizations/1'}) + await testCheckPermission({ orgId: 'organizations/1' }) } catch (err) { - expect(err).toEqual( - new Error('Authentication error. Please sign in.'), - ) + expect(err).toEqual(new Error('Authentication error. Please sign in.')) } expect(consoleOutput).toEqual([ @@ -256,11 +252,9 @@ describe('given the check permission function', () => { userKey: '1', query, }) - await testCheckPermission({orgId: 'organizations/1'}) + await testCheckPermission({ orgId: 'organizations/1' }) } catch (err) { - expect(err).toEqual( - new Error('Authentication error. Please sign in.'), - ) + expect(err).toEqual(new Error('Authentication error. Please sign in.')) } expect(consoleOutput).toEqual([ @@ -285,11 +279,9 @@ describe('given the check permission function', () => { userKey: '1', query, }) - await testCheckPermission({orgId: 'organizations/1'}) + await testCheckPermission({ orgId: 'organizations/1' }) } catch (err) { - expect(err).toEqual( - new Error('Unable to check permission. Please try again.'), - ) + expect(err).toEqual(new Error('Unable to check permission. Please try again.')) } expect(consoleOutput).toEqual([ @@ -319,11 +311,9 @@ describe('given the check permission function', () => { userKey: '1', query, }) - await testCheckPermission({orgId: 'organizations/1'}) + await testCheckPermission({ orgId: 'organizations/1' }) } catch (err) { - expect(err).toEqual( - new Error('Unable to check permission. Please try again.'), - ) + expect(err).toEqual(new Error('Unable to check permission. Please try again.')) } expect(consoleOutput).toEqual([ @@ -338,8 +328,8 @@ describe('given the check permission function', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -351,9 +341,7 @@ describe('given the check permission function', () => { describe('database error occurs', () => { describe('when checking if super admin', () => { it('throws an error', async () => { - query = jest - .fn() - .mockRejectedValue(new Error('Database error occurred.')) + query = jest.fn().mockRejectedValue(new Error('Database error occurred.')) try { const testCheckPermission = checkPermission({ @@ -361,13 +349,9 @@ describe('given the check permission function', () => { userKey: '1', query, }) - await testCheckPermission({orgId: 'organizations/1'}) + await testCheckPermission({ orgId: 'organizations/1' }) } catch (err) { - expect(err).toEqual( - new Error( - "Erreur d'authentification. Veuillez vous connecter.", - ), - ) + expect(err).toEqual(new Error("Erreur d'authentification. Veuillez vous connecter.")) } expect(consoleOutput).toEqual([ @@ -392,13 +376,9 @@ describe('given the check permission function', () => { userKey: '1', query, }) - await testCheckPermission({orgId: 'organizations/1'}) + await testCheckPermission({ orgId: 'organizations/1' }) } catch (err) { - expect(err).toEqual( - new Error( - "Erreur d'authentification. Veuillez vous connecter.", - ), - ) + expect(err).toEqual(new Error("Erreur d'authentification. Veuillez vous connecter.")) } expect(consoleOutput).toEqual([ @@ -423,13 +403,9 @@ describe('given the check permission function', () => { userKey: '1', query, }) - await testCheckPermission({orgId: 'organizations/1'}) + await testCheckPermission({ orgId: 'organizations/1' }) } catch (err) { - expect(err).toEqual( - new Error( - "Impossible de vérifier l'autorisation. Veuillez réessayer.", - ), - ) + expect(err).toEqual(new Error("Impossible de vérifier l'autorisation. Veuillez réessayer.")) } expect(consoleOutput).toEqual([ @@ -459,13 +435,9 @@ describe('given the check permission function', () => { userKey: '1', query, }) - await testCheckPermission({orgId: 'organizations/1'}) + await testCheckPermission({ orgId: 'organizations/1' }) } catch (err) { - expect(err).toEqual( - new Error( - "Impossible de vérifier l'autorisation. Veuillez réessayer.", - ), - ) + expect(err).toEqual(new Error("Impossible de vérifier l'autorisation. Veuillez réessayer.")) } expect(consoleOutput).toEqual([ diff --git a/api/src/auth/__tests__/check-user-belongs-to-org.test.js b/api/src/auth/__tests__/check-user-belongs-to-org.test.js index 0300544a93..6957b05017 100644 --- a/api/src/auth/__tests__/check-user-belongs-to-org.test.js +++ b/api/src/auth/__tests__/check-user-belongs-to-org.test.js @@ -1,18 +1,18 @@ -import {ensure, dbNameFromFile} from 'arango-tools' -import {setupI18n} from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { setupI18n } from '@lingui/core' -import {checkUserBelongsToOrg} from '../check-user-belongs-to-org' +import { checkUserBelongsToOrg } from '../check-user-belongs-to-org' import englishMessages from '../../locale/en/messages' import frenchMessages from '../../locale/fr/messages' import dbschema from '../../../database.json' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env describe('given the checkUserBelongsToOrg function', () => { describe('given a successful call', () => { let query, drop, truncate, collections, user, org beforeAll(async () => { - ;({query, drop, truncate, collections} = await ensure({ + ;({ query, drop, truncate, collections } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -68,8 +68,7 @@ describe('given the checkUserBelongsToOrg function', () => { await collections.affiliations.save({ _from: org._id, _to: user._id, - permission: 'admin', - owner: true, + permission: 'owner', }) }) it('returns true', async () => { @@ -78,7 +77,7 @@ describe('given the checkUserBelongsToOrg function', () => { userKey: user._key, }) - const result = await testCheckUserBelongsToOrg({orgId: org._id}) + const result = await testCheckUserBelongsToOrg({ orgId: org._id }) expect(result).toEqual(true) }) @@ -90,7 +89,7 @@ describe('given the checkUserBelongsToOrg function', () => { userKey: user._key, }) - const result = await testCheckUserBelongsToOrg({orgId: org._id}) + const result = await testCheckUserBelongsToOrg({ orgId: org._id }) expect(result).toEqual(false) }) @@ -111,7 +110,7 @@ describe('given the checkUserBelongsToOrg function', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, + en: { plurals: {} }, }, locales: ['en'], messages: { @@ -121,9 +120,7 @@ describe('given the checkUserBelongsToOrg function', () => { }) describe('database error occurs', () => { it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred')) + const mockedQuery = jest.fn().mockRejectedValue(new Error('Database error occurred')) const testCheckUserBelongsToOrg = checkUserBelongsToOrg({ i18n, @@ -132,13 +129,9 @@ describe('given the checkUserBelongsToOrg function', () => { }) try { - await testCheckUserBelongsToOrg({orgId: '123'}) + await testCheckUserBelongsToOrg({ orgId: '123' }) } catch (err) { - expect(err).toEqual( - new Error( - `Unable to load affiliation information. Please try again.`, - ), - ) + expect(err).toEqual(new Error(`Unable to load affiliation information. Please try again.`)) } expect(consoleOutput).toEqual([ `Database error when checking to see if user: 123 belongs to org: 123: Error: Database error occurred`, @@ -151,7 +144,7 @@ describe('given the checkUserBelongsToOrg function', () => { i18n = setupI18n({ locale: 'fr', localeData: { - fr: {plurals: {}}, + fr: { plurals: {} }, }, locales: ['fr'], messages: { @@ -161,9 +154,7 @@ describe('given the checkUserBelongsToOrg function', () => { }) describe('database error occurs', () => { it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred')) + const mockedQuery = jest.fn().mockRejectedValue(new Error('Database error occurred')) const testCheckUserBelongsToOrg = checkUserBelongsToOrg({ i18n, @@ -172,13 +163,9 @@ describe('given the checkUserBelongsToOrg function', () => { }) try { - await testCheckUserBelongsToOrg({orgId: '123'}) + await testCheckUserBelongsToOrg({ orgId: '123' }) } catch (err) { - expect(err).toEqual( - new Error( - `Impossible de charger les informations d'affiliation. Veuillez réessayer.`, - ), - ) + expect(err).toEqual(new Error(`Impossible de charger les informations d'affiliation. Veuillez réessayer.`)) } expect(consoleOutput).toEqual([ `Database error when checking to see if user: 123 belongs to org: 123: Error: Database error occurred`, diff --git a/api/src/auth/check-domain-ownership.js b/api/src/auth/check-domain-ownership.js index efedb347be..982a78e693 100644 --- a/api/src/auth/check-domain-ownership.js +++ b/api/src/auth/check-domain-ownership.js @@ -1,13 +1,10 @@ -import {t} from '@lingui/macro' +import { t } from '@lingui/macro' export const checkDomainOwnership = - ({i18n, - query, - userKey, - auth: {loginRequiredBool}}) => - async ({domainId}) => { - let userAffiliatedOwnership, ownership - const userKeyString = `users/${userKey}` + ({ i18n, query, userKey }) => + async ({ domainId }) => { + let userAffiliatedOwnership, ownership + const userKeyString = `users/${userKey}` // Check to see if the user is a super admin let superAdminAffiliationCursor @@ -29,9 +26,7 @@ export const checkDomainOwnership = console.error( `Database error when retrieving super admin affiliated organization ownership for user: ${userKey} and domain: ${domainId}: ${err}`, ) - throw new Error( - i18n._(t`Ownership check error. Unable to request domain information.`), - ) + throw new Error(i18n._(t`Ownership check error. Unable to request domain information.`)) } let superAdminAffiliation @@ -41,12 +36,10 @@ export const checkDomainOwnership = console.error( `Cursor error when retrieving super admin affiliated organization ownership for user: ${userKey} and domain: ${domainId}: ${err}`, ) - throw new Error( - i18n._(t`Ownership check error. Unable to request domain information.`), - ) + throw new Error(i18n._(t`Ownership check error. Unable to request domain information.`)) } - if (superAdminAffiliation.superAdmin || !loginRequiredBool) { + if (superAdminAffiliation.superAdmin) { return !!superAdminAffiliation.domainOwnership } @@ -54,18 +47,22 @@ export const checkDomainOwnership = try { userAffiliatedOwnership = await query` WITH affiliations, domains, organizations, ownership, users - LET userAffiliations = (FOR v, e IN 1..1 ANY ${userKeyString} affiliations RETURN e._from) - LET domainOwnerships = (FOR v, e IN 1..1 ANY ${domainId} ownership RETURN e._from) + LET userAffiliations = ( + FOR v, e IN 1..1 ANY ${userKeyString} affiliations + FILTER e.permission != "pending" + RETURN v + ) + LET hasVerifiedOrgAffiliation = POSITION(userAffiliations[*].verified, true) + LET domainOwnerships = (FOR v, e IN 1..1 ANY ${domainId} ownership RETURN v) + LET domainBelongsToVerifiedOrg = POSITION(domainOwnerships[*].verified, true) LET affiliatedOwnership = INTERSECTION(userAffiliations, domainOwnerships) - RETURN affiliatedOwnership + RETURN (domainBelongsToVerifiedOrg && hasVerifiedOrgAffiliation) || LENGTH(affiliatedOwnership) > 0 ` } catch (err) { console.error( `Database error when retrieving affiliated organization ownership for user: ${userKey} and domain: ${domainId}: ${err}`, ) - throw new Error( - i18n._(t`Ownership check error. Unable to request domain information.`), - ) + throw new Error(i18n._(t`Ownership check error. Unable to request domain information.`)) } try { @@ -74,10 +71,8 @@ export const checkDomainOwnership = console.error( `Cursor error when retrieving affiliated organization ownership for user: ${userKey} and domain: ${domainId}: ${err}`, ) - throw new Error( - i18n._(t`Ownership check error. Unable to request domain information.`), - ) + throw new Error(i18n._(t`Ownership check error. Unable to request domain information.`)) } - return ownership[0] !== undefined + return ownership } diff --git a/api/src/auth/check-domain-permission.js b/api/src/auth/check-domain-permission.js index 24006b11aa..54d8e300e1 100644 --- a/api/src/auth/check-domain-permission.js +++ b/api/src/auth/check-domain-permission.js @@ -1,66 +1,65 @@ -import {t} from '@lingui/macro' +import { t } from '@lingui/macro' export const checkDomainPermission = - ({i18n, query, userKey}) => - async ({domainId}) => { - let userAffiliatedClaims, claim - const userKeyString = `users/${userKey}` + ({ i18n, query, userKey }) => + async ({ domainId }) => { + let userAffiliatedClaims + const userKeyString = `users/${userKey}` - // Check to see if the user is a super admin - let superAdminAffiliationCursor - try { - superAdminAffiliationCursor = await query` + // Check to see if the user is a super admin + let superAdminAffiliationCursor + try { + superAdminAffiliationCursor = await query` WITH affiliations, organizations, users FOR v, e IN 1..1 ANY ${userKeyString} affiliations FILTER e.permission == 'super_admin' RETURN e.from ` - } catch (err) { - console.error( - `Database error when retrieving super admin claims for user: ${userKey} and domain: ${domainId}: ${err}`, - ) - throw new Error( - i18n._( - t`Permission check error. Unable to request domain information.`, - ), - ) - } + } catch (err) { + console.error( + `Database error when retrieving super admin claims for user: ${userKey} and domain: ${domainId}: ${err}`, + ) + throw new Error(i18n._(t`Permission check error. Unable to request domain information.`)) + } - if (superAdminAffiliationCursor.count > 0) { - return true - } + if (superAdminAffiliationCursor.count > 0) { + return true + } - // Retrieve user affiliations and affiliated organizations owning provided domain - try { - userAffiliatedClaims = await query` - WITH affiliations, claims, domains, organizations, users - LET userAffiliations = (FOR v, e IN 1..1 ANY ${userKeyString} affiliations RETURN e._from) - LET domainClaims = (FOR v, e IN 1..1 ANY ${domainId} claims RETURN e._from) - LET affiliatedClaims = INTERSECTION(userAffiliations, domainClaims) - RETURN affiliatedClaims - ` - } catch (err) { - console.error( - `Database error when retrieving affiliated organization claims for user: ${userKey} and domain: ${domainId}: ${err}`, + // Retrieve user affiliations and affiliated organizations owning provided domain + try { + userAffiliatedClaims = await query` + WITH domains, users, organizations + LET userAffiliations = ( + FOR v, e IN 1..1 ANY ${userKeyString} affiliations + FILTER e.permission != "pending" + RETURN v ) - throw new Error( - i18n._( - t`Permission check error. Unable to request domain information.`, - ), + LET hasVerifiedOrgAffiliation = POSITION(userAffiliations[*].verified, true) + LET domainOrgClaims = ( + FOR v, e IN 1..1 ANY ${domainId} claims + RETURN v ) - } + LET domainBelongsToVerifiedOrg = POSITION(domainOrgClaims[*].verified, true) + LET affiliatedClaims = INTERSECTION(userAffiliations, domainOrgClaims) + RETURN (domainBelongsToVerifiedOrg && hasVerifiedOrgAffiliation) || LENGTH(affiliatedClaims) > 0 + ` + } catch (err) { + console.error( + `Database error when retrieving affiliated organization claims for user: ${userKey} and domain: ${domainId}: ${err}`, + ) + throw new Error(i18n._(t`Permission check error. Unable to request domain information.`)) + } - try { - claim = await userAffiliatedClaims.next() - } catch (err) { - console.error( - `Cursor error when retrieving affiliated organization claims for user: ${userKey} and domain: ${domainId}: ${err}`, - ) - throw new Error( - i18n._( - t`Permission check error. Unable to request domain information.`, - ), - ) - } - return claim[0] !== undefined + let affiliated + try { + affiliated = await userAffiliatedClaims.next() + } catch (err) { + console.error( + `Cursor error when retrieving affiliated organization claims for user: ${userKey} and domain: ${domainId}: ${err}`, + ) + throw new Error(i18n._(t`Permission check error. Unable to request domain information.`)) } + + return affiliated + } diff --git a/api/src/auth/check-org-owner.js b/api/src/auth/check-org-owner.js index f8baf74e0b..51fac07950 100644 --- a/api/src/auth/check-org-owner.js +++ b/api/src/auth/check-org-owner.js @@ -1,39 +1,31 @@ -import {t} from '@lingui/macro' +import { t } from '@lingui/macro' export const checkOrgOwner = - ({i18n, query, userKey}) => - async ({orgId}) => { - const userIdString = `users/${userKey}` + ({ i18n, query, userKey }) => + async ({ orgId }) => { + const userIdString = `users/${userKey}` - // find affiliation - let affiliationCursor - try { - affiliationCursor = await query` + // find affiliation + let affiliationCursor + try { + affiliationCursor = await query` WITH affiliations, organizations, users FOR v, e IN 1..1 OUTBOUND ${orgId} affiliations FILTER e._to == ${userIdString} - RETURN e.owner + RETURN e.permission == "owner" ` - } catch (err) { - console.error( - `Database error when checking to see if user: ${userKey} is the owner of: ${orgId}: ${err}`, - ) - throw new Error( - i18n._(t`Unable to load owner information. Please try again.`), - ) - } - - let isOrgOwner - try { - isOrgOwner = await affiliationCursor.next() - } catch (err) { - console.error( - `Cursor error when checking to see if user: ${userKey} is the owner of: ${orgId}: ${err}`, - ) - throw new Error( - i18n._(t`Unable to load owner information. Please try again.`), - ) - } + } catch (err) { + console.error(`Database error when checking to see if user: ${userKey} is the owner of: ${orgId}: ${err}`) + throw new Error(i18n._(t`Unable to load owner information. Please try again.`)) + } - return isOrgOwner + let isOrgOwner + try { + isOrgOwner = await affiliationCursor.next() + } catch (err) { + console.error(`Cursor error when checking to see if user: ${userKey} is the owner of: ${orgId}: ${err}`) + throw new Error(i18n._(t`Unable to load owner information. Please try again.`)) } + + return isOrgOwner + } diff --git a/api/src/auth/check-permission.js b/api/src/auth/check-permission.js index 814b1735e0..33d68b47f9 100644 --- a/api/src/auth/check-permission.js +++ b/api/src/auth/check-permission.js @@ -1,63 +1,67 @@ -import {t} from '@lingui/macro' +import { t } from '@lingui/macro' export const checkPermission = - ({i18n, userKey, query}) => - async ({orgId}) => { - let cursor - const userKeyString = `users/${userKey}` - // Check for super admin - try { - cursor = await query` + ({ i18n, userKey, query }) => + async ({ orgId }) => { + let cursor + const userKeyString = `users/${userKey}` + // Check for super admin + try { + cursor = await query` WITH affiliations, organizations, users FOR v, e IN 1 INBOUND ${userKeyString} affiliations FILTER e.permission == "super_admin" RETURN e.permission ` - } catch (err) { - console.error( - `Database error when checking to see if user: ${userKeyString} has super admin permission: ${err}`, - ) - throw new Error(i18n._(t`Authentication error. Please sign in.`)) - } + } catch (err) { + console.error(`Database error when checking to see if user: ${userKeyString} has super admin permission: ${err}`) + throw new Error(i18n._(t`Authentication error. Please sign in.`)) + } - let permission - try { - permission = await cursor.next() - } catch (err) { - console.error( - `Cursor error when checking to see if user ${userKeyString} has super admin permission: ${err}`, - ) - throw new Error(i18n._(t`Unable to check permission. Please try again.`)) - } + let permission + try { + permission = await cursor.next() + } catch (err) { + console.error(`Cursor error when checking to see if user ${userKeyString} has super admin permission: ${err}`) + throw new Error(i18n._(t`Unable to check permission. Please try again.`)) + } - if (permission === 'super_admin') { - return permission - } else { - // Check for other permission level - try { - cursor = await query` + if (permission === 'super_admin') { + return permission + } + + // Check for other permission level + try { + cursor = await query` WITH affiliations, organizations, users - FOR v, e IN 1 INBOUND ${userKeyString} affiliations - FILTER e._from == ${orgId} - RETURN e.permission + LET userAffiliations = ( + FOR v, e IN 1..1 ANY ${userKeyString} affiliations + FILTER e.permission != "pending" + RETURN v + ) + LET hasVerifiedOrgAffiliation = POSITION(userAffiliations[*].verified, true) + LET org = DOCUMENT(${orgId}) + LET orgIsVerified = org.verified + LET userOrgAffiliation = FIRST( + FOR v, e IN 1..1 ANY ${userKeyString} affiliations + FILTER e._from == ${orgId} + RETURN e + ) + RETURN userOrgAffiliation.permission IN ["user", "admin", "owner", "super_admin"] ? userOrgAffiliation.permission + : (orgIsVerified && hasVerifiedOrgAffiliation) ? "user" + : userOrgAffiliation.permission == "pending" ? userOrgAffiliation + : null ` - } catch (err) { - console.error( - `Database error occurred when checking ${userKeyString}'s permission: ${err}`, - ) - throw new Error(i18n._(t`Authentication error. Please sign in.`)) - } + } catch (err) { + console.error(`Database error occurred when checking ${userKeyString}'s permission: ${err}`) + throw new Error(i18n._(t`Authentication error. Please sign in.`)) + } - try { - permission = await cursor.next() - } catch (err) { - console.error( - `Cursor error when checking ${userKeyString}'s permission: ${err}`, - ) - throw new Error( - i18n._(t`Unable to check permission. Please try again.`), - ) - } - return permission - } + try { + permission = await cursor.next() + } catch (err) { + console.error(`Cursor error when checking ${userKeyString}'s permission: ${err}`) + throw new Error(i18n._(t`Unable to check permission. Please try again.`)) } + return permission + } diff --git a/api/src/auth/check-user-belongs-to-org.js b/api/src/auth/check-user-belongs-to-org.js index 9c58928099..44912ed378 100644 --- a/api/src/auth/check-user-belongs-to-org.js +++ b/api/src/auth/check-user-belongs-to-org.js @@ -1,27 +1,24 @@ -import {t} from '@lingui/macro' +import { t } from '@lingui/macro' export const checkUserBelongsToOrg = - ({i18n, query, userKey}) => - async ({orgId}) => { - const userIdString = `users/${userKey}` + ({ i18n, query, userKey }) => + async ({ orgId }) => { + const userIdString = `users/${userKey}` - // find affiliation - let affiliationCursor - try { - affiliationCursor = await query` + // find affiliation + let affiliationCursor + try { + affiliationCursor = await query` WITH affiliations, organizations, users FOR v, e IN 1..1 OUTBOUND ${orgId} affiliations FILTER e._to == ${userIdString} + FILTER e.permission != "pending" RETURN e ` - } catch (err) { - console.error( - `Database error when checking to see if user: ${userKey} belongs to org: ${orgId}: ${err}`, - ) - throw new Error( - i18n._(t`Unable to load affiliation information. Please try again.`), - ) - } - - return affiliationCursor.count > 0 + } catch (err) { + console.error(`Database error when checking to see if user: ${userKey} belongs to org: ${orgId}: ${err}`) + throw new Error(i18n._(t`Unable to load affiliation information. Please try again.`)) } + + return affiliationCursor.count > 0 + } diff --git a/api/src/auth/check-user-is-admin-for-user.js b/api/src/auth/check-user-is-admin-for-user.js index 85cedf3ac4..84a6d3bbc3 100644 --- a/api/src/auth/check-user-is-admin-for-user.js +++ b/api/src/auth/check-user-is-admin-for-user.js @@ -1,40 +1,40 @@ -import {t} from '@lingui/macro' +import { t } from '@lingui/macro' export const checkUserIsAdminForUser = - ({i18n, userKey, query}) => - async ({userName}) => { - const requestingUserId = `users/${userKey}` - let cursor + ({ i18n, userKey, query }) => + async ({ userName }) => { + const requestingUserId = `users/${userKey}` + let cursor - try { - cursor = await query` + try { + cursor = await query` WITH affiliations, organizations, users FOR v, e IN 1 INBOUND ${requestingUserId} affiliations FILTER e.permission == "super_admin" RETURN e.permission ` - } catch (err) { - console.error( - `Database error when checking to see if user: ${userKey} has super admin permission for user: ${userName}, error: ${err}`, - ) - throw new Error(i18n._(t`Permission error, not an admin for this user.`)) - } + } catch (err) { + console.error( + `Database error when checking to see if user: ${userKey} has super admin permission for user: ${userName}, error: ${err}`, + ) + throw new Error(i18n._(t`Permission error, not an admin for this user.`)) + } - let permission - try { - permission = await cursor.next() - } catch (err) { - console.error( - `Cursor error when checking to see if user: ${userKey} has super admin permission for user: ${userName}, error: ${err}`, - ) - throw new Error(i18n._(t`Permission error, not an admin for this user.`)) - } + let permission + try { + permission = await cursor.next() + } catch (err) { + console.error( + `Cursor error when checking to see if user: ${userKey} has super admin permission for user: ${userName}, error: ${err}`, + ) + throw new Error(i18n._(t`Permission error, not an admin for this user.`)) + } - if (permission === 'super_admin') { - return true - } else { - try { - cursor = await query` + if (permission === 'super_admin') { + return true + } else { + try { + cursor = await query` WITH affiliations, organizations, users LET requestingUserOrgKeys = ( FOR v, e IN 1 INBOUND ${requestingUserId} affiliations @@ -50,32 +50,29 @@ export const checkUserIsAdminForUser = LET requestedUserOrgKeys = ( FOR v, e IN 1 INBOUND requestedUser[0]._id affiliations + FILTER e.permission != "pending" RETURN v._key ) RETURN (LENGTH(INTERSECTION(requestingUserOrgKeys, requestedUserOrgKeys)) > 0 ? true : false) ` - } catch (err) { - console.error( - `Database error when checking to see if user: ${userKey} has admin permission for user: ${userName}, error: ${err}`, - ) - throw new Error( - i18n._(t`Permission error, not an admin for this user.`), - ) - } - - let isAdmin - try { - isAdmin = await cursor.next() - } catch (err) { - console.error( - `Cursor error when checking to see if user: ${userKey} has admin permission for user: ${userName}, error: ${err}`, - ) - throw new Error( - i18n._(t`Permission error, not an admin for this user.`), - ) - } + } catch (err) { + console.error( + `Database error when checking to see if user: ${userKey} has admin permission for user: ${userName}, error: ${err}`, + ) + throw new Error(i18n._(t`Permission error, not an admin for this user.`)) + } - return isAdmin + let isAdmin + try { + isAdmin = await cursor.next() + } catch (err) { + console.error( + `Cursor error when checking to see if user: ${userKey} has admin permission for user: ${userName}, error: ${err}`, + ) + throw new Error(i18n._(t`Permission error, not an admin for this user.`)) } + + return isAdmin } + } diff --git a/api/src/create-context.js b/api/src/create-context.js index e2a0b88e95..12fb14ae37 100644 --- a/api/src/create-context.js +++ b/api/src/create-context.js @@ -27,6 +27,7 @@ import { notifyClient, sendAuthEmail, sendAuthTextMsg, + sendInviteRequestEmail, sendOrgInviteCreateAccount, sendOrgInviteEmail, sendPasswordResetEmail, @@ -117,6 +118,7 @@ export async function createContext({ notify: { sendAuthEmail: sendAuthEmail({ notifyClient, i18n }), sendAuthTextMsg: sendAuthTextMsg({ notifyClient, i18n }), + sendInviteRequestEmail: sendInviteRequestEmail({ notifyClient, i18n }), sendOrgInviteCreateAccount: sendOrgInviteCreateAccount({ notifyClient, i18n, diff --git a/api/src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js b/api/src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js index 146b46ae5e..cab6a06a7b 100644 --- a/api/src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js +++ b/api/src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js @@ -2,360 +2,325 @@ import { aql } from 'arangojs' import { fromGlobalId, toGlobalId } from 'graphql-relay' import { t } from '@lingui/macro' -export const loadDmarcSummaryConnectionsByUserId = ({ - query, - userKey, - cleanseInput, - i18n, - loadStartDateFromPeriod, - auth: { loginRequiredBool }, -}) => async ({ - after, - before, - first, - last, - period, - year, - orderBy, - isSuperAdmin, - search, -}) => { - const userDBId = `users/${userKey}` - - if (typeof period === 'undefined') { - console.warn( - `User: ${userKey} did not have \`period\` argument set for: loadDmarcSummaryConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`You must provide a \`period\` value to access the \`DmarcSummaries\` connection.`, - ), - ) - } - const cleansedPeriod = cleanseInput(period) +export const loadDmarcSummaryConnectionsByUserId = + ({ query, userKey, cleanseInput, i18n, loadStartDateFromPeriod }) => + async ({ after, before, first, last, period, year, orderBy, isSuperAdmin, search }) => { + const userDBId = `users/${userKey}` + + if (typeof period === 'undefined') { + console.warn(`User: ${userKey} did not have \`period\` argument set for: loadDmarcSummaryConnectionsByUserId.`) + throw new Error(i18n._(t`You must provide a \`period\` value to access the \`DmarcSummaries\` connection.`)) + } + const cleansedPeriod = cleanseInput(period) - if (typeof year === 'undefined') { - console.warn( - `User: ${userKey} did not have \`year\` argument set for: loadDmarcSummaryConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`You must provide a \`year\` value to access the \`DmarcSummaries\` connection.`, - ), - ) - } - const cleansedYear = cleanseInput(year) - - const startDate = loadStartDateFromPeriod({ - period: cleansedPeriod, - year: cleansedYear, - }) - - let afterTemplate = aql`` - let afterVar = aql`` - if (typeof after !== 'undefined') { - const { id: afterId } = fromGlobalId(cleanseInput(after)) - if (typeof orderBy === 'undefined') { - afterTemplate = aql`FILTER TO_NUMBER(summary._key) > TO_NUMBER(${afterId})` - } else { - let afterTemplateDirection = aql`` - if (orderBy.direction === 'ASC') { - afterTemplateDirection = aql`>` + if (typeof year === 'undefined') { + console.warn(`User: ${userKey} did not have \`year\` argument set for: loadDmarcSummaryConnectionsByUserId.`) + throw new Error(i18n._(t`You must provide a \`year\` value to access the \`DmarcSummaries\` connection.`)) + } + const cleansedYear = cleanseInput(year) + + const startDate = loadStartDateFromPeriod({ + period: cleansedPeriod, + year: cleansedYear, + }) + + let afterTemplate = aql`` + let afterVar = aql`` + if (typeof after !== 'undefined') { + const { id: afterId } = fromGlobalId(cleanseInput(after)) + if (typeof orderBy === 'undefined') { + afterTemplate = aql`FILTER TO_NUMBER(summary._key) > TO_NUMBER(${afterId})` } else { - afterTemplateDirection = aql`<` - } - - afterVar = aql`LET afterVar = DOCUMENT(dmarcSummaries, ${afterId})` + let afterTemplateDirection = aql`` + if (orderBy.direction === 'ASC') { + afterTemplateDirection = aql`>` + } else { + afterTemplateDirection = aql`<` + } - let documentField = aql`` - let summaryField = aql`` - /* istanbul ignore else */ - if (orderBy.field === 'fail-count') { - documentField = aql`afterVar.categoryTotals.fail` - summaryField = aql`summary.categoryTotals.fail` - } else if (orderBy.field === 'pass-count') { - documentField = aql`afterVar.categoryTotals.pass` - summaryField = aql`summary.categoryTotals.pass` - } else if (orderBy.field === 'pass-dkim-count') { - documentField = aql`afterVar.categoryTotals.passDkimOnly` - summaryField = aql`summary.categoryTotals.passDkimOnly` - } else if (orderBy.field === 'pass-spf-count') { - documentField = aql`afterVar.categoryTotals.passSpfOnly` - summaryField = aql`summary.categoryTotals.passSpfOnly` - } else if (orderBy.field === 'fail-percentage') { - documentField = aql`afterVar.categoryPercentages.fail` - summaryField = aql`summary.categoryPercentages.fail` - } else if (orderBy.field === 'pass-percentage') { - documentField = aql`afterVar.categoryPercentages.pass` - summaryField = aql`summary.categoryPercentages.pass` - } else if (orderBy.field === 'pass-dkim-percentage') { - documentField = aql`afterVar.categoryPercentages.passDkimOnly` - summaryField = aql`summary.categoryPercentages.passDkimOnly` - } else if (orderBy.field === 'pass-spf-percentage') { - documentField = aql`afterVar.categoryPercentages.passSpfOnly` - summaryField = aql`summary.categoryPercentages.passSpfOnly` - } else if (orderBy.field === 'total-messages') { - documentField = aql`afterVar.totalMessages` - summaryField = aql`summary.totalMessages` - } else if (orderBy.field === 'domain') { - documentField = aql` + afterVar = aql`LET afterVar = DOCUMENT(dmarcSummaries, ${afterId})` + + let documentField = aql`` + let summaryField = aql`` + /* istanbul ignore else */ + if (orderBy.field === 'fail-count') { + documentField = aql`afterVar.categoryTotals.fail` + summaryField = aql`summary.categoryTotals.fail` + } else if (orderBy.field === 'pass-count') { + documentField = aql`afterVar.categoryTotals.pass` + summaryField = aql`summary.categoryTotals.pass` + } else if (orderBy.field === 'pass-dkim-count') { + documentField = aql`afterVar.categoryTotals.passDkimOnly` + summaryField = aql`summary.categoryTotals.passDkimOnly` + } else if (orderBy.field === 'pass-spf-count') { + documentField = aql`afterVar.categoryTotals.passSpfOnly` + summaryField = aql`summary.categoryTotals.passSpfOnly` + } else if (orderBy.field === 'fail-percentage') { + documentField = aql`afterVar.categoryPercentages.fail` + summaryField = aql`summary.categoryPercentages.fail` + } else if (orderBy.field === 'pass-percentage') { + documentField = aql`afterVar.categoryPercentages.pass` + summaryField = aql`summary.categoryPercentages.pass` + } else if (orderBy.field === 'pass-dkim-percentage') { + documentField = aql`afterVar.categoryPercentages.passDkimOnly` + summaryField = aql`summary.categoryPercentages.passDkimOnly` + } else if (orderBy.field === 'pass-spf-percentage') { + documentField = aql`afterVar.categoryPercentages.passSpfOnly` + summaryField = aql`summary.categoryPercentages.passSpfOnly` + } else if (orderBy.field === 'total-messages') { + documentField = aql`afterVar.totalMessages` + summaryField = aql`summary.totalMessages` + } else if (orderBy.field === 'domain') { + documentField = aql` FIRST( FOR v, e IN 1..1 ANY afterVar._id domainsToDmarcSummaries RETURN v.domain ) ` - summaryField = aql`domain.domain` - } + summaryField = aql`domain.domain` + } - afterTemplate = aql` + afterTemplate = aql` FILTER ${summaryField} ${afterTemplateDirection} ${documentField} OR (${summaryField} == ${documentField} AND TO_NUMBER(summary._key) > TO_NUMBER(${afterId})) ` + } } - } - let beforeTemplate = aql`` - let beforeVar = aql`` - if (typeof before !== 'undefined') { - const { id: beforeId } = fromGlobalId(cleanseInput(before)) - if (typeof orderBy === 'undefined') { - beforeTemplate = aql`FILTER TO_NUMBER(summary._key) < TO_NUMBER(${beforeId})` - } else { - let beforeTemplateDirection = aql`` - if (orderBy.direction === 'ASC') { - beforeTemplateDirection = aql`<` + let beforeTemplate = aql`` + let beforeVar = aql`` + if (typeof before !== 'undefined') { + const { id: beforeId } = fromGlobalId(cleanseInput(before)) + if (typeof orderBy === 'undefined') { + beforeTemplate = aql`FILTER TO_NUMBER(summary._key) < TO_NUMBER(${beforeId})` } else { - beforeTemplateDirection = aql`>` - } - - beforeVar = aql`LET beforeVar = DOCUMENT(dmarcSummaries, ${beforeId})` + let beforeTemplateDirection = aql`` + if (orderBy.direction === 'ASC') { + beforeTemplateDirection = aql`<` + } else { + beforeTemplateDirection = aql`>` + } - let documentField = aql`` - let summaryField = aql`` - /* istanbul ignore else */ - if (orderBy.field === 'fail-count') { - documentField = aql`beforeVar.categoryTotals.fail` - summaryField = aql`summary.categoryTotals.fail` - } else if (orderBy.field === 'pass-count') { - documentField = aql`beforeVar.categoryTotals.pass` - summaryField = aql`summary.categoryTotals.pass` - } else if (orderBy.field === 'pass-dkim-count') { - documentField = aql`beforeVar.categoryTotals.passDkimOnly` - summaryField = aql`summary.categoryTotals.passDkimOnly` - } else if (orderBy.field === 'pass-spf-count') { - documentField = aql`beforeVar.categoryTotals.passSpfOnly` - summaryField = aql`summary.categoryTotals.passSpfOnly` - } else if (orderBy.field === 'fail-percentage') { - documentField = aql`beforeVar.categoryPercentages.fail` - summaryField = aql`summary.categoryPercentages.fail` - } else if (orderBy.field === 'pass-percentage') { - documentField = aql`beforeVar.categoryPercentages.pass` - summaryField = aql`summary.categoryPercentages.pass` - } else if (orderBy.field === 'pass-dkim-percentage') { - documentField = aql`beforeVar.categoryPercentages.passDkimOnly` - summaryField = aql`summary.categoryPercentages.passDkimOnly` - } else if (orderBy.field === 'pass-spf-percentage') { - documentField = aql`beforeVar.categoryPercentages.passSpfOnly` - summaryField = aql`summary.categoryPercentages.passSpfOnly` - } else if (orderBy.field === 'total-messages') { - documentField = aql`beforeVar.totalMessages` - summaryField = aql`summary.totalMessages` - } else if (orderBy.field === 'domain') { - documentField = aql` + beforeVar = aql`LET beforeVar = DOCUMENT(dmarcSummaries, ${beforeId})` + + let documentField = aql`` + let summaryField = aql`` + /* istanbul ignore else */ + if (orderBy.field === 'fail-count') { + documentField = aql`beforeVar.categoryTotals.fail` + summaryField = aql`summary.categoryTotals.fail` + } else if (orderBy.field === 'pass-count') { + documentField = aql`beforeVar.categoryTotals.pass` + summaryField = aql`summary.categoryTotals.pass` + } else if (orderBy.field === 'pass-dkim-count') { + documentField = aql`beforeVar.categoryTotals.passDkimOnly` + summaryField = aql`summary.categoryTotals.passDkimOnly` + } else if (orderBy.field === 'pass-spf-count') { + documentField = aql`beforeVar.categoryTotals.passSpfOnly` + summaryField = aql`summary.categoryTotals.passSpfOnly` + } else if (orderBy.field === 'fail-percentage') { + documentField = aql`beforeVar.categoryPercentages.fail` + summaryField = aql`summary.categoryPercentages.fail` + } else if (orderBy.field === 'pass-percentage') { + documentField = aql`beforeVar.categoryPercentages.pass` + summaryField = aql`summary.categoryPercentages.pass` + } else if (orderBy.field === 'pass-dkim-percentage') { + documentField = aql`beforeVar.categoryPercentages.passDkimOnly` + summaryField = aql`summary.categoryPercentages.passDkimOnly` + } else if (orderBy.field === 'pass-spf-percentage') { + documentField = aql`beforeVar.categoryPercentages.passSpfOnly` + summaryField = aql`summary.categoryPercentages.passSpfOnly` + } else if (orderBy.field === 'total-messages') { + documentField = aql`beforeVar.totalMessages` + summaryField = aql`summary.totalMessages` + } else if (orderBy.field === 'domain') { + documentField = aql` FIRST( FOR v, e IN 1..1 ANY beforeVar._id domainsToDmarcSummaries RETURN v.domain ) ` - summaryField = aql`domain.domain` - } + summaryField = aql`domain.domain` + } - beforeTemplate = aql` + beforeTemplate = aql` FILTER ${summaryField} ${beforeTemplateDirection} ${documentField} OR (${summaryField} == ${documentField} AND TO_NUMBER(summary._key) < TO_NUMBER(${beforeId})) ` + } } - } - let limitTemplate = aql`` - if (typeof first === 'undefined' && typeof last === 'undefined') { - console.warn( - `User: ${userKey} did not have either \`first\` or \`last\` arguments set for: loadDmarcSummaryConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`You must provide a \`first\` or \`last\` value to properly paginate the \`DmarcSummaries\` connection.`, - ), - ) - } else if (typeof first !== 'undefined' && typeof last !== 'undefined') { - console.warn( - `User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadDmarcSummaryConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`Passing both \`first\` and \`last\` to paginate the \`DmarcSummaries\` connection is not supported.`, - ), - ) - } else if (typeof first === 'number' || typeof last === 'number') { - /* istanbul ignore else */ - if (first < 0 || last < 0) { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' + let limitTemplate = aql`` + if (typeof first === 'undefined' && typeof last === 'undefined') { console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadDmarcSummaryConnectionsByUserId.`, + `User: ${userKey} did not have either \`first\` or \`last\` arguments set for: loadDmarcSummaryConnectionsByUserId.`, ) throw new Error( i18n._( - t`\`${argSet}\` on the \`DmarcSummaries\` connection cannot be less than zero.`, + t`You must provide a \`first\` or \`last\` value to properly paginate the \`DmarcSummaries\` connection.`, ), ) - } else if (first > 100 || last > 100) { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - const amount = typeof first !== 'undefined' ? first : last + } else if (typeof first !== 'undefined' && typeof last !== 'undefined') { console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set to ${amount} for: loadDmarcSummaryConnectionsByUserId.`, + `User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadDmarcSummaryConnectionsByUserId.`, ) throw new Error( - i18n._( - t`Requesting \`${amount}\` records on the \`DmarcSummaries\` connection exceeds the \`${argSet}\` limit of 100 records.`, - ), + i18n._(t`Passing both \`first\` and \`last\` to paginate the \`DmarcSummaries\` connection is not supported.`), ) - } else if (typeof first !== 'undefined' && typeof last === 'undefined') { - limitTemplate = aql`TO_NUMBER(summary._key) ASC LIMIT TO_NUMBER(${first})` - } else if (typeof first === 'undefined' && typeof last !== 'undefined') { - limitTemplate = aql`TO_NUMBER(summary._key) DESC LIMIT TO_NUMBER(${last})` - } - } else { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - const typeSet = typeof first !== 'undefined' ? typeof first : typeof last - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadDmarcSummaryConnectionsByUserId.`, - ) - throw new Error( - i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`), - ) - } - - let hasNextPageFilter = aql`FILTER TO_NUMBER(summary._key) > TO_NUMBER(LAST(retrievedSummaries)._key)` - let hasPreviousPageFilter = aql`FILTER TO_NUMBER(summary._key) < TO_NUMBER(FIRST(retrievedSummaries)._key)` - if (typeof orderBy !== 'undefined') { - let hasNextPageDirection = aql`` - let hasPreviousPageDirection = aql`` - if (orderBy.direction === 'ASC') { - hasNextPageDirection = aql`>` - hasPreviousPageDirection = aql`<` + } else if (typeof first === 'number' || typeof last === 'number') { + /* istanbul ignore else */ + if (first < 0 || last < 0) { + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + console.warn( + `User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadDmarcSummaryConnectionsByUserId.`, + ) + throw new Error(i18n._(t`\`${argSet}\` on the \`DmarcSummaries\` connection cannot be less than zero.`)) + } else if (first > 100 || last > 100) { + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + const amount = typeof first !== 'undefined' ? first : last + console.warn( + `User: ${userKey} attempted to have \`${argSet}\` set to ${amount} for: loadDmarcSummaryConnectionsByUserId.`, + ) + throw new Error( + i18n._( + t`Requesting \`${amount}\` records on the \`DmarcSummaries\` connection exceeds the \`${argSet}\` limit of 100 records.`, + ), + ) + } else if (typeof first !== 'undefined' && typeof last === 'undefined') { + limitTemplate = aql`TO_NUMBER(summary._key) ASC LIMIT TO_NUMBER(${first})` + } else if (typeof first === 'undefined' && typeof last !== 'undefined') { + limitTemplate = aql`TO_NUMBER(summary._key) DESC LIMIT TO_NUMBER(${last})` + } } else { - hasNextPageDirection = aql`<` - hasPreviousPageDirection = aql`>` + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + const typeSet = typeof first !== 'undefined' ? typeof first : typeof last + console.warn( + `User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadDmarcSummaryConnectionsByUserId.`, + ) + throw new Error(i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`)) } - let hasNextPageDocumentField = aql`` - let summaryField = aql`` - let hasPreviousPageDocumentField = aql`` - /* istanbul ignore else */ - if (orderBy.field === 'fail-count') { - summaryField = aql`summary.categoryTotals.fail` - hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryTotals.fail` - hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryTotals.fail` - } else if (orderBy.field === 'pass-count') { - summaryField = aql`summary.categoryTotals.pass` - hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryTotals.pass` - hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryTotals.pass` - } else if (orderBy.field === 'pass-dkim-count') { - summaryField = aql`summary.categoryTotals.passDkimOnly` - hasNextPageDocumentField = aql` LAST(retrievedSummaries).categoryTotals.passDkimOnly` - hasPreviousPageDocumentField = aql` FIRST(retrievedSummaries).categoryTotals.passDkimOnly` - } else if (orderBy.field === 'pass-spf-count') { - summaryField = aql`summary.categoryTotals.passSpfOnly` - hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryTotals.passSpfOnly` - hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryTotals.passSpfOnly` - } else if (orderBy.field === 'fail-percentage') { - summaryField = aql`summary.categoryPercentages.fail` - hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryPercentages.fail` - hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryPercentages.fail` - } else if (orderBy.field === 'pass-percentage') { - summaryField = aql`summary.categoryPercentages.pass` - hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryPercentages.pass` - hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryPercentages.pass` - } else if (orderBy.field === 'pass-dkim-percentage') { - summaryField = aql`summary.categoryPercentages.passDkimOnly` - hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryPercentages.passDkimOnly` - hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryPercentages.passDkimOnly` - } else if (orderBy.field === 'pass-spf-percentage') { - summaryField = aql`summary.categoryPercentages.passSpfOnly` - hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryPercentages.passSpfOnly` - hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryPercentages.passSpfOnly` - } else if (orderBy.field === 'total-messages') { - summaryField = aql`summary.totalMessages` - hasNextPageDocumentField = aql`LAST(retrievedSummaries).totalMessages` - hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).totalMessages` - } else if (orderBy.field === 'domain') { - summaryField = aql`domain.domain` - hasNextPageDocumentField = aql` + let hasNextPageFilter = aql`FILTER TO_NUMBER(summary._key) > TO_NUMBER(LAST(retrievedSummaries)._key)` + let hasPreviousPageFilter = aql`FILTER TO_NUMBER(summary._key) < TO_NUMBER(FIRST(retrievedSummaries)._key)` + if (typeof orderBy !== 'undefined') { + let hasNextPageDirection = aql`` + let hasPreviousPageDirection = aql`` + if (orderBy.direction === 'ASC') { + hasNextPageDirection = aql`>` + hasPreviousPageDirection = aql`<` + } else { + hasNextPageDirection = aql`<` + hasPreviousPageDirection = aql`>` + } + + let hasNextPageDocumentField = aql`` + let summaryField = aql`` + let hasPreviousPageDocumentField = aql`` + /* istanbul ignore else */ + if (orderBy.field === 'fail-count') { + summaryField = aql`summary.categoryTotals.fail` + hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryTotals.fail` + hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryTotals.fail` + } else if (orderBy.field === 'pass-count') { + summaryField = aql`summary.categoryTotals.pass` + hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryTotals.pass` + hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryTotals.pass` + } else if (orderBy.field === 'pass-dkim-count') { + summaryField = aql`summary.categoryTotals.passDkimOnly` + hasNextPageDocumentField = aql` LAST(retrievedSummaries).categoryTotals.passDkimOnly` + hasPreviousPageDocumentField = aql` FIRST(retrievedSummaries).categoryTotals.passDkimOnly` + } else if (orderBy.field === 'pass-spf-count') { + summaryField = aql`summary.categoryTotals.passSpfOnly` + hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryTotals.passSpfOnly` + hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryTotals.passSpfOnly` + } else if (orderBy.field === 'fail-percentage') { + summaryField = aql`summary.categoryPercentages.fail` + hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryPercentages.fail` + hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryPercentages.fail` + } else if (orderBy.field === 'pass-percentage') { + summaryField = aql`summary.categoryPercentages.pass` + hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryPercentages.pass` + hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryPercentages.pass` + } else if (orderBy.field === 'pass-dkim-percentage') { + summaryField = aql`summary.categoryPercentages.passDkimOnly` + hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryPercentages.passDkimOnly` + hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryPercentages.passDkimOnly` + } else if (orderBy.field === 'pass-spf-percentage') { + summaryField = aql`summary.categoryPercentages.passSpfOnly` + hasNextPageDocumentField = aql`LAST(retrievedSummaries).categoryPercentages.passSpfOnly` + hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).categoryPercentages.passSpfOnly` + } else if (orderBy.field === 'total-messages') { + summaryField = aql`summary.totalMessages` + hasNextPageDocumentField = aql`LAST(retrievedSummaries).totalMessages` + hasPreviousPageDocumentField = aql`FIRST(retrievedSummaries).totalMessages` + } else if (orderBy.field === 'domain') { + summaryField = aql`domain.domain` + hasNextPageDocumentField = aql` FIRST( FOR v, e IN 1..1 ANY LAST(retrievedSummaries)._id domainsToDmarcSummaries RETURN v.domain ) ` - hasPreviousPageDocumentField = aql` + hasPreviousPageDocumentField = aql` FIRST( FOR v, e IN 1..1 ANY FIRST(retrievedSummaries)._id domainsToDmarcSummaries RETURN v.domain ) ` - } + } - hasNextPageFilter = aql` + hasNextPageFilter = aql` FILTER ${summaryField} ${hasNextPageDirection} ${hasNextPageDocumentField} OR (${summaryField} == ${hasNextPageDocumentField} AND TO_NUMBER(summary._key) > TO_NUMBER(LAST(retrievedSummaries)._key)) ` - hasPreviousPageFilter = aql` + hasPreviousPageFilter = aql` FILTER ${summaryField} ${hasPreviousPageDirection} ${hasPreviousPageDocumentField} OR (${summaryField} == ${hasPreviousPageDocumentField} AND TO_NUMBER(summary._key) < TO_NUMBER(FIRST(retrievedSummaries)._key)) ` - } + } - let sortByField = aql`` - if (typeof orderBy !== 'undefined') { - /* istanbul ignore else */ - if (orderBy.field === 'fail-count') { - sortByField = aql`summary.categoryTotals.fail ${orderBy.direction},` - } else if (orderBy.field === 'pass-count') { - sortByField = aql`summary.categoryTotals.pass ${orderBy.direction},` - } else if (orderBy.field === 'pass-dkim-count') { - sortByField = aql`summary.categoryTotals.passDkimOnly ${orderBy.direction},` - } else if (orderBy.field === 'pass-spf-count') { - sortByField = aql`summary.categoryTotals.passSpfOnly ${orderBy.direction},` - } else if (orderBy.field === 'fail-percentage') { - sortByField = aql`summary.categoryPercentages.fail ${orderBy.direction},` - } else if (orderBy.field === 'pass-percentage') { - sortByField = aql`summary.categoryPercentages.pass ${orderBy.direction},` - } else if (orderBy.field === 'pass-dkim-percentage') { - sortByField = aql`summary.categoryPercentages.passDkimOnly ${orderBy.direction},` - } else if (orderBy.field === 'pass-spf-percentage') { - sortByField = aql`summary.categoryPercentages.passSpfOnly ${orderBy.direction},` - } else if (orderBy.field === 'total-messages') { - sortByField = aql`summary.totalMessages ${orderBy.direction},` - } else if (orderBy.field === 'domain') { - sortByField = aql`domain.domain ${orderBy.direction},` + let sortByField = aql`` + if (typeof orderBy !== 'undefined') { + /* istanbul ignore else */ + if (orderBy.field === 'fail-count') { + sortByField = aql`summary.categoryTotals.fail ${orderBy.direction},` + } else if (orderBy.field === 'pass-count') { + sortByField = aql`summary.categoryTotals.pass ${orderBy.direction},` + } else if (orderBy.field === 'pass-dkim-count') { + sortByField = aql`summary.categoryTotals.passDkimOnly ${orderBy.direction},` + } else if (orderBy.field === 'pass-spf-count') { + sortByField = aql`summary.categoryTotals.passSpfOnly ${orderBy.direction},` + } else if (orderBy.field === 'fail-percentage') { + sortByField = aql`summary.categoryPercentages.fail ${orderBy.direction},` + } else if (orderBy.field === 'pass-percentage') { + sortByField = aql`summary.categoryPercentages.pass ${orderBy.direction},` + } else if (orderBy.field === 'pass-dkim-percentage') { + sortByField = aql`summary.categoryPercentages.passDkimOnly ${orderBy.direction},` + } else if (orderBy.field === 'pass-spf-percentage') { + sortByField = aql`summary.categoryPercentages.passSpfOnly ${orderBy.direction},` + } else if (orderBy.field === 'total-messages') { + sortByField = aql`summary.totalMessages ${orderBy.direction},` + } else if (orderBy.field === 'domain') { + sortByField = aql`domain.domain ${orderBy.direction},` + } } - } - let sortString - if (typeof last !== 'undefined') { - sortString = aql`DESC` - } else { - sortString = aql`ASC` - } + let sortString + if (typeof last !== 'undefined') { + sortString = aql`DESC` + } else { + sortString = aql`ASC` + } - let domainQuery = aql`` - let searchDomainFilter = aql`` - if (typeof search !== 'undefined') { - search = cleanseInput(search) - domainQuery = aql` + let domainQuery = aql`` + let searchDomainFilter = aql`` + if (typeof search !== 'undefined') { + search = cleanseInput(search) + domainQuery = aql` LET tokenArr = TOKENS(${search}, "space-delimiter-analyzer") LET searchedDomains = ( FOR token IN tokenArr @@ -364,12 +329,12 @@ export const loadDmarcSummaryConnectionsByUserId = ({ RETURN domain._id ) ` - searchDomainFilter = aql`FILTER domainId IN searchedDomains` - } + searchDomainFilter = aql`FILTER domainId IN searchedDomains` + } - let domainIdQueries - if (isSuperAdmin || !loginRequiredBool) { - domainIdQueries = aql` + let domainIdQueries + if (isSuperAdmin) { + domainIdQueries = aql` WITH affiliations, dmarcSummaries, domains, domainsToDmarcSummaries, organizations, ownership, users, domainSearch LET domainIds = UNIQUE(FLATTEN( LET ids = [] @@ -379,22 +344,27 @@ export const loadDmarcSummaryConnectionsByUserId = ({ RETURN APPEND(ids, claimDomainIds) )) ` - } else { - domainIdQueries = aql` + } else { + domainIdQueries = aql` WITH affiliations, dmarcSummaries, domains, domainsToDmarcSummaries, organizations, ownership, users, domainSearch - LET domainIds = UNIQUE(FLATTEN( - LET ids = [] - LET orgIds = (FOR v, e IN 1..1 ANY ${userDBId} affiliations RETURN e._from) - FOR orgId IN orgIds - LET claimDomainIds = (FOR v, e IN 1..1 OUTBOUND orgId ownership RETURN v._id) - RETURN APPEND(ids, claimDomainIds) - )) + LET userAffiliations = ( + FOR v, e IN 1..1 ANY ${userDBId} affiliations + FILTER e.permission != "pending" + RETURN v + ) + LET hasVerifiedOrgAffiliation = POSITION(userAffiliations[*].verified, true) + LET domainIds = UNIQUE( + FOR org IN organizations + FILTER org._key IN userAffiliations[*]._key || (hasVerifiedOrgAffiliation == true && org.verified == true) + FOR v, e IN 1..1 OUTBOUND org._id ownership + RETURN v._id + ) ` - } + } - let requestedSummaryInfo - try { - requestedSummaryInfo = await query` + let requestedSummaryInfo + try { + requestedSummaryInfo = await query` ${domainIdQueries} ${domainQuery} @@ -469,56 +439,52 @@ export const loadDmarcSummaryConnectionsByUserId = ({ "endKey": LAST(retrievedSummaries)._key } ` - } catch (err) { - console.error( - `Database error occurred while user: ${userKey} was trying to gather dmarc summaries in loadDmarcSummaryConnectionsByUserId, error: ${err}`, - ) - throw new Error( - i18n._(t`Unable to load DMARC summary data. Please try again.`), - ) - } + } catch (err) { + console.error( + `Database error occurred while user: ${userKey} was trying to gather dmarc summaries in loadDmarcSummaryConnectionsByUserId, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to load DMARC summary data. Please try again.`)) + } - let summariesInfo - try { - summariesInfo = await requestedSummaryInfo.next() - } catch (err) { - console.error( - `Cursor error occurred while user: ${userKey} was trying to gather dmarc summaries in loadDmarcSummaryConnectionsByUserId, error: ${err}`, - ) - throw new Error( - i18n._(t`Unable to load DMARC summary data. Please try again.`), - ) - } + let summariesInfo + try { + summariesInfo = await requestedSummaryInfo.next() + } catch (err) { + console.error( + `Cursor error occurred while user: ${userKey} was trying to gather dmarc summaries in loadDmarcSummaryConnectionsByUserId, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to load DMARC summary data. Please try again.`)) + } - if (summariesInfo.summaries.length === 0) { - return { - edges: [], - totalCount: 0, - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, + if (summariesInfo.summaries.length === 0) { + return { + edges: [], + totalCount: 0, + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', + }, + } } - } - const edges = summariesInfo.summaries.map((summary) => { - summary.startDate = startDate + const edges = summariesInfo.summaries.map((summary) => { + summary.startDate = startDate + return { + cursor: toGlobalId('dmarcSummary', summary.id), + node: summary, + } + }) + return { - cursor: toGlobalId('dmarcSummary', summary.id), - node: summary, + edges, + totalCount: summariesInfo.totalCount, + pageInfo: { + hasNextPage: summariesInfo.hasNextPage, + hasPreviousPage: summariesInfo.hasPreviousPage, + startCursor: toGlobalId('dmarcSummary', summariesInfo.startKey), + endCursor: toGlobalId('dmarcSummary', summariesInfo.endKey), + }, } - }) - - return { - edges, - totalCount: summariesInfo.totalCount, - pageInfo: { - hasNextPage: summariesInfo.hasNextPage, - hasPreviousPage: summariesInfo.hasPreviousPage, - startCursor: toGlobalId('dmarcSummary', summariesInfo.startKey), - endCursor: toGlobalId('dmarcSummary', summariesInfo.endKey), - }, } -} diff --git a/api/src/dmarc-summaries/queries/find-my-dmarc-summaries.js b/api/src/dmarc-summaries/queries/find-my-dmarc-summaries.js index d1259ad88f..385222c1fd 100644 --- a/api/src/dmarc-summaries/queries/find-my-dmarc-summaries.js +++ b/api/src/dmarc-summaries/queries/find-my-dmarc-summaries.js @@ -24,8 +24,7 @@ export const findMyDmarcSummaries = { }, search: { type: GraphQLString, - description: - 'An optional string used to filter the results based on domains.', + description: 'An optional string used to filter the results based on domains.', }, ...connectionArgs, }, @@ -34,19 +33,12 @@ export const findMyDmarcSummaries = { args, { userKey, - auth: { - checkSuperAdmin, - userRequired, - verifiedRequired, - loginRequiredBool, - }, + auth: { checkSuperAdmin, userRequired, verifiedRequired }, loaders: { loadDmarcSummaryConnectionsByUserId }, }, ) => { - if (loginRequiredBool) { - const user = await userRequired() - verifiedRequired({ user }) - } + const user = await userRequired() + verifiedRequired({ user }) const isSuperAdmin = await checkSuperAdmin() diff --git a/api/src/domain/inputs/domain-filter.js b/api/src/domain/inputs/domain-filter.js index c1d8c64b02..2280a63b47 100644 --- a/api/src/domain/inputs/domain-filter.js +++ b/api/src/domain/inputs/domain-filter.js @@ -1,16 +1,8 @@ import { GraphQLInputObjectType, GraphQLEnumType } from 'graphql' -import { - ComparisonEnums, - DomainOrderField, - DomainTagLabel, - StatusEnum, -} from '../../enums' +import { ComparisonEnums, DomainOrderField, DomainTagLabel, StatusEnum } from '../../enums' const filterValueEnumsVals = {} -const filterValueEnums = [ - ...StatusEnum.getValues(), - ...DomainTagLabel.getValues(), -] +const filterValueEnums = [...StatusEnum.getValues(), ...DomainTagLabel.getValues()] filterValueEnums.forEach( ({ name, value, description }) => (filterValueEnumsVals[name] = { @@ -21,8 +13,7 @@ filterValueEnums.forEach( export const domainFilter = new GraphQLInputObjectType({ name: 'DomainFilter', - description: - 'This object is used to provide filtering options when querying org-claimed domains.', + description: 'This object is used to provide filtering options when querying org-claimed domains.', fields: () => ({ filterCategory: { type: DomainOrderField, diff --git a/api/src/domain/loaders/__tests__/load-domain-connections-by-user-id.test.js b/api/src/domain/loaders/__tests__/load-domain-connections-by-user-id.test.js index 6c27006d79..e0871384e3 100644 --- a/api/src/domain/loaders/__tests__/load-domain-connections-by-user-id.test.js +++ b/api/src/domain/loaders/__tests__/load-domain-connections-by-user-id.test.js @@ -1,15 +1,15 @@ -import {stringify} from 'jest-matcher-utils' -import {ensure, dbNameFromFile} from 'arango-tools' -import {toGlobalId} from 'graphql-relay' -import {setupI18n} from '@lingui/core' +import { stringify } from 'jest-matcher-utils' +import { ensure, dbNameFromFile } from 'arango-tools' +import { toGlobalId } from 'graphql-relay' +import { setupI18n } from '@lingui/core' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import {cleanseInput} from '../../../validators' -import {loadDomainConnectionsByUserId, loadDomainByKey} from '../index' +import { cleanseInput } from '../../../validators' +import { loadDomainConnectionsByUserId, loadDomainByKey } from '../index' import dbschema from '../../../../database.json' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env describe('given the load domain connections by user id function', () => { let query, drop, truncate, collections, org, i18n, user, domainOne, domainTwo @@ -28,7 +28,7 @@ describe('given the load domain connections by user id function', () => { describe('given a successful load', () => { beforeAll(async () => { - ;({query, drop, truncate, collections} = await ensure({ + ;({ query, drop, truncate, collections } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -123,14 +123,11 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -139,7 +136,7 @@ describe('given the load domain connections by user id function', () => { first: 10, after: toGlobalId('domain', expectedDomains[0].id), } - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -168,14 +165,11 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -184,7 +178,7 @@ describe('given the load domain connections by user id function', () => { first: 10, before: toGlobalId('domain', expectedDomains[1].id), } - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -213,14 +207,11 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -228,7 +219,7 @@ describe('given the load domain connections by user id function', () => { const connectionArgs = { first: 1, } - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -257,14 +248,11 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -272,7 +260,7 @@ describe('given the load domain connections by user id function', () => { const connectionArgs = { last: 1, } - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -310,10 +298,10 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domainLoader = loadDomainByKey({query}) + const domainLoader = loadDomainByKey({ query }) const expectedDomain = await domainLoader.load(domainOne._key) const connectionArgs = { @@ -321,7 +309,7 @@ describe('given the load domain connections by user id function', () => { search: 'test1.gc.ca', } - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -367,19 +355,17 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequiredBool: true}, + auth: { loginRequiredBool: true }, }) - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainThree._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainThree._key]) const connectionArgs = { first: 1, ownership: true, } - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -408,21 +394,17 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - domainThree._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key, domainThree._key]) const connectionArgs = { first: 3, ownership: false, } - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -463,11 +445,8 @@ describe('given the load domain connections by user id function', () => { describe('ordering on DOMAIN', () => { describe('order direction is ASC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -484,9 +463,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -511,11 +490,8 @@ describe('given the load domain connections by user id function', () => { }) describe('order direction is DESC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -532,9 +508,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -561,11 +537,8 @@ describe('given the load domain connections by user id function', () => { describe('ordering on DKIM_STATUS', () => { describe('order direction is ASC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -582,9 +555,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -609,11 +582,8 @@ describe('given the load domain connections by user id function', () => { }) describe('order direction is DESC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -630,9 +600,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -659,11 +629,8 @@ describe('given the load domain connections by user id function', () => { describe('ordering on DMARC_STATUS', () => { describe('order direction is ASC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -680,9 +647,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -707,11 +674,8 @@ describe('given the load domain connections by user id function', () => { }) describe('order direction is DESC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -728,9 +692,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -757,11 +721,8 @@ describe('given the load domain connections by user id function', () => { describe('ordering on HTTPS_STATUS', () => { describe('order direction is ASC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -778,9 +739,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -805,11 +766,8 @@ describe('given the load domain connections by user id function', () => { }) describe('order direction is DESC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -826,9 +784,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -855,11 +813,8 @@ describe('given the load domain connections by user id function', () => { describe('ordering on SPF_STATUS', () => { describe('order direction is ASC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -876,9 +831,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -903,11 +858,8 @@ describe('given the load domain connections by user id function', () => { }) describe('order direction is DESC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -924,9 +876,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -955,11 +907,8 @@ describe('given the load domain connections by user id function', () => { describe('ordering on DOMAIN', () => { describe('order direction is ASC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -976,9 +925,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -1003,11 +952,8 @@ describe('given the load domain connections by user id function', () => { }) describe('order direction is DESC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -1024,9 +970,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -1053,11 +999,8 @@ describe('given the load domain connections by user id function', () => { describe('ordering on DKIM_STATUS', () => { describe('order direction is ASC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -1074,9 +1017,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -1101,11 +1044,8 @@ describe('given the load domain connections by user id function', () => { }) describe('order direction is DESC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -1122,9 +1062,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -1151,11 +1091,8 @@ describe('given the load domain connections by user id function', () => { describe('ordering on DMARC_STATUS', () => { describe('order direction is ASC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -1172,9 +1109,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -1199,11 +1136,8 @@ describe('given the load domain connections by user id function', () => { }) describe('order direction is DESC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -1220,9 +1154,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -1249,11 +1183,8 @@ describe('given the load domain connections by user id function', () => { describe('ordering on HTTPS_STATUS', () => { describe('order direction is ASC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -1270,9 +1201,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -1297,11 +1228,8 @@ describe('given the load domain connections by user id function', () => { }) describe('order direction is DESC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -1318,9 +1246,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -1347,11 +1275,8 @@ describe('given the load domain connections by user id function', () => { describe('ordering on SPF_STATUS', () => { describe('order direction is ASC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -1368,9 +1293,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -1395,11 +1320,8 @@ describe('given the load domain connections by user id function', () => { }) describe('order direction is DESC', () => { it('returns domains in order', async () => { - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -1416,9 +1338,9 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [ @@ -1450,20 +1372,17 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) const connectionArgs = { first: 10, isSuperAdmin: true, } - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) - const domainLoader = loadDomainByKey({query}) - const expectedDomains = await domainLoader.loadMany([ - domainOne._key, - domainTwo._key, - ]) + const domainLoader = loadDomainByKey({ query }) + const expectedDomains = await domainLoader.loadMany([domainOne._key, domainTwo._key]) expectedDomains[0].id = expectedDomains[0]._key expectedDomains[1].id = expectedDomains[1]._key @@ -1504,13 +1423,13 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }) const connectionArgs = { first: 10, } - const domains = await connectionLoader({...connectionArgs}) + const domains = await connectionLoader({ ...connectionArgs }) const expectedStructure = { edges: [], @@ -1532,8 +1451,8 @@ describe('given the load domain connections by user id function', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -1549,7 +1468,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1577,7 +1496,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1591,9 +1510,7 @@ describe('given the load domain connections by user id function', () => { }) } catch (err) { expect(err).toEqual( - new Error( - `Passing both \`first\` and \`last\` to paginate the \`Domain\` connection is not supported.`, - ), + new Error(`Passing both \`first\` and \`last\` to paginate the \`Domain\` connection is not supported.`), ) } @@ -1609,7 +1526,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1639,7 +1556,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1671,7 +1588,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1683,11 +1600,7 @@ describe('given the load domain connections by user id function', () => { ...connectionArgs, }) } catch (err) { - expect(err).toEqual( - new Error( - `\`first\` on the \`Domain\` connection cannot be less than zero.`, - ), - ) + expect(err).toEqual(new Error(`\`first\` on the \`Domain\` connection cannot be less than zero.`)) } expect(consoleOutput).toEqual([ @@ -1701,7 +1614,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1713,11 +1626,7 @@ describe('given the load domain connections by user id function', () => { ...connectionArgs, }) } catch (err) { - expect(err).toEqual( - new Error( - `\`last\` on the \`Domain\` connection cannot be less than zero.`, - ), - ) + expect(err).toEqual(new Error(`\`last\` on the \`Domain\` connection cannot be less than zero.`)) } expect(consoleOutput).toEqual([ @@ -1729,14 +1638,12 @@ describe('given the load domain connections by user id function', () => { describe('limits are not set to numbers', () => { describe('first limit is set', () => { ;['123', {}, [], null, true].forEach((invalidInput) => { - it(`returns an error when first set to ${stringify( - invalidInput, - )}`, async () => { + it(`returns an error when first set to ${stringify(invalidInput)}`, async () => { const connectionLoader = loadDomainConnectionsByUserId({ query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1749,11 +1656,7 @@ describe('given the load domain connections by user id function', () => { ...connectionArgs, }) } catch (err) { - expect(err).toEqual( - new Error( - `\`first\` must be of type \`number\` not \`${typeof invalidInput}\`.`, - ), - ) + expect(err).toEqual(new Error(`\`first\` must be of type \`number\` not \`${typeof invalidInput}\`.`)) } expect(consoleOutput).toEqual([ `User: ${ @@ -1765,14 +1668,12 @@ describe('given the load domain connections by user id function', () => { }) describe('last limit is set', () => { ;['123', {}, [], null, true].forEach((invalidInput) => { - it(`returns an error when last set to ${stringify( - invalidInput, - )}`, async () => { + it(`returns an error when last set to ${stringify(invalidInput)}`, async () => { const connectionLoader = loadDomainConnectionsByUserId({ query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1785,11 +1686,7 @@ describe('given the load domain connections by user id function', () => { ...connectionArgs, }) } catch (err) { - expect(err).toEqual( - new Error( - `\`last\` must be of type \`number\` not \`${typeof invalidInput}\`.`, - ), - ) + expect(err).toEqual(new Error(`\`last\` must be of type \`number\` not \`${typeof invalidInput}\`.`)) } expect(consoleOutput).toEqual([ `User: ${ @@ -1804,17 +1701,13 @@ describe('given the load domain connections by user id function', () => { describe('given a database error', () => { describe('while querying for domain information', () => { it('returns an error message', async () => { - const query = jest - .fn() - .mockRejectedValue( - new Error('Unable to query domains. Please try again.'), - ) + const query = jest.fn().mockRejectedValue(new Error('Unable to query domains. Please try again.')) const connectionLoader = loadDomainConnectionsByUserId({ query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1826,9 +1719,7 @@ describe('given the load domain connections by user id function', () => { ...connectionArgs, }) } catch (err) { - expect(err).toEqual( - new Error('Unable to query domain(s). Please try again.'), - ) + expect(err).toEqual(new Error('Unable to query domain(s). Please try again.')) } expect(consoleOutput).toEqual([ @@ -1851,7 +1742,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1863,9 +1754,7 @@ describe('given the load domain connections by user id function', () => { ...connectionArgs, }) } catch (err) { - expect(err).toEqual( - new Error('Unable to load domain(s). Please try again.'), - ) + expect(err).toEqual(new Error('Unable to load domain(s). Please try again.')) } expect(consoleOutput).toEqual([ @@ -1880,8 +1769,8 @@ describe('given the load domain connections by user id function', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -1897,7 +1786,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1925,7 +1814,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1939,9 +1828,7 @@ describe('given the load domain connections by user id function', () => { }) } catch (err) { expect(err).toEqual( - new Error( - "Passer à la fois `first` et `last` pour paginer la connexion `Domain` n'est pas supporté.", - ), + new Error("Passer à la fois `first` et `last` pour paginer la connexion `Domain` n'est pas supporté."), ) } @@ -1957,7 +1844,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -1987,7 +1874,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -2019,7 +1906,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -2031,11 +1918,7 @@ describe('given the load domain connections by user id function', () => { ...connectionArgs, }) } catch (err) { - expect(err).toEqual( - new Error( - '`first` sur la connexion `Domain` ne peut être inférieur à zéro.', - ), - ) + expect(err).toEqual(new Error('`first` sur la connexion `Domain` ne peut être inférieur à zéro.')) } expect(consoleOutput).toEqual([ @@ -2049,7 +1932,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -2061,11 +1944,7 @@ describe('given the load domain connections by user id function', () => { ...connectionArgs, }) } catch (err) { - expect(err).toEqual( - new Error( - '`last` sur la connexion `Domain` ne peut être inférieur à zéro.', - ), - ) + expect(err).toEqual(new Error('`last` sur la connexion `Domain` ne peut être inférieur à zéro.')) } expect(consoleOutput).toEqual([ @@ -2077,14 +1956,12 @@ describe('given the load domain connections by user id function', () => { describe('limits are not set to numbers', () => { describe('first limit is set', () => { ;['123', {}, [], null, true].forEach((invalidInput) => { - it(`returns an error when first set to ${stringify( - invalidInput, - )}`, async () => { + it(`returns an error when first set to ${stringify(invalidInput)}`, async () => { const connectionLoader = loadDomainConnectionsByUserId({ query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -2098,9 +1975,7 @@ describe('given the load domain connections by user id function', () => { }) } catch (err) { expect(err).toEqual( - new Error( - `\`first\` doit être de type \`number\` et non \`${typeof invalidInput}\`.`, - ), + new Error(`\`first\` doit être de type \`number\` et non \`${typeof invalidInput}\`.`), ) } expect(consoleOutput).toEqual([ @@ -2113,14 +1988,12 @@ describe('given the load domain connections by user id function', () => { }) describe('last limit is set', () => { ;['123', {}, [], null, true].forEach((invalidInput) => { - it(`returns an error when last set to ${stringify( - invalidInput, - )}`, async () => { + it(`returns an error when last set to ${stringify(invalidInput)}`, async () => { const connectionLoader = loadDomainConnectionsByUserId({ query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -2134,9 +2007,7 @@ describe('given the load domain connections by user id function', () => { }) } catch (err) { expect(err).toEqual( - new Error( - `\`last\` doit être de type \`number\` et non \`${typeof invalidInput}\`.`, - ), + new Error(`\`last\` doit être de type \`number\` et non \`${typeof invalidInput}\`.`), ) } expect(consoleOutput).toEqual([ @@ -2152,17 +2023,13 @@ describe('given the load domain connections by user id function', () => { describe('given a database error', () => { describe('while querying domains', () => { it('returns an error message', async () => { - const query = jest - .fn() - .mockRejectedValue( - new Error('Unable to query domains. Please try again.'), - ) + const query = jest.fn().mockRejectedValue(new Error('Unable to query domains. Please try again.')) const connectionLoader = loadDomainConnectionsByUserId({ query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -2174,11 +2041,7 @@ describe('given the load domain connections by user id function', () => { ...connectionArgs, }) } catch (err) { - expect(err).toEqual( - new Error( - "Impossible d'interroger le(s) domaine(s). Veuillez réessayer.", - ), - ) + expect(err).toEqual(new Error("Impossible d'interroger le(s) domaine(s). Veuillez réessayer.")) } expect(consoleOutput).toEqual([ @@ -2201,7 +2064,7 @@ describe('given the load domain connections by user id function', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }) @@ -2213,11 +2076,7 @@ describe('given the load domain connections by user id function', () => { ...connectionArgs, }) } catch (err) { - expect(err).toEqual( - new Error( - 'Impossible de charger le(s) domaine(s). Veuillez réessayer.', - ), - ) + expect(err).toEqual(new Error('Impossible de charger le(s) domaine(s). Veuillez réessayer.')) } expect(consoleOutput).toEqual([ diff --git a/api/src/domain/loaders/load-domain-connections-by-organizations-id.js b/api/src/domain/loaders/load-domain-connections-by-organizations-id.js index b607f245c9..df6014772e 100644 --- a/api/src/domain/loaders/load-domain-connections-by-organizations-id.js +++ b/api/src/domain/loaders/load-domain-connections-by-organizations-id.js @@ -4,18 +4,7 @@ import { t } from '@lingui/macro' export const loadDomainConnectionsByOrgId = ({ query, userKey, language, cleanseInput, i18n, auth: { loginRequiredBool } }) => - async ({ - orgId, - permission, - after, - before, - first, - last, - ownership, - orderBy, - search, - filters = [], - }) => { + async ({ orgId, permission, after, before, first, last, ownership, orderBy, search, filters = [] }) => { const userDBId = `users/${userKey}` let afterTemplate = aql`` let afterVar = aql`` @@ -67,6 +56,9 @@ export const loadDomainConnectionsByOrgId = } else if (orderBy.field === 'protocols-status') { documentField = aql`afterVar.status.protocols` domainField = aql`domain.status.protocols` + } else if (orderBy.field === 'certificates-status') { + documentField = aql`afterVar.status.certificates` + domainField = aql`domain.status.certificates` } afterTemplate = aql` @@ -127,6 +119,9 @@ export const loadDomainConnectionsByOrgId = } else if (orderBy.field === 'protocols-status') { documentField = aql`beforeVar.status.protocols` domainField = aql`domain.status.protocols` + } else if (orderBy.field === 'certificates-status') { + documentField = aql`beforeVar.status.certificates` + domainField = aql`domain.status.certificates` } beforeTemplate = aql` @@ -248,6 +243,10 @@ export const loadDomainConnectionsByOrgId = domainField = aql`domain.status.protocols` hasNextPageDocumentField = aql`LAST(retrievedDomains).status.protocols` hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.protocols` + } else if (orderBy.field === 'certificates-status') { + domainField = aql`domain.status.certificates` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.certificates` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.certificates` } hasNextPageFilter = aql` @@ -285,6 +284,8 @@ export const loadDomainConnectionsByOrgId = sortByField = aql`domain.status.policy ${orderBy.direction},` } else if (orderBy.field === 'protocols-status') { sortByField = aql`domain.status.protocols ${orderBy.direction},` + } else if (orderBy.field === 'certificates-status') { + sortByField = aql`domain.status.certificates ${orderBy.direction},` } } @@ -363,6 +364,11 @@ export const loadDomainConnectionsByOrgId = ${domainFilters} FILTER domain.status.protocols ${comparison} ${filterValue} ` + } else if (filterCategory === 'certificates-status') { + domainFilters = aql` + ${domainFilters} + FILTER domain.status.certificates ${comparison} ${filterValue} + ` } else if (filterCategory === 'tags') { if (filterValue === 'hidden') { domainFilters = aql` @@ -374,6 +380,21 @@ export const loadDomainConnectionsByOrgId = ${domainFilters} FILTER domain.archived ${comparison} true ` + } else if (filterValue === 'nxdomain') { + domainFilters = aql` + ${domainFilters} + FILTER domain.rcode ${comparison} "NXDOMAIN" + ` + } else if (filterValue === 'blocked') { + domainFilters = aql` + ${domainFilters} + FILTER domain.blocked ${comparison} true + ` + } else if (filterValue === 'scan-pending') { + domainFilters = aql` + ${domainFilters} + FILTER domain.webScanPending ${comparison} true + ` } else { domainFilters = aql` ${domainFilters} @@ -409,7 +430,7 @@ export const loadDomainConnectionsByOrgId = showArchivedDomains = aql`` } let showHiddenDomains = aql`FILTER e.hidden != true` - if (permission === 'super_admin') { + if (['super_admin', 'owner', 'admin', 'user'].includes(permission)) { showHiddenDomains = aql`` } diff --git a/api/src/domain/loaders/load-domain-connections-by-user-id.js b/api/src/domain/loaders/load-domain-connections-by-user-id.js index 5c5fef8658..cf51e8e7ab 100644 --- a/api/src/domain/loaders/load-domain-connections-by-user-id.js +++ b/api/src/domain/loaders/load-domain-connections-by-user-id.js @@ -1,410 +1,397 @@ -import {aql} from 'arangojs' -import {fromGlobalId, toGlobalId} from 'graphql-relay' -import {t} from '@lingui/macro' +import { aql } from 'arangojs' +import { fromGlobalId, toGlobalId } from 'graphql-relay' +import { t } from '@lingui/macro' export const loadDomainConnectionsByUserId = - ({query, userKey, cleanseInput, i18n, auth: {loginRequiredBool}}) => - async ({ - after, - before, - first, - last, - ownership, - orderBy, - isSuperAdmin, - myTracker,search, - }) => { - const userDBId = `users/${userKey}` - - let ownershipOrgsOnly = aql` - LET claimDomainKeys = ( - FOR v, e IN 1..1 OUTBOUND orgId claims - OPTIONS {order: "bfs"} - RETURN v._key - ) + ({ query, userKey, cleanseInput, i18n, auth: { loginRequiredBool } }) => + async ({ after, before, first, last, ownership, orderBy, isSuperAdmin, myTracker, search }) => { + const userDBId = `users/${userKey}` + + let ownershipOrgsOnly = aql` + FOR v, e IN 1..1 OUTBOUND org._id claims ` - if (typeof ownership !== 'undefined') { - if (ownership) { - ownershipOrgsOnly = aql` - LET claimDomainKeys = ( - FOR v, e IN 1..1 OUTBOUND orgId ownership - OPTIONS {order: "bfs"} - RETURN v._key - ) + if (typeof ownership !== 'undefined') { + if (ownership) { + ownershipOrgsOnly = aql` + FOR v, e IN 1..1 OUTBOUND org._id ownership ` - } } + } - let afterTemplate = aql`` - let afterVar = aql`` + let afterTemplate = aql`` + let afterVar = aql`` - if (typeof after !== 'undefined') { - const {id: afterId} = fromGlobalId(cleanseInput(after)) - if (typeof orderBy === 'undefined') { - afterTemplate = aql`FILTER TO_NUMBER(domain._key) > TO_NUMBER(${afterId})` + if (typeof after !== 'undefined') { + const { id: afterId } = fromGlobalId(cleanseInput(after)) + if (typeof orderBy === 'undefined') { + afterTemplate = aql`FILTER TO_NUMBER(domain._key) > TO_NUMBER(${afterId})` + } else { + let afterTemplateDirection = aql`` + if (orderBy.direction === 'ASC') { + afterTemplateDirection = aql`>` } else { - let afterTemplateDirection = aql`` - if (orderBy.direction === 'ASC') { - afterTemplateDirection = aql`>` - } else { - afterTemplateDirection = aql`<` - } - - afterVar = aql`LET afterVar = DOCUMENT(domains, ${afterId})` - - let documentField = aql`` - let domainField = aql`` - /* istanbul ignore else */ - if (orderBy.field === 'domain') { - documentField = aql`afterVar.domain` - domainField = aql`domain.domain` - } else if (orderBy.field === 'dkim-status') { - documentField = aql`afterVar.status.dkim` - domainField = aql`domain.status.dkim` - } else if (orderBy.field === 'dmarc-status') { - documentField = aql`afterVar.status.dmarc` - domainField = aql`domain.status.dmarc` - } else if (orderBy.field === 'https-status') { - documentField = aql`afterVar.status.https` - domainField = aql`domain.status.https` - } else if (orderBy.field === 'spf-status') { - documentField = aql`afterVar.status.spf` - domainField = aql`domain.status.spf` - } else if (orderBy.field === 'ciphers-status') { - documentField = aql`afterVar.status.ciphers` - domainField = aql`domain.status.ciphers` - } else if (orderBy.field === 'curves-status') { - documentField = aql`afterVar.status.curves` - domainField = aql`domain.status.curves` - } else if (orderBy.field === 'hsts-status') { - documentField = aql`afterVar.status.hsts` - domainField = aql`domain.status.hsts` - } else if (orderBy.field === 'policy-status') { - documentField = aql`afterVar.status.policy` - domainField = aql`domain.status.policy` - } else if (orderBy.field === 'protocols-status') { - documentField = aql`afterVar.status.protocols` - domainField = aql`domain.status.protocols` - } - - afterTemplate = aql` - FILTER ${domainField} ${afterTemplateDirection} ${documentField} - OR (${domainField} == ${documentField} - AND TO_NUMBER(domain._key) > TO_NUMBER(${afterId})) - ` + afterTemplateDirection = aql`<` } - } - let beforeTemplate = aql`` - let beforeVar = aql`` + afterVar = aql`LET afterVar = DOCUMENT(domains, ${afterId})` - if (typeof before !== 'undefined') { - const {id: beforeId} = fromGlobalId(cleanseInput(before)) - if (typeof orderBy === 'undefined') { - beforeTemplate = aql`FILTER TO_NUMBER(domain._key) < TO_NUMBER(${beforeId})` - } else { - let beforeTemplateDirection = aql`` - if (orderBy.direction === 'ASC') { - beforeTemplateDirection = aql`<` - } else { - beforeTemplateDirection = aql`>` - } - - beforeVar = aql`LET beforeVar = DOCUMENT(domains, ${beforeId})` - - let documentField = aql`` - let domainField = aql`` - /* istanbul ignore else */ - if (orderBy.field === 'domain') { - documentField = aql`beforeVar.domain` - domainField = aql`domain.domain` - } else if (orderBy.field === 'dkim-status') { - documentField = aql`beforeVar.status.dkim` - domainField = aql`domain.status.dkim` - } else if (orderBy.field === 'dmarc-status') { - documentField = aql`beforeVar.status.dmarc` - domainField = aql`domain.status.dmarc` - } else if (orderBy.field === 'https-status') { - documentField = aql`beforeVar.status.https` - domainField = aql`domain.status.https` - } else if (orderBy.field === 'spf-status') { - documentField = aql`beforeVar.status.spf` - domainField = aql`domain.status.spf` - } else if (orderBy.field === 'ciphers-status') { - documentField = aql`beforeVar.status.ciphers` - domainField = aql`domain.status.ciphers` - } else if (orderBy.field === 'curves-status') { - documentField = aql`beforeVar.status.curves` - domainField = aql`domain.status.curves` - } else if (orderBy.field === 'hsts-status') { - documentField = aql`beforeVar.status.hsts` - domainField = aql`domain.status.hsts` - } else if (orderBy.field === 'policy-status') { - documentField = aql`beforeVar.status.policy` - domainField = aql`domain.status.policy` - } else if (orderBy.field === 'protocols-status') { - documentField = aql`beforeVar.status.protocols` - domainField = aql`domain.status.protocols` - } - - beforeTemplate = aql` - FILTER ${domainField} ${beforeTemplateDirection} ${documentField} + let documentField = aql`` + let domainField = aql`` + /* istanbul ignore else */ + if (orderBy.field === 'domain') { + documentField = aql`afterVar.domain` + domainField = aql`domain.domain` + } else if (orderBy.field === 'dkim-status') { + documentField = aql`afterVar.status.dkim` + domainField = aql`domain.status.dkim` + } else if (orderBy.field === 'dmarc-status') { + documentField = aql`afterVar.status.dmarc` + domainField = aql`domain.status.dmarc` + } else if (orderBy.field === 'https-status') { + documentField = aql`afterVar.status.https` + domainField = aql`domain.status.https` + } else if (orderBy.field === 'spf-status') { + documentField = aql`afterVar.status.spf` + domainField = aql`domain.status.spf` + } else if (orderBy.field === 'ciphers-status') { + documentField = aql`afterVar.status.ciphers` + domainField = aql`domain.status.ciphers` + } else if (orderBy.field === 'curves-status') { + documentField = aql`afterVar.status.curves` + domainField = aql`domain.status.curves` + } else if (orderBy.field === 'hsts-status') { + documentField = aql`afterVar.status.hsts` + domainField = aql`domain.status.hsts` + } else if (orderBy.field === 'policy-status') { + documentField = aql`afterVar.status.policy` + domainField = aql`domain.status.policy` + } else if (orderBy.field === 'protocols-status') { + documentField = aql`afterVar.status.protocols` + domainField = aql`domain.status.protocols` + } else if (orderBy.field === 'certificates-status') { + documentField = aql`afterVar.status.certificates` + domainField = aql`domain.status.certificates` + } + + afterTemplate = aql` + FILTER ${domainField} ${afterTemplateDirection} ${documentField} OR (${domainField} == ${documentField} - AND TO_NUMBER(domain._key) < TO_NUMBER(${beforeId})) + AND TO_NUMBER(domain._key) > TO_NUMBER(${afterId})) ` - } } + } - let limitTemplate = aql`` - if (typeof first === 'undefined' && typeof last === 'undefined') { - console.warn( - `User: ${userKey} did not have either \`first\` or \`last\` arguments set for: loadDomainConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`You must provide a \`first\` or \`last\` value to properly paginate the \`Domain\` connection.`, - ), - ) - } else if (typeof first !== 'undefined' && typeof last !== 'undefined') { - console.warn( - `User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadDomainConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`Passing both \`first\` and \`last\` to paginate the \`Domain\` connection is not supported.`, - ), - ) - } else if (typeof first === 'number' || typeof last === 'number') { - /* istanbul ignore else */ - if (first < 0 || last < 0) { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadDomainConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`\`${argSet}\` on the \`Domain\` connection cannot be less than zero.`, - ), - ) - } else if (first > 100 || last > 100) { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - const amount = typeof first !== 'undefined' ? first : last - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set to ${amount} for: loadDomainConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`Requesting \`${amount}\` records on the \`Domain\` connection exceeds the \`${argSet}\` limit of 100 records.`, - ), - ) - } else if (typeof first !== 'undefined' && typeof last === 'undefined') { - limitTemplate = aql`TO_NUMBER(domain._key) ASC LIMIT TO_NUMBER(${first})` - } else if (typeof first === 'undefined' && typeof last !== 'undefined') { - limitTemplate = aql`TO_NUMBER(domain._key) DESC LIMIT TO_NUMBER(${last})` - } - } else { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - const typeSet = typeof first !== 'undefined' ? typeof first : typeof last - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadDomainConnectionsByUserId.`, - ) - throw new Error( - i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`), - ) - } + let beforeTemplate = aql`` + let beforeVar = aql`` - let hasNextPageFilter = aql`FILTER TO_NUMBER(domain._key) > TO_NUMBER(LAST(retrievedDomains)._key)` - let hasPreviousPageFilter = aql`FILTER TO_NUMBER(domain._key) < TO_NUMBER(FIRST(retrievedDomains)._key)` - if (typeof orderBy !== 'undefined') { - let hasNextPageDirection = aql`` - let hasPreviousPageDirection = aql`` + if (typeof before !== 'undefined') { + const { id: beforeId } = fromGlobalId(cleanseInput(before)) + if (typeof orderBy === 'undefined') { + beforeTemplate = aql`FILTER TO_NUMBER(domain._key) < TO_NUMBER(${beforeId})` + } else { + let beforeTemplateDirection = aql`` if (orderBy.direction === 'ASC') { - hasNextPageDirection = aql`>` - hasPreviousPageDirection = aql`<` + beforeTemplateDirection = aql`<` } else { - hasNextPageDirection = aql`<` - hasPreviousPageDirection = aql`>` + beforeTemplateDirection = aql`>` } + beforeVar = aql`LET beforeVar = DOCUMENT(domains, ${beforeId})` + + let documentField = aql`` let domainField = aql`` - let hasNextPageDocumentField = aql`` - let hasPreviousPageDocumentField = aql`` /* istanbul ignore else */ if (orderBy.field === 'domain') { + documentField = aql`beforeVar.domain` domainField = aql`domain.domain` - hasNextPageDocumentField = aql`LAST(retrievedDomains).domain` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).domain` - } else if (orderBy.field === 'last-ran') { - domainField = aql`domain.lastRan` - hasNextPageDocumentField = aql`LAST(retrievedDomains).lastRan` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).lastRan` } else if (orderBy.field === 'dkim-status') { + documentField = aql`beforeVar.status.dkim` domainField = aql`domain.status.dkim` - hasNextPageDocumentField = aql`LAST(retrievedDomains).status.dkim` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.dkim` } else if (orderBy.field === 'dmarc-status') { + documentField = aql`beforeVar.status.dmarc` domainField = aql`domain.status.dmarc` - hasNextPageDocumentField = aql`LAST(retrievedDomains).status.dmarc` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.dmarc` } else if (orderBy.field === 'https-status') { + documentField = aql`beforeVar.status.https` domainField = aql`domain.status.https` - hasNextPageDocumentField = aql`LAST(retrievedDomains).status.https` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.https` } else if (orderBy.field === 'spf-status') { + documentField = aql`beforeVar.status.spf` domainField = aql`domain.status.spf` - hasNextPageDocumentField = aql`LAST(retrievedDomains).status.spf` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.spf` - } else if (orderBy.field === 'ssl-status') { - domainField = aql`domain.status.ssl` - hasNextPageDocumentField = aql`LAST(retrievedDomains).status.ssl` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.ssl` } else if (orderBy.field === 'ciphers-status') { + documentField = aql`beforeVar.status.ciphers` domainField = aql`domain.status.ciphers` - hasNextPageDocumentField = aql`LAST(retrievedDomains).status.ciphers` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.ciphers` } else if (orderBy.field === 'curves-status') { + documentField = aql`beforeVar.status.curves` domainField = aql`domain.status.curves` - hasNextPageDocumentField = aql`LAST(retrievedDomains).status.curves` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.curves` } else if (orderBy.field === 'hsts-status') { + documentField = aql`beforeVar.status.hsts` domainField = aql`domain.status.hsts` - hasNextPageDocumentField = aql`LAST(retrievedDomains).status.hsts` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.hsts` } else if (orderBy.field === 'policy-status') { + documentField = aql`beforeVar.status.policy` domainField = aql`domain.status.policy` - hasNextPageDocumentField = aql`LAST(retrievedDomains).status.policy` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.policy` } else if (orderBy.field === 'protocols-status') { + documentField = aql`beforeVar.status.protocols` domainField = aql`domain.status.protocols` - hasNextPageDocumentField = aql`LAST(retrievedDomains).status.protocols` - hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.protocols` + } else if (orderBy.field === 'certificates-status') { + documentField = aql`beforeVar.status.certificates` + domainField = aql`domain.status.certificates` } - hasNextPageFilter = aql` + beforeTemplate = aql` + FILTER ${domainField} ${beforeTemplateDirection} ${documentField} + OR (${domainField} == ${documentField} + AND TO_NUMBER(domain._key) < TO_NUMBER(${beforeId})) + ` + } + } + + let limitTemplate = aql`` + if (typeof first === 'undefined' && typeof last === 'undefined') { + console.warn( + `User: ${userKey} did not have either \`first\` or \`last\` arguments set for: loadDomainConnectionsByUserId.`, + ) + throw new Error( + i18n._(t`You must provide a \`first\` or \`last\` value to properly paginate the \`Domain\` connection.`), + ) + } else if (typeof first !== 'undefined' && typeof last !== 'undefined') { + console.warn( + `User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadDomainConnectionsByUserId.`, + ) + throw new Error( + i18n._(t`Passing both \`first\` and \`last\` to paginate the \`Domain\` connection is not supported.`), + ) + } else if (typeof first === 'number' || typeof last === 'number') { + /* istanbul ignore else */ + if (first < 0 || last < 0) { + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + console.warn( + `User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadDomainConnectionsByUserId.`, + ) + throw new Error(i18n._(t`\`${argSet}\` on the \`Domain\` connection cannot be less than zero.`)) + } else if (first > 100 || last > 100) { + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + const amount = typeof first !== 'undefined' ? first : last + console.warn( + `User: ${userKey} attempted to have \`${argSet}\` set to ${amount} for: loadDomainConnectionsByUserId.`, + ) + throw new Error( + i18n._( + t`Requesting \`${amount}\` records on the \`Domain\` connection exceeds the \`${argSet}\` limit of 100 records.`, + ), + ) + } else if (typeof first !== 'undefined' && typeof last === 'undefined') { + limitTemplate = aql`TO_NUMBER(domain._key) ASC LIMIT TO_NUMBER(${first})` + } else if (typeof first === 'undefined' && typeof last !== 'undefined') { + limitTemplate = aql`TO_NUMBER(domain._key) DESC LIMIT TO_NUMBER(${last})` + } + } else { + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + const typeSet = typeof first !== 'undefined' ? typeof first : typeof last + console.warn( + `User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadDomainConnectionsByUserId.`, + ) + throw new Error(i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`)) + } + + let hasNextPageFilter = aql`FILTER TO_NUMBER(domain._key) > TO_NUMBER(LAST(retrievedDomains)._key)` + let hasPreviousPageFilter = aql`FILTER TO_NUMBER(domain._key) < TO_NUMBER(FIRST(retrievedDomains)._key)` + if (typeof orderBy !== 'undefined') { + let hasNextPageDirection = aql`` + let hasPreviousPageDirection = aql`` + if (orderBy.direction === 'ASC') { + hasNextPageDirection = aql`>` + hasPreviousPageDirection = aql`<` + } else { + hasNextPageDirection = aql`<` + hasPreviousPageDirection = aql`>` + } + + let domainField = aql`` + let hasNextPageDocumentField = aql`` + let hasPreviousPageDocumentField = aql`` + /* istanbul ignore else */ + if (orderBy.field === 'domain') { + domainField = aql`domain.domain` + hasNextPageDocumentField = aql`LAST(retrievedDomains).domain` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).domain` + } else if (orderBy.field === 'last-ran') { + domainField = aql`domain.lastRan` + hasNextPageDocumentField = aql`LAST(retrievedDomains).lastRan` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).lastRan` + } else if (orderBy.field === 'dkim-status') { + domainField = aql`domain.status.dkim` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.dkim` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.dkim` + } else if (orderBy.field === 'dmarc-status') { + domainField = aql`domain.status.dmarc` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.dmarc` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.dmarc` + } else if (orderBy.field === 'https-status') { + domainField = aql`domain.status.https` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.https` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.https` + } else if (orderBy.field === 'spf-status') { + domainField = aql`domain.status.spf` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.spf` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.spf` + } else if (orderBy.field === 'ssl-status') { + domainField = aql`domain.status.ssl` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.ssl` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.ssl` + } else if (orderBy.field === 'ciphers-status') { + domainField = aql`domain.status.ciphers` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.ciphers` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.ciphers` + } else if (orderBy.field === 'curves-status') { + domainField = aql`domain.status.curves` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.curves` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.curves` + } else if (orderBy.field === 'hsts-status') { + domainField = aql`domain.status.hsts` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.hsts` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.hsts` + } else if (orderBy.field === 'policy-status') { + domainField = aql`domain.status.policy` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.policy` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.policy` + } else if (orderBy.field === 'protocols-status') { + domainField = aql`domain.status.protocols` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.protocols` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.protocols` + } else if (orderBy.field === 'certificates-status') { + domainField = aql`domain.status.certificates` + hasNextPageDocumentField = aql`LAST(retrievedDomains).status.certificates` + hasPreviousPageDocumentField = aql`FIRST(retrievedDomains).status.certificates` + } + + hasNextPageFilter = aql` FILTER ${domainField} ${hasNextPageDirection} ${hasNextPageDocumentField} OR (${domainField} == ${hasNextPageDocumentField} AND TO_NUMBER(domain._key) > TO_NUMBER(LAST(retrievedDomains)._key)) ` - hasPreviousPageFilter = aql` + hasPreviousPageFilter = aql` FILTER ${domainField} ${hasPreviousPageDirection} ${hasPreviousPageDocumentField} OR (${domainField} == ${hasPreviousPageDocumentField} AND TO_NUMBER(domain._key) < TO_NUMBER(FIRST(retrievedDomains)._key)) ` - } + } - let sortByField = aql`` - if (typeof orderBy !== 'undefined') { - /* istanbul ignore else */ - if (orderBy.field === 'domain') { - sortByField = aql`domain.domain ${orderBy.direction},` - } else if (orderBy.field === 'dkim-status') { - sortByField = aql`domain.status.dkim ${orderBy.direction},` - } else if (orderBy.field === 'dmarc-status') { - sortByField = aql`domain.status.dmarc ${orderBy.direction},` - } else if (orderBy.field === 'https-status') { - sortByField = aql`domain.status.https ${orderBy.direction},` - } else if (orderBy.field === 'spf-status') { - sortByField = aql`domain.status.spf ${orderBy.direction},` - } else if (orderBy.field === 'ciphers-status') { - sortByField = aql`domain.status.ciphers ${orderBy.direction},` - } else if (orderBy.field === 'curves-status') { - sortByField = aql`domain.status.curves ${orderBy.direction},` - } else if (orderBy.field === 'hsts-status') { - sortByField = aql`domain.status.hsts ${orderBy.direction},` - } else if (orderBy.field === 'policy-status') { - sortByField = aql`domain.status.policy ${orderBy.direction},` - } else if (orderBy.field === 'protocols-status') { - sortByField = aql`domain.status.protocols ${orderBy.direction},` - } + let sortByField = aql`` + if (typeof orderBy !== 'undefined') { + /* istanbul ignore else */ + if (orderBy.field === 'domain') { + sortByField = aql`domain.domain ${orderBy.direction},` + } else if (orderBy.field === 'dkim-status') { + sortByField = aql`domain.status.dkim ${orderBy.direction},` + } else if (orderBy.field === 'dmarc-status') { + sortByField = aql`domain.status.dmarc ${orderBy.direction},` + } else if (orderBy.field === 'https-status') { + sortByField = aql`domain.status.https ${orderBy.direction},` + } else if (orderBy.field === 'spf-status') { + sortByField = aql`domain.status.spf ${orderBy.direction},` + } else if (orderBy.field === 'ciphers-status') { + sortByField = aql`domain.status.ciphers ${orderBy.direction},` + } else if (orderBy.field === 'curves-status') { + sortByField = aql`domain.status.curves ${orderBy.direction},` + } else if (orderBy.field === 'hsts-status') { + sortByField = aql`domain.status.hsts ${orderBy.direction},` + } else if (orderBy.field === 'policy-status') { + sortByField = aql`domain.status.policy ${orderBy.direction},` + } else if (orderBy.field === 'protocols-status') { + sortByField = aql`domain.status.protocols ${orderBy.direction},` + } else if (orderBy.field === 'certificates-status') { + sortByField = aql`domain.status.certificates ${orderBy.direction},` } + } - let sortString - if (typeof last !== 'undefined') { - sortString = aql`DESC` - } else { - sortString = aql`ASC` - } + let sortString + if (typeof last !== 'undefined') { + sortString = aql`DESC` + } else { + sortString = aql`ASC` + } - let domainKeysQuery - if (myTracker) { + let domainKeysQuery + if (myTracker) { domainKeysQuery = aql` - WITH favourites, users - LET domainKeys = ( + WITH favourites, users, domains + LET collectedDomains = ( FOR v, e IN 1..1 OUTBOUND ${userDBId} favourites OPTIONS {order: "bfs"} - RETURN v._key + RETURN v ) ` } else if (isSuperAdmin) { - domainKeysQuery = aql` + domainKeysQuery = aql` WITH affiliations, domains, organizations, users, domainSearch, claims, ownership - LET domainKeys = UNIQUE(FLATTEN( - LET keys = [] - LET orgIds = (FOR org IN organizations RETURN org._id) - FOR orgId IN orgIds + LET collectedDomains = UNIQUE( + FOR org IN organizations ${ownershipOrgsOnly} - RETURN APPEND(keys, claimDomainKeys) - )) + RETURN v + ) ` - } else if (!loginRequiredBool) { - domainKeysQuery = aql` + } else if (!loginRequiredBool) { + domainKeysQuery = aql` WITH affiliations, domains, organizations, users, domainSearch, claims, ownership - LET domainKeys = UNIQUE(FLATTEN( - LET keys = [] - LET orgIds = (FOR org IN organizations RETURN org._id) - FOR orgId IN orgIds - LET claimDomainKeys = ( - FOR v, e IN 1..1 OUTBOUND orgId claims - OPTIONS {order: "bfs"} - FILTER v.archived != true - RETURN v._key - ) - RETURN APPEND(keys, claimDomainKeys) - )) + LET collectedDomains = UNIQUE( + LET userAffiliations = ( + FOR v, e IN 1..1 INBOUND ${userDBId} affiliations + FILTER e.permission != "pending" + RETURN v + ) + FOR org IN organizations + FILTER org._key IN userAffiliations[*]._key || org.verified == true + ${ownershipOrgsOnly} + FILTER v.archived != true + RETURN v + ) ` - } else { - domainKeysQuery = aql` + } else { + domainKeysQuery = aql` WITH affiliations, domains, organizations, users, domainSearch, claims, ownership - LET domainKeys = UNIQUE(FLATTEN( - LET keys = [] - LET orgIds = ( - FOR v, e IN 1..1 ANY ${userDBId} affiliations - OPTIONS {order: "bfs"} - RETURN e._from + LET collectedDomains = UNIQUE( + LET userAffiliations = ( + FOR v, e IN 1..1 INBOUND ${userDBId} affiliations + FILTER e.permission != "pending" + RETURN v ) - FOR orgId IN orgIds + LET hasVerifiedOrgAffiliation = POSITION(userAffiliations[*].verified, true) + + FOR org IN organizations + FILTER org._key IN userAffiliations[*]._key || (hasVerifiedOrgAffiliation == true && org.verified == true) ${ownershipOrgsOnly} - RETURN APPEND(keys, claimDomainKeys) - )) + FILTER v.archived != true + RETURN v + ) ` - } + } - let domainQuery = aql`` - let loopString = aql`FOR domain IN domains` - let totalCount = aql`LENGTH(domainKeys)` - if (typeof search !== 'undefined' && search !== '') { - search = cleanseInput(search) - domainQuery = aql` + let domainQuery = aql`` + let loopString = aql`FOR domain IN collectedDomains` + let totalCount = aql`LENGTH(collectedDomains)` + if (typeof search !== 'undefined' && search !== '') { + search = cleanseInput(search) + domainQuery = aql` LET tokenArr = TOKENS(${search}, "space-delimiter-analyzer") LET searchedDomains = ( FOR tokenItem in tokenArr LET token = LOWER(tokenItem) FOR domain IN domainSearch SEARCH ANALYZER(domain.domain LIKE CONCAT("%", token, "%"), "space-delimiter-analyzer") - FILTER domain._key IN domainKeys + FILTER domain IN collectedDomains RETURN domain ) ` - loopString = aql`FOR domain IN searchedDomains` - totalCount = aql`LENGTH(searchedDomains)` - } + loopString = aql`FOR domain IN searchedDomains` + totalCount = aql`LENGTH(searchedDomains)` + } - let showArchivedDomains = aql`FILTER domain.archived != true` + let showArchivedDomains = aql`FILTER domain.archived != true` if (isSuperAdmin) { showArchivedDomains = aql`` - }let requestedDomainInfo - try { - requestedDomainInfo = await query` + } + let requestedDomainInfo + try { + requestedDomainInfo = await query` ${domainKeysQuery} ${domainQuery} @@ -414,7 +401,6 @@ export const loadDomainConnectionsByUserId = LET retrievedDomains = ( ${loopString} - FILTER domain._key IN domainKeys ${showArchivedDomains} ${afterTemplate} ${beforeTemplate} @@ -426,7 +412,6 @@ export const loadDomainConnectionsByUserId = LET hasNextPage = (LENGTH( ${loopString} - FILTER domain._key IN domainKeys ${showArchivedDomains} ${hasNextPageFilter} SORT ${sortByField} TO_NUMBER(domain._key) ${sortString} LIMIT 1 @@ -435,7 +420,6 @@ export const loadDomainConnectionsByUserId = LET hasPreviousPage = (LENGTH( ${loopString} - FILTER domain._key IN domainKeys ${showArchivedDomains} ${hasPreviousPageFilter} SORT ${sortByField} TO_NUMBER(domain._key) ${sortString} LIMIT 1 @@ -451,51 +435,51 @@ export const loadDomainConnectionsByUserId = "endKey": LAST(retrievedDomains)._key } ` - } catch (err) { - console.error( - `Database error occurred while user: ${userKey} was trying to query domains in loadDomainsByUser, error: ${err}`, - ) - throw new Error(i18n._(t`Unable to query domain(s). Please try again.`)) - } - - let domainsInfo - try { - domainsInfo = await requestedDomainInfo.next() - } catch (err) { - console.error( - `Cursor error occurred while user: ${userKey} was trying to gather domains in loadDomainsByUser, error: ${err}`, - ) - throw new Error(i18n._(t`Unable to load domain(s). Please try again.`)) - } - - if (domainsInfo.domains.length === 0) { - return { - edges: [], - totalCount: 0, - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, - } - } + } catch (err) { + console.error( + `Database error occurred while user: ${userKey} was trying to query domains in loadDomainsByUser, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to query domain(s). Please try again.`)) + } - const edges = domainsInfo.domains.map((domain) => { - return { - cursor: toGlobalId('domain', domain._key), - node: domain, - } - }) + let domainsInfo + try { + domainsInfo = await requestedDomainInfo.next() + } catch (err) { + console.error( + `Cursor error occurred while user: ${userKey} was trying to gather domains in loadDomainsByUser, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to load domain(s). Please try again.`)) + } + if (domainsInfo.domains.length === 0) { return { - edges, - totalCount: domainsInfo.totalCount, + edges: [], + totalCount: 0, pageInfo: { - hasNextPage: domainsInfo.hasNextPage, - hasPreviousPage: domainsInfo.hasPreviousPage, - startCursor: toGlobalId('domain', domainsInfo.startKey), - endCursor: toGlobalId('domain', domainsInfo.endKey), + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', }, } } + + const edges = domainsInfo.domains.map((domain) => { + return { + cursor: toGlobalId('domain', domain._key), + node: domain, + } + }) + + return { + edges, + totalCount: domainsInfo.totalCount, + pageInfo: { + hasNextPage: domainsInfo.hasNextPage, + hasPreviousPage: domainsInfo.hasPreviousPage, + startCursor: toGlobalId('domain', domainsInfo.startKey), + endCursor: toGlobalId('domain', domainsInfo.endKey), + }, + } + } diff --git a/api/src/domain/mutations/__tests__/add-organizations-domains.test.js b/api/src/domain/mutations/__tests__/add-organizations-domains.test.js index b1e8adab2c..347bc0afa5 100644 --- a/api/src/domain/mutations/__tests__/add-organizations-domains.test.js +++ b/api/src/domain/mutations/__tests__/add-organizations-domains.test.js @@ -8,13 +8,7 @@ import frenchMessages from '../../../locale/fr/messages' import { createQuerySchema } from '../../../query' import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' -import { - checkPermission, - userRequired, - saltedHash, - verifiedRequired, - tfaRequired, -} from '../../../auth' +import { checkPermission, userRequired, saltedHash, verifiedRequired, tfaRequired } from '../../../auth' import { loadDomainByDomain } from '../../loaders' import { loadOrgByKey } from '../../../organization/loaders' import { loadUserByKey } from '../../../user/loaders' @@ -128,6 +122,7 @@ describe('given the addOrganizationsDomains mutation', () => { domains: ["test.domain.gov", "test.domain2.gov"] hideNewDomains: false tagNewDomains: false + tagStagingDomains: false audit: false } ) { @@ -197,6 +192,7 @@ describe('given the addOrganizationsDomains mutation', () => { domains: ["test.domain.gov", "test.domain2.gov"] hideNewDomains: false tagNewDomains: false + tagStagingDomains: false audit: true } ) { @@ -343,6 +339,7 @@ describe('given the addOrganizationsDomains mutation', () => { domains: ["test.domain.gov", "test.domain2.gov"] hideNewDomains: false tagNewDomains: false + tagStagingDomains: false audit: false } ) { diff --git a/api/src/domain/mutations/__tests__/create-domain.test.js b/api/src/domain/mutations/__tests__/create-domain.test.js index 1ee487a253..529934d060 100644 --- a/api/src/domain/mutations/__tests__/create-domain.test.js +++ b/api/src/domain/mutations/__tests__/create-domain.test.js @@ -1,13 +1,13 @@ -import {ensure, dbNameFromFile} from 'arango-tools' -import {graphql, GraphQLSchema, GraphQLError} from 'graphql' -import {toGlobalId} from 'graphql-relay' -import {setupI18n} from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { toGlobalId } from 'graphql-relay' +import { setupI18n } from '@lingui/core' -import {createQuerySchema} from '../../../query' -import {createMutationSchema} from '../../../mutation' +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import {cleanseInput, slugify} from '../../../validators' +import { cleanseInput, slugify } from '../../../validators' import { checkPermission, userRequired, @@ -15,17 +15,15 @@ import { saltedHash, verifiedRequired, tfaRequired, + checkDomainPermission, } from '../../../auth' -import {loadDomainByDomain} from '../../loaders' -import { - loadOrgByKey, - loadOrgConnectionsByDomainId, -} from '../../../organization/loaders' -import {loadUserByKey} from '../../../user/loaders' +import { loadDomainByDomain } from '../../loaders' +import { loadOrgByKey, loadOrgConnectionsByDomainId } from '../../../organization/loaders' +import { loadUserByKey } from '../../../user/loaders' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' -const {DB_PASS: rootPass, DB_URL: url, HASHING_SECRET} = process.env +const { DB_PASS: rootPass, DB_URL: url, HASHING_SECRET } = process.env describe('create a domain', () => { let query, drop, truncate, schema, collections, transaction, user, org @@ -48,8 +46,20 @@ describe('create a domain', () => { consoleOutput.length = 0 }) describe('given a successful domain creation', () => { + const i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) beforeAll(async () => { - ;({query, drop, truncate, collections, transaction} = await ensure({ + ;({ query, drop, truncate, collections, transaction } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -160,29 +170,34 @@ describe('create a domain', () => { userKey: user._key, publish: jest.fn(), auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), saltedHash: saltedHash(HASHING_SECRET), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - checkSuperAdmin: checkSuperAdmin({userKey: user._key, query}), + checkSuperAdmin: checkSuperAdmin({ userKey: user._key, query }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), }, loaders: { - loadDomainByDomain: loadDomainByDomain({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), loadOrgConnectionsByDomainId: loadOrgConnectionsByDomainId({ query, language: 'en', userKey: user._key, cleanseInput, - auth: {loginRequiredBool: true}, + auth: { loginRequiredBool: true }, }), - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -314,29 +329,34 @@ describe('create a domain', () => { userKey: user._key, publish: jest.fn(), auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), saltedHash: saltedHash(HASHING_SECRET), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - checkSuperAdmin: checkSuperAdmin({userKey: user._key, query}), + checkSuperAdmin: checkSuperAdmin({ userKey: user._key, query }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), }, loaders: { - loadDomainByDomain: loadDomainByDomain({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), loadOrgConnectionsByDomainId: loadOrgConnectionsByDomainId({ query, language: 'en', userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }), - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -445,159 +465,34 @@ describe('create a domain', () => { userKey: user._key, publish: jest.fn(), auth: { - checkPermission: checkPermission({userKey: user._key, query}), - saltedHash: saltedHash(HASHING_SECRET), - userRequired: userRequired({ + checkDomainPermission: checkDomainPermission({ + i18n, userKey: user._key, - loadUserByKey: loadUserByKey({query}), - }), - checkSuperAdmin: checkSuperAdmin({userKey: user._key, query}), - verifiedRequired: verifiedRequired({}), - tfaRequired: tfaRequired({}), - }, - loaders: { - loadDomainByDomain: loadDomainByDomain({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadOrgConnectionsByDomainId: loadOrgConnectionsByDomainId({ query, - language: 'en', - userKey: user._key, - cleanseInput, - auth: {loginRequired: true}, }), - loadUserByKey: loadUserByKey({query}), - }, - validators: {cleanseInput, slugify}, - }, - ) - - const domainCursor = await query` - FOR domain IN domains - RETURN domain - ` - const domain = await domainCursor.next() - - const expectedResponse = { - data: { - createDomain: { - result: { - id: toGlobalId('domain', domain._key), - domain: 'test.gc.ca', - lastRan: null, - selectors: ['selector1', 'selector2'], - status: { - dkim: null, - dmarc: null, - https: null, - spf: null, - ssl: null, - }, - organizations: { - edges: [ - { - node: { - id: toGlobalId('organization', org._key), - name: 'Treasury Board of Canada Secretariat', - }, - }, - ], - }, - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully created ${domain.domain} in org: treasury-board-secretariat.`, - ]) - }) - }) - describe('user has user permission level', () => { - beforeEach(async () => { - await collections.affiliations.save({ - _from: org._id, - _to: user._id, - permission: 'user', - }) - }) - it('returns the domain', async () => { - const response = await graphql( - schema, - ` - mutation { - createDomain( - input: { - orgId: "${toGlobalId('organization', org._key)}" - domain: "test.gc.ca" - selectors: ["selector1", "selector2"] - } - ) { - result { - ... on Domain { - id - domain - lastRan - selectors - status { - dkim - dmarc - https - spf - ssl - } - organizations(first: 5) { - edges { - node { - id - name - } - } - } - } - ... on DomainError { - code - description - } - } - } - } - `, - null, - { - request: { - language: 'en', - }, - query, - collections: collectionNames, - transaction, - userKey: user._key, - publish: jest.fn(), - auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkPermission: checkPermission({ userKey: user._key, query }), saltedHash: saltedHash(HASHING_SECRET), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - checkSuperAdmin: checkSuperAdmin({userKey: user._key, query}), + checkSuperAdmin: checkSuperAdmin({ userKey: user._key, query }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), }, loaders: { - loadDomainByDomain: loadDomainByDomain({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), loadOrgConnectionsByDomainId: loadOrgConnectionsByDomainId({ query, language: 'en', userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }), - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -748,29 +643,34 @@ describe('create a domain', () => { userKey: user._key, publish: jest.fn(), auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), saltedHash: saltedHash(HASHING_SECRET), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - checkSuperAdmin: checkSuperAdmin({userKey: user._key, query}), + checkSuperAdmin: checkSuperAdmin({ userKey: user._key, query }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), }, loaders: { - loadDomainByDomain: loadDomainByDomain({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), loadOrgConnectionsByDomainId: loadOrgConnectionsByDomainId({ query, language: 'en', userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }), - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -895,29 +795,34 @@ describe('create a domain', () => { userKey: user._key, publish: jest.fn(), auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), saltedHash: saltedHash(HASHING_SECRET), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - checkSuperAdmin: checkSuperAdmin({userKey: user._key, query}), + checkSuperAdmin: checkSuperAdmin({ userKey: user._key, query }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), }, loaders: { - loadDomainByDomain: loadDomainByDomain({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), loadOrgConnectionsByDomainId: loadOrgConnectionsByDomainId({ query, language: 'en', userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }), - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -1042,29 +947,34 @@ describe('create a domain', () => { userKey: user._key, publish: jest.fn(), auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), saltedHash: saltedHash(HASHING_SECRET), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - checkSuperAdmin: checkSuperAdmin({userKey: user._key, query}), + checkSuperAdmin: checkSuperAdmin({ userKey: user._key, query }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), }, loaders: { - loadDomainByDomain: loadDomainByDomain({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), loadOrgConnectionsByDomainId: loadOrgConnectionsByDomainId({ query, language: 'en', userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }), - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -1081,12 +991,7 @@ describe('create a domain', () => { id: toGlobalId('domain', domain._key), domain: 'test.gc.ca', lastRan: null, - selectors: [ - 'selector1', - 'selector2', - 'selector3', - 'selector4', - ], + selectors: ['selector1', 'selector2', 'selector3', 'selector4'], status: { dkim: null, dmarc: null, @@ -1194,29 +1099,34 @@ describe('create a domain', () => { userKey: user._key, publish: jest.fn(), auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), saltedHash: saltedHash(HASHING_SECRET), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - checkSuperAdmin: checkSuperAdmin({userKey: user._key, query}), + checkSuperAdmin: checkSuperAdmin({ userKey: user._key, query }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), }, loaders: { - loadDomainByDomain: loadDomainByDomain({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), loadOrgConnectionsByDomainId: loadOrgConnectionsByDomainId({ query, language: 'en', userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }), - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -1341,29 +1251,34 @@ describe('create a domain', () => { userKey: user._key, publish: jest.fn(), auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), saltedHash: saltedHash(HASHING_SECRET), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), - checkSuperAdmin: checkSuperAdmin({userKey: user._key, query}), + checkSuperAdmin: checkSuperAdmin({ userKey: user._key, query }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), }, loaders: { - loadDomainByDomain: loadDomainByDomain({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), loadOrgConnectionsByDomainId: loadOrgConnectionsByDomainId({ query, language: 'en', userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }), - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -1425,8 +1340,8 @@ describe('create a domain', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -1442,11 +1357,7 @@ describe('create a domain', () => { ` mutation { createDomain( - input: { - orgId: "b3JnYW5pemF0aW9uOjE=" - domain: "test.gc.ca" - selectors: ["selector1", "selector2"] - } + input: { orgId: "b3JnYW5pemF0aW9uOjE=", domain: "test.gc.ca", selectors: ["selector1", "selector2"] } ) { result { ... on Domain { @@ -1490,6 +1401,11 @@ describe('create a domain', () => { userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn(), saltedHash: jest.fn(), userRequired: jest.fn(), @@ -1508,7 +1424,7 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -1517,8 +1433,7 @@ describe('create a domain', () => { createDomain: { result: { code: 400, - description: - 'Unable to create domain in unknown organization.', + description: 'Unable to create domain in unknown organization.', }, }, }, @@ -1585,6 +1500,11 @@ describe('create a domain', () => { userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue(undefined), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -1605,7 +1525,7 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -1614,8 +1534,7 @@ describe('create a domain', () => { createDomain: { result: { code: 400, - description: - 'Permission Denied: Please contact organization user for help with creating domain.', + description: 'Permission Denied: Please contact organization user for help with creating domain.', }, }, }, @@ -1684,6 +1603,11 @@ describe('create a domain', () => { userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -1704,7 +1628,7 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -1713,8 +1637,7 @@ describe('create a domain', () => { createDomain: { result: { code: 400, - description: - 'Unable to create domain, organization has already claimed it.', + description: 'Unable to create domain, organization has already claimed it.', }, }, }, @@ -1776,14 +1699,17 @@ describe('create a domain', () => { request: { language: 'en', }, - query: jest - .fn() - .mockRejectedValue(new Error('Database error occurred.')), + query: jest.fn().mockRejectedValue(new Error('Database error occurred.')), collections: collectionNames, transaction, userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -1804,13 +1730,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError('Unable to create domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to create domain. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -1870,15 +1794,18 @@ describe('create a domain', () => { language: 'en', }, query: jest.fn().mockReturnValue({ - next: jest - .fn() - .mockRejectedValue(new Error('Cursor error occurred.')), + next: jest.fn().mockRejectedValue(new Error('Cursor error occurred.')), }), collections: collectionNames, transaction, userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -1899,13 +1826,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError('Unable to create domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to create domain. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -1968,14 +1893,17 @@ describe('create a domain', () => { collections: collectionNames, transaction: jest.fn().mockReturnValue({ step: jest.fn().mockReturnValueOnce({ - next: jest - .fn() - .mockRejectedValue(new Error('cursor error')), + next: jest.fn().mockRejectedValue(new Error('cursor error')), }), }), userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -1996,13 +1924,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError('Unable to create domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to create domain. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2067,13 +1993,16 @@ describe('create a domain', () => { }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), }), userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -2094,13 +2023,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError('Unable to create domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to create domain. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2172,6 +2099,11 @@ describe('create a domain', () => { userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -2192,13 +2124,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError('Unable to create domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to create domain. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2262,13 +2192,16 @@ describe('create a domain', () => { }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), }), userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -2294,13 +2227,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError('Unable to create domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to create domain. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2362,14 +2293,16 @@ describe('create a domain', () => { }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockReturnValueOnce().mockRejectedValue(new Error('trx step error')), }), userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -2395,13 +2328,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError('Unable to create domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to create domain. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2467,13 +2398,16 @@ describe('create a domain', () => { collections: collectionNames, transaction: jest.fn().mockReturnValue({ step: jest.fn().mockReturnValue(), - commit: jest - .fn() - .mockRejectedValue(new Error('trx commit error')), + commit: jest.fn().mockRejectedValue(new Error('trx commit error')), }), userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -2499,13 +2433,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError('Unable to create domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to create domain. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2520,8 +2452,8 @@ describe('create a domain', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -2537,11 +2469,7 @@ describe('create a domain', () => { ` mutation { createDomain( - input: { - orgId: "b3JnYW5pemF0aW9uOjE=" - domain: "test.gc.ca" - selectors: ["selector1", "selector2"] - } + input: { orgId: "b3JnYW5pemF0aW9uOjE=", domain: "test.gc.ca", selectors: ["selector1", "selector2"] } ) { result { ... on Domain { @@ -2585,6 +2513,11 @@ describe('create a domain', () => { userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn(), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -2603,7 +2536,7 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -2612,8 +2545,7 @@ describe('create a domain', () => { createDomain: { result: { code: 400, - description: - 'Impossible de créer un domaine dans une organisation inconnue.', + description: 'Impossible de créer un domaine dans une organisation inconnue.', }, }, }, @@ -2680,6 +2612,11 @@ describe('create a domain', () => { userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue(undefined), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -2700,7 +2637,7 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -2779,6 +2716,11 @@ describe('create a domain', () => { userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -2799,7 +2741,7 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) @@ -2808,8 +2750,7 @@ describe('create a domain', () => { createDomain: { result: { code: 400, - description: - "Impossible de créer le domaine, l'organisation l'a déjà réclamé.", + description: "Impossible de créer le domaine, l'organisation l'a déjà réclamé.", }, }, }, @@ -2871,14 +2812,17 @@ describe('create a domain', () => { request: { language: 'en', }, - query: jest - .fn() - .mockRejectedValue(new Error('Database error occurred.')), + query: jest.fn().mockRejectedValue(new Error('Database error occurred.')), collections: collectionNames, transaction, userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -2899,15 +2843,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError( - 'Impossible de créer un domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de créer un domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2967,15 +2907,18 @@ describe('create a domain', () => { language: 'en', }, query: jest.fn().mockReturnValue({ - next: jest - .fn() - .mockRejectedValue(new Error('Cursor error occurred.')), + next: jest.fn().mockRejectedValue(new Error('Cursor error occurred.')), }), collections: collectionNames, transaction, userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -2996,15 +2939,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError( - 'Impossible de créer un domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de créer un domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -3067,14 +3006,17 @@ describe('create a domain', () => { collections: collectionNames, transaction: jest.fn().mockReturnValue({ step: jest.fn().mockReturnValueOnce({ - next: jest - .fn() - .mockRejectedValue(new Error('cursor error')), + next: jest.fn().mockRejectedValue(new Error('cursor error')), }), }), userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -3095,15 +3037,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError( - 'Impossible de créer un domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de créer un domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -3168,13 +3106,16 @@ describe('create a domain', () => { }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), }), userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -3195,15 +3136,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError( - 'Impossible de créer un domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de créer un domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -3275,6 +3212,11 @@ describe('create a domain', () => { userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -3295,15 +3237,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError( - 'Impossible de créer un domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de créer un domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -3367,13 +3305,16 @@ describe('create a domain', () => { }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), }), userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -3399,15 +3340,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError( - 'Impossible de créer un domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de créer un domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -3469,14 +3406,16 @@ describe('create a domain', () => { }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockReturnValueOnce().mockRejectedValue(new Error('trx step error')), }), userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -3502,15 +3441,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError( - 'Impossible de créer un domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de créer un domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -3576,13 +3511,16 @@ describe('create a domain', () => { collections: collectionNames, transaction: jest.fn().mockReturnValue({ step: jest.fn().mockReturnValue(), - commit: jest - .fn() - .mockRejectedValue(new Error('trx commit error')), + commit: jest.fn().mockRejectedValue(new Error('trx commit error')), }), userKey: 123, publish: jest.fn(), auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkPermission: jest.fn().mockReturnValue('admin'), userRequired: jest.fn(), saltedHash: jest.fn(), @@ -3608,15 +3546,11 @@ describe('create a domain', () => { load: jest.fn(), }, }, - validators: {cleanseInput, slugify}, + validators: { cleanseInput, slugify }, }, ) - const error = [ - new GraphQLError( - 'Impossible de créer un domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de créer un domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ diff --git a/api/src/domain/mutations/__tests__/remove-domain.test.js b/api/src/domain/mutations/__tests__/remove-domain.test.js index 24f8eba505..4ca5e3cac5 100644 --- a/api/src/domain/mutations/__tests__/remove-domain.test.js +++ b/api/src/domain/mutations/__tests__/remove-domain.test.js @@ -3424,7 +3424,7 @@ describe('removing a domain', () => { removeDomain: { result: { code: 403, - description: 'Permission Denied: Please contact super admin for help with removing domain.', + description: 'Permission Denied: Please contact organization admin for help with removing domain.', }, }, }, @@ -3432,7 +3432,7 @@ describe('removing a domain', () => { expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `User: 123 attempted to remove domain.gc.ca in temp-org but does not have permission to remove a domain from a verified check org.`, + `User: 123 attempted to remove domain.gc.ca in temp-org however they do not have permission in that org.`, ]) }) }) @@ -3499,7 +3499,7 @@ describe('removing a domain', () => { removeDomain: { result: { code: 403, - description: 'Permission Denied: Please contact super admin for help with removing domain.', + description: 'Permission Denied: Please contact organization admin for help with removing domain.', }, }, }, @@ -3507,7 +3507,7 @@ describe('removing a domain', () => { expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `User: 123 attempted to remove domain.gc.ca in temp-org but does not have permission to remove a domain from a verified check org.`, + `User: 123 attempted to remove domain.gc.ca in temp-org however they do not have permission in that org.`, ]) }) }) @@ -4572,7 +4572,7 @@ describe('removing a domain', () => { result: { code: 403, description: - "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la mise à jour de ce domaine.", + "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide afin de supprimer le domaine.", }, }, }, @@ -4580,7 +4580,7 @@ describe('removing a domain', () => { expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `User: 123 attempted to remove domain.gc.ca in temp-org but does not have permission to remove a domain from a verified check org.`, + `User: 123 attempted to remove domain.gc.ca in temp-org however they do not have permission in that org.`, ]) }) }) @@ -4648,7 +4648,7 @@ describe('removing a domain', () => { result: { code: 403, description: - "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la mise à jour de ce domaine.", + "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide afin de supprimer le domaine.", }, }, }, @@ -4656,7 +4656,7 @@ describe('removing a domain', () => { expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `User: 123 attempted to remove domain.gc.ca in temp-org but does not have permission to remove a domain from a verified check org.`, + `User: 123 attempted to remove domain.gc.ca in temp-org however they do not have permission in that org.`, ]) }) }) diff --git a/api/src/domain/mutations/__tests__/update-domain.test.js b/api/src/domain/mutations/__tests__/update-domain.test.js index d25bedc353..b5dd6b1280 100644 --- a/api/src/domain/mutations/__tests__/update-domain.test.js +++ b/api/src/domain/mutations/__tests__/update-domain.test.js @@ -1,26 +1,21 @@ -import {setupI18n} from '@lingui/core' -import {ensure, dbNameFromFile} from 'arango-tools' -import {graphql, GraphQLSchema, GraphQLError} from 'graphql' -import {toGlobalId} from 'graphql-relay' +import { setupI18n } from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { toGlobalId } from 'graphql-relay' -import {createQuerySchema} from '../../../query' -import {createMutationSchema} from '../../../mutation' +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import {cleanseInput, slugify} from '../../../validators' -import { - checkPermission, - userRequired, - verifiedRequired, - tfaRequired, -} from '../../../auth' -import {loadDomainByKey} from '../../loaders' -import {loadOrgByKey} from '../../../organization/loaders' -import {loadUserByKey} from '../../../user/loaders' +import { cleanseInput, slugify } from '../../../validators' +import { checkPermission, userRequired, verifiedRequired, tfaRequired, checkDomainPermission } from '../../../auth' +import { loadDomainByKey } from '../../loaders' +import { loadOrgByKey } from '../../../organization/loaders' +import { loadUserByKey } from '../../../user/loaders' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env describe('updating a domain', () => { let query, drop, truncate, schema, collections, transaction, user @@ -45,9 +40,21 @@ describe('updating a domain', () => { describe('given a successful domain update', () => { let org, domain + const i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) beforeAll(async () => { // Generate DB Items - ;({query, drop, truncate, collections, transaction} = await ensure({ + ;({ query, drop, truncate, collections, transaction } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -144,10 +151,15 @@ describe('updating a domain', () => { transaction, userKey: user._key, auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), @@ -157,9 +169,9 @@ describe('updating a domain', () => { slugify, }, loaders: { - loadDomainByKey: loadDomainByKey({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), + loadDomainByKey: loadDomainByKey({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), }, }, ) @@ -178,9 +190,7 @@ describe('updating a domain', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully updated domain: ${domain._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully updated domain: ${domain._key}.`]) }) }) describe('user updates selectors', () => { @@ -217,10 +227,15 @@ describe('updating a domain', () => { transaction, userKey: user._key, auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), @@ -230,9 +245,9 @@ describe('updating a domain', () => { slugify, }, loaders: { - loadDomainByKey: loadDomainByKey({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), + loadDomainByKey: loadDomainByKey({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), }, }, ) @@ -251,9 +266,7 @@ describe('updating a domain', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully updated domain: ${domain._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully updated domain: ${domain._key}.`]) }) }) describe('user updates domain and selectors', () => { @@ -291,10 +304,15 @@ describe('updating a domain', () => { transaction, userKey: user._key, auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), @@ -304,9 +322,9 @@ describe('updating a domain', () => { slugify, }, loaders: { - loadDomainByKey: loadDomainByKey({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), + loadDomainByKey: loadDomainByKey({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), }, }, ) @@ -325,9 +343,7 @@ describe('updating a domain', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully updated domain: ${domain._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully updated domain: ${domain._key}.`]) }) }) }) @@ -370,10 +386,15 @@ describe('updating a domain', () => { transaction, userKey: user._key, auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), @@ -383,9 +404,9 @@ describe('updating a domain', () => { slugify, }, loaders: { - loadDomainByKey: loadDomainByKey({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), + loadDomainByKey: loadDomainByKey({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), }, }, ) @@ -404,9 +425,7 @@ describe('updating a domain', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully updated domain: ${domain._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully updated domain: ${domain._key}.`]) }) }) describe('user updates selectors', () => { @@ -443,10 +462,15 @@ describe('updating a domain', () => { transaction, userKey: user._key, auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), @@ -456,9 +480,9 @@ describe('updating a domain', () => { slugify, }, loaders: { - loadDomainByKey: loadDomainByKey({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), + loadDomainByKey: loadDomainByKey({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), }, }, ) @@ -477,9 +501,7 @@ describe('updating a domain', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully updated domain: ${domain._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully updated domain: ${domain._key}.`]) }) }) describe('user updates domain and selectors', () => { @@ -517,10 +539,15 @@ describe('updating a domain', () => { transaction, userKey: user._key, auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), @@ -530,9 +557,9 @@ describe('updating a domain', () => { slugify, }, loaders: { - loadDomainByKey: loadDomainByKey({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), + loadDomainByKey: loadDomainByKey({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), }, }, ) @@ -551,9 +578,7 @@ describe('updating a domain', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully updated domain: ${domain._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully updated domain: ${domain._key}.`]) }) }) }) @@ -596,10 +621,15 @@ describe('updating a domain', () => { transaction, userKey: user._key, auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), @@ -609,9 +639,9 @@ describe('updating a domain', () => { slugify, }, loaders: { - loadDomainByKey: loadDomainByKey({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), + loadDomainByKey: loadDomainByKey({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), }, }, ) @@ -630,9 +660,7 @@ describe('updating a domain', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully updated domain: ${domain._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully updated domain: ${domain._key}.`]) }) }) describe('user updates selectors', () => { @@ -669,10 +697,15 @@ describe('updating a domain', () => { transaction, userKey: user._key, auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), @@ -682,9 +715,9 @@ describe('updating a domain', () => { slugify, }, loaders: { - loadDomainByKey: loadDomainByKey({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), + loadDomainByKey: loadDomainByKey({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), }, }, ) @@ -703,9 +736,7 @@ describe('updating a domain', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully updated domain: ${domain._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully updated domain: ${domain._key}.`]) }) }) describe('user updates domain and selectors', () => { @@ -743,10 +774,15 @@ describe('updating a domain', () => { transaction, userKey: user._key, auth: { - checkPermission: checkPermission({userKey: user._key, query}), + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), + checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ userKey: user._key, - loadUserByKey: loadUserByKey({query}), + loadUserByKey: loadUserByKey({ query }), }), verifiedRequired: verifiedRequired({}), tfaRequired: tfaRequired({}), @@ -756,9 +792,9 @@ describe('updating a domain', () => { slugify, }, loaders: { - loadDomainByKey: loadDomainByKey({query}), - loadOrgByKey: loadOrgByKey({query, language: 'en'}), - loadUserByKey: loadUserByKey({query}), + loadDomainByKey: loadDomainByKey({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), }, }, ) @@ -777,9 +813,7 @@ describe('updating a domain', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully updated domain: ${domain._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully updated domain: ${domain._key}.`]) }) }) }) @@ -791,8 +825,8 @@ describe('updating a domain', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -857,7 +891,7 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn(), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) @@ -935,7 +969,7 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue(undefined), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) @@ -1013,7 +1047,7 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue({}), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) @@ -1071,7 +1105,7 @@ describe('updating a domain', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 0}), + query: jest.fn().mockReturnValue({ count: 0 }), collections: collectionNames, transaction, userKey: 123, @@ -1092,7 +1126,7 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue({}), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) @@ -1102,8 +1136,7 @@ describe('updating a domain', () => { updateDomain: { result: { code: 400, - description: - 'Unable to update domain that does not belong to the given organization.', + description: 'Unable to update domain that does not belong to the given organization.', }, }, }, @@ -1172,14 +1205,12 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue({}), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) - const error = [ - new GraphQLError('Unable to update domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to update domain. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -1224,12 +1255,10 @@ describe('updating a domain', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 1}), + query: jest.fn().mockReturnValue({ count: 1 }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), }), userKey: 123, auth: { @@ -1249,14 +1278,12 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue({}), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) - const error = [ - new GraphQLError('Unable to update domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to update domain. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -1300,13 +1327,11 @@ describe('updating a domain', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 1}), + query: jest.fn().mockReturnValue({ count: 1 }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ step: jest.fn(), - commit: jest - .fn() - .mockRejectedValue(new Error('trx commit error')), + commit: jest.fn().mockRejectedValue(new Error('trx commit error')), }), userKey: 123, auth: { @@ -1326,14 +1351,12 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue({}), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) - const error = [ - new GraphQLError('Unable to update domain. Please try again.'), - ] + const error = [new GraphQLError('Unable to update domain. Please try again.')] expect(response.errors).toEqual(error) }) @@ -1344,8 +1367,8 @@ describe('updating a domain', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -1410,7 +1433,7 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn(), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) @@ -1420,8 +1443,7 @@ describe('updating a domain', () => { updateDomain: { result: { code: 400, - description: - 'Impossible de mettre à jour un domaine inconnu.', + description: 'Impossible de mettre à jour un domaine inconnu.', }, }, }, @@ -1489,7 +1511,7 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue(undefined), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) @@ -1499,8 +1521,7 @@ describe('updating a domain', () => { updateDomain: { result: { code: 400, - description: - 'Impossible de mettre à jour le domaine dans un org inconnu.', + description: 'Impossible de mettre à jour le domaine dans un org inconnu.', }, }, }, @@ -1568,7 +1589,7 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue({}), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) @@ -1626,7 +1647,7 @@ describe('updating a domain', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 0}), + query: jest.fn().mockReturnValue({ count: 0 }), collections: collectionNames, transaction, userKey: 123, @@ -1647,7 +1668,7 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue({}), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) @@ -1657,8 +1678,7 @@ describe('updating a domain', () => { updateDomain: { result: { code: 400, - description: - "Impossible de mettre à jour un domaine qui n'appartient pas à l'organisation donnée.", + description: "Impossible de mettre à jour un domaine qui n'appartient pas à l'organisation donnée.", }, }, }, @@ -1727,16 +1747,12 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue({}), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) - const error = [ - new GraphQLError( - 'Impossible de mettre à jour le domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de mettre à jour le domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -1781,12 +1797,10 @@ describe('updating a domain', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 1}), + query: jest.fn().mockReturnValue({ count: 1 }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), }), userKey: 123, auth: { @@ -1806,16 +1820,12 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue({}), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) - const error = [ - new GraphQLError( - 'Impossible de mettre à jour le domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de mettre à jour le domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -1859,13 +1869,11 @@ describe('updating a domain', () => { null, { i18n, - query: jest.fn().mockReturnValue({count: 1}), + query: jest.fn().mockReturnValue({ count: 1 }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ step: jest.fn(), - commit: jest - .fn() - .mockRejectedValue(new Error('trx commit error')), + commit: jest.fn().mockRejectedValue(new Error('trx commit error')), }), userKey: 123, auth: { @@ -1885,16 +1893,12 @@ describe('updating a domain', () => { loadOrgByKey: { load: jest.fn().mockReturnValue({}), }, - loadUserByKey: {load: jest.fn()}, + loadUserByKey: { load: jest.fn() }, }, }, ) - const error = [ - new GraphQLError( - 'Impossible de mettre à jour le domaine. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de mettre à jour le domaine. Veuillez réessayer.')] expect(response.errors).toEqual(error) }) diff --git a/api/src/domain/mutations/add-organizations-domains.js b/api/src/domain/mutations/add-organizations-domains.js index f58f5ef6c6..c0b72454d9 100644 --- a/api/src/domain/mutations/add-organizations-domains.js +++ b/api/src/domain/mutations/add-organizations-domains.js @@ -8,13 +8,11 @@ import { logActivity } from '../../audit-logs/mutations/log-activity' export const addOrganizationsDomains = new mutationWithClientMutationId({ name: 'AddOrganizationsDomains', - description: - 'Mutation used to create multiple new domains for an organization.', + description: 'Mutation used to create multiple new domains for an organization.', inputFields: () => ({ orgId: { type: GraphQLNonNull(GraphQLID), - description: - 'The global id of the organization you wish to assign this domain to.', + description: 'The global id of the organization you wish to assign this domain to.', }, domains: { type: GraphQLNonNull(new GraphQLList(Domain)), @@ -28,6 +26,10 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ type: GraphQLBoolean, description: 'New domains will be tagged with NEW.', }, + tagStagingDomains: { + type: GraphQLBoolean, + description: 'New domains will be tagged with STAGING.', + }, audit: { type: GraphQLBoolean, description: 'Audit logs will be created.', @@ -36,8 +38,7 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ outputFields: () => ({ result: { type: bulkModifyDomainsUnion, - description: - '`BulkModifyDomainsUnion` returning either a `DomainBulkResult`, or `DomainErrorType` object.', + description: '`BulkModifyDomainsUnion` returning either a `DomainBulkResult`, or `DomainErrorType` object.', resolve: (payload) => payload, }, }), @@ -50,13 +51,7 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ collections, transaction, userKey, - auth: { - checkPermission, - saltedHash, - userRequired, - verifiedRequired, - tfaRequired, - }, + auth: { checkPermission, saltedHash, userRequired, verifiedRequired, tfaRequired }, loaders: { loadDomainByDomain, loadOrgByKey }, validators: { cleanseInput }, }, @@ -90,9 +85,12 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ } else { tagNewDomains = false } - let tags = [] - if (tagNewDomains) { - tags = [{ en: 'NEW', fr: 'NOUVEAU' }] + + let tagStagingDomains + if (typeof args.tagStagingDomains !== 'undefined') { + tagStagingDomains = args.tagStagingDomains + } else { + tagStagingDomains = false } let audit @@ -106,9 +104,7 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ const org = await loadOrgByKey.load(orgId) if (typeof org === 'undefined') { - console.warn( - `User: ${userKey} attempted to add domains to an organization: ${orgId} that does not exist.`, - ) + console.warn(`User: ${userKey} attempted to add domains to an organization: ${orgId} that does not exist.`) return { _type: 'error', code: 400, @@ -126,12 +122,18 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ return { _type: 'error', code: 400, - description: i18n._( - t`Permission Denied: Please contact organization user for help with creating domains.`, - ), + description: i18n._(t`Permission Denied: Please contact organization user for help with creating domains.`), } } + const tags = [] + if (tagNewDomains) { + tags.push({ en: 'NEW', fr: 'NOUVEAU' }) + } + if (tagStagingDomains) { + tags.push({ en: 'STAGING', fr: 'DÉV' }) + } + const updatedProperties = [] if (typeof tags !== 'undefined' && tags.length > 0) { updatedProperties.push({ @@ -180,9 +182,7 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ RETURN MERGE({ _id: org._id, _key: org._key, _rev: org._rev }, TRANSLATE(${request.language}, org.orgDetails)) ` } catch (err) { - console.error( - `Database error occurred while running check to see if domain already exists in an org: ${err}`, - ) + console.error(`Database error occurred while running check to see if domain already exists in an org: ${err}`) continue } @@ -190,9 +190,7 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ try { checkOrgDomain = await checkDomainCursor.next() } catch (err) { - console.error( - `Cursor error occurred while running check to see if domain already exists in an org: ${err}`, - ) + console.error(`Cursor error occurred while running check to see if domain already exists in an org: ${err}`) continue } @@ -227,9 +225,7 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ `, ) } catch (err) { - console.error( - `Transaction step error occurred for user: ${userKey} when inserting new domain: ${err}`, - ) + console.error(`Transaction step error occurred for user: ${userKey} when inserting new domain: ${err}`) continue } @@ -257,9 +253,7 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ `, ) } catch (err) { - console.error( - `Transaction step error occurred for user: ${userKey} when inserting new domain edge: ${err}`, - ) + console.error(`Transaction step error occurred for user: ${userKey} when inserting new domain edge: ${err}`) continue } } else { @@ -277,9 +271,7 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ `, ) } catch (err) { - console.error( - `Transaction step error occurred for user: ${userKey} when inserting domain edge: ${err}`, - ) + console.error(`Transaction step error occurred for user: ${userKey} when inserting domain edge: ${err}`) continue } } @@ -287,16 +279,12 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ try { await trx.commit() } catch (err) { - console.error( - `Transaction commit error occurred while user: ${userKey} was creating domains: ${err}`, - ) + console.error(`Transaction commit error occurred while user: ${userKey} was creating domains: ${err}`) throw new Error(i18n._(t`Unable to create domains. Please try again.`)) } if (audit) { - console.info( - `User: ${userKey} successfully added domain: ${insertDomain.domain} to org: ${org.slug}.`, - ) + console.info(`User: ${userKey} successfully added domain: ${insertDomain.domain} to org: ${org.slug}.`) await logActivity({ transaction, collections, @@ -322,9 +310,7 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ } if (!audit) { - console.info( - `User: ${userKey} successfully added ${domainCount} domain(s) to org: ${org.slug}.`, - ) + console.info(`User: ${userKey} successfully added ${domainCount} domain(s) to org: ${org.slug}.`) await logActivity({ transaction, collections, @@ -349,9 +335,7 @@ export const addOrganizationsDomains = new mutationWithClientMutationId({ return { _type: 'result', - status: i18n._( - t`Successfully added ${domainCount} domain(s) to ${org.slug}.`, - ), + status: i18n._(t`Successfully added ${domainCount} domain(s) to ${org.slug}.`), } }, }) diff --git a/api/src/domain/mutations/create-domain.js b/api/src/domain/mutations/create-domain.js index 819cb33c35..595c13c1bc 100644 --- a/api/src/domain/mutations/create-domain.js +++ b/api/src/domain/mutations/create-domain.js @@ -111,7 +111,7 @@ export const createDomain = new mutationWithClientMutationId({ // Check to see if user belongs to org const permission = await checkPermission({ orgId: org._id }) - if (permission !== 'user' && permission !== 'admin' && permission !== 'super_admin') { + if (!['admin', 'owner', 'super_admin'].includes(permission)) { console.warn( `User: ${userKey} attempted to create a domain in: ${org.slug}, however they do not have permission to do so.`, ) diff --git a/api/src/domain/mutations/remove-domain.js b/api/src/domain/mutations/remove-domain.js index dffa9eb786..f8a0e4a1c4 100644 --- a/api/src/domain/mutations/remove-domain.js +++ b/api/src/domain/mutations/remove-domain.js @@ -84,26 +84,27 @@ export const removeDomain = new mutationWithClientMutationId({ // Get permission const permission = await checkPermission({ orgId: org._id }) - // Check to see if domain belongs to verified check org - if (org.verified && permission !== 'super_admin') { + if (['admin', 'owner', 'super_admin'].includes(permission) === false) { console.warn( - `User: ${userKey} attempted to remove ${domain.domain} in ${org.slug} but does not have permission to remove a domain from a verified check org.`, + `User: ${userKey} attempted to remove ${domain.domain} in ${org.slug} however they do not have permission in that org.`, ) return { _type: 'error', code: 403, - description: i18n._(t`Permission Denied: Please contact super admin for help with removing domain.`), + description: i18n._(t`Permission Denied: Please contact organization admin for help with removing domain.`), } } - if (permission !== 'super_admin' && permission !== 'admin') { + // Check to see if domain belongs to verified check org + // if domain returns NXDOMAIN, allow removal + if (org.verified && permission !== 'super_admin' && domain.rcode !== 'NXDOMAIN') { console.warn( - `User: ${userKey} attempted to remove ${domain.domain} in ${org.slug} however they do not have permission in that org.`, + `User: ${userKey} attempted to remove ${domain.domain} in ${org.slug} but does not have permission to remove a domain from a verified check org.`, ) return { _type: 'error', code: 403, - description: i18n._(t`Permission Denied: Please contact organization admin for help with removing domain.`), + description: i18n._(t`Permission Denied: Please contact super admin for help with removing domain.`), } } diff --git a/api/src/domain/mutations/request-scan.js b/api/src/domain/mutations/request-scan.js index 2d31967e66..68c5444ec8 100644 --- a/api/src/domain/mutations/request-scan.js +++ b/api/src/domain/mutations/request-scan.js @@ -3,10 +3,11 @@ import { GraphQLString } from 'graphql' import { mutationWithClientMutationId } from 'graphql-relay' import { Domain } from '../../scalars' +import { logActivity } from '../../audit-logs' export const requestScan = new mutationWithClientMutationId({ name: 'RequestScan', - description: 'This mutation is used to step a manual scan on a requested domain.', + description: 'This mutation is used to start a manual scan on a requested domain.', inputFields: () => ({ domain: { type: Domain, @@ -23,10 +24,13 @@ export const requestScan = new mutationWithClientMutationId({ mutateAndGetPayload: async ( args, { + query, + collections, + transaction, i18n, userKey, publish, - auth: { checkDomainPermission, userRequired, verifiedRequired, loginRequiredBool }, + auth: { checkDomainPermission, userRequired, verifiedRequired }, loaders: { loadDomainByDomain, loadWebConnectionsByDomainId, loadWebScansByWebId }, validators: { cleanseInput }, }, @@ -43,23 +47,54 @@ export const requestScan = new mutationWithClientMutationId({ if (typeof domain === 'undefined') { console.warn( - `User: ${userKey} attempted to step a one time scan on: ${domainInput} however domain cannot be found.`, + `User: ${userKey} attempted to start a one time scan on: ${domainInput} however domain cannot be found.`, ) throw new Error(i18n._(t`Unable to request a one time scan on an unknown domain.`)) } - if (loginRequiredBool) { - // Check to see if user has access to domain - const permission = await checkDomainPermission({ domainId: domain._id }) + // Check to see if user has access to domain + const permission = await checkDomainPermission({ domainId: domain._id }) - if (!permission) { - console.warn( - `User: ${userKey} attempted to step a one time scan on: ${domain.domain} however they do not have permission to do so.`, + if (!permission) { + console.warn( + `User: ${userKey} attempted to start a one time scan on: ${domain.domain} however they do not have permission to do so.`, + ) + throw new Error( + i18n._(t`Permission Denied: Please contact organization user for help with scanning this domain.`), + ) + } + + let orgsClaimingDomainQuery + try { + orgsClaimingDomainQuery = await query` + WITH domains, users, organizations + LET userAffiliations = ( + FOR v, e IN 1..1 ANY ${user._id} affiliations + FILTER e.permission != "pending" + RETURN v ) - throw new Error( - i18n._(t`Permission Denied: Please contact organization user for help with scanning this domain.`), + LET domainOrgClaims = ( + FOR v, e IN 1..1 ANY ${domain._id} claims + RETURN v ) - } + LET orgsClaimingDomain = UNIQUE(domainOrgClaims[* FILTER CURRENT.verified == true || CURRENT IN userAffiliations]) + RETURN orgsClaimingDomain + ` + } catch (err) { + console.error( + `Database error when retrieving organizations claiming domain: ${userKey} and domain: ${domain._id}: ${err}`, + ) + throw new Error(i18n._(t`Error while requesting scan. Please try again.`)) + } + + let orgsClaimingDomain + try { + orgsClaimingDomain = await orgsClaimingDomainQuery.next() + } catch (err) { + console.error( + `Cursor error when retrieving organizations claiming domain: ${userKey} and domain: ${domain._id}: ${err}`, + ) + throw new Error(i18n._(t`Error while requesting scan. Please try again.`)) } // Check to see if a scan is already pending @@ -77,7 +112,7 @@ export const requestScan = new mutationWithClientMutationId({ const timeDifferenceInMinutes = (Date.now() - new Date(webConnection.timestamp).getTime()) / 1000 / 60 if (result.status.toUpperCase() === 'PENDING' && timeDifferenceInMinutes < 30) { console.warn( - `User: ${userKey} attempted to step a one time scan on: ${domain.domain} however a scan is already pending.`, + `User: ${userKey} attempted to start a one time scan on: ${domain.domain} however a scan is already pending.`, ) throw new Error(i18n._(t`Unable to request a one time scan on a domain that already has a pending scan.`)) } @@ -85,7 +120,7 @@ export const requestScan = new mutationWithClientMutationId({ } } catch (err) { console.error( - `Error occurred when user: ${userKey} attempted to step a one time scan on: ${domain.domain}, error: ${err}`, + `Error occurred when user: ${userKey} attempted to start a one time scan on: ${domain.domain}, error: ${err}`, ) throw new Error(i18n._(t`Unable to request a one time scan. Please try again.`)) } @@ -102,6 +137,29 @@ export const requestScan = new mutationWithClientMutationId({ }, }) + // Logs scan request activity for each org claiming domain + for (const orgClaimingDomain of orgsClaimingDomain) { + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'scan', + target: { + resource: domain.domain, + organization: { + id: orgClaimingDomain._key, + name: orgClaimingDomain.orgDetails.en.name, + }, // name of resource being acted upon + resourceType: 'domain', // user, org, domain + }, + }) + } + console.info(`User: ${userKey} successfully dispatched a one time scan on domain: ${domain.domain}.`) return { diff --git a/api/src/domain/mutations/update-domain.js b/api/src/domain/mutations/update-domain.js index 6383786449..923af0e7b5 100644 --- a/api/src/domain/mutations/update-domain.js +++ b/api/src/domain/mutations/update-domain.js @@ -1,16 +1,15 @@ -import {GraphQLID, GraphQLNonNull, GraphQLList, GraphQLBoolean} from 'graphql' -import {mutationWithClientMutationId, fromGlobalId} from 'graphql-relay' -import {t} from '@lingui/macro' +import { GraphQLID, GraphQLNonNull, GraphQLList, GraphQLBoolean } from 'graphql' +import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay' +import { t } from '@lingui/macro' -import {updateDomainUnion} from '../unions' -import {Domain, Selectors} from '../../scalars' +import { updateDomainUnion } from '../unions' +import { Domain, Selectors } from '../../scalars' import { logActivity } from '../../audit-logs/mutations/log-activity' import { inputTag } from '../inputs/domain-tag' export const updateDomain = new mutationWithClientMutationId({ name: 'UpdateDomain', - description: - 'Mutation allows the modification of domains if domain is updated through out its life-cycle', + description: 'Mutation allows the modification of domains if domain is updated through out its life-cycle', inputFields: () => ({ domainId: { type: GraphQLNonNull(GraphQLID), @@ -18,8 +17,7 @@ export const updateDomain = new mutationWithClientMutationId({ }, orgId: { type: GraphQLNonNull(GraphQLID), - description: - 'The global ID of the organization used for permission checks.', + description: 'The global ID of the organization used for permission checks.', }, domain: { type: Domain, @@ -27,29 +25,25 @@ export const updateDomain = new mutationWithClientMutationId({ }, selectors: { type: new GraphQLList(Selectors), - description: - 'The updated DKIM selector strings corresponding to this domain.', + description: 'The updated DKIM selector strings corresponding to this domain.', }, tags: { description: 'List of labelled tags users have applied to the domain.', type: new GraphQLList(inputTag), }, hidden: { - description: - "Value that determines if the domain is excluded from an organization's score.", + description: "Value that determines if the domain is excluded from an organization's score.", type: GraphQLBoolean, }, archived: { - description: - 'Value that determines if the domain is excluded from the scanning process.', + description: 'Value that determines if the domain is excluded from the scanning process.', type: GraphQLBoolean, }, }), outputFields: () => ({ result: { type: updateDomainUnion, - description: - '`UpdateDomainUnion` returning either a `Domain`, or `DomainError` object.', + description: '`UpdateDomainUnion` returning either a `Domain`, or `DomainError` object.', resolve: (payload) => payload, }, }), @@ -61,19 +55,19 @@ export const updateDomain = new mutationWithClientMutationId({ collections, transaction, userKey, - auth: {checkPermission, userRequired, verifiedRequired, tfaRequired}, - validators: {cleanseInput}, - loaders: {loadDomainByKey, loadOrgByKey}, + auth: { checkPermission, userRequired, verifiedRequired, tfaRequired }, + validators: { cleanseInput }, + loaders: { loadDomainByKey, loadOrgByKey }, }, ) => { // Get User const user = await userRequired() - verifiedRequired({user}) - tfaRequired({user}) + verifiedRequired({ user }) + tfaRequired({ user }) - const {id: domainId} = fromGlobalId(cleanseInput(args.domainId)) - const {id: orgId} = fromGlobalId(cleanseInput(args.orgId)) + const { id: domainId } = fromGlobalId(cleanseInput(args.domainId)) + const { id: orgId } = fromGlobalId(cleanseInput(args.orgId)) const updatedDomain = cleanseInput(args.domain) let selectors @@ -133,22 +127,16 @@ export const updateDomain = new mutationWithClientMutationId({ } // Check permission - const permission = await checkPermission({orgId: org._id}) + const permission = await checkPermission({ orgId: org._id }) - if ( - permission !== 'user' && - permission !== 'admin' && - permission !== 'super_admin' - ) { + if (!['admin', 'owner', 'super_admin'].includes(permission)) { console.warn( `User: ${userKey} attempted to update domain: ${domainId} for org: ${orgId}, however they do not have permission in that org.`, ) return { _type: 'error', code: 403, - description: i18n._( - t`Permission Denied: Please contact organization user for help with updating this domain.`, - ), + description: i18n._(t`Permission Denied: Please contact organization user for help with updating this domain.`), } } @@ -175,9 +163,7 @@ export const updateDomain = new mutationWithClientMutationId({ return { _type: 'error', code: 400, - description: i18n._( - t`Unable to update domain that does not belong to the given organization.`, - ), + description: i18n._(t`Unable to update domain that does not belong to the given organization.`), } } @@ -219,17 +205,13 @@ export const updateDomain = new mutationWithClientMutationId({ RETURN MERGE({ id: claim._key, _type: "claim" }, claim) ` } catch (err) { - console.error( - `Database error occurred when user: ${userKey} running loadDomainByKey: ${err}`, - ) + console.error(`Database error occurred when user: ${userKey} running loadDomainByKey: ${err}`) } let claim try { claim = await claimCursor.next() } catch (err) { - console.error( - `Cursor error occurred when user: ${userKey} running loadDomainByKey: ${err}`, - ) + console.error(`Cursor error occurred when user: ${userKey} running loadDomainByKey: ${err}`) } const claimToInsert = { @@ -252,9 +234,7 @@ export const updateDomain = new mutationWithClientMutationId({ console.error( `Transaction step error occurred when user: ${userKey} attempted to update domain edge, error: ${err}`, ) - throw new Error( - i18n._(t`Unable to update domain edge. Please try again.`), - ) + throw new Error(i18n._(t`Unable to update domain edge. Please try again.`)) } // Commit transaction @@ -283,8 +263,7 @@ export const updateDomain = new mutationWithClientMutationId({ } if ( typeof selectors !== 'undefined' && - JSON.stringify(domainToInsert.selectors) !== - JSON.stringify(domain.selectors) + JSON.stringify(domainToInsert.selectors) !== JSON.stringify(domain.selectors) ) { updatedProperties.push({ name: 'selectors', @@ -293,10 +272,7 @@ export const updateDomain = new mutationWithClientMutationId({ }) } - if ( - typeof tags !== 'undefined' && - JSON.stringify(claim.tags) !== JSON.stringify(tags) - ) { + if (typeof tags !== 'undefined' && JSON.stringify(claim.tags) !== JSON.stringify(tags)) { updatedProperties.push({ name: 'tags', oldValue: claim.tags, @@ -349,9 +325,7 @@ export const updateDomain = new mutationWithClientMutationId({ target: { resource: domain.domain, resourceType: 'domain', // user, org, domain - updatedProperties: [ - { name: 'archived', oldValue: domain.archived, newValue: archived }, - ], + updatedProperties: [{ name: 'archived', oldValue: domain.archived, newValue: archived }], }, }) } diff --git a/api/src/domain/objects/__tests__/domain.test.js b/api/src/domain/objects/__tests__/domain.test.js index c3235b8f0c..851b4effce 100644 --- a/api/src/domain/objects/__tests__/domain.test.js +++ b/api/src/domain/objects/__tests__/domain.test.js @@ -1,23 +1,17 @@ -import { - GraphQLNonNull, - GraphQLID, - GraphQLList, - GraphQLString, - GraphQLBoolean, -} from 'graphql' -import {toGlobalId} from 'graphql-relay' -import {setupI18n} from '@lingui/core' - -import {tokenize} from '../../../auth' -import {organizationConnection} from '../../../organization' -import {domainStatus} from '../domain-status' -import {dmarcSummaryType} from '../../../dmarc-summaries' -import {webConnection} from '../../../web-scan' -import {domainType} from '../../index' -import {Domain, Selectors} from '../../../scalars' +import { GraphQLNonNull, GraphQLID, GraphQLList, GraphQLString, GraphQLBoolean } from 'graphql' +import { toGlobalId } from 'graphql-relay' +import { setupI18n } from '@lingui/core' + +import { tokenize } from '../../../auth' +import { organizationConnection } from '../../../organization' +import { domainStatus } from '../domain-status' +import { dmarcSummaryType } from '../../../dmarc-summaries' +import { webConnection } from '../../../web-scan' +import { domainType } from '../../index' +import { Domain, Selectors } from '../../../scalars' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import {dnsScanConnection} from "../../../dns-scan" +import { dnsScanConnection } from '../../../dns-scan' describe('given the domain object', () => { describe('testing its field definitions', () => { @@ -67,9 +61,7 @@ describe('given the domain object', () => { const demoType = domainType.getFields() expect(demoType).toHaveProperty('organizations') - expect(demoType.organizations.type).toMatchObject( - organizationConnection.connectionType, - ) + expect(demoType.organizations.type).toMatchObject(organizationConnection.connectionType) }) it('has an dnsScan field', () => { const demoType = domainType.getFields() @@ -93,9 +85,7 @@ describe('given the domain object', () => { const demoType = domainType.getFields() expect(demoType).toHaveProperty('yearlyDmarcSummaries') - expect(demoType.yearlyDmarcSummaries.type).toMatchObject( - GraphQLList(dmarcSummaryType), - ) + expect(demoType.yearlyDmarcSummaries.type).toMatchObject(GraphQLList(dmarcSummaryType)) }) }) describe('testing the field resolvers', () => { @@ -113,27 +103,21 @@ describe('given the domain object', () => { it('returns the resolved value', () => { const demoType = domainType.getFields() - expect(demoType.id.resolve({id: '1'})).toEqual( - toGlobalId('domain', 1), - ) + expect(demoType.id.resolve({ id: '1' })).toEqual(toGlobalId('domain', 1)) }) }) describe('testing the domain resolver', () => { it('returns the resolved value', () => { const demoType = domainType.getFields() - expect(demoType.domain.resolve({domain: 'test.gc.ca'})).toEqual( - 'test.gc.ca', - ) + expect(demoType.domain.resolve({ domain: 'test.gc.ca' })).toEqual('test.gc.ca') }) }) describe('testing the dmarcPhase resolver', () => { it('returns the resolved value', () => { const demoType = domainType.getFields() - expect( - demoType.dmarcPhase.resolve({phase: 'not implemented'}), - ).toEqual('not implemented') + expect(demoType.dmarcPhase.resolve({ phase: 'not implemented' })).toEqual('not implemented') }) }) describe('testing the hasDMARCReport resolver', () => { @@ -146,7 +130,7 @@ describe('given the domain object', () => { await expect( demoType.hasDMARCReport.resolve( - {_id: 1}, + { _id: 1 }, {}, { auth: { @@ -167,7 +151,7 @@ describe('given the domain object', () => { await expect( demoType.hasDMARCReport.resolve( - {_id: 1}, + { _id: 1 }, {}, { auth: { @@ -184,9 +168,7 @@ describe('given the domain object', () => { it('returns the resolved value', () => { const demoType = domainType.getFields() - expect( - demoType.lastRan.resolve({lastRan: '2020-10-02T12:43:39Z'}), - ).toEqual('2020-10-02T12:43:39Z') + expect(demoType.lastRan.resolve({ lastRan: '2020-10-02T12:43:39Z' })).toEqual('2020-10-02T12:43:39Z') }) }) describe('testing the selectors resolver', () => { @@ -195,10 +177,18 @@ describe('given the domain object', () => { const selectors = ['selector1', 'selector2'] - expect(demoType.selectors.resolve({selectors})).toEqual([ - 'selector1', - 'selector2', - ]) + expect( + demoType.selectors.resolve( + { selectors }, + {}, + { + auth: { + userRequired: jest.fn().mockReturnValue(true), + checkDomainPermission: jest.fn().mockReturnValue(true), + }, + }, + ), + ).resolves.toEqual(['selector1', 'selector2']) }) }) describe('testing the status resolver', () => { @@ -213,7 +203,7 @@ describe('given the domain object', () => { ssl: 'fail', } - expect(demoType.status.resolve({status})).toEqual({ + expect(demoType.status.resolve({ status })).toEqual({ dkim: 'pass', dmarc: 'pass', https: 'info', @@ -272,13 +262,11 @@ describe('given the domain object', () => { await expect( demoType.organizations.resolve( - {_id: '1'}, - {first: 1}, + { _id: '1' }, + { first: 1 }, { loaders: { - loadOrgConnectionsByDomainId: jest - .fn() - .mockReturnValue(expectedResult), + loadOrgConnectionsByDomainId: jest.fn().mockReturnValue(expectedResult), }, auth: { checkSuperAdmin: jest.fn().mockReturnValue(false), @@ -293,19 +281,19 @@ describe('given the domain object', () => { const demoType = domainType.getFields() const response = await demoType.web.resolve( - {_id: '1'}, - {limit: 1}, - { - loaders: { - loadWebConnectionsByDomainId: jest - .fn() - .mockReturnValue({"_id": "1", "_key": "1"}), - }, - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(false), - }, + { _id: '1' }, + { limit: 1 }, + { + loaders: { + loadWebConnectionsByDomainId: jest.fn().mockReturnValue({ _id: '1', _key: '1' }), + }, + auth: { + checkDomainPermission: jest.fn().mockReturnValue(true), + checkSuperAdmin: jest.fn().mockReturnValue(false), + userRequired: jest.fn().mockReturnValue(true), }, - ) + }, + ) expect(response).toEqual({ _id: '1', @@ -318,19 +306,19 @@ describe('given the domain object', () => { const demoType = domainType.getFields() const response = await demoType.dnsScan.resolve( - {_id: '1'}, - {limit: 1}, - { - loaders: { - loadDnsConnectionsByDomainId: jest - .fn() - .mockReturnValue({"_id": "1", "_key": "1"}), - }, - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(false), - }, + { _id: '1' }, + { limit: 1 }, + { + loaders: { + loadDnsConnectionsByDomainId: jest.fn().mockReturnValue({ _id: '1', _key: '1' }), }, - ) + auth: { + checkDomainPermission: jest.fn().mockReturnValue(true), + checkSuperAdmin: jest.fn().mockReturnValue(false), + userRequired: jest.fn().mockReturnValue(true), + }, + }, + ) expect(response).toEqual({ _id: '1', @@ -360,14 +348,10 @@ describe('given the domain object', () => { { userKey: '1', loaders: { - loadDmarcSummaryEdgeByDomainIdAndPeriod: jest - .fn() - .mockReturnValue({ - _to: 'dmarcSummaries/1', - }), - loadStartDateFromPeriod: jest - .fn() - .mockReturnValue('2021-01-01'), + loadDmarcSummaryEdgeByDomainIdAndPeriod: jest.fn().mockReturnValue({ + _to: 'dmarcSummaries/1', + }), + loadStartDateFromPeriod: jest.fn().mockReturnValue('2021-01-01'), }, auth: { checkDomainOwnership: jest.fn().mockReturnValue(true), @@ -388,8 +372,8 @@ describe('given the domain object', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -417,9 +401,7 @@ describe('given the domain object', () => { userKey: '1', loaders: { loadDmarcSummaryEdgeByDomainIdAndPeriod: jest.fn(), - loadStartDateFromPeriod: jest - .fn() - .mockReturnValue('2021-01-01'), + loadStartDateFromPeriod: jest.fn().mockReturnValue('2021-01-01'), }, auth: { checkDomainOwnership: jest.fn().mockReturnValue(false), @@ -428,11 +410,7 @@ describe('given the domain object', () => { }, }, ), - ).rejects.toEqual( - new Error( - 'Unable to retrieve DMARC report information for: test1.gc.ca', - ), - ) + ).rejects.toEqual(new Error('Unable to retrieve DMARC report information for: test1.gc.ca')) expect(consoleOutput).toEqual([ `User: 1 attempted to access dmarc report period data for 1, but does not belong to an org with ownership.`, @@ -445,8 +423,8 @@ describe('given the domain object', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -474,9 +452,7 @@ describe('given the domain object', () => { userKey: '1', loaders: { loadDmarcSummaryEdgeByDomainIdAndPeriod: jest.fn(), - loadStartDateFromPeriod: jest - .fn() - .mockReturnValue('2021-01-01'), + loadStartDateFromPeriod: jest.fn().mockReturnValue('2021-01-01'), }, auth: { checkDomainOwnership: jest.fn().mockReturnValue(false), @@ -485,11 +461,7 @@ describe('given the domain object', () => { }, }, ), - ).rejects.toEqual( - new Error( - 'Impossible de récupérer les informations du rapport DMARC pour : test1.gc.ca', - ), - ) + ).rejects.toEqual(new Error('Impossible de récupérer les informations du rapport DMARC pour : test1.gc.ca')) expect(consoleOutput).toEqual([ `User: 1 attempted to access dmarc report period data for 1, but does not belong to an org with ownership.`, @@ -544,8 +516,8 @@ describe('given the domain object', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -584,11 +556,7 @@ describe('given the domain object', () => { }, }, ), - ).rejects.toEqual( - new Error( - 'Unable to retrieve DMARC report information for: test1.gc.ca', - ), - ) + ).rejects.toEqual(new Error('Unable to retrieve DMARC report information for: test1.gc.ca')) expect(consoleOutput).toEqual([ `User: 1 attempted to access dmarc report period data for 1, but does not belong to an org with ownership.`, ]) @@ -600,8 +568,8 @@ describe('given the domain object', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -637,11 +605,7 @@ describe('given the domain object', () => { }, }, ), - ).rejects.toEqual( - new Error( - 'Impossible de récupérer les informations du rapport DMARC pour : test1.gc.ca', - ), - ) + ).rejects.toEqual(new Error('Impossible de récupérer les informations du rapport DMARC pour : test1.gc.ca')) expect(consoleOutput).toEqual([ `User: 1 attempted to access dmarc report period data for 1, but does not belong to an org with ownership.`, ]) diff --git a/api/src/domain/objects/domain.js b/api/src/domain/objects/domain.js index ab19a9cc3c..63f3947c72 100644 --- a/api/src/domain/objects/domain.js +++ b/api/src/domain/objects/domain.js @@ -32,13 +32,10 @@ export const domainType = new GraphQLObjectType({ hasDMARCReport: { type: GraphQLBoolean, description: 'Whether or not the domain has a aggregate dmarc report.', - resolve: async ({ _id }, _, { auth: { checkDomainOwnership, userRequired, loginRequiredBool } }) => { - if (loginRequiredBool) await userRequired() - const hasDMARCReport = await checkDomainOwnership({ + resolve: async ({ _id }, _, { auth: { checkDomainOwnership } }) => { + return await checkDomainOwnership({ domainId: _id, }) - - return hasDMARCReport }, }, lastRan: { @@ -53,7 +50,16 @@ export const domainType = new GraphQLObjectType({ selectors: { type: new GraphQLList(Selectors), description: 'Domain Keys Identified Mail (DKIM) selector strings associated with domain.', - resolve: ({ selectors }) => selectors, + resolve: async ({ _id, selectors }, _, { userKey, auth: { checkDomainPermission, userRequired } }) => { + await userRequired() + const permitted = await checkDomainPermission({ domainId: _id }) + if (!permitted) { + console.warn(`User: ${userKey} attempted to access selectors for ${_id}, but does not have permission.`) + throw new Error(t`Cannot query domain selectors without permission.`) + } + + return selectors + }, }, status: { type: domainStatus, @@ -100,12 +106,11 @@ export const domainType = new GraphQLObjectType({ resolve: async ({ _id }, args, { auth: { checkSuperAdmin }, loaders: { loadOrgConnectionsByDomainId } }) => { const isSuperAdmin = await checkSuperAdmin() - const orgs = await loadOrgConnectionsByDomainId({ + return await loadOrgConnectionsByDomainId({ domainId: _id, isSuperAdmin, ...args, }) - return orgs }, }, dnsScan: { @@ -130,7 +135,20 @@ export const domainType = new GraphQLObjectType({ ...connectionArgs, }, description: `DNS scan results.`, - resolve: async ({ _id }, args, { loaders: { loadDnsConnectionsByDomainId } }) => { + resolve: async ( + { _id }, + args, + { userKey, auth: { checkDomainPermission, userRequired }, loaders: { loadDnsConnectionsByDomainId } }, + ) => { + await userRequired() + const permitted = await checkDomainPermission({ domainId: _id }) + if (!permitted) { + console.warn( + `User: ${userKey} attempted to access dns scan results for ${_id}, but does not have permission.`, + ) + throw new Error(t`Cannot query dns scan results without permission.`) + } + return await loadDnsConnectionsByDomainId({ domainId: _id, ...args, @@ -163,7 +181,20 @@ export const domainType = new GraphQLObjectType({ }, ...connectionArgs, }, - resolve: async ({ _id }, args, { loaders: { loadWebConnectionsByDomainId } }) => { + resolve: async ( + { _id }, + args, + { userKey, auth: { checkDomainPermission, userRequired }, loaders: { loadWebConnectionsByDomainId } }, + ) => { + await userRequired() + const permitted = await checkDomainPermission({ domainId: _id }) + if (!permitted) { + console.warn( + `User: ${userKey} attempted to access web scan results for ${_id}, but does not have permission.`, + ) + throw new Error(t`Cannot query web scan results without permission.`) + } + return await loadWebConnectionsByDomainId({ domainId: _id, ...args, @@ -190,21 +221,19 @@ export const domainType = new GraphQLObjectType({ i18n, userKey, loaders: { loadDmarcSummaryEdgeByDomainIdAndPeriod, loadStartDateFromPeriod }, - auth: { checkDomainOwnership, userRequired, loginRequiredBool }, + auth: { checkDomainOwnership, userRequired }, }, ) => { - if (loginRequiredBool) { - await userRequired() - const permitted = await checkDomainOwnership({ - domainId: _id, - }) + await userRequired() + const permitted = await checkDomainOwnership({ + domainId: _id, + }) - if (!permitted) { - console.warn( - `User: ${userKey} attempted to access dmarc report period data for ${_key}, but does not belong to an org with ownership.`, - ) - throw new Error(i18n._(t`Unable to retrieve DMARC report information for: ${domain}`)) - } + if (!permitted) { + console.warn( + `User: ${userKey} attempted to access dmarc report period data for ${_key}, but does not belong to an org with ownership.`, + ) + throw new Error(i18n._(t`Unable to retrieve DMARC report information for: ${domain}`)) } const startDate = loadStartDateFromPeriod({ period: month, year }) @@ -227,39 +256,30 @@ export const domainType = new GraphQLObjectType({ resolve: async ( { _id, _key, domain }, __, - { - i18n, - userKey, - loaders: { loadDmarcYearlySumEdge }, - auth: { checkDomainOwnership, userRequired, loginRequiredBool }, - }, + { i18n, userKey, loaders: { loadDmarcYearlySumEdge }, auth: { checkDomainOwnership, userRequired } }, ) => { - if (loginRequiredBool) { - await userRequired() + await userRequired() - const permitted = await checkDomainOwnership({ - domainId: _id, - }) + const permitted = await checkDomainOwnership({ + domainId: _id, + }) - if (!permitted) { - console.warn( - `User: ${userKey} attempted to access dmarc report period data for ${_key}, but does not belong to an org with ownership.`, - ) - throw new Error(i18n._(t`Unable to retrieve DMARC report information for: ${domain}`)) - } + if (!permitted) { + console.warn( + `User: ${userKey} attempted to access dmarc report period data for ${_key}, but does not belong to an org with ownership.`, + ) + throw new Error(i18n._(t`Unable to retrieve DMARC report information for: ${domain}`)) } const dmarcSummaryEdges = await loadDmarcYearlySumEdge({ domainId: _id, }) - const edges = dmarcSummaryEdges.map((edge) => ({ + return dmarcSummaryEdges.map((edge) => ({ domainKey: _key, _id: edge._to, startDate: edge.startDate, })) - - return edges }, }, claimTags: { @@ -272,6 +292,16 @@ export const domainType = new GraphQLObjectType({ type: GraphQLBoolean, resolve: ({ hidden }) => hidden, }, + userHasPermission: { + description: + 'Value that determines if a user is affiliated with a domain, whether through organization affiliation, verified organization network affiliation, or through super admin status.', + type: GraphQLBoolean, + resolve: async ({ _id }, __, { auth: { checkDomainPermission } }) => { + return await checkDomainPermission({ + domainId: _id, + }) + }, + }, }), interfaces: [nodeInterface], description: 'Domain object containing information for a given domain.', diff --git a/api/src/domain/queries/__tests__/find-my-domains.test.js b/api/src/domain/queries/__tests__/find-my-domains.test.js index 8155adac20..3310d86982 100644 --- a/api/src/domain/queries/__tests__/find-my-domains.test.js +++ b/api/src/domain/queries/__tests__/find-my-domains.test.js @@ -1,19 +1,19 @@ -import {ensure, dbNameFromFile} from 'arango-tools' -import {graphql, GraphQLSchema, GraphQLError} from 'graphql' -import {toGlobalId} from 'graphql-relay' -import {setupI18n} from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { toGlobalId } from 'graphql-relay' +import { setupI18n } from '@lingui/core' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import {createQuerySchema} from '../../../query' -import {createMutationSchema} from '../../../mutation' -import {cleanseInput} from '../../../validators' -import {checkSuperAdmin, userRequired, verifiedRequired} from '../../../auth' -import {loadDomainConnectionsByUserId} from '../../loaders' -import {loadUserByKey} from '../../../user' +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import { cleanseInput } from '../../../validators' +import { checkDomainPermission, checkSuperAdmin, userRequired, verifiedRequired } from '../../../auth' +import { loadDomainConnectionsByUserId } from '../../loaders' +import { loadUserByKey } from '../../../user' import dbschema from '../../../../database.json' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env describe('given findMyDomainsQuery', () => { let query, drop, truncate, schema, collections, org, i18n, user @@ -39,7 +39,7 @@ describe('given findMyDomainsQuery', () => { let domainOne, domainTwo beforeAll(async () => { // Generate DB Items - ;({query, drop, truncate, collections} = await ensure({ + ;({ query, drop, truncate, collections } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -158,6 +158,11 @@ describe('given findMyDomainsQuery', () => { i18n, userKey: user._key, auth: { + checkDomainPermission: checkDomainPermission({ + i18n, + userKey: user._key, + query, + }), checkSuperAdmin: checkSuperAdmin({ i18n, userKey: user._key, @@ -179,7 +184,7 @@ describe('given findMyDomainsQuery', () => { query, userKey: user._key, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, }), }, }, @@ -219,9 +224,7 @@ describe('given findMyDomainsQuery', () => { }, } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully retrieved their domains.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key} successfully retrieved their domains.`]) }) }) }) @@ -230,8 +233,8 @@ describe('given findMyDomainsQuery', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -243,9 +246,7 @@ describe('given findMyDomainsQuery', () => { describe('given an error thrown during retrieving domains', () => { describe('user queries for their domains', () => { it('returns domains', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred')) + const mockedQuery = jest.fn().mockRejectedValue(new Error('Database error occurred')) const response = await graphql( schema, @@ -285,16 +286,14 @@ describe('given findMyDomainsQuery', () => { query: mockedQuery, userKey: 1, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }), }, }, ) - const error = [ - new GraphQLError(`Unable to query domain(s). Please try again.`), - ] + const error = [new GraphQLError(`Unable to query domain(s). Please try again.`)] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -309,8 +308,8 @@ describe('given findMyDomainsQuery', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -322,9 +321,7 @@ describe('given findMyDomainsQuery', () => { describe('given an error thrown during retrieving domains', () => { describe('user queries for their domains', () => { it('returns domains', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('Database error occurred')) + const mockedQuery = jest.fn().mockRejectedValue(new Error('Database error occurred')) const response = await graphql( schema, @@ -364,18 +361,14 @@ describe('given findMyDomainsQuery', () => { query: mockedQuery, userKey: 1, cleanseInput, - auth: {loginRequired: true}, + auth: { loginRequired: true }, i18n, }), }, }, ) - const error = [ - new GraphQLError( - "Impossible d'interroger le(s) domaine(s). Veuillez réessayer.", - ), - ] + const error = [new GraphQLError("Impossible d'interroger le(s) domaine(s). Veuillez réessayer.")] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ diff --git a/api/src/enums/domain-order-field.js b/api/src/enums/domain-order-field.js index 4abe5cfb26..98ec17b492 100644 --- a/api/src/enums/domain-order-field.js +++ b/api/src/enums/domain-order-field.js @@ -1,9 +1,13 @@ -import {GraphQLEnumType} from 'graphql' +import { GraphQLEnumType } from 'graphql' export const DomainOrderField = new GraphQLEnumType({ name: 'DomainOrderField', description: 'Properties by which domain connections can be ordered.', values: { + CERTIFICATES_STATUS: { + value: 'certificates-status', + description: 'Order domains by certificates status.', + }, CIPHERS_STATUS: { value: 'ciphers-status', description: 'Order domains by ciphers status.', diff --git a/api/src/enums/domain-tag-label.js b/api/src/enums/domain-tag-label.js index d50fe795ee..478070a893 100644 --- a/api/src/enums/domain-tag-label.js +++ b/api/src/enums/domain-tag-label.js @@ -13,13 +13,11 @@ export const DomainTagLabel = new GraphQLEnumType({ }, PROD: { value: 'PROD', - description: - 'Bilingual Label for tagging domains as a production environment.', + description: 'Bilingual Label for tagging domains as a production environment.', }, STAGING: { value: 'STAGING', - description: - 'English label for tagging domains as a staging environment.', + description: 'English label for tagging domains as a staging environment.', }, DEV: { value: 'DÉV', @@ -49,6 +47,18 @@ export const DomainTagLabel = new GraphQLEnumType({ value: 'archived', description: 'English label for tagging domains that are archived.', }, + NXDOMAIN: { + value: 'nxdomain', + description: 'Label for tagging domains that have an rcode status of NXDOMAIN.', + }, + BLOCKED: { + value: 'blocked', + description: 'Label for tagging domains that are possibly blocked by a firewall.', + }, + SCAN_PENDING: { + value: 'scan-pending', + description: 'Label for tagging domains that have a pending web scan.', + }, }, description: 'An enum used to assign and test user-generated domain tags', }) diff --git a/api/src/enums/index.js b/api/src/enums/index.js index f85ec5e9a9..0d36ba8e5f 100644 --- a/api/src/enums/index.js +++ b/api/src/enums/index.js @@ -11,6 +11,7 @@ export * from './domain-tag-label' export * from './filter-comparisons' export * from './guidance-tag-order-field' export * from './https-order-field' +export * from './invitation-roles' export * from './languages' export * from './log-order-field' export * from './order-direction' diff --git a/api/src/enums/invitation-roles.js b/api/src/enums/invitation-roles.js new file mode 100644 index 0000000000..e2ed9e0169 --- /dev/null +++ b/api/src/enums/invitation-roles.js @@ -0,0 +1,21 @@ +import { GraphQLEnumType } from 'graphql' + +export const InvitationRoleEnums = new GraphQLEnumType({ + name: 'InvitationRoleEnums', + values: { + USER: { + value: 'user', + description: 'A user who has been given access to view an organization.', + }, + ADMIN: { + value: 'admin', + description: + 'A user who has the same access as a user write account, but can define new user read/write accounts.', + }, + SUPER_ADMIN: { + value: 'super_admin', + description: 'A user who has the same access as an admin, but can define new admins.', + }, + }, + description: 'An enum used when inviting users to an organization to assign their role.', +}) diff --git a/api/src/enums/roles.js b/api/src/enums/roles.js index ae3a65a4a3..61080e6fbd 100644 --- a/api/src/enums/roles.js +++ b/api/src/enums/roles.js @@ -1,8 +1,12 @@ -import {GraphQLEnumType} from 'graphql' +import { GraphQLEnumType } from 'graphql' export const RoleEnums = new GraphQLEnumType({ name: 'RoleEnums', values: { + PENDING: { + value: 'pending', + description: 'A user who has requested an invite to an organization.', + }, USER: { value: 'user', description: 'A user who has been given access to view an organization.', @@ -12,10 +16,14 @@ export const RoleEnums = new GraphQLEnumType({ description: 'A user who has the same access as a user write account, but can define new user read/write accounts.', }, + OWNER: { + value: 'owner', + description: + 'A user who has the same access as an admin, but can define new admins, and delete the organization.', + }, SUPER_ADMIN: { value: 'super_admin', - description: - 'A user who has the same access as an admin, but can define new admins.', + description: 'A user who has the same access as an admin, but can define new admins.', }, }, description: 'An enum used to assign, and test users roles.', diff --git a/api/src/enums/user-action.js b/api/src/enums/user-action.js index 8c3819d9b1..e5160ffc85 100644 --- a/api/src/enums/user-action.js +++ b/api/src/enums/user-action.js @@ -23,6 +23,14 @@ export const UserActionEnums = new GraphQLEnumType({ value: 'remove', description: 'An affiliation between resources was deleted.', }, + SCAN: { + value: 'scan', + description: 'A scan was requested on a resource.', + }, + EXPORT: { + value: 'export', + description: 'A resource was exported.', + }, }, description: 'Describes actions performed by users to modify resources.', }) diff --git a/api/src/initialize-loaders.js b/api/src/initialize-loaders.js index 9f0118a2d0..adbcfbd082 100644 --- a/api/src/initialize-loaders.js +++ b/api/src/initialize-loaders.js @@ -45,13 +45,8 @@ import { loadAllOrganizationDomainStatuses, loadOrganizationDomainStatuses, } from './organization/loaders' -import { - loadMyTrackerByUserId, - loadUserByUserName, - loadUserByKey, - loadUserConnectionsByUserId, -} from './user/loaders' -import { loadWebConnectionsByDomainId, loadWebScansByWebId} from './web-scan/loaders' +import { loadMyTrackerByUserId, loadUserByUserName, loadUserByKey, loadUserConnectionsByUserId } from './user/loaders' +import { loadWebConnectionsByDomainId, loadWebScansByWebId } from './web-scan/loaders' import { loadVerifiedDomainsById, loadVerifiedDomainByKey, @@ -65,18 +60,9 @@ import { loadVerifiedOrgConnections, } from './verified-organizations/loaders' import { loadChartSummaryByKey } from './summaries/loaders' -import {loadDnsConnectionsByDomainId} from "./dns-scan" +import { loadDnsConnectionsByDomainId } from './dns-scan' -export function initializeLoaders({ - query, - db, - userKey, - i18n, - language, - cleanseInput, - loginRequiredBool, - moment, -}) { +export function initializeLoaders({ query, db, userKey, i18n, language, cleanseInput, loginRequiredBool, moment }) { return { loadChartSummaryByKey: loadChartSummaryByKey({ query, userKey, i18n }), loadAggregateGuidanceTagByTagId: loadAggregateGuidanceTagByTagId({ @@ -85,14 +71,13 @@ export function initializeLoaders({ i18n, language, }), - loadAggregateGuidanceTagConnectionsByTagId: - loadAggregateGuidanceTagConnectionsByTagId({ - query, - userKey, - i18n, - cleanseInput, - language, - }), + loadAggregateGuidanceTagConnectionsByTagId: loadAggregateGuidanceTagConnectionsByTagId({ + query, + userKey, + i18n, + cleanseInput, + language, + }), loadAuditLogsByOrgId: loadAuditLogsByOrgId({ query, userKey, @@ -123,12 +108,11 @@ export function initializeLoaders({ i18n, }), }), - loadDmarcSummaryEdgeByDomainIdAndPeriod: - loadDmarcSummaryEdgeByDomainIdAndPeriod({ - query, - userKey, - i18n, - }), + loadDmarcSummaryEdgeByDomainIdAndPeriod: loadDmarcSummaryEdgeByDomainIdAndPeriod({ + query, + userKey, + i18n, + }), loadDmarcSummaryByKey: loadDmarcSummaryByKey({ query, userKey, i18n }), loadFullPassConnectionsBySumId: loadFullPassConnectionsBySumId({ query, @@ -193,42 +177,39 @@ export function initializeLoaders({ i18n, language, }), - loadDkimGuidanceTagConnectionsByTagId: - loadDkimGuidanceTagConnectionsByTagId({ - query, - userKey, - cleanseInput, - i18n, - language, - }), + loadDkimGuidanceTagConnectionsByTagId: loadDkimGuidanceTagConnectionsByTagId({ + query, + userKey, + cleanseInput, + i18n, + language, + }), loadDmarcGuidanceTagByTagId: loadDmarcGuidanceTagByTagId({ query, userKey, i18n, language, }), - loadDmarcGuidanceTagConnectionsByTagId: - loadDmarcGuidanceTagConnectionsByTagId({ - query, - userKey, - cleanseInput, - i18n, - language, - }), + loadDmarcGuidanceTagConnectionsByTagId: loadDmarcGuidanceTagConnectionsByTagId({ + query, + userKey, + cleanseInput, + i18n, + language, + }), loadHttpsGuidanceTagByTagId: loadHttpsGuidanceTagByTagId({ query, userKey, i18n, language, }), - loadHttpsGuidanceTagConnectionsByTagId: - loadHttpsGuidanceTagConnectionsByTagId({ - query, - userKey, - cleanseInput, - i18n, - language, - }), + loadHttpsGuidanceTagConnectionsByTagId: loadHttpsGuidanceTagConnectionsByTagId({ + query, + userKey, + cleanseInput, + i18n, + language, + }), loadSpfGuidanceTagByTagId: loadSpfGuidanceTagByTagId({ query, userKey, diff --git a/api/src/locale/en/messages.js b/api/src/locale/en/messages.js index 422ca019c8..9fd71f2be0 100644 --- a/api/src/locale/en/messages.js +++ b/api/src/locale/en/messages.js @@ -1 +1 @@ -/*eslint-disable*/module.exports={messages:{"Authentication error. Please sign in.":"Authentication error. Please sign in.","Cannot query affiliations on organization without admin permission or higher.":"Cannot query affiliations on organization without admin permission or higher.","Email already in use.":"Email already in use.","If an account with this username is found, a password reset link will be found in your inbox.":"If an account with this username is found, a password reset link will be found in your inbox.","If an account with this username is found, an email verification link will be found in your inbox.":"If an account with this username is found, an email verification link will be found in your inbox.","Incorrect TFA code. Please sign in again.":"Incorrect TFA code. Please sign in again.","Incorrect token value. Please request a new email.":"Incorrect token value. Please request a new email.","Incorrect username or password. Please try again.":"Incorrect username or password. Please try again.","Invalid token, please sign in.":"Invalid token, please sign in.","New passwords do not match.":"New passwords do not match.","No organization with the provided slug could be found.":"No organization with the provided slug could be found.","No verified domain with the provided domain could be found.":"No verified domain with the provided domain could be found.","No verified organization with the provided slug could be found.":"No verified organization with the provided slug could be found.","Organization has already been verified.":"Organization has already been verified.","Organization name already in use, please choose another and try again.":"Organization name already in use, please choose another and try again.","Organization name already in use. Please try again with a different name.":"Organization name already in use. Please try again with a different name.","Ownership check error. Unable to request domain information.":"Ownership check error. Unable to request domain information.","Passing both `first` and `last` to paginate the `Affiliation` connection is not supported.":"Passing both `first` and `last` to paginate the `Affiliation` connection is not supported.","Passing both `first` and `last` to paginate the `DKIMResults` connection is not supported.":"Passing both `first` and `last` to paginate the `DKIMResults` connection is not supported.","Passing both `first` and `last` to paginate the `DKIM` connection is not supported.":"Passing both `first` and `last` to paginate the `DKIM` connection is not supported.","Passing both `first` and `last` to paginate the `DMARC` connection is not supported.":"Passing both `first` and `last` to paginate the `DMARC` connection is not supported.","Passing both `first` and `last` to paginate the `DkimFailureTable` connection is not supported.":"Passing both `first` and `last` to paginate the `DkimFailureTable` connection is not supported.","Passing both `first` and `last` to paginate the `DmarcFailureTable` connection is not supported.":"Passing both `first` and `last` to paginate the `DmarcFailureTable` connection is not supported.","Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported.":"Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported.","Passing both `first` and `last` to paginate the `Domain` connection is not supported.":"Passing both `first` and `last` to paginate the `Domain` connection is not supported.","Passing both `first` and `last` to paginate the `FullPassTable` connection is not supported.":"Passing both `first` and `last` to paginate the `FullPassTable` connection is not supported.","Passing both `first` and `last` to paginate the `GuidanceTag` connection is not supported.":"Passing both `first` and `last` to paginate the `GuidanceTag` connection is not supported.","Passing both `first` and `last` to paginate the `HTTPS` connection is not supported.":"Passing both `first` and `last` to paginate the `HTTPS` connection is not supported.","Passing both `first` and `last` to paginate the `Organization` connection is not supported.":"Passing both `first` and `last` to paginate the `Organization` connection is not supported.","Passing both `first` and `last` to paginate the `SPF` connection is not supported.":"Passing both `first` and `last` to paginate the `SPF` connection is not supported.","Passing both `first` and `last` to paginate the `SSL` connection is not supported.":"Passing both `first` and `last` to paginate the `SSL` connection is not supported.","Passing both `first` and `last` to paginate the `SpfFailureTable` connection is not supported.":"Passing both `first` and `last` to paginate the `SpfFailureTable` connection is not supported.","Passing both `first` and `last` to paginate the `User` connection is not supported.":"Passing both `first` and `last` to paginate the `User` connection is not supported.","Passing both `first` and `last` to paginate the `VerifiedDomain` connection is not supported.":"Passing both `first` and `last` to paginate the `VerifiedDomain` connection is not supported.","Passing both `first` and `last` to paginate the `VerifiedOrganization` connection is not supported.":"Passing both `first` and `last` to paginate the `VerifiedOrganization` connection is not supported.","Password does not meet requirements.":"Password does not meet requirements.","Password was successfully reset.":"Password was successfully reset.","Password was successfully updated.":"Password was successfully updated.","Passwords do not match.":"Passwords do not match.","Permission Denied: Could not retrieve specified organization.":"Permission Denied: Could not retrieve specified organization.","Permission Denied: Multi-factor authentication is required for admin accounts":"Permission Denied: Multi-factor authentication is required for admin accounts","Permission Denied: Please contact org owner to transfer ownership.":"Permission Denied: Please contact org owner to transfer ownership.","Permission Denied: Please contact organization admin for help with removing domain.":"Permission Denied: Please contact organization admin for help with removing domain.","Permission Denied: Please contact organization admin for help with removing organization.":"Permission Denied: Please contact organization admin for help with removing organization.","Permission Denied: Please contact organization admin for help with removing users.":"Permission Denied: Please contact organization admin for help with removing users.","Permission Denied: Please contact organization admin for help with updating organization.":"Permission Denied: Please contact organization admin for help with updating organization.","Permission Denied: Please contact organization admin for help with updating user roles.":"Permission Denied: Please contact organization admin for help with updating user roles.","Permission Denied: Please contact organization admin for help with user invitations.":"Permission Denied: Please contact organization admin for help with user invitations.","Permission Denied: Please contact organization admin for help with user role changes.":"Permission Denied: Please contact organization admin for help with user role changes.","Permission Denied: Please contact organization user for help with creating domain.":"Permission Denied: Please contact organization user for help with creating domain.","Permission Denied: Please contact organization user for help with retrieving this domain.":"Permission Denied: Please contact organization user for help with retrieving this domain.","Permission Denied: Please contact organization user for help with scanning this domain.":"Permission Denied: Please contact organization user for help with scanning this domain.","Permission Denied: Please contact organization user for help with updating this domain.":"Permission Denied: Please contact organization user for help with updating this domain.","Permission Denied: Please contact super admin for help with removing domain.":"Permission Denied: Please contact super admin for help with removing domain.","Permission Denied: Please contact super admin for help with removing organization.":"Permission Denied: Please contact super admin for help with removing organization.","Permission Denied: Please contact super admin for help with verifying this organization.":"Permission Denied: Please contact super admin for help with verifying this organization.","Permission check error. Unable to request domain information.":"Permission check error. Unable to request domain information.","Permission error, not an admin for this user.":"Permission error, not an admin for this user.","Permission error: Unable to close other user's account.":"Permission error: Unable to close other user's account.","Permissions error. You do not have sufficient permissions to access this data.":"Permissions error. You do not have sufficient permissions to access this data.","Phone number has been successfully removed.":"Phone number has been successfully removed.","Phone number has been successfully set, you will receive a verification text message shortly.":"Phone number has been successfully set, you will receive a verification text message shortly.","Profile successfully updated.":"Profile successfully updated.","Requesting `{amount}` records on the `Affiliation` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `Affiliation` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `DkimFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `DkimFailureTable` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `DmarcFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `DmarcFailureTable` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `DmarcSummaries` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `DmarcSummaries` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `Domain` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `Domain` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `FullPassTable` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `FullPassTable` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `GuidanceTag` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `GuidanceTag` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `Organization` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `Organization` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `SpfFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `SpfFailureTable` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `User` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `User` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `VerifiedDomain` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `VerifiedDomain` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `VerifiedOrganization` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `VerifiedOrganization` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `DKIMResults` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `DKIMResults` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `DKIM` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `DKIM` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `DMARC` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `DMARC` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `HTTPS` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `HTTPS` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `SPF` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `SPF` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `SSL` connection exceeds the `",["argSet"],"` limit of 100 records."],"Successfully closed account.":"Successfully closed account.","Successfully dispatched one time scan.":"Successfully dispatched one time scan.","Successfully email verified account, and set TFA send method to email.":"Successfully email verified account, and set TFA send method to email.","Successfully invited user to organization, and sent notification email.":"Successfully invited user to organization, and sent notification email.","Successfully left organization: {0}":["Successfully left organization: ",["0"]],"Successfully removed domain: {0} from {1}.":["Successfully removed domain: ",["0"]," from ",["1"],"."],"Successfully removed organization: {0}.":["Successfully removed organization: ",["0"],"."],"Successfully removed user from organization.":"Successfully removed user from organization.","Successfully sent invitation to service, and organization email.":"Successfully sent invitation to service, and organization email.","Successfully signed out.":"Successfully signed out.","Successfully transferred org: {0} ownership to user: {1}":["Successfully transferred org: ",["0"]," ownership to user: ",["1"]],"Successfully verified organization: {0}.":["Successfully verified organization: ",["0"],"."],"Successfully verified phone number, and set TFA send method to text.":"Successfully verified phone number, and set TFA send method to text.","Token value incorrect, please sign in again.":"Token value incorrect, please sign in again.","Too many failed login attempts, please reset your password, and try again.":"Too many failed login attempts, please reset your password, and try again.","Two factor code is incorrect. Please try again.":"Two factor code is incorrect. Please try again.","Two factor code length is incorrect. Please try again.":"Two factor code length is incorrect. Please try again.","Unable leave organization. Please try again.":"Unable leave organization. Please try again.","Unable to authenticate. Please try again.":"Unable to authenticate. Please try again.","Unable to check permission. Please try again.":"Unable to check permission. Please try again.","Unable to close account of an undefined user.":"Unable to close account of an undefined user.","Unable to close account. Please try again.":"Unable to close account. Please try again.","Unable to create domain in unknown organization.":"Unable to create domain in unknown organization.","Unable to create domain, organization has already claimed it.":"Unable to create domain, organization has already claimed it.","Unable to create domain. Please try again.":"Unable to create domain. Please try again.","Unable to create organization. Please try again.":"Unable to create organization. Please try again.","Unable to dispatch one time scan. Please try again.":"Unable to dispatch one time scan. Please try again.","Unable to find Aggregate guidance tag(s). Please try again.":"Unable to find Aggregate guidance tag(s). Please try again.","Unable to find DKIM guidance tag(s). Please try again.":"Unable to find DKIM guidance tag(s). Please try again.","Unable to find DKIM result(s). Please try again.":"Unable to find DKIM result(s). Please try again.","Unable to find DKIM scan(s). Please try again.":"Unable to find DKIM scan(s). Please try again.","Unable to find DMARC guidance tag(s). Please try again.":"Unable to find DMARC guidance tag(s). Please try again.","Unable to find DMARC scan(s). Please try again.":"Unable to find DMARC scan(s). Please try again.","Unable to find DMARC summary data. Please try again.":"Unable to find DMARC summary data. Please try again.","Unable to find DNS scan(s). Please try again.":"Unable to find DNS scan(s). Please try again.","Unable to find HTTPS guidance tag(s). Please try again.":"Unable to find HTTPS guidance tag(s). Please try again.","Unable to find HTTPS scan(s). Please try again.":"Unable to find HTTPS scan(s). Please try again.","Unable to find SPF guidance tag(s). Please try again.":"Unable to find SPF guidance tag(s). Please try again.","Unable to find SPF scan(s). Please try again.":"Unable to find SPF scan(s). Please try again.","Unable to find SSL guidance tag(s). Please try again.":"Unable to find SSL guidance tag(s). Please try again.","Unable to find SSL scan(s). Please try again.":"Unable to find SSL scan(s). Please try again.","Unable to find the requested domain.":"Unable to find the requested domain.","Unable to find user affiliation(s). Please try again.":"Unable to find user affiliation(s). Please try again.","Unable to find verified organization(s). Please try again.":"Unable to find verified organization(s). Please try again.","Unable to invite user to unknown organization.":"Unable to invite user to unknown organization.","Unable to invite user. Please try again.":"Unable to invite user. Please try again.","Unable to invite yourself to an org.":"Unable to invite yourself to an org.","Unable to leave organization. Please try again.":"Unable to leave organization. Please try again.","Unable to leave undefined organization.":"Unable to leave undefined organization.","Unable to load Aggregate guidance tag(s). Please try again.":"Unable to load Aggregate guidance tag(s). Please try again.","Unable to load DKIM failure data. Please try again.":"Unable to load DKIM failure data. Please try again.","Unable to load DKIM guidance tag(s). Please try again.":"Unable to load DKIM guidance tag(s). Please try again.","Unable to load DKIM result(s). Please try again.":"Unable to load DKIM result(s). Please try again.","Unable to load DKIM scan(s). Please try again.":"Unable to load DKIM scan(s). Please try again.","Unable to load DMARC failure data. Please try again.":"Unable to load DMARC failure data. Please try again.","Unable to load DMARC guidance tag(s). Please try again.":"Unable to load DMARC guidance tag(s). Please try again.","Unable to load DMARC phase summary. Please try again.":"Unable to load DMARC phase summary. Please try again.","Unable to load DMARC scan(s). Please try again.":"Unable to load DMARC scan(s). Please try again.","Unable to load DMARC summary data. Please try again.":"Unable to load DMARC summary data. Please try again.","Unable to load DNS scan(s). Please try again.":"Unable to load DNS scan(s). Please try again.","Unable to load HTTPS guidance tag(s). Please try again.":"Unable to load HTTPS guidance tag(s). Please try again.","Unable to load HTTPS scan(s). Please try again.":"Unable to load HTTPS scan(s). Please try again.","Unable to load HTTPS summary. Please try again.":"Unable to load HTTPS summary. Please try again.","Unable to load SPF failure data. Please try again.":"Unable to load SPF failure data. Please try again.","Unable to load SPF guidance tag(s). Please try again.":"Unable to load SPF guidance tag(s). Please try again.","Unable to load SPF scan(s). Please try again.":"Unable to load SPF scan(s). Please try again.","Unable to load SSL guidance tag(s). Please try again.":"Unable to load SSL guidance tag(s). Please try again.","Unable to load SSL scan(s). Please try again.":"Unable to load SSL scan(s). Please try again.","Unable to load affiliation information. Please try again.":"Unable to load affiliation information. Please try again.","Unable to load affiliation(s). Please try again.":"Unable to load affiliation(s). Please try again.","Unable to load domain(s). Please try again.":"Unable to load domain(s). Please try again.","Unable to load domain. Please try again.":"Unable to load domain. Please try again.","Unable to load full pass data. Please try again.":"Unable to load full pass data. Please try again.","Unable to load mail summary. Please try again.":"Unable to load mail summary. Please try again.","Unable to load organization domain statuses. Please try again.":"Unable to load organization domain statuses. Please try again.","Unable to load organization(s). Please try again.":"Unable to load organization(s). Please try again.","Unable to load owner information. Please try again.":"Unable to load owner information. Please try again.","Unable to load summary. Please try again.":"Unable to load summary. Please try again.","Unable to load tags(s). Please try again.":"Unable to load tags(s). Please try again.","Unable to load user(s). Please try again.":"Unable to load user(s). Please try again.","Unable to load verified domain(s). Please try again.":"Unable to load verified domain(s). Please try again.","Unable to load verified organization(s). Please try again.":"Unable to load verified organization(s). Please try again.","Unable to load web scan(s). Please try again.":"Unable to load web scan(s). Please try again.","Unable to load web summary. Please try again.":"Unable to load web summary. Please try again.","Unable to query affiliation(s). Please try again.":"Unable to query affiliation(s). Please try again.","Unable to query domain(s). Please try again.":"Unable to query domain(s). Please try again.","Unable to query user(s). Please try again.":"Unable to query user(s). Please try again.","Unable to refresh tokens, please sign in.":"Unable to refresh tokens, please sign in.","Unable to remove a user that already does not belong to this organization.":"Unable to remove a user that already does not belong to this organization.","Unable to remove domain from unknown organization.":"Unable to remove domain from unknown organization.","Unable to remove domain. Please try again.":"Unable to remove domain. Please try again.","Unable to remove organization. Please try again.":"Unable to remove organization. Please try again.","Unable to remove phone number. Please try again.":"Unable to remove phone number. Please try again.","Unable to remove unknown domain.":"Unable to remove unknown domain.","Unable to remove unknown organization.":"Unable to remove unknown organization.","Unable to remove unknown user from organization.":"Unable to remove unknown user from organization.","Unable to remove user from organization.":"Unable to remove user from organization.","Unable to remove user from this organization. Please try again.":"Unable to remove user from this organization. Please try again.","Unable to remove user from unknown organization.":"Unable to remove user from unknown organization.","Unable to request a one time scan on an unknown domain.":"Unable to request a one time scan on an unknown domain.","Unable to reset password. Please request a new email.":"Unable to reset password. Please request a new email.","Unable to reset password. Please try again.":"Unable to reset password. Please try again.","Unable to retrieve DMARC report information for: {domain}":["Unable to retrieve DMARC report information for: ",["domain"]],"Unable to select DMARC report(s) for this period and year.":"Unable to select DMARC report(s) for this period and year.","Unable to send authentication email. Please try again.":"Unable to send authentication email. Please try again.","Unable to send authentication text message. Please try again.":"Unable to send authentication text message. Please try again.","Unable to send org invite email. Please try again.":"Unable to send org invite email. Please try again.","Unable to send password reset email. Please try again.":"Unable to send password reset email. Please try again.","Unable to send two factor authentication message. Please try again.":"Unable to send two factor authentication message. Please try again.","Unable to send verification email. Please try again.":"Unable to send verification email. Please try again.","Unable to set phone number, please try again.":"Unable to set phone number, please try again.","Unable to sign in, please try again.":"Unable to sign in, please try again.","Unable to sign up, please contact org admin for a new invite.":"Unable to sign up, please contact org admin for a new invite.","Unable to sign up. Please try again.":"Unable to sign up. Please try again.","Unable to transfer organization ownership. Please try again.":"Unable to transfer organization ownership. Please try again.","Unable to transfer ownership of a verified organization.":"Unable to transfer ownership of a verified organization.","Unable to transfer ownership of an org to an undefined user.":"Unable to transfer ownership of an org to an undefined user.","Unable to transfer ownership of undefined organization.":"Unable to transfer ownership of undefined organization.","Unable to transfer ownership to a user outside the org. Please invite the user and try again.":"Unable to transfer ownership to a user outside the org. Please invite the user and try again.","Unable to two factor authenticate. Please try again.":"Unable to two factor authenticate. Please try again.","Unable to update domain in an unknown org.":"Unable to update domain in an unknown org.","Unable to update domain that does not belong to the given organization.":"Unable to update domain that does not belong to the given organization.","Unable to update domain. Please try again.":"Unable to update domain. Please try again.","Unable to update organization. Please try again.":"Unable to update organization. Please try again.","Unable to update password, current password does not match. Please try again.":"Unable to update password, current password does not match. Please try again.","Unable to update password, new passwords do not match. Please try again.":"Unable to update password, new passwords do not match. Please try again.","Unable to update password, passwords do not match requirements. Please try again.":"Unable to update password, passwords do not match requirements. Please try again.","Unable to update password. Please try again.":"Unable to update password. Please try again.","Unable to update profile. Please try again.":"Unable to update profile. Please try again.","Unable to update role: organization unknown.":"Unable to update role: organization unknown.","Unable to update role: user does not belong to organization.":"Unable to update role: user does not belong to organization.","Unable to update role: user unknown.":"Unable to update role: user unknown.","Unable to update unknown domain.":"Unable to update unknown domain.","Unable to update unknown organization.":"Unable to update unknown organization.","Unable to update user's role. Please try again.":"Unable to update user's role. Please try again.","Unable to update your own role.":"Unable to update your own role.","Unable to verify account. Please request a new email.":"Unable to verify account. Please request a new email.","Unable to verify account. Please try again.":"Unable to verify account. Please try again.","Unable to verify if user is a super admin, please try again.":"Unable to verify if user is a super admin, please try again.","Unable to verify if user is an admin, please try again.":"Unable to verify if user is an admin, please try again.","Unable to verify organization. Please try again.":"Unable to verify organization. Please try again.","Unable to verify unknown organization.":"Unable to verify unknown organization.","User could not be queried.":"User could not be queried.","User role was updated successfully.":"User role was updated successfully.","Username not available, please try another.":"Username not available, please try another.","Verification error. Please activate multi-factor authentication to access content.":"Verification error. Please activate multi-factor authentication to access content.","Verification error. Please verify your account via email to access content.":"Verification error. Please verify your account via email to access content.","You must provide a `first` or `last` value to properly paginate the `Affiliation` connection.":"You must provide a `first` or `last` value to properly paginate the `Affiliation` connection.","You must provide a `first` or `last` value to properly paginate the `DKIMResults` connection.":"You must provide a `first` or `last` value to properly paginate the `DKIMResults` connection.","You must provide a `first` or `last` value to properly paginate the `DKIM` connection.":"You must provide a `first` or `last` value to properly paginate the `DKIM` connection.","You must provide a `first` or `last` value to properly paginate the `DMARC` connection.":"You must provide a `first` or `last` value to properly paginate the `DMARC` connection.","You must provide a `first` or `last` value to properly paginate the `DkimFailureTable` connection.":"You must provide a `first` or `last` value to properly paginate the `DkimFailureTable` connection.","You must provide a `first` or `last` value to properly paginate the `DmarcFailureTable` connection.":"You must provide a `first` or `last` value to properly paginate the `DmarcFailureTable` connection.","You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection.":"You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection.","You must provide a `first` or `last` value to properly paginate the `Domain` connection.":"You must provide a `first` or `last` value to properly paginate the `Domain` connection.","You must provide a `first` or `last` value to properly paginate the `FullPassTable` connection.":"You must provide a `first` or `last` value to properly paginate the `FullPassTable` connection.","You must provide a `first` or `last` value to properly paginate the `GuidanceTag` connection.":"You must provide a `first` or `last` value to properly paginate the `GuidanceTag` connection.","You must provide a `first` or `last` value to properly paginate the `HTTPS` connection.":"You must provide a `first` or `last` value to properly paginate the `HTTPS` connection.","You must provide a `first` or `last` value to properly paginate the `Organization` connection.":"You must provide a `first` or `last` value to properly paginate the `Organization` connection.","You must provide a `first` or `last` value to properly paginate the `SPF` connection.":"You must provide a `first` or `last` value to properly paginate the `SPF` connection.","You must provide a `first` or `last` value to properly paginate the `SSL` connection.":"You must provide a `first` or `last` value to properly paginate the `SSL` connection.","You must provide a `first` or `last` value to properly paginate the `SpfFailureTable` connection.":"You must provide a `first` or `last` value to properly paginate the `SpfFailureTable` connection.","You must provide a `first` or `last` value to properly paginate the `User` connection.":"You must provide a `first` or `last` value to properly paginate the `User` connection.","You must provide a `first` or `last` value to properly paginate the `VerifiedDomain` connection.":"You must provide a `first` or `last` value to properly paginate the `VerifiedDomain` connection.","You must provide a `first` or `last` value to properly paginate the `VerifiedOrganization` connection.":"You must provide a `first` or `last` value to properly paginate the `VerifiedOrganization` connection.","You must provide a `limit` value in the range of 1-100 to properly paginate the `DNS` connection.":"You must provide a `limit` value in the range of 1-100 to properly paginate the `DNS` connection.","You must provide a `limit` value in the range of 1-100 to properly paginate the `web` connection.":"You must provide a `limit` value in the range of 1-100 to properly paginate the `web` connection.","You must provide a `limit` value to properly paginate the `DNS` connection.":"You must provide a `limit` value to properly paginate the `DNS` connection.","You must provide a `limit` value to properly paginate the `web` connection.":"You must provide a `limit` value to properly paginate the `web` connection.","You must provide a `period` value to access the `DmarcSummaries` connection.":"You must provide a `period` value to access the `DmarcSummaries` connection.","You must provide a `year` value to access the `DmarcSummaries` connection.":"You must provide a `year` value to access the `DmarcSummaries` connection.","You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `DNS` connection.":"You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `DNS` connection.","You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `web` connection.":"You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `web` connection.","`{argSet}` must be of type `number` not `{typeSet}`.":["`",["argSet"],"` must be of type `number` not `",["typeSet"],"`."],"`{argSet}` on the `Affiliation` connection cannot be less than zero.":["`",["argSet"],"` on the `Affiliation` connection cannot be less than zero."],"`{argSet}` on the `DKIMResults` connection cannot be less than zero.":["`",["argSet"],"` on the `DKIMResults` connection cannot be less than zero."],"`{argSet}` on the `DKIM` connection cannot be less than zero.":["`",["argSet"],"` on the `DKIM` connection cannot be less than zero."],"`{argSet}` on the `DMARC` connection cannot be less than zero.":["`",["argSet"],"` on the `DMARC` connection cannot be less than zero."],"`{argSet}` on the `DkimFailureTable` connection cannot be less than zero.":["`",["argSet"],"` on the `DkimFailureTable` connection cannot be less than zero."],"`{argSet}` on the `DmarcFailureTable` connection cannot be less than zero.":["`",["argSet"],"` on the `DmarcFailureTable` connection cannot be less than zero."],"`{argSet}` on the `DmarcSummaries` connection cannot be less than zero.":["`",["argSet"],"` on the `DmarcSummaries` connection cannot be less than zero."],"`{argSet}` on the `Domain` connection cannot be less than zero.":["`",["argSet"],"` on the `Domain` connection cannot be less than zero."],"`{argSet}` on the `FullPassTable` connection cannot be less than zero.":["`",["argSet"],"` on the `FullPassTable` connection cannot be less than zero."],"`{argSet}` on the `GuidanceTag` connection cannot be less than zero.":["`",["argSet"],"` on the `GuidanceTag` connection cannot be less than zero."],"`{argSet}` on the `HTTPS` connection cannot be less than zero.":["`",["argSet"],"` on the `HTTPS` connection cannot be less than zero."],"`{argSet}` on the `Organization` connection cannot be less than zero.":["`",["argSet"],"` on the `Organization` connection cannot be less than zero."],"`{argSet}` on the `SPF` connection cannot be less than zero.":["`",["argSet"],"` on the `SPF` connection cannot be less than zero."],"`{argSet}` on the `SSL` connection cannot be less than zero.":["`",["argSet"],"` on the `SSL` connection cannot be less than zero."],"`{argSet}` on the `SpfFailureTable` connection cannot be less than zero.":["`",["argSet"],"` on the `SpfFailureTable` connection cannot be less than zero."],"`{argSet}` on the `User` connection cannot be less than zero.":["`",["argSet"],"` on the `User` connection cannot be less than zero."],"`{argSet}` on the `VerifiedDomain` connection cannot be less than zero.":["`",["argSet"],"` on the `VerifiedDomain` connection cannot be less than zero."],"`{argSet}` on the `VerifiedOrganization` connection cannot be less than zero.":["`",["argSet"],"` on the `VerifiedOrganization` connection cannot be less than zero."]}}; \ No newline at end of file +/*eslint-disable*/module.exports={messages:{"Authentication error. Please sign in.":"Authentication error. Please sign in.","Cannot query affiliations on organization without admin permission or higher.":"Cannot query affiliations on organization without admin permission or higher.","Cannot query audit logs on organization without admin permission or higher.":"Cannot query audit logs on organization without admin permission or higher.","Cannot query dns scan results without permission.":"Cannot query dns scan results without permission.","Cannot query domain selectors without permission.":"Cannot query domain selectors without permission.","Cannot query web scan results without permission.":"Cannot query web scan results without permission.","Email already in use.":"Email already in use.","Error while requesting scan. Please try again.":"Error while requesting scan. Please try again.","If an account with this username is found, a password reset link will be found in your inbox.":"If an account with this username is found, a password reset link will be found in your inbox.","If an account with this username is found, an email verification link will be found in your inbox.":"If an account with this username is found, an email verification link will be found in your inbox.","Incorrect TFA code. Please sign in again.":"Incorrect TFA code. Please sign in again.","Incorrect token value. Please request a new email.":"Incorrect token value. Please request a new email.","Incorrect username or password. Please try again.":"Incorrect username or password. Please try again.","Invalid token, please sign in.":"Invalid token, please sign in.","New passwords do not match.":"New passwords do not match.","No organization with the provided slug could be found.":"No organization with the provided slug could be found.","No verified domain with the provided domain could be found.":"No verified domain with the provided domain could be found.","No verified organization with the provided slug could be found.":"No verified organization with the provided slug could be found.","Organization has already been verified.":"Organization has already been verified.","Organization name already in use, please choose another and try again.":"Organization name already in use, please choose another and try again.","Organization name already in use. Please try again with a different name.":"Organization name already in use. Please try again with a different name.","Ownership check error. Unable to request domain information.":"Ownership check error. Unable to request domain information.","Passing both `first` and `last` to paginate the `Affiliation` connection is not supported.":"Passing both `first` and `last` to paginate the `Affiliation` connection is not supported.","Passing both `first` and `last` to paginate the `DKIMResults` connection is not supported.":"Passing both `first` and `last` to paginate the `DKIMResults` connection is not supported.","Passing both `first` and `last` to paginate the `DKIM` connection is not supported.":"Passing both `first` and `last` to paginate the `DKIM` connection is not supported.","Passing both `first` and `last` to paginate the `DMARC` connection is not supported.":"Passing both `first` and `last` to paginate the `DMARC` connection is not supported.","Passing both `first` and `last` to paginate the `DkimFailureTable` connection is not supported.":"Passing both `first` and `last` to paginate the `DkimFailureTable` connection is not supported.","Passing both `first` and `last` to paginate the `DmarcFailureTable` connection is not supported.":"Passing both `first` and `last` to paginate the `DmarcFailureTable` connection is not supported.","Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported.":"Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported.","Passing both `first` and `last` to paginate the `Domain` connection is not supported.":"Passing both `first` and `last` to paginate the `Domain` connection is not supported.","Passing both `first` and `last` to paginate the `FullPassTable` connection is not supported.":"Passing both `first` and `last` to paginate the `FullPassTable` connection is not supported.","Passing both `first` and `last` to paginate the `GuidanceTag` connection is not supported.":"Passing both `first` and `last` to paginate the `GuidanceTag` connection is not supported.","Passing both `first` and `last` to paginate the `HTTPS` connection is not supported.":"Passing both `first` and `last` to paginate the `HTTPS` connection is not supported.","Passing both `first` and `last` to paginate the `Log` connection is not supported.":"Passing both `first` and `last` to paginate the `Log` connection is not supported.","Passing both `first` and `last` to paginate the `Organization` connection is not supported.":"Passing both `first` and `last` to paginate the `Organization` connection is not supported.","Passing both `first` and `last` to paginate the `SPF` connection is not supported.":"Passing both `first` and `last` to paginate the `SPF` connection is not supported.","Passing both `first` and `last` to paginate the `SSL` connection is not supported.":"Passing both `first` and `last` to paginate the `SSL` connection is not supported.","Passing both `first` and `last` to paginate the `SpfFailureTable` connection is not supported.":"Passing both `first` and `last` to paginate the `SpfFailureTable` connection is not supported.","Passing both `first` and `last` to paginate the `User` connection is not supported.":"Passing both `first` and `last` to paginate the `User` connection is not supported.","Passing both `first` and `last` to paginate the `VerifiedDomain` connection is not supported.":"Passing both `first` and `last` to paginate the `VerifiedDomain` connection is not supported.","Passing both `first` and `last` to paginate the `VerifiedOrganization` connection is not supported.":"Passing both `first` and `last` to paginate the `VerifiedOrganization` connection is not supported.","Password does not meet requirements.":"Password does not meet requirements.","Password was successfully reset.":"Password was successfully reset.","Password was successfully updated.":"Password was successfully updated.","Passwords do not match.":"Passwords do not match.","Permission Denied: Could not retrieve specified organization.":"Permission Denied: Could not retrieve specified organization.","Permission Denied: Multi-factor authentication is required for admin accounts":"Permission Denied: Multi-factor authentication is required for admin accounts","Permission Denied: Please contact org owner to transfer ownership.":"Permission Denied: Please contact org owner to transfer ownership.","Permission Denied: Please contact organization admin for help with archiving domains.":"Permission Denied: Please contact organization admin for help with archiving domains.","Permission Denied: Please contact organization admin for help with removing domain.":"Permission Denied: Please contact organization admin for help with removing domain.","Permission Denied: Please contact organization admin for help with removing domains.":"Permission Denied: Please contact organization admin for help with removing domains.","Permission Denied: Please contact organization admin for help with removing organization.":"Permission Denied: Please contact organization admin for help with removing organization.","Permission Denied: Please contact organization admin for help with removing users.":"Permission Denied: Please contact organization admin for help with removing users.","Permission Denied: Please contact organization admin for help with updating organization.":"Permission Denied: Please contact organization admin for help with updating organization.","Permission Denied: Please contact organization admin for help with updating user roles.":"Permission Denied: Please contact organization admin for help with updating user roles.","Permission Denied: Please contact organization admin for help with user invitations.":"Permission Denied: Please contact organization admin for help with user invitations.","Permission Denied: Please contact organization admin for help with user role changes.":"Permission Denied: Please contact organization admin for help with user role changes.","Permission Denied: Please contact organization user for help with creating domain.":"Permission Denied: Please contact organization user for help with creating domain.","Permission Denied: Please contact organization user for help with creating domains.":"Permission Denied: Please contact organization user for help with creating domains.","Permission Denied: Please contact organization user for help with retrieving this domain.":"Permission Denied: Please contact organization user for help with retrieving this domain.","Permission Denied: Please contact organization user for help with scanning this domain.":"Permission Denied: Please contact organization user for help with scanning this domain.","Permission Denied: Please contact organization user for help with updating this domain.":"Permission Denied: Please contact organization user for help with updating this domain.","Permission Denied: Please contact super admin for help with archiving organization.":"Permission Denied: Please contact super admin for help with archiving organization.","Permission Denied: Please contact super admin for help with removing domain.":"Permission Denied: Please contact super admin for help with removing domain.","Permission Denied: Please contact super admin for help with removing organization.":"Permission Denied: Please contact super admin for help with removing organization.","Permission Denied: Please contact super admin for help with scanning this domain.":"Permission Denied: Please contact super admin for help with scanning this domain.","Permission Denied: Please contact super admin for help with verifying this organization.":"Permission Denied: Please contact super admin for help with verifying this organization.","Permission check error. Unable to request domain information.":"Permission check error. Unable to request domain information.","Permission error, not an admin for this user.":"Permission error, not an admin for this user.","Permission error: Unable to close other user's account.":"Permission error: Unable to close other user's account.","Permissions error. You do not have sufficient permissions to access this data.":"Permissions error. You do not have sufficient permissions to access this data.","Phone number has been successfully removed.":"Phone number has been successfully removed.","Phone number has been successfully set, you will receive a verification text message shortly.":"Phone number has been successfully set, you will receive a verification text message shortly.","Profile successfully updated.":"Profile successfully updated.","Requesting `{amount}` records on the `Affiliation` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `Affiliation` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `DkimFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `DkimFailureTable` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `DmarcFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `DmarcFailureTable` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `DmarcSummaries` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `DmarcSummaries` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `Domain` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `Domain` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `FullPassTable` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `FullPassTable` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `GuidanceTag` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `GuidanceTag` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `Log` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `Log` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `Organization` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `Organization` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `SpfFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `SpfFailureTable` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `User` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `User` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `VerifiedDomain` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `VerifiedDomain` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting `{amount}` records on the `VerifiedOrganization` connection exceeds the `{argSet}` limit of 100 records.":["Requesting `",["amount"],"` records on the `VerifiedOrganization` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `DKIMResults` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `DKIMResults` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `DKIM` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `DKIM` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `DMARC` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `DMARC` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `HTTPS` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `HTTPS` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `SPF` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `SPF` connection exceeds the `",["argSet"],"` limit of 100 records."],"Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records.":["Requesting ",["amount"]," records on the `SSL` connection exceeds the `",["argSet"],"` limit of 100 records."],"Successfully added {domainCount} domain(s) to {0}.":["Successfully added ",["domainCount"]," domain(s) to ",["0"],"."],"Successfully added {domainCount} domains to {0}.":["Successfully added ",["domainCount"]," domains to ",["0"],"."],"Successfully archived organization: {0}.":["Successfully archived organization: ",["0"],"."],"Successfully closed account.":"Successfully closed account.","Successfully dispatched one time scan.":"Successfully dispatched one time scan.","Successfully email verified account, and set TFA send method to email.":"Successfully email verified account, and set TFA send method to email.","Successfully invited user to organization, and sent notification email.":"Successfully invited user to organization, and sent notification email.","Successfully left organization: {0}":["Successfully left organization: ",["0"]],"Successfully removed domain: {0} from favourites.":["Successfully removed domain: ",["0"]," from favourites."],"Successfully removed domain: {0} from {1}.":["Successfully removed domain: ",["0"]," from ",["1"],"."],"Successfully removed organization: {0}.":["Successfully removed organization: ",["0"],"."],"Successfully removed user from organization.":"Successfully removed user from organization.","Successfully removed {domainCount} domain(s) from {0}.":["Successfully removed ",["domainCount"]," domain(s) from ",["0"],"."],"Successfully removed {domainCount} domains from {0}.":["Successfully removed ",["domainCount"]," domains from ",["0"],"."],"Successfully requested invite to organization, and sent notification email.":"Successfully requested invite to organization, and sent notification email.","Successfully sent invitation to service, and organization email.":"Successfully sent invitation to service, and organization email.","Successfully signed out.":"Successfully signed out.","Successfully transferred org: {0} ownership to user: {1}":["Successfully transferred org: ",["0"]," ownership to user: ",["1"]],"Successfully verified organization: {0}.":["Successfully verified organization: ",["0"],"."],"Successfully verified phone number, and set TFA send method to text.":"Successfully verified phone number, and set TFA send method to text.","Token value incorrect, please sign in again.":"Token value incorrect, please sign in again.","Too many failed login attempts, please reset your password, and try again.":"Too many failed login attempts, please reset your password, and try again.","Two factor code is incorrect. Please try again.":"Two factor code is incorrect. Please try again.","Two factor code length is incorrect. Please try again.":"Two factor code length is incorrect. Please try again.","Unable leave organization. Please try again.":"Unable leave organization. Please try again.","Unable to add domains in unknown organization.":"Unable to add domains in unknown organization.","Unable to archive organization. Please try again.":"Unable to archive organization. Please try again.","Unable to archive unknown organization.":"Unable to archive unknown organization.","Unable to authenticate. Please try again.":"Unable to authenticate. Please try again.","Unable to check permission. Please try again.":"Unable to check permission. Please try again.","Unable to close account of an undefined user.":"Unable to close account of an undefined user.","Unable to close account. Please try again.":"Unable to close account. Please try again.","Unable to create domain in unknown organization.":"Unable to create domain in unknown organization.","Unable to create domain, organization has already claimed it.":"Unable to create domain, organization has already claimed it.","Unable to create domain. Please try again.":"Unable to create domain. Please try again.","Unable to create domains. Please try again.":"Unable to create domains. Please try again.","Unable to create organization. Please try again.":"Unable to create organization. Please try again.","Unable to dispatch one time scan. Please try again.":"Unable to dispatch one time scan. Please try again.","Unable to export organization. Please try again.":"Unable to export organization. Please try again.","Unable to favourite domain, user has already favourited it.":"Unable to favourite domain, user has already favourited it.","Unable to favourite domain. Please try again.":"Unable to favourite domain. Please try again.","Unable to favourite unknown domain.":"Unable to favourite unknown domain.","Unable to find Aggregate guidance tag(s). Please try again.":"Unable to find Aggregate guidance tag(s). Please try again.","Unable to find DKIM guidance tag(s). Please try again.":"Unable to find DKIM guidance tag(s). Please try again.","Unable to find DKIM result(s). Please try again.":"Unable to find DKIM result(s). Please try again.","Unable to find DKIM scan(s). Please try again.":"Unable to find DKIM scan(s). Please try again.","Unable to find DMARC guidance tag(s). Please try again.":"Unable to find DMARC guidance tag(s). Please try again.","Unable to find DMARC scan(s). Please try again.":"Unable to find DMARC scan(s). Please try again.","Unable to find DMARC summary data. Please try again.":"Unable to find DMARC summary data. Please try again.","Unable to find DNS scan(s). Please try again.":"Unable to find DNS scan(s). Please try again.","Unable to find HTTPS guidance tag(s). Please try again.":"Unable to find HTTPS guidance tag(s). Please try again.","Unable to find HTTPS scan(s). Please try again.":"Unable to find HTTPS scan(s). Please try again.","Unable to find SPF guidance tag(s). Please try again.":"Unable to find SPF guidance tag(s). Please try again.","Unable to find SPF scan(s). Please try again.":"Unable to find SPF scan(s). Please try again.","Unable to find SSL guidance tag(s). Please try again.":"Unable to find SSL guidance tag(s). Please try again.","Unable to find SSL scan(s). Please try again.":"Unable to find SSL scan(s). Please try again.","Unable to find the requested domain.":"Unable to find the requested domain.","Unable to find user affiliation(s). Please try again.":"Unable to find user affiliation(s). Please try again.","Unable to find verified organization(s). Please try again.":"Unable to find verified organization(s). Please try again.","Unable to invite user to organization. Please try again.":"Unable to invite user to organization. Please try again.","Unable to invite user to organization. User is already affiliated with organization.":"Unable to invite user to organization. User is already affiliated with organization.","Unable to invite user to unknown organization.":"Unable to invite user to unknown organization.","Unable to invite user. Please try again.":"Unable to invite user. Please try again.","Unable to invite yourself to an org.":"Unable to invite yourself to an org.","Unable to leave organization. Please try again.":"Unable to leave organization. Please try again.","Unable to leave undefined organization.":"Unable to leave undefined organization.","Unable to load Aggregate guidance tag(s). Please try again.":"Unable to load Aggregate guidance tag(s). Please try again.","Unable to load DKIM failure data. Please try again.":"Unable to load DKIM failure data. Please try again.","Unable to load DKIM guidance tag(s). Please try again.":"Unable to load DKIM guidance tag(s). Please try again.","Unable to load DKIM result(s). Please try again.":"Unable to load DKIM result(s). Please try again.","Unable to load DKIM scan(s). Please try again.":"Unable to load DKIM scan(s). Please try again.","Unable to load DKIM summary. Please try again.":"Unable to load DKIM summary. Please try again.","Unable to load DMARC failure data. Please try again.":"Unable to load DMARC failure data. Please try again.","Unable to load DMARC guidance tag(s). Please try again.":"Unable to load DMARC guidance tag(s). Please try again.","Unable to load DMARC phase summary. Please try again.":"Unable to load DMARC phase summary. Please try again.","Unable to load DMARC scan(s). Please try again.":"Unable to load DMARC scan(s). Please try again.","Unable to load DMARC summary data. Please try again.":"Unable to load DMARC summary data. Please try again.","Unable to load DMARC summary. Please try again.":"Unable to load DMARC summary. Please try again.","Unable to load DNS scan(s). Please try again.":"Unable to load DNS scan(s). Please try again.","Unable to load HTTPS guidance tag(s). Please try again.":"Unable to load HTTPS guidance tag(s). Please try again.","Unable to load HTTPS scan(s). Please try again.":"Unable to load HTTPS scan(s). Please try again.","Unable to load HTTPS summary. Please try again.":"Unable to load HTTPS summary. Please try again.","Unable to load SPF failure data. Please try again.":"Unable to load SPF failure data. Please try again.","Unable to load SPF guidance tag(s). Please try again.":"Unable to load SPF guidance tag(s). Please try again.","Unable to load SPF scan(s). Please try again.":"Unable to load SPF scan(s). Please try again.","Unable to load SPF summary. Please try again.":"Unable to load SPF summary. Please try again.","Unable to load SSL guidance tag(s). Please try again.":"Unable to load SSL guidance tag(s). Please try again.","Unable to load SSL scan(s). Please try again.":"Unable to load SSL scan(s). Please try again.","Unable to load SSL summary. Please try again.":"Unable to load SSL summary. Please try again.","Unable to load affiliation information. Please try again.":"Unable to load affiliation information. Please try again.","Unable to load affiliation(s). Please try again.":"Unable to load affiliation(s). Please try again.","Unable to load all organization domain statuses. Please try again.":"Unable to load all organization domain statuses. Please try again.","Unable to load domain(s). Please try again.":"Unable to load domain(s). Please try again.","Unable to load domain. Please try again.":"Unable to load domain. Please try again.","Unable to load full pass data. Please try again.":"Unable to load full pass data. Please try again.","Unable to load log(s). Please try again.":"Unable to load log(s). Please try again.","Unable to load log. Please try again.":"Unable to load log. Please try again.","Unable to load mail summary. Please try again.":"Unable to load mail summary. Please try again.","Unable to load organization domain statuses. Please try again.":"Unable to load organization domain statuses. Please try again.","Unable to load organization(s). Please try again.":"Unable to load organization(s). Please try again.","Unable to load owner information. Please try again.":"Unable to load owner information. Please try again.","Unable to load summary. Please try again.":"Unable to load summary. Please try again.","Unable to load tags(s). Please try again.":"Unable to load tags(s). Please try again.","Unable to load user(s). Please try again.":"Unable to load user(s). Please try again.","Unable to load verified domain(s). Please try again.":"Unable to load verified domain(s). Please try again.","Unable to load verified organization(s). Please try again.":"Unable to load verified organization(s). Please try again.","Unable to load web connections summary. Please try again.":"Unable to load web connections summary. Please try again.","Unable to load web scan(s). Please try again.":"Unable to load web scan(s). Please try again.","Unable to load web summary. Please try again.":"Unable to load web summary. Please try again.","Unable to query affiliation(s). Please try again.":"Unable to query affiliation(s). Please try again.","Unable to query domain(s). Please try again.":"Unable to query domain(s). Please try again.","Unable to query log(s). Please try again.":"Unable to query log(s). Please try again.","Unable to query user(s). Please try again.":"Unable to query user(s). Please try again.","Unable to refresh tokens, please sign in.":"Unable to refresh tokens, please sign in.","Unable to remove a user that already does not belong to this organization.":"Unable to remove a user that already does not belong to this organization.","Unable to remove domain from unknown organization.":"Unable to remove domain from unknown organization.","Unable to remove domain. Domain is not part of organization.":"Unable to remove domain. Domain is not part of organization.","Unable to remove domain. Please try again.":"Unable to remove domain. Please try again.","Unable to remove domains from unknown organization.":"Unable to remove domains from unknown organization.","Unable to remove organization. Please try again.":"Unable to remove organization. Please try again.","Unable to remove phone number. Please try again.":"Unable to remove phone number. Please try again.","Unable to remove unknown domain.":"Unable to remove unknown domain.","Unable to remove unknown organization.":"Unable to remove unknown organization.","Unable to remove unknown user from organization.":"Unable to remove unknown user from organization.","Unable to remove user from organization.":"Unable to remove user from organization.","Unable to remove user from this organization. Please try again.":"Unable to remove user from this organization. Please try again.","Unable to remove user from unknown organization.":"Unable to remove user from unknown organization.","Unable to request a one time scan on a domain that already has a pending scan.":"Unable to request a one time scan on a domain that already has a pending scan.","Unable to request a one time scan on an unknown domain.":"Unable to request a one time scan on an unknown domain.","Unable to request a one time scan. Please try again.":"Unable to request a one time scan. Please try again.","Unable to request invite to organization with which you are already affiliated.":"Unable to request invite to organization with which you are already affiliated.","Unable to request invite to organization with which you have already requested to join.":"Unable to request invite to organization with which you have already requested to join.","Unable to request invite to unknown organization.":"Unable to request invite to unknown organization.","Unable to request invite. Please try again.":"Unable to request invite. Please try again.","Unable to reset password. Please request a new email.":"Unable to reset password. Please request a new email.","Unable to reset password. Please try again.":"Unable to reset password. Please try again.","Unable to retrieve DMARC report information for: {domain}":["Unable to retrieve DMARC report information for: ",["domain"]],"Unable to select DMARC report(s) for this period and year.":"Unable to select DMARC report(s) for this period and year.","Unable to send authentication email. Please try again.":"Unable to send authentication email. Please try again.","Unable to send authentication text message. Please try again.":"Unable to send authentication text message. Please try again.","Unable to send org invite email. Please try again.":"Unable to send org invite email. Please try again.","Unable to send org invite request email. Please try again.":"Unable to send org invite request email. Please try again.","Unable to send password reset email. Please try again.":"Unable to send password reset email. Please try again.","Unable to send two factor authentication message. Please try again.":"Unable to send two factor authentication message. Please try again.","Unable to send verification email. Please try again.":"Unable to send verification email. Please try again.","Unable to set phone number, please try again.":"Unable to set phone number, please try again.","Unable to sign in, please try again.":"Unable to sign in, please try again.","Unable to sign up, please contact org admin for a new invite.":"Unable to sign up, please contact org admin for a new invite.","Unable to sign up. Please try again.":"Unable to sign up. Please try again.","Unable to transfer organization ownership. Please try again.":"Unable to transfer organization ownership. Please try again.","Unable to transfer ownership of a verified organization.":"Unable to transfer ownership of a verified organization.","Unable to transfer ownership of an org to an undefined user.":"Unable to transfer ownership of an org to an undefined user.","Unable to transfer ownership of undefined organization.":"Unable to transfer ownership of undefined organization.","Unable to transfer ownership to a user outside the org. Please invite the user and try again.":"Unable to transfer ownership to a user outside the org. Please invite the user and try again.","Unable to two factor authenticate. Please try again.":"Unable to two factor authenticate. Please try again.","Unable to unfavourite domain, domain is not favourited.":"Unable to unfavourite domain, domain is not favourited.","Unable to unfavourite domain. Please try again.":"Unable to unfavourite domain. Please try again.","Unable to unfavourite unknown domain.":"Unable to unfavourite unknown domain.","Unable to update domain edge. Please try again.":"Unable to update domain edge. Please try again.","Unable to update domain in an unknown org.":"Unable to update domain in an unknown org.","Unable to update domain that does not belong to the given organization.":"Unable to update domain that does not belong to the given organization.","Unable to update domain. Please try again.":"Unable to update domain. Please try again.","Unable to update organization. Please try again.":"Unable to update organization. Please try again.","Unable to update password, current password does not match. Please try again.":"Unable to update password, current password does not match. Please try again.","Unable to update password, new passwords do not match. Please try again.":"Unable to update password, new passwords do not match. Please try again.","Unable to update password, passwords do not match requirements. Please try again.":"Unable to update password, passwords do not match requirements. Please try again.","Unable to update password. Please try again.":"Unable to update password. Please try again.","Unable to update profile. Please try again.":"Unable to update profile. Please try again.","Unable to update role: organization unknown.":"Unable to update role: organization unknown.","Unable to update role: user does not belong to organization.":"Unable to update role: user does not belong to organization.","Unable to update role: user unknown.":"Unable to update role: user unknown.","Unable to update unknown domain.":"Unable to update unknown domain.","Unable to update unknown organization.":"Unable to update unknown organization.","Unable to update user's role. Please try again.":"Unable to update user's role. Please try again.","Unable to update your own role.":"Unable to update your own role.","Unable to verify account. Please request a new email.":"Unable to verify account. Please request a new email.","Unable to verify account. Please try again.":"Unable to verify account. Please try again.","Unable to verify if user is a super admin, please try again.":"Unable to verify if user is a super admin, please try again.","Unable to verify if user is an admin, please try again.":"Unable to verify if user is an admin, please try again.","Unable to verify organization. Please try again.":"Unable to verify organization. Please try again.","Unable to verify unknown organization.":"Unable to verify unknown organization.","User could not be queried.":"User could not be queried.","User role was updated successfully.":"User role was updated successfully.","Username not available, please try another.":"Username not available, please try another.","Verification error. Please activate multi-factor authentication to access content.":"Verification error. Please activate multi-factor authentication to access content.","Verification error. Please verify your account via email to access content.":"Verification error. Please verify your account via email to access content.","You must provide a `first` or `last` value to properly paginate the `Affiliation` connection.":"You must provide a `first` or `last` value to properly paginate the `Affiliation` connection.","You must provide a `first` or `last` value to properly paginate the `DKIMResults` connection.":"You must provide a `first` or `last` value to properly paginate the `DKIMResults` connection.","You must provide a `first` or `last` value to properly paginate the `DKIM` connection.":"You must provide a `first` or `last` value to properly paginate the `DKIM` connection.","You must provide a `first` or `last` value to properly paginate the `DMARC` connection.":"You must provide a `first` or `last` value to properly paginate the `DMARC` connection.","You must provide a `first` or `last` value to properly paginate the `DkimFailureTable` connection.":"You must provide a `first` or `last` value to properly paginate the `DkimFailureTable` connection.","You must provide a `first` or `last` value to properly paginate the `DmarcFailureTable` connection.":"You must provide a `first` or `last` value to properly paginate the `DmarcFailureTable` connection.","You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection.":"You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection.","You must provide a `first` or `last` value to properly paginate the `Domain` connection.":"You must provide a `first` or `last` value to properly paginate the `Domain` connection.","You must provide a `first` or `last` value to properly paginate the `FullPassTable` connection.":"You must provide a `first` or `last` value to properly paginate the `FullPassTable` connection.","You must provide a `first` or `last` value to properly paginate the `GuidanceTag` connection.":"You must provide a `first` or `last` value to properly paginate the `GuidanceTag` connection.","You must provide a `first` or `last` value to properly paginate the `HTTPS` connection.":"You must provide a `first` or `last` value to properly paginate the `HTTPS` connection.","You must provide a `first` or `last` value to properly paginate the `Log` connection.":"You must provide a `first` or `last` value to properly paginate the `Log` connection.","You must provide a `first` or `last` value to properly paginate the `Organization` connection.":"You must provide a `first` or `last` value to properly paginate the `Organization` connection.","You must provide a `first` or `last` value to properly paginate the `SPF` connection.":"You must provide a `first` or `last` value to properly paginate the `SPF` connection.","You must provide a `first` or `last` value to properly paginate the `SSL` connection.":"You must provide a `first` or `last` value to properly paginate the `SSL` connection.","You must provide a `first` or `last` value to properly paginate the `SpfFailureTable` connection.":"You must provide a `first` or `last` value to properly paginate the `SpfFailureTable` connection.","You must provide a `first` or `last` value to properly paginate the `User` connection.":"You must provide a `first` or `last` value to properly paginate the `User` connection.","You must provide a `first` or `last` value to properly paginate the `VerifiedDomain` connection.":"You must provide a `first` or `last` value to properly paginate the `VerifiedDomain` connection.","You must provide a `first` or `last` value to properly paginate the `VerifiedOrganization` connection.":"You must provide a `first` or `last` value to properly paginate the `VerifiedOrganization` connection.","You must provide a `limit` value in the range of 1-100 to properly paginate the `DNS` connection.":"You must provide a `limit` value in the range of 1-100 to properly paginate the `DNS` connection.","You must provide a `limit` value in the range of 1-100 to properly paginate the `web` connection.":"You must provide a `limit` value in the range of 1-100 to properly paginate the `web` connection.","You must provide a `limit` value to properly paginate the `DNS` connection.":"You must provide a `limit` value to properly paginate the `DNS` connection.","You must provide a `limit` value to properly paginate the `web` connection.":"You must provide a `limit` value to properly paginate the `web` connection.","You must provide a `period` value to access the `DmarcSummaries` connection.":"You must provide a `period` value to access the `DmarcSummaries` connection.","You must provide a `year` value to access the `DmarcSummaries` connection.":"You must provide a `year` value to access the `DmarcSummaries` connection.","You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `DNS` connection.":"You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `DNS` connection.","You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `web` connection.":"You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `web` connection.","`{argSet}` must be of type `number` not `{typeSet}`.":["`",["argSet"],"` must be of type `number` not `",["typeSet"],"`."],"`{argSet}` on the `Affiliation` connection cannot be less than zero.":["`",["argSet"],"` on the `Affiliation` connection cannot be less than zero."],"`{argSet}` on the `DKIMResults` connection cannot be less than zero.":["`",["argSet"],"` on the `DKIMResults` connection cannot be less than zero."],"`{argSet}` on the `DKIM` connection cannot be less than zero.":["`",["argSet"],"` on the `DKIM` connection cannot be less than zero."],"`{argSet}` on the `DMARC` connection cannot be less than zero.":["`",["argSet"],"` on the `DMARC` connection cannot be less than zero."],"`{argSet}` on the `DkimFailureTable` connection cannot be less than zero.":["`",["argSet"],"` on the `DkimFailureTable` connection cannot be less than zero."],"`{argSet}` on the `DmarcFailureTable` connection cannot be less than zero.":["`",["argSet"],"` on the `DmarcFailureTable` connection cannot be less than zero."],"`{argSet}` on the `DmarcSummaries` connection cannot be less than zero.":["`",["argSet"],"` on the `DmarcSummaries` connection cannot be less than zero."],"`{argSet}` on the `Domain` connection cannot be less than zero.":["`",["argSet"],"` on the `Domain` connection cannot be less than zero."],"`{argSet}` on the `FullPassTable` connection cannot be less than zero.":["`",["argSet"],"` on the `FullPassTable` connection cannot be less than zero."],"`{argSet}` on the `GuidanceTag` connection cannot be less than zero.":["`",["argSet"],"` on the `GuidanceTag` connection cannot be less than zero."],"`{argSet}` on the `HTTPS` connection cannot be less than zero.":["`",["argSet"],"` on the `HTTPS` connection cannot be less than zero."],"`{argSet}` on the `Log` connection cannot be less than zero.":["`",["argSet"],"` on the `Log` connection cannot be less than zero."],"`{argSet}` on the `Organization` connection cannot be less than zero.":["`",["argSet"],"` on the `Organization` connection cannot be less than zero."],"`{argSet}` on the `SPF` connection cannot be less than zero.":["`",["argSet"],"` on the `SPF` connection cannot be less than zero."],"`{argSet}` on the `SSL` connection cannot be less than zero.":["`",["argSet"],"` on the `SSL` connection cannot be less than zero."],"`{argSet}` on the `SpfFailureTable` connection cannot be less than zero.":["`",["argSet"],"` on the `SpfFailureTable` connection cannot be less than zero."],"`{argSet}` on the `User` connection cannot be less than zero.":["`",["argSet"],"` on the `User` connection cannot be less than zero."],"`{argSet}` on the `VerifiedDomain` connection cannot be less than zero.":["`",["argSet"],"` on the `VerifiedDomain` connection cannot be less than zero."],"`{argSet}` on the `VerifiedOrganization` connection cannot be less than zero.":["`",["argSet"],"` on the `VerifiedOrganization` connection cannot be less than zero."]}}; \ No newline at end of file diff --git a/api/src/locale/en/messages.po b/api/src/locale/en/messages.po index 110903c079..17cf3b9ad0 100644 --- a/api/src/locale/en/messages.po +++ b/api/src/locale/en/messages.po @@ -11,27 +11,44 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" -#: src/auth/check-permission.js:20 -#: src/auth/check-permission.js:48 +#: src/auth/check-permission.js:18 +#: src/auth/check-permission.js:57 #: src/auth/user-required.js:10 #: src/auth/user-required.js:21 #: src/auth/user-required.js:28 msgid "Authentication error. Please sign in." msgstr "Authentication error. Please sign in." -#: src/organization/objects/organization.js:188 +#: src/organization/objects/organization.js:241 msgid "Cannot query affiliations on organization without admin permission or higher." msgstr "Cannot query affiliations on organization without admin permission or higher." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:244 -#: src/audit-logs/queries/find-audit-logs.js:62 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:224 +#: src/audit-logs/queries/find-audit-logs.js:51 msgid "Cannot query audit logs on organization without admin permission or higher." msgstr "Cannot query audit logs on organization without admin permission or higher." -#: src/user/mutations/sign-up.js:117 +#: src/domain/objects/domain.js:149 +msgid "Cannot query dns scan results without permission." +msgstr "Cannot query dns scan results without permission." + +#: src/domain/objects/domain.js:58 +msgid "Cannot query domain selectors without permission." +msgstr "Cannot query domain selectors without permission." + +#: src/domain/objects/domain.js:195 +msgid "Cannot query web scan results without permission." +msgstr "Cannot query web scan results without permission." + +#: src/user/mutations/sign-up.js:106 msgid "Email already in use." msgstr "Email already in use." +#: src/domain/mutations/request-scan.js:87 +#: src/domain/mutations/request-scan.js:97 +msgid "Error while requesting scan. Please try again." +msgstr "Error while requesting scan. Please try again." + #: src/user/mutations/send-password-reset.js:62 msgid "If an account with this username is found, a password reset link will be found in your inbox." msgstr "If an account with this username is found, a password reset link will be found in your inbox." @@ -61,7 +78,7 @@ msgstr "Invalid token, please sign in." msgid "New passwords do not match." msgstr "New passwords do not match." -#: src/organization/queries/find-organization-by-slug.js:49 +#: src/organization/queries/find-organization-by-slug.js:41 #: src/user/queries/find-my-tracker.js:29 msgid "No organization with the provided slug could be found." msgstr "No organization with the provided slug could be found." @@ -74,7 +91,7 @@ msgstr "No verified domain with the provided domain could be found." msgid "No verified organization with the provided slug could be found." msgstr "No verified organization with the provided slug could be found." -#: src/organization/mutations/verify-organization.js:82 +#: src/organization/mutations/verify-organization.js:81 msgid "Organization has already been verified." msgstr "Organization has already been verified." @@ -82,18 +99,18 @@ msgstr "Organization has already been verified." msgid "Organization name already in use, please choose another and try again." msgstr "Organization name already in use, please choose another and try again." -#: src/organization/mutations/create-organization.js:139 +#: src/organization/mutations/create-organization.js:124 msgid "Organization name already in use. Please try again with a different name." msgstr "Organization name already in use. Please try again with a different name." -#: src/auth/check-domain-ownership.js:30 -#: src/auth/check-domain-ownership.js:42 -#: src/auth/check-domain-ownership.js:64 -#: src/auth/check-domain-ownership.js:75 +#: src/auth/check-domain-ownership.js:29 +#: src/auth/check-domain-ownership.js:39 +#: src/auth/check-domain-ownership.js:65 +#: src/auth/check-domain-ownership.js:74 msgid "Ownership check error. Unable to request domain information." msgstr "Ownership check error. Unable to request domain information." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:80 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:77 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:170 msgid "Passing both `first` and `last` to paginate the `Affiliation` connection is not supported." msgstr "Passing both `first` and `last` to paginate the `Affiliation` connection is not supported." @@ -119,12 +136,12 @@ msgstr "Passing both `first` and `last` to paginate the `DkimFailureTable` conne msgid "Passing both `first` and `last` to paginate the `DmarcFailureTable` connection is not supported." msgstr "Passing both `first` and `last` to paginate the `DmarcFailureTable` connection is not supported." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:198 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:170 msgid "Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported." msgstr "Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:163 -#: src/domain/loaders/load-domain-connections-by-user-id.js:175 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:148 +#: src/domain/loaders/load-domain-connections-by-user-id.js:154 msgid "Passing both `first` and `last` to paginate the `Domain` connection is not supported." msgstr "Passing both `first` and `last` to paginate the `Domain` connection is not supported." @@ -145,12 +162,12 @@ msgstr "Passing both `first` and `last` to paginate the `GuidanceTag` connection #~ msgid "Passing both `first` and `last` to paginate the `HTTPS` connection is not supported." #~ msgstr "Passing both `first` and `last` to paginate the `HTTPS` connection is not supported." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:110 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:105 msgid "Passing both `first` and `last` to paginate the `Log` connection is not supported." msgstr "Passing both `first` and `last` to paginate the `Log` connection is not supported." #: src/organization/loaders/load-organization-connections-by-domain-id.js:192 -#: src/organization/loaders/load-organization-connections-by-user-id.js:191 +#: src/organization/loaders/load-organization-connections-by-user-id.js:173 #: src/organization/loaders/load-web-check-connections-by-user-id.js:98 msgid "Passing both `first` and `last` to paginate the `Organization` connection is not supported." msgstr "Passing both `first` and `last` to paginate the `Organization` connection is not supported." @@ -182,7 +199,7 @@ msgid "Passing both `first` and `last` to paginate the `VerifiedOrganization` co msgstr "Passing both `first` and `last` to paginate the `VerifiedOrganization` connection is not supported." #: src/user/mutations/reset-password.js:120 -#: src/user/mutations/sign-up.js:91 +#: src/user/mutations/sign-up.js:84 msgid "Password does not meet requirements." msgstr "Password does not meet requirements." @@ -194,32 +211,32 @@ msgstr "Password was successfully reset." msgid "Password was successfully updated." msgstr "Password was successfully updated." -#: src/user/mutations/sign-up.js:103 +#: src/user/mutations/sign-up.js:94 msgid "Passwords do not match." msgstr "Passwords do not match." -#: src/organization/queries/find-organization-by-slug.js:61 -#: src/organization/queries/find-organization-by-slug.js:74 +#: src/organization/queries/find-organization-by-slug.js:50 +#: src/organization/queries/find-organization-by-slug.js:55 msgid "Permission Denied: Could not retrieve specified organization." msgstr "Permission Denied: Could not retrieve specified organization." -#: src/user/mutations/update-user-profile.js:115 +#: src/user/mutations/update-user-profile.js:110 msgid "Permission Denied: Multi-factor authentication is required for admin accounts" msgstr "Permission Denied: Multi-factor authentication is required for admin accounts" -#: src/affiliation/mutations/transfer-org-ownership.js:95 +#: src/affiliation/mutations/transfer-org-ownership.js:74 msgid "Permission Denied: Please contact org owner to transfer ownership." msgstr "Permission Denied: Please contact org owner to transfer ownership." -#: src/domain/mutations/remove-organizations-domains.js:135 +#: src/domain/mutations/remove-organizations-domains.js:127 msgid "Permission Denied: Please contact organization admin for help with archiving domains." msgstr "Permission Denied: Please contact organization admin for help with archiving domains." -#: src/domain/mutations/remove-domain.js:117 +#: src/domain/mutations/remove-domain.js:94 msgid "Permission Denied: Please contact organization admin for help with removing domain." msgstr "Permission Denied: Please contact organization admin for help with removing domain." -#: src/domain/mutations/remove-organizations-domains.js:122 +#: src/domain/mutations/remove-organizations-domains.js:116 msgid "Permission Denied: Please contact organization admin for help with removing domains." msgstr "Permission Denied: Please contact organization admin for help with removing domains." @@ -227,7 +244,8 @@ msgstr "Permission Denied: Please contact organization admin for help with remov msgid "Permission Denied: Please contact organization admin for help with removing organization." msgstr "Permission Denied: Please contact organization admin for help with removing organization." -#: src/affiliation/mutations/remove-user-from-org.js:233 +#: src/affiliation/mutations/remove-user-from-org.js:126 +#: src/affiliation/mutations/remove-user-from-org.js:138 msgid "Permission Denied: Please contact organization admin for help with removing users." msgstr "Permission Denied: Please contact organization admin for help with removing users." @@ -235,39 +253,49 @@ msgstr "Permission Denied: Please contact organization admin for help with remov msgid "Permission Denied: Please contact organization admin for help with updating organization." msgstr "Permission Denied: Please contact organization admin for help with updating organization." -#: src/affiliation/mutations/update-user-role.js:186 -#: src/affiliation/mutations/update-user-role.js:209 -#: src/affiliation/mutations/update-user-role.js:227 -msgid "Permission Denied: Please contact organization admin for help with updating user roles." -msgstr "Permission Denied: Please contact organization admin for help with updating user roles." +#: src/affiliation/mutations/update-user-role.js:170 +#: src/affiliation/mutations/update-user-role.js:193 +#: src/affiliation/mutations/update-user-role.js:210 +#~ msgid "Permission Denied: Please contact organization admin for help with updating user roles." +#~ msgstr "Permission Denied: Please contact organization admin for help with updating user roles." -#: src/affiliation/mutations/invite-user-to-org.js:114 +#: src/affiliation/mutations/invite-user-to-org.js:99 msgid "Permission Denied: Please contact organization admin for help with user invitations." msgstr "Permission Denied: Please contact organization admin for help with user invitations." -#: src/affiliation/mutations/update-user-role.js:114 +#: src/affiliation/mutations/update-user-role.js:109 +#: src/affiliation/mutations/update-user-role.js:160 +#: src/affiliation/mutations/update-user-role.js:172 msgid "Permission Denied: Please contact organization admin for help with user role changes." msgstr "Permission Denied: Please contact organization admin for help with user role changes." -#: src/domain/mutations/add-organizations-domains.js:131 -#: src/domain/mutations/create-domain.js:140 +#: src/domain/mutations/create-domain.js:121 msgid "Permission Denied: Please contact organization user for help with creating domain." msgstr "Permission Denied: Please contact organization user for help with creating domain." +#: src/domain/mutations/add-organizations-domains.js:130 +msgid "Permission Denied: Please contact organization user for help with creating domains." +msgstr "Permission Denied: Please contact organization user for help with creating domains." + #: src/domain/queries/find-domain-by-domain.js:57 +#: src/organization/objects/organization.js:98 msgid "Permission Denied: Please contact organization user for help with retrieving this domain." msgstr "Permission Denied: Please contact organization user for help with retrieving this domain." -#: src/domain/mutations/request-scan.js:71 +#: src/domain/mutations/request-scan.js:63 msgid "Permission Denied: Please contact organization user for help with scanning this domain." msgstr "Permission Denied: Please contact organization user for help with scanning this domain." -#: src/domain/mutations/update-domain.js:150 +#: src/domain/mutations/update-domain.js:139 msgid "Permission Denied: Please contact organization user for help with updating this domain." msgstr "Permission Denied: Please contact organization user for help with updating this domain." -#: src/domain/mutations/remove-domain.js:104 -#: src/domain/mutations/remove-organizations-domains.js:109 +#: src/organization/mutations/archive-organization.js:68 +msgid "Permission Denied: Please contact super admin for help with archiving organization." +msgstr "Permission Denied: Please contact super admin for help with archiving organization." + +#: src/domain/mutations/remove-domain.js:107 +#: src/domain/mutations/remove-organizations-domains.js:105 msgid "Permission Denied: Please contact super admin for help with removing domain." msgstr "Permission Denied: Please contact super admin for help with removing domain." @@ -279,12 +307,12 @@ msgstr "Permission Denied: Please contact super admin for help with removing org #~ msgid "Permission Denied: Please contact super admin for help with scanning this domain." #~ msgstr "Permission Denied: Please contact super admin for help with scanning this domain." -#: src/organization/mutations/verify-organization.js:69 +#: src/organization/mutations/verify-organization.js:68 msgid "Permission Denied: Please contact super admin for help with verifying this organization." msgstr "Permission Denied: Please contact super admin for help with verifying this organization." -#: src/auth/check-domain-permission.js:24 -#: src/auth/check-domain-permission.js:48 +#: src/auth/check-domain-permission.js:22 +#: src/auth/check-domain-permission.js:51 #: src/auth/check-domain-permission.js:61 msgid "Permission check error. Unable to request domain information." msgstr "Permission check error. Unable to request domain information." @@ -292,16 +320,16 @@ msgstr "Permission check error. Unable to request domain information." #: src/auth/check-user-is-admin-for-user.js:20 #: src/auth/check-user-is-admin-for-user.js:30 #: src/auth/check-user-is-admin-for-user.js:63 -#: src/auth/check-user-is-admin-for-user.js:75 +#: src/auth/check-user-is-admin-for-user.js:73 msgid "Permission error, not an admin for this user." msgstr "Permission error, not an admin for this user." -#: src/user/mutations/close-account.js:56 +#: src/user/mutations/close-account.js:54 msgid "Permission error: Unable to close other user's account." msgstr "Permission error: Unable to close other user's account." #: src/auth/super-admin-required.js:15 -#: src/organization/queries/get-all-organization-domain-statuses.js:36 +#: src/organization/queries/get-all-organization-domain-statuses.js:27 msgid "Permissions error. You do not have sufficient permissions to access this data." msgstr "Permissions error. You do not have sufficient permissions to access this data." @@ -313,11 +341,11 @@ msgstr "Phone number has been successfully removed." msgid "Phone number has been successfully set, you will receive a verification text message shortly." msgstr "Phone number has been successfully set, you will receive a verification text message shortly." -#: src/user/mutations/update-user-profile.js:198 +#: src/user/mutations/update-user-profile.js:184 msgid "Profile successfully updated." msgstr "Profile successfully updated." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:103 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:95 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:193 msgid "Requesting `{amount}` records on the `Affiliation` connection exceeds the `{argSet}` limit of 100 records." msgstr "Requesting `{amount}` records on the `Affiliation` connection exceeds the `{argSet}` limit of 100 records." @@ -330,12 +358,12 @@ msgstr "Requesting `{amount}` records on the `DkimFailureTable` connection excee msgid "Requesting `{amount}` records on the `DmarcFailureTable` connection exceeds the `{argSet}` limit of 100 records." msgstr "Requesting `{amount}` records on the `DmarcFailureTable` connection exceeds the `{argSet}` limit of 100 records." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:221 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:188 msgid "Requesting `{amount}` records on the `DmarcSummaries` connection exceeds the `{argSet}` limit of 100 records." msgstr "Requesting `{amount}` records on the `DmarcSummaries` connection exceeds the `{argSet}` limit of 100 records." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:186 -#: src/domain/loaders/load-domain-connections-by-user-id.js:198 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:164 +#: src/domain/loaders/load-domain-connections-by-user-id.js:172 msgid "Requesting `{amount}` records on the `Domain` connection exceeds the `{argSet}` limit of 100 records." msgstr "Requesting `{amount}` records on the `Domain` connection exceeds the `{argSet}` limit of 100 records." @@ -352,12 +380,12 @@ msgstr "Requesting `{amount}` records on the `FullPassTable` connection exceeds msgid "Requesting `{amount}` records on the `GuidanceTag` connection exceeds the `{argSet}` limit of 100 records." msgstr "Requesting `{amount}` records on the `GuidanceTag` connection exceeds the `{argSet}` limit of 100 records." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:133 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:119 msgid "Requesting `{amount}` records on the `Log` connection exceeds the `{argSet}` limit of 100 records." msgstr "Requesting `{amount}` records on the `Log` connection exceeds the `{argSet}` limit of 100 records." #: src/organization/loaders/load-organization-connections-by-domain-id.js:215 -#: src/organization/loaders/load-organization-connections-by-user-id.js:214 +#: src/organization/loaders/load-organization-connections-by-user-id.js:187 #: src/organization/loaders/load-web-check-connections-by-user-id.js:121 msgid "Requesting `{amount}` records on the `Organization` connection exceeds the `{argSet}` limit of 100 records." msgstr "Requesting `{amount}` records on the `Organization` connection exceeds the `{argSet}` limit of 100 records." @@ -404,15 +432,23 @@ msgstr "Requesting `{amount}` records on the `VerifiedOrganization` connection e #~ msgid "Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records." #~ msgstr "Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records." +#: src/domain/mutations/add-organizations-domains.js:353 +msgid "Successfully added {domainCount} domain(s) to {0}." +msgstr "Successfully added {domainCount} domain(s) to {0}." + #: src/domain/mutations/add-organizations-domains.js:351 -msgid "Successfully added {domainCount} domains to {0}." -msgstr "Successfully added {domainCount} domains to {0}." +#~ msgid "Successfully added {domainCount} domains to {0}." +#~ msgstr "Successfully added {domainCount} domains to {0}." + +#: src/organization/mutations/archive-organization.js:188 +msgid "Successfully archived organization: {0}." +msgstr "Successfully archived organization: {0}." -#: src/user/mutations/close-account.js:502 +#: src/user/mutations/close-account.js:405 msgid "Successfully closed account." msgstr "Successfully closed account." -#: src/domain/mutations/request-scan.js:94 +#: src/domain/mutations/request-scan.js:166 msgid "Successfully dispatched one time scan." msgstr "Successfully dispatched one time scan." @@ -420,11 +456,11 @@ msgstr "Successfully dispatched one time scan." msgid "Successfully email verified account, and set TFA send method to email." msgstr "Successfully email verified account, and set TFA send method to email." -#: src/affiliation/mutations/invite-user-to-org.js:241 +#: src/affiliation/mutations/invite-user-to-org.js:280 msgid "Successfully invited user to organization, and sent notification email." msgstr "Successfully invited user to organization, and sent notification email." -#: src/affiliation/mutations/leave-organization.js:319 +#: src/affiliation/mutations/leave-organization.js:84 msgid "Successfully left organization: {0}" msgstr "Successfully left organization: {0}" @@ -432,23 +468,31 @@ msgstr "Successfully left organization: {0}" msgid "Successfully removed domain: {0} from favourites." msgstr "Successfully removed domain: {0} from favourites." -#: src/domain/mutations/remove-domain.js:395 +#: src/domain/mutations/remove-domain.js:326 msgid "Successfully removed domain: {0} from {1}." msgstr "Successfully removed domain: {0} from {1}." -#: src/organization/mutations/remove-organization.js:502 +#: src/organization/mutations/remove-organization.js:384 msgid "Successfully removed organization: {0}." msgstr "Successfully removed organization: {0}." -#: src/affiliation/mutations/remove-user-from-org.js:219 +#: src/affiliation/mutations/remove-user-from-org.js:195 msgid "Successfully removed user from organization." msgstr "Successfully removed user from organization." +#: src/domain/mutations/remove-organizations-domains.js:459 +msgid "Successfully removed {domainCount} domain(s) from {0}." +msgstr "Successfully removed {domainCount} domain(s) from {0}." + #: src/domain/mutations/remove-organizations-domains.js:530 -msgid "Successfully removed {domainCount} domains from {0}." -msgstr "Successfully removed {domainCount} domains from {0}." +#~ msgid "Successfully removed {domainCount} domains from {0}." +#~ msgstr "Successfully removed {domainCount} domains from {0}." + +#: src/affiliation/mutations/request-org-affiliation.js:197 +msgid "Successfully requested invite to organization, and sent notification email." +msgstr "Successfully requested invite to organization, and sent notification email." -#: src/affiliation/mutations/invite-user-to-org.js:167 +#: src/affiliation/mutations/invite-user-to-org.js:172 msgid "Successfully sent invitation to service, and organization email." msgstr "Successfully sent invitation to service, and organization email." @@ -456,11 +500,11 @@ msgstr "Successfully sent invitation to service, and organization email." msgid "Successfully signed out." msgstr "Successfully signed out." -#: src/affiliation/mutations/transfer-org-ownership.js:218 +#: src/affiliation/mutations/transfer-org-ownership.js:185 msgid "Successfully transferred org: {0} ownership to user: {1}" msgstr "Successfully transferred org: {0} ownership to user: {1}" -#: src/organization/mutations/verify-organization.js:151 +#: src/organization/mutations/verify-organization.js:119 msgid "Successfully verified organization: {0}." msgstr "Successfully verified organization: {0}." @@ -484,72 +528,79 @@ msgstr "Two factor code is incorrect. Please try again." msgid "Two factor code length is incorrect. Please try again." msgstr "Two factor code length is incorrect. Please try again." -#: src/affiliation/mutations/leave-organization.js:74 -#: src/affiliation/mutations/leave-organization.js:84 -#: src/affiliation/mutations/leave-organization.js:115 -#: src/affiliation/mutations/leave-organization.js:132 -#: src/affiliation/mutations/leave-organization.js:163 -#: src/affiliation/mutations/leave-organization.js:173 -#: src/affiliation/mutations/leave-organization.js:247 -#: src/affiliation/mutations/leave-organization.js:284 -#: src/affiliation/mutations/leave-organization.js:302 -#: src/affiliation/mutations/leave-organization.js:312 +#: src/affiliation/mutations/leave-organization.js:70 +#: src/affiliation/mutations/leave-organization.js:77 msgid "Unable leave organization. Please try again." msgstr "Unable leave organization. Please try again." +#: src/domain/mutations/add-organizations-domains.js:115 +msgid "Unable to add domains in unknown organization." +msgstr "Unable to add domains in unknown organization." + +#: src/organization/mutations/archive-organization.js:101 +#: src/organization/mutations/archive-organization.js:111 +#: src/organization/mutations/archive-organization.js:129 +#: src/organization/mutations/archive-organization.js:146 +#: src/organization/mutations/archive-organization.js:155 +msgid "Unable to archive organization. Please try again." +msgstr "Unable to archive organization. Please try again." + +#: src/organization/mutations/archive-organization.js:54 +msgid "Unable to archive unknown organization." +msgstr "Unable to archive unknown organization." + #: src/user/mutations/authenticate.js:77 #: src/user/mutations/authenticate.js:117 #: src/user/mutations/authenticate.js:126 msgid "Unable to authenticate. Please try again." msgstr "Unable to authenticate. Please try again." -#: src/auth/check-permission.js:30 -#: src/auth/check-permission.js:58 +#: src/auth/check-permission.js:26 +#: src/auth/check-permission.js:64 #: src/auth/check-super-admin.js:20 #: src/auth/check-super-admin.js:30 msgid "Unable to check permission. Please try again." msgstr "Unable to check permission. Please try again." -#: src/user/mutations/close-account.js:69 +#: src/user/mutations/close-account.js:66 msgid "Unable to close account of an undefined user." msgstr "Unable to close account of an undefined user." -#: src/user/mutations/close-account.js:92 -#: src/user/mutations/close-account.js:102 -#: src/user/mutations/close-account.js:121 -#: src/user/mutations/close-account.js:131 -#: src/user/mutations/close-account.js:162 -#: src/user/mutations/close-account.js:177 -#: src/user/mutations/close-account.js:206 -#: src/user/mutations/close-account.js:216 -#: src/user/mutations/close-account.js:252 -#: src/user/mutations/close-account.js:364 -#: src/user/mutations/close-account.js:395 -#: src/user/mutations/close-account.js:420 -#: src/user/mutations/close-account.js:456 -#: src/user/mutations/close-account.js:473 -#: src/user/mutations/close-account.js:488 -#: src/user/mutations/close-account.js:497 +#: src/user/mutations/close-account.js:89 +#: src/user/mutations/close-account.js:99 +#: src/user/mutations/close-account.js:118 +#: src/user/mutations/close-account.js:128 +#: src/user/mutations/close-account.js:159 +#: src/user/mutations/close-account.js:174 +#: src/user/mutations/close-account.js:203 +#: src/user/mutations/close-account.js:213 +#: src/user/mutations/close-account.js:239 +#: src/user/mutations/close-account.js:257 +#: src/user/mutations/close-account.js:286 +#: src/user/mutations/close-account.js:309 +#: src/user/mutations/close-account.js:344 +#: src/user/mutations/close-account.js:361 +#: src/user/mutations/close-account.js:376 +#: src/user/mutations/close-account.js:383 msgid "Unable to close account. Please try again." msgstr "Unable to close account. Please try again." -#: src/domain/mutations/add-organizations-domains.js:115 -#: src/domain/mutations/create-domain.js:120 +#: src/domain/mutations/create-domain.js:107 msgid "Unable to create domain in unknown organization." msgstr "Unable to create domain in unknown organization." -#: src/domain/mutations/create-domain.js:198 +#: src/domain/mutations/create-domain.js:174 msgid "Unable to create domain, organization has already claimed it." msgstr "Unable to create domain, organization has already claimed it." -#: src/domain/mutations/create-domain.js:177 -#: src/domain/mutations/create-domain.js:187 -#: src/domain/mutations/create-domain.js:230 -#: src/domain/mutations/create-domain.js:240 +#: src/domain/mutations/create-domain.js:156 +#: src/domain/mutations/create-domain.js:164 +#: src/domain/mutations/create-domain.js:203 +#: src/domain/mutations/create-domain.js:213 +#: src/domain/mutations/create-domain.js:231 #: src/domain/mutations/create-domain.js:260 -#: src/domain/mutations/create-domain.js:291 -#: src/domain/mutations/create-domain.js:311 -#: src/domain/mutations/create-domain.js:321 +#: src/domain/mutations/create-domain.js:278 +#: src/domain/mutations/create-domain.js:286 msgid "Unable to create domain. Please try again." msgstr "Unable to create domain. Please try again." @@ -557,9 +608,9 @@ msgstr "Unable to create domain. Please try again." msgid "Unable to create domains. Please try again." msgstr "Unable to create domains. Please try again." -#: src/organization/mutations/create-organization.js:201 -#: src/organization/mutations/create-organization.js:224 -#: src/organization/mutations/create-organization.js:235 +#: src/organization/mutations/create-organization.js:182 +#: src/organization/mutations/create-organization.js:202 +#: src/organization/mutations/create-organization.js:211 msgid "Unable to create organization. Please try again." msgstr "Unable to create organization. Please try again." @@ -569,6 +620,11 @@ msgstr "Unable to create organization. Please try again." #~ msgid "Unable to dispatch one time scan. Please try again." #~ msgstr "Unable to dispatch one time scan. Please try again." +#: src/organization/objects/organization.js:139 +#: src/organization/objects/organization.js:149 +msgid "Unable to export organization. Please try again." +msgstr "Unable to export organization. Please try again." + #: src/domain/mutations/favourite-domain.js:94 msgid "Unable to favourite domain, user has already favourited it." msgstr "Unable to favourite domain, user has already favourited it." @@ -671,25 +727,35 @@ msgstr "Unable to find user affiliation(s). Please try again." msgid "Unable to find verified organization(s). Please try again." msgstr "Unable to find verified organization(s). Please try again." -#: src/affiliation/mutations/invite-user-to-org.js:95 +#: src/affiliation/mutations/invite-user-to-org.js:117 +#: src/affiliation/mutations/invite-user-to-org.js:127 +#: src/affiliation/mutations/invite-user-to-org.js:192 +msgid "Unable to invite user to organization. Please try again." +msgstr "Unable to invite user to organization. Please try again." + +#: src/affiliation/mutations/invite-user-to-org.js:204 +msgid "Unable to invite user to organization. User is already affiliated with organization." +msgstr "Unable to invite user to organization. User is already affiliated with organization." + +#: src/affiliation/mutations/invite-user-to-org.js:82 msgid "Unable to invite user to unknown organization." msgstr "Unable to invite user to unknown organization." -#: src/affiliation/mutations/invite-user-to-org.js:194 -#: src/affiliation/mutations/invite-user-to-org.js:209 +#: src/affiliation/mutations/invite-user-to-org.js:233 +#: src/affiliation/mutations/invite-user-to-org.js:252 msgid "Unable to invite user. Please try again." msgstr "Unable to invite user. Please try again." -#: src/affiliation/mutations/invite-user-to-org.js:81 +#: src/affiliation/mutations/invite-user-to-org.js:68 msgid "Unable to invite yourself to an org." msgstr "Unable to invite yourself to an org." -#: src/affiliation/mutations/leave-organization.js:197 -#: src/affiliation/mutations/leave-organization.js:215 -msgid "Unable to leave organization. Please try again." -msgstr "Unable to leave organization. Please try again." +#: src/affiliation/mutations/leave-organization.js:190 +#: src/affiliation/mutations/leave-organization.js:208 +#~ msgid "Unable to leave organization. Please try again." +#~ msgstr "Unable to leave organization. Please try again." -#: src/affiliation/mutations/leave-organization.js:51 +#: src/affiliation/mutations/leave-organization.js:48 msgid "Unable to leave undefined organization." msgstr "Unable to leave undefined organization." @@ -719,6 +785,10 @@ msgstr "Unable to load DKIM guidance tag(s). Please try again." #~ msgid "Unable to load DKIM scan(s). Please try again." #~ msgstr "Unable to load DKIM scan(s). Please try again." +#: src/summaries/queries/dkim-summary.js:12 +msgid "Unable to load DKIM summary. Please try again." +msgstr "Unable to load DKIM summary. Please try again." + #: src/dmarc-summaries/loaders/load-dmarc-failure-connections-by-sum-id.js:13 #: src/dmarc-summaries/loaders/load-dmarc-failure-connections-by-sum-id.js:141 #: src/dmarc-summaries/loaders/load-dmarc-failure-connections-by-sum-id.js:153 @@ -739,8 +809,8 @@ msgstr "Unable to load DMARC phase summary. Please try again." #~ msgid "Unable to load DMARC scan(s). Please try again." #~ msgstr "Unable to load DMARC scan(s). Please try again." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:477 -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:489 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:446 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:456 #: src/dmarc-summaries/loaders/load-dmarc-sum-edge-by-domain-id-period.js:20 #: src/dmarc-summaries/loaders/load-dmarc-sum-edge-by-domain-id-period.js:32 #: src/dmarc-summaries/loaders/load-yearly-dmarc-sum-edges.js:20 @@ -748,8 +818,12 @@ msgstr "Unable to load DMARC phase summary. Please try again." msgid "Unable to load DMARC summary data. Please try again." msgstr "Unable to load DMARC summary data. Please try again." -#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:173 -#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:185 +#: src/summaries/queries/dmarc-summary.js:12 +msgid "Unable to load DMARC summary. Please try again." +msgstr "Unable to load DMARC summary. Please try again." + +#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:154 +#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:164 msgid "Unable to load DNS scan(s). Please try again." msgstr "Unable to load DNS scan(s). Please try again." @@ -784,6 +858,10 @@ msgstr "Unable to load SPF guidance tag(s). Please try again." #~ msgid "Unable to load SPF scan(s). Please try again." #~ msgstr "Unable to load SPF scan(s). Please try again." +#: src/summaries/queries/spf-summary.js:12 +msgid "Unable to load SPF summary. Please try again." +msgstr "Unable to load SPF summary. Please try again." + #: src/guidance-tag/loaders/load-ssl-guidance-tags-connections.js:260 #: src/guidance-tag/loaders/load-ssl-guidance-tags-connections.js:272 msgid "Unable to load SSL guidance tag(s). Please try again." @@ -794,24 +872,28 @@ msgstr "Unable to load SSL guidance tag(s). Please try again." #~ msgid "Unable to load SSL scan(s). Please try again." #~ msgstr "Unable to load SSL scan(s). Please try again." -#: src/auth/check-user-belongs-to-org.js:22 +#: src/summaries/queries/ssl-summary.js:12 +msgid "Unable to load SSL summary. Please try again." +msgstr "Unable to load SSL summary. Please try again." + +#: src/auth/check-user-belongs-to-org.js:20 msgid "Unable to load affiliation information. Please try again." msgstr "Unable to load affiliation information. Please try again." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:266 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:259 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:449 msgid "Unable to load affiliation(s). Please try again." msgstr "Unable to load affiliation(s). Please try again." -#: src/organization/loaders/load-all-organization-domain-statuses.js:36 +#: src/organization/loaders/load-all-organization-domain-statuses.js:32 msgid "Unable to load all organization domain statuses. Please try again." msgstr "Unable to load all organization domain statuses. Please try again." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:547 -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:557 -#: src/domain/loaders/load-domain-connections-by-user-id.js:471 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:549 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:559 +#: src/domain/loaders/load-domain-connections-by-user-id.js:448 #: src/domain/loaders/load-domain-tags-by-org-id.js:27 -#: src/user/loaders/load-my-tracker-by-user-id.js:43 +#: src/user/loaders/load-my-tracker-by-user-id.js:44 msgid "Unable to load domain(s). Please try again." msgstr "Unable to load domain(s). Please try again." @@ -828,7 +910,7 @@ msgstr "Unable to load domain. Please try again." msgid "Unable to load full pass data. Please try again." msgstr "Unable to load full pass data. Please try again." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:340 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:318 msgid "Unable to load log(s). Please try again." msgstr "Unable to load log(s). Please try again." @@ -851,15 +933,15 @@ msgstr "Unable to load organization domain statuses. Please try again." #: src/organization/loaders/load-organization-by-slug.js:51 #: src/organization/loaders/load-organization-connections-by-domain-id.js:533 #: src/organization/loaders/load-organization-connections-by-domain-id.js:545 -#: src/organization/loaders/load-organization-connections-by-user-id.js:520 -#: src/organization/loaders/load-organization-connections-by-user-id.js:532 +#: src/organization/loaders/load-organization-connections-by-user-id.js:505 +#: src/organization/loaders/load-organization-connections-by-user-id.js:515 #: src/organization/loaders/load-web-check-connections-by-user-id.js:331 #: src/organization/loaders/load-web-check-connections-by-user-id.js:343 msgid "Unable to load organization(s). Please try again." msgstr "Unable to load organization(s). Please try again." -#: src/auth/check-org-owner.js:22 -#: src/auth/check-org-owner.js:34 +#: src/auth/check-org-owner.js:19 +#: src/auth/check-org-owner.js:27 msgid "Unable to load owner information. Please try again." msgstr "Unable to load owner information. Please try again." @@ -898,11 +980,15 @@ msgstr "Unable to load verified domain(s). Please try again." msgid "Unable to load verified organization(s). Please try again." msgstr "Unable to load verified organization(s). Please try again." -#: src/web-scan/loaders/load-web-connections-by-domain-id.js:173 -#: src/web-scan/loaders/load-web-connections-by-domain-id.js:185 -#: src/web-scan/loaders/load-web-scans-by-web-id.js:15 -#: src/web-scan/loaders/load-web-scans-by-web-id.js:33 -#: src/web-scan/loaders/load-web-scans-by-web-id.js:45 +#: src/summaries/queries/web-connections-summary.js:12 +msgid "Unable to load web connections summary. Please try again." +msgstr "Unable to load web connections summary. Please try again." + +#: src/web-scan/loaders/load-web-connections-by-domain-id.js:169 +#: src/web-scan/loaders/load-web-connections-by-domain-id.js:179 +#: src/web-scan/loaders/load-web-scans-by-web-id.js:9 +#: src/web-scan/loaders/load-web-scans-by-web-id.js:25 +#: src/web-scan/loaders/load-web-scans-by-web-id.js:35 msgid "Unable to load web scan(s). Please try again." msgstr "Unable to load web scan(s). Please try again." @@ -910,17 +996,17 @@ msgstr "Unable to load web scan(s). Please try again." msgid "Unable to load web summary. Please try again." msgstr "Unable to load web summary. Please try again." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:254 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:249 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:437 msgid "Unable to query affiliation(s). Please try again." msgstr "Unable to query affiliation(s). Please try again." -#: src/domain/loaders/load-domain-connections-by-user-id.js:461 -#: src/user/loaders/load-my-tracker-by-user-id.js:33 +#: src/domain/loaders/load-domain-connections-by-user-id.js:438 +#: src/user/loaders/load-my-tracker-by-user-id.js:34 msgid "Unable to query domain(s). Please try again." msgstr "Unable to query domain(s). Please try again." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:330 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:308 msgid "Unable to query log(s). Please try again." msgstr "Unable to query log(s). Please try again." @@ -938,34 +1024,31 @@ msgstr "Unable to query user(s). Please try again." msgid "Unable to refresh tokens, please sign in." msgstr "Unable to refresh tokens, please sign in." -#: src/affiliation/mutations/remove-user-from-org.js:124 +#: src/affiliation/mutations/remove-user-from-org.js:104 msgid "Unable to remove a user that already does not belong to this organization." msgstr "Unable to remove a user that already does not belong to this organization." -#: src/domain/mutations/remove-domain.js:87 +#: src/domain/mutations/remove-domain.js:80 msgid "Unable to remove domain from unknown organization." msgstr "Unable to remove domain from unknown organization." -#: src/domain/mutations/remove-domain.js:148 +#: src/domain/mutations/remove-domain.js:136 msgid "Unable to remove domain. Domain is not part of organization." msgstr "Unable to remove domain. Domain is not part of organization." -#: src/domain/mutations/remove-domain.js:134 -#: src/domain/mutations/remove-domain.js:165 -#: src/domain/mutations/remove-domain.js:198 -#: src/domain/mutations/remove-domain.js:217 -#: src/domain/mutations/remove-domain.js:243 -#: src/domain/mutations/remove-domain.js:260 -#: src/domain/mutations/remove-domain.js:278 -#: src/domain/mutations/remove-domain.js:296 -#: src/domain/mutations/remove-domain.js:314 -#: src/domain/mutations/remove-domain.js:331 -#: src/domain/mutations/remove-domain.js:354 -#: src/domain/mutations/remove-domain.js:365 +#: src/domain/mutations/remove-domain.js:123 +#: src/domain/mutations/remove-domain.js:152 +#: src/domain/mutations/remove-domain.js:185 +#: src/domain/mutations/remove-domain.js:204 +#: src/domain/mutations/remove-domain.js:230 +#: src/domain/mutations/remove-domain.js:248 +#: src/domain/mutations/remove-domain.js:265 +#: src/domain/mutations/remove-domain.js:288 +#: src/domain/mutations/remove-domain.js:299 msgid "Unable to remove domain. Please try again." msgstr "Unable to remove domain. Please try again." -#: src/domain/mutations/remove-organizations-domains.js:92 +#: src/domain/mutations/remove-organizations-domains.js:90 msgid "Unable to remove domains from unknown organization." msgstr "Unable to remove domains from unknown organization." @@ -975,11 +1058,11 @@ msgstr "Unable to remove domains from unknown organization." #: src/organization/mutations/remove-organization.js:168 #: src/organization/mutations/remove-organization.js:200 #: src/organization/mutations/remove-organization.js:212 -#: src/organization/mutations/remove-organization.js:251 -#: src/organization/mutations/remove-organization.js:373 -#: src/organization/mutations/remove-organization.js:406 -#: src/organization/mutations/remove-organization.js:462 -#: src/organization/mutations/remove-organization.js:473 +#: src/organization/mutations/remove-organization.js:237 +#: src/organization/mutations/remove-organization.js:255 +#: src/organization/mutations/remove-organization.js:288 +#: src/organization/mutations/remove-organization.js:344 +#: src/organization/mutations/remove-organization.js:355 msgid "Unable to remove organization. Please try again." msgstr "Unable to remove organization. Please try again." @@ -988,7 +1071,7 @@ msgstr "Unable to remove organization. Please try again." msgid "Unable to remove phone number. Please try again." msgstr "Unable to remove phone number. Please try again." -#: src/domain/mutations/remove-domain.js:71 +#: src/domain/mutations/remove-domain.js:65 msgid "Unable to remove unknown domain." msgstr "Unable to remove unknown domain." @@ -996,29 +1079,57 @@ msgstr "Unable to remove unknown domain." msgid "Unable to remove unknown organization." msgstr "Unable to remove unknown organization." -#: src/affiliation/mutations/remove-user-from-org.js:91 +#: src/affiliation/mutations/remove-user-from-org.js:77 msgid "Unable to remove unknown user from organization." msgstr "Unable to remove unknown user from organization." -#: src/affiliation/mutations/remove-user-from-org.js:77 -msgid "Unable to remove user from organization." -msgstr "Unable to remove user from organization." +#: src/affiliation/mutations/remove-user-from-org.js:74 +#~ msgid "Unable to remove user from organization." +#~ msgstr "Unable to remove user from organization." -#: src/affiliation/mutations/remove-user-from-org.js:111 -#: src/affiliation/mutations/remove-user-from-org.js:138 -#: src/affiliation/mutations/remove-user-from-org.js:176 -#: src/affiliation/mutations/remove-user-from-org.js:189 +#: src/affiliation/mutations/remove-user-from-org.js:94 +#: src/affiliation/mutations/remove-user-from-org.js:115 +#: src/affiliation/mutations/remove-user-from-org.js:160 +#: src/affiliation/mutations/remove-user-from-org.js:169 msgid "Unable to remove user from this organization. Please try again." msgstr "Unable to remove user from this organization. Please try again." -#: src/affiliation/mutations/remove-user-from-org.js:63 +#: src/affiliation/mutations/remove-user-from-org.js:61 msgid "Unable to remove user from unknown organization." msgstr "Unable to remove user from unknown organization." -#: src/domain/mutations/request-scan.js:57 +#: src/domain/mutations/request-scan.js:117 +msgid "Unable to request a one time scan on a domain that already has a pending scan." +msgstr "Unable to request a one time scan on a domain that already has a pending scan." + +#: src/domain/mutations/request-scan.js:52 msgid "Unable to request a one time scan on an unknown domain." msgstr "Unable to request a one time scan on an unknown domain." +#: src/domain/mutations/request-scan.js:125 +msgid "Unable to request a one time scan. Please try again." +msgstr "Unable to request a one time scan. Please try again." + +#: src/affiliation/mutations/request-org-affiliation.js:95 +msgid "Unable to request invite to organization with which you are already affiliated." +msgstr "Unable to request invite to organization with which you are already affiliated." + +#: src/affiliation/mutations/request-org-affiliation.js:85 +msgid "Unable to request invite to organization with which you have already requested to join." +msgstr "Unable to request invite to organization with which you have already requested to join." + +#: src/affiliation/mutations/request-org-affiliation.js:56 +msgid "Unable to request invite to unknown organization." +msgstr "Unable to request invite to unknown organization." + +#: src/affiliation/mutations/request-org-affiliation.js:72 +#: src/affiliation/mutations/request-org-affiliation.js:120 +#: src/affiliation/mutations/request-org-affiliation.js:136 +#: src/affiliation/mutations/request-org-affiliation.js:146 +#: src/affiliation/mutations/request-org-affiliation.js:165 +msgid "Unable to request invite. Please try again." +msgstr "Unable to request invite. Please try again." + #: src/user/mutations/reset-password.js:95 msgid "Unable to reset password. Please request a new email." msgstr "Unable to reset password. Please request a new email." @@ -1029,8 +1140,8 @@ msgstr "Unable to reset password. Please request a new email." msgid "Unable to reset password. Please try again." msgstr "Unable to reset password. Please try again." -#: src/domain/objects/domain.js:167 -#: src/domain/objects/domain.js:213 +#: src/domain/objects/domain.js:236 +#: src/domain/objects/domain.js:271 msgid "Unable to retrieve DMARC report information for: {domain}" msgstr "Unable to retrieve DMARC report information for: {domain}" @@ -1047,11 +1158,15 @@ msgstr "Unable to send authentication email. Please try again." msgid "Unable to send authentication text message. Please try again." msgstr "Unable to send authentication text message. Please try again." -#: src/notify/notify-send-org-invite-create-account.js:31 +#: src/notify/notify-send-org-invite-create-account.js:19 #: src/notify/notify-send-org-invite-email.js:26 msgid "Unable to send org invite email. Please try again." msgstr "Unable to send org invite email. Please try again." +#: src/notify/notify-send-invite-request-email.js:23 +msgid "Unable to send org invite request email. Please try again." +msgstr "Unable to send org invite request email. Please try again." + #: src/notify/notify-send-password-reset-email.js:30 msgid "Unable to send password reset email. Please try again." msgstr "Unable to send password reset email. Please try again." @@ -1079,38 +1194,38 @@ msgstr "Unable to set phone number, please try again." msgid "Unable to sign in, please try again." msgstr "Unable to sign in, please try again." -#: src/user/mutations/sign-up.js:200 -#: src/user/mutations/sign-up.js:214 +#: src/user/mutations/sign-up.js:183 +#: src/user/mutations/sign-up.js:193 msgid "Unable to sign up, please contact org admin for a new invite." msgstr "Unable to sign up, please contact org admin for a new invite." -#: src/user/mutations/sign-up.js:168 -#: src/user/mutations/sign-up.js:178 -#: src/user/mutations/sign-up.js:236 -#: src/user/mutations/sign-up.js:246 +#: src/user/mutations/sign-up.js:156 +#: src/user/mutations/sign-up.js:164 +#: src/user/mutations/sign-up.js:213 +#: src/user/mutations/sign-up.js:221 msgid "Unable to sign up. Please try again." msgstr "Unable to sign up. Please try again." -#: src/affiliation/mutations/transfer-org-ownership.js:131 -#: src/affiliation/mutations/transfer-org-ownership.js:172 -#: src/affiliation/mutations/transfer-org-ownership.js:196 -#: src/affiliation/mutations/transfer-org-ownership.js:208 +#: src/affiliation/mutations/transfer-org-ownership.js:106 +#: src/affiliation/mutations/transfer-org-ownership.js:145 +#: src/affiliation/mutations/transfer-org-ownership.js:167 +#: src/affiliation/mutations/transfer-org-ownership.js:177 msgid "Unable to transfer organization ownership. Please try again." msgstr "Unable to transfer organization ownership. Please try again." -#: src/affiliation/mutations/transfer-org-ownership.js:78 -msgid "Unable to transfer ownership of a verified organization." -msgstr "Unable to transfer ownership of a verified organization." +#: src/affiliation/mutations/transfer-org-ownership.js:68 +#~ msgid "Unable to transfer ownership of a verified organization." +#~ msgstr "Unable to transfer ownership of a verified organization." -#: src/affiliation/mutations/transfer-org-ownership.js:112 +#: src/affiliation/mutations/transfer-org-ownership.js:89 msgid "Unable to transfer ownership of an org to an undefined user." msgstr "Unable to transfer ownership of an org to an undefined user." -#: src/affiliation/mutations/transfer-org-ownership.js:65 +#: src/affiliation/mutations/transfer-org-ownership.js:59 msgid "Unable to transfer ownership of undefined organization." msgstr "Unable to transfer ownership of undefined organization." -#: src/affiliation/mutations/transfer-org-ownership.js:144 +#: src/affiliation/mutations/transfer-org-ownership.js:118 msgid "Unable to transfer ownership to a user outside the org. Please invite the user and try again." msgstr "Unable to transfer ownership to a user outside the org. Please invite the user and try again." @@ -1132,21 +1247,21 @@ msgstr "Unable to unfavourite domain. Please try again." msgid "Unable to unfavourite unknown domain." msgstr "Unable to unfavourite unknown domain." -#: src/domain/mutations/update-domain.js:256 +#: src/domain/mutations/update-domain.js:237 msgid "Unable to update domain edge. Please try again." msgstr "Unable to update domain edge. Please try again." -#: src/domain/mutations/update-domain.js:131 +#: src/domain/mutations/update-domain.js:125 msgid "Unable to update domain in an unknown org." msgstr "Unable to update domain in an unknown org." -#: src/domain/mutations/update-domain.js:179 +#: src/domain/mutations/update-domain.js:166 msgid "Unable to update domain that does not belong to the given organization." msgstr "Unable to update domain that does not belong to the given organization." -#: src/domain/mutations/update-domain.js:168 -#: src/domain/mutations/update-domain.js:210 -#: src/domain/mutations/update-domain.js:267 +#: src/domain/mutations/update-domain.js:156 +#: src/domain/mutations/update-domain.js:196 +#: src/domain/mutations/update-domain.js:247 msgid "Unable to update domain. Please try again." msgstr "Unable to update domain. Please try again." @@ -1175,24 +1290,24 @@ msgstr "Unable to update password, passwords do not match requirements. Please t msgid "Unable to update password. Please try again." msgstr "Unable to update password. Please try again." -#: src/user/mutations/update-user-profile.js:172 -#: src/user/mutations/update-user-profile.js:181 +#: src/user/mutations/update-user-profile.js:160 +#: src/user/mutations/update-user-profile.js:167 msgid "Unable to update profile. Please try again." msgstr "Unable to update profile. Please try again." -#: src/affiliation/mutations/update-user-role.js:99 +#: src/affiliation/mutations/update-user-role.js:94 msgid "Unable to update role: organization unknown." msgstr "Unable to update role: organization unknown." -#: src/affiliation/mutations/update-user-role.js:145 +#: src/affiliation/mutations/update-user-role.js:137 msgid "Unable to update role: user does not belong to organization." msgstr "Unable to update role: user does not belong to organization." -#: src/affiliation/mutations/update-user-role.js:85 +#: src/affiliation/mutations/update-user-role.js:80 msgid "Unable to update role: user unknown." msgstr "Unable to update role: user unknown." -#: src/domain/mutations/update-domain.js:117 +#: src/domain/mutations/update-domain.js:111 msgid "Unable to update unknown domain." msgstr "Unable to update unknown domain." @@ -1200,14 +1315,14 @@ msgstr "Unable to update unknown domain." msgid "Unable to update unknown organization." msgstr "Unable to update unknown organization." -#: src/affiliation/mutations/update-user-role.js:133 -#: src/affiliation/mutations/update-user-role.js:158 -#: src/affiliation/mutations/update-user-role.js:247 -#: src/affiliation/mutations/update-user-role.js:258 +#: src/affiliation/mutations/update-user-role.js:127 +#: src/affiliation/mutations/update-user-role.js:149 +#: src/affiliation/mutations/update-user-role.js:201 +#: src/affiliation/mutations/update-user-role.js:211 msgid "Unable to update user's role. Please try again." msgstr "Unable to update user's role. Please try again." -#: src/affiliation/mutations/update-user-role.js:71 +#: src/affiliation/mutations/update-user-role.js:66 msgid "Unable to update your own role." msgstr "Unable to update your own role." @@ -1225,18 +1340,17 @@ msgstr "Unable to verify account. Please try again." msgid "Unable to verify if user is a super admin, please try again." msgstr "Unable to verify if user is a super admin, please try again." -#: src/user/mutations/update-user-profile.js:103 +#: src/user/mutations/update-user-profile.js:100 #: src/user/queries/is-user-admin.js:59 msgid "Unable to verify if user is an admin, please try again." msgstr "Unable to verify if user is an admin, please try again." -#: src/organization/mutations/verify-organization.js:109 -#: src/organization/mutations/verify-organization.js:131 -#: src/organization/mutations/verify-organization.js:142 +#: src/organization/mutations/verify-organization.js:105 +#: src/organization/mutations/verify-organization.js:112 msgid "Unable to verify organization. Please try again." msgstr "Unable to verify organization. Please try again." -#: src/organization/mutations/verify-organization.js:54 +#: src/organization/mutations/verify-organization.js:53 msgid "Unable to verify unknown organization." msgstr "Unable to verify unknown organization." @@ -1244,7 +1358,7 @@ msgstr "Unable to verify unknown organization." msgid "User could not be queried." msgstr "User could not be queried." -#: src/affiliation/mutations/update-user-role.js:294 +#: src/affiliation/mutations/update-user-role.js:244 msgid "User role was updated successfully." msgstr "User role was updated successfully." @@ -1260,7 +1374,7 @@ msgstr "Verification error. Please activate multi-factor authentication to acces msgid "Verification error. Please verify your account via email to access content." msgstr "Verification error. Please verify your account via email to access content." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:71 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:70 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:161 msgid "You must provide a `first` or `last` value to properly paginate the `Affiliation` connection." msgstr "You must provide a `first` or `last` value to properly paginate the `Affiliation` connection." @@ -1285,12 +1399,12 @@ msgstr "You must provide a `first` or `last` value to properly paginate the `Dki msgid "You must provide a `first` or `last` value to properly paginate the `DmarcFailureTable` connection." msgstr "You must provide a `first` or `last` value to properly paginate the `DmarcFailureTable` connection." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:189 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:162 msgid "You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection." msgstr "You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:154 -#: src/domain/loaders/load-domain-connections-by-user-id.js:166 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:141 +#: src/domain/loaders/load-domain-connections-by-user-id.js:147 msgid "You must provide a `first` or `last` value to properly paginate the `Domain` connection." msgstr "You must provide a `first` or `last` value to properly paginate the `Domain` connection." @@ -1311,12 +1425,12 @@ msgstr "You must provide a `first` or `last` value to properly paginate the `Gui #~ msgid "You must provide a `first` or `last` value to properly paginate the `HTTPS` connection." #~ msgstr "You must provide a `first` or `last` value to properly paginate the `HTTPS` connection." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:101 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:100 msgid "You must provide a `first` or `last` value to properly paginate the `Log` connection." msgstr "You must provide a `first` or `last` value to properly paginate the `Log` connection." #: src/organization/loaders/load-organization-connections-by-domain-id.js:183 -#: src/organization/loaders/load-organization-connections-by-user-id.js:182 +#: src/organization/loaders/load-organization-connections-by-user-id.js:166 #: src/organization/loaders/load-web-check-connections-by-user-id.js:89 msgid "You must provide a `first` or `last` value to properly paginate the `Organization` connection." msgstr "You must provide a `first` or `last` value to properly paginate the `Organization` connection." @@ -1347,52 +1461,48 @@ msgstr "You must provide a `first` or `last` value to properly paginate the `Ver msgid "You must provide a `first` or `last` value to properly paginate the `VerifiedOrganization` connection." msgstr "You must provide a `first` or `last` value to properly paginate the `VerifiedOrganization` connection." -#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:33 +#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:16 msgid "You must provide a `limit` value in the range of 1-100 to properly paginate the `DNS` connection." msgstr "You must provide a `limit` value in the range of 1-100 to properly paginate the `DNS` connection." -#: src/web-scan/loaders/load-web-connections-by-domain-id.js:33 +#: src/web-scan/loaders/load-web-connections-by-domain-id.js:16 msgid "You must provide a `limit` value in the range of 1-100 to properly paginate the `web` connection." msgstr "You must provide a `limit` value in the range of 1-100 to properly paginate the `web` connection." -#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:22 +#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:9 msgid "You must provide a `limit` value to properly paginate the `DNS` connection." msgstr "You must provide a `limit` value to properly paginate the `DNS` connection." -#: src/web-scan/loaders/load-web-connections-by-domain-id.js:22 +#: src/web-scan/loaders/load-web-connections-by-domain-id.js:9 msgid "You must provide a `limit` value to properly paginate the `web` connection." msgstr "You must provide a `limit` value to properly paginate the `web` connection." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:31 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:12 msgid "You must provide a `period` value to access the `DmarcSummaries` connection." msgstr "You must provide a `period` value to access the `DmarcSummaries` connection." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:43 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:18 msgid "You must provide a `year` value to access the `DmarcSummaries` connection." msgstr "You must provide a `year` value to access the `DmarcSummaries` connection." -#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:49 +#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:30 msgid "You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `DNS` connection." msgstr "You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `DNS` connection." -#: src/web-scan/loaders/load-web-connections-by-domain-id.js:49 +#: src/web-scan/loaders/load-web-connections-by-domain-id.js:30 msgid "You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `web` connection." msgstr "You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `web` connection." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:118 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:109 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:208 -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:148 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:131 #: src/dmarc-summaries/loaders/load-dkim-failure-connections-by-sum-id.js:83 #: src/dmarc-summaries/loaders/load-dmarc-failure-connections-by-sum-id.js:83 -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:236 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:202 #: src/dmarc-summaries/loaders/load-full-pass-connections-by-sum-id.js:83 #: src/dmarc-summaries/loaders/load-spf-failure-connections-by-sum-id.js:83 -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:201 -#: src/domain/loaders/load-domain-connections-by-user-id.js:213 -#: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:170 -#: src/email-scan/loaders/load-dkim-results-connections-by-dkim-id.js:136 -#: src/email-scan/loaders/load-dmarc-connections-by-domain-id.js:186 -#: src/email-scan/loaders/load-spf-connections-by-domain-id.js:180 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:178 +#: src/domain/loaders/load-domain-connections-by-user-id.js:186 #: src/guidance-tag/loaders/load-aggregate-guidance-tags-connections.js:132 #: src/guidance-tag/loaders/load-dkim-guidance-tags-connections.js:136 #: src/guidance-tag/loaders/load-dmarc-guidance-tags-connections.js:136 @@ -1400,7 +1510,7 @@ msgstr "You must provide at most one pagination method (`before`, `after`, `offs #: src/guidance-tag/loaders/load-spf-guidance-tags-connections.js:136 #: src/guidance-tag/loaders/load-ssl-guidance-tags-connections.js:136 #: src/organization/loaders/load-organization-connections-by-domain-id.js:230 -#: src/organization/loaders/load-organization-connections-by-user-id.js:229 +#: src/organization/loaders/load-organization-connections-by-user-id.js:201 #: src/organization/loaders/load-web-check-connections-by-user-id.js:136 #: src/user/loaders/load-user-connections-by-user-id.js:154 #: src/verified-domains/loaders/load-verified-domain-connections-by-organization-id.js:164 @@ -1410,7 +1520,7 @@ msgstr "You must provide at most one pagination method (`before`, `after`, `offs msgid "`{argSet}` must be of type `number` not `{typeSet}`." msgstr "`{argSet}` must be of type `number` not `{typeSet}`." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:92 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:86 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:182 msgid "`{argSet}` on the `Affiliation` connection cannot be less than zero." msgstr "`{argSet}` on the `Affiliation` connection cannot be less than zero." @@ -1435,12 +1545,12 @@ msgstr "`{argSet}` on the `DkimFailureTable` connection cannot be less than zero msgid "`{argSet}` on the `DmarcFailureTable` connection cannot be less than zero." msgstr "`{argSet}` on the `DmarcFailureTable` connection cannot be less than zero." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:210 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:179 msgid "`{argSet}` on the `DmarcSummaries` connection cannot be less than zero." msgstr "`{argSet}` on the `DmarcSummaries` connection cannot be less than zero." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:175 -#: src/domain/loaders/load-domain-connections-by-user-id.js:187 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:157 +#: src/domain/loaders/load-domain-connections-by-user-id.js:163 msgid "`{argSet}` on the `Domain` connection cannot be less than zero." msgstr "`{argSet}` on the `Domain` connection cannot be less than zero." @@ -1461,12 +1571,12 @@ msgstr "`{argSet}` on the `GuidanceTag` connection cannot be less than zero." #~ msgid "`{argSet}` on the `HTTPS` connection cannot be less than zero." #~ msgstr "`{argSet}` on the `HTTPS` connection cannot be less than zero." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:122 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:112 msgid "`{argSet}` on the `Log` connection cannot be less than zero." msgstr "`{argSet}` on the `Log` connection cannot be less than zero." #: src/organization/loaders/load-organization-connections-by-domain-id.js:204 -#: src/organization/loaders/load-organization-connections-by-user-id.js:203 +#: src/organization/loaders/load-organization-connections-by-user-id.js:180 #: src/organization/loaders/load-web-check-connections-by-user-id.js:110 msgid "`{argSet}` on the `Organization` connection cannot be less than zero." msgstr "`{argSet}` on the `Organization` connection cannot be less than zero." diff --git a/api/src/locale/fr/messages.js b/api/src/locale/fr/messages.js index adc866c9e2..d77e0afa4a 100644 --- a/api/src/locale/fr/messages.js +++ b/api/src/locale/fr/messages.js @@ -1 +1 @@ -/*eslint-disable*/module.exports={messages:{"Authentication error. Please sign in.":"Erreur d'authentification. Veuillez vous connecter.","Cannot query affiliations on organization without admin permission or higher.":"Impossible d'interroger les affiliations sur l'organisation sans l'autorisation de l'administrateur ou plus.","Email already in use.":"Courriel déjà utilisé.","If an account with this username is found, a password reset link will be found in your inbox.":"Si un compte avec ce nom d'utilisateur est trouvé, un lien de réinitialisation du mot de passe se trouvera dans votre boîte de réception.","If an account with this username is found, an email verification link will be found in your inbox.":"Si un compte avec ce nom d'utilisateur est trouvé, un lien de vérification par e-mail sera trouvé dans votre boîte de réception.","Incorrect TFA code. Please sign in again.":"Code TFA incorrect. Veuillez vous reconnecter.","Incorrect token value. Please request a new email.":"La valeur du jeton est incorrecte. Veuillez demander un nouvel e-mail.","Incorrect username or password. Please try again.":"Le nom d'utilisateur ou le mot de passe est incorrect. Veuillez réessayer.","Invalid token, please sign in.":"Jeton invalide, veuillez vous connecter.","New passwords do not match.":"Les nouveaux mots de passe ne correspondent pas.","No organization with the provided slug could be found.":"Aucune organisation avec le slug fourni n'a pu être trouvée.","No verified domain with the provided domain could be found.":"Aucun domaine vérifié avec le domaine fourni n'a pu être trouvé.","No verified organization with the provided slug could be found.":"Aucune organisation vérifiée avec le slug fourni n'a pu être trouvée.","Organization has already been verified.":"L'organisation a déjà été vérifiée.","Organization name already in use, please choose another and try again.":"Le nom de l'organisation est déjà utilisé, veuillez en choisir un autre et réessayer.","Organization name already in use. Please try again with a different name.":"Le nom de l'organisation est déjà utilisé. Veuillez réessayer avec un nom différent.","Ownership check error. Unable to request domain information.":"Erreur de vérification de la propriété. Impossible de demander des informations sur le domaine.","Passing both `first` and `last` to paginate the `Affiliation` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `Affiliation` n'est pas supporté.","Passing both `first` and `last` to paginate the `DKIMResults` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DKIMResults` n'est pas supporté.","Passing both `first` and `last` to paginate the `DKIM` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DKIMResults` n'est pas supporté.","Passing both `first` and `last` to paginate the `DMARC` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DMARC` n'est pas supporté.","Passing both `first` and `last` to paginate the `DkimFailureTable` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DkimFailureTable` n'est pas supporté.","Passing both `first` and `last` to paginate the `DmarcFailureTable` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DmarcFailureTable` n'est pas supporté.","Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DmarcSummaries` n'est pas supporté.","Passing both `first` and `last` to paginate the `Domain` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `Domain` n'est pas supporté.","Passing both `first` and `last` to paginate the `FullPassTable` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `FullPassTable` n'est pas supporté.","Passing both `first` and `last` to paginate the `GuidanceTag` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `GuidanceTag` n'est pas supporté.","Passing both `first` and `last` to paginate the `HTTPS` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `HTTPS` n'est pas supporté.","Passing both `first` and `last` to paginate the `Organization` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `Organization` n'est pas supporté.","Passing both `first` and `last` to paginate the `SPF` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `SPF` n'est pas supporté.","Passing both `first` and `last` to paginate the `SSL` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `SSL` n'est pas supporté.","Passing both `first` and `last` to paginate the `SpfFailureTable` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `SpfFailureTable` n'est pas supporté.","Passing both `first` and `last` to paginate the `User` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `User` n'est pas supporté.","Passing both `first` and `last` to paginate the `VerifiedDomain` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `VerifiedDomain` n'est pas supporté.","Passing both `first` and `last` to paginate the `VerifiedOrganization` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `VerifiedOrganization` n'est pas supporté.","Password does not meet requirements.":"Le mot de passe ne répond pas aux exigences.","Password was successfully reset.":"Le mot de passe a été réinitialisé avec succès.","Password was successfully updated.":"Le mot de passe a été mis à jour avec succès.","Passwords do not match.":"Les mots de passe ne correspondent pas.","Permission Denied: Could not retrieve specified organization.":"Permission refusée : Impossible de récupérer l'organisation spécifiée.","Permission Denied: Multi-factor authentication is required for admin accounts":"Permission refusée : L'authentification multifactorielle est requise pour les comptes admin.","Permission Denied: Please contact org owner to transfer ownership.":"Permission refusée : Veuillez contacter le propriétaire de l'org pour transférer la propriété.","Permission Denied: Please contact organization admin for help with removing domain.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide afin de supprimer le domaine.","Permission Denied: Please contact organization admin for help with removing organization.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide afin de supprimer l'organisation.","Permission Denied: Please contact organization admin for help with removing users.":"Autorisation refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des utilisateurs.","Permission Denied: Please contact organization admin for help with updating organization.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des utilisateurs.","Permission Denied: Please contact organization admin for help with updating user roles.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la mise à jour des rôles des utilisateurs.","Permission Denied: Please contact organization admin for help with user invitations.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide concernant les invitations d'utilisateurs.","Permission Denied: Please contact organization admin for help with user role changes.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur les changements de rôle des utilisateurs.","Permission Denied: Please contact organization user for help with creating domain.":"Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la création du domaine.","Permission Denied: Please contact organization user for help with retrieving this domain.":"Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide pour récupérer ce domaine.","Permission Denied: Please contact organization user for help with scanning this domain.":"Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur l'analyse de ce domaine.","Permission Denied: Please contact organization user for help with updating this domain.":"Autorisation refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la mise à jour de ce domaine.","Permission Denied: Please contact super admin for help with removing domain.":"Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la mise à jour de ce domaine.","Permission Denied: Please contact super admin for help with removing organization.":"Permission refusée : Veuillez contacter le super administrateur pour qu'il vous aide à supprimer l'organisation.","Permission Denied: Please contact super admin for help with verifying this organization.":"Permission refusée : Veuillez contacter le super administrateur pour qu'il vous aide à vérifier cette organisation.","Permission check error. Unable to request domain information.":"Erreur de vérification des permissions. Impossible de demander des informations sur le domaine.","Permission error, not an admin for this user.":"Erreur de permission, pas d'administrateur pour cet utilisateur.","Permission error: Unable to close other user's account.":"Erreur de permission: Impossible de fermer le compte d'un autre utilisateur.","Permissions error. You do not have sufficient permissions to access this data.":"Erreur de permissions. Vous n'avez pas les autorisations suffisantes pour accéder à ces données.","Phone number has been successfully removed.":"Le numéro de téléphone a été supprimé avec succès.","Phone number has been successfully set, you will receive a verification text message shortly.":"Le numéro de téléphone a été configuré avec succès, vous recevrez bientôt un message de vérification.","Profile successfully updated.":"Le profil a été mis à jour avec succès.","Requesting `{amount}` records on the `Affiliation` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `Affiliation` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `DkimFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `DkimFailureTable` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `DmarcFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `DkimFailureTable` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `DmarcSummaries` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `DmarcSummaries` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `Domain` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `Domain` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `FullPassTable` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `FullPassTable` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `GuidanceTag` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `GuidanceTag` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `Organization` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `Organization` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `SpfFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `SpfFailureTable` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `User` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `User` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `VerifiedDomain` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `VerifiedDomain` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `VerifiedOrganization` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `VerifiedOrganization` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `DKIMResults` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `DKIMResults` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `DKIM` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `DKIM` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `DMARC` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `DMARC` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `HTTPS` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `HTTPS` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `SPF` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `SPF` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `SSL` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Successfully closed account.":"Le compte a été fermé avec succès.","Successfully dispatched one time scan.":"Un seul balayage a été effectué avec succès.","Successfully email verified account, and set TFA send method to email.":"Réussir à envoyer un email au compte vérifié, et définir la méthode d'envoi de la TFA sur email.","Successfully invited user to organization, and sent notification email.":"L'utilisateur a été invité avec succès à l'organisation et l'email de notification a été envoyé.","Successfully left organization: {0}":["L'organisation a été quittée avec succès: ",["0"]],"Successfully removed domain: {0} from {1}.":["A réussi à supprimer le domaine : ",["0"]," de ",["1"],"."],"Successfully removed organization: {0}.":["A réussi à supprimer l'organisation : ",["0"],"."],"Successfully removed user from organization.":"L'utilisateur a été retiré de l'organisation avec succès.","Successfully sent invitation to service, and organization email.":"Envoi réussi de l'invitation au service, et de l'email de l'organisation.","Successfully signed out.":"J'ai réussi à me déconnecter.","Successfully transferred org: {0} ownership to user: {1}":["A réussi à transférer la propriété de org: ",["0"]," à l'utilisateur: ",["1"]],"Successfully verified organization: {0}.":"Envoi réussi de l'invitation au service, et de l'email de l'organisation.","Successfully verified phone number, and set TFA send method to text.":"Le numéro de téléphone a été vérifié avec succès, et la méthode d'envoi de la TFA a été réglée sur le texte.","Token value incorrect, please sign in again.":"La valeur du jeton est incorrecte, veuillez vous connecter à nouveau.","Too many failed login attempts, please reset your password, and try again.":"Trop de tentatives de connexion ont échoué, veuillez réinitialiser votre mot de passe et réessayer.","Two factor code is incorrect. Please try again.":"Le code à deux facteurs est incorrect. Veuillez réessayer.","Two factor code length is incorrect. Please try again.":"La longueur du code à deux facteurs est incorrecte. Veuillez réessayer.","Unable leave organization. Please try again.":"Impossible de quitter l'organisation. Veuillez réessayer.","Unable to authenticate. Please try again.":"Impossible de s'authentifier. Veuillez réessayer.","Unable to check permission. Please try again.":"Impossible de vérifier l'autorisation. Veuillez réessayer.","Unable to close account of an undefined user.":"Impossible de fermer le compte d'un utilisateur non défini.","Unable to close account. Please try again.":"Impossible de fermer le compte. Veuillez réessayer.","Unable to create domain in unknown organization.":"Impossible de créer un domaine dans une organisation inconnue.","Unable to create domain, organization has already claimed it.":"Impossible de créer le domaine, l'organisation l'a déjà réclamé.","Unable to create domain. Please try again.":"Impossible de créer un domaine. Veuillez réessayer.","Unable to create organization. Please try again.":"Impossible de créer une organisation. Veuillez réessayer.","Unable to dispatch one time scan. Please try again.":"Impossible d'envoyer un scan unique. Veuillez réessayer.","Unable to find Aggregate guidance tag(s). Please try again.":"Impossible de trouver le(s) tag(s) d'orientation des agrégats. Veuillez réessayer.","Unable to find DKIM guidance tag(s). Please try again.":"Impossible de trouver le(s) tag(s) d'orientation DKIM. Veuillez réessayer.","Unable to find DKIM result(s). Please try again.":"Impossible de trouver le(s) résultat(s) DKIM. Veuillez réessayer.","Unable to find DKIM scan(s). Please try again.":"Impossible de trouver le(s) scan(s) DKIM. Veuillez réessayer.","Unable to find DMARC guidance tag(s). Please try again.":"Impossible de trouver le(s) tag(s) d'orientation DMARC. Veuillez réessayer.","Unable to find DMARC scan(s). Please try again.":"Impossible de trouver le(s) scan(s) DMARC. Veuillez réessayer.","Unable to find DMARC summary data. Please try again.":"Impossible de trouver les données de synthèse DMARC. Veuillez réessayer.","Unable to find DNS scan(s). Please try again.":"Impossible de trouver le(s) scan(s) DNS. Veuillez réessayer.","Unable to find HTTPS guidance tag(s). Please try again.":"Impossible de trouver la ou les balises d'orientation HTTPS. Veuillez réessayer.","Unable to find HTTPS scan(s). Please try again.":"Impossible de trouver le(s) scan(s) HTTPS. Veuillez réessayer.","Unable to find SPF guidance tag(s). Please try again.":"Impossible de trouver le(s) tag(s) d'orientation SPF. Veuillez réessayer.","Unable to find SPF scan(s). Please try again.":"Impossible de trouver le(s) scan(s) SPF. Veuillez réessayer.","Unable to find SSL guidance tag(s). Please try again.":"Impossible de trouver le(s) tag(s) d'orientation SSL. Veuillez réessayer.","Unable to find SSL scan(s). Please try again.":"Impossible de trouver le(s) scan(s) SSL. Veuillez réessayer.","Unable to find the requested domain.":"Impossible de trouver le domaine demandé.","Unable to find user affiliation(s). Please try again.":"Impossible de trouver l'affiliation de l'utilisateur (s). Veuillez réessayer.","Unable to find verified organization(s). Please try again.":"Impossible de trouver une ou plusieurs organisations vérifiées. Veuillez réessayer.","Unable to invite user to unknown organization.":"Impossible d'inviter un utilisateur à une organisation inconnue.","Unable to invite user. Please try again.":"Impossible d'inviter un utilisateur. Veuillez réessayer.","Unable to invite yourself to an org.":"Impossible de s'inviter à un org.","Unable to leave organization. Please try again.":"Impossible de quitter l'organisation. Veuillez réessayer.","Unable to leave undefined organization.":"Impossible de quitter une organisation non définie.","Unable to load Aggregate guidance tag(s). Please try again.":"Impossible de charger le(s) tag(s) d'orientation des agrégats. Veuillez réessayer.","Unable to load DKIM failure data. Please try again.":"Impossible de charger les données d'échec DKIM. Veuillez réessayer.","Unable to load DKIM guidance tag(s). Please try again.":"Impossible de charger le(s) tag(s) d'orientation DKIM. Veuillez réessayer.","Unable to load DKIM result(s). Please try again.":"Impossible de charger le(s) résultat(s) DKIM. Veuillez réessayer.","Unable to load DKIM scan(s). Please try again.":"Impossible de charger le(s) scan(s) DKIM. Veuillez réessayer.","Unable to load DMARC failure data. Please try again.":"Impossible de charger les données d'échec DMARC. Veuillez réessayer.","Unable to load DMARC guidance tag(s). Please try again.":"Impossible de charger le(s) tag(s) d'orientation DMARC. Veuillez réessayer.","Unable to load DMARC phase summary. Please try again.":"Impossible de charger le résumé DMARC. Veuillez réessayer.","Unable to load DMARC scan(s). Please try again.":"Impossible de charger le(s) scan(s) DMARC. Veuillez réessayer.","Unable to load DMARC summary data. Please try again.":"Impossible de charger les données de synthèse DMARC. Veuillez réessayer.","Unable to load DNS scan(s). Please try again.":"Impossible de charger le(s) scan(s) DNS. Veuillez réessayer.","Unable to load HTTPS guidance tag(s). Please try again.":"Impossible de charger la ou les balises d'orientation HTTPS. Veuillez réessayer.","Unable to load HTTPS scan(s). Please try again.":"Impossible de charger le(s) scan(s) HTTPS. Veuillez réessayer.","Unable to load HTTPS summary. Please try again.":"Impossible de charger le résumé HTTPS. Veuillez réessayer.","Unable to load SPF failure data. Please try again.":"Impossible de charger les données d'échec SPF. Veuillez réessayer.","Unable to load SPF guidance tag(s). Please try again.":"Impossible de charger le(s) tag(s) d'orientation SPF. Veuillez réessayer.","Unable to load SPF scan(s). Please try again.":"Impossible de charger le(s) scan(s) SPF. Veuillez réessayer.","Unable to load SSL guidance tag(s). Please try again.":"Impossible de charger le(s) tag(s) d'orientation SSL. Veuillez réessayer.","Unable to load SSL scan(s). Please try again.":"Impossible de charger le(s) scan(s) SSL. Veuillez réessayer.","Unable to load affiliation information. Please try again.":"Impossible de charger les informations d'affiliation. Veuillez réessayer.","Unable to load affiliation(s). Please try again.":"Impossible de charger l'affiliation (s). Veuillez réessayer.","Unable to load domain(s). Please try again.":"Impossible de charger le(s) domaine(s). Veuillez réessayer.","Unable to load domain. Please try again.":"Impossible de charger le domaine. Veuillez réessayer.","Unable to load full pass data. Please try again.":"Impossible de charger les données complètes de la passe. Veuillez réessayer.","Unable to load mail summary. Please try again.":"Impossible de charger le résumé du courrier. Veuillez réessayer.","Unable to load organization domain statuses. Please try again.":"Impossible de charger les statuts des domaines d'organisation. Veuillez réessayer.","Unable to load organization(s). Please try again.":"Impossible de charger l'organisation (s). Veuillez réessayer.","Unable to load owner information. Please try again.":"Impossible de charger les informations sur le propriétaire. Veuillez réessayer.","Unable to load summary. Please try again.":"Impossible de charger le résumé. Veuillez réessayer.","Unable to load tags(s). Please try again.":"Impossible de charger les balises. Veuillez réessayer.","Unable to load user(s). Please try again.":"Impossible de charger le(s) utilisateur(s). Veuillez réessayer.","Unable to load verified domain(s). Please try again.":"Impossible de charger le(s) domaine(s) vérifié(s). Veuillez réessayer.","Unable to load verified organization(s). Please try again.":"Impossible de charger le(s) organisme(s) vérifié(s). Veuillez réessayer.","Unable to load web scan(s). Please try again.":"Impossible de charger le(s) scan(s) web. Veuillez réessayer.","Unable to load web summary. Please try again.":"Impossible de charger le résumé web. Veuillez réessayer.","Unable to query affiliation(s). Please try again.":"Impossible de demander l'affiliation (s). Veuillez réessayer.","Unable to query domain(s). Please try again.":"Impossible d'interroger le(s) domaine(s). Veuillez réessayer.","Unable to query user(s). Please try again.":"Impossible d'interroger le(s) utilisateur(s). Veuillez réessayer.","Unable to refresh tokens, please sign in.":"Impossible de rafraîchir les jetons, veuillez vous connecter.","Unable to remove a user that already does not belong to this organization.":"Impossible de supprimer un utilisateur qui n'appartient déjà plus à cette organisation.","Unable to remove domain from unknown organization.":"Impossible de supprimer le domaine d'une organisation inconnue.","Unable to remove domain. Please try again.":"Impossible de supprimer le domaine. Veuillez réessayer.","Unable to remove organization. Please try again.":"Impossible de supprimer l'organisation. Veuillez réessayer.","Unable to remove phone number. Please try again.":"Impossible de supprimer le numéro de téléphone. Veuillez réessayer.","Unable to remove unknown domain.":"Impossible de supprimer un domaine inconnu.","Unable to remove unknown organization.":"Impossible de supprimer une organisation inconnue.","Unable to remove unknown user from organization.":"Impossible de supprimer un utilisateur inconnu de l'organisation.","Unable to remove user from organization.":"Impossible de supprimer un utilisateur de l'organisation.","Unable to remove user from this organization. Please try again.":"Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer.","Unable to remove user from unknown organization.":"Impossible de supprimer un utilisateur d'une organisation inconnue.","Unable to request a one time scan on an unknown domain.":"Impossible de demander un scan unique sur un domaine inconnu.","Unable to reset password. Please request a new email.":"Impossible de réinitialiser le mot de passe. Veuillez demander un nouvel e-mail.","Unable to reset password. Please try again.":"Impossible de réinitialiser le mot de passe. Veuillez réessayer.","Unable to retrieve DMARC report information for: {domain}":["Impossible de récupérer les informations du rapport DMARC pour : ",["domain"]],"Unable to select DMARC report(s) for this period and year.":"Impossible de sélectionner le(s) rapport(s) DMARC pour cette période et cette année.","Unable to send authentication email. Please try again.":"Impossible d'envoyer l'email d'authentification. Veuillez réessayer.","Unable to send authentication text message. Please try again.":"Impossible d'envoyer un message texte d'authentification. Veuillez réessayer.","Unable to send org invite email. Please try again.":"Impossible d'envoyer l'e-mail d'invitation à l'org. Veuillez réessayer.","Unable to send password reset email. Please try again.":"Impossible d'envoyer l'email de réinitialisation du mot de passe. Veuillez réessayer.","Unable to send two factor authentication message. Please try again.":"Impossible d'envoyer le message d'authentification à deux facteurs. Veuillez réessayer.","Unable to send verification email. Please try again.":"Impossible d'envoyer l'email de vérification. Veuillez réessayer.","Unable to set phone number, please try again.":"Impossible de définir le numéro de téléphone, veuillez réessayer.","Unable to sign in, please try again.":"Impossible de se connecter, veuillez réessayer.","Unable to sign up, please contact org admin for a new invite.":"Impossible de s'inscrire, veuillez contacter l'administrateur de l'organisation pour obtenir une nouvelle invitation.","Unable to sign up. Please try again.":"Impossible de s'inscrire. Veuillez réessayer.","Unable to transfer organization ownership. Please try again.":"Impossible de transférer la propriété de l'organisation. Veuillez réessayer.","Unable to transfer ownership of a verified organization.":"Impossible de transférer la propriété d'une organisation vérifiée.","Unable to transfer ownership of an org to an undefined user.":"Impossible de transférer la propriété d'un org à un utilisateur non défini.","Unable to transfer ownership of undefined organization.":"Impossible de transférer la propriété d'une organisation non définie.","Unable to transfer ownership to a user outside the org. Please invite the user and try again.":"Impossible de transférer la propriété à un utilisateur extérieur à l'org. Veuillez inviter l'utilisateur et réessayer.","Unable to two factor authenticate. Please try again.":"Impossible de s'authentifier par deux facteurs. Veuillez réessayer.","Unable to update domain in an unknown org.":"Impossible de mettre à jour le domaine dans un org inconnu.","Unable to update domain that does not belong to the given organization.":"Impossible de mettre à jour un domaine qui n'appartient pas à l'organisation donnée.","Unable to update domain. Please try again.":"Impossible de mettre à jour le domaine. Veuillez réessayer.","Unable to update organization. Please try again.":"Impossible de mettre à jour l'organisation. Veuillez réessayer.","Unable to update password, current password does not match. Please try again.":"Impossible de mettre à jour le mot de passe, le mot de passe actuel ne correspond pas. Veuillez réessayer.","Unable to update password, new passwords do not match. Please try again.":"Impossible de mettre à jour le mot de passe, les nouveaux mots de passe ne correspondent pas. Veuillez réessayer.","Unable to update password, passwords do not match requirements. Please try again.":"Impossible de mettre à jour le mot de passe, les mots de passe ne correspondent pas aux exigences. Veuillez réessayer.","Unable to update password. Please try again.":"Impossible de mettre à jour le mot de passe. Veuillez réessayer.","Unable to update profile. Please try again.":"Impossible de mettre à jour le profil. Veuillez réessayer.","Unable to update role: organization unknown.":"Impossible de mettre à jour le rôle : organisation inconnue.","Unable to update role: user does not belong to organization.":"Impossible de mettre à jour le rôle : l'utilisateur n'appartient pas à l'organisation.","Unable to update role: user unknown.":"Impossible de mettre à jour le rôle : utilisateur inconnu.","Unable to update unknown domain.":"Impossible de mettre à jour un domaine inconnu.","Unable to update unknown organization.":"Impossible de mettre à jour une organisation inconnue.","Unable to update user's role. Please try again.":"Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer.","Unable to update your own role.":"Impossible de mettre à jour votre propre rôle.","Unable to verify account. Please request a new email.":"Impossible de vérifier le compte. Veuillez demander un nouvel e-mail.","Unable to verify account. Please try again.":"Impossible de vérifier le compte. Veuillez réessayer.","Unable to verify if user is a super admin, please try again.":"Impossible de vérifier si l'utilisateur est un super administrateur, veuillez réessayer.","Unable to verify if user is an admin, please try again.":"Impossible de vérifier si l'utilisateur est un administrateur, veuillez réessayer.","Unable to verify organization. Please try again.":"Impossible de vérifier l'organisation. Veuillez réessayer.","Unable to verify unknown organization.":"Impossible de vérifier une organisation inconnue.","User could not be queried.":"L'utilisateur n'a pas pu être interrogé.","User role was updated successfully.":"Le rôle de l'utilisateur a été mis à jour avec succès.","Username not available, please try another.":"Le nom d'utilisateur n'est pas disponible, veuillez en essayer un autre.","Verification error. Please activate multi-factor authentication to access content.":"Erreur de vérification. Veuillez activer l'authentification multifactorielle pour accéder au contenu.","Verification error. Please verify your account via email to access content.":"Erreur de vérification. Veuillez vérifier votre compte par e-mail pour accéder au contenu.","You must provide a `first` or `last` value to properly paginate the `Affiliation` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Affiliation`.","You must provide a `first` or `last` value to properly paginate the `DKIMResults` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DKIMResults`.","You must provide a `first` or `last` value to properly paginate the `DKIM` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DKIM`.","You must provide a `first` or `last` value to properly paginate the `DMARC` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DMARC`.","You must provide a `first` or `last` value to properly paginate the `DkimFailureTable` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DkimFailureTable`.","You must provide a `first` or `last` value to properly paginate the `DmarcFailureTable` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DmarcFailureTable`.","You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DmarcSummaries`.","You must provide a `first` or `last` value to properly paginate the `Domain` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Domain`.","You must provide a `first` or `last` value to properly paginate the `FullPassTable` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `FullPassTable`.","You must provide a `first` or `last` value to properly paginate the `GuidanceTag` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `GuidanceTag`.","You must provide a `first` or `last` value to properly paginate the `HTTPS` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `HTTPS`.","You must provide a `first` or `last` value to properly paginate the `Organization` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Organization`.","You must provide a `first` or `last` value to properly paginate the `SPF` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `SPF`.","You must provide a `first` or `last` value to properly paginate the `SSL` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `SSL`.","You must provide a `first` or `last` value to properly paginate the `SpfFailureTable` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `SpfFailureTable`.","You must provide a `first` or `last` value to properly paginate the `User` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `User`.","You must provide a `first` or `last` value to properly paginate the `VerifiedDomain` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `VerifiedDomain`.","You must provide a `first` or `last` value to properly paginate the `VerifiedOrganization` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `VerifiedOrganization`.","You must provide a `limit` value in the range of 1-100 to properly paginate the `DNS` connection.":"Vous devez fournir une valeur `limit` dans la gamme de 1-100 pour paginer correctement la connexion `DNS`.","You must provide a `limit` value in the range of 1-100 to properly paginate the `web` connection.":"Vous devez fournir une valeur `limit` dans la gamme de 1-100 pour paginer correctement la connexion `web`.","You must provide a `limit` value to properly paginate the `DNS` connection.":"Vous devez fournir une valeur `limit` pour paginer correctement la connexion `DNS`.","You must provide a `limit` value to properly paginate the `web` connection.":"Vous devez fournir une valeur `limit` pour paginer correctement la connexion `web`.","You must provide a `period` value to access the `DmarcSummaries` connection.":"Vous devez fournir une valeur `period` pour accéder à la connexion `DmarcSummaries`.","You must provide a `year` value to access the `DmarcSummaries` connection.":"Vous devez fournir une valeur `year` pour accéder à la connexion `DmarcSummaries`.","You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `DNS` connection.":"Vous devez fournir au plus une valeur de méthode de pagination (`before`, `after`, `offset`) pour paginer correctement la connexion `DNS`.","You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `web` connection.":"Vous devez fournir au plus une valeur de méthode de pagination (`before`, `after`, `offset`) pour paginer correctement la connexion `web`.","`{argSet}` must be of type `number` not `{typeSet}`.":["`",["argSet"],"` doit être de type `number` et non `",["typeSet"],"`."],"`{argSet}` on the `Affiliation` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `Affiliation` ne peut être inférieur à zéro."],"`{argSet}` on the `DKIMResults` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DKIMResults` ne peut être inférieur à zéro."],"`{argSet}` on the `DKIM` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DKIM` ne peut être inférieur à zéro."],"`{argSet}` on the `DMARC` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DMARC` ne peut être inférieur à zéro."],"`{argSet}` on the `DkimFailureTable` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DkimFailureTable` ne peut être inférieur à zéro."],"`{argSet}` on the `DmarcFailureTable` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DmarcFailureTable` ne peut être inférieur à zéro."],"`{argSet}` on the `DmarcSummaries` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DmarcSummaries` ne peut être inférieur à zéro."],"`{argSet}` on the `Domain` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `Domain` ne peut être inférieur à zéro."],"`{argSet}` on the `FullPassTable` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `FullPassTable` ne peut être inférieur à zéro."],"`{argSet}` on the `GuidanceTag` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `GuidanceTag` ne peut être inférieure à zéro."],"`{argSet}` on the `HTTPS` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `HTTPS` ne peut être inférieur à zéro."],"`{argSet}` on the `Organization` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `Organization` ne peut être inférieure à zéro."],"`{argSet}` on the `SPF` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `SPF` ne peut être inférieure à zéro."],"`{argSet}` on the `SSL` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `SSL` ne peut être inférieur à zéro."],"`{argSet}` on the `SpfFailureTable` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `SpfFailureTable` ne peut être inférieur à zéro."],"`{argSet}` on the `User` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `User` ne peut être inférieure à zéro."],"`{argSet}` on the `VerifiedDomain` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `VerifiedDomain` ne peut être inférieur à zéro."],"`{argSet}` on the `VerifiedOrganization` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `VerifiedOrganization` ne peut être inférieur à zéro."]}}; \ No newline at end of file +/*eslint-disable*/module.exports={messages:{"Authentication error. Please sign in.":"Erreur d'authentification. Veuillez vous connecter.","Cannot query affiliations on organization without admin permission or higher.":"Impossible d'interroger les affiliations sur l'organisation sans l'autorisation de l'administrateur ou plus.","Cannot query audit logs on organization without admin permission or higher.":"Impossible d'interroger les journaux d'audit sur l'organisation sans l'autorisation d'administrateur ou plus.","Cannot query dns scan results without permission.":"Impossible d'interroger les résultats de l'analyse DNS sans autorisation.","Cannot query domain selectors without permission.":"Impossible d'interroger les sélecteurs de domaine sans autorisation.","Cannot query web scan results without permission.":"Impossible d'interroger les résultats de l'analyse web sans autorisation.","Email already in use.":"Courriel déjà utilisé.","Error while requesting scan. Please try again.":"Erreur lors de la demande d'analyse. Veuillez réessayer.","If an account with this username is found, a password reset link will be found in your inbox.":"Si un compte avec ce nom d'utilisateur est trouvé, un lien de réinitialisation du mot de passe se trouvera dans votre boîte de réception.","If an account with this username is found, an email verification link will be found in your inbox.":"Si un compte avec ce nom d'utilisateur est trouvé, un lien de vérification par e-mail sera trouvé dans votre boîte de réception.","Incorrect TFA code. Please sign in again.":"Code TFA incorrect. Veuillez vous reconnecter.","Incorrect token value. Please request a new email.":"La valeur du jeton est incorrecte. Veuillez demander un nouvel e-mail.","Incorrect username or password. Please try again.":"Le nom d'utilisateur ou le mot de passe est incorrect. Veuillez réessayer.","Invalid token, please sign in.":"Jeton invalide, veuillez vous connecter.","New passwords do not match.":"Les nouveaux mots de passe ne correspondent pas.","No organization with the provided slug could be found.":"Aucune organisation avec le slug fourni n'a pu être trouvée.","No verified domain with the provided domain could be found.":"Aucun domaine vérifié avec le domaine fourni n'a pu être trouvé.","No verified organization with the provided slug could be found.":"Aucune organisation vérifiée avec le slug fourni n'a pu être trouvée.","Organization has already been verified.":"L'organisation a déjà été vérifiée.","Organization name already in use, please choose another and try again.":"Le nom de l'organisation est déjà utilisé, veuillez en choisir un autre et réessayer.","Organization name already in use. Please try again with a different name.":"Le nom de l'organisation est déjà utilisé. Veuillez réessayer avec un nom différent.","Ownership check error. Unable to request domain information.":"Erreur de vérification de la propriété. Impossible de demander des informations sur le domaine.","Passing both `first` and `last` to paginate the `Affiliation` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `Affiliation` n'est pas supporté.","Passing both `first` and `last` to paginate the `DKIMResults` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DKIMResults` n'est pas supporté.","Passing both `first` and `last` to paginate the `DKIM` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DKIMResults` n'est pas supporté.","Passing both `first` and `last` to paginate the `DMARC` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DMARC` n'est pas supporté.","Passing both `first` and `last` to paginate the `DkimFailureTable` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DkimFailureTable` n'est pas supporté.","Passing both `first` and `last` to paginate the `DmarcFailureTable` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DmarcFailureTable` n'est pas supporté.","Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `DmarcSummaries` n'est pas supporté.","Passing both `first` and `last` to paginate the `Domain` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `Domain` n'est pas supporté.","Passing both `first` and `last` to paginate the `FullPassTable` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `FullPassTable` n'est pas supporté.","Passing both `first` and `last` to paginate the `GuidanceTag` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `GuidanceTag` n'est pas supporté.","Passing both `first` and `last` to paginate the `HTTPS` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `HTTPS` n'est pas supporté.","Passing both `first` and `last` to paginate the `Log` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `Log` n'est pas supporté.","Passing both `first` and `last` to paginate the `Organization` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `Organization` n'est pas supporté.","Passing both `first` and `last` to paginate the `SPF` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `SPF` n'est pas supporté.","Passing both `first` and `last` to paginate the `SSL` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `SSL` n'est pas supporté.","Passing both `first` and `last` to paginate the `SpfFailureTable` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `SpfFailureTable` n'est pas supporté.","Passing both `first` and `last` to paginate the `User` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `User` n'est pas supporté.","Passing both `first` and `last` to paginate the `VerifiedDomain` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `VerifiedDomain` n'est pas supporté.","Passing both `first` and `last` to paginate the `VerifiedOrganization` connection is not supported.":"Passer à la fois `first` et `last` pour paginer la connexion `VerifiedOrganization` n'est pas supporté.","Password does not meet requirements.":"Le mot de passe ne répond pas aux exigences.","Password was successfully reset.":"Le mot de passe a été réinitialisé avec succès.","Password was successfully updated.":"Le mot de passe a été mis à jour avec succès.","Passwords do not match.":"Les mots de passe ne correspondent pas.","Permission Denied: Could not retrieve specified organization.":"Permission refusée : Impossible de récupérer l'organisation spécifiée.","Permission Denied: Multi-factor authentication is required for admin accounts":"Permission refusée : L'authentification multifactorielle est requise pour les comptes admin.","Permission Denied: Please contact org owner to transfer ownership.":"Permission refusée : Veuillez contacter le propriétaire de l'org pour transférer la propriété.","Permission Denied: Please contact organization admin for help with archiving domains.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur l'archivage des domaines.","Permission Denied: Please contact organization admin for help with removing domain.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide afin de supprimer le domaine.","Permission Denied: Please contact organization admin for help with removing domains.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des domaines.","Permission Denied: Please contact organization admin for help with removing organization.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide afin de supprimer l'organisation.","Permission Denied: Please contact organization admin for help with removing users.":"Autorisation refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des utilisateurs.","Permission Denied: Please contact organization admin for help with updating organization.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des utilisateurs.","Permission Denied: Please contact organization admin for help with updating user roles.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la mise à jour des rôles des utilisateurs.","Permission Denied: Please contact organization admin for help with user invitations.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide concernant les invitations d'utilisateurs.","Permission Denied: Please contact organization admin for help with user role changes.":"Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur les changements de rôle des utilisateurs.","Permission Denied: Please contact organization user for help with creating domain.":"Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la création du domaine.","Permission Denied: Please contact organization user for help with creating domains.":"Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la création de domaines.","Permission Denied: Please contact organization user for help with retrieving this domain.":"Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide pour récupérer ce domaine.","Permission Denied: Please contact organization user for help with scanning this domain.":"Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur l'analyse de ce domaine.","Permission Denied: Please contact organization user for help with updating this domain.":"Autorisation refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la mise à jour de ce domaine.","Permission Denied: Please contact super admin for help with archiving organization.":"Permission refusée : Veuillez contacter le super administrateur pour obtenir de l'aide sur l'organisation de l'archivage.","Permission Denied: Please contact super admin for help with removing domain.":"Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la mise à jour de ce domaine.","Permission Denied: Please contact super admin for help with removing organization.":"Permission refusée : Veuillez contacter le super administrateur pour qu'il vous aide à supprimer l'organisation.","Permission Denied: Please contact super admin for help with scanning this domain.":"Permission refusée : Veuillez contacter le super administrateur pour obtenir de l'aide sur l'analyse de ce domaine.","Permission Denied: Please contact super admin for help with verifying this organization.":"Permission refusée : Veuillez contacter le super administrateur pour qu'il vous aide à vérifier cette organisation.","Permission check error. Unable to request domain information.":"Erreur de vérification des permissions. Impossible de demander des informations sur le domaine.","Permission error, not an admin for this user.":"Erreur de permission, pas d'administrateur pour cet utilisateur.","Permission error: Unable to close other user's account.":"Erreur de permission: Impossible de fermer le compte d'un autre utilisateur.","Permissions error. You do not have sufficient permissions to access this data.":"Erreur de permissions. Vous n'avez pas les autorisations suffisantes pour accéder à ces données.","Phone number has been successfully removed.":"Le numéro de téléphone a été supprimé avec succès.","Phone number has been successfully set, you will receive a verification text message shortly.":"Le numéro de téléphone a été configuré avec succès, vous recevrez bientôt un message de vérification.","Profile successfully updated.":"Le profil a été mis à jour avec succès.","Requesting `{amount}` records on the `Affiliation` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `Affiliation` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `DkimFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `DkimFailureTable` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `DmarcFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `DkimFailureTable` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `DmarcSummaries` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `DmarcSummaries` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `Domain` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `Domain` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `FullPassTable` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `FullPassTable` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `GuidanceTag` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `GuidanceTag` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `Log` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `Log` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `Organization` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `Organization` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `SpfFailureTable` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `SpfFailureTable` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `User` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `User` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `VerifiedDomain` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `VerifiedDomain` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting `{amount}` records on the `VerifiedOrganization` connection exceeds the `{argSet}` limit of 100 records.":["La demande d'enregistrements `",["amount"],"` sur la connexion `VerifiedOrganization` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `DKIMResults` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `DKIMResults` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `DKIM` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `DKIM` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `DMARC` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `DMARC` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `HTTPS` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `HTTPS` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `SPF` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `SPF` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records.":["La demande de ",["amount"]," enregistrements sur la connexion `SSL` dépasse la limite `",["argSet"],"` de 100 enregistrements."],"Successfully added {domainCount} domain(s) to {0}.":["Ajouté avec succès le(s) domaine(s) ",["domainCount"]," à ",["0"],"."],"Successfully added {domainCount} domains to {0}.":["Ajouté avec succès les domaines ",["domainCount"]," à ",["0"],"."],"Successfully archived organization: {0}.":["Organisation archivée avec succès : ",["0"],"."],"Successfully closed account.":"Le compte a été fermé avec succès.","Successfully dispatched one time scan.":"Un seul balayage a été effectué avec succès.","Successfully email verified account, and set TFA send method to email.":"Réussir à envoyer un email au compte vérifié, et définir la méthode d'envoi de la TFA sur email.","Successfully invited user to organization, and sent notification email.":"L'utilisateur a été invité avec succès à l'organisation et l'email de notification a été envoyé.","Successfully left organization: {0}":["L'organisation a été quittée avec succès: ",["0"]],"Successfully removed domain: {0} from favourites.":["A réussi à supprimer le domaine : ",["0"]," des favoris."],"Successfully removed domain: {0} from {1}.":["A réussi à supprimer le domaine : ",["0"]," de ",["1"],"."],"Successfully removed organization: {0}.":["A réussi à supprimer l'organisation : ",["0"],"."],"Successfully removed user from organization.":"L'utilisateur a été retiré de l'organisation avec succès.","Successfully removed {domainCount} domain(s) from {0}.":["Supprimé avec succès le(s) domaine(s) ",["domainCount"]," de ",["0"],"."],"Successfully removed {domainCount} domains from {0}.":["Suppression réussie des domaines ",["domainCount"]," de ",["0"],"."],"Successfully requested invite to organization, and sent notification email.":"La demande d'invitation à l'organisation a été effectuée avec succès et un courriel de notification a été envoyé.","Successfully sent invitation to service, and organization email.":"Envoi réussi de l'invitation au service, et de l'email de l'organisation.","Successfully signed out.":"J'ai réussi à me déconnecter.","Successfully transferred org: {0} ownership to user: {1}":["A réussi à transférer la propriété de org: ",["0"]," à l'utilisateur: ",["1"]],"Successfully verified organization: {0}.":"Envoi réussi de l'invitation au service, et de l'email de l'organisation.","Successfully verified phone number, and set TFA send method to text.":"Le numéro de téléphone a été vérifié avec succès, et la méthode d'envoi de la TFA a été réglée sur le texte.","Token value incorrect, please sign in again.":"La valeur du jeton est incorrecte, veuillez vous connecter à nouveau.","Too many failed login attempts, please reset your password, and try again.":"Trop de tentatives de connexion ont échoué, veuillez réinitialiser votre mot de passe et réessayer.","Two factor code is incorrect. Please try again.":"Le code à deux facteurs est incorrect. Veuillez réessayer.","Two factor code length is incorrect. Please try again.":"La longueur du code à deux facteurs est incorrecte. Veuillez réessayer.","Unable leave organization. Please try again.":"Impossible de quitter l'organisation. Veuillez réessayer.","Unable to add domains in unknown organization.":"Impossible d'ajouter des domaines dans une organisation inconnue.","Unable to archive organization. Please try again.":"Impossible d'archiver l'organisation. Veuillez réessayer.","Unable to archive unknown organization.":"Impossible d'archiver une organisation inconnue.","Unable to authenticate. Please try again.":"Impossible de s'authentifier. Veuillez réessayer.","Unable to check permission. Please try again.":"Impossible de vérifier l'autorisation. Veuillez réessayer.","Unable to close account of an undefined user.":"Impossible de fermer le compte d'un utilisateur non défini.","Unable to close account. Please try again.":"Impossible de fermer le compte. Veuillez réessayer.","Unable to create domain in unknown organization.":"Impossible de créer un domaine dans une organisation inconnue.","Unable to create domain, organization has already claimed it.":"Impossible de créer le domaine, l'organisation l'a déjà réclamé.","Unable to create domain. Please try again.":"Impossible de créer un domaine. Veuillez réessayer.","Unable to create domains. Please try again.":"Impossible de créer des domaines. Veuillez réessayer.","Unable to create organization. Please try again.":"Impossible de créer une organisation. Veuillez réessayer.","Unable to dispatch one time scan. Please try again.":"Impossible d'envoyer un scan unique. Veuillez réessayer.","Unable to export organization. Please try again.":"Impossible d'exporter l'organisation. Veuillez réessayer.","Unable to favourite domain, user has already favourited it.":"Impossible de favoriser le domaine, l'utilisateur l'a déjà favorisé.","Unable to favourite domain. Please try again.":"Impossible d'accéder au domaine favori. Veuillez réessayer.","Unable to favourite unknown domain.":"Impossible de favoriser le domaine inconnu.","Unable to find Aggregate guidance tag(s). Please try again.":"Impossible de trouver le(s) tag(s) d'orientation des agrégats. Veuillez réessayer.","Unable to find DKIM guidance tag(s). Please try again.":"Impossible de trouver le(s) tag(s) d'orientation DKIM. Veuillez réessayer.","Unable to find DKIM result(s). Please try again.":"Impossible de trouver le(s) résultat(s) DKIM. Veuillez réessayer.","Unable to find DKIM scan(s). Please try again.":"Impossible de trouver le(s) scan(s) DKIM. Veuillez réessayer.","Unable to find DMARC guidance tag(s). Please try again.":"Impossible de trouver le(s) tag(s) d'orientation DMARC. Veuillez réessayer.","Unable to find DMARC scan(s). Please try again.":"Impossible de trouver le(s) scan(s) DMARC. Veuillez réessayer.","Unable to find DMARC summary data. Please try again.":"Impossible de trouver les données de synthèse DMARC. Veuillez réessayer.","Unable to find DNS scan(s). Please try again.":"Impossible de trouver le(s) scan(s) DNS. Veuillez réessayer.","Unable to find HTTPS guidance tag(s). Please try again.":"Impossible de trouver la ou les balises d'orientation HTTPS. Veuillez réessayer.","Unable to find HTTPS scan(s). Please try again.":"Impossible de trouver le(s) scan(s) HTTPS. Veuillez réessayer.","Unable to find SPF guidance tag(s). Please try again.":"Impossible de trouver le(s) tag(s) d'orientation SPF. Veuillez réessayer.","Unable to find SPF scan(s). Please try again.":"Impossible de trouver le(s) scan(s) SPF. Veuillez réessayer.","Unable to find SSL guidance tag(s). Please try again.":"Impossible de trouver le(s) tag(s) d'orientation SSL. Veuillez réessayer.","Unable to find SSL scan(s). Please try again.":"Impossible de trouver le(s) scan(s) SSL. Veuillez réessayer.","Unable to find the requested domain.":"Impossible de trouver le domaine demandé.","Unable to find user affiliation(s). Please try again.":"Impossible de trouver l'affiliation de l'utilisateur (s). Veuillez réessayer.","Unable to find verified organization(s). Please try again.":"Impossible de trouver une ou plusieurs organisations vérifiées. Veuillez réessayer.","Unable to invite user to organization. Please try again.":"Impossible d'inviter un utilisateur dans une organisation. Veuillez réessayer.","Unable to invite user to organization. User is already affiliated with organization.":"Impossible d'inviter un utilisateur dans une organisation. L'utilisateur est déjà affilié à l'organisation.","Unable to invite user to unknown organization.":"Impossible d'inviter un utilisateur à une organisation inconnue.","Unable to invite user. Please try again.":"Impossible d'inviter un utilisateur. Veuillez réessayer.","Unable to invite yourself to an org.":"Impossible de s'inviter à un org.","Unable to leave organization. Please try again.":"Impossible de quitter l'organisation. Veuillez réessayer.","Unable to leave undefined organization.":"Impossible de quitter une organisation non définie.","Unable to load Aggregate guidance tag(s). Please try again.":"Impossible de charger le(s) tag(s) d'orientation des agrégats. Veuillez réessayer.","Unable to load DKIM failure data. Please try again.":"Impossible de charger les données d'échec DKIM. Veuillez réessayer.","Unable to load DKIM guidance tag(s). Please try again.":"Impossible de charger le(s) tag(s) d'orientation DKIM. Veuillez réessayer.","Unable to load DKIM result(s). Please try again.":"Impossible de charger le(s) résultat(s) DKIM. Veuillez réessayer.","Unable to load DKIM scan(s). Please try again.":"Impossible de charger le(s) scan(s) DKIM. Veuillez réessayer.","Unable to load DKIM summary. Please try again.":"Impossible de charger le résumé DKIM. Veuillez réessayer.","Unable to load DMARC failure data. Please try again.":"Impossible de charger les données d'échec DMARC. Veuillez réessayer.","Unable to load DMARC guidance tag(s). Please try again.":"Impossible de charger le(s) tag(s) d'orientation DMARC. Veuillez réessayer.","Unable to load DMARC phase summary. Please try again.":"Impossible de charger le résumé DMARC. Veuillez réessayer.","Unable to load DMARC scan(s). Please try again.":"Impossible de charger le(s) scan(s) DMARC. Veuillez réessayer.","Unable to load DMARC summary data. Please try again.":"Impossible de charger les données de synthèse DMARC. Veuillez réessayer.","Unable to load DMARC summary. Please try again.":"Impossible de charger le résumé DMARC. Veuillez réessayer.","Unable to load DNS scan(s). Please try again.":"Impossible de charger le(s) scan(s) DNS. Veuillez réessayer.","Unable to load HTTPS guidance tag(s). Please try again.":"Impossible de charger la ou les balises d'orientation HTTPS. Veuillez réessayer.","Unable to load HTTPS scan(s). Please try again.":"Impossible de charger le(s) scan(s) HTTPS. Veuillez réessayer.","Unable to load HTTPS summary. Please try again.":"Impossible de charger le résumé HTTPS. Veuillez réessayer.","Unable to load SPF failure data. Please try again.":"Impossible de charger les données d'échec SPF. Veuillez réessayer.","Unable to load SPF guidance tag(s). Please try again.":"Impossible de charger le(s) tag(s) d'orientation SPF. Veuillez réessayer.","Unable to load SPF scan(s). Please try again.":"Impossible de charger le(s) scan(s) SPF. Veuillez réessayer.","Unable to load SPF summary. Please try again.":"Impossible de charger le résumé SPF. Veuillez réessayer.","Unable to load SSL guidance tag(s). Please try again.":"Impossible de charger le(s) tag(s) d'orientation SSL. Veuillez réessayer.","Unable to load SSL scan(s). Please try again.":"Impossible de charger le(s) scan(s) SSL. Veuillez réessayer.","Unable to load SSL summary. Please try again.":"Impossible de charger le résumé SSL. Veuillez réessayer.","Unable to load affiliation information. Please try again.":"Impossible de charger les informations d'affiliation. Veuillez réessayer.","Unable to load affiliation(s). Please try again.":"Impossible de charger l'affiliation (s). Veuillez réessayer.","Unable to load all organization domain statuses. Please try again.":"Impossible de charger tous les statuts de domaine d'organisation. Veuillez réessayer.","Unable to load domain(s). Please try again.":"Impossible de charger le(s) domaine(s). Veuillez réessayer.","Unable to load domain. Please try again.":"Impossible de charger le domaine. Veuillez réessayer.","Unable to load full pass data. Please try again.":"Impossible de charger les données complètes de la passe. Veuillez réessayer.","Unable to load log(s). Please try again.":"Impossible de charger le(s) journal(s). Veuillez réessayer.","Unable to load log. Please try again.":"Impossible de charger le journal. Veuillez réessayer.","Unable to load mail summary. Please try again.":"Impossible de charger le résumé du courrier. Veuillez réessayer.","Unable to load organization domain statuses. Please try again.":"Impossible de charger les statuts des domaines d'organisation. Veuillez réessayer.","Unable to load organization(s). Please try again.":"Impossible de charger l'organisation (s). Veuillez réessayer.","Unable to load owner information. Please try again.":"Impossible de charger les informations sur le propriétaire. Veuillez réessayer.","Unable to load summary. Please try again.":"Impossible de charger le résumé. Veuillez réessayer.","Unable to load tags(s). Please try again.":"Impossible de charger les balises. Veuillez réessayer.","Unable to load user(s). Please try again.":"Impossible de charger le(s) utilisateur(s). Veuillez réessayer.","Unable to load verified domain(s). Please try again.":"Impossible de charger le(s) domaine(s) vérifié(s). Veuillez réessayer.","Unable to load verified organization(s). Please try again.":"Impossible de charger le(s) organisme(s) vérifié(s). Veuillez réessayer.","Unable to load web connections summary. Please try again.":"Impossible de charger le résumé des connexions web. Veuillez réessayer.","Unable to load web scan(s). Please try again.":"Impossible de charger le(s) scan(s) web. Veuillez réessayer.","Unable to load web summary. Please try again.":"Impossible de charger le résumé web. Veuillez réessayer.","Unable to query affiliation(s). Please try again.":"Impossible de demander l'affiliation (s). Veuillez réessayer.","Unable to query domain(s). Please try again.":"Impossible d'interroger le(s) domaine(s). Veuillez réessayer.","Unable to query log(s). Please try again.":"Impossible d'interroger le(s) journal(s). Veuillez réessayer.","Unable to query user(s). Please try again.":"Impossible d'interroger le(s) utilisateur(s). Veuillez réessayer.","Unable to refresh tokens, please sign in.":"Impossible de rafraîchir les jetons, veuillez vous connecter.","Unable to remove a user that already does not belong to this organization.":"Impossible de supprimer un utilisateur qui n'appartient déjà plus à cette organisation.","Unable to remove domain from unknown organization.":"Impossible de supprimer le domaine d'une organisation inconnue.","Unable to remove domain. Domain is not part of organization.":"Impossible de supprimer le domaine. Le domaine ne fait pas partie de l'organisation.","Unable to remove domain. Please try again.":"Impossible de supprimer le domaine. Veuillez réessayer.","Unable to remove domains from unknown organization.":"Impossible de supprimer les domaines d'une organisation inconnue.","Unable to remove organization. Please try again.":"Impossible de supprimer l'organisation. Veuillez réessayer.","Unable to remove phone number. Please try again.":"Impossible de supprimer le numéro de téléphone. Veuillez réessayer.","Unable to remove unknown domain.":"Impossible de supprimer un domaine inconnu.","Unable to remove unknown organization.":"Impossible de supprimer une organisation inconnue.","Unable to remove unknown user from organization.":"Impossible de supprimer un utilisateur inconnu de l'organisation.","Unable to remove user from organization.":"Impossible de supprimer un utilisateur de l'organisation.","Unable to remove user from this organization. Please try again.":"Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer.","Unable to remove user from unknown organization.":"Impossible de supprimer un utilisateur d'une organisation inconnue.","Unable to request a one time scan on a domain that already has a pending scan.":"Impossible de demander une analyse unique sur un domaine qui a déjà une analyse en cours.","Unable to request a one time scan on an unknown domain.":"Impossible de demander un scan unique sur un domaine inconnu.","Unable to request a one time scan. Please try again.":"Impossible de demander une analyse unique. Veuillez réessayer.","Unable to request invite to organization with which you are already affiliated.":"Impossible de demander une invitation à une organisation à laquelle vous êtes déjà affilié.","Unable to request invite to organization with which you have already requested to join.":"Impossible de demander une invitation à une organisation à laquelle vous avez déjà demandé à adhérer.","Unable to request invite to unknown organization.":"Impossible de demander une invitation à une organisation inconnue.","Unable to request invite. Please try again.":"Impossible de demander une invitation. Veuillez réessayer.","Unable to reset password. Please request a new email.":"Impossible de réinitialiser le mot de passe. Veuillez demander un nouvel e-mail.","Unable to reset password. Please try again.":"Impossible de réinitialiser le mot de passe. Veuillez réessayer.","Unable to retrieve DMARC report information for: {domain}":["Impossible de récupérer les informations du rapport DMARC pour : ",["domain"]],"Unable to select DMARC report(s) for this period and year.":"Impossible de sélectionner le(s) rapport(s) DMARC pour cette période et cette année.","Unable to send authentication email. Please try again.":"Impossible d'envoyer l'email d'authentification. Veuillez réessayer.","Unable to send authentication text message. Please try again.":"Impossible d'envoyer un message texte d'authentification. Veuillez réessayer.","Unable to send org invite email. Please try again.":"Impossible d'envoyer l'e-mail d'invitation à l'org. Veuillez réessayer.","Unable to send org invite request email. Please try again.":"Impossible d'envoyer l'email de demande d'invitation à l'org. Veuillez réessayer.","Unable to send password reset email. Please try again.":"Impossible d'envoyer l'email de réinitialisation du mot de passe. Veuillez réessayer.","Unable to send two factor authentication message. Please try again.":"Impossible d'envoyer le message d'authentification à deux facteurs. Veuillez réessayer.","Unable to send verification email. Please try again.":"Impossible d'envoyer l'email de vérification. Veuillez réessayer.","Unable to set phone number, please try again.":"Impossible de définir le numéro de téléphone, veuillez réessayer.","Unable to sign in, please try again.":"Impossible de se connecter, veuillez réessayer.","Unable to sign up, please contact org admin for a new invite.":"Impossible de s'inscrire, veuillez contacter l'administrateur de l'organisation pour obtenir une nouvelle invitation.","Unable to sign up. Please try again.":"Impossible de s'inscrire. Veuillez réessayer.","Unable to transfer organization ownership. Please try again.":"Impossible de transférer la propriété de l'organisation. Veuillez réessayer.","Unable to transfer ownership of a verified organization.":"Impossible de transférer la propriété d'une organisation vérifiée.","Unable to transfer ownership of an org to an undefined user.":"Impossible de transférer la propriété d'un org à un utilisateur non défini.","Unable to transfer ownership of undefined organization.":"Impossible de transférer la propriété d'une organisation non définie.","Unable to transfer ownership to a user outside the org. Please invite the user and try again.":"Impossible de transférer la propriété à un utilisateur extérieur à l'org. Veuillez inviter l'utilisateur et réessayer.","Unable to two factor authenticate. Please try again.":"Impossible de s'authentifier par deux facteurs. Veuillez réessayer.","Unable to unfavourite domain, domain is not favourited.":"Impossible de désactiver le domaine, le domaine n'est pas favorisé.","Unable to unfavourite domain. Please try again.":"Impossible de défavoriser le domaine. Veuillez réessayer.","Unable to unfavourite unknown domain.":"Impossible de défavoriser un domaine inconnu.","Unable to update domain edge. Please try again.":"Impossible de mettre à jour le bord du domaine. Veuillez réessayer.","Unable to update domain in an unknown org.":"Impossible de mettre à jour le domaine dans un org inconnu.","Unable to update domain that does not belong to the given organization.":"Impossible de mettre à jour un domaine qui n'appartient pas à l'organisation donnée.","Unable to update domain. Please try again.":"Impossible de mettre à jour le domaine. Veuillez réessayer.","Unable to update organization. Please try again.":"Impossible de mettre à jour l'organisation. Veuillez réessayer.","Unable to update password, current password does not match. Please try again.":"Impossible de mettre à jour le mot de passe, le mot de passe actuel ne correspond pas. Veuillez réessayer.","Unable to update password, new passwords do not match. Please try again.":"Impossible de mettre à jour le mot de passe, les nouveaux mots de passe ne correspondent pas. Veuillez réessayer.","Unable to update password, passwords do not match requirements. Please try again.":"Impossible de mettre à jour le mot de passe, les mots de passe ne correspondent pas aux exigences. Veuillez réessayer.","Unable to update password. Please try again.":"Impossible de mettre à jour le mot de passe. Veuillez réessayer.","Unable to update profile. Please try again.":"Impossible de mettre à jour le profil. Veuillez réessayer.","Unable to update role: organization unknown.":"Impossible de mettre à jour le rôle : organisation inconnue.","Unable to update role: user does not belong to organization.":"Impossible de mettre à jour le rôle : l'utilisateur n'appartient pas à l'organisation.","Unable to update role: user unknown.":"Impossible de mettre à jour le rôle : utilisateur inconnu.","Unable to update unknown domain.":"Impossible de mettre à jour un domaine inconnu.","Unable to update unknown organization.":"Impossible de mettre à jour une organisation inconnue.","Unable to update user's role. Please try again.":"Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer.","Unable to update your own role.":"Impossible de mettre à jour votre propre rôle.","Unable to verify account. Please request a new email.":"Impossible de vérifier le compte. Veuillez demander un nouvel e-mail.","Unable to verify account. Please try again.":"Impossible de vérifier le compte. Veuillez réessayer.","Unable to verify if user is a super admin, please try again.":"Impossible de vérifier si l'utilisateur est un super administrateur, veuillez réessayer.","Unable to verify if user is an admin, please try again.":"Impossible de vérifier si l'utilisateur est un administrateur, veuillez réessayer.","Unable to verify organization. Please try again.":"Impossible de vérifier l'organisation. Veuillez réessayer.","Unable to verify unknown organization.":"Impossible de vérifier une organisation inconnue.","User could not be queried.":"L'utilisateur n'a pas pu être interrogé.","User role was updated successfully.":"Le rôle de l'utilisateur a été mis à jour avec succès.","Username not available, please try another.":"Le nom d'utilisateur n'est pas disponible, veuillez en essayer un autre.","Verification error. Please activate multi-factor authentication to access content.":"Erreur de vérification. Veuillez activer l'authentification multifactorielle pour accéder au contenu.","Verification error. Please verify your account via email to access content.":"Erreur de vérification. Veuillez vérifier votre compte par e-mail pour accéder au contenu.","You must provide a `first` or `last` value to properly paginate the `Affiliation` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Affiliation`.","You must provide a `first` or `last` value to properly paginate the `DKIMResults` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DKIMResults`.","You must provide a `first` or `last` value to properly paginate the `DKIM` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DKIM`.","You must provide a `first` or `last` value to properly paginate the `DMARC` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DMARC`.","You must provide a `first` or `last` value to properly paginate the `DkimFailureTable` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DkimFailureTable`.","You must provide a `first` or `last` value to properly paginate the `DmarcFailureTable` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DmarcFailureTable`.","You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DmarcSummaries`.","You must provide a `first` or `last` value to properly paginate the `Domain` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Domain`.","You must provide a `first` or `last` value to properly paginate the `FullPassTable` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `FullPassTable`.","You must provide a `first` or `last` value to properly paginate the `GuidanceTag` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `GuidanceTag`.","You must provide a `first` or `last` value to properly paginate the `HTTPS` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `HTTPS`.","You must provide a `first` or `last` value to properly paginate the `Log` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Log`.","You must provide a `first` or `last` value to properly paginate the `Organization` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Organization`.","You must provide a `first` or `last` value to properly paginate the `SPF` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `SPF`.","You must provide a `first` or `last` value to properly paginate the `SSL` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `SSL`.","You must provide a `first` or `last` value to properly paginate the `SpfFailureTable` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `SpfFailureTable`.","You must provide a `first` or `last` value to properly paginate the `User` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `User`.","You must provide a `first` or `last` value to properly paginate the `VerifiedDomain` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `VerifiedDomain`.","You must provide a `first` or `last` value to properly paginate the `VerifiedOrganization` connection.":"Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `VerifiedOrganization`.","You must provide a `limit` value in the range of 1-100 to properly paginate the `DNS` connection.":"Vous devez fournir une valeur `limit` comprise entre 1 et 100 pour paginer correctement la connexion `DNS`.","You must provide a `limit` value in the range of 1-100 to properly paginate the `web` connection.":"Vous devez fournir une valeur `limit` comprise entre 1 et 100 pour paginer correctement la connexion `web`.","You must provide a `limit` value to properly paginate the `DNS` connection.":"Vous devez fournir une valeur `limit` pour paginer correctement la connexion `DNS`.","You must provide a `limit` value to properly paginate the `web` connection.":"Vous devez fournir une valeur `limit` pour paginer correctement la connexion `web`.","You must provide a `period` value to access the `DmarcSummaries` connection.":"Vous devez fournir une valeur `period` pour accéder à la connexion `DmarcSummaries`.","You must provide a `year` value to access the `DmarcSummaries` connection.":"Vous devez fournir une valeur `year` pour accéder à la connexion `DmarcSummaries`.","You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `DNS` connection.":"Vous devez fournir au plus une valeur de méthode de pagination (`before`, `after`, `offset`) pour paginer correctement la connexion `DNS`.","You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `web` connection.":"Vous devez fournir au plus une valeur de méthode de pagination (`before`, `after`, `offset`) pour paginer correctement la connexion `web`.","`{argSet}` must be of type `number` not `{typeSet}`.":["`",["argSet"],"` doit être de type `number` et non `",["typeSet"],"`."],"`{argSet}` on the `Affiliation` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `Affiliation` ne peut être inférieur à zéro."],"`{argSet}` on the `DKIMResults` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DKIMResults` ne peut être inférieur à zéro."],"`{argSet}` on the `DKIM` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DKIM` ne peut être inférieur à zéro."],"`{argSet}` on the `DMARC` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DMARC` ne peut être inférieur à zéro."],"`{argSet}` on the `DkimFailureTable` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DkimFailureTable` ne peut être inférieur à zéro."],"`{argSet}` on the `DmarcFailureTable` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DmarcFailureTable` ne peut être inférieur à zéro."],"`{argSet}` on the `DmarcSummaries` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `DmarcSummaries` ne peut être inférieur à zéro."],"`{argSet}` on the `Domain` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `Domain` ne peut être inférieur à zéro."],"`{argSet}` on the `FullPassTable` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `FullPassTable` ne peut être inférieur à zéro."],"`{argSet}` on the `GuidanceTag` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `GuidanceTag` ne peut être inférieure à zéro."],"`{argSet}` on the `HTTPS` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `HTTPS` ne peut être inférieur à zéro."],"`{argSet}` on the `Log` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `Log` ne peut être inférieur à zéro."],"`{argSet}` on the `Organization` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `Organization` ne peut être inférieure à zéro."],"`{argSet}` on the `SPF` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `SPF` ne peut être inférieure à zéro."],"`{argSet}` on the `SSL` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `SSL` ne peut être inférieur à zéro."],"`{argSet}` on the `SpfFailureTable` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `SpfFailureTable` ne peut être inférieur à zéro."],"`{argSet}` on the `User` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `User` ne peut être inférieure à zéro."],"`{argSet}` on the `VerifiedDomain` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `VerifiedDomain` ne peut être inférieur à zéro."],"`{argSet}` on the `VerifiedOrganization` connection cannot be less than zero.":["`",["argSet"],"` sur la connexion `VerifiedOrganization` ne peut être inférieur à zéro."]}}; \ No newline at end of file diff --git a/api/src/locale/fr/messages.po b/api/src/locale/fr/messages.po index be7c260aed..d8ea62b327 100644 --- a/api/src/locale/fr/messages.po +++ b/api/src/locale/fr/messages.po @@ -11,27 +11,44 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" -#: src/auth/check-permission.js:20 -#: src/auth/check-permission.js:48 +#: src/auth/check-permission.js:18 +#: src/auth/check-permission.js:57 #: src/auth/user-required.js:10 #: src/auth/user-required.js:21 #: src/auth/user-required.js:28 msgid "Authentication error. Please sign in." msgstr "Erreur d'authentification. Veuillez vous connecter." -#: src/organization/objects/organization.js:188 +#: src/organization/objects/organization.js:241 msgid "Cannot query affiliations on organization without admin permission or higher." msgstr "Impossible d'interroger les affiliations sur l'organisation sans l'autorisation de l'administrateur ou plus." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:244 -#: src/audit-logs/queries/find-audit-logs.js:62 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:224 +#: src/audit-logs/queries/find-audit-logs.js:51 msgid "Cannot query audit logs on organization without admin permission or higher." msgstr "Impossible d'interroger les journaux d'audit sur l'organisation sans l'autorisation d'administrateur ou plus." -#: src/user/mutations/sign-up.js:117 +#: src/domain/objects/domain.js:149 +msgid "Cannot query dns scan results without permission." +msgstr "Impossible d'interroger les résultats de l'analyse DNS sans autorisation." + +#: src/domain/objects/domain.js:58 +msgid "Cannot query domain selectors without permission." +msgstr "Impossible d'interroger les sélecteurs de domaine sans autorisation." + +#: src/domain/objects/domain.js:195 +msgid "Cannot query web scan results without permission." +msgstr "Impossible d'interroger les résultats de l'analyse web sans autorisation." + +#: src/user/mutations/sign-up.js:106 msgid "Email already in use." msgstr "Courriel déjà utilisé." +#: src/domain/mutations/request-scan.js:87 +#: src/domain/mutations/request-scan.js:97 +msgid "Error while requesting scan. Please try again." +msgstr "Erreur lors de la demande d'analyse. Veuillez réessayer." + #: src/user/mutations/send-password-reset.js:62 msgid "If an account with this username is found, a password reset link will be found in your inbox." msgstr "Si un compte avec ce nom d'utilisateur est trouvé, un lien de réinitialisation du mot de passe se trouvera dans votre boîte de réception." @@ -61,7 +78,7 @@ msgstr "Jeton invalide, veuillez vous connecter." msgid "New passwords do not match." msgstr "Les nouveaux mots de passe ne correspondent pas." -#: src/organization/queries/find-organization-by-slug.js:49 +#: src/organization/queries/find-organization-by-slug.js:41 #: src/user/queries/find-my-tracker.js:29 msgid "No organization with the provided slug could be found." msgstr "Aucune organisation avec le slug fourni n'a pu être trouvée." @@ -74,7 +91,7 @@ msgstr "Aucun domaine vérifié avec le domaine fourni n'a pu être trouvé." msgid "No verified organization with the provided slug could be found." msgstr "Aucune organisation vérifiée avec le slug fourni n'a pu être trouvée." -#: src/organization/mutations/verify-organization.js:82 +#: src/organization/mutations/verify-organization.js:81 msgid "Organization has already been verified." msgstr "L'organisation a déjà été vérifiée." @@ -82,34 +99,34 @@ msgstr "L'organisation a déjà été vérifiée." msgid "Organization name already in use, please choose another and try again." msgstr "Le nom de l'organisation est déjà utilisé, veuillez en choisir un autre et réessayer." -#: src/organization/mutations/create-organization.js:139 +#: src/organization/mutations/create-organization.js:124 msgid "Organization name already in use. Please try again with a different name." msgstr "Le nom de l'organisation est déjà utilisé. Veuillez réessayer avec un nom différent." -#: src/auth/check-domain-ownership.js:30 -#: src/auth/check-domain-ownership.js:42 -#: src/auth/check-domain-ownership.js:64 -#: src/auth/check-domain-ownership.js:75 +#: src/auth/check-domain-ownership.js:29 +#: src/auth/check-domain-ownership.js:39 +#: src/auth/check-domain-ownership.js:65 +#: src/auth/check-domain-ownership.js:74 msgid "Ownership check error. Unable to request domain information." msgstr "Erreur de vérification de la propriété. Impossible de demander des informations sur le domaine." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:80 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:77 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:170 msgid "Passing both `first` and `last` to paginate the `Affiliation` connection is not supported." msgstr "Passer à la fois `first` et `last` pour paginer la connexion `Affiliation` n'est pas supporté." #: src/email-scan/loaders/load-dkim-results-connections-by-dkim-id.js:98 -msgid "Passing both `first` and `last` to paginate the `DKIMResults` connection is not supported." -msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DKIMResults` n'est pas supporté." +#~ msgid "Passing both `first` and `last` to paginate the `DKIMResults` connection is not supported." +#~ msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DKIMResults` n'est pas supporté." #: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:123 #: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:159 -msgid "Passing both `first` and `last` to paginate the `DKIM` connection is not supported." -msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DKIMResults` n'est pas supporté." +#~ msgid "Passing both `first` and `last` to paginate the `DKIM` connection is not supported." +#~ msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DKIMResults` n'est pas supporté." #: src/email-scan/loaders/load-dmarc-connections-by-domain-id.js:147 -msgid "Passing both `first` and `last` to paginate the `DMARC` connection is not supported." -msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DMARC` n'est pas supporté." +#~ msgid "Passing both `first` and `last` to paginate the `DMARC` connection is not supported." +#~ msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DMARC` n'est pas supporté." #: src/dmarc-summaries/loaders/load-dkim-failure-connections-by-sum-id.js:45 msgid "Passing both `first` and `last` to paginate the `DkimFailureTable` connection is not supported." @@ -119,12 +136,12 @@ msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DkimFailu msgid "Passing both `first` and `last` to paginate the `DmarcFailureTable` connection is not supported." msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DmarcFailureTable` n'est pas supporté." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:198 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:170 msgid "Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported." msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DmarcSummaries` n'est pas supporté." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:163 -#: src/domain/loaders/load-domain-connections-by-user-id.js:175 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:148 +#: src/domain/loaders/load-domain-connections-by-user-id.js:154 msgid "Passing both `first` and `last` to paginate the `Domain` connection is not supported." msgstr "Passer à la fois `first` et `last` pour paginer la connexion `Domain` n'est pas supporté." @@ -142,26 +159,26 @@ msgid "Passing both `first` and `last` to paginate the `GuidanceTag` connection msgstr "Passer à la fois `first` et `last` pour paginer la connexion `GuidanceTag` n'est pas supporté." #: src/web-scan/loaders/load-https-connections-by-domain-id.js:156 -msgid "Passing both `first` and `last` to paginate the `HTTPS` connection is not supported." -msgstr "Passer à la fois `first` et `last` pour paginer la connexion `HTTPS` n'est pas supporté." +#~ msgid "Passing both `first` and `last` to paginate the `HTTPS` connection is not supported." +#~ msgstr "Passer à la fois `first` et `last` pour paginer la connexion `HTTPS` n'est pas supporté." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:110 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:105 msgid "Passing both `first` and `last` to paginate the `Log` connection is not supported." msgstr "Passer à la fois `first` et `last` pour paginer la connexion `Log` n'est pas supporté." #: src/organization/loaders/load-organization-connections-by-domain-id.js:192 -#: src/organization/loaders/load-organization-connections-by-user-id.js:191 +#: src/organization/loaders/load-organization-connections-by-user-id.js:173 #: src/organization/loaders/load-web-check-connections-by-user-id.js:98 msgid "Passing both `first` and `last` to paginate the `Organization` connection is not supported." msgstr "Passer à la fois `first` et `last` pour paginer la connexion `Organization` n'est pas supporté." #: src/email-scan/loaders/load-spf-connections-by-domain-id.js:142 -msgid "Passing both `first` and `last` to paginate the `SPF` connection is not supported." -msgstr "Passer à la fois `first` et `last` pour paginer la connexion `SPF` n'est pas supporté." +#~ msgid "Passing both `first` and `last` to paginate the `SPF` connection is not supported." +#~ msgstr "Passer à la fois `first` et `last` pour paginer la connexion `SPF` n'est pas supporté." #: src/web-scan/loaders/load-ssl-connections-by-domain-id.js:180 -msgid "Passing both `first` and `last` to paginate the `SSL` connection is not supported." -msgstr "Passer à la fois `first` et `last` pour paginer la connexion `SSL` n'est pas supporté." +#~ msgid "Passing both `first` and `last` to paginate the `SSL` connection is not supported." +#~ msgstr "Passer à la fois `first` et `last` pour paginer la connexion `SSL` n'est pas supporté." #: src/dmarc-summaries/loaders/load-spf-failure-connections-by-sum-id.js:45 msgid "Passing both `first` and `last` to paginate the `SpfFailureTable` connection is not supported." @@ -182,7 +199,7 @@ msgid "Passing both `first` and `last` to paginate the `VerifiedOrganization` co msgstr "Passer à la fois `first` et `last` pour paginer la connexion `VerifiedOrganization` n'est pas supporté." #: src/user/mutations/reset-password.js:120 -#: src/user/mutations/sign-up.js:91 +#: src/user/mutations/sign-up.js:84 msgid "Password does not meet requirements." msgstr "Le mot de passe ne répond pas aux exigences." @@ -194,32 +211,32 @@ msgstr "Le mot de passe a été réinitialisé avec succès." msgid "Password was successfully updated." msgstr "Le mot de passe a été mis à jour avec succès." -#: src/user/mutations/sign-up.js:103 +#: src/user/mutations/sign-up.js:94 msgid "Passwords do not match." msgstr "Les mots de passe ne correspondent pas." -#: src/organization/queries/find-organization-by-slug.js:61 -#: src/organization/queries/find-organization-by-slug.js:74 +#: src/organization/queries/find-organization-by-slug.js:50 +#: src/organization/queries/find-organization-by-slug.js:55 msgid "Permission Denied: Could not retrieve specified organization." msgstr "Permission refusée : Impossible de récupérer l'organisation spécifiée." -#: src/user/mutations/update-user-profile.js:115 +#: src/user/mutations/update-user-profile.js:110 msgid "Permission Denied: Multi-factor authentication is required for admin accounts" msgstr "Permission refusée : L'authentification multifactorielle est requise pour les comptes admin." -#: src/affiliation/mutations/transfer-org-ownership.js:95 +#: src/affiliation/mutations/transfer-org-ownership.js:74 msgid "Permission Denied: Please contact org owner to transfer ownership." msgstr "Permission refusée : Veuillez contacter le propriétaire de l'org pour transférer la propriété." -#: src/domain/mutations/remove-organizations-domains.js:135 +#: src/domain/mutations/remove-organizations-domains.js:127 msgid "Permission Denied: Please contact organization admin for help with archiving domains." msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur l'archivage des domaines." -#: src/domain/mutations/remove-domain.js:117 +#: src/domain/mutations/remove-domain.js:94 msgid "Permission Denied: Please contact organization admin for help with removing domain." msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide afin de supprimer le domaine." -#: src/domain/mutations/remove-organizations-domains.js:122 +#: src/domain/mutations/remove-organizations-domains.js:116 msgid "Permission Denied: Please contact organization admin for help with removing domains." msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des domaines." @@ -227,7 +244,8 @@ msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisat msgid "Permission Denied: Please contact organization admin for help with removing organization." msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide afin de supprimer l'organisation." -#: src/affiliation/mutations/remove-user-from-org.js:233 +#: src/affiliation/mutations/remove-user-from-org.js:126 +#: src/affiliation/mutations/remove-user-from-org.js:138 msgid "Permission Denied: Please contact organization admin for help with removing users." msgstr "Autorisation refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des utilisateurs." @@ -235,39 +253,49 @@ msgstr "Autorisation refusée : Veuillez contacter l'administrateur de l'organis msgid "Permission Denied: Please contact organization admin for help with updating organization." msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des utilisateurs." -#: src/affiliation/mutations/update-user-role.js:186 -#: src/affiliation/mutations/update-user-role.js:209 -#: src/affiliation/mutations/update-user-role.js:227 -msgid "Permission Denied: Please contact organization admin for help with updating user roles." -msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la mise à jour des rôles des utilisateurs." +#: src/affiliation/mutations/update-user-role.js:170 +#: src/affiliation/mutations/update-user-role.js:193 +#: src/affiliation/mutations/update-user-role.js:210 +#~ msgid "Permission Denied: Please contact organization admin for help with updating user roles." +#~ msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la mise à jour des rôles des utilisateurs." -#: src/affiliation/mutations/invite-user-to-org.js:114 +#: src/affiliation/mutations/invite-user-to-org.js:99 msgid "Permission Denied: Please contact organization admin for help with user invitations." msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide concernant les invitations d'utilisateurs." -#: src/affiliation/mutations/update-user-role.js:114 +#: src/affiliation/mutations/update-user-role.js:109 +#: src/affiliation/mutations/update-user-role.js:160 +#: src/affiliation/mutations/update-user-role.js:172 msgid "Permission Denied: Please contact organization admin for help with user role changes." msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur les changements de rôle des utilisateurs." -#: src/domain/mutations/add-organizations-domains.js:131 -#: src/domain/mutations/create-domain.js:140 +#: src/domain/mutations/create-domain.js:121 msgid "Permission Denied: Please contact organization user for help with creating domain." msgstr "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la création du domaine." +#: src/domain/mutations/add-organizations-domains.js:130 +msgid "Permission Denied: Please contact organization user for help with creating domains." +msgstr "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la création de domaines." + #: src/domain/queries/find-domain-by-domain.js:57 +#: src/organization/objects/organization.js:98 msgid "Permission Denied: Please contact organization user for help with retrieving this domain." msgstr "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide pour récupérer ce domaine." -#: src/domain/mutations/request-scan.js:71 +#: src/domain/mutations/request-scan.js:63 msgid "Permission Denied: Please contact organization user for help with scanning this domain." msgstr "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur l'analyse de ce domaine." -#: src/domain/mutations/update-domain.js:150 +#: src/domain/mutations/update-domain.js:139 msgid "Permission Denied: Please contact organization user for help with updating this domain." msgstr "Autorisation refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la mise à jour de ce domaine." -#: src/domain/mutations/remove-domain.js:104 -#: src/domain/mutations/remove-organizations-domains.js:109 +#: src/organization/mutations/archive-organization.js:68 +msgid "Permission Denied: Please contact super admin for help with archiving organization." +msgstr "Permission refusée : Veuillez contacter le super administrateur pour obtenir de l'aide sur l'organisation de l'archivage." + +#: src/domain/mutations/remove-domain.js:107 +#: src/domain/mutations/remove-organizations-domains.js:105 msgid "Permission Denied: Please contact super admin for help with removing domain." msgstr "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la mise à jour de ce domaine." @@ -279,12 +307,12 @@ msgstr "Permission refusée : Veuillez contacter le super administrateur pour qu #~ msgid "Permission Denied: Please contact super admin for help with scanning this domain." #~ msgstr "Permission refusée : Veuillez contacter le super administrateur pour obtenir de l'aide sur l'analyse de ce domaine." -#: src/organization/mutations/verify-organization.js:69 +#: src/organization/mutations/verify-organization.js:68 msgid "Permission Denied: Please contact super admin for help with verifying this organization." msgstr "Permission refusée : Veuillez contacter le super administrateur pour qu'il vous aide à vérifier cette organisation." -#: src/auth/check-domain-permission.js:24 -#: src/auth/check-domain-permission.js:48 +#: src/auth/check-domain-permission.js:22 +#: src/auth/check-domain-permission.js:51 #: src/auth/check-domain-permission.js:61 msgid "Permission check error. Unable to request domain information." msgstr "Erreur de vérification des permissions. Impossible de demander des informations sur le domaine." @@ -292,16 +320,16 @@ msgstr "Erreur de vérification des permissions. Impossible de demander des info #: src/auth/check-user-is-admin-for-user.js:20 #: src/auth/check-user-is-admin-for-user.js:30 #: src/auth/check-user-is-admin-for-user.js:63 -#: src/auth/check-user-is-admin-for-user.js:75 +#: src/auth/check-user-is-admin-for-user.js:73 msgid "Permission error, not an admin for this user." msgstr "Erreur de permission, pas d'administrateur pour cet utilisateur." -#: src/user/mutations/close-account.js:56 +#: src/user/mutations/close-account.js:54 msgid "Permission error: Unable to close other user's account." msgstr "Erreur de permission: Impossible de fermer le compte d'un autre utilisateur." #: src/auth/super-admin-required.js:15 -#: src/organization/queries/get-all-organization-domain-statuses.js:36 +#: src/organization/queries/get-all-organization-domain-statuses.js:27 msgid "Permissions error. You do not have sufficient permissions to access this data." msgstr "Erreur de permissions. Vous n'avez pas les autorisations suffisantes pour accéder à ces données." @@ -313,11 +341,11 @@ msgstr "Le numéro de téléphone a été supprimé avec succès." msgid "Phone number has been successfully set, you will receive a verification text message shortly." msgstr "Le numéro de téléphone a été configuré avec succès, vous recevrez bientôt un message de vérification." -#: src/user/mutations/update-user-profile.js:198 +#: src/user/mutations/update-user-profile.js:184 msgid "Profile successfully updated." msgstr "Le profil a été mis à jour avec succès." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:103 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:95 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:193 msgid "Requesting `{amount}` records on the `Affiliation` connection exceeds the `{argSet}` limit of 100 records." msgstr "La demande d'enregistrements `{amount}` sur la connexion `Affiliation` dépasse la limite `{argSet}` de 100 enregistrements." @@ -330,12 +358,12 @@ msgstr "La demande d'enregistrements `{amount}` sur la connexion `DkimFailureTab msgid "Requesting `{amount}` records on the `DmarcFailureTable` connection exceeds the `{argSet}` limit of 100 records." msgstr "La demande d'enregistrements `{amount}` sur la connexion `DkimFailureTable` dépasse la limite `{argSet}` de 100 enregistrements." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:221 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:188 msgid "Requesting `{amount}` records on the `DmarcSummaries` connection exceeds the `{argSet}` limit of 100 records." msgstr "La demande d'enregistrements `{amount}` sur la connexion `DmarcSummaries` dépasse la limite `{argSet}` de 100 enregistrements." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:186 -#: src/domain/loaders/load-domain-connections-by-user-id.js:198 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:164 +#: src/domain/loaders/load-domain-connections-by-user-id.js:172 msgid "Requesting `{amount}` records on the `Domain` connection exceeds the `{argSet}` limit of 100 records." msgstr "La demande d'enregistrements `{amount}` sur la connexion `Domain` dépasse la limite `{argSet}` de 100 enregistrements." @@ -352,12 +380,12 @@ msgstr "La demande d'enregistrements `{amount}` sur la connexion `FullPassTable` msgid "Requesting `{amount}` records on the `GuidanceTag` connection exceeds the `{argSet}` limit of 100 records." msgstr "La demande d'enregistrements `{amount}` sur la connexion `GuidanceTag` dépasse la limite `{argSet}` de 100 enregistrements." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:133 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:119 msgid "Requesting `{amount}` records on the `Log` connection exceeds the `{argSet}` limit of 100 records." msgstr "La demande d'enregistrements `{amount}` sur la connexion `Log` dépasse la limite `{argSet}` de 100 enregistrements." #: src/organization/loaders/load-organization-connections-by-domain-id.js:215 -#: src/organization/loaders/load-organization-connections-by-user-id.js:214 +#: src/organization/loaders/load-organization-connections-by-user-id.js:187 #: src/organization/loaders/load-web-check-connections-by-user-id.js:121 msgid "Requesting `{amount}` records on the `Organization` connection exceeds the `{argSet}` limit of 100 records." msgstr "La demande d'enregistrements `{amount}` sur la connexion `Organization` dépasse la limite `{argSet}` de 100 enregistrements." @@ -381,38 +409,46 @@ msgid "Requesting `{amount}` records on the `VerifiedOrganization` connection ex msgstr "La demande d'enregistrements `{amount}` sur la connexion `VerifiedOrganization` dépasse la limite `{argSet}` de 100 enregistrements." #: src/email-scan/loaders/load-dkim-results-connections-by-dkim-id.js:121 -msgid "Requesting {amount} records on the `DKIMResults` connection exceeds the `{argSet}` limit of 100 records." -msgstr "La demande de {amount} enregistrements sur la connexion `DKIMResults` dépasse la limite `{argSet}` de 100 enregistrements." +#~ msgid "Requesting {amount} records on the `DKIMResults` connection exceeds the `{argSet}` limit of 100 records." +#~ msgstr "La demande de {amount} enregistrements sur la connexion `DKIMResults` dépasse la limite `{argSet}` de 100 enregistrements." #: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:146 -msgid "Requesting {amount} records on the `DKIM` connection exceeds the `{argSet}` limit of 100 records." -msgstr "La demande de {amount} enregistrements sur la connexion `DKIM` dépasse la limite `{argSet}` de 100 enregistrements." +#~ msgid "Requesting {amount} records on the `DKIM` connection exceeds the `{argSet}` limit of 100 records." +#~ msgstr "La demande de {amount} enregistrements sur la connexion `DKIM` dépasse la limite `{argSet}` de 100 enregistrements." #: src/email-scan/loaders/load-dmarc-connections-by-domain-id.js:171 -msgid "Requesting {amount} records on the `DMARC` connection exceeds the `{argSet}` limit of 100 records." -msgstr "La demande de {amount} enregistrements sur la connexion `DMARC` dépasse la limite `{argSet}` de 100 enregistrements." +#~ msgid "Requesting {amount} records on the `DMARC` connection exceeds the `{argSet}` limit of 100 records." +#~ msgstr "La demande de {amount} enregistrements sur la connexion `DMARC` dépasse la limite `{argSet}` de 100 enregistrements." #: src/web-scan/loaders/load-https-connections-by-domain-id.js:179 -msgid "Requesting {amount} records on the `HTTPS` connection exceeds the `{argSet}` limit of 100 records." -msgstr "La demande de {amount} enregistrements sur la connexion `HTTPS` dépasse la limite `{argSet}` de 100 enregistrements." +#~ msgid "Requesting {amount} records on the `HTTPS` connection exceeds the `{argSet}` limit of 100 records." +#~ msgstr "La demande de {amount} enregistrements sur la connexion `HTTPS` dépasse la limite `{argSet}` de 100 enregistrements." #: src/email-scan/loaders/load-spf-connections-by-domain-id.js:165 -msgid "Requesting {amount} records on the `SPF` connection exceeds the `{argSet}` limit of 100 records." -msgstr "La demande de {amount} enregistrements sur la connexion `SPF` dépasse la limite `{argSet}` de 100 enregistrements." +#~ msgid "Requesting {amount} records on the `SPF` connection exceeds the `{argSet}` limit of 100 records." +#~ msgstr "La demande de {amount} enregistrements sur la connexion `SPF` dépasse la limite `{argSet}` de 100 enregistrements." #: src/web-scan/loaders/load-ssl-connections-by-domain-id.js:203 -msgid "Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records." -msgstr "La demande de {amount} enregistrements sur la connexion `SSL` dépasse la limite `{argSet}` de 100 enregistrements." +#~ msgid "Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records." +#~ msgstr "La demande de {amount} enregistrements sur la connexion `SSL` dépasse la limite `{argSet}` de 100 enregistrements." + +#: src/domain/mutations/add-organizations-domains.js:353 +msgid "Successfully added {domainCount} domain(s) to {0}." +msgstr "Ajouté avec succès le(s) domaine(s) {domainCount} à {0}." #: src/domain/mutations/add-organizations-domains.js:351 -msgid "Successfully added {domainCount} domains to {0}." -msgstr "Ajouté avec succès les domaines {domainCount} à {0}." +#~ msgid "Successfully added {domainCount} domains to {0}." +#~ msgstr "Ajouté avec succès les domaines {domainCount} à {0}." + +#: src/organization/mutations/archive-organization.js:188 +msgid "Successfully archived organization: {0}." +msgstr "Organisation archivée avec succès : {0}." -#: src/user/mutations/close-account.js:521 +#: src/user/mutations/close-account.js:405 msgid "Successfully closed account." msgstr "Le compte a été fermé avec succès." -#: src/domain/mutations/request-scan.js:94 +#: src/domain/mutations/request-scan.js:166 msgid "Successfully dispatched one time scan." msgstr "Un seul balayage a été effectué avec succès." @@ -420,11 +456,11 @@ msgstr "Un seul balayage a été effectué avec succès." msgid "Successfully email verified account, and set TFA send method to email." msgstr "Réussir à envoyer un email au compte vérifié, et définir la méthode d'envoi de la TFA sur email." -#: src/affiliation/mutations/invite-user-to-org.js:241 +#: src/affiliation/mutations/invite-user-to-org.js:280 msgid "Successfully invited user to organization, and sent notification email." msgstr "L'utilisateur a été invité avec succès à l'organisation et l'email de notification a été envoyé." -#: src/affiliation/mutations/leave-organization.js:438 +#: src/affiliation/mutations/leave-organization.js:84 msgid "Successfully left organization: {0}" msgstr "L'organisation a été quittée avec succès: {0}" @@ -432,23 +468,31 @@ msgstr "L'organisation a été quittée avec succès: {0}" msgid "Successfully removed domain: {0} from favourites." msgstr "A réussi à supprimer le domaine : {0} des favoris." -#: src/domain/mutations/remove-domain.js:395 +#: src/domain/mutations/remove-domain.js:326 msgid "Successfully removed domain: {0} from {1}." msgstr "A réussi à supprimer le domaine : {0} de {1}." -#: src/organization/mutations/remove-organization.js:502 +#: src/organization/mutations/remove-organization.js:384 msgid "Successfully removed organization: {0}." msgstr "A réussi à supprimer l'organisation : {0}." -#: src/affiliation/mutations/remove-user-from-org.js:219 +#: src/affiliation/mutations/remove-user-from-org.js:195 msgid "Successfully removed user from organization." msgstr "L'utilisateur a été retiré de l'organisation avec succès." +#: src/domain/mutations/remove-organizations-domains.js:459 +msgid "Successfully removed {domainCount} domain(s) from {0}." +msgstr "Supprimé avec succès le(s) domaine(s) {domainCount} de {0}." + #: src/domain/mutations/remove-organizations-domains.js:530 -msgid "Successfully removed {domainCount} domains from {0}." -msgstr "Suppression réussie des domaines {domainCount} de {0}." +#~ msgid "Successfully removed {domainCount} domains from {0}." +#~ msgstr "Suppression réussie des domaines {domainCount} de {0}." + +#: src/affiliation/mutations/request-org-affiliation.js:197 +msgid "Successfully requested invite to organization, and sent notification email." +msgstr "La demande d'invitation à l'organisation a été effectuée avec succès et un courriel de notification a été envoyé." -#: src/affiliation/mutations/invite-user-to-org.js:167 +#: src/affiliation/mutations/invite-user-to-org.js:172 msgid "Successfully sent invitation to service, and organization email." msgstr "Envoi réussi de l'invitation au service, et de l'email de l'organisation." @@ -456,11 +500,11 @@ msgstr "Envoi réussi de l'invitation au service, et de l'email de l'organisatio msgid "Successfully signed out." msgstr "J'ai réussi à me déconnecter." -#: src/affiliation/mutations/transfer-org-ownership.js:218 +#: src/affiliation/mutations/transfer-org-ownership.js:185 msgid "Successfully transferred org: {0} ownership to user: {1}" msgstr "A réussi à transférer la propriété de org: {0} à l'utilisateur: {1}" -#: src/organization/mutations/verify-organization.js:151 +#: src/organization/mutations/verify-organization.js:119 msgid "Successfully verified organization: {0}." msgstr "Envoi réussi de l'invitation au service, et de l'email de l'organisation." @@ -484,74 +528,79 @@ msgstr "Le code à deux facteurs est incorrect. Veuillez réessayer." msgid "Two factor code length is incorrect. Please try again." msgstr "La longueur du code à deux facteurs est incorrecte. Veuillez réessayer." -#: src/affiliation/mutations/leave-organization.js:74 -#: src/affiliation/mutations/leave-organization.js:84 -#: src/affiliation/mutations/leave-organization.js:115 -#: src/affiliation/mutations/leave-organization.js:132 -#: src/affiliation/mutations/leave-organization.js:163 -#: src/affiliation/mutations/leave-organization.js:173 -#: src/affiliation/mutations/leave-organization.js:211 -#: src/affiliation/mutations/leave-organization.js:333 -#: src/affiliation/mutations/leave-organization.js:366 -#: src/affiliation/mutations/leave-organization.js:403 -#: src/affiliation/mutations/leave-organization.js:421 -#: src/affiliation/mutations/leave-organization.js:431 +#: src/affiliation/mutations/leave-organization.js:70 +#: src/affiliation/mutations/leave-organization.js:77 msgid "Unable leave organization. Please try again." msgstr "Impossible de quitter l'organisation. Veuillez réessayer." +#: src/domain/mutations/add-organizations-domains.js:115 +msgid "Unable to add domains in unknown organization." +msgstr "Impossible d'ajouter des domaines dans une organisation inconnue." + +#: src/organization/mutations/archive-organization.js:101 +#: src/organization/mutations/archive-organization.js:111 +#: src/organization/mutations/archive-organization.js:129 +#: src/organization/mutations/archive-organization.js:146 +#: src/organization/mutations/archive-organization.js:155 +msgid "Unable to archive organization. Please try again." +msgstr "Impossible d'archiver l'organisation. Veuillez réessayer." + +#: src/organization/mutations/archive-organization.js:54 +msgid "Unable to archive unknown organization." +msgstr "Impossible d'archiver une organisation inconnue." + #: src/user/mutations/authenticate.js:77 #: src/user/mutations/authenticate.js:117 #: src/user/mutations/authenticate.js:126 msgid "Unable to authenticate. Please try again." msgstr "Impossible de s'authentifier. Veuillez réessayer." -#: src/auth/check-permission.js:30 -#: src/auth/check-permission.js:58 +#: src/auth/check-permission.js:26 +#: src/auth/check-permission.js:64 #: src/auth/check-super-admin.js:20 #: src/auth/check-super-admin.js:30 msgid "Unable to check permission. Please try again." msgstr "Impossible de vérifier l'autorisation. Veuillez réessayer." -#: src/user/mutations/close-account.js:69 +#: src/user/mutations/close-account.js:66 msgid "Unable to close account of an undefined user." msgstr "Impossible de fermer le compte d'un utilisateur non défini." -#: src/user/mutations/close-account.js:92 -#: src/user/mutations/close-account.js:102 -#: src/user/mutations/close-account.js:121 -#: src/user/mutations/close-account.js:131 -#: src/user/mutations/close-account.js:162 -#: src/user/mutations/close-account.js:177 -#: src/user/mutations/close-account.js:206 -#: src/user/mutations/close-account.js:216 -#: src/user/mutations/close-account.js:252 -#: src/user/mutations/close-account.js:364 -#: src/user/mutations/close-account.js:395 -#: src/user/mutations/close-account.js:420 -#: src/user/mutations/close-account.js:456 -#: src/user/mutations/close-account.js:473 -#: src/user/mutations/close-account.js:488 -#: src/user/mutations/close-account.js:497 +#: src/user/mutations/close-account.js:89 +#: src/user/mutations/close-account.js:99 +#: src/user/mutations/close-account.js:118 +#: src/user/mutations/close-account.js:128 +#: src/user/mutations/close-account.js:159 +#: src/user/mutations/close-account.js:174 +#: src/user/mutations/close-account.js:203 +#: src/user/mutations/close-account.js:213 +#: src/user/mutations/close-account.js:239 +#: src/user/mutations/close-account.js:257 +#: src/user/mutations/close-account.js:286 +#: src/user/mutations/close-account.js:309 +#: src/user/mutations/close-account.js:344 +#: src/user/mutations/close-account.js:361 +#: src/user/mutations/close-account.js:376 +#: src/user/mutations/close-account.js:383 msgid "Unable to close account. Please try again." msgstr "Impossible de fermer le compte. Veuillez réessayer." -#: src/domain/mutations/add-organizations-domains.js:115 -#: src/domain/mutations/create-domain.js:120 +#: src/domain/mutations/create-domain.js:107 msgid "Unable to create domain in unknown organization." msgstr "Impossible de créer un domaine dans une organisation inconnue." -#: src/domain/mutations/create-domain.js:198 +#: src/domain/mutations/create-domain.js:174 msgid "Unable to create domain, organization has already claimed it." msgstr "Impossible de créer le domaine, l'organisation l'a déjà réclamé." -#: src/domain/mutations/create-domain.js:177 -#: src/domain/mutations/create-domain.js:187 -#: src/domain/mutations/create-domain.js:230 -#: src/domain/mutations/create-domain.js:240 +#: src/domain/mutations/create-domain.js:156 +#: src/domain/mutations/create-domain.js:164 +#: src/domain/mutations/create-domain.js:203 +#: src/domain/mutations/create-domain.js:213 +#: src/domain/mutations/create-domain.js:231 #: src/domain/mutations/create-domain.js:260 -#: src/domain/mutations/create-domain.js:291 -#: src/domain/mutations/create-domain.js:311 -#: src/domain/mutations/create-domain.js:321 +#: src/domain/mutations/create-domain.js:278 +#: src/domain/mutations/create-domain.js:286 msgid "Unable to create domain. Please try again." msgstr "Impossible de créer un domaine. Veuillez réessayer." @@ -559,9 +608,9 @@ msgstr "Impossible de créer un domaine. Veuillez réessayer." msgid "Unable to create domains. Please try again." msgstr "Impossible de créer des domaines. Veuillez réessayer." -#: src/organization/mutations/create-organization.js:201 -#: src/organization/mutations/create-organization.js:224 -#: src/organization/mutations/create-organization.js:235 +#: src/organization/mutations/create-organization.js:182 +#: src/organization/mutations/create-organization.js:202 +#: src/organization/mutations/create-organization.js:211 msgid "Unable to create organization. Please try again." msgstr "Impossible de créer une organisation. Veuillez réessayer." @@ -571,6 +620,11 @@ msgstr "Impossible de créer une organisation. Veuillez réessayer." #~ msgid "Unable to dispatch one time scan. Please try again." #~ msgstr "Impossible d'envoyer un scan unique. Veuillez réessayer." +#: src/organization/objects/organization.js:139 +#: src/organization/objects/organization.js:149 +msgid "Unable to export organization. Please try again." +msgstr "Impossible d'exporter l'organisation. Veuillez réessayer." + #: src/domain/mutations/favourite-domain.js:94 msgid "Unable to favourite domain, user has already favourited it." msgstr "Impossible de favoriser le domaine, l'utilisateur l'a déjà favorisé." @@ -600,13 +654,13 @@ msgstr "Impossible de trouver le(s) tag(s) d'orientation DKIM. Veuillez réessay #: src/email-scan/loaders/load-dkim-result-by-key.js:20 #: src/email-scan/loaders/load-dkim-result-by-key.js:34 -msgid "Unable to find DKIM result(s). Please try again." -msgstr "Impossible de trouver le(s) résultat(s) DKIM. Veuillez réessayer." +#~ msgid "Unable to find DKIM result(s). Please try again." +#~ msgstr "Impossible de trouver le(s) résultat(s) DKIM. Veuillez réessayer." #: src/email-scan/loaders/load-dkim-by-key.js:19 #: src/email-scan/loaders/load-dkim-by-key.js:31 -msgid "Unable to find DKIM scan(s). Please try again." -msgstr "Impossible de trouver le(s) scan(s) DKIM. Veuillez réessayer." +#~ msgid "Unable to find DKIM scan(s). Please try again." +#~ msgstr "Impossible de trouver le(s) scan(s) DKIM. Veuillez réessayer." #: src/guidance-tag/loaders/load-dmarc-guidance-tags.js:34 #: src/guidance-tag/loaders/load-dmarc-guidance-tags.js:48 @@ -615,42 +669,47 @@ msgstr "Impossible de trouver le(s) tag(s) d'orientation DMARC. Veuillez réessa #: src/email-scan/loaders/load-dmarc-by-key.js:20 #: src/email-scan/loaders/load-dmarc-by-key.js:34 -msgid "Unable to find DMARC scan(s). Please try again." -msgstr "Impossible de trouver le(s) scan(s) DMARC. Veuillez réessayer." +#~ msgid "Unable to find DMARC scan(s). Please try again." +#~ msgstr "Impossible de trouver le(s) scan(s) DMARC. Veuillez réessayer." #: src/dmarc-summaries/loaders/load-dmarc-summary-by-key.js:37 #: src/dmarc-summaries/loaders/load-dmarc-summary-by-key.js:51 msgid "Unable to find DMARC summary data. Please try again." msgstr "Impossible de trouver les données de synthèse DMARC. Veuillez réessayer." -#: src/guidance-tag/loaders/load-https-guidance-tags.js:34 -#: src/guidance-tag/loaders/load-https-guidance-tags.js:48 +#: src/dns-scan/loaders/load-dns-by-key.js:20 +#: src/dns-scan/loaders/load-dns-by-key.js:34 +msgid "Unable to find DNS scan(s). Please try again." +msgstr "Impossible de trouver le(s) scan(s) DNS. Veuillez réessayer." + +#: src/guidance-tag/loaders/load-https-guidance-tags.js:33 +#: src/guidance-tag/loaders/load-https-guidance-tags.js:47 msgid "Unable to find HTTPS guidance tag(s). Please try again." msgstr "Impossible de trouver la ou les balises d'orientation HTTPS. Veuillez réessayer." #: src/web-scan/loaders/load-https-by-key.js:19 -msgid "Unable to find HTTPS scan(s). Please try again." -msgstr "Impossible de trouver le(s) scan(s) HTTPS. Veuillez réessayer." +#~ msgid "Unable to find HTTPS scan(s). Please try again." +#~ msgstr "Impossible de trouver le(s) scan(s) HTTPS. Veuillez réessayer." -#: src/guidance-tag/loaders/load-spf-guidance-tags.js:29 -#: src/guidance-tag/loaders/load-spf-guidance-tags.js:43 +#: src/guidance-tag/loaders/load-spf-guidance-tags.js:28 +#: src/guidance-tag/loaders/load-spf-guidance-tags.js:42 msgid "Unable to find SPF guidance tag(s). Please try again." msgstr "Impossible de trouver le(s) tag(s) d'orientation SPF. Veuillez réessayer." #: src/email-scan/loaders/load-spf-by-key.js:19 #: src/email-scan/loaders/load-spf-by-key.js:31 -msgid "Unable to find SPF scan(s). Please try again." -msgstr "Impossible de trouver le(s) scan(s) SPF. Veuillez réessayer." +#~ msgid "Unable to find SPF scan(s). Please try again." +#~ msgstr "Impossible de trouver le(s) scan(s) SPF. Veuillez réessayer." -#: src/guidance-tag/loaders/load-ssl-guidance-tags.js:29 -#: src/guidance-tag/loaders/load-ssl-guidance-tags.js:43 +#: src/guidance-tag/loaders/load-ssl-guidance-tags.js:28 +#: src/guidance-tag/loaders/load-ssl-guidance-tags.js:42 msgid "Unable to find SSL guidance tag(s). Please try again." msgstr "Impossible de trouver le(s) tag(s) d'orientation SSL. Veuillez réessayer." #: src/web-scan/loaders/load-ssl-by-key.js:18 #: src/web-scan/loaders/load-ssl-by-key.js:30 -msgid "Unable to find SSL scan(s). Please try again." -msgstr "Impossible de trouver le(s) scan(s) SSL. Veuillez réessayer." +#~ msgid "Unable to find SSL scan(s). Please try again." +#~ msgstr "Impossible de trouver le(s) scan(s) SSL. Veuillez réessayer." #: src/domain/queries/find-domain-by-domain.js:46 msgid "Unable to find the requested domain." @@ -668,20 +727,35 @@ msgstr "Impossible de trouver l'affiliation de l'utilisateur (s). Veuillez rées msgid "Unable to find verified organization(s). Please try again." msgstr "Impossible de trouver une ou plusieurs organisations vérifiées. Veuillez réessayer." -#: src/affiliation/mutations/invite-user-to-org.js:95 +#: src/affiliation/mutations/invite-user-to-org.js:117 +#: src/affiliation/mutations/invite-user-to-org.js:127 +#: src/affiliation/mutations/invite-user-to-org.js:192 +msgid "Unable to invite user to organization. Please try again." +msgstr "Impossible d'inviter un utilisateur dans une organisation. Veuillez réessayer." + +#: src/affiliation/mutations/invite-user-to-org.js:204 +msgid "Unable to invite user to organization. User is already affiliated with organization." +msgstr "Impossible d'inviter un utilisateur dans une organisation. L'utilisateur est déjà affilié à l'organisation." + +#: src/affiliation/mutations/invite-user-to-org.js:82 msgid "Unable to invite user to unknown organization." msgstr "Impossible d'inviter un utilisateur à une organisation inconnue." -#: src/affiliation/mutations/invite-user-to-org.js:194 -#: src/affiliation/mutations/invite-user-to-org.js:209 +#: src/affiliation/mutations/invite-user-to-org.js:233 +#: src/affiliation/mutations/invite-user-to-org.js:252 msgid "Unable to invite user. Please try again." msgstr "Impossible d'inviter un utilisateur. Veuillez réessayer." -#: src/affiliation/mutations/invite-user-to-org.js:81 +#: src/affiliation/mutations/invite-user-to-org.js:68 msgid "Unable to invite yourself to an org." msgstr "Impossible de s'inviter à un org." -#: src/affiliation/mutations/leave-organization.js:51 +#: src/affiliation/mutations/leave-organization.js:190 +#: src/affiliation/mutations/leave-organization.js:208 +#~ msgid "Unable to leave organization. Please try again." +#~ msgstr "Impossible de quitter l'organisation. Veuillez réessayer." + +#: src/affiliation/mutations/leave-organization.js:48 msgid "Unable to leave undefined organization." msgstr "Impossible de quitter une organisation non définie." @@ -703,13 +777,17 @@ msgstr "Impossible de charger le(s) tag(s) d'orientation DKIM. Veuillez réessay #: src/email-scan/loaders/load-dkim-results-connections-by-dkim-id.js:258 #: src/email-scan/loaders/load-dkim-results-connections-by-dkim-id.js:270 -msgid "Unable to load DKIM result(s). Please try again." -msgstr "Impossible de charger le(s) résultat(s) DKIM. Veuillez réessayer." +#~ msgid "Unable to load DKIM result(s). Please try again." +#~ msgstr "Impossible de charger le(s) résultat(s) DKIM. Veuillez réessayer." #: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:279 #: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:289 -msgid "Unable to load DKIM scan(s). Please try again." -msgstr "Impossible de charger le(s) scan(s) DKIM. Veuillez réessayer." +#~ msgid "Unable to load DKIM scan(s). Please try again." +#~ msgstr "Impossible de charger le(s) scan(s) DKIM. Veuillez réessayer." + +#: src/summaries/queries/dkim-summary.js:12 +msgid "Unable to load DKIM summary. Please try again." +msgstr "Impossible de charger le résumé DKIM. Veuillez réessayer." #: src/dmarc-summaries/loaders/load-dmarc-failure-connections-by-sum-id.js:13 #: src/dmarc-summaries/loaders/load-dmarc-failure-connections-by-sum-id.js:141 @@ -728,11 +806,11 @@ msgstr "Impossible de charger le résumé DMARC. Veuillez réessayer." #: src/email-scan/loaders/load-dmarc-connections-by-domain-id.js:319 #: src/email-scan/loaders/load-dmarc-connections-by-domain-id.js:331 -msgid "Unable to load DMARC scan(s). Please try again." -msgstr "Impossible de charger le(s) scan(s) DMARC. Veuillez réessayer." +#~ msgid "Unable to load DMARC scan(s). Please try again." +#~ msgstr "Impossible de charger le(s) scan(s) DMARC. Veuillez réessayer." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:477 -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:489 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:446 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:456 #: src/dmarc-summaries/loaders/load-dmarc-sum-edge-by-domain-id-period.js:20 #: src/dmarc-summaries/loaders/load-dmarc-sum-edge-by-domain-id-period.js:32 #: src/dmarc-summaries/loaders/load-yearly-dmarc-sum-edges.js:20 @@ -740,6 +818,15 @@ msgstr "Impossible de charger le(s) scan(s) DMARC. Veuillez réessayer." msgid "Unable to load DMARC summary data. Please try again." msgstr "Impossible de charger les données de synthèse DMARC. Veuillez réessayer." +#: src/summaries/queries/dmarc-summary.js:12 +msgid "Unable to load DMARC summary. Please try again." +msgstr "Impossible de charger le résumé DMARC. Veuillez réessayer." + +#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:154 +#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:164 +msgid "Unable to load DNS scan(s). Please try again." +msgstr "Impossible de charger le(s) scan(s) DNS. Veuillez réessayer." + #: src/guidance-tag/loaders/load-https-guidance-tags-connections.js:260 #: src/guidance-tag/loaders/load-https-guidance-tags-connections.js:272 msgid "Unable to load HTTPS guidance tag(s). Please try again." @@ -748,8 +835,8 @@ msgstr "Impossible de charger la ou les balises d'orientation HTTPS. Veuillez r #: src/web-scan/loaders/load-https-by-key.js:33 #: src/web-scan/loaders/load-https-connections-by-domain-id.js:333 #: src/web-scan/loaders/load-https-connections-by-domain-id.js:345 -msgid "Unable to load HTTPS scan(s). Please try again." -msgstr "Impossible de charger le(s) scan(s) HTTPS. Veuillez réessayer." +#~ msgid "Unable to load HTTPS scan(s). Please try again." +#~ msgstr "Impossible de charger le(s) scan(s) HTTPS. Veuillez réessayer." #: src/summaries/queries/https-summary.js:13 msgid "Unable to load HTTPS summary. Please try again." @@ -768,8 +855,12 @@ msgstr "Impossible de charger le(s) tag(s) d'orientation SPF. Veuillez réessaye #: src/email-scan/loaders/load-spf-connections-by-domain-id.js:306 #: src/email-scan/loaders/load-spf-connections-by-domain-id.js:316 -msgid "Unable to load SPF scan(s). Please try again." -msgstr "Impossible de charger le(s) scan(s) SPF. Veuillez réessayer." +#~ msgid "Unable to load SPF scan(s). Please try again." +#~ msgstr "Impossible de charger le(s) scan(s) SPF. Veuillez réessayer." + +#: src/summaries/queries/spf-summary.js:12 +msgid "Unable to load SPF summary. Please try again." +msgstr "Impossible de charger le résumé SPF. Veuillez réessayer." #: src/guidance-tag/loaders/load-ssl-guidance-tags-connections.js:260 #: src/guidance-tag/loaders/load-ssl-guidance-tags-connections.js:272 @@ -778,27 +869,31 @@ msgstr "Impossible de charger le(s) tag(s) d'orientation SSL. Veuillez réessaye #: src/web-scan/loaders/load-ssl-connections-by-domain-id.js:380 #: src/web-scan/loaders/load-ssl-connections-by-domain-id.js:390 -msgid "Unable to load SSL scan(s). Please try again." -msgstr "Impossible de charger le(s) scan(s) SSL. Veuillez réessayer." +#~ msgid "Unable to load SSL scan(s). Please try again." +#~ msgstr "Impossible de charger le(s) scan(s) SSL. Veuillez réessayer." + +#: src/summaries/queries/ssl-summary.js:12 +msgid "Unable to load SSL summary. Please try again." +msgstr "Impossible de charger le résumé SSL. Veuillez réessayer." -#: src/auth/check-user-belongs-to-org.js:22 +#: src/auth/check-user-belongs-to-org.js:20 msgid "Unable to load affiliation information. Please try again." msgstr "Impossible de charger les informations d'affiliation. Veuillez réessayer." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:266 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:259 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:449 msgid "Unable to load affiliation(s). Please try again." msgstr "Impossible de charger l'affiliation (s). Veuillez réessayer." -#: src/organization/loaders/load-all-organization-domain-statuses.js:36 +#: src/organization/loaders/load-all-organization-domain-statuses.js:32 msgid "Unable to load all organization domain statuses. Please try again." msgstr "Impossible de charger tous les statuts de domaine d'organisation. Veuillez réessayer." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:547 -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:557 -#: src/domain/loaders/load-domain-connections-by-user-id.js:471 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:549 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:559 +#: src/domain/loaders/load-domain-connections-by-user-id.js:448 #: src/domain/loaders/load-domain-tags-by-org-id.js:27 -#: src/user/loaders/load-my-tracker-by-user-id.js:43 +#: src/user/loaders/load-my-tracker-by-user-id.js:44 msgid "Unable to load domain(s). Please try again." msgstr "Impossible de charger le(s) domaine(s). Veuillez réessayer." @@ -815,7 +910,7 @@ msgstr "Impossible de charger le domaine. Veuillez réessayer." msgid "Unable to load full pass data. Please try again." msgstr "Impossible de charger les données complètes de la passe. Veuillez réessayer." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:340 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:318 msgid "Unable to load log(s). Please try again." msgstr "Impossible de charger le(s) journal(s). Veuillez réessayer." @@ -838,15 +933,15 @@ msgstr "Impossible de charger les statuts des domaines d'organisation. Veuillez #: src/organization/loaders/load-organization-by-slug.js:51 #: src/organization/loaders/load-organization-connections-by-domain-id.js:533 #: src/organization/loaders/load-organization-connections-by-domain-id.js:545 -#: src/organization/loaders/load-organization-connections-by-user-id.js:520 -#: src/organization/loaders/load-organization-connections-by-user-id.js:532 +#: src/organization/loaders/load-organization-connections-by-user-id.js:505 +#: src/organization/loaders/load-organization-connections-by-user-id.js:515 #: src/organization/loaders/load-web-check-connections-by-user-id.js:331 #: src/organization/loaders/load-web-check-connections-by-user-id.js:343 msgid "Unable to load organization(s). Please try again." msgstr "Impossible de charger l'organisation (s). Veuillez réessayer." -#: src/auth/check-org-owner.js:22 -#: src/auth/check-org-owner.js:34 +#: src/auth/check-org-owner.js:19 +#: src/auth/check-org-owner.js:27 msgid "Unable to load owner information. Please try again." msgstr "Impossible de charger les informations sur le propriétaire. Veuillez réessayer." @@ -885,21 +980,33 @@ msgstr "Impossible de charger le(s) domaine(s) vérifié(s). Veuillez réessayer msgid "Unable to load verified organization(s). Please try again." msgstr "Impossible de charger le(s) organisme(s) vérifié(s). Veuillez réessayer." +#: src/summaries/queries/web-connections-summary.js:12 +msgid "Unable to load web connections summary. Please try again." +msgstr "Impossible de charger le résumé des connexions web. Veuillez réessayer." + +#: src/web-scan/loaders/load-web-connections-by-domain-id.js:169 +#: src/web-scan/loaders/load-web-connections-by-domain-id.js:179 +#: src/web-scan/loaders/load-web-scans-by-web-id.js:9 +#: src/web-scan/loaders/load-web-scans-by-web-id.js:25 +#: src/web-scan/loaders/load-web-scans-by-web-id.js:35 +msgid "Unable to load web scan(s). Please try again." +msgstr "Impossible de charger le(s) scan(s) web. Veuillez réessayer." + #: src/summaries/queries/web-summary.js:13 msgid "Unable to load web summary. Please try again." msgstr "Impossible de charger le résumé web. Veuillez réessayer." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:254 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:249 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:437 msgid "Unable to query affiliation(s). Please try again." msgstr "Impossible de demander l'affiliation (s). Veuillez réessayer." -#: src/domain/loaders/load-domain-connections-by-user-id.js:461 -#: src/user/loaders/load-my-tracker-by-user-id.js:33 +#: src/domain/loaders/load-domain-connections-by-user-id.js:438 +#: src/user/loaders/load-my-tracker-by-user-id.js:34 msgid "Unable to query domain(s). Please try again." msgstr "Impossible d'interroger le(s) domaine(s). Veuillez réessayer." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:330 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:308 msgid "Unable to query log(s). Please try again." msgstr "Impossible d'interroger le(s) journal(s). Veuillez réessayer." @@ -917,34 +1024,31 @@ msgstr "Impossible d'interroger le(s) utilisateur(s). Veuillez réessayer." msgid "Unable to refresh tokens, please sign in." msgstr "Impossible de rafraîchir les jetons, veuillez vous connecter." -#: src/affiliation/mutations/remove-user-from-org.js:124 +#: src/affiliation/mutations/remove-user-from-org.js:104 msgid "Unable to remove a user that already does not belong to this organization." msgstr "Impossible de supprimer un utilisateur qui n'appartient déjà plus à cette organisation." -#: src/domain/mutations/remove-domain.js:87 +#: src/domain/mutations/remove-domain.js:80 msgid "Unable to remove domain from unknown organization." msgstr "Impossible de supprimer le domaine d'une organisation inconnue." -#: src/domain/mutations/remove-domain.js:148 +#: src/domain/mutations/remove-domain.js:136 msgid "Unable to remove domain. Domain is not part of organization." msgstr "Impossible de supprimer le domaine. Le domaine ne fait pas partie de l'organisation." -#: src/domain/mutations/remove-domain.js:134 -#: src/domain/mutations/remove-domain.js:165 -#: src/domain/mutations/remove-domain.js:198 -#: src/domain/mutations/remove-domain.js:217 -#: src/domain/mutations/remove-domain.js:243 -#: src/domain/mutations/remove-domain.js:260 -#: src/domain/mutations/remove-domain.js:278 -#: src/domain/mutations/remove-domain.js:296 -#: src/domain/mutations/remove-domain.js:314 -#: src/domain/mutations/remove-domain.js:331 -#: src/domain/mutations/remove-domain.js:354 -#: src/domain/mutations/remove-domain.js:365 +#: src/domain/mutations/remove-domain.js:123 +#: src/domain/mutations/remove-domain.js:152 +#: src/domain/mutations/remove-domain.js:185 +#: src/domain/mutations/remove-domain.js:204 +#: src/domain/mutations/remove-domain.js:230 +#: src/domain/mutations/remove-domain.js:248 +#: src/domain/mutations/remove-domain.js:265 +#: src/domain/mutations/remove-domain.js:288 +#: src/domain/mutations/remove-domain.js:299 msgid "Unable to remove domain. Please try again." msgstr "Impossible de supprimer le domaine. Veuillez réessayer." -#: src/domain/mutations/remove-organizations-domains.js:92 +#: src/domain/mutations/remove-organizations-domains.js:90 msgid "Unable to remove domains from unknown organization." msgstr "Impossible de supprimer les domaines d'une organisation inconnue." @@ -954,11 +1058,11 @@ msgstr "Impossible de supprimer les domaines d'une organisation inconnue." #: src/organization/mutations/remove-organization.js:168 #: src/organization/mutations/remove-organization.js:200 #: src/organization/mutations/remove-organization.js:212 -#: src/organization/mutations/remove-organization.js:251 -#: src/organization/mutations/remove-organization.js:373 -#: src/organization/mutations/remove-organization.js:406 -#: src/organization/mutations/remove-organization.js:462 -#: src/organization/mutations/remove-organization.js:473 +#: src/organization/mutations/remove-organization.js:237 +#: src/organization/mutations/remove-organization.js:255 +#: src/organization/mutations/remove-organization.js:288 +#: src/organization/mutations/remove-organization.js:344 +#: src/organization/mutations/remove-organization.js:355 msgid "Unable to remove organization. Please try again." msgstr "Impossible de supprimer l'organisation. Veuillez réessayer." @@ -967,7 +1071,7 @@ msgstr "Impossible de supprimer l'organisation. Veuillez réessayer." msgid "Unable to remove phone number. Please try again." msgstr "Impossible de supprimer le numéro de téléphone. Veuillez réessayer." -#: src/domain/mutations/remove-domain.js:71 +#: src/domain/mutations/remove-domain.js:65 msgid "Unable to remove unknown domain." msgstr "Impossible de supprimer un domaine inconnu." @@ -975,29 +1079,57 @@ msgstr "Impossible de supprimer un domaine inconnu." msgid "Unable to remove unknown organization." msgstr "Impossible de supprimer une organisation inconnue." -#: src/affiliation/mutations/remove-user-from-org.js:91 +#: src/affiliation/mutations/remove-user-from-org.js:77 msgid "Unable to remove unknown user from organization." msgstr "Impossible de supprimer un utilisateur inconnu de l'organisation." -#: src/affiliation/mutations/remove-user-from-org.js:77 -msgid "Unable to remove user from organization." -msgstr "Impossible de supprimer un utilisateur de l'organisation." +#: src/affiliation/mutations/remove-user-from-org.js:74 +#~ msgid "Unable to remove user from organization." +#~ msgstr "Impossible de supprimer un utilisateur de l'organisation." -#: src/affiliation/mutations/remove-user-from-org.js:111 -#: src/affiliation/mutations/remove-user-from-org.js:138 -#: src/affiliation/mutations/remove-user-from-org.js:176 -#: src/affiliation/mutations/remove-user-from-org.js:189 +#: src/affiliation/mutations/remove-user-from-org.js:94 +#: src/affiliation/mutations/remove-user-from-org.js:115 +#: src/affiliation/mutations/remove-user-from-org.js:160 +#: src/affiliation/mutations/remove-user-from-org.js:169 msgid "Unable to remove user from this organization. Please try again." msgstr "Impossible de supprimer l'utilisateur de cette organisation. Veuillez réessayer." -#: src/affiliation/mutations/remove-user-from-org.js:63 +#: src/affiliation/mutations/remove-user-from-org.js:61 msgid "Unable to remove user from unknown organization." msgstr "Impossible de supprimer un utilisateur d'une organisation inconnue." -#: src/domain/mutations/request-scan.js:57 +#: src/domain/mutations/request-scan.js:117 +msgid "Unable to request a one time scan on a domain that already has a pending scan." +msgstr "Impossible de demander une analyse unique sur un domaine qui a déjà une analyse en cours." + +#: src/domain/mutations/request-scan.js:52 msgid "Unable to request a one time scan on an unknown domain." msgstr "Impossible de demander un scan unique sur un domaine inconnu." +#: src/domain/mutations/request-scan.js:125 +msgid "Unable to request a one time scan. Please try again." +msgstr "Impossible de demander une analyse unique. Veuillez réessayer." + +#: src/affiliation/mutations/request-org-affiliation.js:95 +msgid "Unable to request invite to organization with which you are already affiliated." +msgstr "Impossible de demander une invitation à une organisation à laquelle vous êtes déjà affilié." + +#: src/affiliation/mutations/request-org-affiliation.js:85 +msgid "Unable to request invite to organization with which you have already requested to join." +msgstr "Impossible de demander une invitation à une organisation à laquelle vous avez déjà demandé à adhérer." + +#: src/affiliation/mutations/request-org-affiliation.js:56 +msgid "Unable to request invite to unknown organization." +msgstr "Impossible de demander une invitation à une organisation inconnue." + +#: src/affiliation/mutations/request-org-affiliation.js:72 +#: src/affiliation/mutations/request-org-affiliation.js:120 +#: src/affiliation/mutations/request-org-affiliation.js:136 +#: src/affiliation/mutations/request-org-affiliation.js:146 +#: src/affiliation/mutations/request-org-affiliation.js:165 +msgid "Unable to request invite. Please try again." +msgstr "Impossible de demander une invitation. Veuillez réessayer." + #: src/user/mutations/reset-password.js:95 msgid "Unable to reset password. Please request a new email." msgstr "Impossible de réinitialiser le mot de passe. Veuillez demander un nouvel e-mail." @@ -1008,8 +1140,8 @@ msgstr "Impossible de réinitialiser le mot de passe. Veuillez demander un nouve msgid "Unable to reset password. Please try again." msgstr "Impossible de réinitialiser le mot de passe. Veuillez réessayer." -#: src/domain/objects/domain.js:167 -#: src/domain/objects/domain.js:213 +#: src/domain/objects/domain.js:236 +#: src/domain/objects/domain.js:271 msgid "Unable to retrieve DMARC report information for: {domain}" msgstr "Impossible de récupérer les informations du rapport DMARC pour : {domain}" @@ -1026,11 +1158,15 @@ msgstr "Impossible d'envoyer l'email d'authentification. Veuillez réessayer." msgid "Unable to send authentication text message. Please try again." msgstr "Impossible d'envoyer un message texte d'authentification. Veuillez réessayer." -#: src/notify/notify-send-org-invite-create-account.js:31 +#: src/notify/notify-send-org-invite-create-account.js:19 #: src/notify/notify-send-org-invite-email.js:26 msgid "Unable to send org invite email. Please try again." msgstr "Impossible d'envoyer l'e-mail d'invitation à l'org. Veuillez réessayer." +#: src/notify/notify-send-invite-request-email.js:23 +msgid "Unable to send org invite request email. Please try again." +msgstr "Impossible d'envoyer l'email de demande d'invitation à l'org. Veuillez réessayer." + #: src/notify/notify-send-password-reset-email.js:30 msgid "Unable to send password reset email. Please try again." msgstr "Impossible d'envoyer l'email de réinitialisation du mot de passe. Veuillez réessayer." @@ -1058,38 +1194,38 @@ msgstr "Impossible de définir le numéro de téléphone, veuillez réessayer." msgid "Unable to sign in, please try again." msgstr "Impossible de se connecter, veuillez réessayer." -#: src/user/mutations/sign-up.js:200 -#: src/user/mutations/sign-up.js:214 +#: src/user/mutations/sign-up.js:183 +#: src/user/mutations/sign-up.js:193 msgid "Unable to sign up, please contact org admin for a new invite." msgstr "Impossible de s'inscrire, veuillez contacter l'administrateur de l'organisation pour obtenir une nouvelle invitation." -#: src/user/mutations/sign-up.js:168 -#: src/user/mutations/sign-up.js:178 -#: src/user/mutations/sign-up.js:236 -#: src/user/mutations/sign-up.js:246 +#: src/user/mutations/sign-up.js:156 +#: src/user/mutations/sign-up.js:164 +#: src/user/mutations/sign-up.js:213 +#: src/user/mutations/sign-up.js:221 msgid "Unable to sign up. Please try again." msgstr "Impossible de s'inscrire. Veuillez réessayer." -#: src/affiliation/mutations/transfer-org-ownership.js:131 -#: src/affiliation/mutations/transfer-org-ownership.js:172 -#: src/affiliation/mutations/transfer-org-ownership.js:196 -#: src/affiliation/mutations/transfer-org-ownership.js:208 +#: src/affiliation/mutations/transfer-org-ownership.js:106 +#: src/affiliation/mutations/transfer-org-ownership.js:145 +#: src/affiliation/mutations/transfer-org-ownership.js:167 +#: src/affiliation/mutations/transfer-org-ownership.js:177 msgid "Unable to transfer organization ownership. Please try again." msgstr "Impossible de transférer la propriété de l'organisation. Veuillez réessayer." -#: src/affiliation/mutations/transfer-org-ownership.js:78 -msgid "Unable to transfer ownership of a verified organization." -msgstr "Impossible de transférer la propriété d'une organisation vérifiée." +#: src/affiliation/mutations/transfer-org-ownership.js:68 +#~ msgid "Unable to transfer ownership of a verified organization." +#~ msgstr "Impossible de transférer la propriété d'une organisation vérifiée." -#: src/affiliation/mutations/transfer-org-ownership.js:112 +#: src/affiliation/mutations/transfer-org-ownership.js:89 msgid "Unable to transfer ownership of an org to an undefined user." msgstr "Impossible de transférer la propriété d'un org à un utilisateur non défini." -#: src/affiliation/mutations/transfer-org-ownership.js:65 +#: src/affiliation/mutations/transfer-org-ownership.js:59 msgid "Unable to transfer ownership of undefined organization." msgstr "Impossible de transférer la propriété d'une organisation non définie." -#: src/affiliation/mutations/transfer-org-ownership.js:144 +#: src/affiliation/mutations/transfer-org-ownership.js:118 msgid "Unable to transfer ownership to a user outside the org. Please invite the user and try again." msgstr "Impossible de transférer la propriété à un utilisateur extérieur à l'org. Veuillez inviter l'utilisateur et réessayer." @@ -1111,21 +1247,21 @@ msgstr "Impossible de défavoriser le domaine. Veuillez réessayer." msgid "Unable to unfavourite unknown domain." msgstr "Impossible de défavoriser un domaine inconnu." -#: src/domain/mutations/update-domain.js:256 +#: src/domain/mutations/update-domain.js:237 msgid "Unable to update domain edge. Please try again." msgstr "Impossible de mettre à jour le bord du domaine. Veuillez réessayer." -#: src/domain/mutations/update-domain.js:131 +#: src/domain/mutations/update-domain.js:125 msgid "Unable to update domain in an unknown org." msgstr "Impossible de mettre à jour le domaine dans un org inconnu." -#: src/domain/mutations/update-domain.js:179 +#: src/domain/mutations/update-domain.js:166 msgid "Unable to update domain that does not belong to the given organization." msgstr "Impossible de mettre à jour un domaine qui n'appartient pas à l'organisation donnée." -#: src/domain/mutations/update-domain.js:168 -#: src/domain/mutations/update-domain.js:210 -#: src/domain/mutations/update-domain.js:267 +#: src/domain/mutations/update-domain.js:156 +#: src/domain/mutations/update-domain.js:196 +#: src/domain/mutations/update-domain.js:247 msgid "Unable to update domain. Please try again." msgstr "Impossible de mettre à jour le domaine. Veuillez réessayer." @@ -1154,24 +1290,24 @@ msgstr "Impossible de mettre à jour le mot de passe, les mots de passe ne corre msgid "Unable to update password. Please try again." msgstr "Impossible de mettre à jour le mot de passe. Veuillez réessayer." -#: src/user/mutations/update-user-profile.js:172 -#: src/user/mutations/update-user-profile.js:181 +#: src/user/mutations/update-user-profile.js:160 +#: src/user/mutations/update-user-profile.js:167 msgid "Unable to update profile. Please try again." msgstr "Impossible de mettre à jour le profil. Veuillez réessayer." -#: src/affiliation/mutations/update-user-role.js:99 +#: src/affiliation/mutations/update-user-role.js:94 msgid "Unable to update role: organization unknown." msgstr "Impossible de mettre à jour le rôle : organisation inconnue." -#: src/affiliation/mutations/update-user-role.js:145 +#: src/affiliation/mutations/update-user-role.js:137 msgid "Unable to update role: user does not belong to organization." msgstr "Impossible de mettre à jour le rôle : l'utilisateur n'appartient pas à l'organisation." -#: src/affiliation/mutations/update-user-role.js:85 +#: src/affiliation/mutations/update-user-role.js:80 msgid "Unable to update role: user unknown." msgstr "Impossible de mettre à jour le rôle : utilisateur inconnu." -#: src/domain/mutations/update-domain.js:117 +#: src/domain/mutations/update-domain.js:111 msgid "Unable to update unknown domain." msgstr "Impossible de mettre à jour un domaine inconnu." @@ -1179,14 +1315,14 @@ msgstr "Impossible de mettre à jour un domaine inconnu." msgid "Unable to update unknown organization." msgstr "Impossible de mettre à jour une organisation inconnue." -#: src/affiliation/mutations/update-user-role.js:133 -#: src/affiliation/mutations/update-user-role.js:158 -#: src/affiliation/mutations/update-user-role.js:247 -#: src/affiliation/mutations/update-user-role.js:258 +#: src/affiliation/mutations/update-user-role.js:127 +#: src/affiliation/mutations/update-user-role.js:149 +#: src/affiliation/mutations/update-user-role.js:201 +#: src/affiliation/mutations/update-user-role.js:211 msgid "Unable to update user's role. Please try again." msgstr "Impossible de mettre à jour le rôle de l'utilisateur. Veuillez réessayer." -#: src/affiliation/mutations/update-user-role.js:71 +#: src/affiliation/mutations/update-user-role.js:66 msgid "Unable to update your own role." msgstr "Impossible de mettre à jour votre propre rôle." @@ -1204,18 +1340,17 @@ msgstr "Impossible de vérifier le compte. Veuillez réessayer." msgid "Unable to verify if user is a super admin, please try again." msgstr "Impossible de vérifier si l'utilisateur est un super administrateur, veuillez réessayer." -#: src/user/mutations/update-user-profile.js:103 +#: src/user/mutations/update-user-profile.js:100 #: src/user/queries/is-user-admin.js:59 msgid "Unable to verify if user is an admin, please try again." msgstr "Impossible de vérifier si l'utilisateur est un administrateur, veuillez réessayer." -#: src/organization/mutations/verify-organization.js:109 -#: src/organization/mutations/verify-organization.js:131 -#: src/organization/mutations/verify-organization.js:142 +#: src/organization/mutations/verify-organization.js:105 +#: src/organization/mutations/verify-organization.js:112 msgid "Unable to verify organization. Please try again." msgstr "Impossible de vérifier l'organisation. Veuillez réessayer." -#: src/organization/mutations/verify-organization.js:54 +#: src/organization/mutations/verify-organization.js:53 msgid "Unable to verify unknown organization." msgstr "Impossible de vérifier une organisation inconnue." @@ -1223,7 +1358,7 @@ msgstr "Impossible de vérifier une organisation inconnue." msgid "User could not be queried." msgstr "L'utilisateur n'a pas pu être interrogé." -#: src/affiliation/mutations/update-user-role.js:294 +#: src/affiliation/mutations/update-user-role.js:244 msgid "User role was updated successfully." msgstr "Le rôle de l'utilisateur a été mis à jour avec succès." @@ -1239,22 +1374,22 @@ msgstr "Erreur de vérification. Veuillez activer l'authentification multifactor msgid "Verification error. Please verify your account via email to access content." msgstr "Erreur de vérification. Veuillez vérifier votre compte par e-mail pour accéder au contenu." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:71 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:70 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:161 msgid "You must provide a `first` or `last` value to properly paginate the `Affiliation` connection." msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Affiliation`." #: src/email-scan/loaders/load-dkim-results-connections-by-dkim-id.js:89 -msgid "You must provide a `first` or `last` value to properly paginate the `DKIMResults` connection." -msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DKIMResults`." +#~ msgid "You must provide a `first` or `last` value to properly paginate the `DKIMResults` connection." +#~ msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DKIMResults`." #: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:114 -msgid "You must provide a `first` or `last` value to properly paginate the `DKIM` connection." -msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DKIM`." +#~ msgid "You must provide a `first` or `last` value to properly paginate the `DKIM` connection." +#~ msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DKIM`." #: src/email-scan/loaders/load-dmarc-connections-by-domain-id.js:138 -msgid "You must provide a `first` or `last` value to properly paginate the `DMARC` connection." -msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DMARC`." +#~ msgid "You must provide a `first` or `last` value to properly paginate the `DMARC` connection." +#~ msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DMARC`." #: src/dmarc-summaries/loaders/load-dkim-failure-connections-by-sum-id.js:36 msgid "You must provide a `first` or `last` value to properly paginate the `DkimFailureTable` connection." @@ -1264,12 +1399,12 @@ msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctemen msgid "You must provide a `first` or `last` value to properly paginate the `DmarcFailureTable` connection." msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DmarcFailureTable`." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:189 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:162 msgid "You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection." msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DmarcSummaries`." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:154 -#: src/domain/loaders/load-domain-connections-by-user-id.js:166 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:141 +#: src/domain/loaders/load-domain-connections-by-user-id.js:147 msgid "You must provide a `first` or `last` value to properly paginate the `Domain` connection." msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Domain`." @@ -1287,26 +1422,26 @@ msgid "You must provide a `first` or `last` value to properly paginate the `Guid msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `GuidanceTag`." #: src/web-scan/loaders/load-https-connections-by-domain-id.js:147 -msgid "You must provide a `first` or `last` value to properly paginate the `HTTPS` connection." -msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `HTTPS`." +#~ msgid "You must provide a `first` or `last` value to properly paginate the `HTTPS` connection." +#~ msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `HTTPS`." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:101 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:100 msgid "You must provide a `first` or `last` value to properly paginate the `Log` connection." msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Log`." #: src/organization/loaders/load-organization-connections-by-domain-id.js:183 -#: src/organization/loaders/load-organization-connections-by-user-id.js:182 +#: src/organization/loaders/load-organization-connections-by-user-id.js:166 #: src/organization/loaders/load-web-check-connections-by-user-id.js:89 msgid "You must provide a `first` or `last` value to properly paginate the `Organization` connection." msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Organization`." #: src/email-scan/loaders/load-spf-connections-by-domain-id.js:133 -msgid "You must provide a `first` or `last` value to properly paginate the `SPF` connection." -msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `SPF`." +#~ msgid "You must provide a `first` or `last` value to properly paginate the `SPF` connection." +#~ msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `SPF`." #: src/web-scan/loaders/load-ssl-connections-by-domain-id.js:171 -msgid "You must provide a `first` or `last` value to properly paginate the `SSL` connection." -msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `SSL`." +#~ msgid "You must provide a `first` or `last` value to properly paginate the `SSL` connection." +#~ msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `SSL`." #: src/dmarc-summaries/loaders/load-spf-failure-connections-by-sum-id.js:36 msgid "You must provide a `first` or `last` value to properly paginate the `SpfFailureTable` connection." @@ -1326,28 +1461,48 @@ msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctemen msgid "You must provide a `first` or `last` value to properly paginate the `VerifiedOrganization` connection." msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `VerifiedOrganization`." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:31 +#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:16 +msgid "You must provide a `limit` value in the range of 1-100 to properly paginate the `DNS` connection." +msgstr "Vous devez fournir une valeur `limit` comprise entre 1 et 100 pour paginer correctement la connexion `DNS`." + +#: src/web-scan/loaders/load-web-connections-by-domain-id.js:16 +msgid "You must provide a `limit` value in the range of 1-100 to properly paginate the `web` connection." +msgstr "Vous devez fournir une valeur `limit` comprise entre 1 et 100 pour paginer correctement la connexion `web`." + +#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:9 +msgid "You must provide a `limit` value to properly paginate the `DNS` connection." +msgstr "Vous devez fournir une valeur `limit` pour paginer correctement la connexion `DNS`." + +#: src/web-scan/loaders/load-web-connections-by-domain-id.js:9 +msgid "You must provide a `limit` value to properly paginate the `web` connection." +msgstr "Vous devez fournir une valeur `limit` pour paginer correctement la connexion `web`." + +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:12 msgid "You must provide a `period` value to access the `DmarcSummaries` connection." msgstr "Vous devez fournir une valeur `period` pour accéder à la connexion `DmarcSummaries`." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:43 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:18 msgid "You must provide a `year` value to access the `DmarcSummaries` connection." msgstr "Vous devez fournir une valeur `year` pour accéder à la connexion `DmarcSummaries`." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:118 +#: src/dns-scan/loaders/load-dns-connections-by-domain-id.js:30 +msgid "You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `DNS` connection." +msgstr "Vous devez fournir au plus une valeur de méthode de pagination (`before`, `after`, `offset`) pour paginer correctement la connexion `DNS`." + +#: src/web-scan/loaders/load-web-connections-by-domain-id.js:30 +msgid "You must provide at most one pagination method (`before`, `after`, `offset`) value to properly paginate the `web` connection." +msgstr "Vous devez fournir au plus une valeur de méthode de pagination (`before`, `after`, `offset`) pour paginer correctement la connexion `web`." + +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:109 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:208 -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:148 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:131 #: src/dmarc-summaries/loaders/load-dkim-failure-connections-by-sum-id.js:83 #: src/dmarc-summaries/loaders/load-dmarc-failure-connections-by-sum-id.js:83 -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:236 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:202 #: src/dmarc-summaries/loaders/load-full-pass-connections-by-sum-id.js:83 #: src/dmarc-summaries/loaders/load-spf-failure-connections-by-sum-id.js:83 -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:201 -#: src/domain/loaders/load-domain-connections-by-user-id.js:213 -#: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:170 -#: src/email-scan/loaders/load-dkim-results-connections-by-dkim-id.js:136 -#: src/email-scan/loaders/load-dmarc-connections-by-domain-id.js:186 -#: src/email-scan/loaders/load-spf-connections-by-domain-id.js:180 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:178 +#: src/domain/loaders/load-domain-connections-by-user-id.js:186 #: src/guidance-tag/loaders/load-aggregate-guidance-tags-connections.js:132 #: src/guidance-tag/loaders/load-dkim-guidance-tags-connections.js:136 #: src/guidance-tag/loaders/load-dmarc-guidance-tags-connections.js:136 @@ -1355,34 +1510,32 @@ msgstr "Vous devez fournir une valeur `year` pour accéder à la connexion `Dmar #: src/guidance-tag/loaders/load-spf-guidance-tags-connections.js:136 #: src/guidance-tag/loaders/load-ssl-guidance-tags-connections.js:136 #: src/organization/loaders/load-organization-connections-by-domain-id.js:230 -#: src/organization/loaders/load-organization-connections-by-user-id.js:229 +#: src/organization/loaders/load-organization-connections-by-user-id.js:201 #: src/organization/loaders/load-web-check-connections-by-user-id.js:136 #: src/user/loaders/load-user-connections-by-user-id.js:154 #: src/verified-domains/loaders/load-verified-domain-connections-by-organization-id.js:164 #: src/verified-domains/loaders/load-verified-domain-connections.js:164 #: src/verified-organizations/loaders/load-verified-organization-connections-by-domain-id.js:214 #: src/verified-organizations/loaders/load-verified-organizations-connections.js:212 -#: src/web-scan/loaders/load-https-connections-by-domain-id.js:194 -#: src/web-scan/loaders/load-ssl-connections-by-domain-id.js:218 msgid "`{argSet}` must be of type `number` not `{typeSet}`." msgstr "`{argSet}` doit être de type `number` et non `{typeSet}`." -#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:92 +#: src/affiliation/loaders/load-affiliation-connections-by-org-id.js:86 #: src/affiliation/loaders/load-affiliation-connections-by-user-id.js:182 msgid "`{argSet}` on the `Affiliation` connection cannot be less than zero." msgstr "`{argSet}` sur la connexion `Affiliation` ne peut être inférieur à zéro." #: src/email-scan/loaders/load-dkim-results-connections-by-dkim-id.js:110 -msgid "`{argSet}` on the `DKIMResults` connection cannot be less than zero." -msgstr "`{argSet}` sur la connexion `DKIMResults` ne peut être inférieur à zéro." +#~ msgid "`{argSet}` on the `DKIMResults` connection cannot be less than zero." +#~ msgstr "`{argSet}` sur la connexion `DKIMResults` ne peut être inférieur à zéro." #: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:135 -msgid "`{argSet}` on the `DKIM` connection cannot be less than zero." -msgstr "`{argSet}` sur la connexion `DKIM` ne peut être inférieur à zéro." +#~ msgid "`{argSet}` on the `DKIM` connection cannot be less than zero." +#~ msgstr "`{argSet}` sur la connexion `DKIM` ne peut être inférieur à zéro." #: src/email-scan/loaders/load-dmarc-connections-by-domain-id.js:160 -msgid "`{argSet}` on the `DMARC` connection cannot be less than zero." -msgstr "`{argSet}` sur la connexion `DMARC` ne peut être inférieur à zéro." +#~ msgid "`{argSet}` on the `DMARC` connection cannot be less than zero." +#~ msgstr "`{argSet}` sur la connexion `DMARC` ne peut être inférieur à zéro." #: src/dmarc-summaries/loaders/load-dkim-failure-connections-by-sum-id.js:57 msgid "`{argSet}` on the `DkimFailureTable` connection cannot be less than zero." @@ -1392,12 +1545,12 @@ msgstr "`{argSet}` sur la connexion `DkimFailureTable` ne peut être inférieur msgid "`{argSet}` on the `DmarcFailureTable` connection cannot be less than zero." msgstr "`{argSet}` sur la connexion `DmarcFailureTable` ne peut être inférieur à zéro." -#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:210 +#: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:179 msgid "`{argSet}` on the `DmarcSummaries` connection cannot be less than zero." msgstr "`{argSet}` sur la connexion `DmarcSummaries` ne peut être inférieur à zéro." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:175 -#: src/domain/loaders/load-domain-connections-by-user-id.js:187 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:157 +#: src/domain/loaders/load-domain-connections-by-user-id.js:163 msgid "`{argSet}` on the `Domain` connection cannot be less than zero." msgstr "`{argSet}` sur la connexion `Domain` ne peut être inférieur à zéro." @@ -1415,26 +1568,26 @@ msgid "`{argSet}` on the `GuidanceTag` connection cannot be less than zero." msgstr "`{argSet}` sur la connexion `GuidanceTag` ne peut être inférieure à zéro." #: src/web-scan/loaders/load-https-connections-by-domain-id.js:168 -msgid "`{argSet}` on the `HTTPS` connection cannot be less than zero." -msgstr "`{argSet}` sur la connexion `HTTPS` ne peut être inférieur à zéro." +#~ msgid "`{argSet}` on the `HTTPS` connection cannot be less than zero." +#~ msgstr "`{argSet}` sur la connexion `HTTPS` ne peut être inférieur à zéro." -#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:122 +#: src/audit-logs/loaders/load-audit-logs-by-org-id.js:112 msgid "`{argSet}` on the `Log` connection cannot be less than zero." msgstr "`{argSet}` sur la connexion `Log` ne peut être inférieur à zéro." #: src/organization/loaders/load-organization-connections-by-domain-id.js:204 -#: src/organization/loaders/load-organization-connections-by-user-id.js:203 +#: src/organization/loaders/load-organization-connections-by-user-id.js:180 #: src/organization/loaders/load-web-check-connections-by-user-id.js:110 msgid "`{argSet}` on the `Organization` connection cannot be less than zero." msgstr "`{argSet}` sur la connexion `Organization` ne peut être inférieure à zéro." #: src/email-scan/loaders/load-spf-connections-by-domain-id.js:154 -msgid "`{argSet}` on the `SPF` connection cannot be less than zero." -msgstr "`{argSet}` sur la connexion `SPF` ne peut être inférieure à zéro." +#~ msgid "`{argSet}` on the `SPF` connection cannot be less than zero." +#~ msgstr "`{argSet}` sur la connexion `SPF` ne peut être inférieure à zéro." #: src/web-scan/loaders/load-ssl-connections-by-domain-id.js:192 -msgid "`{argSet}` on the `SSL` connection cannot be less than zero." -msgstr "`{argSet}` sur la connexion `SSL` ne peut être inférieur à zéro." +#~ msgid "`{argSet}` on the `SSL` connection cannot be less than zero." +#~ msgstr "`{argSet}` sur la connexion `SSL` ne peut être inférieur à zéro." #: src/dmarc-summaries/loaders/load-spf-failure-connections-by-sum-id.js:57 msgid "`{argSet}` on the `SpfFailureTable` connection cannot be less than zero." diff --git a/api/src/notify/__tests__/notify-send-org-invite-create-account.test.js b/api/src/notify/__tests__/notify-send-org-invite-create-account.test.js index a0aafaffd5..f76dea6b5e 100644 --- a/api/src/notify/__tests__/notify-send-org-invite-create-account.test.js +++ b/api/src/notify/__tests__/notify-send-org-invite-create-account.test.js @@ -1,13 +1,10 @@ -import {setupI18n} from '@lingui/core' +import { setupI18n } from '@lingui/core' import englishMessages from '../../locale/en/messages' import frenchMessages from '../../locale/fr/messages' -import {sendOrgInviteCreateAccount} from '../index' +import { sendOrgInviteCreateAccount } from '../index' -const { - NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_EN, - NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_FR, -} = process.env +const { NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_BILINGUAL } = process.env describe('given the sendOrgInviteCreateAccount function', () => { let i18n @@ -23,8 +20,8 @@ describe('given the sendOrgInviteCreateAccount function', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -38,8 +35,8 @@ describe('given the sendOrgInviteCreateAccount function', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -67,18 +64,20 @@ describe('given the sendOrgInviteCreateAccount function', () => { }) await mockedSendOrgInviteCreateAccount({ user, - orgName: 'Test Org', + orgNameEN: 'Test Org EN', + orgNameFR: 'Test Org FR', createAccountLink: 'TestLink.ca', }) expect(notifyClient.sendEmail).toHaveBeenCalledWith( - NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_EN, + NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_BILINGUAL, user.userName, { personalisation: { create_account_link: 'TestLink.ca', display_name: user.userName, - organization_name: 'Test Org', + organization_name_en: 'Test Org EN', + organization_name_fr: 'Test Org FR', }, }, ) @@ -86,9 +85,7 @@ describe('given the sendOrgInviteCreateAccount function', () => { }) describe('an error occurs while sending email', () => { it('throws an error message', async () => { - const sendEmail = jest - .fn() - .mockRejectedValue(new Error('Notification error occurred.')) + const sendEmail = jest.fn().mockRejectedValue(new Error('Notification error occurred.')) const notifyClient = { sendEmail, } @@ -110,9 +107,7 @@ describe('given the sendOrgInviteCreateAccount function', () => { createAccountLink: 'TestLink.ca', }) } catch (err) { - expect(err).toEqual( - new Error('Unable to send org invite email. Please try again.'), - ) + expect(err).toEqual(new Error('Unable to send org invite email. Please try again.')) } expect(consoleOutput).toEqual([ @@ -126,8 +121,8 @@ describe('given the sendOrgInviteCreateAccount function', () => { i18n = setupI18n({ locale: 'fr', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -155,18 +150,20 @@ describe('given the sendOrgInviteCreateAccount function', () => { }) await mockedSendOrgInviteCreateAccount({ user, - orgName: 'Test Org', + orgNameEN: 'Test Org EN', + orgNameFR: 'Test Org FR', createAccountLink: 'TestLink.ca', }) expect(notifyClient.sendEmail).toHaveBeenCalledWith( - NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_FR, + NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_BILINGUAL, user.userName, { personalisation: { create_account_link: 'TestLink.ca', display_name: user.userName, - organization_name: 'Test Org', + organization_name_en: 'Test Org EN', + organization_name_fr: 'Test Org FR', }, }, ) @@ -174,9 +171,7 @@ describe('given the sendOrgInviteCreateAccount function', () => { }) describe('an error occurs while sending email', () => { it('throws an error message', async () => { - const sendEmail = jest - .fn() - .mockRejectedValue(new Error('Notification error occurred.')) + const sendEmail = jest.fn().mockRejectedValue(new Error('Notification error occurred.')) const notifyClient = { sendEmail, } @@ -198,9 +193,7 @@ describe('given the sendOrgInviteCreateAccount function', () => { createAccountLink: 'TestLink.ca', }) } catch (err) { - expect(err).toEqual( - new Error('Unable to send org invite email. Please try again.'), - ) + expect(err).toEqual(new Error('Unable to send org invite email. Please try again.')) } expect(consoleOutput).toEqual([ diff --git a/api/src/notify/index.js b/api/src/notify/index.js index fc86b60b02..0a1fc75a15 100644 --- a/api/src/notify/index.js +++ b/api/src/notify/index.js @@ -1,6 +1,7 @@ export * from './notify-client' export * from './notify-send-authenticate-email' export * from './notify-send-authenticate-text-msg' +export * from './notify-send-invite-request-email' export * from './notify-send-org-invite-create-account' export * from './notify-send-org-invite-email' export * from './notify-send-password-reset-email' diff --git a/api/src/notify/notify-send-invite-request-email.js b/api/src/notify/notify-send-invite-request-email.js new file mode 100644 index 0000000000..36266f9402 --- /dev/null +++ b/api/src/notify/notify-send-invite-request-email.js @@ -0,0 +1,25 @@ +import { t } from '@lingui/macro' + +const { NOTIFICATION_ORG_INVITE_REQUEST_EN, NOTIFICATION_ORG_INVITE_REQUEST_FR } = process.env + +export const sendInviteRequestEmail = + ({ notifyClient, i18n }) => + async ({ user, orgName, adminLink }) => { + let templateId = NOTIFICATION_ORG_INVITE_REQUEST_EN + if (user.preferredLang === 'french') { + templateId = NOTIFICATION_ORG_INVITE_REQUEST_FR + } + + try { + await notifyClient.sendEmail(templateId, user.userName, { + personalisation: { + admin_link: adminLink, + display_name: user.displayName, + organization_name: orgName, + }, + }) + } catch (err) { + console.error(`Error occurred when sending org invite request email for ${user._key}: ${err}`) + throw new Error(i18n._(t`Unable to send org invite request email. Please try again.`)) + } + } diff --git a/api/src/notify/notify-send-org-invite-create-account.js b/api/src/notify/notify-send-org-invite-create-account.js index ff44900fe8..9fb2e9aff3 100644 --- a/api/src/notify/notify-send-org-invite-create-account.js +++ b/api/src/notify/notify-send-org-invite-create-account.js @@ -1,34 +1,21 @@ -import {t} from '@lingui/macro' +import { t } from '@lingui/macro' -const { - NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_EN, - NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_FR, -} = process.env +const { NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_BILINGUAL } = process.env -export const sendOrgInviteCreateAccount = ({notifyClient, i18n}) => async ({ - user, - orgName, - createAccountLink, - }) => { - let templateId = NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_EN - if (user.preferredLang === 'french') { - templateId = NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_FR +export const sendOrgInviteCreateAccount = + ({ notifyClient, i18n }) => + async ({ user, orgNameEN, orgNameFR, createAccountLink }) => { + try { + await notifyClient.sendEmail(NOTIFICATION_ORG_INVITE_CREATE_ACCOUNT_BILINGUAL, user.userName, { + personalisation: { + create_account_link: createAccountLink, + display_name: user.userName, + organization_name_en: orgNameEN, + organization_name_fr: orgNameFR, + }, + }) + } catch (err) { + console.error(`Error occurred when sending org create account invite email for ${user._key}: ${err}`) + throw new Error(i18n._(t`Unable to send org invite email. Please try again.`)) + } } - - try { - await notifyClient.sendEmail(templateId, user.userName, { - personalisation: { - create_account_link: createAccountLink, - display_name: user.userName, - organization_name: orgName, - }, - }) - } catch (err) { - console.error( - `Error occurred when sending org create account invite email for ${user._key}: ${err}`, - ) - throw new Error( - i18n._(t`Unable to send org invite email. Please try again.`), - ) - } -} diff --git a/api/src/organization/loaders/load-organization-connections-by-user-id.js b/api/src/organization/loaders/load-organization-connections-by-user-id.js index 35b9cc0527..96a42cbded 100644 --- a/api/src/organization/loaders/load-organization-connections-by-user-id.js +++ b/api/src/organization/loaders/load-organization-connections-by-user-id.js @@ -2,427 +2,413 @@ import { aql } from 'arangojs' import { fromGlobalId, toGlobalId } from 'graphql-relay' import { t } from '@lingui/macro' -export const loadOrgConnectionsByUserId = ({ - query, - userKey, - cleanseInput, - language, - i18n, - auth: { loginRequiredBool }, -}) => async ({ - after, - before, - first, - last, - orderBy, - isSuperAdmin, - search, - isAdmin, - includeSuperAdminOrg, -}) => { - const userDBId = `users/${userKey}` +export const loadOrgConnectionsByUserId = + ({ query, userKey, cleanseInput, language, i18n, auth: { loginRequiredBool } }) => + async ({ after, before, first, last, orderBy, isSuperAdmin, search, isAdmin, includeSuperAdminOrg, isVerified }) => { + const userDBId = `users/${userKey}` - let afterTemplate = aql`` - let afterVar = aql`` - if (typeof after !== 'undefined') { - const { id: afterId } = fromGlobalId(cleanseInput(after)) - if (typeof orderBy === 'undefined') { - afterTemplate = aql`FILTER TO_NUMBER(org._key) > TO_NUMBER(${afterId})` + let afterTemplate = aql`` + let afterVar = aql`` + if (typeof after !== 'undefined') { + const { id: afterId } = fromGlobalId(cleanseInput(after)) + if (typeof orderBy === 'undefined') { + afterTemplate = aql`FILTER TO_NUMBER(org._key) > TO_NUMBER(${afterId})` + } else { + let afterTemplateDirection = aql`<` + if (orderBy.direction === 'ASC') { + afterTemplateDirection = aql`>` + } + + afterVar = aql`LET afterVar = DOCUMENT(organizations, ${afterId})` + + let documentField = aql`` + let orgField = aql`` + /* istanbul ignore else */ + if (orderBy.field === 'acronym') { + documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).acronym` + orgField = aql`TRANSLATE(${language}, org.orgDetails).acronym` + } else if (orderBy.field === 'name') { + documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).name` + orgField = aql`TRANSLATE(${language}, org.orgDetails).name` + } else if (orderBy.field === 'slug') { + documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).slug` + orgField = aql`TRANSLATE(${language}, org.orgDetails).slug` + } else if (orderBy.field === 'zone') { + documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).zone` + orgField = aql`TRANSLATE(${language}, org.orgDetails).zone` + } else if (orderBy.field === 'sector') { + documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).sector` + orgField = aql`TRANSLATE(${language}, org.orgDetails).sector` + } else if (orderBy.field === 'country') { + documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).country` + orgField = aql`TRANSLATE(${language}, org.orgDetails).country` + } else if (orderBy.field === 'province') { + documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).province` + orgField = aql`TRANSLATE(${language}, org.orgDetails).province` + } else if (orderBy.field === 'city') { + documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).city` + orgField = aql`TRANSLATE(${language}, org.orgDetails).city` + } else if (orderBy.field === 'verified') { + documentField = aql`afterVar.verified` + orgField = aql`org.verified` + } else if (orderBy.field === 'summary-mail-pass') { + documentField = aql`afterVar.summaries.mail.pass` + orgField = aql`org.summaries.mail.pass` + } else if (orderBy.field === 'summary-mail-fail') { + documentField = aql`afterVar.summaries.mail.fail` + orgField = aql`org.summaries.mail.fail` + } else if (orderBy.field === 'summary-mail-total') { + documentField = aql`afterVar.summaries.mail.total` + orgField = aql`org.summaries.mail.total` + } else if (orderBy.field === 'summary-web-pass') { + documentField = aql`afterVar.summaries.web.pass` + orgField = aql`org.summaries.web.pass` + } else if (orderBy.field === 'summary-web-fail') { + documentField = aql`afterVar.summaries.web.fail` + orgField = aql`org.summaries.web.fail` + } else if (orderBy.field === 'summary-web-total') { + documentField = aql`afterVar.summaries.web.total` + orgField = aql`org.summaries.web.total` + } else if (orderBy.field === 'domain-count') { + documentField = aql`COUNT(FOR v, e IN 1..1 OUTBOUND afterVar._id claims RETURN e._to)` + orgField = aql`COUNT(orgDomains)` + } + + afterTemplate = aql` + FILTER ${orgField} ${afterTemplateDirection} ${documentField} + OR (${orgField} == ${documentField} + AND TO_NUMBER(org._key) > TO_NUMBER(${afterId})) + ` + } + } + + let beforeTemplate = aql`` + let beforeVar = aql`` + if (typeof before !== 'undefined') { + const { id: beforeId } = fromGlobalId(cleanseInput(before)) + if (typeof orderBy === 'undefined') { + beforeTemplate = aql`FILTER TO_NUMBER(org._key) < TO_NUMBER(${beforeId})` + } else { + let beforeTemplateDirection = aql`>` + if (orderBy.direction === 'ASC') { + beforeTemplateDirection = aql`<` + } + + beforeVar = aql`LET beforeVar = DOCUMENT(organizations, ${beforeId})` + + let documentField = aql`` + let orgField = aql`` + /* istanbul ignore else */ + if (orderBy.field === 'acronym') { + documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).acronym` + orgField = aql`TRANSLATE(${language}, org.orgDetails).acronym` + } else if (orderBy.field === 'name') { + documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).name` + orgField = aql`TRANSLATE(${language}, org.orgDetails).name` + } else if (orderBy.field === 'slug') { + documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).slug` + orgField = aql`TRANSLATE(${language}, org.orgDetails).slug` + } else if (orderBy.field === 'zone') { + documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).zone` + orgField = aql`TRANSLATE(${language}, org.orgDetails).zone` + } else if (orderBy.field === 'sector') { + documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).sector` + orgField = aql`TRANSLATE(${language}, org.orgDetails).sector` + } else if (orderBy.field === 'country') { + documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).country` + orgField = aql`TRANSLATE(${language}, org.orgDetails).country` + } else if (orderBy.field === 'province') { + documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).province` + orgField = aql`TRANSLATE(${language}, org.orgDetails).province` + } else if (orderBy.field === 'city') { + documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).city` + orgField = aql`TRANSLATE(${language}, org.orgDetails).city` + } else if (orderBy.field === 'verified') { + documentField = aql`beforeVar.verified` + orgField = aql`org.verified` + } else if (orderBy.field === 'summary-mail-pass') { + documentField = aql`beforeVar.summaries.mail.pass` + orgField = aql`org.summaries.mail.pass` + } else if (orderBy.field === 'summary-mail-fail') { + documentField = aql`beforeVar.summaries.mail.fail` + orgField = aql`org.summaries.mail.fail` + } else if (orderBy.field === 'summary-mail-total') { + documentField = aql`beforeVar.summaries.mail.total` + orgField = aql`org.summaries.mail.total` + } else if (orderBy.field === 'summary-web-pass') { + documentField = aql`beforeVar.summaries.web.pass` + orgField = aql`org.summaries.web.pass` + } else if (orderBy.field === 'summary-web-fail') { + documentField = aql`beforeVar.summaries.web.fail` + orgField = aql`org.summaries.web.fail` + } else if (orderBy.field === 'summary-web-total') { + documentField = aql`beforeVar.summaries.web.total` + orgField = aql`org.summaries.web.total` + } else if (orderBy.field === 'domain-count') { + documentField = aql`COUNT(FOR v, e IN 1..1 OUTBOUND beforeVar._id claims RETURN e._to)` + orgField = aql`COUNT(orgDomains)` + } + + beforeTemplate = aql` + FILTER ${orgField} ${beforeTemplateDirection} ${documentField} + OR (${orgField} == ${documentField} + AND TO_NUMBER(org._key) < TO_NUMBER(${beforeId})) + ` + } + } + + let limitTemplate = aql`` + if (typeof first === 'undefined' && typeof last === 'undefined') { + console.warn( + `User: ${userKey} did not have either \`first\` or \`last\` arguments set for: loadOrgConnectionsByUserId.`, + ) + throw new Error( + i18n._(t`You must provide a \`first\` or \`last\` value to properly paginate the \`Organization\` connection.`), + ) + } else if (typeof first !== 'undefined' && typeof last !== 'undefined') { + console.warn( + `User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadOrgConnectionsByUserId.`, + ) + throw new Error( + i18n._(t`Passing both \`first\` and \`last\` to paginate the \`Organization\` connection is not supported.`), + ) + } else if (typeof first === 'number' || typeof last === 'number') { + /* istanbul ignore else */ + if (first < 0 || last < 0) { + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + console.warn(`User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadOrgConnectionsByUserId.`) + throw new Error(i18n._(t`\`${argSet}\` on the \`Organization\` connection cannot be less than zero.`)) + } else if (first > 100 || last > 100) { + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + const amount = typeof first !== 'undefined' ? first : last + console.warn(`User: ${userKey} attempted to have \`${argSet}\` to ${amount} for: loadOrgConnectionsByUserId.`) + throw new Error( + i18n._( + t`Requesting \`${amount}\` records on the \`Organization\` connection exceeds the \`${argSet}\` limit of 100 records.`, + ), + ) + } else if (typeof first !== 'undefined' && typeof last === 'undefined') { + limitTemplate = aql`TO_NUMBER(org._key) ASC LIMIT TO_NUMBER(${first})` + } else if (typeof first === 'undefined' && typeof last !== 'undefined') { + limitTemplate = aql`TO_NUMBER(org._key) DESC LIMIT TO_NUMBER(${last})` + } } else { - let afterTemplateDirection = aql`<` + const argSet = typeof first !== 'undefined' ? 'first' : 'last' + const typeSet = typeof first !== 'undefined' ? typeof first : typeof last + console.warn( + `User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadOrgConnectionsByUserId.`, + ) + throw new Error(i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`)) + } + + let hasNextPageFilter = aql`FILTER TO_NUMBER(org._key) > TO_NUMBER(LAST(retrievedOrgs)._key)` + let hasPreviousPageFilter = aql`FILTER TO_NUMBER(org._key) < TO_NUMBER(FIRST(retrievedOrgs)._key)` + if (typeof orderBy !== 'undefined') { + let hasNextPageDirection = aql`<` + let hasPreviousPageDirection = aql`>` if (orderBy.direction === 'ASC') { - afterTemplateDirection = aql`>` + hasNextPageDirection = aql`>` + hasPreviousPageDirection = aql`<` } - afterVar = aql`LET afterVar = DOCUMENT(organizations, ${afterId})` - - let documentField = aql`` let orgField = aql`` + let hasNextPageDocumentField = aql`` + let hasPreviousPageDocumentField = aql`` /* istanbul ignore else */ if (orderBy.field === 'acronym') { - documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).acronym` orgField = aql`TRANSLATE(${language}, org.orgDetails).acronym` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).acronym` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).acronym` } else if (orderBy.field === 'name') { - documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).name` orgField = aql`TRANSLATE(${language}, org.orgDetails).name` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).name` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).name` } else if (orderBy.field === 'slug') { - documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).slug` orgField = aql`TRANSLATE(${language}, org.orgDetails).slug` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).slug` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).slug` } else if (orderBy.field === 'zone') { - documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).zone` orgField = aql`TRANSLATE(${language}, org.orgDetails).zone` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).zone` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).zone` } else if (orderBy.field === 'sector') { - documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).sector` orgField = aql`TRANSLATE(${language}, org.orgDetails).sector` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).sector` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).sector` } else if (orderBy.field === 'country') { - documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).country` orgField = aql`TRANSLATE(${language}, org.orgDetails).country` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).country` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).country` } else if (orderBy.field === 'province') { - documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).province` orgField = aql`TRANSLATE(${language}, org.orgDetails).province` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).province` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).province` } else if (orderBy.field === 'city') { - documentField = aql`TRANSLATE(${language}, afterVar.orgDetails).city` orgField = aql`TRANSLATE(${language}, org.orgDetails).city` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).city` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).city` } else if (orderBy.field === 'verified') { - documentField = aql`afterVar.verified` orgField = aql`org.verified` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).verified` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).verified` } else if (orderBy.field === 'summary-mail-pass') { - documentField = aql`afterVar.summaries.mail.pass` orgField = aql`org.summaries.mail.pass` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.mail.pass` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.mail.pass` } else if (orderBy.field === 'summary-mail-fail') { - documentField = aql`afterVar.summaries.mail.fail` orgField = aql`org.summaries.mail.fail` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.mail.fail` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.mail.fail` } else if (orderBy.field === 'summary-mail-total') { - documentField = aql`afterVar.summaries.mail.total` orgField = aql`org.summaries.mail.total` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.mail.total` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.mail.total` } else if (orderBy.field === 'summary-web-pass') { - documentField = aql`afterVar.summaries.web.pass` orgField = aql`org.summaries.web.pass` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.web.pass` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.web.pass` } else if (orderBy.field === 'summary-web-fail') { - documentField = aql`afterVar.summaries.web.fail` orgField = aql`org.summaries.web.fail` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.web.fail` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.web.fail` } else if (orderBy.field === 'summary-web-total') { - documentField = aql`afterVar.summaries.web.total` orgField = aql`org.summaries.web.total` + hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.web.total` + hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.web.total` } else if (orderBy.field === 'domain-count') { - documentField = aql`COUNT(FOR v, e IN 1..1 OUTBOUND afterVar._id claims RETURN e._to)` orgField = aql`COUNT(orgDomains)` + hasNextPageDocumentField = aql`COUNT(FOR v, e IN 1..1 OUTBOUND LAST(retrievedOrgs)._id claims RETURN e._to)` + hasPreviousPageDocumentField = aql`COUNT(FOR v, e IN 1..1 OUTBOUND FIRST(retrievedOrgs)._id claims RETURN e._to)` } - afterTemplate = aql` - FILTER ${orgField} ${afterTemplateDirection} ${documentField} - OR (${orgField} == ${documentField} - AND TO_NUMBER(org._key) > TO_NUMBER(${afterId})) - ` + hasNextPageFilter = aql` + FILTER ${orgField} ${hasNextPageDirection} ${hasNextPageDocumentField} + OR (${orgField} == ${hasNextPageDocumentField} + AND TO_NUMBER(org._key) > TO_NUMBER(LAST(retrievedOrgs)._key)) + ` + hasPreviousPageFilter = aql` + FILTER ${orgField} ${hasPreviousPageDirection} ${hasPreviousPageDocumentField} + OR (${orgField} == ${hasPreviousPageDocumentField} + AND TO_NUMBER(org._key) < TO_NUMBER(FIRST(retrievedOrgs)._key)) + ` } - } - let beforeTemplate = aql`` - let beforeVar = aql`` - if (typeof before !== 'undefined') { - const { id: beforeId } = fromGlobalId(cleanseInput(before)) - if (typeof orderBy === 'undefined') { - beforeTemplate = aql`FILTER TO_NUMBER(org._key) < TO_NUMBER(${beforeId})` - } else { - let beforeTemplateDirection = aql`>` - if (orderBy.direction === 'ASC') { - beforeTemplateDirection = aql`<` - } - - beforeVar = aql`LET beforeVar = DOCUMENT(organizations, ${beforeId})` - - let documentField = aql`` - let orgField = aql`` + let sortByField = aql`` + if (typeof orderBy !== 'undefined') { /* istanbul ignore else */ if (orderBy.field === 'acronym') { - documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).acronym` - orgField = aql`TRANSLATE(${language}, org.orgDetails).acronym` + sortByField = aql`TRANSLATE(${language}, org.orgDetails).acronym ${orderBy.direction},` } else if (orderBy.field === 'name') { - documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).name` - orgField = aql`TRANSLATE(${language}, org.orgDetails).name` + sortByField = aql`TRANSLATE(${language}, org.orgDetails).name ${orderBy.direction},` } else if (orderBy.field === 'slug') { - documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).slug` - orgField = aql`TRANSLATE(${language}, org.orgDetails).slug` + sortByField = aql`TRANSLATE(${language}, org.orgDetails).slug ${orderBy.direction},` } else if (orderBy.field === 'zone') { - documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).zone` - orgField = aql`TRANSLATE(${language}, org.orgDetails).zone` + sortByField = aql`TRANSLATE(${language}, org.orgDetails).zone ${orderBy.direction},` } else if (orderBy.field === 'sector') { - documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).sector` - orgField = aql`TRANSLATE(${language}, org.orgDetails).sector` + sortByField = aql`TRANSLATE(${language}, org.orgDetails).sector ${orderBy.direction},` } else if (orderBy.field === 'country') { - documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).country` - orgField = aql`TRANSLATE(${language}, org.orgDetails).country` + sortByField = aql`TRANSLATE(${language}, org.orgDetails).country ${orderBy.direction},` } else if (orderBy.field === 'province') { - documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).province` - orgField = aql`TRANSLATE(${language}, org.orgDetails).province` + sortByField = aql`TRANSLATE(${language}, org.orgDetails).province ${orderBy.direction},` } else if (orderBy.field === 'city') { - documentField = aql`TRANSLATE(${language}, beforeVar.orgDetails).city` - orgField = aql`TRANSLATE(${language}, org.orgDetails).city` + sortByField = aql`TRANSLATE(${language}, org.orgDetails).city ${orderBy.direction},` } else if (orderBy.field === 'verified') { - documentField = aql`beforeVar.verified` - orgField = aql`org.verified` + sortByField = aql`org.verified ${orderBy.direction},` } else if (orderBy.field === 'summary-mail-pass') { - documentField = aql`beforeVar.summaries.mail.pass` - orgField = aql`org.summaries.mail.pass` + sortByField = aql`org.summaries.mail.pass ${orderBy.direction},` } else if (orderBy.field === 'summary-mail-fail') { - documentField = aql`beforeVar.summaries.mail.fail` - orgField = aql`org.summaries.mail.fail` + sortByField = aql`org.summaries.mail.fail ${orderBy.direction},` } else if (orderBy.field === 'summary-mail-total') { - documentField = aql`beforeVar.summaries.mail.total` - orgField = aql`org.summaries.mail.total` + sortByField = aql`org.summaries.mail.total ${orderBy.direction},` } else if (orderBy.field === 'summary-web-pass') { - documentField = aql`beforeVar.summaries.web.pass` - orgField = aql`org.summaries.web.pass` + sortByField = aql`org.summaries.web.pass ${orderBy.direction},` } else if (orderBy.field === 'summary-web-fail') { - documentField = aql`beforeVar.summaries.web.fail` - orgField = aql`org.summaries.web.fail` + sortByField = aql`org.summaries.web.fail ${orderBy.direction},` } else if (orderBy.field === 'summary-web-total') { - documentField = aql`beforeVar.summaries.web.total` - orgField = aql`org.summaries.web.total` + sortByField = aql`org.summaries.web.total ${orderBy.direction},` } else if (orderBy.field === 'domain-count') { - documentField = aql`COUNT(FOR v, e IN 1..1 OUTBOUND beforeVar._id claims RETURN e._to)` - orgField = aql`COUNT(orgDomains)` + sortByField = aql`COUNT(orgDomains) ${orderBy.direction},` } - - beforeTemplate = aql` - FILTER ${orgField} ${beforeTemplateDirection} ${documentField} - OR (${orgField} == ${documentField} - AND TO_NUMBER(org._key) < TO_NUMBER(${beforeId})) - ` } - } - let limitTemplate = aql`` - if (typeof first === 'undefined' && typeof last === 'undefined') { - console.warn( - `User: ${userKey} did not have either \`first\` or \`last\` arguments set for: loadOrgConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`You must provide a \`first\` or \`last\` value to properly paginate the \`Organization\` connection.`, - ), - ) - } else if (typeof first !== 'undefined' && typeof last !== 'undefined') { - console.warn( - `User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadOrgConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`Passing both \`first\` and \`last\` to paginate the \`Organization\` connection is not supported.`, - ), - ) - } else if (typeof first === 'number' || typeof last === 'number') { - /* istanbul ignore else */ - if (first < 0 || last < 0) { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadOrgConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`\`${argSet}\` on the \`Organization\` connection cannot be less than zero.`, - ), - ) - } else if (first > 100 || last > 100) { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - const amount = typeof first !== 'undefined' ? first : last - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` to ${amount} for: loadOrgConnectionsByUserId.`, - ) - throw new Error( - i18n._( - t`Requesting \`${amount}\` records on the \`Organization\` connection exceeds the \`${argSet}\` limit of 100 records.`, - ), - ) - } else if (typeof first !== 'undefined' && typeof last === 'undefined') { - limitTemplate = aql`TO_NUMBER(org._key) ASC LIMIT TO_NUMBER(${first})` - } else if (typeof first === 'undefined' && typeof last !== 'undefined') { - limitTemplate = aql`TO_NUMBER(org._key) DESC LIMIT TO_NUMBER(${last})` + let sortString + if (typeof last !== 'undefined') { + sortString = aql`DESC` + } else { + sortString = aql`ASC` } - } else { - const argSet = typeof first !== 'undefined' ? 'first' : 'last' - const typeSet = typeof first !== 'undefined' ? typeof first : typeof last - console.warn( - `User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadOrgConnectionsByUserId.`, - ) - throw new Error( - i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`), - ) - } - let hasNextPageFilter = aql`FILTER TO_NUMBER(org._key) > TO_NUMBER(LAST(retrievedOrgs)._key)` - let hasPreviousPageFilter = aql`FILTER TO_NUMBER(org._key) < TO_NUMBER(FIRST(retrievedOrgs)._key)` - if (typeof orderBy !== 'undefined') { - let hasNextPageDirection = aql`<` - let hasPreviousPageDirection = aql`>` - if (orderBy.direction === 'ASC') { - hasNextPageDirection = aql`>` - hasPreviousPageDirection = aql`<` + let includeSuperAdminOrgQuery = aql`` + if (!includeSuperAdminOrg) { + includeSuperAdminOrgQuery = aql`FILTER org.orgDetails.en.slug != "super-admin" OR org.orgDetails.fr.slug != "super-admin"` } - let orgField = aql`` - let hasNextPageDocumentField = aql`` - let hasPreviousPageDocumentField = aql`` - /* istanbul ignore else */ - if (orderBy.field === 'acronym') { - orgField = aql`TRANSLATE(${language}, org.orgDetails).acronym` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).acronym` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).acronym` - } else if (orderBy.field === 'name') { - orgField = aql`TRANSLATE(${language}, org.orgDetails).name` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).name` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).name` - } else if (orderBy.field === 'slug') { - orgField = aql`TRANSLATE(${language}, org.orgDetails).slug` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).slug` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).slug` - } else if (orderBy.field === 'zone') { - orgField = aql`TRANSLATE(${language}, org.orgDetails).zone` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).zone` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).zone` - } else if (orderBy.field === 'sector') { - orgField = aql`TRANSLATE(${language}, org.orgDetails).sector` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).sector` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).sector` - } else if (orderBy.field === 'country') { - orgField = aql`TRANSLATE(${language}, org.orgDetails).country` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).country` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).country` - } else if (orderBy.field === 'province') { - orgField = aql`TRANSLATE(${language}, org.orgDetails).province` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).province` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).province` - } else if (orderBy.field === 'city') { - orgField = aql`TRANSLATE(${language}, org.orgDetails).city` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).city` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).city` - } else if (orderBy.field === 'verified') { - orgField = aql`org.verified` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).verified` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).verified` - } else if (orderBy.field === 'summary-mail-pass') { - orgField = aql`org.summaries.mail.pass` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.mail.pass` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.mail.pass` - } else if (orderBy.field === 'summary-mail-fail') { - orgField = aql`org.summaries.mail.fail` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.mail.fail` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.mail.fail` - } else if (orderBy.field === 'summary-mail-total') { - orgField = aql`org.summaries.mail.total` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.mail.total` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.mail.total` - } else if (orderBy.field === 'summary-web-pass') { - orgField = aql`org.summaries.web.pass` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.web.pass` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.web.pass` - } else if (orderBy.field === 'summary-web-fail') { - orgField = aql`org.summaries.web.fail` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.web.fail` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.web.fail` - } else if (orderBy.field === 'summary-web-total') { - orgField = aql`org.summaries.web.total` - hasNextPageDocumentField = aql`LAST(retrievedOrgs).summaries.web.total` - hasPreviousPageDocumentField = aql`FIRST(retrievedOrgs).summaries.web.total` - } else if (orderBy.field === 'domain-count') { - orgField = aql`COUNT(orgDomains)` - hasNextPageDocumentField = aql`COUNT(FOR v, e IN 1..1 OUTBOUND LAST(retrievedOrgs)._id claims RETURN e._to)` - hasPreviousPageDocumentField = aql`COUNT(FOR v, e IN 1..1 OUTBOUND FIRST(retrievedOrgs)._id claims RETURN e._to)` + let isVerifiedQuery = aql`` + if (isVerified) { + isVerifiedQuery = aql`FILTER org.verified == true` } - hasNextPageFilter = aql` - FILTER ${orgField} ${hasNextPageDirection} ${hasNextPageDocumentField} - OR (${orgField} == ${hasNextPageDocumentField} - AND TO_NUMBER(org._key) > TO_NUMBER(LAST(retrievedOrgs)._key)) - ` - hasPreviousPageFilter = aql` - FILTER ${orgField} ${hasPreviousPageDirection} ${hasPreviousPageDocumentField} - OR (${orgField} == ${hasPreviousPageDocumentField} - AND TO_NUMBER(org._key) < TO_NUMBER(FIRST(retrievedOrgs)._key)) - ` - } - - let sortByField = aql`` - if (typeof orderBy !== 'undefined') { - /* istanbul ignore else */ - if (orderBy.field === 'acronym') { - sortByField = aql`TRANSLATE(${language}, org.orgDetails).acronym ${orderBy.direction},` - } else if (orderBy.field === 'name') { - sortByField = aql`TRANSLATE(${language}, org.orgDetails).name ${orderBy.direction},` - } else if (orderBy.field === 'slug') { - sortByField = aql`TRANSLATE(${language}, org.orgDetails).slug ${orderBy.direction},` - } else if (orderBy.field === 'zone') { - sortByField = aql`TRANSLATE(${language}, org.orgDetails).zone ${orderBy.direction},` - } else if (orderBy.field === 'sector') { - sortByField = aql`TRANSLATE(${language}, org.orgDetails).sector ${orderBy.direction},` - } else if (orderBy.field === 'country') { - sortByField = aql`TRANSLATE(${language}, org.orgDetails).country ${orderBy.direction},` - } else if (orderBy.field === 'province') { - sortByField = aql`TRANSLATE(${language}, org.orgDetails).province ${orderBy.direction},` - } else if (orderBy.field === 'city') { - sortByField = aql`TRANSLATE(${language}, org.orgDetails).city ${orderBy.direction},` - } else if (orderBy.field === 'verified') { - sortByField = aql`org.verified ${orderBy.direction},` - } else if (orderBy.field === 'summary-mail-pass') { - sortByField = aql`org.summaries.mail.pass ${orderBy.direction},` - } else if (orderBy.field === 'summary-mail-fail') { - sortByField = aql`org.summaries.mail.fail ${orderBy.direction},` - } else if (orderBy.field === 'summary-mail-total') { - sortByField = aql`org.summaries.mail.total ${orderBy.direction},` - } else if (orderBy.field === 'summary-web-pass') { - sortByField = aql`org.summaries.web.pass ${orderBy.direction},` - } else if (orderBy.field === 'summary-web-fail') { - sortByField = aql`org.summaries.web.fail ${orderBy.direction},` - } else if (orderBy.field === 'summary-web-total') { - sortByField = aql`org.summaries.web.total ${orderBy.direction},` - } else if (orderBy.field === 'domain-count') { - sortByField = aql`COUNT(orgDomains) ${orderBy.direction},` + let isAdminFilter = aql`` + if (isAdmin) { + isAdminFilter = aql`FILTER e.permission IN ["admin", "owner", "super_admin"]` } - } - - let sortString - if (typeof last !== 'undefined') { - sortString = aql`DESC` - } else { - sortString = aql`ASC` - } - let includeSuperAdminOrgQuery = aql`` - if (!includeSuperAdminOrg) { - includeSuperAdminOrgQuery = aql`FILTER org.orgDetails.en.slug != "super-admin" OR org.orgDetails.fr.slug != "super-admin"` - } - - let orgKeysQuery - if (isSuperAdmin) { - orgKeysQuery = aql` + let orgKeysQuery + if (isSuperAdmin) { + orgKeysQuery = aql` WITH claims, domains, organizations, organizationSearch LET orgKeys = ( FOR org IN organizations + ${isVerifiedQuery} ${includeSuperAdminOrgQuery} RETURN org._key ) ` - } else if (isAdmin) { - orgKeysQuery = aql` + } else if (!loginRequiredBool) { + orgKeysQuery = aql` WITH affiliations, claims, domains, organizations, organizationSearch, users - LET orgKeys = ( - FOR org, e IN 1..1 - INBOUND ${userDBId} affiliations - FILTER e.permission == "admin" - OR e.permission == "super_admin" - ${includeSuperAdminOrgQuery} - RETURN org._key + LET userAffiliations = ( + FOR v, e IN 1..1 ANY ${userDBId} affiliations + FILTER e.permission != "pending" + ${isAdminFilter} + RETURN v ) - ` - } else { - if (!loginRequiredBool) { - orgKeysQuery = aql` - WITH claims, domains, organizations, organizationSearch LET orgKeys = ( FOR org IN organizations - FILTER org.orgDetails.en.slug != "super-admin" OR org.orgDetails.fr.slug != "super-admin" + ${isVerifiedQuery} + ${includeSuperAdminOrgQuery} + FILTER org._key IN userAffiliations[*]._key ${isAdmin ? aql`` : aql`|| org.verified == true`} RETURN org._key ) - ` + ` } else { orgKeysQuery = aql` WITH affiliations, claims, domains, organizations, organizationSearch, users + LET userAffiliations = ( + FOR v, e IN 1..1 ANY ${userDBId} affiliations + FILTER e.permission != "pending" + ${isAdminFilter} + RETURN v + ) + LET hasVerifiedOrgAffiliation = POSITION(userAffiliations[*].verified, true) LET orgKeys = ( - FOR org, e IN 1..1 - INBOUND ${userDBId} affiliations - ${includeSuperAdminOrgQuery} - RETURN org._key + FOR org IN organizations + ${isVerifiedQuery} + ${includeSuperAdminOrgQuery} + FILTER org._key IN userAffiliations[*]._key ${ + isAdmin ? aql`` : aql`|| (org.verified == true && hasVerifiedOrgAffiliation == true)` + } + RETURN org._key ) ` } - } - let orgQuery = aql`` - let filterString = aql`FILTER org._key IN orgKeys` - let totalCount = aql`LENGTH(orgKeys)` - if (typeof search !== 'undefined' && search !== '') { - search = cleanseInput(search) - orgQuery = aql` + let orgQuery = aql`` + let filterString = aql`FILTER org._key IN orgKeys` + let totalCount = aql`LENGTH(orgKeys)` + if (typeof search !== 'undefined' && search !== '') { + search = cleanseInput(search) + orgQuery = aql` LET tokenArrEN = TOKENS(${search}, "text_en") LET searchedOrgsEN = FLATTEN(UNIQUE( FOR token IN tokenArrEN @@ -447,13 +433,13 @@ export const loadOrgConnectionsByUserId = ({ )) LET searchedOrgs = UNION_DISTINCT(searchedOrgsEN, searchedOrgsFR) ` - filterString = aql`FILTER org._key IN searchedOrgs` - totalCount = aql`LENGTH(searchedOrgs)` - } + filterString = aql`FILTER org._key IN searchedOrgs` + totalCount = aql`LENGTH(searchedOrgs)` + } - let organizationInfoCursor - try { - organizationInfoCursor = await query` + let organizationInfoCursor + try { + organizationInfoCursor = await query` ${orgKeysQuery} ${orgQuery} @@ -512,55 +498,51 @@ export const loadOrgConnectionsByUserId = ({ "endKey": LAST(retrievedOrgs)._key } ` - } catch (err) { - console.error( - `Database error occurred while user: ${userKey} was trying to query organizations in loadOrgConnectionsByUserId, error: ${err}`, - ) - throw new Error( - i18n._(t`Unable to load organization(s). Please try again.`), - ) - } + } catch (err) { + console.error( + `Database error occurred while user: ${userKey} was trying to query organizations in loadOrgConnectionsByUserId, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to load organization(s). Please try again.`)) + } - let organizationInfo - try { - organizationInfo = await organizationInfoCursor.next() - } catch (err) { - console.error( - `Cursor error occurred while user: ${userKey} was trying to gather organizations in loadOrgConnectionsByUserId, error: ${err}`, - ) - throw new Error( - i18n._(t`Unable to load organization(s). Please try again.`), - ) - } + let organizationInfo + try { + organizationInfo = await organizationInfoCursor.next() + } catch (err) { + console.error( + `Cursor error occurred while user: ${userKey} was trying to gather organizations in loadOrgConnectionsByUserId, error: ${err}`, + ) + throw new Error(i18n._(t`Unable to load organization(s). Please try again.`)) + } - if (organizationInfo.organizations.length === 0) { - return { - edges: [], - totalCount: 0, - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, + if (organizationInfo.organizations.length === 0) { + return { + edges: [], + totalCount: 0, + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', + }, + } } - } - const edges = organizationInfo.organizations.map((org) => { + const edges = organizationInfo.organizations.map((org) => { + return { + cursor: toGlobalId('organization', org._key), + node: org, + } + }) + return { - cursor: toGlobalId('organization', org._key), - node: org, + edges, + totalCount: organizationInfo.totalCount, + pageInfo: { + hasNextPage: organizationInfo.hasNextPage, + hasPreviousPage: organizationInfo.hasPreviousPage, + startCursor: toGlobalId('organization', organizationInfo.startKey), + endCursor: toGlobalId('organization', organizationInfo.endKey), + }, } - }) - - return { - edges, - totalCount: organizationInfo.totalCount, - pageInfo: { - hasNextPage: organizationInfo.hasNextPage, - hasPreviousPage: organizationInfo.hasPreviousPage, - startCursor: toGlobalId('organization', organizationInfo.startKey), - endCursor: toGlobalId('organization', organizationInfo.endKey), - }, } -} diff --git a/api/src/organization/loaders/load-organization-domain-statuses.js b/api/src/organization/loaders/load-organization-domain-statuses.js index 2eb9380ab8..80a48921db 100644 --- a/api/src/organization/loaders/load-organization-domain-statuses.js +++ b/api/src/organization/loaders/load-organization-domain-statuses.js @@ -1,25 +1,24 @@ import { t } from '@lingui/macro' -export const loadOrganizationDomainStatuses = ({ query, userKey, i18n }) => - async ({orgId}) => { +export const loadOrganizationDomainStatuses = + ({ query, userKey, i18n }) => + async ({ orgId }) => { let domains try { - domains = (await query` + domains = ( + await query` WITH claims, domains, organizations FOR v, e IN 1..1 OUTBOUND ${orgId} claims RETURN { domain: v.domain, status: v.status } - `).all() + ` + ).all() } catch (err) { - console.error( - `Database error occurred when user: ${userKey} running loadOrganizationDomainStatuses: ${err}`, - ) - throw new Error( - i18n._(t`Unable to load organization domain statuses. Please try again.`), - ) + console.error(`Database error occurred when user: ${userKey} running loadOrganizationDomainStatuses: ${err}`) + throw new Error(i18n._(t`Unable to load organization domain statuses. Please try again.`)) } return domains diff --git a/api/src/organization/mutations/__tests__/archive-organization.test.js b/api/src/organization/mutations/__tests__/archive-organization.test.js new file mode 100644 index 0000000000..507dfc69ab --- /dev/null +++ b/api/src/organization/mutations/__tests__/archive-organization.test.js @@ -0,0 +1,529 @@ +import { setupI18n } from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { toGlobalId } from 'graphql-relay' + +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import englishMessages from '../../../locale/en/messages' +import frenchMessages from '../../../locale/fr/messages' +import { cleanseInput } from '../../../validators' +import { checkPermission, userRequired, verifiedRequired } from '../../../auth' +import { loadUserByKey } from '../../../user/loaders' +import { loadOrgByKey } from '../../loaders' +import dbschema from '../../../../database.json' +import { collectionNames } from '../../../collection-names' + +const { DB_PASS: rootPass, DB_URL: url } = process.env + +describe('archiving an organization', () => { + let query, drop, truncate, schema, collections, transaction, user, i18n + + const consoleOutput = [] + const mockedInfo = (output) => consoleOutput.push(output) + const mockedWarn = (output) => consoleOutput.push(output) + const mockedError = (output) => consoleOutput.push(output) + beforeAll(() => { + console.info = mockedInfo + console.warn = mockedWarn + console.error = mockedError + // Create GQL Schema + schema = new GraphQLSchema({ + query: createQuerySchema(), + mutation: createMutationSchema(), + }) + }) + afterEach(() => { + consoleOutput.length = 0 + }) + describe('given a successful archival', () => { + let org, domain + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + beforeEach(async () => { + ;({ query, drop, truncate, collections, transaction } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + user = await collections.users.save({ + userName: 'test.account@istio.actually.exists', + emailValidated: true, + }) + domain = await collections.domains.save({ + domain: 'test.gc.ca', + slug: 'test-gc-ca', + }) + }) + afterEach(async () => { + await truncate() + await drop() + }) + describe('users permission is super admin', () => { + describe('org is verified', () => { + beforeEach(async () => { + org = await collections.organizations.save({ + verified: true, + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }) + const superAdminOrg = await collections.organizations.save({ + verified: false, + orgDetails: { + en: { + slug: 'super-admin', + acronym: 'SA', + }, + fr: { + slug: 'super-admin', + acronym: 'SA', + }, + }, + }) + await collections.affiliations.save({ + _from: superAdminOrg._id, + _to: user._id, + permission: 'super_admin', + }) + await collections.claims.save({ + _from: org._id, + _to: domain._id, + }) + }) + + describe('org is the only one claiming the domain', () => { + it('archives the domain', async () => { + await graphql( + schema, + ` + mutation { + archiveOrganization( + input: { + orgId: "${toGlobalId('organization', org._key)}" + } + ) { + result { + ... on OrganizationResult { + status + organization { + name + } + } + ... on OrganizationError { + code + description + } + } + } + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkPermission: checkPermission({ + userKey: user._key, + query, + }), + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({}), + }, + validators: { cleanseInput }, + loaders: { + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + }, + }, + ) + + await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` + + const domainCursor = await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` + const domainCheck = await domainCursor.next() + expect(domainCheck.archived).toEqual(true) + }) + }) + describe('multiple orgs claim the domain', () => { + beforeEach(async () => { + const secondOrg = await collections.organizations.save({}) + await collections.claims.save({ + _from: secondOrg._id, + _to: domain._id, + }) + }) + it('does not archive the domain', async () => { + await graphql( + schema, + ` + mutation { + archiveOrganization( + input: { + orgId: "${toGlobalId('organization', org._key)}" + } + ) { + result { + ... on OrganizationResult { + status + organization { + name + } + } + ... on OrganizationError { + code + description + } + } + } + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkPermission: checkPermission({ + userKey: user._key, + query, + }), + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({}), + }, + validators: { cleanseInput }, + loaders: { + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + loadUserByKey: loadUserByKey({ query }), + }, + }, + ) + + await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` + + const domainCursor = await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` + const domainCheck = await domainCursor.next() + expect(domainCheck.archived).toEqual(false || undefined) + }) + }) + }) + }) + }) + describe('given an unsuccessful archival', () => { + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + describe('the requested org is undefined', () => { + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + archiveOrganization( + input: { + orgId: "${toGlobalId('organization', 123)}" + } + ) { + result { + ... on OrganizationResult { + status + organization { + name + } + } + ... on OrganizationError { + code + description + } + } + } + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: 123, + auth: { + checkPermission: jest.fn(), + userRequired: jest.fn(), + verifiedRequired: jest.fn(), + }, + validators: { cleanseInput }, + loaders: { + loadOrgByKey: { + load: jest.fn().mockReturnValue(undefined), + }, + }, + }, + ) + + const expectedResponse = { + data: { + archiveOrganization: { + result: { + code: 400, + description: 'Unable to archive unknown organization.', + }, + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: 123 attempted to archive org: 123, but there is no org associated with that id.`, + ]) + }) + }) + describe('given an incorrect permission', () => { + describe('users belong to the org', () => { + describe('users role is admin', () => { + describe('user attempts to archive an org', () => { + it('returns an error', async () => { + const response = await graphql( + schema, + ` + mutation { + archiveOrganization( + input: { + orgId: "${toGlobalId('organization', 123)}" + } + ) { + result { + ... on OrganizationResult { + status + organization { + name + } + } + ... on OrganizationError { + code + description + } + } + } + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: 123, + auth: { + checkPermission: jest.fn().mockReturnValue('admin'), + userRequired: jest.fn(), + verifiedRequired: jest.fn(), + }, + validators: { cleanseInput }, + loaders: { + loadOrgByKey: { + load: jest.fn().mockReturnValue({ + _key: 123, + verified: true, + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }), + }, + }, + }, + ) + + const expectedResponse = { + data: { + archiveOrganization: { + result: { + code: 403, + description: + 'Permission Denied: Please contact super admin for help with archiving organization.', + }, + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: 123 attempted to archive org: 123, however they do not have the correct permission level. Permission: admin`, + ]) + }) + }) + }) + }) + }) + describe('given a trx commit error', () => { + it('throws an error', async () => { + const mockedCursor = { + all: jest.fn().mockReturnValueOnce([]).mockReturnValue([]), + } + + const mockedQuery = jest.fn().mockReturnValue(mockedCursor) + + const mockedTransaction = jest.fn().mockReturnValue({ + step: jest.fn().mockReturnValue({}), + commit: jest.fn().mockRejectedValue(new Error('Commit Error')), + }) + + const response = await graphql( + schema, + ` + mutation { + archiveOrganization( + input: { + orgId: "${toGlobalId('organization', 123)}" + } + ) { + result { + ... on OrganizationResult { + status + organization { + name + } + } + ... on OrganizationError { + code + description + } + } + } + } + `, + null, + { + i18n, + query: mockedQuery, + collections: collectionNames, + transaction: mockedTransaction, + userKey: 123, + auth: { + checkPermission: jest.fn().mockReturnValue('super_admin'), + userRequired: jest.fn(), + verifiedRequired: jest.fn(), + }, + validators: { cleanseInput }, + loaders: { + loadOrgByKey: { + load: jest.fn().mockReturnValue({ + _key: 123, + verified: false, + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }), + }, + }, + }, + ) + + const error = [new GraphQLError('Unable to archive organization. Please try again.')] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `Trx commit error occurred for user: 123 while attempting archive of org: 123, Error: Commit Error`, + ]) + }) + }) + }) + }) +}) diff --git a/api/src/organization/mutations/__tests__/verify-organization.test.js b/api/src/organization/mutations/__tests__/verify-organization.test.js index f1f4059648..7f7eaac007 100644 --- a/api/src/organization/mutations/__tests__/verify-organization.test.js +++ b/api/src/organization/mutations/__tests__/verify-organization.test.js @@ -182,8 +182,7 @@ describe('removing an organization', () => { data: { verifyOrganization: { result: { - status: - 'Successfully verified organization: treasury-board-secretariat.', + status: 'Successfully verified organization: treasury-board-secretariat.', organization: { name: 'Treasury Board of Canada Secretariat', }, @@ -193,9 +192,7 @@ describe('removing an organization', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key}, successfully verified org: ${org._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key}, successfully verified org: ${org._key}.`]) const orgLoader = loadOrgByKey({ query, @@ -294,8 +291,7 @@ describe('removing an organization', () => { data: { verifyOrganization: { result: { - status: - "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", + status: "Envoi réussi de l'invitation au service, et de l'email de l'organisation.", organization: { name: 'Secrétariat du Conseil Trésor du Canada', }, @@ -305,9 +301,7 @@ describe('removing an organization', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key}, successfully verified org: ${org._key}.`, - ]) + expect(consoleOutput).toEqual([`User: ${user._key}, successfully verified org: ${org._key}.`]) const orgLoader = loadOrgByKey({ query, @@ -637,9 +631,7 @@ describe('removing an organization', () => { query, collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), }), userKey: 123, auth: { @@ -658,11 +650,7 @@ describe('removing an organization', () => { }, ) - const error = [ - new GraphQLError( - 'Unable to verify organization. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to verify organization. Please try again.')] expect(response.errors).toEqual(error) @@ -703,10 +691,7 @@ describe('removing an organization', () => { query, collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), }), userKey: 123, auth: { @@ -725,16 +710,12 @@ describe('removing an organization', () => { }, ) - const error = [ - new GraphQLError( - 'Unable to verify organization. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to verify organization. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ - `Trx step error occurred when clearing owners for org: 123: Error: trx step error`, + `Transaction error occurred while upserting verified org: 123, err: Error: trx step error`, ]) }) }) @@ -772,9 +753,7 @@ describe('removing an organization', () => { collections: collectionNames, transaction: jest.fn().mockReturnValue({ step: jest.fn().mockReturnValue(), - commit: jest - .fn() - .mockRejectedValue(new Error('trx commit error')), + commit: jest.fn().mockRejectedValue(new Error('trx commit error')), }), userKey: 123, auth: { @@ -793,11 +772,7 @@ describe('removing an organization', () => { }, ) - const error = [ - new GraphQLError( - 'Unable to verify organization. Please try again.', - ), - ] + const error = [new GraphQLError('Unable to verify organization. Please try again.')] expect(response.errors).toEqual(error) @@ -875,8 +850,7 @@ describe('removing an organization', () => { verifyOrganization: { result: { code: 400, - description: - 'Impossible de vérifier une organisation inconnue.', + description: 'Impossible de vérifier une organisation inconnue.', }, }, }, @@ -1124,9 +1098,7 @@ describe('removing an organization', () => { query, collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), }), userKey: 123, auth: { @@ -1145,11 +1117,7 @@ describe('removing an organization', () => { }, ) - const error = [ - new GraphQLError( - "Impossible de vérifier l'organisation. Veuillez réessayer.", - ), - ] + const error = [new GraphQLError("Impossible de vérifier l'organisation. Veuillez réessayer.")] expect(response.errors).toEqual(error) @@ -1190,10 +1158,7 @@ describe('removing an organization', () => { query, collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), }), userKey: 123, auth: { @@ -1212,16 +1177,12 @@ describe('removing an organization', () => { }, ) - const error = [ - new GraphQLError( - "Impossible de vérifier l'organisation. Veuillez réessayer.", - ), - ] + const error = [new GraphQLError("Impossible de vérifier l'organisation. Veuillez réessayer.")] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ - `Trx step error occurred when clearing owners for org: 123: Error: trx step error`, + `Transaction error occurred while upserting verified org: 123, err: Error: trx step error`, ]) }) }) @@ -1259,9 +1220,7 @@ describe('removing an organization', () => { collections: collectionNames, transaction: jest.fn().mockReturnValue({ step: jest.fn().mockReturnValue(), - commit: jest - .fn() - .mockRejectedValue(new Error('trx commit error')), + commit: jest.fn().mockRejectedValue(new Error('trx commit error')), }), userKey: 123, auth: { @@ -1280,11 +1239,7 @@ describe('removing an organization', () => { }, ) - const error = [ - new GraphQLError( - "Impossible de vérifier l'organisation. Veuillez réessayer.", - ), - ] + const error = [new GraphQLError("Impossible de vérifier l'organisation. Veuillez réessayer.")] expect(response.errors).toEqual(error) diff --git a/api/src/organization/mutations/archive-organization.js b/api/src/organization/mutations/archive-organization.js new file mode 100644 index 0000000000..f8e88f4a11 --- /dev/null +++ b/api/src/organization/mutations/archive-organization.js @@ -0,0 +1,192 @@ +import { GraphQLNonNull, GraphQLID } from 'graphql' +import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay' +import { t } from '@lingui/macro' + +import { removeOrganizationUnion } from '../unions' +import { logActivity } from '../../audit-logs/mutations/log-activity' + +export const archiveOrganization = new mutationWithClientMutationId({ + name: 'ArchiveOrganization', + description: 'This mutation allows the archival of unused organizations.', + inputFields: () => ({ + orgId: { + type: GraphQLNonNull(GraphQLID), + description: 'The global id of the organization you wish you archive.', + }, + }), + outputFields: () => ({ + result: { + type: GraphQLNonNull(removeOrganizationUnion), + description: '`RemoveOrganizationUnion` returning either an `OrganizationResult`, or `OrganizationError` object.', + resolve: (payload) => payload, + }, + }), + mutateAndGetPayload: async ( + args, + { + i18n, + query, + collections, + transaction, + userKey, + auth: { checkPermission, userRequired, verifiedRequired }, + validators: { cleanseInput }, + loaders: { loadOrgByKey }, + }, + ) => { + // Get user + const user = await userRequired() + + verifiedRequired({ user }) + + // Cleanse Input + const { type: _orgType, id: orgId } = fromGlobalId(cleanseInput(args.orgId)) + + // Get org from db + const organization = await loadOrgByKey.load(orgId) + + // Check to see if org exists + if (!organization) { + console.warn(`User: ${userKey} attempted to archive org: ${orgId}, but there is no org associated with that id.`) + return { + _type: 'error', + code: 400, + description: i18n._(t`Unable to archive unknown organization.`), + } + } + + // Get users permission + const permission = await checkPermission({ orgId: organization._id }) + + if (permission !== 'super_admin') { + console.warn( + `User: ${userKey} attempted to archive org: ${organization._key}, however they do not have the correct permission level. Permission: ${permission}`, + ) + return { + _type: 'error', + code: 403, + description: i18n._(t`Permission Denied: Please contact super admin for help with archiving organization.`), + } + } + + // Setup Trans action + const trx = await transaction(collections) + + // check to see if any other orgs are using this domain + let countCursor + try { + countCursor = await query` + WITH claims, domains, organizations + LET domainIds = ( + FOR v, e IN 1..1 OUTBOUND ${organization._id} claims + RETURN e._to + ) + FOR domain IN domains + FILTER domain._id IN domainIds + LET count = LENGTH( + FOR v, e IN 1..1 INBOUND domain._id claims + RETURN 1 + ) + RETURN { + _id: domain._id, + _key: domain._key, + domain: domain.domain, + count + } + ` + } catch (err) { + console.error( + `Database error occurred for user: ${userKey} while attempting to gather domain count while archiving org: ${organization._key}, ${err}`, + ) + throw new Error(i18n._(t`Unable to archive organization. Please try again.`)) + } + + let domainInfo + try { + domainInfo = await countCursor.all() + } catch (err) { + console.error( + `Cursor error occurred for user: ${userKey} while attempting to gather domain count while archiving org: ${organization._key}, ${err}`, + ) + throw new Error(i18n._(t`Unable to archive organization. Please try again.`)) + } + + for (const domain of domainInfo) { + if (domain.count === 1) { + try { + // Archive domain + await trx.step( + async () => + await query` + WITH domains + UPDATE { _key: ${domain._key}, archived: true } IN domains + `, + ) + } catch (err) { + console.error( + `Trx step error occurred for user: ${userKey} while attempting to archive domains while archiving org: ${organization._key}, ${err}`, + ) + throw new Error(i18n._(t`Unable to archive organization. Please try again.`)) + } + } + } + + try { + await trx.step( + () => + query` + WITH organizations + UPDATE { _key: ${organization._key}, verified: false } IN organizations + `, + ) + } catch (err) { + console.error( + `Trx step error occurred for user: ${userKey} while attempting to unverify while archiving org: ${organization._key}, ${err}`, + ) + throw new Error(i18n._(t`Unable to archive organization. Please try again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error( + `Trx commit error occurred for user: ${userKey} while attempting archive of org: ${organization._key}, ${err}`, + ) + throw new Error(i18n._(t`Unable to archive organization. Please try again.`)) + } + + console.info(`User: ${userKey} successfully archived org: ${organization._key}.`) + + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'update', + updatedProperties: [ + { + name: 'archived', + oldValue: false, + newValue: true, + }, + ], + target: { + resource: { + en: organization.name, + fr: organization.name, + }, // name of resource being acted upon + resourceType: 'organization', // user, org, domain + }, + }) + + return { + _type: 'result', + status: i18n._(t`Successfully archived organization: ${organization.slug}.`), + organization, + } + }, +}) diff --git a/api/src/organization/mutations/create-organization.js b/api/src/organization/mutations/create-organization.js index 2fef6741d5..3fadac03a5 100644 --- a/api/src/organization/mutations/create-organization.js +++ b/api/src/organization/mutations/create-organization.js @@ -8,8 +8,7 @@ import { logActivity } from '../../audit-logs/mutations/log-activity' export const createOrganization = new mutationWithClientMutationId({ name: 'CreateOrganization', - description: - 'This mutation allows the creation of an organization inside the database.', + description: 'This mutation allows the creation of an organization inside the database.', inputFields: () => ({ acronymEN: { type: GraphQLNonNull(Acronym), @@ -29,60 +28,49 @@ export const createOrganization = new mutationWithClientMutationId({ }, zoneEN: { type: GraphQLNonNull(GraphQLString), - description: - 'The English translation of the zone the organization belongs to.', + description: 'The English translation of the zone the organization belongs to.', }, zoneFR: { type: GraphQLNonNull(GraphQLString), - description: - 'The English translation of the zone the organization belongs to.', + description: 'The English translation of the zone the organization belongs to.', }, sectorEN: { type: GraphQLNonNull(GraphQLString), - description: - 'The English translation of the sector the organization belongs to.', + description: 'The English translation of the sector the organization belongs to.', }, sectorFR: { type: GraphQLNonNull(GraphQLString), - description: - 'The French translation of the sector the organization belongs to.', + description: 'The French translation of the sector the organization belongs to.', }, countryEN: { type: GraphQLNonNull(GraphQLString), - description: - 'The English translation of the country the organization resides in.', + description: 'The English translation of the country the organization resides in.', }, countryFR: { type: GraphQLNonNull(GraphQLString), - description: - 'The French translation of the country the organization resides in.', + description: 'The French translation of the country the organization resides in.', }, provinceEN: { type: GraphQLNonNull(GraphQLString), - description: - 'The English translation of the province the organization resides in.', + description: 'The English translation of the province the organization resides in.', }, provinceFR: { type: GraphQLNonNull(GraphQLString), - description: - 'The French translation of the province the organization resides in.', + description: 'The French translation of the province the organization resides in.', }, cityEN: { type: GraphQLNonNull(GraphQLString), - description: - 'The English translation of the city the organization resides in.', + description: 'The English translation of the city the organization resides in.', }, cityFR: { type: GraphQLNonNull(GraphQLString), - description: - 'The French translation of the city the organization resides in.', + description: 'The French translation of the city the organization resides in.', }, }), outputFields: () => ({ result: { type: createOrganizationUnion, - description: - '`CreateOrganizationUnion` returning either an `Organization`, or `OrganizationError` object.', + description: '`CreateOrganizationUnion` returning either an `Organization`, or `OrganizationError` object.', resolve: (payload) => payload, }, }), @@ -129,15 +117,11 @@ export const createOrganization = new mutationWithClientMutationId({ const [orgEN, orgFR] = await loadOrgBySlug.loadMany([slugEN, slugFR]) if (typeof orgEN !== 'undefined' || typeof orgFR !== 'undefined') { - console.warn( - `User: ${userKey} attempted to create an organization that already exists: ${slugEN}`, - ) + console.warn(`User: ${userKey} attempted to create an organization that already exists: ${slugEN}`) return { _type: 'error', code: 400, - description: i18n._( - t`Organization name already in use. Please try again with a different name.`, - ), + description: i18n._(t`Organization name already in use. Please try again with a different name.`), } } @@ -194,12 +178,8 @@ export const createOrganization = new mutationWithClientMutationId({ `, ) } catch (err) { - console.error( - `Transaction error occurred when user: ${userKey} was creating new organization ${slugEN}: ${err}`, - ) - throw new Error( - i18n._(t`Unable to create organization. Please try again.`), - ) + console.error(`Transaction error occurred when user: ${userKey} was creating new organization ${slugEN}: ${err}`) + throw new Error(i18n._(t`Unable to create organization. Please try again.`)) } const organization = await cursor.next() @@ -211,8 +191,7 @@ export const createOrganization = new mutationWithClientMutationId({ INSERT { _from: ${organization._id}, _to: ${user._id}, - permission: "admin", - owner: true + permission: "owner", } INTO affiliations `, ) @@ -220,9 +199,7 @@ export const createOrganization = new mutationWithClientMutationId({ console.error( `Transaction error occurred when inserting edge definition for user: ${userKey} to ${slugEN}: ${err}`, ) - throw new Error( - i18n._(t`Unable to create organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to create organization. Please try again.`)) } try { @@ -231,14 +208,10 @@ export const createOrganization = new mutationWithClientMutationId({ console.error( `Transaction error occurred when committing new organization: ${slugEN} for user: ${userKey} to db: ${err}`, ) - throw new Error( - i18n._(t`Unable to create organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to create organization. Please try again.`)) } - console.info( - `User: ${userKey} successfully created a new organization: ${slugEN}`, - ) + console.info(`User: ${userKey} successfully created a new organization: ${slugEN}`) await logActivity({ transaction, collections, diff --git a/api/src/organization/mutations/index.js b/api/src/organization/mutations/index.js index a2e9b7d45f..2ee2d34e15 100644 --- a/api/src/organization/mutations/index.js +++ b/api/src/organization/mutations/index.js @@ -1,3 +1,4 @@ +export * from './archive-organization' export * from './create-organization' export * from './remove-organization' export * from './update-organization' diff --git a/api/src/organization/mutations/remove-organization.js b/api/src/organization/mutations/remove-organization.js index c920e34f63..56eeda0b61 100644 --- a/api/src/organization/mutations/remove-organization.js +++ b/api/src/organization/mutations/remove-organization.js @@ -17,8 +17,7 @@ export const removeOrganization = new mutationWithClientMutationId({ outputFields: () => ({ result: { type: GraphQLNonNull(removeOrganizationUnion), - description: - '`RemoveOrganizationUnion` returning either an `OrganizationResult`, or `OrganizationError` object.', + description: '`RemoveOrganizationUnion` returning either an `OrganizationResult`, or `OrganizationError` object.', resolve: (payload) => payload, }, }), @@ -48,9 +47,7 @@ export const removeOrganization = new mutationWithClientMutationId({ // Check to see if org exists if (!organization) { - console.warn( - `User: ${userKey} attempted to remove org: ${orgId}, but there is no org associated with that id.`, - ) + console.warn(`User: ${userKey} attempted to remove org: ${orgId}, but there is no org associated with that id.`) return { _type: 'error', code: 400, @@ -82,9 +79,7 @@ export const removeOrganization = new mutationWithClientMutationId({ return { _type: 'error', code: 403, - description: i18n._( - t`Permission Denied: Please contact super admin for help with removing organization.`, - ), + description: i18n._(t`Permission Denied: Please contact super admin for help with removing organization.`), } } @@ -103,9 +98,7 @@ export const removeOrganization = new mutationWithClientMutationId({ console.error( `Database error occurred for user: ${userKey} while attempting to get dmarcSummaryInfo while removing org: ${organization._key}, ${err}`, ) - throw new Error( - i18n._(t`Unable to remove organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to remove organization. Please try again.`)) } let dmarcSummaryCheckList @@ -115,9 +108,7 @@ export const removeOrganization = new mutationWithClientMutationId({ console.error( `Cursor error occurred for user: ${userKey} while attempting to get dmarcSummaryInfo while removing org: ${organization._key}, ${err}`, ) - throw new Error( - i18n._(t`Unable to remove organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to remove organization. Please try again.`)) } for (const ownership of dmarcSummaryCheckList) { @@ -147,9 +138,7 @@ export const removeOrganization = new mutationWithClientMutationId({ console.error( `Trx step error occurred for user: ${userKey} while attempting to remove dmarc summaries while removing org: ${organization._key}, ${err}`, ) - throw new Error( - i18n._(t`Unable to remove organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to remove organization. Please try again.`)) } try { @@ -164,9 +153,7 @@ export const removeOrganization = new mutationWithClientMutationId({ console.error( `Trx step error occurred for user: ${userKey} while attempting to remove ownerships while removing org: ${organization._key}, ${err}`, ) - throw new Error( - i18n._(t`Unable to remove organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to remove organization. Please try again.`)) } } @@ -196,9 +183,7 @@ export const removeOrganization = new mutationWithClientMutationId({ console.error( `Database error occurred for user: ${userKey} while attempting to gather domain count while removing org: ${organization._key}, ${err}`, ) - throw new Error( - i18n._(t`Unable to remove organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to remove organization. Please try again.`)) } let domainInfo @@ -208,9 +193,7 @@ export const removeOrganization = new mutationWithClientMutationId({ console.error( `Cursor error occurred for user: ${userKey} while attempting to gather domain count while removing org: ${organization._key}, ${err}`, ) - throw new Error( - i18n._(t`Unable to remove organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to remove organization. Please try again.`)) } for (const domain of domainInfo) { @@ -284,9 +267,7 @@ export const removeOrganization = new mutationWithClientMutationId({ console.error( `Trx step error occurred for user: ${userKey} while attempting to remove domains while removing org: ${organization._key}, ${err}`, ) - throw new Error( - i18n._(t`Unable to remove organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to remove organization. Please try again.`)) } } } @@ -310,10 +291,9 @@ export const removeOrganization = new mutationWithClientMutationId({ } try { - await Promise.all([ - trx.step( - () => - query` + await trx.step( + () => + query` WITH affiliations, organizations, users LET userEdges = ( FOR v, e IN 1..1 OUTBOUND ${organization._id} affiliations @@ -326,23 +306,20 @@ export const removeOrganization = new mutationWithClientMutationId({ ) RETURN true `, - ), - trx.step( - () => - query` + ) + await trx.step( + () => + query` WITH organizations REMOVE ${organization._key} IN organizations OPTIONS { waitForSync: true } `, - ), - ]) + ) } catch (err) { console.error( `Trx step error occurred for user: ${userKey} while attempting to remove affiliations, and the org while removing org: ${organization._key}, ${err}`, ) - throw new Error( - i18n._(t`Unable to remove organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to remove organization. Please try again.`)) } try { @@ -351,14 +328,10 @@ export const removeOrganization = new mutationWithClientMutationId({ console.error( `Trx commit error occurred for user: ${userKey} while attempting remove of org: ${organization._key}, ${err}`, ) - throw new Error( - i18n._(t`Unable to remove organization. Please try again.`), - ) + throw new Error(i18n._(t`Unable to remove organization. Please try again.`)) } - console.info( - `User: ${userKey} successfully removed org: ${organization._key}.`, - ) + console.info(`User: ${userKey} successfully removed org: ${organization._key}.`) await logActivity({ transaction, collections, @@ -380,9 +353,7 @@ export const removeOrganization = new mutationWithClientMutationId({ return { _type: 'result', - status: i18n._( - t`Successfully removed organization: ${organization.slug}.`, - ), + status: i18n._(t`Successfully removed organization: ${organization.slug}.`), organization, } }, diff --git a/api/src/organization/mutations/verify-organization.js b/api/src/organization/mutations/verify-organization.js index 24d65f0a28..ebac063cca 100644 --- a/api/src/organization/mutations/verify-organization.js +++ b/api/src/organization/mutations/verify-organization.js @@ -16,8 +16,7 @@ export const verifyOrganization = new mutationWithClientMutationId({ outputFields: () => ({ result: { type: verifyOrganizationUnion, - description: - '`VerifyOrganizationUnion` returning either an `OrganizationResult`, or `OrganizationError` object.', + description: '`VerifyOrganizationUnion` returning either an `OrganizationResult`, or `OrganizationError` object.', resolve: (payload) => payload, }, }), @@ -102,54 +101,22 @@ export const verifyOrganization = new mutationWithClientMutationId({ `, ) } catch (err) { - console.error( - `Transaction error occurred while upserting verified org: ${orgKey}, err: ${err}`, - ) - throw new Error( - i18n._(t`Unable to verify organization. Please try again.`), - ) - } - - // Set all affiliation owner fields to false - try { - await trx.step( - () => query` - WITH affiliations, organizations, users - FOR v, e IN 1..1 OUTBOUND ${currentOrg._id} affiliations - UPSERT { _key: e._key } - INSERT { owner: false } - UPDATE { owner: false } - IN affiliations - RETURN e - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when clearing owners for org: ${orgKey}: ${err}`, - ) - throw new Error( - i18n._(t`Unable to verify organization. Please try again.`), - ) + console.error(`Transaction error occurred while upserting verified org: ${orgKey}, err: ${err}`) + throw new Error(i18n._(t`Unable to verify organization. Please try again.`)) } try { await trx.commit() } catch (err) { - console.error( - `Transaction error occurred while committing newly verified org: ${orgKey}, err: ${err}`, - ) - throw new Error( - i18n._(t`Unable to verify organization. Please try again.`), - ) + console.error(`Transaction error occurred while committing newly verified org: ${orgKey}, err: ${err}`) + throw new Error(i18n._(t`Unable to verify organization. Please try again.`)) } console.info(`User: ${userKey}, successfully verified org: ${orgKey}.`) return { _type: 'result', - status: i18n._( - t`Successfully verified organization: ${currentOrg.slug}.`, - ), + status: i18n._(t`Successfully verified organization: ${currentOrg.slug}.`), organization: currentOrg, } }, diff --git a/api/src/organization/objects/__tests__/organization-summary.test.js b/api/src/organization/objects/__tests__/organization-summary.test.js index ee4ce5a9b5..273661e993 100644 --- a/api/src/organization/objects/__tests__/organization-summary.test.js +++ b/api/src/organization/objects/__tests__/organization-summary.test.js @@ -33,6 +33,30 @@ describe('given the organization summary object', () => { expect(demoType).toHaveProperty('dmarcPhase') expect(demoType.dmarcPhase.type).toMatchObject(categorizedSummaryType) }) + it('has a webConnections field', () => { + const demoType = organizationSummaryType.getFields() + + expect(demoType).toHaveProperty('webConnections') + expect(demoType.dmarcPhase.type).toMatchObject(categorizedSummaryType) + }) + it('has a ssl field', () => { + const demoType = organizationSummaryType.getFields() + + expect(demoType).toHaveProperty('ssl') + expect(demoType.dmarcPhase.type).toMatchObject(categorizedSummaryType) + }) + it('has a spf field', () => { + const demoType = organizationSummaryType.getFields() + + expect(demoType).toHaveProperty('spf') + expect(demoType.dmarcPhase.type).toMatchObject(categorizedSummaryType) + }) + it('has a dkim field', () => { + const demoType = organizationSummaryType.getFields() + + expect(demoType).toHaveProperty('dkim') + expect(demoType.dmarcPhase.type).toMatchObject(categorizedSummaryType) + }) }) describe('field resolvers', () => { @@ -48,10 +72,7 @@ describe('given the organization summary object', () => { } const i18n = { - _: jest - .fn() - .mockReturnValueOnce('pass') - .mockReturnValueOnce('fail'), + _: jest.fn().mockReturnValueOnce('pass').mockReturnValueOnce('fail'), } expect(demoType.dmarc.resolve({ dmarc }, {}, { i18n })).toEqual({ @@ -83,10 +104,7 @@ describe('given the organization summary object', () => { } const i18n = { - _: jest - .fn() - .mockReturnValueOnce('pass') - .mockReturnValueOnce('fail'), + _: jest.fn().mockReturnValueOnce('pass').mockReturnValueOnce('fail'), } expect(demoType.dmarc.resolve({ dmarc }, {}, { i18n })).toEqual({ @@ -120,10 +138,7 @@ describe('given the organization summary object', () => { } const i18n = { - _: jest - .fn() - .mockReturnValueOnce('pass') - .mockReturnValueOnce('fail'), + _: jest.fn().mockReturnValueOnce('pass').mockReturnValueOnce('fail'), } expect(demoType.https.resolve({ https }, {}, { i18n })).toEqual({ @@ -155,10 +170,7 @@ describe('given the organization summary object', () => { } const i18n = { - _: jest - .fn() - .mockReturnValueOnce('pass') - .mockReturnValueOnce('fail'), + _: jest.fn().mockReturnValueOnce('pass').mockReturnValueOnce('fail'), } expect(demoType.https.resolve({ https }, {}, { i18n })).toEqual({ @@ -191,10 +203,7 @@ describe('given the organization summary object', () => { } const i18n = { - _: jest - .fn() - .mockReturnValueOnce('pass') - .mockReturnValueOnce('fail'), + _: jest.fn().mockReturnValueOnce('pass').mockReturnValueOnce('fail'), } expect(demoType.mail.resolve({ mail }, {}, { i18n })).toEqual({ @@ -226,10 +235,7 @@ describe('given the organization summary object', () => { } const i18n = { - _: jest - .fn() - .mockReturnValueOnce('pass') - .mockReturnValueOnce('fail'), + _: jest.fn().mockReturnValueOnce('pass').mockReturnValueOnce('fail'), } expect(demoType.mail.resolve({ mail }, {}, { i18n })).toEqual({ @@ -262,10 +268,7 @@ describe('given the organization summary object', () => { } const i18n = { - _: jest - .fn() - .mockReturnValueOnce('pass') - .mockReturnValueOnce('fail'), + _: jest.fn().mockReturnValueOnce('pass').mockReturnValueOnce('fail'), } expect(demoType.web.resolve({ web }, {}, { i18n })).toEqual({ @@ -296,10 +299,7 @@ describe('given the organization summary object', () => { } const i18n = { - _: jest - .fn() - .mockReturnValueOnce('pass') - .mockReturnValueOnce('fail'), + _: jest.fn().mockReturnValueOnce('pass').mockReturnValueOnce('fail'), } expect(demoType.web.resolve({ web }, {}, { i18n })).toEqual({ @@ -344,13 +344,7 @@ describe('given the organization summary object', () => { .mockReturnValueOnce('maintain'), } - expect( - demoType.dmarcPhase.resolve( - { dmarc_phase: dmarcPhase }, - {}, - { i18n }, - ), - ).toEqual({ + expect(demoType.dmarcPhase.resolve({ dmarc_phase: dmarcPhase }, {}, { i18n })).toEqual({ categories: [ { count: 0, @@ -406,13 +400,7 @@ describe('given the organization summary object', () => { .mockReturnValueOnce('maintain'), } - expect( - demoType.dmarcPhase.resolve( - { dmarc_phase: dmarcPhase }, - {}, - { i18n }, - ), - ).toEqual({ + expect(demoType.dmarcPhase.resolve({ dmarc_phase: dmarcPhase }, {}, { i18n })).toEqual({ categories: [ { count: 50, diff --git a/api/src/organization/objects/organization-summary.js b/api/src/organization/objects/organization-summary.js index 68e00334f6..fcc7b52a8e 100644 --- a/api/src/organization/objects/organization-summary.js +++ b/api/src/organization/objects/organization-summary.js @@ -8,8 +8,7 @@ export const organizationSummaryType = new GraphQLObjectType({ fields: () => ({ dmarc: { type: categorizedSummaryType, - description: - 'Summary based on DMARC scan results for a given organization.', + description: 'Summary based on DMARC scan results for a given organization.', resolve: ({ dmarc }, _) => { let percentPass, percentageFail if (dmarc.total <= 0) { @@ -41,8 +40,7 @@ export const organizationSummaryType = new GraphQLObjectType({ }, https: { type: categorizedSummaryType, - description: - 'Summary based on HTTPS scan results for a given organization.', + description: 'Summary based on HTTPS scan results for a given organization.', resolve: ({ https }, _) => { let percentPass, percentageFail if (https.total <= 0) { @@ -74,8 +72,7 @@ export const organizationSummaryType = new GraphQLObjectType({ }, mail: { type: categorizedSummaryType, - description: - 'Summary based on mail scan results for a given organization.', + description: 'Summary based on mail scan results for a given organization.', resolve: ({ mail }, _) => { let percentPass, percentageFail if (mail.total <= 0) { @@ -107,8 +104,7 @@ export const organizationSummaryType = new GraphQLObjectType({ }, web: { type: categorizedSummaryType, - description: - 'Summary based on web scan results for a given organization.', + description: 'Summary based on web scan results for a given organization.', resolve: ({ web }, _) => { let percentPass, percentageFail if (web.total <= 0) { @@ -142,35 +138,19 @@ export const organizationSummaryType = new GraphQLObjectType({ type: categorizedSummaryType, description: 'Summary based on DMARC phases for a given organization.', resolve: ({ dmarc_phase }, _) => { - let percentNotImplemented, - percentAsses, - percentDeploy, - percentEnforce, - percentMaintain + let percentNotImplemented, percentAssess, percentDeploy, percentEnforce, percentMaintain if (dmarc_phase.total <= 0) { percentNotImplemented = 0 - percentAsses = 0 + percentAssess = 0 percentDeploy = 0 percentEnforce = 0 percentMaintain = 0 } else { - percentNotImplemented = Number( - ((dmarc_phase.not_implemented / dmarc_phase.total) * 100).toFixed( - 1, - ), - ) - percentAsses = Number( - ((dmarc_phase.assess / dmarc_phase.total) * 100).toFixed(1), - ) - percentDeploy = Number( - ((dmarc_phase.deploy / dmarc_phase.total) * 100).toFixed(1), - ) - percentEnforce = Number( - ((dmarc_phase.enforce / dmarc_phase.total) * 100).toFixed(1), - ) - percentMaintain = Number( - ((dmarc_phase.maintain / dmarc_phase.total) * 100).toFixed(1), - ) + percentNotImplemented = Number(((dmarc_phase.not_implemented / dmarc_phase.total) * 100).toFixed(1)) + percentAssess = Number(((dmarc_phase.assess / dmarc_phase.total) * 100).toFixed(1)) + percentDeploy = Number(((dmarc_phase.deploy / dmarc_phase.total) * 100).toFixed(1)) + percentEnforce = Number(((dmarc_phase.enforce / dmarc_phase.total) * 100).toFixed(1)) + percentMaintain = Number(((dmarc_phase.maintain / dmarc_phase.total) * 100).toFixed(1)) } const categories = [ @@ -182,7 +162,7 @@ export const organizationSummaryType = new GraphQLObjectType({ { name: 'assess', count: dmarc_phase.assess, - percentage: percentAsses, + percentage: percentAssess, }, { name: 'deploy', @@ -207,5 +187,207 @@ export const organizationSummaryType = new GraphQLObjectType({ } }, }, + ssl: { + type: categorizedSummaryType, + description: 'Summary based on SSL scan results for a given organization.', + resolve: ({ ssl }, _) => { + let percentPass, percentageFail + if (ssl.total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((ssl.pass / ssl.total) * 100).toFixed(1)) + percentageFail = Number(((ssl.fail / ssl.total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: ssl.pass, + percentage: percentPass, + }, + { + name: 'fail', + count: ssl.fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total: ssl.total, + } + }, + }, + webConnections: { + type: categorizedSummaryType, + description: 'Summary based on HTTPS and HSTS scan results for a given organization.', + resolve: ({ web_connections }, _) => { + let percentPass, percentageFail + if (web_connections.total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((web_connections.pass / web_connections.total) * 100).toFixed(1)) + percentageFail = Number(((web_connections.fail / web_connections.total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: web_connections.pass, + percentage: percentPass, + }, + { + name: 'fail', + count: web_connections.fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total: web_connections.total, + } + }, + }, + spf: { + type: categorizedSummaryType, + description: 'Summary based on SPF scan results for a given organization.', + resolve: ({ spf }, _) => { + let percentPass, percentageFail + if (spf.total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((spf.pass / spf.total) * 100).toFixed(1)) + percentageFail = Number(((spf.fail / spf.total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: spf.pass, + percentage: percentPass, + }, + { + name: 'fail', + count: spf.fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total: spf.total, + } + }, + }, + dkim: { + type: categorizedSummaryType, + description: 'Summary based on DKIM scan results for a given organization.', + resolve: ({ dkim }, _) => { + let percentPass, percentageFail + if (dkim.total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((dkim.pass / dkim.total) * 100).toFixed(1)) + percentageFail = Number(((dkim.fail / dkim.total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: dkim.pass, + percentage: percentPass, + }, + { + name: 'fail', + count: dkim.fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total: dkim.total, + } + }, + }, + httpsIncludeHidden: { + type: categorizedSummaryType, + description: + 'Summary based on HTTPS scan results for a given organization that includes domains marked as hidden.', + resolve: ({ https, hidden }, _) => { + const pass = https.pass + hidden.https.pass + const fail = https.fail + hidden.https.fail + const total = https.total + hidden.https.total + + let percentPass, percentageFail + if (total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((pass / total) * 100).toFixed(1)) + percentageFail = Number(((fail / total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: pass, + percentage: percentPass, + }, + { + name: 'fail', + count: fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total, + } + }, + }, + dmarcIncludeHidden: { + type: categorizedSummaryType, + description: + 'Summary based on HTTPS scan results for a given organization that includes domains marked as hidden.', + resolve: ({ dmarc, hidden }, _) => { + const pass = dmarc.pass + hidden.dmarc.pass + const fail = dmarc.fail + hidden.dmarc.fail + const total = dmarc.total + hidden.dmarc.total + + let percentPass, percentageFail + if (total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((pass / total) * 100).toFixed(1)) + percentageFail = Number(((fail / total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: pass, + percentage: percentPass, + }, + { + name: 'fail', + count: fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total, + } + }, + }, }), }) diff --git a/api/src/organization/objects/organization.js b/api/src/organization/objects/organization.js index d97fba897a..f94031d5d0 100644 --- a/api/src/organization/objects/organization.js +++ b/api/src/organization/objects/organization.js @@ -1,11 +1,5 @@ import { t } from '@lingui/macro' -import { - GraphQLBoolean, - GraphQLInt, - GraphQLObjectType, - GraphQLString, - GraphQLList, -} from 'graphql' +import { GraphQLBoolean, GraphQLInt, GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql' import { connectionArgs, globalIdField } from 'graphql-relay' import { organizationSummaryType } from './organization-summary' @@ -15,6 +9,7 @@ import { affiliationUserOrder } from '../../affiliation/inputs' import { affiliationConnection } from '../../affiliation/objects' import { domainOrder, domainFilter } from '../../domain/inputs' import { domainConnection } from '../../domain/objects' +import { logActivity } from '../../audit-logs' export const organizationType = new GraphQLObjectType({ name: 'Organization', @@ -67,8 +62,7 @@ export const organizationType = new GraphQLObjectType({ }, summaries: { type: organizationSummaryType, - description: - 'Summaries based on scan types that are preformed on the given organizations domains.', + description: 'Summaries based on scan types that are preformed on the given organizations domains.', resolve: ({ summaries }) => summaries, }, domainCount: { @@ -83,8 +77,27 @@ export const organizationType = new GraphQLObjectType({ resolve: async ( { _id }, _args, - { loaders: { loadOrganizationDomainStatuses } }, + { + i18n, + userKey, + query, + transaction, + collections, + auth: { checkPermission, userRequired, verifiedRequired }, + loaders: { loadOrganizationDomainStatuses }, + }, ) => { + const user = await userRequired() + verifiedRequired({ user }) + + const permission = await checkPermission({ orgId: _id }) + if (!['user', 'admin', 'owner', 'super_admin'].includes(permission)) { + console.error( + `User "${userKey}" attempted to retrieve CSV output for organization "${_id}". Permission: ${permission}`, + ) + throw new Error(t`Permission Denied: Please contact organization user for help with retrieving this domain.`) + } + const domains = await loadOrganizationDomainStatuses({ orgId: _id, }) @@ -92,9 +105,10 @@ export const organizationType = new GraphQLObjectType({ 'domain', 'https', 'hsts', + 'certificates', + 'protocols', 'ciphers', 'curves', - 'protocols', 'spf', 'dkim', 'dmarc', @@ -107,6 +121,57 @@ export const organizationType = new GraphQLObjectType({ }, '') csvOutput += `\n${csvLine}` }) + + // Get org names to use in activity log + let orgNamesCursor + try { + orgNamesCursor = await query` + LET org = DOCUMENT(organizations, ${_id}) + RETURN { + "orgNameEN": org.orgDetails.en.name, + "orgNameFR": org.orgDetails.fr.name, + } + ` + } catch (err) { + console.error( + `Database error occurred when user: ${userKey} attempted to export org: ${_id}. Error while creating cursor for retrieving organization names. error: ${err}`, + ) + throw new Error(i18n._(t`Unable to export organization. Please try again.`)) + } + + let orgNames + try { + orgNames = await orgNamesCursor.next() + } catch (err) { + console.error( + `Cursor error occurred when user: ${userKey} attempted to export org: ${_id}. Error while retrieving organization names. error: ${err}`, + ) + throw new Error(i18n._(t`Unable to export organization. Please try again.`)) + } + + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'export', + target: { + resource: { + en: orgNames.orgNameEN, + fr: orgNames.orgNameFR, + }, + organization: { + id: _id, + name: orgNames.orgNameEN, + }, // name of resource being acted upon + resourceType: 'organization', + }, + }) + return csvOutput }, }, @@ -120,8 +185,7 @@ export const organizationType = new GraphQLObjectType({ }, ownership: { type: GraphQLBoolean, - description: - 'Limit domains to those that belong to an organization that has ownership.', + description: 'Limit domains to those that belong to an organization that has ownership.', }, search: { type: GraphQLString, @@ -137,10 +201,7 @@ export const organizationType = new GraphQLObjectType({ { _id }, args, - { - auth: { checkPermission }, - loaders: { loadDomainConnectionsByOrgId }, - }, + { auth: { checkPermission }, loaders: { loadDomainConnectionsByOrgId } }, ) => { // Check to see requesting users permission to the org is const permission = await checkPermission({ orgId: _id }) @@ -164,34 +225,39 @@ export const organizationType = new GraphQLObjectType({ type: GraphQLString, description: 'String used to search for affiliated users.', }, + includePending: { + type: GraphQLBoolean, + description: 'Exclude (false) or include only (true) pending affiliations in the results.', + }, ...connectionArgs, }, resolve: async ( { _id }, args, - { - i18n, - auth: { checkPermission }, - loaders: { loadAffiliationConnectionsByOrgId }, - }, + { i18n, auth: { checkPermission }, loaders: { loadAffiliationConnectionsByOrgId } }, ) => { const permission = await checkPermission({ orgId: _id }) - if (permission === 'admin' || permission === 'super_admin') { - const affiliations = await loadAffiliationConnectionsByOrgId({ - orgId: _id, - ...args, - }) - return affiliations + if (['admin', 'owner', 'super_admin'].includes(permission) === false) { + throw new Error(i18n._(t`Cannot query affiliations on organization without admin permission or higher.`)) } - throw new Error( - i18n._( - t`Cannot query affiliations on organization without admin permission or higher.`, - ), - ) + + const affiliations = await loadAffiliationConnectionsByOrgId({ + orgId: _id, + ...args, + }) + return affiliations + }, + }, + userHasPermission: { + type: GraphQLBoolean, + description: + 'Value that determines if a user is affiliated with an organization, whether through organization affiliation, verified affiliation, or through super admin status.', + resolve: async ({ _id }, _args, { auth: { checkPermission } }) => { + const permission = await checkPermission({ orgId: _id }) + return ['user', 'admin', 'super_admin', 'owner'].includes(permission) }, }, }), interfaces: [nodeInterface], - description: - 'Organization object containing information for a given Organization.', + description: 'Organization object containing information for a given Organization.', }) diff --git a/api/src/organization/queries/__tests__/get-all-organization-domain-statuses.test.js b/api/src/organization/queries/__tests__/get-all-organization-domain-statuses.test.js index 07fc951564..269a63bca8 100644 --- a/api/src/organization/queries/__tests__/get-all-organization-domain-statuses.test.js +++ b/api/src/organization/queries/__tests__/get-all-organization-domain-statuses.test.js @@ -14,18 +14,7 @@ import frenchMessages from '../../../locale/fr/messages' const { DB_PASS: rootPass, DB_URL: url } = process.env describe('given getAllOrganizationDomainStatuses', () => { - let query, - drop, - truncate, - schema, - collections, - orgOne, - orgTwo, - superAdminOrg, - domainOne, - domainTwo, - i18n, - user + let query, drop, truncate, schema, collections, orgOne, orgTwo, superAdminOrg, domainOne, domainTwo, i18n, user const consoleOutput = [] const mockedInfo = (output) => consoleOutput.push(output) @@ -198,7 +187,7 @@ describe('given getAllOrganizationDomainStatuses', () => { loginRequiredBool = false }) describe('the user is not a super admin', () => { - it('returns all domain status results', async () => { + it('returns a permission error', async () => { const response = await graphql( schema, ` @@ -229,25 +218,20 @@ describe('given getAllOrganizationDomainStatuses', () => { loginRequiredBool: loginRequiredBool, }, loaders: { - loadAllOrganizationDomainStatuses: - loadAllOrganizationDomainStatuses({ - query, - userKey: user._key, - i18n, - }), + loadAllOrganizationDomainStatuses: loadAllOrganizationDomainStatuses({ + query, + userKey: user._key, + i18n, + }), }, }, ) - const expectedResponse = { - data: { - getAllOrganizationDomainStatuses: `Organization name (English),Nom de l'organisation (Français),Domain,HTTPS,HSTS,Ciphers,Curves,Protocols,SPF,DKIM,DMARC -"Definitely Treasury Board of Canada Secretariat","Définitivement Secrétariat du Conseil du Trésor du Canada","domain.one","fail","pass","pass","pass","pass","pass","pass","pass" -"Not Treasury Board of Canada Secretariat","Ne Pas Secrétariat du Conseil Trésor du Canada","domain.two","pass","fail","fail","pass","fail","pass","pass","fail"`, - }, - } - expect(response).toEqual(expectedResponse) + const error = [ + new GraphQLError('Permissions error. You do not have sufficient permissions to access this data.'), + ] + expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ - `User ${user._key} successfully retrieved all domain statuses.`, + `User: ${user._key} attempted to load all organization statuses but login is required and they are not a super admin.`, ]) }) }) @@ -291,12 +275,11 @@ describe('given getAllOrganizationDomainStatuses', () => { loginRequiredBool: loginRequiredBool, }, loaders: { - loadAllOrganizationDomainStatuses: - loadAllOrganizationDomainStatuses({ - query, - userKey: user._key, - i18n, - }), + loadAllOrganizationDomainStatuses: loadAllOrganizationDomainStatuses({ + query, + userKey: user._key, + i18n, + }), }, }, ) @@ -310,9 +293,7 @@ describe('given getAllOrganizationDomainStatuses', () => { } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User ${user._key} successfully retrieved all domain statuses.`, - ]) + expect(consoleOutput).toEqual([`User ${user._key} successfully retrieved all domain statuses.`]) }) }) }) @@ -352,19 +333,16 @@ describe('given getAllOrganizationDomainStatuses', () => { loginRequiredBool: loginRequiredBool, }, loaders: { - loadAllOrganizationDomainStatuses: - loadAllOrganizationDomainStatuses({ - query, - userKey: user._key, - i18n, - }), + loadAllOrganizationDomainStatuses: loadAllOrganizationDomainStatuses({ + query, + userKey: user._key, + i18n, + }), }, }, ) const error = [ - new GraphQLError( - 'Permissions error. You do not have sufficient permissions to access this data.', - ), + new GraphQLError('Permissions error. You do not have sufficient permissions to access this data.'), ] expect(response.errors).toEqual(error) @@ -413,12 +391,11 @@ describe('given getAllOrganizationDomainStatuses', () => { loginRequiredBool: loginRequiredBool, }, loaders: { - loadAllOrganizationDomainStatuses: - loadAllOrganizationDomainStatuses({ - query, - userKey: user._key, - i18n, - }), + loadAllOrganizationDomainStatuses: loadAllOrganizationDomainStatuses({ + query, + userKey: user._key, + i18n, + }), }, }, ) @@ -430,9 +407,7 @@ describe('given getAllOrganizationDomainStatuses', () => { }, } expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User ${user._key} successfully retrieved all domain statuses.`, - ]) + expect(consoleOutput).toEqual([`User ${user._key} successfully retrieved all domain statuses.`]) }) }) }) diff --git a/api/src/organization/queries/find-my-organizations.js b/api/src/organization/queries/find-my-organizations.js index a468ac6125..2c4752208f 100644 --- a/api/src/organization/queries/find-my-organizations.js +++ b/api/src/organization/queries/find-my-organizations.js @@ -22,8 +22,11 @@ export const findMyOrganizations = { }, includeSuperAdminOrg: { type: GraphQLBoolean, - description: - 'Filter org list to either include or exclude the super admin org.', + description: 'Filter org list to either include or exclude the super admin org.', + }, + isVerified: { + type: GraphQLBoolean, + description: 'Filter org list to include only verified organizations.', }, ...connectionArgs, }, @@ -32,12 +35,7 @@ export const findMyOrganizations = { args, { userKey, - auth: { - checkSuperAdmin, - userRequired, - verifiedRequired, - loginRequiredBool, - }, + auth: { checkSuperAdmin, userRequired, verifiedRequired, loginRequiredBool }, loaders: { loadOrgConnectionsByUserId }, }, ) => { diff --git a/api/src/organization/queries/find-organization-by-slug.js b/api/src/organization/queries/find-organization-by-slug.js index 16ef398c82..1fde3c47c4 100644 --- a/api/src/organization/queries/find-organization-by-slug.js +++ b/api/src/organization/queries/find-organization-by-slug.js @@ -6,13 +6,11 @@ const { organizationType } = require('../objects') export const findOrganizationBySlug = { type: organizationType, - description: - 'Select all information on a selected organization that a user has access to.', + description: 'Select all information on a selected organization that a user has access to.', args: { orgSlug: { type: GraphQLNonNull(Slug), - description: - 'The slugified organization name you want to retrieve data for.', + description: 'The slugified organization name you want to retrieve data for.', }, }, resolve: async ( @@ -21,12 +19,7 @@ export const findOrganizationBySlug = { { i18n, userKey, - auth: { - checkPermission, - userRequired, - verifiedRequired, - loginRequiredBool, - }, + auth: { checkPermission, userRequired, verifiedRequired, loginRequiredBool }, loaders: { loadOrgBySlug }, validators: { cleanseInput }, }, @@ -45,42 +38,25 @@ export const findOrganizationBySlug = { if (typeof org === 'undefined') { console.warn(`User ${userKey} could not retrieve organization.`) - throw new Error( - i18n._(t`No organization with the provided slug could be found.`), - ) + throw new Error(i18n._(t`No organization with the provided slug could be found.`)) } - if (loginRequiredBool) { - // Check user permission for organization access - const permission = await checkPermission({ orgId: org._id }) + // Check user permission for organization access + const permission = await checkPermission({ orgId: org._id }) - if (!['super_admin', 'admin', 'user'].includes(permission)) { + if (loginRequiredBool) { + if (!['user', 'admin', 'owner', 'super_admin'].includes(permission)) { console.warn(`User ${userKey} could not retrieve organization.`) - throw new Error( - i18n._( - t`Permission Denied: Could not retrieve specified organization.`, - ), - ) + throw new Error(i18n._(t`Permission Denied: Could not retrieve specified organization.`)) } } else { - if (org.slugEN === 'super-admin' || org.slugFR === 'super-admin') { - // Check user permission for super-admin organization access - const permission = await checkPermission({ orgId: org._id }) - - if (!['super_admin', 'admin', 'user'].includes(permission)) { - console.warn(`User ${userKey} could not retrieve organization.`) - throw new Error( - i18n._( - t`Permission Denied: Could not retrieve specified organization.`, - ), - ) - } + if (org.verified !== true && !['user', 'admin', 'owner', 'super_admin'].includes(permission)) { + console.warn(`User ${userKey} could not retrieve organization.`) + throw new Error(i18n._(t`Permission Denied: Could not retrieve specified organization.`)) } } - console.info( - `User ${userKey} successfully retrieved organization ${org._key}.`, - ) + console.info(`User ${userKey} successfully retrieved organization ${org._key}.`) org.id = org._key return org }, diff --git a/api/src/organization/queries/get-all-organization-domain-statuses.js b/api/src/organization/queries/get-all-organization-domain-statuses.js index b4cd0691bc..61b7c45a84 100644 --- a/api/src/organization/queries/get-all-organization-domain-statuses.js +++ b/api/src/organization/queries/get-all-organization-domain-statuses.js @@ -4,39 +4,27 @@ import { t } from '@lingui/macro' export const getAllOrganizationDomainStatuses = { type: GraphQLString, - description: - 'CSV formatted output of all domains in all organizations including their email and web scan statuses.', + description: 'CSV formatted output of all domains in all organizations including their email and web scan statuses.', resolve: async ( _, _args, { userKey, i18n, - auth: { - checkSuperAdmin, - userRequired, - verifiedRequired, - loginRequiredBool, - }, + auth: { checkSuperAdmin, userRequired, verifiedRequired }, loaders: { loadAllOrganizationDomainStatuses }, }, ) => { - if (loginRequiredBool) { - const user = await userRequired() - verifiedRequired({ user }) + const user = await userRequired() + verifiedRequired({ user }) - const isSuperAdmin = await checkSuperAdmin() + const isSuperAdmin = await checkSuperAdmin() - if (!isSuperAdmin) { - console.warn( - `User: ${userKey} attempted to load all organization statuses but login is required and they are not a super admin.`, - ) - throw new Error( - i18n._( - t`Permissions error. You do not have sufficient permissions to access this data.`, - ), - ) - } + if (!isSuperAdmin) { + console.warn( + `User: ${userKey} attempted to load all organization statuses but login is required and they are not a super admin.`, + ) + throw new Error(i18n._(t`Permissions error. You do not have sufficient permissions to access this data.`)) } const domainStatuses = await loadAllOrganizationDomainStatuses() @@ -60,9 +48,7 @@ export const getAllOrganizationDomainStatuses = { ] let csvOutput = headers.join(',') domainStatuses.forEach((domainStatus) => { - const csvLine = headers - .map((header) => `"${domainStatus[header]}"`) - .join(',') + const csvLine = headers.map((header) => `"${domainStatus[header]}"`).join(',') csvOutput += `\n${csvLine}` }) diff --git a/api/src/summaries/queries/__tests__/dkim-summary.test.js b/api/src/summaries/queries/__tests__/dkim-summary.test.js new file mode 100644 index 0000000000..1865d8cbb8 --- /dev/null +++ b/api/src/summaries/queries/__tests__/dkim-summary.test.js @@ -0,0 +1,174 @@ +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { setupI18n } from '@lingui/core' + +import englishMessages from '../../../locale/en/messages' +import frenchMessages from '../../../locale/fr/messages' +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import { loadChartSummaryByKey } from '../../loaders' +import dbschema from '../../../../database.json' + +const { DB_PASS: rootPass, DB_URL: url } = process.env + +describe('given dkimSummary query', () => { + let query, drop, truncate, schema, collections, i18n + + const consoleOutput = [] + const mockedInfo = (output) => consoleOutput.push(output) + const mockedWarn = (output) => consoleOutput.push(output) + const mockedError = (output) => consoleOutput.push(output) + beforeAll(() => { + console.info = mockedInfo + console.warn = mockedWarn + console.error = mockedError + // Create GQL Schema + schema = new GraphQLSchema({ + query: createQuerySchema(), + mutation: createMutationSchema(), + }) + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + afterEach(() => { + consoleOutput.length = 0 + }) + + describe('given successful dkim summary retrieval', () => { + beforeAll(async () => { + // Generate DB Items + ;({ query, drop, truncate, collections } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + }) + beforeEach(async () => { + await collections.chartSummaries.save({ + _key: 'dkim', + total: 1000, + fail: 500, + pass: 500, + }) + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + it('returns dkim summary', async () => { + const response = await graphql( + schema, + ` + query { + dkimSummary { + total + categories { + name + count + percentage + } + } + } + `, + null, + { + i18n, + loaders: { + loadChartSummaryByKey: loadChartSummaryByKey({ query }), + }, + }, + ) + + const expectedResponse = { + data: { + dkimSummary: { + total: 1000, + categories: [ + { + name: 'pass', + count: 500, + percentage: 50, + }, + { + name: 'fail', + count: 500, + percentage: 50, + }, + ], + }, + }, + } + expect(response).toEqual(expectedResponse) + }) + }) + + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + describe('given unsuccessful dkim summary retrieval', () => { + describe('summary cannot be found', () => { + it('returns an appropriate error message', async () => { + const response = await graphql( + schema, + ` + query { + dkimSummary { + total + categories { + name + count + percentage + } + } + } + `, + null, + { + i18n, + loaders: { + loadChartSummaryByKey: { + load: jest.fn().mockReturnValue(undefined), + }, + }, + }, + ) + + const error = [new GraphQLError(`Unable to load DKIM summary. Please try again.`)] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([`User could not retrieve DKIM summary.`]) + }) + }) + }) + }) +}) diff --git a/api/src/summaries/queries/__tests__/dmarc-summary.test.js b/api/src/summaries/queries/__tests__/dmarc-summary.test.js new file mode 100644 index 0000000000..395a557675 --- /dev/null +++ b/api/src/summaries/queries/__tests__/dmarc-summary.test.js @@ -0,0 +1,174 @@ +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { setupI18n } from '@lingui/core' + +import englishMessages from '../../../locale/en/messages' +import frenchMessages from '../../../locale/fr/messages' +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import { loadChartSummaryByKey } from '../../loaders' +import dbschema from '../../../../database.json' + +const { DB_PASS: rootPass, DB_URL: url } = process.env + +describe('given dmarcSummary query', () => { + let query, drop, truncate, schema, collections, i18n + + const consoleOutput = [] + const mockedInfo = (output) => consoleOutput.push(output) + const mockedWarn = (output) => consoleOutput.push(output) + const mockedError = (output) => consoleOutput.push(output) + beforeAll(() => { + console.info = mockedInfo + console.warn = mockedWarn + console.error = mockedError + // Create GQL Schema + schema = new GraphQLSchema({ + query: createQuerySchema(), + mutation: createMutationSchema(), + }) + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + afterEach(() => { + consoleOutput.length = 0 + }) + + describe('given successful dmarc summary retrieval', () => { + beforeAll(async () => { + // Generate DB Items + ;({ query, drop, truncate, collections } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + }) + beforeEach(async () => { + await collections.chartSummaries.save({ + _key: 'dmarc', + total: 1000, + fail: 500, + pass: 500, + }) + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + it('returns dmarc summary', async () => { + const response = await graphql( + schema, + ` + query { + dmarcSummary { + total + categories { + name + count + percentage + } + } + } + `, + null, + { + i18n, + loaders: { + loadChartSummaryByKey: loadChartSummaryByKey({ query }), + }, + }, + ) + + const expectedResponse = { + data: { + dmarcSummary: { + total: 1000, + categories: [ + { + name: 'pass', + count: 500, + percentage: 50, + }, + { + name: 'fail', + count: 500, + percentage: 50, + }, + ], + }, + }, + } + expect(response).toEqual(expectedResponse) + }) + }) + + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + describe('given unsuccessful dmarc summary retrieval', () => { + describe('summary cannot be found', () => { + it('returns an appropriate error message', async () => { + const response = await graphql( + schema, + ` + query { + dmarcSummary { + total + categories { + name + count + percentage + } + } + } + `, + null, + { + i18n, + loaders: { + loadChartSummaryByKey: { + load: jest.fn().mockReturnValue(undefined), + }, + }, + }, + ) + + const error = [new GraphQLError(`Unable to load DMARC summary. Please try again.`)] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([`User could not retrieve DMARC summary.`]) + }) + }) + }) + }) +}) diff --git a/api/src/summaries/queries/__tests__/spf-summary.test.js b/api/src/summaries/queries/__tests__/spf-summary.test.js new file mode 100644 index 0000000000..74b12f6753 --- /dev/null +++ b/api/src/summaries/queries/__tests__/spf-summary.test.js @@ -0,0 +1,174 @@ +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { setupI18n } from '@lingui/core' + +import englishMessages from '../../../locale/en/messages' +import frenchMessages from '../../../locale/fr/messages' +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import { loadChartSummaryByKey } from '../../loaders' +import dbschema from '../../../../database.json' + +const { DB_PASS: rootPass, DB_URL: url } = process.env + +describe('given spfSummary query', () => { + let query, drop, truncate, schema, collections, i18n + + const consoleOutput = [] + const mockedInfo = (output) => consoleOutput.push(output) + const mockedWarn = (output) => consoleOutput.push(output) + const mockedError = (output) => consoleOutput.push(output) + beforeAll(() => { + console.info = mockedInfo + console.warn = mockedWarn + console.error = mockedError + // Create GQL Schema + schema = new GraphQLSchema({ + query: createQuerySchema(), + mutation: createMutationSchema(), + }) + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + afterEach(() => { + consoleOutput.length = 0 + }) + + describe('given successful spf summary retrieval', () => { + beforeAll(async () => { + // Generate DB Items + ;({ query, drop, truncate, collections } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + }) + beforeEach(async () => { + await collections.chartSummaries.save({ + _key: 'spf', + total: 1000, + fail: 500, + pass: 500, + }) + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + it('returns spf summary', async () => { + const response = await graphql( + schema, + ` + query { + spfSummary { + total + categories { + name + count + percentage + } + } + } + `, + null, + { + i18n, + loaders: { + loadChartSummaryByKey: loadChartSummaryByKey({ query }), + }, + }, + ) + + const expectedResponse = { + data: { + spfSummary: { + total: 1000, + categories: [ + { + name: 'pass', + count: 500, + percentage: 50, + }, + { + name: 'fail', + count: 500, + percentage: 50, + }, + ], + }, + }, + } + expect(response).toEqual(expectedResponse) + }) + }) + + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + describe('given unsuccessful spf summary retrieval', () => { + describe('summary cannot be found', () => { + it('returns an appropriate error message', async () => { + const response = await graphql( + schema, + ` + query { + spfSummary { + total + categories { + name + count + percentage + } + } + } + `, + null, + { + i18n, + loaders: { + loadChartSummaryByKey: { + load: jest.fn().mockReturnValue(undefined), + }, + }, + }, + ) + + const error = [new GraphQLError(`Unable to load SPF summary. Please try again.`)] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([`User could not retrieve SPF summary.`]) + }) + }) + }) + }) +}) diff --git a/api/src/summaries/queries/__tests__/ssl-summary.test.js b/api/src/summaries/queries/__tests__/ssl-summary.test.js new file mode 100644 index 0000000000..178c5d2a2e --- /dev/null +++ b/api/src/summaries/queries/__tests__/ssl-summary.test.js @@ -0,0 +1,174 @@ +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { setupI18n } from '@lingui/core' + +import englishMessages from '../../../locale/en/messages' +import frenchMessages from '../../../locale/fr/messages' +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import { loadChartSummaryByKey } from '../../loaders' +import dbschema from '../../../../database.json' + +const { DB_PASS: rootPass, DB_URL: url } = process.env + +describe('given sslSummary query', () => { + let query, drop, truncate, schema, collections, i18n + + const consoleOutput = [] + const mockedInfo = (output) => consoleOutput.push(output) + const mockedWarn = (output) => consoleOutput.push(output) + const mockedError = (output) => consoleOutput.push(output) + beforeAll(() => { + console.info = mockedInfo + console.warn = mockedWarn + console.error = mockedError + // Create GQL Schema + schema = new GraphQLSchema({ + query: createQuerySchema(), + mutation: createMutationSchema(), + }) + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + afterEach(() => { + consoleOutput.length = 0 + }) + + describe('given successful ssl summary retrieval', () => { + beforeAll(async () => { + // Generate DB Items + ;({ query, drop, truncate, collections } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + }) + beforeEach(async () => { + await collections.chartSummaries.save({ + _key: 'ssl', + total: 1000, + fail: 500, + pass: 500, + }) + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + it('returns ssl summary', async () => { + const response = await graphql( + schema, + ` + query { + sslSummary { + total + categories { + name + count + percentage + } + } + } + `, + null, + { + i18n, + loaders: { + loadChartSummaryByKey: loadChartSummaryByKey({ query }), + }, + }, + ) + + const expectedResponse = { + data: { + sslSummary: { + total: 1000, + categories: [ + { + name: 'pass', + count: 500, + percentage: 50, + }, + { + name: 'fail', + count: 500, + percentage: 50, + }, + ], + }, + }, + } + expect(response).toEqual(expectedResponse) + }) + }) + + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + describe('given unsuccessful ssl summary retrieval', () => { + describe('summary cannot be found', () => { + it('returns an appropriate error message', async () => { + const response = await graphql( + schema, + ` + query { + sslSummary { + total + categories { + name + count + percentage + } + } + } + `, + null, + { + i18n, + loaders: { + loadChartSummaryByKey: { + load: jest.fn().mockReturnValue(undefined), + }, + }, + }, + ) + + const error = [new GraphQLError(`Unable to load SSL summary. Please try again.`)] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([`User could not retrieve SSL summary.`]) + }) + }) + }) + }) +}) diff --git a/api/src/summaries/queries/__tests__/web-connections-summary.test.js b/api/src/summaries/queries/__tests__/web-connections-summary.test.js new file mode 100644 index 0000000000..e216a8c19a --- /dev/null +++ b/api/src/summaries/queries/__tests__/web-connections-summary.test.js @@ -0,0 +1,174 @@ +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { setupI18n } from '@lingui/core' + +import englishMessages from '../../../locale/en/messages' +import frenchMessages from '../../../locale/fr/messages' +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import { loadChartSummaryByKey } from '../../loaders' +import dbschema from '../../../../database.json' + +const { DB_PASS: rootPass, DB_URL: url } = process.env + +describe('given webConnectionsSummary query', () => { + let query, drop, truncate, schema, collections, i18n + + const consoleOutput = [] + const mockedInfo = (output) => consoleOutput.push(output) + const mockedWarn = (output) => consoleOutput.push(output) + const mockedError = (output) => consoleOutput.push(output) + beforeAll(() => { + console.info = mockedInfo + console.warn = mockedWarn + console.error = mockedError + // Create GQL Schema + schema = new GraphQLSchema({ + query: createQuerySchema(), + mutation: createMutationSchema(), + }) + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + afterEach(() => { + consoleOutput.length = 0 + }) + + describe('given successful webConnections summary retrieval', () => { + beforeAll(async () => { + // Generate DB Items + ;({ query, drop, truncate, collections } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + }) + beforeEach(async () => { + await collections.chartSummaries.save({ + _key: 'web_connections', + total: 1000, + fail: 500, + pass: 500, + }) + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + it('returns webConnections summary', async () => { + const response = await graphql( + schema, + ` + query { + webConnectionsSummary { + total + categories { + name + count + percentage + } + } + } + `, + null, + { + i18n, + loaders: { + loadChartSummaryByKey: loadChartSummaryByKey({ query }), + }, + }, + ) + + const expectedResponse = { + data: { + webConnectionsSummary: { + total: 1000, + categories: [ + { + name: 'pass', + count: 500, + percentage: 50, + }, + { + name: 'fail', + count: 500, + percentage: 50, + }, + ], + }, + }, + } + expect(response).toEqual(expectedResponse) + }) + }) + + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + describe('given unsuccessful webConnections summary retrieval', () => { + describe('summary cannot be found', () => { + it('returns an appropriate error message', async () => { + const response = await graphql( + schema, + ` + query { + webConnectionsSummary { + total + categories { + name + count + percentage + } + } + } + `, + null, + { + i18n, + loaders: { + loadChartSummaryByKey: { + load: jest.fn().mockReturnValue(undefined), + }, + }, + }, + ) + + const error = [new GraphQLError(`Unable to load web connections summary. Please try again.`)] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([`User could not retrieve web connections summary.`]) + }) + }) + }) + }) +}) diff --git a/api/src/summaries/queries/dkim-summary.js b/api/src/summaries/queries/dkim-summary.js new file mode 100644 index 0000000000..9a91978ce4 --- /dev/null +++ b/api/src/summaries/queries/dkim-summary.js @@ -0,0 +1,33 @@ +import { categorizedSummaryType } from '../objects' +import { t } from '@lingui/macro' + +export const dkimSummary = { + type: categorizedSummaryType, + description: 'DKIM summary computed values, used to build summary cards.', + resolve: async (_, __, { i18n, loaders: { loadChartSummaryByKey } }) => { + const summary = await loadChartSummaryByKey.load('dkim') + + if (typeof summary === 'undefined') { + console.warn(`User could not retrieve DKIM summary.`) + throw new Error(i18n._(t`Unable to load DKIM summary. Please try again.`)) + } + + const categories = [ + { + name: 'pass', + count: summary.pass, + percentage: Number(((summary.pass / summary.total) * 100).toFixed(1)), + }, + { + name: 'fail', + count: summary.fail, + percentage: Number(((summary.fail / summary.total) * 100).toFixed(1)), + }, + ] + + return { + categories, + total: summary.total, + } + }, +} diff --git a/api/src/summaries/queries/dmarc-summary.js b/api/src/summaries/queries/dmarc-summary.js new file mode 100644 index 0000000000..e7ca64b3c8 --- /dev/null +++ b/api/src/summaries/queries/dmarc-summary.js @@ -0,0 +1,33 @@ +import { categorizedSummaryType } from '../objects' +import { t } from '@lingui/macro' + +export const dmarcSummary = { + type: categorizedSummaryType, + description: 'DMARC summary computed values, used to build summary cards.', + resolve: async (_, __, { i18n, loaders: { loadChartSummaryByKey } }) => { + const summary = await loadChartSummaryByKey.load('dmarc') + + if (typeof summary === 'undefined') { + console.warn(`User could not retrieve DMARC summary.`) + throw new Error(i18n._(t`Unable to load DMARC summary. Please try again.`)) + } + + const categories = [ + { + name: 'pass', + count: summary.pass, + percentage: Number(((summary.pass / summary.total) * 100).toFixed(1)), + }, + { + name: 'fail', + count: summary.fail, + percentage: Number(((summary.fail / summary.total) * 100).toFixed(1)), + }, + ] + + return { + categories, + total: summary.total, + } + }, +} diff --git a/api/src/summaries/queries/index.js b/api/src/summaries/queries/index.js index 6b51658f0f..c4a8068b9a 100644 --- a/api/src/summaries/queries/index.js +++ b/api/src/summaries/queries/index.js @@ -1,4 +1,9 @@ -export * from './mail-summary' -export * from './web-summary' +export * from './dkim-summary' export * from './dmarc-phase-summary' +export * from './dmarc-summary' export * from './https-summary' +export * from './mail-summary' +export * from './spf-summary' +export * from './ssl-summary' +export * from './web-connections-summary' +export * from './web-summary' diff --git a/api/src/summaries/queries/spf-summary.js b/api/src/summaries/queries/spf-summary.js new file mode 100644 index 0000000000..c320ff40ff --- /dev/null +++ b/api/src/summaries/queries/spf-summary.js @@ -0,0 +1,33 @@ +import { categorizedSummaryType } from '../objects' +import { t } from '@lingui/macro' + +export const spfSummary = { + type: categorizedSummaryType, + description: 'SPF summary computed values, used to build summary cards.', + resolve: async (_, __, { i18n, loaders: { loadChartSummaryByKey } }) => { + const summary = await loadChartSummaryByKey.load('spf') + + if (typeof summary === 'undefined') { + console.warn(`User could not retrieve SPF summary.`) + throw new Error(i18n._(t`Unable to load SPF summary. Please try again.`)) + } + + const categories = [ + { + name: 'pass', + count: summary.pass, + percentage: Number(((summary.pass / summary.total) * 100).toFixed(1)), + }, + { + name: 'fail', + count: summary.fail, + percentage: Number(((summary.fail / summary.total) * 100).toFixed(1)), + }, + ] + + return { + categories, + total: summary.total, + } + }, +} diff --git a/api/src/summaries/queries/ssl-summary.js b/api/src/summaries/queries/ssl-summary.js new file mode 100644 index 0000000000..ceafecefde --- /dev/null +++ b/api/src/summaries/queries/ssl-summary.js @@ -0,0 +1,33 @@ +import { categorizedSummaryType } from '../objects' +import { t } from '@lingui/macro' + +export const sslSummary = { + type: categorizedSummaryType, + description: 'SSL summary computed values, used to build summary cards.', + resolve: async (_, __, { i18n, loaders: { loadChartSummaryByKey } }) => { + const summary = await loadChartSummaryByKey.load('ssl') + + if (typeof summary === 'undefined') { + console.warn(`User could not retrieve SSL summary.`) + throw new Error(i18n._(t`Unable to load SSL summary. Please try again.`)) + } + + const categories = [ + { + name: 'pass', + count: summary.pass, + percentage: Number(((summary.pass / summary.total) * 100).toFixed(1)), + }, + { + name: 'fail', + count: summary.fail, + percentage: Number(((summary.fail / summary.total) * 100).toFixed(1)), + }, + ] + + return { + categories, + total: summary.total, + } + }, +} diff --git a/api/src/summaries/queries/web-connections-summary.js b/api/src/summaries/queries/web-connections-summary.js new file mode 100644 index 0000000000..a25acc7d38 --- /dev/null +++ b/api/src/summaries/queries/web-connections-summary.js @@ -0,0 +1,33 @@ +import { categorizedSummaryType } from '../objects' +import { t } from '@lingui/macro' + +export const webConnectionsSummary = { + type: categorizedSummaryType, + description: 'Web connections (HTTPS + HSTS) summary computed values, used to build summary cards.', + resolve: async (_, __, { i18n, loaders: { loadChartSummaryByKey } }) => { + const summary = await loadChartSummaryByKey.load('web_connections') + + if (typeof summary === 'undefined') { + console.warn(`User could not retrieve web connections summary.`) + throw new Error(i18n._(t`Unable to load web connections summary. Please try again.`)) + } + + const categories = [ + { + name: 'pass', + count: summary.pass, + percentage: Number(((summary.pass / summary.total) * 100).toFixed(1)), + }, + { + name: 'fail', + count: summary.fail, + percentage: Number(((summary.fail / summary.total) * 100).toFixed(1)), + }, + ] + + return { + categories, + total: summary.total, + } + }, +} diff --git a/api/src/user/loaders/load-my-tracker-by-user-id.js b/api/src/user/loaders/load-my-tracker-by-user-id.js index a88d42b57b..559bdf601d 100644 --- a/api/src/user/loaders/load-my-tracker-by-user-id.js +++ b/api/src/user/loaders/load-my-tracker-by-user-id.js @@ -21,7 +21,8 @@ export const loadMyTrackerByUserId = id: domain._key, _type: "domain", "phase": domain.phase, - "httpsStatus": domain.status.https + "https": domain.status.https, + "dmarc": domain.status.dmarc } ) RETURN { "domains": favDomains } @@ -49,6 +50,11 @@ export const loadMyTrackerByUserId = fail: 0, total: 0, }, + dmarc: { + pass: 0, + fail: 0, + total: 0, + }, dmarc_phase: { not_implemented: 0, assess: 0, @@ -59,16 +65,21 @@ export const loadMyTrackerByUserId = }, } - domainsInfo.domains.forEach(({ phase, httpsStatus }) => { + domainsInfo.domains.forEach(({ phase, https, dmarc }) => { // calculate https summary - if (httpsStatus === 'pass') { + if (https === 'pass') { returnSummaries.https.pass++ returnSummaries.https.total++ - } else if (httpsStatus === 'fail') { + } else if (https === 'fail') { returnSummaries.https.fail++ returnSummaries.https.total++ } + // calculate DMARC summary + if (dmarc === 'pass') returnSummaries.dmarc.pass++ + else if (dmarc === 'fail') returnSummaries.dmarc.fail++ + returnSummaries.dmarc.total++ + // calculate dmarcPhase summary if (phase === 'not implemented') returnSummaries.dmarc_phase.not_implemented++ else if (phase === 'assess') returnSummaries.dmarc_phase.assess++ diff --git a/api/src/user/mutations/__tests__/close-account.test.js b/api/src/user/mutations/__tests__/close-account.test.js index b23a448cfb..be1356e126 100644 --- a/api/src/user/mutations/__tests__/close-account.test.js +++ b/api/src/user/mutations/__tests__/close-account.test.js @@ -1,32 +1,23 @@ -import {setupI18n} from '@lingui/core' -import {ensure, dbNameFromFile} from 'arango-tools' -import {graphql, GraphQLSchema, GraphQLError} from 'graphql' -import {toGlobalId} from 'graphql-relay' +import { setupI18n } from '@lingui/core' +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' -import {checkSuperAdmin, userRequired} from '../../../auth' -import {loadOrgByKey} from '../../../organization/loaders' -import {loadUserByKey} from '../../../user/loaders' -import {cleanseInput} from '../../../validators' -import {createMutationSchema} from '../../../mutation' -import {createQuerySchema} from '../../../query' +import { checkSuperAdmin, userRequired } from '../../../auth' +import { loadOrgByKey } from '../../../organization/loaders' +import { loadUserByKey } from '../../../user/loaders' +import { cleanseInput } from '../../../validators' +import { createMutationSchema } from '../../../mutation' +import { createQuerySchema } from '../../../query' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' -const {DB_PASS: rootPass, DB_URL: url} = process.env +const { DB_PASS: rootPass, DB_URL: url } = process.env describe('given the closeAccount mutation', () => { - let i18n, - query, - drop, - truncate, - schema, - collections, - transaction, - user, - org, - domain + let i18n, query, drop, truncate, schema, collections, transaction, user, org, domain const consoleOutput = [] const mockedConsole = (output) => consoleOutput.push(output) @@ -41,8 +32,8 @@ describe('given the closeAccount mutation', () => { i18n = setupI18n({ locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -57,7 +48,7 @@ describe('given the closeAccount mutation', () => { describe('given a successful closing of an account', () => { beforeEach(async () => { - ;({query, drop, truncate, collections, transaction} = await ensure({ + ;({ query, drop, truncate, collections, transaction } = await ensure({ variables: { dbname: dbNameFromFile(__filename), username: 'root', @@ -140,3833 +131,580 @@ describe('given the closeAccount mutation', () => { _to: dmarcSummary._id, }) }) - describe('user is an org owner', () => { - beforeEach(async () => { - await collections.affiliations.save({ - _from: org._id, - _to: user._id, - permission: 'admin', - owner: true, - }) + beforeEach(async () => { + await collections.affiliations.save({ + _from: org._id, + _to: user._id, + permission: 'user', }) - describe('org is owner of a domain', () => { - beforeEach(async () => { - await collections.ownership.save({ - _from: org._id, - _to: domain._id, - }) - }) - it('removes dmarc summary data', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } + }) + it('removes the users affiliations', async () => { + await graphql( + schema, + ` + mutation { + closeAccount(input: {}) { + result { + ... on CloseAccountResult { + status + } + ... on CloseAccountError { + code + description } } - `, - null, - { + } + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkSuperAdmin: checkSuperAdmin({ i18n, + userKey: user._key, query, - collections: collectionNames, - transaction, + }), + userRequired: userRequired({ + i18n, userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` + loadUserByKey: loadUserByKey({ + query, + userKey: user._key, + i18n, + }), + }), + }, + loaders: { + loadOrgByKey: loadOrgByKey({ + query, + language: 'en', + i18n, + userKey: user._key, + }), + }, + validators: { cleanseInput }, + }, + ) - const testDmarcSummaryCursor = - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - const testDmarcSummary = await testDmarcSummaryCursor.next() - expect(testDmarcSummary).toEqual(undefined) + await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - const testDomainsToDmarcSumCursor = - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - const testDomainsToDmarcSum = - await testDomainsToDmarcSumCursor.next() - expect(testDomainsToDmarcSum).toEqual(undefined) + const testAffiliationCursor = await query` + FOR aff IN affiliations + OPTIONS { waitForSync: true } + RETURN aff + ` + const testAffiliation = await testAffiliationCursor.next() + expect(testAffiliation).toEqual(undefined) + }) + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, }) - it('removes ownership', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } + }) + it('returns a status message', async () => { + const response = await graphql( + schema, + ` + mutation { + closeAccount(input: {}) { + result { + ... on CloseAccountResult { + status + } + ... on CloseAccountError { + code + description } } } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkSuperAdmin: checkSuperAdmin({ + i18n, + userKey: user._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: user._key, + loadUserByKey: loadUserByKey({ query, - }), - userRequired: userRequired({ - i18n, userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', i18n, - userKey: user._key, }), - }, - validators: {cleanseInput}, + }), }, - ) + loaders: { + loadOrgByKey: loadOrgByKey({ + query, + language: 'en', + i18n, + userKey: user._key, + }), + }, + validators: { cleanseInput }, + }, + ) - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` + const expectedResponse = { + data: { + closeAccount: { + result: { + status: 'Successfully closed account.', + }, + }, + }, + } - const testOwnershipCursor = - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - const testOwnership = await testOwnershipCursor.next() - expect(testOwnership).toEqual(undefined) - }) + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([`User: ${user._key} successfully closed user: ${user._id} account.`]) }) - describe('org is not owner of a domain', () => { - beforeEach(async () => { - await collections.ownership.save({ - _from: 'organizations/1', - _to: domain._id, - }) + }) + describe('users language is set to french', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'fr', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, }) - it('does not remove dmarc summary data', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } + }) + it('returns a status message', async () => { + const response = await graphql( + schema, + ` + mutation { + closeAccount(input: {}) { + result { + ... on CloseAccountResult { + status + } + ... on CloseAccountError { + code + description } } } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - - const testDmarcSummaryCursor = - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - const testDmarcSummary = await testDmarcSummaryCursor.next() - expect(testDmarcSummary).toBeDefined() - - const testDomainsToDmarcSumCursor = - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - const testDomainsToDmarcSum = - await testDomainsToDmarcSumCursor.next() - expect(testDomainsToDmarcSum).toBeDefined() - }) - it('does not remove ownership', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkSuperAdmin: checkSuperAdmin({ + i18n, + userKey: user._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: user._key, + loadUserByKey: loadUserByKey({ query, - }), - userRequired: userRequired({ - i18n, userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', i18n, - userKey: user._key, }), - }, - validators: {cleanseInput}, + }), }, - ) + loaders: { + loadOrgByKey: loadOrgByKey({ + query, + language: 'en', + i18n, + userKey: user._key, + }), + }, + validators: { cleanseInput }, + }, + ) - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` + const expectedResponse = { + data: { + closeAccount: { + result: { + status: 'Le compte a été fermé avec succès.', + }, + }, + }, + } - const testOwnershipCursor = - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - const testOwnership = await testOwnershipCursor.next() - expect(testOwnership).toBeDefined() - }) + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([`User: ${user._key} successfully closed user: ${user._id} account.`]) }) - describe('org is the only one claiming a domain', () => { - it('removes web scan result data', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } + }) + it('closes the users account', async () => { + await graphql( + schema, + ` + mutation { + closeAccount(input: {}) { + result { + ... on CloseAccountResult { + status + } + ... on CloseAccountError { + code + description } } - `, - null, - { + } + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkSuperAdmin: checkSuperAdmin({ i18n, - query, - collections: collectionNames, - transaction, userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const testWebScanCursor = - await query`FOR wScan IN webScan OPTIONS { waitForSync: true } RETURN wScan` - const testWebScan = await testWebScanCursor.next() - expect(testWebScan).toEqual(undefined) - - }) - it('removes scan data', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { + query, + }), + userRequired: userRequired({ i18n, + userKey: user._key, + loadUserByKey: loadUserByKey({ + query, + userKey: user._key, + i18n, + }), + }), + }, + loaders: { + loadOrgByKey: loadOrgByKey({ query, - collections: collectionNames, - transaction, + language: 'en', + i18n, userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR dnsResult IN dns OPTIONS { waitForSync: true } RETURN dnsResult` - await query`FOR webResult IN web OPTIONS { waitForSync: true } RETURN webResult` - - const testDNSCursor = - await query`FOR dnsResult IN dns OPTIONS { waitForSync: true } RETURN dnsResult` - const testDNS = await testDNSCursor.next() - expect(testDNS).toEqual(undefined) + }), + }, + validators: { cleanseInput }, + }, + ) - const testWebCursor = - await query`FOR webResult IN web OPTIONS { waitForSync: true } RETURN webResult` - const testWeb = await testWebCursor.next() - expect(testWeb).toEqual(undefined) + await query`FOR user IN users OPTIONS { waitForSync: true } RETURN user` - }) - it('removes claims', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR claim IN claims OPTIONS { waitForSync: true } RETURN claim` - - const testClaimCursor = - await query`FOR claim IN claims OPTIONS { waitForSync: true } RETURN claim` - const testOrg = await testClaimCursor.next() - expect(testOrg).toEqual(undefined) - }) - it('removes domain', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - - const testDomainCursor = - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - const testDomain = await testDomainCursor.next() - expect(testDomain).toEqual(undefined) - }) - }) - describe('multiple orgs claim the domain', () => { - let org2 - beforeEach(async () => { - org2 = await collections.organizations.save({ - orgDetails: { - en: { - slug: 'treasury-board-secretariat-2', - acronym: 'TBS', - name: 'Treasury Board of Canada Secretariat', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - fr: { - slug: 'secretariat-conseil-tresor-2', - acronym: 'SCT', - name: 'Secrétariat du Conseil Trésor du Canada', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - }, - }) - await collections.claims.save({ - _from: org2._id, - _to: domain._id, - }) - }) - it('does not remove the domain', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - - const testDomainCursor = - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - const testDomain = await testDomainCursor.next() - expect(testDomain).toBeDefined() - }) - it('removes the claim', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR claim IN claims OPTIONS { waitForSync: true } RETURN claim` - - const testClaimCursor = await query` - FOR claim IN claims - OPTIONS { waitForSync: true } - FILTER claim._from == ${org._id} - RETURN claim - ` - const testClaim = await testClaimCursor.next() - expect(testClaim).toEqual(undefined) - }) - }) - it('removes affiliated users and org', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - await query`FOR org IN organizations OPTIONS { waitForSync: true } RETURN org` - - const testAffiliationCursor = await query` - FOR aff IN affiliations - OPTIONS { waitForSync: true } - RETURN aff - ` - const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toEqual(undefined) - - const testOrgCursor = await query` - FOR org IN organizations - OPTIONS { waitForSync: true } - RETURN org - ` - const testOrg = await testOrgCursor.next() - expect(testOrg).toEqual(undefined) - }) - describe('user belongs to multiple orgs', () => { - let org2 - beforeEach(async () => { - org2 = await collections.organizations.save({ - orgDetails: { - en: { - slug: 'treasury-board-secretariat-2', - acronym: 'TBS', - name: 'Treasury Board of Canada Secretariat', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - fr: { - slug: 'secretariat-conseil-tresor-2', - acronym: 'SCT', - name: 'Secrétariat du Conseil Trésor du Canada', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - }, - }) - await collections.affiliations.save({ - _from: org2._id, - _to: user._id, - permission: 'user', - owner: false, - }) - }) - it('removes requesting users remaining affiliations', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - - const testAffiliationCursor = await query` - FOR aff IN affiliations - OPTIONS { waitForSync: true } - RETURN aff - ` - const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toEqual(undefined) - }) - }) - }) - describe('user is not an org owner', () => { - beforeEach(async () => { - await collections.affiliations.save({ - _from: org._id, - _to: user._id, - permission: 'user', - owner: false, - }) - }) - it('removes the users affiliations', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - - const testAffiliationCursor = await query` - FOR aff IN affiliations - OPTIONS { waitForSync: true } - RETURN aff - ` - const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toEqual(undefined) - }) - }) - describe('users language is set to english', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'en', - localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - it('returns a status message', async () => { - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResponse = { - data: { - closeAccount: { - result: { - status: 'Successfully closed account.', - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully closed user: ${user._id} account.`, - ]) - }) - }) - describe('users language is set to french', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'fr', - localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - it('returns a status message', async () => { - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResponse = { - data: { - closeAccount: { - result: { - status: 'Le compte a été fermé avec succès.', - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully closed user: ${user._id} account.`, - ]) - }) - }) - it('closes the users account', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR user IN users OPTIONS { waitForSync: true } RETURN user` - - const testUserCursor = - await query`FOR user IN users OPTIONS { waitForSync: true } RETURN user` - const testUser = await testUserCursor.next() - expect(testUser).toEqual(undefined) - }) - }) - describe('super admin is closing another users account', () => { - let superAdmin, superAdminOrg - beforeEach(async () => { - superAdmin = await collections.users.save({ - userName: 'super.admin@istio.actually.exists', - emailValidated: true, - }) - superAdminOrg = await collections.organizations.save({ - orgDetails: { - en: { - slug: 'super-admin', - acronym: 'SA', - name: 'Super Admin', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - fr: { - slug: 'super-admin', - acronym: 'SA', - name: 'Super Admin', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - }, - }) - await collections.affiliations.save({ - _from: superAdminOrg._id, - _to: superAdmin._id, - permission: 'super_admin', - owner: false, - }) - user = await collections.users.save({ - userName: 'test.account@istio.actually.exists', - emailValidated: true, - }) - org = await collections.organizations.save({ - orgDetails: { - en: { - slug: 'treasury-board-secretariat', - acronym: 'TBS', - name: 'Treasury Board of Canada Secretariat', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - fr: { - slug: 'secretariat-conseil-tresor', - acronym: 'SCT', - name: 'Secrétariat du Conseil Trésor du Canada', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - }, - }) - domain = await collections.domains.save({ - domain: 'test.gc.ca', - slug: 'test-gc-ca', - }) - await collections.claims.save({ - _from: org._id, - _to: domain._id, - }) - - const dns = await collections.dns.save({ dns: true }) - await collections.domainsDNS.save({ - _from: domain._id, - _to: dns._id, - }) - - const web = await collections.web.save({ web: true }) - await collections.domainsWeb.save({ - _from: domain._id, - _to: web._id, - }) - - const webScan = await collections.webScan.save({ - webScan: true, - }) - await collections.webToWebScans.save({ - _from: web._id, - _to: webScan._id, - }) - - const dmarcSummary = await collections.dmarcSummaries.save({ - dmarcSummary: true, - }) - await collections.domainsToDmarcSummaries.save({ - _from: domain._id, - _to: dmarcSummary._id, - }) - }) - describe('user is an org owner', () => { - beforeEach(async () => { - await collections.affiliations.save({ - _from: org._id, - _to: user._id, - permission: 'admin', - owner: true, - }) - }) - describe('org is owner of a domain', () => { - beforeEach(async () => { - await collections.ownership.save({ - _from: org._id, - _to: domain._id, - }) - }) - it('removes dmarc summary data', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', user._key)}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: superAdmin._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: superAdmin._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: superAdmin._key, - loadUserByKey: loadUserByKey({ - query, - userKey: superAdmin._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: superAdmin._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - - const testDmarcSummaryCursor = - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - const testDmarcSummary = await testDmarcSummaryCursor.next() - expect(testDmarcSummary).toEqual(undefined) - - const testDomainsToDmarcSumCursor = - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - const testDomainsToDmarcSum = - await testDomainsToDmarcSumCursor.next() - expect(testDomainsToDmarcSum).toEqual(undefined) - }) - it('removes ownership', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', user._key)}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: superAdmin._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: superAdmin._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: superAdmin._key, - loadUserByKey: loadUserByKey({ - query, - userKey: superAdmin._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: superAdmin._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - - const testOwnershipCursor = - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - const testOwnership = await testOwnershipCursor.next() - expect(testOwnership).toEqual(undefined) - }) - }) - describe('org is not owner of a domain', () => { - beforeEach(async () => { - await collections.ownership.save({ - _from: 'organizations/1', - _to: domain._id, - }) - }) - it('does not remove dmarc summary data', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', user._key)}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: superAdmin._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: superAdmin._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: superAdmin._key, - loadUserByKey: loadUserByKey({ - query, - userKey: superAdmin._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: superAdmin._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - - const testDmarcSummaryCursor = - await query`FOR dmarcSum IN dmarcSummaries OPTIONS { waitForSync: true } RETURN dmarcSum` - const testDmarcSummary = await testDmarcSummaryCursor.next() - expect(testDmarcSummary).toBeDefined() - - const testDomainsToDmarcSumCursor = - await query`FOR item IN domainsToDmarcSummaries OPTIONS { waitForSync: true } RETURN item` - const testDomainsToDmarcSum = - await testDomainsToDmarcSumCursor.next() - expect(testDomainsToDmarcSum).toBeDefined() - }) - it('does not remove ownership', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - - const testOwnershipCursor = - await query`FOR owner IN ownership OPTIONS { waitForSync: true } RETURN owner` - const testOwnership = await testOwnershipCursor.next() - expect(testOwnership).toBeDefined() - }) - }) - describe('org is the only one claiming a domain', () => { - it('removes web scan result data', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', user._key)}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: superAdmin._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: superAdmin._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: superAdmin._key, - loadUserByKey: loadUserByKey({ - query, - userKey: superAdmin._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: superAdmin._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - const testWebScanCursor = - await query`FOR wScan IN webScan OPTIONS { waitForSync: true } RETURN wScan` - const testWebScan = await testWebScanCursor.next() - expect(testWebScan).toEqual(undefined) - - }) - it('removes scan data', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', user._key)}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: superAdmin._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: superAdmin._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: superAdmin._key, - loadUserByKey: loadUserByKey({ - query, - userKey: superAdmin._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: superAdmin._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR dnsResult IN dns OPTIONS { waitForSync: true } RETURN dnsResult` - await query`FOR webResult IN web OPTIONS { waitForSync: true } RETURN webResult` - - const testDNSCursor = - await query`FOR dnsResult IN dns OPTIONS { waitForSync: true } RETURN dnsResult` - const testDNS = await testDNSCursor.next() - expect(testDNS).toEqual(undefined) - - const testWebCursor = - await query`FOR webResult IN web OPTIONS { waitForSync: true } RETURN webResult` - const testWeb = await testWebCursor.next() - expect(testWeb).toEqual(undefined) - - }) - it('removes claims', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', user._key)}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: superAdmin._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: superAdmin._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: superAdmin._key, - loadUserByKey: loadUserByKey({ - query, - userKey: superAdmin._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: superAdmin._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR claim IN claims OPTIONS { waitForSync: true } RETURN claim` - - const testClaimCursor = - await query`FOR claim IN claims OPTIONS { waitForSync: true } RETURN claim` - const testOrg = await testClaimCursor.next() - expect(testOrg).toEqual(undefined) - }) - it('removes domain', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', user._key)}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: superAdmin._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: superAdmin._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: superAdmin._key, - loadUserByKey: loadUserByKey({ - query, - userKey: superAdmin._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: superAdmin._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - - const testDomainCursor = - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - const testDomain = await testDomainCursor.next() - expect(testDomain).toEqual(undefined) - }) - }) - describe('multiple orgs claim the domain', () => { - let org2 - beforeEach(async () => { - org2 = await collections.organizations.save({ - orgDetails: { - en: { - slug: 'treasury-board-secretariat-2', - acronym: 'TBS', - name: 'Treasury Board of Canada Secretariat', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - fr: { - slug: 'secretariat-conseil-tresor-2', - acronym: 'SCT', - name: 'Secrétariat du Conseil Trésor du Canada', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - }, - }) - await collections.claims.save({ - _from: org2._id, - _to: domain._id, - }) - }) - it('does not remove the domain', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - - const testDomainCursor = - await query`FOR domain IN domains OPTIONS { waitForSync: true } RETURN domain` - const testDomain = await testDomainCursor.next() - expect(testDomain).toBeDefined() - }) - it('removes the claim', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR claim IN claims OPTIONS { waitForSync: true } RETURN claim` - - const testClaimCursor = await query` - FOR claim IN claims - OPTIONS { waitForSync: true } - FILTER claim._from == ${org._id} - RETURN claim - ` - const testClaim = await testClaimCursor.next() - expect(testClaim).toEqual(undefined) - }) - }) - describe('user belongs to multiple orgs', () => { - let org2 - beforeEach(async () => { - org2 = await collections.organizations.save({ - orgDetails: { - en: { - slug: 'treasury-board-secretariat-2', - acronym: 'TBS', - name: 'Treasury Board of Canada Secretariat', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - fr: { - slug: 'secretariat-conseil-tresor-2', - acronym: 'SCT', - name: 'Secrétariat du Conseil Trésor du Canada', - zone: 'FED', - sector: 'TBS', - country: 'Canada', - province: 'Ontario', - city: 'Ottawa', - }, - }, - }) - await collections.affiliations.save({ - _from: org2._id, - _to: user._id, - permission: 'user', - owner: false, - }) - }) - it('removes requesting users remaining affiliations', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - - const testAffiliationCursor = await query` - FOR aff IN affiliations - OPTIONS { waitForSync: true } - FILTER aff._from != ${superAdminOrg._id} - RETURN aff - ` - const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toEqual(undefined) - }) - }) - it('removes affiliated users and org', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - await query`FOR org IN organizations OPTIONS { waitForSync: true } RETURN org` - - const testAffiliationCursor = await query` - FOR aff IN affiliations - OPTIONS { waitForSync: true } - FILTER aff._from != ${superAdminOrg._id} - RETURN aff - ` - const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toEqual(undefined) - - const testOrgCursor = await query` - FOR org IN organizations - OPTIONS { waitForSync: true } - FILTER org._key != ${superAdminOrg._key} - RETURN org - ` - const testOrg = await testOrgCursor.next() - expect(testOrg).toEqual(undefined) - }) - }) - describe('user is not an org owner', () => { - beforeEach(async () => { - await collections.affiliations.save({ - _from: org._id, - _to: user._id, - permission: 'user', - owner: false, - }) - }) - it('removes the users affiliations', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - - const testAffiliationCursor = await query` - FOR aff IN affiliations - OPTIONS { waitForSync: true } - FILTER aff._from != ${superAdminOrg._id} - RETURN aff - ` - const testAffiliation = await testAffiliationCursor.next() - expect(testAffiliation).toEqual(undefined) - }) - }) - describe('users language is set to english', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'en', - localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - it('returns a status message', async () => { - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResponse = { - data: { - closeAccount: { - result: { - status: 'Successfully closed account.', - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully closed user: ${user._id} account.`, - ]) - }) - }) - describe('users language is set to french', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'fr', - localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - it('returns a status message', async () => { - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: user._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: user._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: user._key, - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: user._key, - }), - loadUserByKey: loadUserByKey({ - query, - userKey: user._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResponse = { - data: { - closeAccount: { - result: { - status: 'Le compte a été fermé avec succès.', - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: ${user._key} successfully closed user: ${user._id} account.`, - ]) - }) - }) - it('closes the users account', async () => { - await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', user._key)}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: superAdmin._key, - auth: { - checkSuperAdmin: checkSuperAdmin({ - i18n, - userKey: superAdmin._key, - query, - }), - userRequired: userRequired({ - i18n, - userKey: superAdmin._key, - loadUserByKey: loadUserByKey({ - query, - userKey: superAdmin._key, - i18n, - }), - }), - }, - loaders: { - loadUserByKey: loadUserByKey({ - query, - userKey: superAdmin._key, - i18n, - }), - }, - validators: {cleanseInput}, - }, - ) - - await query`FOR user IN users OPTIONS { waitForSync: true } RETURN user` - - const testUserCursor = await query` - FOR user IN users - OPTIONS { waitForSync: true } - FILTER user.userName != "super.admin@istio.actually.exists" - RETURN user - ` - const testUser = await testUserCursor.next() - expect(testUser).toEqual(undefined) - }) - }) - }) - - describe('given an unsuccessful closing of an account', () => { - describe('language is set to english', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'en', - localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - describe('user attempts to close another users account', () => { - describe('requesting user is not a super admin', () => { - it('returns an error', async () => { - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', '456')}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(false), - userRequired: jest.fn().mockReturnValue({_key: '123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResponse = { - data: { - closeAccount: { - result: { - code: 400, - description: - "Permission error: Unable to close other user's account.", - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: 123 attempted to close user: 456 account, but requesting user is not a super admin.`, - ]) - }) - }) - describe('requested user is undefined', () => { - it('returns an error', async () => { - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', '456')}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({_key: '123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue(undefined), - }, - }, - validators: {cleanseInput}, - }, - ) - - const expectedResponse = { - data: { - closeAccount: { - result: { - code: 400, - description: - 'Unable to close account of an undefined user.', - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: 123 attempted to close user: 456 account, but requested user is undefined.`, - ]) - }) - }) - }) - describe('database error occurs', () => { - describe('when getting affiliation info', () => { - it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('database error')) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Database error occurred when getting affiliations when user: 123 attempted to close account: users/123: Error: database error`, - ]) - }) - }) - describe('when getting ownership info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{}]), - } - - const mockedQuery = jest - .fn() - .mockReturnValueOnce(mockedCursor) - .mockRejectedValue(new Error('database error')) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Database error occurred when getting ownership info when user: 123 attempted to close account: users/123: Error: database error`, - ]) - }) - }) - describe('when gathering domain claim info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{}]), - } - - const mockedQuery = jest - .fn() - .mockReturnValueOnce(mockedCursor) - .mockReturnValueOnce(mockedCursor) - .mockRejectedValue(new Error('database error')) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Database error occurred when getting claim info when user: 123 attempted to close account: users/123: Error: database error`, - ]) - }) - }) - }) - describe('cursor error occurs', () => { - describe('when getting affiliation info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockRejectedValue(new Error('cursor error')), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Cursor error occurred when getting affiliations when user: 123 attempted to close account: users/123: Error: cursor error`, - ]) - }) + const testUserCursor = await query`FOR user IN users OPTIONS { waitForSync: true } RETURN user` + const testUser = await testUserCursor.next() + expect(testUser).toEqual(undefined) + }) + }) + describe('super admin is closing another users account', () => { + let superAdmin, superAdminOrg + beforeEach(async () => { + superAdmin = await collections.users.save({ + userName: 'super.admin@istio.actually.exists', + emailValidated: true, }) - describe('when getting ownership info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest - .fn() - .mockReturnValueOnce([{}]) - .mockRejectedValue(new Error('cursor error')), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Cursor error occurred when getting ownership info when user: 123 attempted to close account: users/123: Error: cursor error`, - ]) - }) + superAdminOrg = await collections.organizations.save({ + orgDetails: { + en: { + slug: 'super-admin', + acronym: 'SA', + name: 'Super Admin', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'super-admin', + acronym: 'SA', + name: 'Super Admin', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, }) - describe('when getting claim info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest - .fn() - .mockReturnValueOnce([{}]) - .mockReturnValueOnce([{}]) - .mockRejectedValue(new Error('cursor error')), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Cursor error occurred when getting claim info when user: 123 attempted to close account: users/123: Error: cursor error`, - ]) - }) + await collections.affiliations.save({ + _from: superAdminOrg._id, + _to: superAdmin._id, + permission: 'super_admin', }) - }) - describe('trx step error occurs', () => { - describe('domain is only claimed by one org', () => { - describe('when removing dmarc summary info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing dmarc summaries when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) - }) - describe('when removing ownership info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing ownerships when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) - }) - describe('when removing web scan result data', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn(). - mockReturnValue([{count: 1, domain: "test.gc.ca", _from: "organizations/123"}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while user: users/123 attempted to remove web data for test.gc.ca in org: organizations/123 while closing account, Error: trx step error`, - ]) - }) - }) - describe('when removing scan data', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn(). - mockReturnValue([{count: 1, domain: "test.gc.ca", _from: "organizations/123"}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while user: users/123 attempted to remove web data for test.gc.ca in org: organizations/123 while closing account, Error: trx step error`, - ]) - }) - }) - describe('when removing domain info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn(). - mockReturnValue([{count: 1, domain: "test.gc.ca", _from: "organizations/123"}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing domains and claims when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) - }) + user = await collections.users.save({ + userName: 'test.account@istio.actually.exists', + emailValidated: true, + }) + org = await collections.organizations.save({ + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, }) - describe('domain is claimed by multiple orgs', () => { - describe('when removing domain claim', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing domain claims when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) - }) + domain = await collections.domains.save({ + domain: 'test.gc.ca', + slug: 'test-gc-ca', }) - describe('when removing ownership orgs, and affiliations', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing domain claims when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) + await collections.claims.save({ + _from: org._id, + _to: domain._id, }) - describe('when removing the orgs remaining affiliations', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing ownership org and users affiliations when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) + const dns = await collections.dns.save({ dns: true }) + await collections.domainsDNS.save({ + _from: domain._id, + _to: dns._id, }) - describe('when removing the users affiliations', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), - } - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) + const web = await collections.web.save({ web: true }) + await collections.domainsWeb.save({ + _from: domain._id, + _to: web._id, + }) - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) + const webScan = await collections.webScan.save({ + webScan: true, + }) + await collections.webToWebScans.save({ + _from: web._id, + _to: webScan._id, + }) - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } + const dmarcSummary = await collections.dmarcSummaries.save({ + dmarcSummary: true, + }) + await collections.domainsToDmarcSummaries.save({ + _from: domain._id, + _to: dmarcSummary._id, + }) + }) + beforeEach(async () => { + await collections.affiliations.save({ + _from: org._id, + _to: user._id, + permission: 'user', + }) + }) + it('removes the users affiliations', async () => { + await graphql( + schema, + ` + mutation { + closeAccount(input: {}) { + result { + ... on CloseAccountResult { + status + } + ... on CloseAccountError { + code + description } } - `, - null, - { + } + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkSuperAdmin: checkSuperAdmin({ i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) + userKey: user._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: user._key, + loadUserByKey: loadUserByKey({ + query, + userKey: user._key, + i18n, + }), + }), + }, + loaders: { + loadOrgByKey: loadOrgByKey({ + query, + language: 'en', + i18n, + userKey: user._key, + }), + loadUserByKey: loadUserByKey({ + query, + userKey: user._key, + i18n, + }), + }, + validators: { cleanseInput }, + }, + ) - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] + await query`FOR aff IN affiliations OPTIONS { waitForSync: true } RETURN aff` - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing users remaining affiliations when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) + const testAffiliationCursor = await query` + FOR aff IN affiliations + OPTIONS { waitForSync: true } + FILTER aff._from != ${superAdminOrg._id} + RETURN aff + ` + const testAffiliation = await testAffiliationCursor.next() + expect(testAffiliation).toEqual(undefined) + }) + describe('users language is set to english', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, }) }) - describe('when removing the user', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } + it('returns a status message', async () => { + const response = await graphql( + schema, + ` + mutation { + closeAccount(input: {}) { + result { + ... on CloseAccountResult { + status + } + ... on CloseAccountError { + code + description } } } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkSuperAdmin: checkSuperAdmin({ + i18n, + userKey: user._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: user._key, + loadUserByKey: loadUserByKey({ query, - language: 'en', + userKey: user._key, i18n, - userKey: '123', }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, + }), }, - ) + loaders: { + loadOrgByKey: loadOrgByKey({ + query, + language: 'en', + i18n, + userKey: user._key, + }), + loadUserByKey: loadUserByKey({ + query, + userKey: user._key, + i18n, + }), + }, + validators: { cleanseInput }, + }, + ) - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] + const expectedResponse = { + data: { + closeAccount: { + result: { + status: 'Successfully closed account.', + }, + }, + }, + } - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([`User: ${user._key} successfully closed user: ${user._id} account.`]) }) }) - describe('trx commit error occurs', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn().mockRejectedValue(new Error('trx commit error')), + describe('users language is set to french', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'fr', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, }) - + }) + it('returns a status message', async () => { const response = await graphql( schema, ` @@ -3987,49 +725,133 @@ describe('given the closeAccount mutation', () => { null, { i18n, - query: mockedQuery, + query, collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', + transaction, + userKey: user._key, auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), + checkSuperAdmin: checkSuperAdmin({ + i18n, + userKey: user._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: user._key, + loadUserByKey: loadUserByKey({ + query, + userKey: user._key, + i18n, + }), + }), }, loaders: { loadOrgByKey: loadOrgByKey({ query, language: 'en', i18n, - userKey: '123', + userKey: user._key, }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), + loadUserByKey: loadUserByKey({ + query, + userKey: user._key, + i18n, + }), + }, + validators: { cleanseInput }, + }, + ) + + const expectedResponse = { + data: { + closeAccount: { + result: { + status: 'Le compte a été fermé avec succès.', }, }, - validators: {cleanseInput}, }, - ) + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([`User: ${user._key} successfully closed user: ${user._id} account.`]) + }) + }) + it('closes the users account', async () => { + await graphql( + schema, + ` + mutation { + closeAccount(input: { + userId: "${toGlobalId('user', user._key)}" + }) { + result { + ... on CloseAccountResult { + status + } + ... on CloseAccountError { + code + description + } + } + } + } + `, + null, + { + i18n, + query, + collections: collectionNames, + transaction, + userKey: superAdmin._key, + auth: { + checkSuperAdmin: checkSuperAdmin({ + i18n, + userKey: superAdmin._key, + query, + }), + userRequired: userRequired({ + i18n, + userKey: superAdmin._key, + loadUserByKey: loadUserByKey({ + query, + userKey: superAdmin._key, + i18n, + }), + }), + }, + loaders: { + loadUserByKey: loadUserByKey({ + query, + userKey: superAdmin._key, + i18n, + }), + }, + validators: { cleanseInput }, + }, + ) - const error = [ - new GraphQLError('Unable to close account. Please try again.'), - ] + await query`FOR user IN users OPTIONS { waitForSync: true } RETURN user` - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx commit error occurred when user: 123 attempted to close account: users/123: Error: trx commit error`, - ]) - }) + const testUserCursor = await query` + FOR user IN users + OPTIONS { waitForSync: true } + FILTER user.userName != "super.admin@istio.actually.exists" + RETURN user + ` + const testUser = await testUserCursor.next() + expect(testUser).toEqual(undefined) }) }) - describe('language is set to french', () => { + }) + + describe('given an unsuccessful closing of an account', () => { + describe('language is set to english', () => { beforeAll(() => { i18n = setupI18n({ - locale: 'fr', + locale: 'en', localeData: { - en: {plurals: {}}, - fr: {plurals: {}}, + en: { plurals: {} }, + fr: { plurals: {} }, }, locales: ['en', 'fr'], messages: { @@ -4069,69 +891,7 @@ describe('given the closeAccount mutation', () => { userKey: '123', auth: { checkSuperAdmin: jest.fn().mockReturnValue(false), - userRequired: jest.fn().mockReturnValue({_key: '123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - }, - validators: {cleanseInput}, - }, - ) - - const expectedResponse = { - data: { - closeAccount: { - result: { - code: 400, - description: - "Erreur de permission: Impossible de fermer le compte d'un autre utilisateur.", - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: 123 attempted to close user: 456 account, but requesting user is not a super admin.`, - ]) - }) - }) - describe('requested user is undefined', () => { - it('returns an error', async () => { - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: { - userId: "${toGlobalId('user', '456')}" - }) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query, - collections: collectionNames, - transaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({_key: '123'}), + userRequired: jest.fn().mockReturnValue({ _key: '123' }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -4140,11 +900,8 @@ describe('given the closeAccount mutation', () => { i18n, userKey: '123', }), - loadUserByKey: { - load: jest.fn().mockReturnValue(undefined), - }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) @@ -4153,251 +910,27 @@ describe('given the closeAccount mutation', () => { closeAccount: { result: { code: 400, - description: - "Impossible de fermer le compte d'un utilisateur non défini.", - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([ - `User: 123 attempted to close user: 456 account, but requested user is undefined.`, - ]) - }) - }) - }) - describe('database error occurs', () => { - describe('when getting affiliation info', () => { - it('throws an error', async () => { - const mockedQuery = jest - .fn() - .mockRejectedValue(new Error('database error')) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Database error occurred when getting affiliations when user: 123 attempted to close account: users/123: Error: database error`, - ]) - }) - }) - describe('when getting ownership info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{}]), - } - - const mockedQuery = jest - .fn() - .mockReturnValueOnce(mockedCursor) - .mockRejectedValue(new Error('database error')) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), + description: "Permission error: Unable to close other user's account.", }, }, - validators: {cleanseInput}, }, - ) - - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Database error occurred when getting ownership info when user: 123 attempted to close account: users/123: Error: database error`, - ]) - }) - }) - describe('when gathering domain claim info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{}]), } - const mockedQuery = jest - .fn() - .mockReturnValueOnce(mockedCursor) - .mockReturnValueOnce(mockedCursor) - .mockRejectedValue(new Error('database error')) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] - - expect(response.errors).toEqual(error) + expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ - `Database error occurred when getting claim info when user: 123 attempted to close account: users/123: Error: database error`, + `User: 123 attempted to close user: 456 account, but requesting user is not a super admin.`, ]) }) }) - }) - describe('cursor error occurs', () => { - describe('when getting affiliation info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockRejectedValue(new Error('cursor error')), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn(), - }) - + describe('requested user is undefined', () => { + it('returns an error', async () => { const response = await graphql( schema, ` mutation { - closeAccount(input: {}) { + closeAccount(input: { + userId: "${toGlobalId('user', '456')}" + }) { result { ... on CloseAccountResult { status @@ -4413,15 +946,13 @@ describe('given the closeAccount mutation', () => { null, { i18n, - query: mockedQuery, + query, collections: collectionNames, - transaction: mockedTransaction, + transaction, userKey: '123', auth: { checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), + userRequired: jest.fn().mockReturnValue({ _key: '123' }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -4431,38 +962,42 @@ describe('given the closeAccount mutation', () => { userKey: '123', }), loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), + load: jest.fn().mockReturnValue(undefined), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] + const expectedResponse = { + data: { + closeAccount: { + result: { + code: 400, + description: 'Unable to close account of an undefined user.', + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ - `Cursor error occurred when getting affiliations when user: 123 attempted to close account: users/123: Error: cursor error`, + `User: 123 attempted to close user: 456 account, but requested user is undefined.`, ]) }) }) - describe('when getting ownership info', () => { + }) + describe('trx step error occurs', () => { + describe('when removing the users affiliations', () => { it('throws an error', async () => { const mockedCursor = { - all: jest - .fn() - .mockReturnValueOnce([{}]) - .mockRejectedValue(new Error('cursor error')), + all: jest.fn().mockReturnValue([{ count: 2 }]), } const mockedQuery = jest.fn().mockReturnValue(mockedCursor) const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), + step: jest.fn().mockRejectedValue(new Error('trx step error')), commit: jest.fn(), }) @@ -4492,9 +1027,7 @@ describe('given the closeAccount mutation', () => { userKey: '123', auth: { checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), + userRequired: jest.fn().mockReturnValue({ _key: '123', _id: 'users/123' }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -4504,39 +1037,31 @@ describe('given the closeAccount mutation', () => { userKey: '123', }), loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), + load: jest.fn().mockReturnValue({ _key: '123' }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Unable to close account. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ - `Cursor error occurred when getting ownership info when user: 123 attempted to close account: users/123: Error: cursor error`, + `Trx step error occurred when removing users remaining affiliations when user: 123 attempted to close account: users/123: Error: trx step error`, ]) }) }) - describe('when getting claim info', () => { + describe('when removing the user', () => { it('throws an error', async () => { const mockedCursor = { - all: jest - .fn() - .mockReturnValueOnce([{}]) - .mockReturnValueOnce([{}]) - .mockRejectedValue(new Error('cursor error')), + all: jest.fn().mockReturnValue([{ count: 2 }]), } const mockedQuery = jest.fn().mockReturnValue(mockedCursor) const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), + step: jest.fn().mockReturnValueOnce().mockRejectedValue(new Error('trx step error')), commit: jest.fn(), }) @@ -4566,9 +1091,7 @@ describe('given the closeAccount mutation', () => { userKey: '123', auth: { checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), + userRequired: jest.fn().mockReturnValue({ _key: '123', _id: 'users/123' }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -4578,497 +1101,112 @@ describe('given the closeAccount mutation', () => { userKey: '123', }), loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), + load: jest.fn().mockReturnValue({ _key: '123' }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Unable to close account. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ - `Cursor error occurred when getting claim info when user: 123 attempted to close account: users/123: Error: cursor error`, + `Trx step error occurred when removing user: 123 attempted to close account: users/123: Error: trx step error`, ]) }) }) }) - describe('trx step error occurs', () => { - describe('domain is only claimed by one org', () => { - describe('when removing dmarc summary info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing dmarc summaries when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) - }) - describe('when removing ownership info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) + describe('trx commit error occurs', () => { + it('throws an error', async () => { + const mockedCursor = { + all: jest.fn().mockReturnValue([{ count: 2 }]), + } - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] + const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing ownerships when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) + const mockedTransaction = jest.fn().mockReturnValue({ + step: jest.fn().mockReturnValue(), + commit: jest.fn().mockRejectedValue(new Error('trx commit error')), }) - describe('when removing web scan result data', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn(). - mockReturnValue([{count: 1, domain: "test.gc.ca", _from: "organizations/123"}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } + const response = await graphql( + schema, + ` + mutation { + closeAccount(input: {}) { + result { + ... on CloseAccountResult { + status } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while user: users/123 attempted to remove web data for test.gc.ca in org: organizations/123 while closing account, Error: trx step error`, - ]) - }) - }) - describe('when removing scan data', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn(). - mockReturnValue([{count: 1, domain: "test.gc.ca", _from: "organizations/123"}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } + ... on CloseAccountError { + code + description } } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred while user: users/123 attempted to remove web data for test.gc.ca in org: organizations/123 while closing account, Error: trx step error`, - ]) - }) - }) - describe('when removing domain info', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn(). - mockReturnValue([{count: 1, domain: "test.gc.ca", _from: "organizations/123"}]), + } } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { + `, + null, + { + i18n, + query: mockedQuery, + collections: collectionNames, + transaction: mockedTransaction, + userKey: '123', + auth: { + checkSuperAdmin: jest.fn().mockReturnValue(true), + userRequired: jest.fn().mockReturnValue({ _key: '123', _id: 'users/123' }), + }, + loaders: { + loadOrgByKey: loadOrgByKey({ + query, + language: 'en', i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, + }), + loadUserByKey: { + load: jest.fn().mockReturnValue({ _key: '123' }), }, - ) + }, + validators: { cleanseInput }, + }, + ) - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Unable to close account. Please try again.')] - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing domains and claims when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) - }) + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `Trx commit error occurred when user: 123 attempted to close account: users/123: Error: trx commit error`, + ]) }) - describe('domain is claimed by multiple orgs', () => { - describe('when removing domain claim', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - - const response = await graphql( - schema, - ` - mutation { - closeAccount(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - null, - { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, - }, - validators: {cleanseInput}, - }, - ) - - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing domain claims when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) - }) + }) + }) + describe('language is set to french', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'fr', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, }) - describe('when removing ownership orgs, and affiliations', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - + }) + describe('user attempts to close another users account', () => { + describe('requesting user is not a super admin', () => { + it('returns an error', async () => { const response = await graphql( schema, ` mutation { - closeAccount(input: {}) { + closeAccount(input: { + userId: "${toGlobalId('user', '456')}" + }) { result { ... on CloseAccountResult { status @@ -5084,15 +1222,13 @@ describe('given the closeAccount mutation', () => { null, { i18n, - query: mockedQuery, + query, collections: collectionNames, - transaction: mockedTransaction, + transaction, userKey: '123', auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), + checkSuperAdmin: jest.fn().mockReturnValue(false), + userRequired: jest.fn().mockReturnValue({ _key: '123' }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -5101,49 +1237,37 @@ describe('given the closeAccount mutation', () => { i18n, userKey: '123', }), - loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), - }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] + const expectedResponse = { + data: { + closeAccount: { + result: { + code: 400, + description: "Erreur de permission: Impossible de fermer le compte d'un autre utilisateur.", + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ - `Trx step error occurred when removing domain claims when user: 123 attempted to close account: users/123: Error: trx step error`, + `User: 123 attempted to close user: 456 account, but requesting user is not a super admin.`, ]) }) }) - describe('when removing the orgs remaining affiliations', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - }) - + describe('requested user is undefined', () => { + it('returns an error', async () => { const response = await graphql( schema, ` mutation { - closeAccount(input: {}) { + closeAccount(input: { + userId: "${toGlobalId('user', '456')}" + }) { result { ... on CloseAccountResult { status @@ -5159,15 +1283,13 @@ describe('given the closeAccount mutation', () => { null, { i18n, - query: mockedQuery, + query, collections: collectionNames, - transaction: mockedTransaction, + transaction, userKey: '123', auth: { checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), + userRequired: jest.fn().mockReturnValue({ _key: '123' }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -5177,42 +1299,42 @@ describe('given the closeAccount mutation', () => { userKey: '123', }), loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), + load: jest.fn().mockReturnValue(undefined), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] + const expectedResponse = { + data: { + closeAccount: { + result: { + code: 400, + description: "Impossible de fermer le compte d'un utilisateur non défini.", + }, + }, + }, + } - expect(response.errors).toEqual(error) + expect(response).toEqual(expectedResponse) expect(consoleOutput).toEqual([ - `Trx step error occurred when removing ownership org and users affiliations when user: 123 attempted to close account: users/123: Error: trx step error`, + `User: 123 attempted to close user: 456 account, but requested user is undefined.`, ]) }) }) + }) + describe('trx step error occurs', () => { describe('when removing the users affiliations', () => { it('throws an error', async () => { const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), + all: jest.fn().mockReturnValue([{ count: 2 }]), } const mockedQuery = jest.fn().mockReturnValue(mockedCursor) const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockRejectedValue(new Error('trx step error')), commit: jest.fn(), }) @@ -5242,9 +1364,7 @@ describe('given the closeAccount mutation', () => { userKey: '123', auth: { checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), + userRequired: jest.fn().mockReturnValue({ _key: '123', _id: 'users/123' }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -5254,18 +1374,14 @@ describe('given the closeAccount mutation', () => { userKey: '123', }), loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), + load: jest.fn().mockReturnValue({ _key: '123' }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de fermer le compte. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -5276,21 +1392,13 @@ describe('given the closeAccount mutation', () => { describe('when removing the user', () => { it('throws an error', async () => { const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), + all: jest.fn().mockReturnValue([{ count: 2 }]), } const mockedQuery = jest.fn().mockReturnValue(mockedCursor) const mockedTransaction = jest.fn().mockReturnValue({ - step: jest - .fn() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockReturnValueOnce() - .mockRejectedValue(new Error('trx step error')), + step: jest.fn().mockReturnValueOnce().mockRejectedValue(new Error('trx step error')), commit: jest.fn(), }) @@ -5320,9 +1428,7 @@ describe('given the closeAccount mutation', () => { userKey: '123', auth: { checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), + userRequired: jest.fn().mockReturnValue({ _key: '123', _id: 'users/123' }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -5332,18 +1438,14 @@ describe('given the closeAccount mutation', () => { userKey: '123', }), loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), + load: jest.fn().mockReturnValue({ _key: '123' }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de fermer le compte. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -5355,7 +1457,7 @@ describe('given the closeAccount mutation', () => { describe('trx commit error occurs', () => { it('throws an error', async () => { const mockedCursor = { - all: jest.fn().mockReturnValue([{count: 2}]), + all: jest.fn().mockReturnValue([{ count: 2 }]), } const mockedQuery = jest.fn().mockReturnValue(mockedCursor) @@ -5391,9 +1493,7 @@ describe('given the closeAccount mutation', () => { userKey: '123', auth: { checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest - .fn() - .mockReturnValue({_key: '123', _id: 'users/123'}), + userRequired: jest.fn().mockReturnValue({ _key: '123', _id: 'users/123' }), }, loaders: { loadOrgByKey: loadOrgByKey({ @@ -5403,18 +1503,14 @@ describe('given the closeAccount mutation', () => { userKey: '123', }), loadUserByKey: { - load: jest.fn().mockReturnValue({_key: '123'}), + load: jest.fn().mockReturnValue({ _key: '123' }), }, }, - validators: {cleanseInput}, + validators: { cleanseInput }, }, ) - const error = [ - new GraphQLError( - 'Impossible de fermer le compte. Veuillez réessayer.', - ), - ] + const error = [new GraphQLError('Impossible de fermer le compte. Veuillez réessayer.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ diff --git a/api/src/user/mutations/close-account.js b/api/src/user/mutations/close-account.js index 78eec8d8c3..012dbc0b76 100644 --- a/api/src/user/mutations/close-account.js +++ b/api/src/user/mutations/close-account.js @@ -73,278 +73,9 @@ export const closeAccount = new mutationWithClientMutationId({ targetUserName = user.userName } - // check to see if user owns any orgs - let orgOwnerAffiliationCursor - try { - orgOwnerAffiliationCursor = await query` - WITH users, affiliations, organizations - FOR v, e IN 1..1 INBOUND ${userId} affiliations - FILTER e.owner == true - RETURN e - ` - } catch (err) { - console.error( - `Database error occurred when getting affiliations when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - let orgOwnerAffiliationCheck - try { - orgOwnerAffiliationCheck = await orgOwnerAffiliationCursor.all() - } catch (err) { - console.error( - `Cursor error occurred when getting affiliations when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - // Setup Trans action const trx = await transaction(collections) - // loop through each found org - for (const affiliation of orgOwnerAffiliationCheck) { - let dmarcSummaryCheckCursor - try { - dmarcSummaryCheckCursor = await query` - WITH domains, ownership, organizations, users - FOR v, e IN 1..1 OUTBOUND ${affiliation._from} ownership - RETURN e - ` - } catch (err) { - console.error( - `Database error occurred when getting ownership info when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - let dmarcSummaryCheckList - try { - dmarcSummaryCheckList = await dmarcSummaryCheckCursor.all() - } catch (err) { - console.error( - `Cursor error occurred when getting ownership info when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - // remove dmarc summary related things - for (const ownership of dmarcSummaryCheckList) { - try { - await trx.step( - () => query` - WITH ownership, organizations, domains, dmarcSummaries, domainsToDmarcSummaries - LET dmarcSummaryEdges = ( - FOR v, e IN 1..1 OUTBOUND ${ownership._to} domainsToDmarcSummaries - RETURN { edgeKey: e._key, dmarcSummaryId: e._to } - ) - LET removeDmarcSummaryEdges = ( - FOR dmarcSummaryEdge IN dmarcSummaryEdges - REMOVE dmarcSummaryEdge.edgeKey IN domainsToDmarcSummaries - OPTIONS { waitForSync: true } - ) - LET removeDmarcSummary = ( - FOR dmarcSummaryEdge IN dmarcSummaryEdges - LET key = PARSE_IDENTIFIER(dmarcSummaryEdge.dmarcSummaryId).key - REMOVE key IN dmarcSummaries - OPTIONS { waitForSync: true } - ) - RETURN true - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when removing dmarc summaries when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - try { - await trx.step( - () => query` - WITH ownership, organizations, domains - REMOVE ${ownership._key} IN ownership - OPTIONS { waitForSync: true } - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when removing ownerships when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - } - - let domainCountCursor - try { - domainCountCursor = await query` - WITH claims, domains, organizations, users - LET domainIds = ( - FOR v, e IN 1..1 OUTBOUND ${affiliation._from} claims - RETURN e._to - ) - FOR domain IN domains - FILTER domain._id IN domainIds - LET count = LENGTH( - FOR v, e IN 1..1 INBOUND domain._id claims - RETURN 1 - ) - RETURN { - _id: domain._id, - _key: domain._key, - domain: domain.domain, - count: count - } - ` - } catch (err) { - console.error( - `Database error occurred when getting claim info when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - let domainCountList - try { - domainCountList = await domainCountCursor.all() - } catch (err) { - console.error( - `Cursor error occurred when getting claim info when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - for (const domainObj of domainCountList) { - if (domainObj.count === 1) { - // Remove scan data - - try { - // Remove web data - await trx.step(async () => { - await query` - WITH web, webScan, domains - FOR webV, domainsWebEdge IN 1..1 OUTBOUND ${domainObj._id} domainsWeb - FOR webScanV, webToWebScansV In 1..1 OUTBOUND webV._id webToWebScans - REMOVE webScanV IN webScan - REMOVE webToWebScansV IN webToWebScans - OPTIONS { waitForSync: true } - REMOVE webV IN web - REMOVE domainsWebEdge IN domainsWeb - OPTIONS { waitForSync: true } - ` - }) - } catch (err) { - console.error( - `Trx step error occurred while user: ${userId} attempted to remove web data for ${domainObj.domain} in org: ${affiliation._from} while closing account, ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - try { - // Remove DNS data - await trx.step(async () => { - await query` - WITH dns, domains - FOR dnsV, domainsDNSEdge IN 1..1 OUTBOUND ${domainObj._id} domainsDNS - REMOVE dnsV IN dns - REMOVE domainsDNSEdge IN domainsDNS - OPTIONS { waitForSync: true } - ` - }) - } catch (err) { - console.error( - `Trx step error occurred while user: ${userId} attempted to remove DNS data for ${domainObj.domain} in org: ${affiliation._from}, ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - try { - await trx.step( - () => query` - WITH claims, domains, organizations, users - LET domainEdges = ( - FOR v, e IN 1..1 OUTBOUND ${affiliation._from} claims - FILTER e._to == ${domainObj._id} - RETURN { edgeKey: e._key, domainId: e._to } - ) - LET removeDomainEdges = ( - FOR domainEdge IN domainEdges - REMOVE domainEdge.edgeKey IN claims - OPTIONS { waitForSync: true } - ) - LET removeDomain = ( - FOR domainEdge IN domainEdges - REMOVE PARSE_IDENTIFIER(domainEdge.domainId).key - IN domains OPTIONS { waitForSync: true } - ) - RETURN true - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when removing domains and claims when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - } else { - try { - await trx.step( - () => query` - WITH claims, domains, organizations, users - LET domainEdges = ( - FOR v, e IN 1..1 OUTBOUND ${affiliation._from} claims - RETURN { edgeKey: e._key, domainId: e._to } - ) - LET removeDomainEdges = ( - FOR domainEdge IN domainEdges - REMOVE domainEdge.edgeKey IN claims - OPTIONS { waitForSync: true } - ) - RETURN true - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when removing domain claims when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - } - } - - // remove users affiliation - try { - await Promise.all([ - trx.step( - () => query` - WITH affiliations, organizations, users - LET userEdges = ( - FOR v, e IN 1..1 INBOUND ${affiliation._from} affiliations - RETURN { edgeKey: e._key, userKey: e._to } - ) - LET removeUserEdges = ( - FOR userEdge IN userEdges - REMOVE userEdge.userKey IN affiliations - OPTIONS { waitForSync: true } - ) - RETURN true - `, - ), - trx.step( - () => query` - WITH organizations - REMOVE PARSE_IDENTIFIER(${affiliation._from}).key - IN organizations OPTIONS { waitForSync: true } - `, - ), - ]) - } catch (err) { - console.error( - `Trx step error occurred when removing ownership org and users affiliations when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - } - try { await trx.step( () => query` diff --git a/api/src/user/mutations/sign-up.js b/api/src/user/mutations/sign-up.js index 45775e6818..90f5f0e81d 100644 --- a/api/src/user/mutations/sign-up.js +++ b/api/src/user/mutations/sign-up.js @@ -121,6 +121,7 @@ export const signUp = new mutationWithClientMutationId({ phoneValidated: false, emailValidated: false, insideUser: false, + receiveUpdateEmails: true, failedLoginAttempts: 0, tfaSendMethod: 'none', refreshInfo: { @@ -202,7 +203,6 @@ export const signUp = new mutationWithClientMutationId({ _from: ${checkOrg._id}, _to: ${insertedUser._id}, permission: ${tokenRequestedRole}, - owner: false } INTO affiliations `, ) diff --git a/api/src/user/mutations/update-user-profile.js b/api/src/user/mutations/update-user-profile.js index 1717cb6a07..3602dcce2d 100644 --- a/api/src/user/mutations/update-user-profile.js +++ b/api/src/user/mutations/update-user-profile.js @@ -1,10 +1,10 @@ -import {GraphQLString, GraphQLBoolean} from 'graphql' -import {mutationWithClientMutationId} from 'graphql-relay' -import {GraphQLEmailAddress} from 'graphql-scalars' -import {t} from '@lingui/macro' +import { GraphQLString, GraphQLBoolean } from 'graphql' +import { mutationWithClientMutationId } from 'graphql-relay' +import { GraphQLEmailAddress } from 'graphql-scalars' +import { t } from '@lingui/macro' -import {LanguageEnums, TfaSendMethodEnum} from '../../enums' -import {updateUserProfileUnion} from '../unions' +import { LanguageEnums, TfaSendMethodEnum } from '../../enums' +import { updateUserProfileUnion } from '../unions' export const updateUserProfile = new mutationWithClientMutationId({ name: 'UpdateUserProfile', @@ -21,18 +21,19 @@ export const updateUserProfile = new mutationWithClientMutationId({ }, preferredLang: { type: LanguageEnums, - description: - 'The updated preferred language the user wishes to change to.', + description: 'The updated preferred language the user wishes to change to.', }, tfaSendMethod: { type: TfaSendMethodEnum, - description: - 'The method in which the user wishes to have their TFA code sent via.', + description: 'The method in which the user wishes to have their TFA code sent via.', }, insideUser: { type: GraphQLBoolean, - description: - 'The updated boolean which represents if the user wants to see features in progress.', + description: 'The updated boolean which represents if the user wants to see features in progress.', + }, + receiveUpdateEmails: { + type: GraphQLBoolean, + description: 'The updated boolean which represents if the user wants to receive update emails.', }, }), outputFields: () => ({ @@ -52,10 +53,10 @@ export const updateUserProfile = new mutationWithClientMutationId({ transaction, userKey, request, - auth: {tokenize, userRequired}, - loaders: {loadUserByKey, loadUserByUserName}, - notify: {sendVerificationEmail}, - validators: {cleanseInput}, + auth: { tokenize, userRequired }, + loaders: { loadUserByKey, loadUserByUserName }, + notify: { sendVerificationEmail }, + validators: { cleanseInput }, }, ) => { // Cleanse Input @@ -64,6 +65,7 @@ export const updateUserProfile = new mutationWithClientMutationId({ const preferredLang = cleanseInput(args.preferredLang) const subTfaSendMethod = cleanseInput(args.tfaSendMethod) const insideUserBool = args.insideUser + const receiveUpdateEmailsBool = args.receiveUpdateEmails // Get user info from DB const user = await userRequired() @@ -72,9 +74,7 @@ export const updateUserProfile = new mutationWithClientMutationId({ if (userName !== '') { const checkUser = await loadUserByUserName.load(userName) if (typeof checkUser !== 'undefined') { - console.warn( - `User: ${userKey} attempted to update their username, but the username is already in use.`, - ) + console.warn(`User: ${userKey} attempted to update their username, but the username is already in use.`) return { _type: 'error', code: 400, @@ -96,12 +96,8 @@ export const updateUserProfile = new mutationWithClientMutationId({ RETURN e.permission ` } catch (err) { - console.error( - `Database error occurred when user: ${userKey} was seeing if they were an admin, err: ${err}`, - ) - throw new Error( - i18n._(t`Unable to verify if user is an admin, please try again.`), - ) + console.error(`Database error occurred when user: ${userKey} was seeing if they were an admin, err: ${err}`) + throw new Error(i18n._(t`Unable to verify if user is an admin, please try again.`)) } if (userAdmin.count > 0) { @@ -111,9 +107,7 @@ export const updateUserProfile = new mutationWithClientMutationId({ return { _type: 'error', code: 403, - description: i18n._( - t`Permission Denied: Multi-factor authentication is required for admin accounts`, - ), + description: i18n._(t`Permission Denied: Multi-factor authentication is required for admin accounts`), } } } @@ -123,10 +117,7 @@ export const updateUserProfile = new mutationWithClientMutationId({ tfaSendMethod = 'phone' } else if (subTfaSendMethod === 'email' && user.emailValidated) { tfaSendMethod = 'email' - } else if ( - subTfaSendMethod === 'none' || - typeof user.tfaSendMethod === 'undefined' - ) { + } else if (subTfaSendMethod === 'none' || typeof user.tfaSendMethod === 'undefined') { tfaSendMethod = 'none' } else { tfaSendMethod = user.tfaSendMethod @@ -146,10 +137,9 @@ export const updateUserProfile = new mutationWithClientMutationId({ preferredLang: preferredLang || user.preferredLang, tfaSendMethod: tfaSendMethod, emailValidated, - insideUser: - typeof insideUserBool !== 'undefined' - ? insideUserBool - : user?.insideUser, + insideUser: typeof insideUserBool !== 'undefined' ? insideUserBool : user?.insideUser, + receiveUpdateEmails: + typeof receiveUpdateEmailsBool !== 'undefined' ? receiveUpdateEmailsBool : user?.receiveUpdateEmails, } // Setup Transaction @@ -166,18 +156,14 @@ export const updateUserProfile = new mutationWithClientMutationId({ `, ) } catch (err) { - console.error( - `Trx step error occurred when user: ${userKey} attempted to update their profile: ${err}`, - ) + console.error(`Trx step error occurred when user: ${userKey} attempted to update their profile: ${err}`) throw new Error(i18n._(t`Unable to update profile. Please try again.`)) } try { await trx.commit() } catch (err) { - console.error( - `Trx commit error occurred when user: ${userKey} attempted to update their profile: ${err}`, - ) + console.error(`Trx commit error occurred when user: ${userKey} attempted to update their profile: ${err}`) throw new Error(i18n._(t`Unable to update profile. Please try again.`)) } @@ -185,11 +171,11 @@ export const updateUserProfile = new mutationWithClientMutationId({ const returnUser = await loadUserByKey.load(userKey) if (changedUserName) { - const token = tokenize({parameters: {userKey: returnUser._key}}) + const token = tokenize({ parameters: { userKey: returnUser._key } }) const verifyUrl = `https://${request.get('host')}/validate/${token}` - await sendVerificationEmail({user: returnUser, verifyUrl}) + await sendVerificationEmail({ user: returnUser, verifyUrl }) } console.info(`User: ${userKey} successfully updated their profile.`) diff --git a/api/src/user/objects/user-personal.js b/api/src/user/objects/user-personal.js index 905deb1f6c..cbb871c68e 100644 --- a/api/src/user/objects/user-personal.js +++ b/api/src/user/objects/user-personal.js @@ -1,11 +1,11 @@ -import {GraphQLBoolean, GraphQLObjectType, GraphQLString} from 'graphql' -import {connectionArgs, globalIdField} from 'graphql-relay' -import {GraphQLEmailAddress, GraphQLPhoneNumber} from 'graphql-scalars' +import { GraphQLBoolean, GraphQLObjectType, GraphQLString } from 'graphql' +import { connectionArgs, globalIdField } from 'graphql-relay' +import { GraphQLEmailAddress, GraphQLPhoneNumber } from 'graphql-scalars' -import {affiliationOrgOrder} from '../../affiliation/inputs' -import {affiliationConnection} from '../../affiliation/objects' -import {LanguageEnums, TfaSendMethodEnum} from '../../enums' -import {nodeInterface} from '../../node' +import { affiliationOrgOrder } from '../../affiliation/inputs' +import { affiliationConnection } from '../../affiliation/objects' +import { LanguageEnums, TfaSendMethodEnum } from '../../enums' +import { nodeInterface } from '../../node' export const userPersonalType = new GraphQLObjectType({ name: 'PersonalUser', @@ -14,21 +14,17 @@ export const userPersonalType = new GraphQLObjectType({ userName: { type: GraphQLEmailAddress, description: 'Users email address.', - resolve: ({userName}) => userName, + resolve: ({ userName }) => userName, }, displayName: { type: GraphQLString, description: 'Name displayed to other users.', - resolve: ({displayName}) => displayName, + resolve: ({ displayName }) => displayName, }, phoneNumber: { type: GraphQLPhoneNumber, description: 'The phone number the user has setup with tfa.', - resolve: ( - {phoneDetails}, - _args, - {validators: {decryptPhoneNumber}}, - ) => { + resolve: ({ phoneDetails }, _args, { validators: { decryptPhoneNumber } }) => { if (typeof phoneDetails === 'undefined' || phoneDetails === null) { return null } @@ -38,28 +34,33 @@ export const userPersonalType = new GraphQLObjectType({ preferredLang: { type: LanguageEnums, description: 'Users preferred language.', - resolve: ({preferredLang}) => preferredLang, + resolve: ({ preferredLang }) => preferredLang, }, phoneValidated: { type: GraphQLBoolean, description: 'Has the user completed phone validation.', - resolve: ({phoneValidated}) => phoneValidated, + resolve: ({ phoneValidated }) => phoneValidated, }, emailValidated: { type: GraphQLBoolean, description: 'Has the user email verified their account.', - resolve: ({emailValidated}) => emailValidated, + resolve: ({ emailValidated }) => emailValidated, }, tfaSendMethod: { type: TfaSendMethodEnum, description: 'The method in which TFA codes are sent.', - resolve: ({tfaSendMethod}) => tfaSendMethod, + resolve: ({ tfaSendMethod }) => tfaSendMethod, }, insideUser: { type: GraphQLBoolean, description: 'Does the user want to see new features in progress.', resolve: ({ insideUser }) => insideUser, }, + receiveUpdateEmails: { + type: GraphQLBoolean, + description: 'Does the user want to receive update emails.', + resolve: ({ receiveUpdateEmails }) => receiveUpdateEmails, + }, affiliations: { type: affiliationConnection.connectionType, description: 'Users affiliations to various organizations.', @@ -74,11 +75,7 @@ export const userPersonalType = new GraphQLObjectType({ }, ...connectionArgs, }, - resolve: async ( - {_id}, - args, - {loaders: {loadAffiliationConnectionsByUserId}}, - ) => { + resolve: async ({ _id }, args, { loaders: { loadAffiliationConnectionsByUserId } }) => { const affiliations = await loadAffiliationConnectionsByUserId({ userId: _id, ...args, diff --git a/app/aks/backup-cronjob.yaml b/app/aks/backup-cronjob.yaml index 66f75dc2e2..099a77167e 100644 --- a/app/aks/backup-cronjob.yaml +++ b/app/aks/backup-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: labels: diff --git a/app/bases/dmarc-report-cronjob.yaml b/app/bases/dmarc-report-cronjob.yaml index ec6022c045..cd9611245a 100644 --- a/app/bases/dmarc-report-cronjob.yaml +++ b/app/bases/dmarc-report-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: name: dmarc-report @@ -13,7 +13,7 @@ spec: spec: containers: - name: dmarc-report - image: gcr.io/track-compliance/dmarc-report:master-770f2e2-1674589724 # {"$imagepolicy": "flux-system:dmarc-report"} + image: gcr.io/track-compliance/dmarc-report:master-927f20c-1685459197 # {"$imagepolicy": "flux-system:dmarc-report"} env: - name: DB_NAME valueFrom: diff --git a/app/bases/guidance-cronjob.yaml b/app/bases/guidance-cronjob.yaml index ab28d8a2b1..0b0c31fc9f 100755 --- a/app/bases/guidance-cronjob.yaml +++ b/app/bases/guidance-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: name: guidance diff --git a/app/bases/scan-dispatcher-cronjob.yaml b/app/bases/scan-dispatcher-cronjob.yaml index 57f28f3d23..c982010ed8 100644 --- a/app/bases/scan-dispatcher-cronjob.yaml +++ b/app/bases/scan-dispatcher-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: name: scan-dispatcher diff --git a/app/bases/summaries-cronjob.yaml b/app/bases/summaries-cronjob.yaml index 8f47d8d971..35ef98f9de 100644 --- a/app/bases/summaries-cronjob.yaml +++ b/app/bases/summaries-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: name: summaries @@ -13,7 +13,7 @@ spec: spec: containers: - name: summaries - image: gcr.io/track-compliance/services/summaries:master-cbbb2fd-1681235183 # {"$imagepolicy": "flux-system:summaries"} + image: gcr.io/track-compliance/services/summaries:master-955c53b-1685969471 # {"$imagepolicy": "flux-system:summaries"} env: - name: DB_USER valueFrom: diff --git a/app/bases/tracker-api-deployment.yaml b/app/bases/tracker-api-deployment.yaml index e1660eca6c..1d9fe49c0b 100644 --- a/app/bases/tracker-api-deployment.yaml +++ b/app/bases/tracker-api-deployment.yaml @@ -50,7 +50,7 @@ spec: - name: database-config mountPath: /app containers: - - image: gcr.io/track-compliance/api-js:master-a802403-1680796913 # {"$imagepolicy": "flux-system:api"} + - image: gcr.io/track-compliance/api-js:master-af5d359-1686937994 # {"$imagepolicy": "flux-system:api"} name: api ports: - containerPort: 4000 diff --git a/app/bases/tracker-frontend-deployment.yaml b/app/bases/tracker-frontend-deployment.yaml index cbd7548d00..4a4a0b695e 100644 --- a/app/bases/tracker-frontend-deployment.yaml +++ b/app/bases/tracker-frontend-deployment.yaml @@ -18,7 +18,7 @@ spec: app: tracker-frontend spec: containers: - - image: gcr.io/track-compliance/frontend:master-a802403-1680797001 # {"$imagepolicy": "flux-system:frontend"} + - image: gcr.io/track-compliance/frontend:master-af5d359-1687177261 # {"$imagepolicy": "flux-system:frontend"} name: frontend resources: limits: diff --git a/app/gke/backup-cronjob.yaml b/app/gke/backup-cronjob.yaml index 25adfba158..8815a7b084 100644 --- a/app/gke/backup-cronjob.yaml +++ b/app/gke/backup-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: labels: diff --git a/app/gke/log4shell-scanner-deployment.yaml b/app/gke/log4shell-scanner-deployment.yaml index 3a5e5e7018..8dd1fa1ed5 100644 --- a/app/gke/log4shell-scanner-deployment.yaml +++ b/app/gke/log4shell-scanner-deployment.yaml @@ -17,7 +17,7 @@ spec: spec: containers: - name: log4shell-scanner - image: gcr.io/track-compliance/log4shell-scanner:master-74c49c3-1670863247 # {"$imagepolicy": "flux-system:log4shell-scanner"} + image: gcr.io/track-compliance/log4shell-scanner:master-8d91653-1685369491 # {"$imagepolicy": "flux-system:log4shell-scanner"} env: - name: PYTHONWARNINGS value: ignore diff --git a/app/jobs/super-admin.yaml b/app/jobs/super-admin.yaml index dc5876def1..b9f13abe8e 100644 --- a/app/jobs/super-admin.yaml +++ b/app/jobs/super-admin.yaml @@ -8,7 +8,7 @@ spec: spec: containers: - name: super-admin - image: gcr.io/track-compliance/super-admin:master-a802403-1680796755 # {"$imagepolicy": "flux-system:super-admin"} + image: gcr.io/track-compliance/super-admin:master-927f20c-1685459193 # {"$imagepolicy": "flux-system:super-admin"} env: - name: DB_PASS valueFrom: diff --git a/app/production/backup-cronjob.yaml b/app/production/backup-cronjob.yaml index 66f75dc2e2..099a77167e 100644 --- a/app/production/backup-cronjob.yaml +++ b/app/production/backup-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: labels: diff --git a/app/staging/backup-cronjob.yaml b/app/staging/backup-cronjob.yaml index 66f75dc2e2..099a77167e 100644 --- a/app/staging/backup-cronjob.yaml +++ b/app/staging/backup-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: labels: diff --git a/app/test/backup-cronjob.yaml b/app/test/backup-cronjob.yaml index e86cb5ba37..5c4ae9b263 100644 --- a/app/test/backup-cronjob.yaml +++ b/app/test/backup-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: labels: diff --git a/frontend/mocking/faked_schema.js b/frontend/mocking/faked_schema.js index 279560e685..c2c0db0390 100644 --- a/frontend/mocking/faked_schema.js +++ b/frontend/mocking/faked_schema.js @@ -112,6 +112,9 @@ export const getTypeNames = () => gql` # Filter org list to either include or exclude the super admin org. includeSuperAdminOrg: Boolean + # Filter org list to include only verified organizations. + isVerified: Boolean + # Returns the items in the list that come after the specified cursor. after: String @@ -155,18 +158,33 @@ export const getTypeNames = () => gql` # CSV formatted output of all domains in all organizations including their email and web scan statuses. getAllOrganizationDomainStatuses: String - # Email summary computed values, used to build summary cards. - mailSummary: CategorizedSummary - - # Web summary computed values, used to build summary cards. - webSummary: CategorizedSummary + # DKIM summary computed values, used to build summary cards. + dkimSummary: CategorizedSummary # DMARC phase summary computed values, used to build summary cards. dmarcPhaseSummary: CategorizedSummary + # DMARC summary computed values, used to build summary cards. + dmarcSummary: CategorizedSummary + # HTTPS summary computed values, used to build summary cards. httpsSummary: CategorizedSummary + # Email summary computed values, used to build summary cards. + mailSummary: CategorizedSummary + + # SPF summary computed values, used to build summary cards. + spfSummary: CategorizedSummary + + # SSL summary computed values, used to build summary cards. + sslSummary: CategorizedSummary + + # SSL summary computed values, used to build summary cards. + webConnectionsSummary: CategorizedSummary + + # Web summary computed values, used to build summary cards. + webSummary: CategorizedSummary + # Query the currently logged in user. findMe: PersonalUser @@ -308,8 +326,8 @@ export const getTypeNames = () => gql` # The ID of an object id: ID! - # Datetime string the activity occured. - timestamp: String + # Datetime string the activity occurred. + timestamp: DateTime # Username of admin that initiated the activity. initiatedBy: InitiatedBy @@ -324,6 +342,9 @@ export const getTypeNames = () => gql` reason: DomainRemovalReasonEnum } + # A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the 'date-time' format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. + scalar DateTime + # Information on the user that initiated the logged action type InitiatedBy { # The ID of an object @@ -344,6 +365,9 @@ export const getTypeNames = () => gql` # An enum used to assign, and test users roles. enum RoleEnums { + # A user who has requested an invite to an organization. + PENDING + # A user who has been given access to view an organization. USER @@ -450,12 +474,12 @@ export const getTypeNames = () => gql` RESOURCE_NAME } - # Possible directions in which to order a list of items when provided an "orderBy" argument. + # Possible directions in which to order a list of items when provided an 'orderBy' argument. enum OrderDirection { - # Specifies an ascending order for a given "orderBy" argument. + # Specifies an ascending order for a given 'orderBy' argument. ASC - # Specifies a descending order for a given "orderBy" argument. + # Specifies a descending order for a given 'orderBy' argument. DESC } @@ -539,6 +563,15 @@ export const getTypeNames = () => gql` # The domains scan status, based on the latest scan data. status: DomainStatus + # Value that determines if a domain is excluded from any results and scans. + archived: Boolean + + # Value that determines if a domain is possibly blocked. + blocked: Boolean + + # Value that determines if a domain has a web scan pending. + webScanPending: Boolean + # The organization that this domain belongs to. organizations( # Ordering options for organization connections @@ -569,10 +602,10 @@ export const getTypeNames = () => gql` # DNS scan results. dnsScan( # Start date for date filter. - startDate: Date + startDate: DateTime # End date for date filter. - endDate: Date + endDate: DateTime # Ordering options for DNS connections. orderBy: DNSOrder @@ -596,10 +629,10 @@ export const getTypeNames = () => gql` # HTTPS, and TLS scan results. web( # Start date for date filter. - startDate: Date + startDate: DateTime # End date for date filter. - endDate: Date + endDate: DateTime # Ordering options for web connections. orderBy: WebOrder @@ -607,6 +640,9 @@ export const getTypeNames = () => gql` # Number of web scans to retrieve. limit: Int + # Exclude web scans which have pending status. + excludePending: Boolean + # Returns the items in the list that come after the specified cursor. after: String @@ -637,9 +673,6 @@ export const getTypeNames = () => gql` # Value that determines if a domain is excluded from an organization's results. hidden: Boolean - - # Value that determines if a domain is excluded from any results and scans. - archived: Boolean } # String that conforms to a domain structure. @@ -650,6 +683,9 @@ export const getTypeNames = () => gql` # This object contains how the domain is doing on the various scans we preform, based on the latest scan data. type DomainStatus { + # Certificates Status + certificates: StatusEnum + # Ciphers Status ciphers: StatusEnum @@ -755,6 +791,8 @@ export const getTypeNames = () => gql` # CSV formatted output of all domains in the organization including their email and web scan statuses. toCsv: String + userHasPermission: Boolean + # The domains which are associated with this organization. domains( # Ordering options for domain connections. @@ -766,6 +804,9 @@ export const getTypeNames = () => gql` # String used to search for domains. search: String + # Filters used to limit domains returned. + filters: [DomainFilter] + # Returns the items in the list that come after the specified cursor. after: String @@ -787,6 +828,9 @@ export const getTypeNames = () => gql` # String used to search for affiliated users. search: String + # Exclude (false) or include only (true) pending affiliations in the results. + includePending: Boolean + # Returns the items in the list that come after the specified cursor. after: String @@ -823,6 +867,24 @@ export const getTypeNames = () => gql` # Summary based on DMARC phases for a given organization. dmarcPhase: CategorizedSummary + + # Summary based on SSL scan results for a given organization. + ssl: CategorizedSummary + + # Summary based on HTTPS and HSTS scan results for a given organization. + webConnections: CategorizedSummary + + # Summary based on SPF scan results for a given organization. + spf: CategorizedSummary + + # Summary based on DKIM scan results for a given organization. + dkim: CategorizedSummary + + # Summary based on HTTPS scan results for a given organization that includes domains marked as hidden. + httpsIncludeHidden: CategorizedSummary + + # Summary based on HTTPS scan results for a given organization that includes domains marked as hidden. + dmarcIncludeHidden: CategorizedSummary } # This object contains the list of different categories for pre-computed @@ -880,6 +942,9 @@ export const getTypeNames = () => gql` # Properties by which domain connections can be ordered. enum DomainOrderField { + # Order domains by certificates status. + CERTIFICATES_STATUS + # Order domains by ciphers status. CIPHERS_STATUS @@ -909,6 +974,75 @@ export const getTypeNames = () => gql` # Order domains by spf status. SPF_STATUS + + # Order domains by tags. + TAGS + } + + # This object is used to provide filtering options when querying org-claimed domains. + input DomainFilter { + # Category of filter to be applied. + filterCategory: DomainOrderField + + # First value equals or does not equal second value. + comparison: ComparisonEnums + + # Status type or tag label. + filterValue: filterValueEnums + } + + # + enum ComparisonEnums { + # + EQUAL + + # + NOT_EQUAL + } + + # + enum filterValueEnums { + # If the given check meets the passing requirements. + PASS + + # If the given check has flagged something that can provide information on the domain that aren't scan related. + INFO + + # If the given check does not meet the passing requirements + FAIL + + # English label for tagging domains as new to the system. + NEW + + # French label for tagging domains as new to the system. + NOUVEAU + + # Bilingual Label for tagging domains as a production environment. + PROD + + # English label for tagging domains as a staging environment. + STAGING + + # French label for tagging domains as a staging environment. + DEV + + # Bilingual label for tagging domains as a test environment. + TEST + + # Bilingual label for tagging domains as web-hosting. + WEB + + # English label for tagging domains that are not active. + INACTIVE + + # French label for tagging domains that are not active. + INACTIF + + # English label for tagging domains that are hidden. + HIDDEN + + # English label for tagging domains that are archived. + ARCHIVED } # A connection to a list of items. @@ -1164,7 +1298,7 @@ export const getTypeNames = () => gql` domain: String # The time when the scan was initiated. - timestamp: Date + timestamp: DateTime # String of the base domain the scan was run on. baseDomain: String @@ -1194,9 +1328,6 @@ export const getTypeNames = () => gql` dkim: DKIM } - # A date string, such as 2007-12-03, compliant with the "full-date" format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. - scalar Date - type MXRecord { # Hosts listed in the domain's MX record. hosts: [MXHost] @@ -1330,6 +1461,15 @@ export const getTypeNames = () => gql` # The compliance status for DKIM for the scanned domain. status: String + # List of positive tags for the scanned domain from this scan. + positiveTags: [GuidanceTag] + + # List of neutral tags for the scanned domain from this scan. + neutralTags: [GuidanceTag] + + # List of negative tags for the scanned domain from this scan. + negativeTags: [GuidanceTag] + # Individual scans results for each DKIM selector. selectors: [DKIMSelectorResult] } @@ -1416,7 +1556,7 @@ export const getTypeNames = () => gql` domain: String # The time when the scan was initiated. - timestamp: Date + timestamp: DateTime # Results of the web scan at each IP address. results: [WebScan] @@ -1424,9 +1564,6 @@ export const getTypeNames = () => gql` # Information for the TLS and HTTP connection scans on the given domain. type WebScan { - # The time when the scan was initiated. - timestamp: Date - # IP address for scan target. ipAddress: String @@ -1440,7 +1577,7 @@ export const getTypeNames = () => gql` # Results of TLS and HTTP connection scans on the given domain. type WebScanResult { # The time when the scan was initiated. - timestamp: Date + timestamp: DateTime # The result for the TLS scan for the scanned server. tlsResult: TLSResult @@ -1466,6 +1603,9 @@ export const getTypeNames = () => gql` # Whether or not the scanned server is vulnerable to heartbleed. heartbleedVulnerable: Boolean + # Whether or not the scanned server is vulnerable to heartbleed. + robotVulnerable: String + # Whether or not the scanned server is vulnerable to CCS injection. ccsInjectionVulnerable: Boolean @@ -1484,6 +1624,9 @@ export const getTypeNames = () => gql` # List of negative tags for the scanned server from this scan. negativeTags: [GuidanceTag] + # The compliance status of the certificate bundle for the scanned server from this scan. + certificateStatus: String + # The compliance status for TLS for the scanned server from this scan. sslStatus: String @@ -1745,7 +1888,7 @@ export const getTypeNames = () => gql` HSTS: Boolean } - # The "JSONObject" scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). + # The 'JSONObject' scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). scalar JSONObject # Ordering options for web connections. @@ -2302,6 +2445,9 @@ export const getTypeNames = () => gql` # Does the user want to see new features in progress. insideUser: Boolean + # Does the user want to receive update emails. + receiveUpdateEmails: Boolean + # Users affiliations to various organizations. affiliations( # Ordering options for affiliation connections. @@ -2413,7 +2559,7 @@ export const getTypeNames = () => gql` domain: DomainScalar # The last time that a scan was ran on this domain. - lastRan: Date + lastRan: DateTime # The domains scan status, based on the latest scan data. status: DomainStatus @@ -2629,6 +2775,9 @@ export const getTypeNames = () => gql` # This mutation allows admins or higher to remove users from any organizations they belong to. removeUserFromOrg(input: RemoveUserFromOrgInput!): RemoveUserFromOrgPayload + # This mutation allows users to request to join an organization. + requestOrgAffiliation(input: RequestOrgAffiliationInput!): RequestOrgAffiliationPayload + # This mutation allows a user to transfer org ownership to another user in the given org. transferOrgOwnership(input: TransferOrgOwnershipInput!): TransferOrgOwnershipPayload @@ -2637,6 +2786,9 @@ export const getTypeNames = () => gql` # given organization. updateUserRole(input: UpdateUserRoleInput!): UpdateUserRolePayload + # Mutation used to create multiple new domains for an organization. + addOrganizationsDomains(input: AddOrganizationsDomainsInput!): AddOrganizationsDomainsPayload + # Mutation used to create a new domain for an organization. createDomain(input: CreateDomainInput!): CreateDomainPayload @@ -2646,6 +2798,9 @@ export const getTypeNames = () => gql` # This mutation allows the removal of unused domains. removeDomain(input: RemoveDomainInput!): RemoveDomainPayload + # This mutation allows the removal of unused domains. + removeOrganizationsDomains(input: RemoveOrganizationsDomainsInput!): RemoveOrganizationsDomainsPayload + # This mutation is used to step a manual scan on a requested domain. requestScan(input: RequestScanInput!): RequestScanPayload @@ -2714,12 +2869,12 @@ export const getTypeNames = () => gql` } type InviteUserToOrgPayload { - # "InviteUserToOrgUnion" returning either a "InviteUserToOrgResult", or "InviteUserToOrgError" object. + # 'InviteUserToOrgUnion' returning either a 'InviteUserToOrgResult', or 'InviteUserToOrgError' object. result: InviteUserToOrgUnion clientMutationId: String } - # This union is used with the "InviteUserToOrg" mutation, allowing for users to invite user to their org, and support any errors that may occur + # This union is used with the 'InviteUserToOrg' mutation, allowing for users to invite user to their org, and support any errors that may occur union InviteUserToOrgUnion = AffiliationError | InviteUserToOrgResult # This object is used to inform the user if any errors occurred while executing affiliation mutations. @@ -2747,18 +2902,16 @@ export const getTypeNames = () => gql` # The organization you wish to invite the user to. orgId: ID! - # The language in which the email will be sent in. - preferredLang: LanguageEnums! clientMutationId: String } type LeaveOrganizationPayload { - # "LeaveOrganizationUnion" resolving to either a "LeaveOrganizationResult" or "AffiliationError". + # 'LeaveOrganizationUnion' resolving to either a 'LeaveOrganizationResult' or 'AffiliationError'. result: LeaveOrganizationUnion clientMutationId: String } - # This union is used with the "leaveOrganization" mutation, allowing for users to leave a given organization, and support any errors that may occur. + # This union is used with the 'leaveOrganization' mutation, allowing for users to leave a given organization, and support any errors that may occur. union LeaveOrganizationUnion = AffiliationError | LeaveOrganizationResult # This object is used to inform the user that they successful left a given organization. @@ -2774,12 +2927,12 @@ export const getTypeNames = () => gql` } type RemoveUserFromOrgPayload { - # "RemoveUserFromOrgUnion" returning either a "RemoveUserFromOrgResult", or "RemoveUserFromOrgError" object. + # 'RemoveUserFromOrgUnion' returning either a 'RemoveUserFromOrgResult', or 'RemoveUserFromOrgError' object. result: RemoveUserFromOrgUnion clientMutationId: String } - # This union is used with the "RemoveUserFromOrg" mutation, allowing for users to remove a user from their org, and support any errors that may occur + # This union is used with the 'RemoveUserFromOrg' mutation, allowing for users to remove a user from their org, and support any errors that may occur union RemoveUserFromOrgUnion = AffiliationError | RemoveUserFromOrgResult # This object is used to inform the user of the removal status. @@ -2800,13 +2953,25 @@ export const getTypeNames = () => gql` clientMutationId: String } + type RequestOrgAffiliationPayload { + # 'InviteUserToOrgUnion' returning either a 'InviteUserToOrgResult', or 'InviteUserToOrgError' object. + result: InviteUserToOrgUnion + clientMutationId: String + } + + input RequestOrgAffiliationInput { + # The organization you wish to invite the user to. + orgId: ID! + clientMutationId: String + } + type TransferOrgOwnershipPayload { - # "TransferOrgOwnershipUnion" resolving to either a "TransferOrgOwnershipResult" or "AffiliationError". + # 'TransferOrgOwnershipUnion' resolving to either a 'TransferOrgOwnershipResult' or 'AffiliationError'. result: TransferOrgOwnershipUnion clientMutationId: String } - # This union is used with the "transferOrgOwnership" mutation, allowing for + # This union is used with the 'transferOrgOwnership' mutation, allowing for # users to transfer ownership of a given organization, and support any errors that may occur. union TransferOrgOwnershipUnion = AffiliationError | TransferOrgOwnershipResult @@ -2826,12 +2991,12 @@ export const getTypeNames = () => gql` } type UpdateUserRolePayload { - # "UpdateUserRoleUnion" returning either a "UpdateUserRoleResult", or "UpdateUserRoleError" object. + # 'UpdateUserRoleUnion' returning either a 'UpdateUserRoleResult', or 'UpdateUserRoleError' object. result: UpdateUserRoleUnion clientMutationId: String } - # This union is used with the "UpdateUserRole" mutation, allowing for users to update a users role in an org, and support any errors that may occur + # This union is used with the 'UpdateUserRole' mutation, allowing for users to update a users role in an org, and support any errors that may occur union UpdateUserRoleUnion = AffiliationError | UpdateUserRoleResult # This object is used to inform the user of the status of the role update. @@ -2855,16 +3020,16 @@ export const getTypeNames = () => gql` clientMutationId: String } - type CreateDomainPayload { - # "CreateDomainUnion" returning either a "Domain", or "CreateDomainError" object. - result: CreateDomainUnion + type AddOrganizationsDomainsPayload { + # 'BulkModifyDomainsUnion' returning either a 'DomainBulkResult', or 'DomainErrorType' object. + result: BulkModifyDomainsUnion clientMutationId: String } - # This union is used with the "CreateDomain" mutation, - # allowing for users to create a domain and add it to their org, - # and support any errors that may occur - union CreateDomainUnion = DomainError | Domain + # This union is used with the 'AddOrganizationsDomains' and 'RemoveOrganizationsDomains' mutation, + # allowing for users to add/remove multiple domains belonging to their org, + # and support any errors that may occur + union BulkModifyDomainsUnion = DomainError | DomainBulkResult # This object is used to inform the user if any errors occurred while using a domain mutation. type DomainError { @@ -2875,6 +3040,41 @@ export const getTypeNames = () => gql` description: String } + # This object is used to inform the user that no errors were encountered while mutating a domain. + type DomainBulkResult { + # Informs the user if the domain mutation was successful. + status: String + } + + input AddOrganizationsDomainsInput { + # The global id of the organization you wish to assign this domain to. + orgId: ID! + + # Url that you would like to be added to the database. + domains: [DomainScalar]! + + # New domains will be hidden. + hideNewDomains: Boolean + + # New domains will be tagged with NEW. + tagNewDomains: Boolean + + # Audit logs will be created. + audit: Boolean + clientMutationId: String + } + + type CreateDomainPayload { + # 'CreateDomainUnion' returning either a 'Domain', or 'CreateDomainError' object. + result: CreateDomainUnion + clientMutationId: String + } + + # This union is used with the 'CreateDomain' mutation, + # allowing for users to create a domain and add it to their org, + # and support any errors that may occur + union CreateDomainUnion = DomainError | Domain + input CreateDomainInput { # The global id of the organization you wish to assign this domain to. orgId: ID! @@ -2887,6 +3087,12 @@ export const getTypeNames = () => gql` # List of labelled tags users have applied to the domain. tags: [InputTag] + + # Value that determines if the domain is excluded from an organization's score. + hidden: Boolean + + # Value that determines if the domain is excluded from the scanning process. + archived: Boolean clientMutationId: String } @@ -2927,10 +3133,16 @@ export const getTypeNames = () => gql` # French label for tagging domains that are not active. INACTIF + + # English label for tagging domains that are hidden. + HIDDEN + + # English label for tagging domains that are archived. + ARCHIVED } type FavouriteDomainPayload { - # "CreateDomainUnion" returning either a "Domain", or "CreateDomainError" object. + # 'CreateDomainUnion' returning either a 'Domain', or 'CreateDomainError' object. result: CreateDomainUnion clientMutationId: String } @@ -2942,19 +3154,19 @@ export const getTypeNames = () => gql` } type RemoveDomainPayload { - # "RemoveDomainUnion" returning either a "DomainResultType", or "DomainErrorType" object. + # 'RemoveDomainUnion' returning either a 'DomainResultType', or 'DomainErrorType' object. result: RemoveDomainUnion! clientMutationId: String } - # This union is used with the "RemoveDomain" mutation, + # This union is used with the 'RemoveDomain' mutation, # allowing for users to remove a domain belonging to their org, # and support any errors that may occur union RemoveDomainUnion = DomainError | DomainResult - # This object is used to inform the user that no errors were encountered while removing a domain. + # This object is used to inform the user that no errors were encountered while mutating a domain. type DomainResult { - # Informs the user if the domain removal was successful. + # Informs the user if the domain mutation was successful. status: String # The domain that is being mutated. @@ -2973,6 +3185,27 @@ export const getTypeNames = () => gql` clientMutationId: String } + type RemoveOrganizationsDomainsPayload { + # 'BulkModifyDomainsUnion' returning either a 'DomainBulkResult', or 'DomainErrorType' object. + result: BulkModifyDomainsUnion! + clientMutationId: String + } + + input RemoveOrganizationsDomainsInput { + # Domains you wish to remove from the organization. + domains: [DomainScalar]! + + # The organization you wish to remove the domain from. + orgId: ID! + + # Domains will be archived. + archiveDomains: Boolean + + # Audit logs will be created. + audit: Boolean + clientMutationId: String + } + type RequestScanPayload { # Informs the user if the scan was dispatched successfully. status: String @@ -2986,7 +3219,7 @@ export const getTypeNames = () => gql` } type UnfavouriteDomainPayload { - # "RemoveDomainUnion" returning either a "DomainResultType", or "DomainErrorType" object. + # 'RemoveDomainUnion' returning either a 'DomainResultType', or 'DomainErrorType' object. result: RemoveDomainUnion! clientMutationId: String } @@ -2998,12 +3231,12 @@ export const getTypeNames = () => gql` } type UpdateDomainPayload { - # "UpdateDomainUnion" returning either a "Domain", or "DomainError" object. + # 'UpdateDomainUnion' returning either a 'Domain', or 'DomainError' object. result: UpdateDomainUnion clientMutationId: String } - # This union is used with the "UpdateDomain" mutation, + # This union is used with the 'UpdateDomain' mutation, # allowing for users to update a domain belonging to their org, # and support any errors that may occur union UpdateDomainUnion = DomainError | Domain @@ -3023,16 +3256,22 @@ export const getTypeNames = () => gql` # List of labelled tags users have applied to the domain. tags: [InputTag] + + # Value that determines if the domain is excluded from an organization's score. + hidden: Boolean + + # Value that determines if the domain is excluded from the scanning process. + archived: Boolean clientMutationId: String } type CreateOrganizationPayload { - # "CreateOrganizationUnion" returning either an "Organization", or "OrganizationError" object. + # 'CreateOrganizationUnion' returning either an 'Organization', or 'OrganizationError' object. result: CreateOrganizationUnion clientMutationId: String } - # This union is used with the "CreateOrganization" mutation, + # This union is used with the 'CreateOrganization' mutation, # allowing for users to create an organization, and support any errors that may occur union CreateOrganizationUnion = OrganizationError | Organization @@ -3091,12 +3330,12 @@ export const getTypeNames = () => gql` } type RemoveOrganizationPayload { - # "RemoveOrganizationUnion" returning either an "OrganizationResult", or "OrganizationError" object. + # 'RemoveOrganizationUnion' returning either an 'OrganizationResult', or 'OrganizationError' object. result: RemoveOrganizationUnion! clientMutationId: String } - # This union is used with the "RemoveOrganization" mutation, + # This union is used with the 'RemoveOrganization' mutation, # allowing for users to remove an organization they belong to, # and support any errors that may occur union RemoveOrganizationUnion = OrganizationError | OrganizationResult @@ -3117,12 +3356,12 @@ export const getTypeNames = () => gql` } type UpdateOrganizationPayload { - # "UpdateOrganizationUnion" returning either an "Organization", or "OrganizationError" object. + # 'UpdateOrganizationUnion' returning either an 'Organization', or 'OrganizationError' object. result: UpdateOrganizationUnion! clientMutationId: String } - # This union is used with the "UpdateOrganization" mutation, + # This union is used with the 'UpdateOrganization' mutation, # allowing for users to update an organization, and support any errors that may occur union UpdateOrganizationUnion = OrganizationError | Organization @@ -3175,12 +3414,12 @@ export const getTypeNames = () => gql` } type VerifyOrganizationPayload { - # "VerifyOrganizationUnion" returning either an "OrganizationResult", or "OrganizationError" object. + # 'VerifyOrganizationUnion' returning either an 'OrganizationResult', or 'OrganizationError' object. result: VerifyOrganizationUnion clientMutationId: String } - # This union is used with the "VerifyOrganization" mutation, + # This union is used with the 'VerifyOrganization' mutation, # allowing for super admins to verify an organization, # and support any errors that may occur union VerifyOrganizationUnion = OrganizationError | OrganizationResult @@ -3192,12 +3431,12 @@ export const getTypeNames = () => gql` } type AuthenticatePayload { - # Authenticate union returning either a "authResult" or "authenticateError" object. + # Authenticate union returning either a 'authResult' or 'authenticateError' object. result: AuthenticateUnion clientMutationId: String } - # This union is used with the "authenticate" mutation, allowing for the user to authenticate, and support any errors that may occur + # This union is used with the 'authenticate' mutation, allowing for the user to authenticate, and support any errors that may occur union AuthenticateUnion = AuthResult | AuthenticateError # An object used to return information when users sign up or authenticate. @@ -3228,12 +3467,12 @@ export const getTypeNames = () => gql` } type CloseAccountPayload { - # "CloseAccountUnion" returning either a "CloseAccountResult", or "CloseAccountError" object. + # 'CloseAccountUnion' returning either a 'CloseAccountResult', or 'CloseAccountError' object. result: CloseAccountUnion clientMutationId: String } - # This union is used for the "closeAccount" mutation, to support successful or errors that may occur. + # This union is used for the 'closeAccount' mutation, to support successful or errors that may occur. union CloseAccountUnion = CloseAccountResult | CloseAccountError # This object is used to inform the user of the status of closing their account. @@ -3258,12 +3497,12 @@ export const getTypeNames = () => gql` } type RefreshTokensPayload { - # Refresh tokens union returning either a "authResult" or "authenticateError" object. + # Refresh tokens union returning either a 'authResult' or 'authenticateError' object. result: RefreshTokensUnion clientMutationId: String } - # This union is used with the "refreshTokens" mutation, allowing for the user to refresh their tokens, and support any errors that may occur + # This union is used with the 'refreshTokens' mutation, allowing for the user to refresh their tokens, and support any errors that may occur union RefreshTokensUnion = AuthResult | AuthenticateError input RefreshTokensInput { @@ -3271,12 +3510,12 @@ export const getTypeNames = () => gql` } type RemovePhoneNumberPayload { - # "RemovePhoneNumberUnion" returning either a "RemovePhoneNumberResult", or "RemovePhoneNumberError" object. + # 'RemovePhoneNumberUnion' returning either a 'RemovePhoneNumberResult', or 'RemovePhoneNumberError' object. result: RemovePhoneNumberUnion clientMutationId: String } - # This union is used with the "RemovePhoneNumber" mutation, allowing for users to remove their phone number, and support any errors that may occur + # This union is used with the 'RemovePhoneNumber' mutation, allowing for users to remove their phone number, and support any errors that may occur union RemovePhoneNumberUnion = RemovePhoneNumberError | RemovePhoneNumberResult # This object is used to inform the user if any errors occurred while removing their phone number. @@ -3299,12 +3538,12 @@ export const getTypeNames = () => gql` } type ResetPasswordPayload { - # "ResetPasswordUnion" returning either a "ResetPasswordResult", or "ResetPasswordError" object. + # 'ResetPasswordUnion' returning either a 'ResetPasswordResult', or 'ResetPasswordError' object. result: ResetPasswordUnion clientMutationId: String } - # This union is used with the "ResetPassword" mutation, allowing for users to reset their password, and support any errors that may occur + # This union is used with the 'ResetPassword' mutation, allowing for users to reset their password, and support any errors that may occur union ResetPasswordUnion = ResetPasswordError | ResetPasswordResult # This object is used to inform the user if any errors occurred while resetting their password. @@ -3359,12 +3598,12 @@ export const getTypeNames = () => gql` } type SetPhoneNumberPayload { - # "SetPhoneNumberUnion" returning either a "SetPhoneNumberResult", or "SetPhoneNumberError" object. + # 'SetPhoneNumberUnion' returning either a 'SetPhoneNumberResult', or 'SetPhoneNumberError' object. result: SetPhoneNumberUnion clientMutationId: String } - # This union is used with the "setPhoneNumber" mutation, allowing for users to send a verification code to their phone, and support any errors that may occur + # This union is used with the 'setPhoneNumber' mutation, allowing for users to send a verification code to their phone, and support any errors that may occur union SetPhoneNumberUnion = SetPhoneNumberError | SetPhoneNumberResult # This object is used to inform the user if any errors occurred while setting a new phone number. @@ -3392,12 +3631,12 @@ export const getTypeNames = () => gql` } type SignInPayload { - # "SignInUnion" returning either a "regularSignInResult", "tfaSignInResult", or "signInError" object. + # 'SignInUnion' returning either a 'regularSignInResult', 'tfaSignInResult', or 'signInError' object. result: SignInUnion clientMutationId: String } - # This union is used with the "SignIn" mutation, allowing for multiple styles of logging in, and support any errors that may occur + # This union is used with the 'SignIn' mutation, allowing for multiple styles of logging in, and support any errors that may occur union SignInUnion = AuthResult | SignInError | TFASignInResult # This object is used to inform the user if any errors occurred during sign in. @@ -3441,12 +3680,12 @@ export const getTypeNames = () => gql` } type SignUpPayload { - # "SignUpUnion" returning either a "AuthResult", or "SignUpError" object. + # 'SignUpUnion' returning either a 'AuthResult', or 'SignUpError' object. result: SignUpUnion clientMutationId: String } - # This union is used with the "signUp" mutation, allowing for the user to sign up, and support any errors that may occur. + # This union is used with the 'signUp' mutation, allowing for the user to sign up, and support any errors that may occur. union SignUpUnion = AuthResult | SignUpError # This object is used to inform the user if any errors occurred during sign up. @@ -3483,12 +3722,12 @@ export const getTypeNames = () => gql` } type UpdateUserPasswordPayload { - # "UpdateUserPasswordUnion" returning either a "UpdateUserPasswordResultType", or "UpdateUserPasswordError" object. + # 'UpdateUserPasswordUnion' returning either a 'UpdateUserPasswordResultType', or 'UpdateUserPasswordError' object. result: UpdateUserPasswordUnion clientMutationId: String } - # This union is used with the "updateUserPassword" mutation, allowing for users to update their password, and support any errors that may occur + # This union is used with the 'updateUserPassword' mutation, allowing for users to update their password, and support any errors that may occur union UpdateUserPasswordUnion = UpdateUserPasswordError | UpdateUserPasswordResultType # This object is used to inform the user if any errors occurred while updating their password. @@ -3519,12 +3758,12 @@ export const getTypeNames = () => gql` } type UpdateUserProfilePayload { - # "UpdateUserProfileUnion" returning either a "UpdateUserProfileResult", or "UpdateUserProfileError" object. + # 'UpdateUserProfileUnion' returning either a 'UpdateUserProfileResult', or 'UpdateUserProfileError' object. result: UpdateUserProfileUnion clientMutationId: String } - # This union is used with the "updateUserProfile" mutation, allowing for users to update their profile, and support any errors that may occur + # This union is used with the 'updateUserProfile' mutation, allowing for users to update their profile, and support any errors that may occur union UpdateUserProfileUnion = UpdateUserProfileError | UpdateUserProfileResult # This object is used to inform the user if any errors occurred while updating their profile. @@ -3560,16 +3799,20 @@ export const getTypeNames = () => gql` # The updated boolean which represents if the user wants to see features in progress. insideUser: Boolean + + # The updated boolean which represents if the user wants to receive update emails. + receiveUpdateEmails: Boolean + clientMutationId: String } type VerifyAccountPayload { - # "VerifyAccountUnion" returning either a "VerifyAccountResult", or "VerifyAccountError" object. + # 'VerifyAccountUnion' returning either a 'VerifyAccountResult', or 'VerifyAccountError' object. result: VerifyAccountUnion clientMutationId: String } - # This union is used with the "verifyAccount" mutation, allowing for users to verify their account, and support any errors that may occur + # This union is used with the 'verifyAccount' mutation, allowing for users to verify their account, and support any errors that may occur union VerifyAccountUnion = VerifyAccountError | VerifyAccountResult # This object is used to inform the user if any errors occurred while verifying their account. @@ -3594,12 +3837,12 @@ export const getTypeNames = () => gql` } type verifyPhoneNumberPayload { - # "VerifyPhoneNumberUnion" returning either a "VerifyPhoneNumberResult", or "VerifyPhoneNumberError" object. + # 'VerifyPhoneNumberUnion' returning either a 'VerifyPhoneNumberResult', or 'VerifyPhoneNumberError' object. result: VerifyPhoneNumberUnion clientMutationId: String } - # This union is used with the "verifyPhoneNumber" mutation, allowing for users to verify their phone number, and support any errors that may occur + # This union is used with the 'verifyPhoneNumber' mutation, allowing for users to verify their phone number, and support any errors that may occur union VerifyPhoneNumberUnion = VerifyPhoneNumberError | VerifyPhoneNumberResult # This object is used to inform the user if any errors occurred while verifying their phone number. diff --git a/frontend/mocking/mocker.js b/frontend/mocking/mocker.js index dc3f288554..c06e31d3fc 100644 --- a/frontend/mocking/mocker.js +++ b/frontend/mocking/mocker.js @@ -336,7 +336,7 @@ const mocks = { const tagId = 'tag' + faker.datatype.number({ min: 1, max: 14 }) const tagName = 'TAG-' + faker.helpers.randomize(['missing', 'downgraded', 'bad-chain', 'short-age', 'certificate-expired']) - const guidance = faker.lorem.sentence() + // const guidance = faker.lorem.sentence() const refLinks = [...new Array(1)] const refLinksTech = [...new Array(1)] @@ -356,6 +356,58 @@ const mocks = { totalCount: numberOfEdges, } }, + MyTrackerResult: () => { + const domainCount = faker.datatype.number({ min: 0, max: 500 }) + const httpsPassCount = faker.datatype.number({ min: 0, max: domainCount }) + const httpsFailCount = domainCount - httpsPassCount + const httpsPassPercentage = (httpsPassCount / domainCount) * 100 + const httpsFailPercentage = 100 - httpsPassPercentage + const https = { + total: domainCount, + categories: [ + { + name: 'pass', + count: httpsPassCount, + percentage: httpsPassPercentage, + }, + { + name: 'fail', + count: httpsFailCount, + percentage: httpsFailPercentage, + }, + ], + } + + const mailPassCount = faker.datatype.number({ min: 0, max: domainCount }) + const mailFailCount = domainCount - mailPassCount + const mailPassPercentage = (mailPassCount / domainCount) * 100 + const mailFailPercentage = 100 - mailPassPercentage + const dmarc = { + total: domainCount, + categories: [ + { + name: 'pass', + count: mailPassCount, + percentage: mailPassPercentage, + }, + { + name: 'fail', + count: mailFailCount, + percentage: mailFailPercentage, + }, + ], + } + + const dmarcPhase = dmarcPhaseSummaryMock() + return { + domainCount, + domains: { + edges: [...new Array(domainCount)], + totalCount: domainCount, + }, + summaries: { https, dmarc, dmarcPhase }, + } + }, Organization: () => { const name = faker.company.companyName() const slug = faker.helpers.slugify(name) @@ -600,6 +652,9 @@ const schemaWithMocks = addMocksToSchema({ findMyOrganizations: (_, args, _context, resolveInfo) => { return getConnectionObject(store, args, resolveInfo) }, + findMyTracker: (_, _args, _context, _resolveInfo) => { + return store.get('MyTrackerResult') + }, dmarcPhaseSummary: (_, _args, _context, _resolveInfo) => { return dmarcPhaseSummaryMock() }, diff --git a/frontend/src/admin/AdminDomainCard.js b/frontend/src/admin/AdminDomainCard.js index 7fbd63533c..aa89a109d1 100644 --- a/frontend/src/admin/AdminDomainCard.js +++ b/frontend/src/admin/AdminDomainCard.js @@ -1,29 +1,15 @@ import React from 'react' import { Trans } from '@lingui/macro' import { array, bool, string } from 'prop-types' -import { - Flex, - Grid, - Link, - ListItem, - Stack, - Tag, - TagLabel, - Text, -} from '@chakra-ui/react' +import { Flex, Grid, Link, ListItem, Stack, Tag, TagLabel, Text } from '@chakra-ui/react' import { ExternalLinkIcon } from '@chakra-ui/icons' import { sanitizeUrl } from '../utilities/sanitizeUrl' -import { ABTestingWrapper } from '../app/ABTestWrapper' -import { ABTestVariant } from '../app/ABTestVariant' -export function AdminDomainCard({ url, tags, isHidden, isArchived, ...rest }) { +export function AdminDomainCard({ url, tags, isHidden, isArchived, rcode, ...rest }) { return ( - + Domain: @@ -41,49 +27,34 @@ export function AdminDomainCard({ url, tags, isHidden, isArchived, ...rest }) { - - - - {tags?.map((tag, idx) => { - return ( - - {tag} - - ) - })} - {isHidden && ( - - - Hidden - - - )} - {isArchived && ( - - - Archived - - - )} - - - + + {tags?.map((tag, idx) => { + return ( + + {tag} + + ) + })} + {rcode === 'NXDOMAIN' && ( + + NXDOMAIN + + )} + {isHidden && ( + + + Hidden + + + )} + {isArchived && ( + + + Archived + + + )} + ) @@ -93,4 +64,5 @@ AdminDomainCard.propTypes = { tags: array, isHidden: bool, isArchived: bool, + rcode: string, } diff --git a/frontend/src/admin/AdminDomainModal.js b/frontend/src/admin/AdminDomainModal.js index 47ed65deb0..8111b7795c 100644 --- a/frontend/src/admin/AdminDomainModal.js +++ b/frontend/src/admin/AdminDomainModal.js @@ -30,28 +30,15 @@ import { Tooltip, useToast, } from '@chakra-ui/react' -import { - AddIcon, - MinusIcon, - QuestionOutlineIcon, - SmallAddIcon, -} from '@chakra-ui/icons' +import { AddIcon, MinusIcon, QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons' import { array, bool, func, number, object, string } from 'prop-types' import { Field, FieldArray, Formik } from 'formik' import { useMutation } from '@apollo/client' import { DomainField } from '../components/fields/DomainField' import { CREATE_DOMAIN, UPDATE_DOMAIN } from '../graphql/mutations' -import { ABTestingWrapper } from '../app/ABTestWrapper' -import { ABTestVariant } from '../app/ABTestVariant' -export function AdminDomainModal({ - isOpen, - onClose, - validationSchema, - orgId, - ...props -}) { +export function AdminDomainModal({ isOpen, onClose, validationSchema, orgId, ...props }) { const { editingDomainId, editingDomainUrl, @@ -84,9 +71,7 @@ export function AdminDomainModal({ if (createDomain.result.__typename === 'Domain') { toast({ title: i18n._(t`Domain added`), - description: i18n._( - t`${createDomain.result.domain} was added to ${orgSlug}`, - ), + description: i18n._(t`${createDomain.result.domain} was added to ${orgSlug}`), status: 'success', duration: 9000, isClosable: true, @@ -178,9 +163,7 @@ export function AdminDomainModal({ const stringValues = values?.map((label) => { return label[i18n.locale] }) - const difference = tagOptions.filter( - (label) => !stringValues?.includes(label[i18n.locale]), - ) + const difference = tagOptions.filter((label) => !stringValues?.includes(label[i18n.locale])) return difference?.map((label, idx) => { return ( + - {({ - handleSubmit, - handleChange, - isSubmitting, - values, - errors, - touched, - }) => ( + {({ handleSubmit, handleChange, isSubmitting, values, errors, touched }) => (
- {mutation === 'update' ? ( - Edit Domain Details - ) : ( - Add Domain Details - )} + {mutation === 'update' ? Edit Domain Details : Add Domain Details} - + - + } @@ -312,11 +270,7 @@ export function AdminDomainModal({ onClick={() => arrayHelpers.remove(index)} aria-label="remove-dkim-selector" /> - + {({ field }) => ( - {errors && - errors.selectors && - errors.selectors[index]} + {errors && errors.selectors && errors.selectors[index]} @@ -348,114 +300,89 @@ export function AdminDomainModal({ )} /> - - - ( - - Tags: - - {values.tags?.map((label, idx) => { - return ( - - {label[i18n.locale]} - arrayHelpers.remove(idx)} - aria-label={`remove-tag-${ - label[i18n.locale] - }`} - /> - - ) - })} - - - - {addableTags(values.tags, arrayHelpers)} - - - )} + ( + + Tags: + + {values.tags?.map((label, idx) => { + return ( + + {label[i18n.locale]} + arrayHelpers.remove(idx)} + aria-label={`remove-tag-${label[i18n.locale]}`} + /> + + ) + })} + + + + {addableTags(values.tags, arrayHelpers)} + + + )} + /> + + + + + + + Hide domain + + + {permission === 'SUPER_ADMIN' && ( + - Hide domain + Archive domain - {permission === 'SUPER_ADMIN' && ( - - - - - - - - Archive domain - - - - - {orgCount > 0 ? ( - - Note: This will affect results for {orgCount}{' '} - organizations - - ) : ( - - Note: This could affect results for multiple - organizations - - )} - - - )} - - - - Please allow up to 24 hours for summaries to reflect - any changes. - + + {orgCount > 0 ? ( + Note: This will affect results for {orgCount} organizations + ) : ( + Note: This could affect results for multiple organizations + )} - - + + )} + + Please allow up to 24 hours for summaries to reflect any changes. + - diff --git a/frontend/src/admin/AdminDomains.js b/frontend/src/admin/AdminDomains.js index 6a86ca4c3c..8340a83d40 100644 --- a/frontend/src/admin/AdminDomains.js +++ b/frontend/src/admin/AdminDomains.js @@ -48,8 +48,12 @@ export function AdminDomains({ orgSlug, domainsPerPage, orgId, permission }) { const { i18n } = useLingui() const [newDomainUrl, setNewDomainUrl] = useState('') - const [selectedRemoveDomainUrl, setSelectedRemoveDomainUrl] = useState() - const [selectedRemoveDomainId, setSelectedRemoveDomainId] = useState() + const [selectedRemoveProps, setSelectedRemoveProps] = useState({ + domain: '', + domainId: '', + rcode: '', + }) + // const [selectedRemoveDomainId, setSelectedRemoveDomainId] = useState() const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('') const [modalProps, setModalProps] = useState({ hidden: false, @@ -61,34 +65,19 @@ export function AdminDomains({ orgSlug, domainsPerPage, orgId, permission }) { editingDomainUrl: '', }) - const { - isOpen: updateIsOpen, - onOpen: updateOnOpen, - onClose: updateOnClose, - } = useDisclosure() - const { - isOpen: removeIsOpen, - onOpen: removeOnOpen, - onClose: removeOnClose, - } = useDisclosure() + const { isOpen: updateIsOpen, onOpen: updateOnOpen, onClose: updateOnClose } = useDisclosure() + const { isOpen: removeIsOpen, onOpen: removeOnOpen, onClose: removeOnClose } = useDisclosure() - const { - loading, - isLoadingMore, - error, - nodes, - next, - previous, - hasNextPage, - hasPreviousPage, - } = usePaginatedCollection({ - fetchForward: FORWARD, - recordsPerPage: domainsPerPage, - variables: { orgSlug, search: debouncedSearchTerm }, - relayRoot: 'findOrganizationBySlug.domains', - fetchPolicy: 'cache-and-network', - nextFetchPolicy: 'cache-first', - }) + const { loading, isLoadingMore, error, nodes, next, previous, hasNextPage, hasPreviousPage } = usePaginatedCollection( + { + fetchForward: FORWARD, + recordsPerPage: domainsPerPage, + variables: { orgSlug, search: debouncedSearchTerm }, + relayRoot: 'findOrganizationBySlug.domains', + fetchPolicy: 'cache-and-network', + nextFetchPolicy: 'cache-first', + }, + ) const memoizedSetDebouncedSearchTermCallback = useCallback(() => { setDebouncedSearchTerm(newDomainUrl) @@ -157,26 +146,14 @@ export function AdminDomains({ orgSlug, domainsPerPage, orgId, permission }) { )} > - {( - { - id: domainId, - domain, - selectors, - claimTags, - hidden, - archived, - organizations, - }, - index, - ) => ( + {({ id: domainId, domain, selectors, claimTags, hidden, archived, rcode, organizations }, index) => ( { - setSelectedRemoveDomainUrl(domain) - setSelectedRemoveDomainId(domainId) + setSelectedRemoveProps({ domain, domainId, rcode }) removeOnOpen() }} variant="danger" @@ -210,6 +187,7 @@ export function AdminDomains({ orgSlug, domainsPerPage, orgId, permission }) { tags={claimTags} isHidden={hidden} isArchived={archived} + rcode={rcode} locale={i18n.locale} flexGrow={1} fontSize={{ base: '75%', sm: '100%' }} @@ -241,21 +219,10 @@ export function AdminDomains({ orgSlug, domainsPerPage, orgId, permission }) { }} > - + Search: - + @@ -267,12 +234,7 @@ export function AdminDomains({ orgSlug, domainsPerPage, orgId, permission }) { onChange={(e) => setNewDomainUrl(e.target.value)} /> - @@ -300,16 +262,12 @@ export function AdminDomains({ orgSlug, domainsPerPage, orgId, permission }) { permission={permission} /> - + { removeDomain({ variables: { - domainId: selectedRemoveDomainId, + domainId: selectedRemoveProps.domainId, orgId: orgId, reason: values.reason, }, }) }} > - {({ handleSubmit, isSubmitting, handleChange }) => ( + {({ values, handleSubmit, isSubmitting, handleChange }) => ( Remove Domain @@ -335,15 +293,13 @@ export function AdminDomains({ orgSlug, domainsPerPage, orgId, permission }) { Confirm removal of domain: - {selectedRemoveDomainUrl} + {selectedRemoveProps.domain} - A domain may only be removed for one of the reasons - below. For a domain to no longer exist, it must be - removed from the DNS. If you need to remove this domain - for a different reason, please contact TBS Cyber - Security. + A domain may only be removed for one of the reasons below. For a domain to no longer exist, it + must be removed from the DNS. If you need to remove this domain for a different reason, please + contact TBS Cyber Security. @@ -353,23 +309,20 @@ export function AdminDomains({ orgSlug, domainsPerPage, orgId, permission }) { @@ -377,12 +330,7 @@ export function AdminDomains({ orgSlug, domainsPerPage, orgId, permission }) { - diff --git a/frontend/src/admin/AdminPanel.js b/frontend/src/admin/AdminPanel.js index b445913d39..9fd4cb055c 100644 --- a/frontend/src/admin/AdminPanel.js +++ b/frontend/src/admin/AdminPanel.js @@ -1,12 +1,5 @@ import React from 'react' -import { - Stack, - Tab, - TabList, - TabPanel, - TabPanels, - Tabs, -} from '@chakra-ui/react' +import { Stack, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react' import { Trans } from '@lingui/macro' import { string } from 'prop-types' import { ErrorBoundary } from 'react-error-boundary' @@ -16,8 +9,6 @@ import { UserList } from './UserList' import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' import { AuditLogTable } from './AuditLogTable' -import { ABTestingWrapper } from '../app/ABTestWrapper' -import { ABTestVariant } from '../app/ABTestVariant' export function AdminPanel({ activeMenu, orgSlug, permission, orgId }) { return ( @@ -30,29 +21,21 @@ export function AdminPanel({ activeMenu, orgSlug, permission, orgId }) { Users - - - - Activity - - - + + Activity + - + - - - - - - - + + + diff --git a/frontend/src/admin/AuditLogTable.js b/frontend/src/admin/AuditLogTable.js index b057a674a6..a842f17b43 100644 --- a/frontend/src/admin/AuditLogTable.js +++ b/frontend/src/admin/AuditLogTable.js @@ -37,30 +37,21 @@ export function AuditLogTable({ orgId = null }) { setDebouncedSearchTerm(searchTerm) }, [searchTerm]) useDebouncedFunction(memoizedSetDebouncedSearchTermCallback, 500) - const { - loading, - isLoadingMore, - error, - nodes, - next, - previous, - resetToFirstPage, - hasNextPage, - hasPreviousPage, - } = usePaginatedCollection({ - fetchForward: AUDIT_LOGS, - recordsPerPage: logsPerPage, - relayRoot: 'findAuditLogs', - variables: { - orgId, - orderBy: { field: orderField, direction: orderDirection }, - search: debouncedSearchTerm, - filters: { resource: activeResourceFilters, action: activeActionFilters }, - }, - fetchPolicy: 'cache-and-network', - nextFetchPolicy: 'cache-first', - errorPolicy: 'ignore', // allow partial success - }) + const { loading, isLoadingMore, error, nodes, next, previous, resetToFirstPage, hasNextPage, hasPreviousPage } = + usePaginatedCollection({ + fetchForward: AUDIT_LOGS, + recordsPerPage: logsPerPage, + relayRoot: 'findAuditLogs', + variables: { + orgId, + orderBy: { field: orderField, direction: orderDirection }, + search: debouncedSearchTerm, + filters: { resource: activeResourceFilters, action: activeActionFilters }, + }, + fetchPolicy: 'cache-and-network', + nextFetchPolicy: 'cache-first', + errorPolicy: 'ignore', // allow partial success + }) if (error) { return @@ -83,6 +74,8 @@ export function AuditLogTable({ orgId = null }) { { value: 'UPDATE', text: t`Update` }, { value: 'REMOVE', text: t`Remove` }, { value: 'DELETE', text: t`Delete` }, + { value: 'EXPORT', text: t`Export` }, + { value: 'SCAN', text: t`Scan` }, ] let logTable @@ -131,64 +124,50 @@ export function AuditLogTable({ orgId = null }) { - {nodes.map( - ({ id, timestamp, initiatedBy, action, target, reason }) => { - const formatTimestamp = (ts) => { - const dateTime = ts.split('T') - return dateTime[0] + ', ' + dateTime[1].substring(0, 5) + {nodes.map(({ id, timestamp, initiatedBy, action, target, reason }) => { + const formatTimestamp = (ts) => { + const dateTime = ts.split('T') + return dateTime[0] + ', ' + dateTime[1].substring(0, 5) + } + const resourceType = resourceFilters.find(({ value }) => target.resourceType.toUpperCase() === value) + action = actionFilters.find(({ value }) => action.toUpperCase() === value) + if (typeof reason !== 'undefined') { + if (reason === 'NONEXISTENT') { + reason = This domain no longer exists + } else if (reason === 'WRONG_ORG') { + reason = This domain does not belong to this organization } - const resourceType = resourceFilters.find( - ({ value }) => target.resourceType.toUpperCase() === value, - ) - action = actionFilters.find( - ({ value }) => action.toUpperCase() === value, - ) - if (typeof reason !== 'undefined') { - if (reason === 'NONEXISTENT') { - reason = This domain no longer exists - } else if (reason === 'WRONG_ORG') { - reason = ( - - This domain does not belong to this organization - - ) - } - } - return ( - - {formatTimestamp(timestamp)} - {initiatedBy?.userName} - {action?.text.toUpperCase()} - {resourceType?.text.toUpperCase()} - {target?.resource} - {target?.organization?.name} - - {target?.updatedProperties?.map( - ({ name, oldValue, newValue }) => { - return ( - - - Name: {name} - - - Old Value: {oldValue} - - - New Value: {newValue} - - {target?.updatedProperties?.length > 1 && ( - - )} - - ) - }, - )} - - {reason} - - ) - }, - )} + } + return ( + + {formatTimestamp(timestamp)} + {initiatedBy?.userName} + {action?.text.toUpperCase()} + {resourceType?.text.toUpperCase()} + {target?.resource} + {target?.organization?.name} + + {target?.updatedProperties?.map(({ name, oldValue, newValue }) => { + return ( + + + Name: {name} + + + Old Value: {oldValue} + + + New Value: {newValue} + + {target?.updatedProperties?.length > 1 && } + + ) + })} + + {reason} + + ) + })} @@ -227,33 +206,16 @@ export function AuditLogTable({ orgId = null }) { borderRadius="full" borderWidth="1px" borderColor="gray.900" - bg={ - activeResourceFilters.indexOf(value) < 0 - ? 'gray.50' - : 'gray.900' - } - color={ - activeResourceFilters.indexOf(value) < 0 - ? 'gray.900' - : 'gray.50' - } + bg={activeResourceFilters.indexOf(value) < 0 ? 'gray.50' : 'gray.900'} + color={activeResourceFilters.indexOf(value) < 0 ? 'gray.900' : 'gray.50'} as="button" - _hover={ - activeResourceFilters.indexOf(value) < 0 - ? { bg: 'gray.200' } - : { bg: 'gray.500' } - } + _hover={activeResourceFilters.indexOf(value) < 0 ? { bg: 'gray.200' } : { bg: 'gray.500' }} onClick={() => { let optionIdx = activeResourceFilters.indexOf(value) if (optionIdx < 0) { - setActiveResourceFilters([ - ...activeResourceFilters, - value, - ]) + setActiveResourceFilters([...activeResourceFilters, value]) } else { - setActiveResourceFilters( - activeResourceFilters.filter((tag) => tag !== value), - ) + setActiveResourceFilters(activeResourceFilters.filter((tag) => tag !== value)) } }} > @@ -275,30 +237,16 @@ export function AuditLogTable({ orgId = null }) { borderRadius="full" borderWidth="1px" borderColor="gray.900" - bg={ - activeActionFilters.indexOf(value) < 0 - ? 'gray.50' - : 'gray.900' - } - color={ - activeActionFilters.indexOf(value) < 0 - ? 'gray.900' - : 'gray.50' - } + bg={activeActionFilters.indexOf(value) < 0 ? 'gray.50' : 'gray.900'} + color={activeActionFilters.indexOf(value) < 0 ? 'gray.900' : 'gray.50'} as="button" - _hover={ - activeActionFilters.indexOf(value) < 0 - ? { bg: 'gray.200' } - : { bg: 'gray.500' } - } + _hover={activeActionFilters.indexOf(value) < 0 ? { bg: 'gray.200' } : { bg: 'gray.500' }} onClick={() => { let optionIdx = activeActionFilters.indexOf(value) if (optionIdx < 0) { setActiveActionFilters([...activeActionFilters, value]) } else { - setActiveActionFilters( - activeActionFilters.filter((tag) => tag !== value), - ) + setActiveActionFilters(activeActionFilters.filter((tag) => tag !== value)) } }} > diff --git a/frontend/src/admin/SuperAdminUserList.js b/frontend/src/admin/SuperAdminUserList.js index d5485292cf..0f24ad3cd3 100644 --- a/frontend/src/admin/SuperAdminUserList.js +++ b/frontend/src/admin/SuperAdminUserList.js @@ -148,7 +148,7 @@ export function SuperAdminUserList({ permission }) { { value: 'USER_USERNAME', text: t`Email` }, { value: 'USER_DISPLAYNAME', text: t`Display Name` }, { value: 'USER_EMAIL_VALIDATED', text: t`Verified` }, - { value: 'USER_INSIDER', text: t`Insider` }, + { value: 'USER_INSIDER', text: t`Inside User` }, ] const userList = @@ -340,15 +340,17 @@ export function SuperAdminUserList({ permission }) { > Verified - - Insider - + {insideUser && ( + + Inside User + + )} Affiliations: {totalCount} diff --git a/frontend/src/admin/UserList.js b/frontend/src/admin/UserList.js index ae4f297745..e7f1c775c6 100644 --- a/frontend/src/admin/UserList.js +++ b/frontend/src/admin/UserList.js @@ -18,7 +18,6 @@ import { number, string } from 'prop-types' import { UserListModal } from './UserListModal' -import { REMOVE_USER_FROM_ORG } from '../graphql/mutations' import { PAGINATED_ORG_AFFILIATIONS_ADMIN_PAGE as FORWARD } from '../graphql/queries' import { UserCard } from '../components/UserCard' import { LoadingMessage } from '../components/LoadingMessage' @@ -26,8 +25,9 @@ import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' import { RelayPaginationControls } from '../components/RelayPaginationControls' import { usePaginatedCollection } from '../utilities/usePaginatedCollection' import { useDebouncedFunction } from '../utilities/useDebouncedFunction' +import { bool } from 'prop-types' -export function UserList({ permission, orgSlug, usersPerPage, orgId }) { +export function UserList({ includePending, permission, orgSlug, usersPerPage, orgId }) { const [mutation, setMutation] = useState() const [addedUserName, setAddedUserName] = useState('') const [selectedRemoveUser, setSelectedRemoveUser] = useState({ @@ -47,23 +47,16 @@ export function UserList({ permission, orgSlug, usersPerPage, orgId }) { useDebouncedFunction(memoizedSetDebouncedSearchTermCallback, 500) - const { - loading, - isLoadingMore, - error, - nodes, - next, - previous, - hasNextPage, - hasPreviousPage, - } = usePaginatedCollection({ - fetchForward: FORWARD, - recordsPerPage: usersPerPage, - variables: { orgSlug, search: debouncedSearchUser }, - relayRoot: 'findOrganizationBySlug.affiliations', - fetchPolicy: 'cache-and-network', - nextFetchPolicy: 'cache-first', - }) + const { loading, isLoadingMore, error, nodes, next, previous, hasNextPage, hasPreviousPage } = usePaginatedCollection( + { + fetchForward: FORWARD, + recordsPerPage: usersPerPage, + variables: { orgSlug, search: debouncedSearchUser, includePending }, + relayRoot: 'findOrganizationBySlug.affiliations', + fetchPolicy: 'cache-and-network', + nextFetchPolicy: 'cache-first', + }, + ) if (error) return @@ -133,26 +126,11 @@ export function UserList({ permission, orgSlug, usersPerPage, orgId }) { onOpen() }} > - - + + Search: - + @@ -164,11 +142,7 @@ export function UserList({ permission, orgSlug, usersPerPage, orgId }) { /> - @@ -190,9 +164,7 @@ export function UserList({ permission, orgSlug, usersPerPage, orgId }) { isOpen={isOpen} onClose={onClose} orgId={orgId} - editingUserName={ - mutation === 'remove' ? selectedRemoveUser.userName : editingUserName - } + editingUserName={mutation === 'remove' ? selectedRemoveUser.userName : editingUserName} editingUserRole={editingUserRole} editingUserId={selectedRemoveUser.id} orgSlug={orgSlug} @@ -208,4 +180,5 @@ UserList.propTypes = { permission: string, usersPerPage: number, orgId: string.isRequired, + includePending: bool, } diff --git a/frontend/src/admin/UserListModal.js b/frontend/src/admin/UserListModal.js index 972505509b..c4904503e5 100644 --- a/frontend/src/admin/UserListModal.js +++ b/frontend/src/admin/UserListModal.js @@ -20,11 +20,7 @@ import { useMutation } from '@apollo/client' import { bool, func, string } from 'prop-types' import { EmailField } from '../components/fields/EmailField' -import { - UPDATE_USER_ROLE, - INVITE_USER_TO_ORG, - REMOVE_USER_FROM_ORG, -} from '../graphql/mutations' +import { UPDATE_USER_ROLE, INVITE_USER_TO_ORG, REMOVE_USER_FROM_ORG } from '../graphql/mutations' import { createValidationSchema } from '../utilities/fieldRequirements' export function UserListModal({ @@ -42,152 +38,139 @@ export function UserListModal({ const toast = useToast() const initialFocusRef = useRef() - const [addUser, { loading: _addUserLoading }] = useMutation( - INVITE_USER_TO_ORG, - { - refetchQueries: ['PaginatedOrgAffiliations', 'FindAuditLogs'], - awaitRefetchQueries: true, + const refetchQueriesValues = { + refetchQueries: ['PaginatedOrgAffiliations', 'FindAuditLogs', 'FindMyUsers'], + awaitRefetchQueries: true, + } - onError(error) { + const [addUser, { loading: _addUserLoading }] = useMutation(INVITE_USER_TO_ORG, { + ...refetchQueriesValues, + onError(error) { + toast({ + title: t`An error occurred.`, + description: error.message, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + }, + onCompleted({ inviteUserToOrg }) { + if (inviteUserToOrg.result.__typename === 'InviteUserToOrgResult') { + toast({ + title: t`User invited`, + description: t`Email invitation sent`, + status: 'success', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + onClose() + } else if (inviteUserToOrg.result.__typename === 'AffiliationError') { + toast({ + title: t`Unable to invite user.`, + description: inviteUserToOrg.result.description, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + } else { toast({ - title: t`An error occurred.`, - description: error.message, + title: t`Incorrect send method received.`, + description: t`Incorrect inviteUserToOrg.result typename.`, status: 'error', duration: 9000, isClosable: true, position: 'top-left', }) - }, - onCompleted({ inviteUserToOrg }) { - if (inviteUserToOrg.result.__typename === 'InviteUserToOrgResult') { - toast({ - title: t`User invited`, - description: t`Email invitation sent`, - status: 'success', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - onClose() - } else if (inviteUserToOrg.result.__typename === 'AffiliationError') { - toast({ - title: t`Unable to invite user.`, - description: inviteUserToOrg.result.description, - status: 'error', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - } else { - toast({ - title: t`Incorrect send method received.`, - description: t`Incorrect inviteUserToOrg.result typename.`, - status: 'error', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - } - }, + } }, - ) + }) - const [updateUserRole, { loading: _updateLoading, error: _updateError }] = - useMutation(UPDATE_USER_ROLE, { - refetchQueries: ['FindMyUsers', 'FindAuditLogs'], - awaitRefetchQueries: true, - - onError(updateError) { + const [updateUserRole, { loading: _updateLoading, error: _updateError }] = useMutation(UPDATE_USER_ROLE, { + ...refetchQueriesValues, + onError(updateError) { + toast({ + title: updateError.message, + description: t`Unable to change user role, please try again.`, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + }, + onCompleted({ updateUserRole }) { + if (updateUserRole.result.__typename === 'UpdateUserRoleResult') { + toast({ + title: t`Role updated`, + description: t`The user's role has been successfully updated`, + status: 'success', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + onClose() + } else if (updateUserRole.result.__typename === 'AffiliationError') { toast({ - title: updateError.message, - description: t`Unable to change user role, please try again.`, + title: t`Unable to update user role.`, + description: updateUserRole.result.description, status: 'error', duration: 9000, isClosable: true, position: 'top-left', }) - }, - onCompleted({ updateUserRole }) { - if (updateUserRole.result.__typename === 'UpdateUserRoleResult') { - toast({ - title: t`Role updated`, - description: t`The user's role has been successfully updated`, - status: 'success', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - onClose() - } else if (updateUserRole.result.__typename === 'AffiliationError') { - toast({ - title: t`Unable to update user role.`, - description: updateUserRole.result.description, - status: 'error', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - } else { - toast({ - title: t`Incorrect send method received.`, - description: t`Incorrect updateUserRole.result typename.`, - status: 'error', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - } - }, - }) - - const [removeUser, { loading: _removeUserLoading }] = useMutation( - REMOVE_USER_FROM_ORG, - { - refetchQueries: ['FindMyUsers', 'FindAuditLogs'], - awaitRefetchQueries: true, + } else { + toast({ + title: t`Incorrect send method received.`, + description: t`Incorrect updateUserRole.result typename.`, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + } + }, + }) - onError(error) { + const [removeUser, { loading: _removeUserLoading }] = useMutation(REMOVE_USER_FROM_ORG, { + ...refetchQueriesValues, + onError(error) { + toast({ + title: t`An error occurred.`, + description: error.message, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + }, + onCompleted({ removeUserFromOrg }) { + if (removeUserFromOrg.result.__typename === 'RemoveUserFromOrgResult') { + onClose() toast({ - title: t`An error occurred.`, - description: error.message, + title: t`User removed.`, + description: t`Successfully removed user ${removeUserFromOrg.result.user.userName}.`, + status: 'success', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + } else if (removeUserFromOrg.result.__typename === 'AffiliationError') { + toast({ + title: t`Unable to remove user.`, + description: removeUserFromOrg.result.description, status: 'error', duration: 9000, isClosable: true, position: 'top-left', }) - }, - onCompleted({ removeUserFromOrg }) { - if (removeUserFromOrg.result.__typename === 'RemoveUserFromOrgResult') { - onClose() - toast({ - title: t`User removed.`, - description: t`Successfully removed user ${removeUserFromOrg.result.user.userName}.`, - status: 'success', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - } else if (removeUserFromOrg.result.__typename === 'AffiliationError') { - toast({ - title: t`Unable to remove user.`, - description: removeUserFromOrg.result.description, - status: 'error', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - } - }, + } }, - ) + }) return ( - + - {(editingUserRole === 'USER' || - (permission === 'SUPER_ADMIN' && - editingUserRole === 'ADMIN')) && ( + {editingUserRole === 'PENDING' && } + {(['PENDING', 'USER'].includes(editingUserRole) || + (['ADMIN', 'OWNER', 'SUPER_ADMIN'].includes(permission) && editingUserRole === 'ADMIN')) && ( )} - {(editingUserRole === 'USER' || - editingUserRole === 'ADMIN') && ( + {['PENDING', 'USER', 'ADMIN'].includes(editingUserRole) && ( )} {(editingUserRole === 'SUPER_ADMIN' || - (permission === 'SUPER_ADMIN' && - ['super-admin', 'sa'].includes(orgSlug))) && ( + (permission === 'SUPER_ADMIN' && ['super-admin', 'sa'].includes(orgSlug))) && ( )} @@ -290,12 +270,7 @@ export function UserListModal({ - diff --git a/frontend/src/admin/__tests__/AdminDomainCard.test.js b/frontend/src/admin/__tests__/AdminDomainCard.test.js index 528017800d..42d1eeb1ae 100644 --- a/frontend/src/admin/__tests__/AdminDomainCard.test.js +++ b/frontend/src/admin/__tests__/AdminDomainCard.test.js @@ -6,6 +6,8 @@ import { I18nProvider } from '@lingui/react' import { setupI18n } from '@lingui/core' import { AdminDomainCard } from '../AdminDomainCard' +import { MockedProvider } from '@apollo/client/testing' +import { IS_USER_SUPER_ADMIN } from '../../graphql/queries' const i18n = setupI18n({ locale: 'en', @@ -17,21 +19,33 @@ const i18n = setupI18n({ }, }) +const mocks = [ + { + request: { + query: IS_USER_SUPER_ADMIN, + }, + result: { + data: { + isUserSuperAdmin: false, + }, + }, + }, +] + describe('', () => { it('represents a domain', async () => { const { getByText } = render( - - - - - - - - - , + + + + + + + + + + + , ) await waitFor(() => { diff --git a/frontend/src/admin/__tests__/AdminDomains.test.js b/frontend/src/admin/__tests__/AdminDomains.test.js index 437523adf6..e035332e1c 100644 --- a/frontend/src/admin/__tests__/AdminDomains.test.js +++ b/frontend/src/admin/__tests__/AdminDomains.test.js @@ -279,7 +279,7 @@ describe('', () => { userEvent.click(confirmButton) await waitFor(() => - expect(getByText(/Unable to create new domain./i)).toBeVisible(), + expect(getByText(/Unable to create new domain./i)).toBeInTheDocument(), ) }) diff --git a/frontend/src/admin/__tests__/AdminPage.test.js b/frontend/src/admin/__tests__/AdminPage.test.js index 1e5eafa695..4b72b401d5 100644 --- a/frontend/src/admin/__tests__/AdminPage.test.js +++ b/frontend/src/admin/__tests__/AdminPage.test.js @@ -32,15 +32,10 @@ describe('', () => { it('shows a list of the users organizations', async () => { const { getByText } = render( - + - + @@ -67,10 +62,7 @@ describe('', () => { > - + @@ -107,10 +99,7 @@ describe('', () => { > - + @@ -295,7 +284,7 @@ function mocks() { { request: { query: PAGINATED_ORG_AFFILIATIONS_ADMIN_PAGE, - variables: { orgSlug: 'Wolf-Group', first: 10, search: '' }, + variables: { orgSlug: 'Wolf-Group', first: 10, search: '', includePending: true }, }, result: { data: { diff --git a/frontend/src/admin/__tests__/UserList.test.js b/frontend/src/admin/__tests__/UserList.test.js index ae93f4d6d3..1b04c40b45 100644 --- a/frontend/src/admin/__tests__/UserList.test.js +++ b/frontend/src/admin/__tests__/UserList.test.js @@ -13,11 +13,7 @@ import { UserList } from '../UserList' import { UserVarProvider } from '../../utilities/userState' import { createCache } from '../../client' import { PAGINATED_ORG_AFFILIATIONS_ADMIN_PAGE as FORWARD } from '../../graphql/queries' -import { - UPDATE_USER_ROLE, - INVITE_USER_TO_ORG, - REMOVE_USER_FROM_ORG, -} from '../../graphql/mutations' +import { UPDATE_USER_ROLE, INVITE_USER_TO_ORG, REMOVE_USER_FROM_ORG } from '../../graphql/mutations' import { rawOrgUserListData } from '../../fixtures/orgUserListData' const i18n = setupI18n({ @@ -34,14 +30,14 @@ const successMocks = [ { request: { query: FORWARD, - variables: { first: 10, orgSlug: 'test-org.slug', search: '' }, + variables: { first: 10, orgSlug: 'test-org.slug', search: '', includePending: true }, }, result: { data: rawOrgUserListData }, }, { request: { query: FORWARD, - variables: { first: 10, orgSlug: 'test-org.slug' }, + variables: { first: 10, orgSlug: 'test-org.slug', search: '', includePending: true }, }, result: { data: rawOrgUserListData }, }, @@ -49,9 +45,7 @@ const successMocks = [ request: { query: UPDATE_USER_ROLE, variables: { - userName: - rawOrgUserListData.findOrganizationBySlug.affiliations.edges[0].node - .user.userName, + userName: rawOrgUserListData.findOrganizationBySlug.affiliations.edges[0].node.user.userName, orgId: rawOrgUserListData.findOrganizationBySlug.id, role: 'ADMIN', }, @@ -94,9 +88,7 @@ const successMocks = [ request: { query: REMOVE_USER_FROM_ORG, variables: { - userId: - rawOrgUserListData.findOrganizationBySlug.affiliations.edges[0].node - .user.id, + userId: rawOrgUserListData.findOrganizationBySlug.affiliations.edges[0].node.user.id, orgId: rawOrgUserListData.findOrganizationBySlug.id, }, }, @@ -122,13 +114,12 @@ describe('', () => { it('successfully renders with mocked data', async () => { const { getByText } = render( - + ', () => { await waitFor(() => expect( - getByText( - rawOrgUserListData.findOrganizationBySlug.affiliations.edges[0].node - .user.userName, - ), + getByText(rawOrgUserListData.findOrganizationBySlug.affiliations.edges[0].node.user.userName), ).toBeInTheDocument(), ) }) @@ -156,13 +144,7 @@ describe('', () => { // edit success it('updateUserRole elements render', async () => { - const { - getAllByText, - getByDisplayValue, - getByText, - findByLabelText, - findAllByLabelText, - } = render( + const { getAllByText, getByDisplayValue, getByText, findByLabelText, findAllByLabelText } = render( ', () => { ', () => { ', () => { ', () => { it('can be opened and closed', async () => { const { getByRole, queryByText } = render( - + - + @@ -93,9 +83,7 @@ describe('', () => { // modal closed userEvent.click(closeModalButton) - await waitFor(() => - expect(queryByText(/test-username/)).not.toBeInTheDocument(), - ) + await waitFor(() => expect(queryByText(/test-username/)).not.toBeInTheDocument()) }) describe('admin is updating a user', () => { @@ -129,11 +117,7 @@ describe('', () => { - + @@ -153,12 +137,8 @@ describe('', () => { name: /Role:/, }) expect(roleChangeSelect.options.length).toEqual(2) - expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent(/ADMIN/) // select new role and update userEvent.selectOptions(roleChangeSelect, 'ADMIN') @@ -169,9 +149,7 @@ describe('', () => { // check for "error" toast await waitFor(() => { - expect( - getAllByText(/Unable to change user role, please try again./)[0], - ) + expect(getAllByText(/Unable to change user role, please try again./)[0]) }) }) it('a client-side error occurs', async () => { @@ -211,11 +189,7 @@ describe('', () => { - + @@ -235,12 +209,8 @@ describe('', () => { name: /Role:/, }) expect(roleChangeSelect.options.length).toEqual(2) - expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent(/ADMIN/) // select new role and update userEvent.selectOptions(roleChangeSelect, 'ADMIN') @@ -251,9 +221,7 @@ describe('', () => { // check for "error" toast await waitFor(() => { - expect( - getAllByText(/Unable to update user role./)[0], - ).toBeInTheDocument() + expect(getAllByText(/Unable to update user role./)[0]).toBeInTheDocument() }) }) it('a type error occurs', async () => { @@ -293,11 +261,7 @@ describe('', () => { - + @@ -317,12 +281,8 @@ describe('', () => { name: /Role:/, }) expect(roleChangeSelect.options.length).toEqual(2) - expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent(/ADMIN/) // select new role and update userEvent.selectOptions(roleChangeSelect, 'ADMIN') @@ -333,9 +293,7 @@ describe('', () => { // check for "error" toast await waitFor(() => { - expect( - getAllByText(/Incorrect send method received./)[0], - ).toBeInTheDocument() + expect(getAllByText(/Incorrect send method received./)[0]).toBeInTheDocument() }) }) }) @@ -378,11 +336,7 @@ describe('', () => { - + @@ -402,12 +356,8 @@ describe('', () => { name: /Role:/, }) expect(roleChangeSelect.options.length).toEqual(2) - expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent(/ADMIN/) // select new role and update userEvent.selectOptions(roleChangeSelect, 'ADMIN') @@ -418,9 +368,7 @@ describe('', () => { // check for "success" toast await waitFor(() => { - expect( - getAllByText(/The user's role has been successfully updated/)[0], - ).toBeInTheDocument() + expect(getAllByText(/The user's role has been successfully updated/)[0]).toBeInTheDocument() }) }) it('admin can not change user role to "SUPER_ADMIN"', async () => { @@ -436,11 +384,7 @@ describe('', () => { - + @@ -460,17 +404,13 @@ describe('', () => { name: /Role:/, }) expect(roleChangeSelect.options.length).toEqual(2) - expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent(/ADMIN/) expect(roleChangeSelect).not.toHaveTextContent(/SUPER_ADMIN/) }) }) describe('admin is updating user with "ADMIN" privileges', () => { - it('admin cannot downgrade user to "USER"', async () => { + it('admin can downgrade user to "USER"', async () => { const { getByRole, queryByText } = render( ', () => { - + @@ -506,11 +442,11 @@ describe('', () => { const roleChangeSelect = getByRole('combobox', { name: /Role:/, }) - expect(roleChangeSelect.options.length).toEqual(1) - expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent( - /ADMIN/, - ) - expect(roleChangeSelect).not.toHaveTextContent(/USER/) + expect(roleChangeSelect.options.length).toEqual(2) + expect(roleChangeSelect.value).toEqual('ADMIN') + expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent(/ADMIN/) + expect(roleChangeSelect).toHaveTextContent(/USER/) }) }) }) @@ -553,11 +489,7 @@ describe('', () => { - + @@ -577,12 +509,8 @@ describe('', () => { name: /Role:/, }) expect(roleChangeSelect.options.length).toEqual(2) - expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent(/ADMIN/) // select new role and update userEvent.selectOptions(roleChangeSelect, 'ADMIN') @@ -594,9 +522,7 @@ describe('', () => { // check for "success" toast await waitFor(() => { - expect( - getAllByText(/The user's role has been successfully updated/)[0], - ).toBeInTheDocument() + expect(getAllByText(/The user's role has been successfully updated/)[0]).toBeInTheDocument() }) }) it('admin can not change user role to "SUPER_ADMIN"', async () => { @@ -612,11 +538,7 @@ describe('', () => { - + @@ -636,12 +558,8 @@ describe('', () => { name: /Role:/, }) expect(roleChangeSelect.options.length).toEqual(2) - expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(roleChangeSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(roleChangeSelect.options)[1]).toHaveTextContent(/ADMIN/) expect(roleChangeSelect).not.toHaveTextContent(/SUPER_ADMIN/) }) }) @@ -678,10 +596,7 @@ describe('', () => { - + @@ -742,10 +657,7 @@ describe('', () => { - + @@ -784,7 +696,6 @@ describe('', () => { orgId: orgId, requestedRole: 'USER', userName: editingUserName, - preferredLang: 'ENGLISH', }, }, result: { @@ -807,11 +718,7 @@ describe('', () => { - + @@ -835,12 +742,8 @@ describe('', () => { }) expect(newUserRoleSelect.options.length).toEqual(2) - expect(Object.values(newUserRoleSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(newUserRoleSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(newUserRoleSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(newUserRoleSelect.options)[1]).toHaveTextContent(/ADMIN/) const confirmUserInviteButton = getByRole('button', { name: /Confirm/i, @@ -861,7 +764,6 @@ describe('', () => { orgId: orgId, requestedRole: 'USER', userName: editingUserName, - preferredLang: 'ENGLISH', }, }, result: { @@ -890,11 +792,7 @@ describe('', () => { - + @@ -918,12 +816,8 @@ describe('', () => { }) expect(newUserRoleSelect.options.length).toEqual(2) - expect(Object.values(newUserRoleSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(newUserRoleSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(newUserRoleSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(newUserRoleSelect.options)[1]).toHaveTextContent(/ADMIN/) const confirmUserInviteButton = getByRole('button', { name: /Confirm/i, @@ -944,7 +838,6 @@ describe('', () => { orgId: orgId, requestedRole: 'USER', userName: editingUserName, - preferredLang: 'ENGLISH', }, }, result: { @@ -973,11 +866,7 @@ describe('', () => { - + @@ -1001,12 +890,8 @@ describe('', () => { }) expect(newUserRoleSelect.options.length).toEqual(2) - expect(Object.values(newUserRoleSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(newUserRoleSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(newUserRoleSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(newUserRoleSelect.options)[1]).toHaveTextContent(/ADMIN/) const confirmUserInviteButton = getByRole('button', { name: /Confirm/i, @@ -1015,9 +900,7 @@ describe('', () => { // check for "success" toast and modal to close await waitFor(() => { - expect( - getAllByText(/Incorrect send method received./)[0], - ).toBeVisible() + expect(getAllByText(/Incorrect send method received./)[0]).toBeVisible() }) }) }) @@ -1032,7 +915,6 @@ describe('', () => { orgId: orgId, requestedRole: 'USER', userName: editingUserName, - preferredLang: 'ENGLISH', }, }, result: { @@ -1061,11 +943,7 @@ describe('', () => { - + @@ -1089,12 +967,8 @@ describe('', () => { }) expect(newUserRoleSelect.options.length).toEqual(2) - expect(Object.values(newUserRoleSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(newUserRoleSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(newUserRoleSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(newUserRoleSelect.options)[1]).toHaveTextContent(/ADMIN/) const confirmUserInviteButton = getByRole('button', { name: /Confirm/i, @@ -1124,7 +998,6 @@ describe('', () => { orgId: orgId, requestedRole: 'ADMIN', userName: editingUserName, - preferredLang: 'ENGLISH', }, }, result: { @@ -1153,11 +1026,7 @@ describe('', () => { - + @@ -1178,12 +1047,8 @@ describe('', () => { }) expect(newUserRoleSelect.options.length).toEqual(2) - expect(Object.values(newUserRoleSelect.options)[0]).toHaveTextContent( - /USER/, - ) - expect(Object.values(newUserRoleSelect.options)[1]).toHaveTextContent( - /ADMIN/, - ) + expect(Object.values(newUserRoleSelect.options)[0]).toHaveTextContent(/USER/) + expect(Object.values(newUserRoleSelect.options)[1]).toHaveTextContent(/ADMIN/) userEvent.selectOptions(newUserRoleSelect, 'ADMIN') const confirmUserInviteButton = getByRole('button', { diff --git a/frontend/src/app/ABTestVariant.js b/frontend/src/app/ABTestVariant.js deleted file mode 100644 index 03a2d305c8..0000000000 --- a/frontend/src/app/ABTestVariant.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { any } from 'prop-types' - -export function ABTestVariant({ children }) { - return <>{children} -} - -ABTestVariant.propTypes = { - children: any, -} diff --git a/frontend/src/app/ABTestWrapper.js b/frontend/src/app/ABTestWrapper.js index 25660a0be3..b7be0c8314 100644 --- a/frontend/src/app/ABTestWrapper.js +++ b/frontend/src/app/ABTestWrapper.js @@ -1,12 +1,19 @@ import React from 'react' import { any, string } from 'prop-types' import { useUserVar } from '../utilities/userState' +import { IS_USER_SUPER_ADMIN } from '../graphql/queries' +import { useQuery } from '@apollo/client' -const isInsiderUser = ({ userName, insideUser }) => { - return userName.endsWith('@tbs-sct.gc.ca') || insideUser +const isInsiderUser = ({ isUserSuperAdmin, insideUser }) => { + return insideUser || isUserSuperAdmin +} + +export function ABTestVariant({ children }) { + return <>{children} } export function ABTestingWrapper({ children, insiderVariantName = 'B' }) { + const { data } = useQuery(IS_USER_SUPER_ADMIN) const { currentUser } = useUserVar() let childIndex = 0 @@ -14,7 +21,7 @@ export function ABTestingWrapper({ children, insiderVariantName = 'B' }) { if (!children.length) { if ( isInsiderUser({ - userName: currentUser?.userName || '', + isUserSuperAdmin: data?.isUserSuperAdmin || false, insideUser: currentUser?.insideUser || false, }) ) { @@ -28,21 +35,20 @@ export function ABTestingWrapper({ children, insiderVariantName = 'B' }) { // A + B variants if ( isInsiderUser({ - userName: currentUser?.userName || '', + isUserSuperAdmin: data?.isUserSuperAdmin || false, insideUser: currentUser?.insideUser || false, }) ) { - childIndex = children.findIndex( - (variant) => variant.props.name === insiderVariantName, - ) + childIndex = children.findIndex((variant) => variant.props.name === insiderVariantName) } else { - childIndex = children.findIndex( - (variant) => variant.props.name !== insiderVariantName, - ) + childIndex = children.findIndex((variant) => variant.props.name !== insiderVariantName) } return <>{children[childIndex]} } +ABTestVariant.propTypes = { + children: any, +} ABTestingWrapper.propTypes = { insiderVariantName: string, children: any, diff --git a/frontend/src/app/App.js b/frontend/src/app/App.js index b222061978..dc7434f92f 100644 --- a/frontend/src/app/App.js +++ b/frontend/src/app/App.js @@ -1,10 +1,5 @@ import React, { Suspense, useEffect } from 'react' -import { - Switch, - Link as RouteLink, - Redirect, - useLocation, -} from 'react-router-dom' +import { Switch, Link as RouteLink, Redirect, useLocation } from 'react-router-dom' import { CSSReset, Flex, Link, Text } from '@chakra-ui/react' import { t, Trans } from '@lingui/macro' import { ErrorBoundary } from 'react-error-boundary' @@ -29,8 +24,6 @@ import { LandingPage } from '../landing/LandingPage' import { NotificationBanner } from './NotificationBanner' import { IS_LOGIN_REQUIRED } from '../graphql/queries' import { useLingui } from '@lingui/react' -import { ABTestingWrapper } from './ABTestWrapper' -import { ABTestVariant } from './ABTestVariant' const GuidancePage = lazyWithRetry(() => import('../guidance/GuidancePage')) const PageNotFound = lazyWithRetry(() => import('./PageNotFound')) @@ -58,6 +51,7 @@ export function App() { const { currentUser, isLoggedIn, isEmailValidated, currentTFAMethod } = useUserVar() const { i18n } = useLingui() const { data } = useQuery(IS_LOGIN_REQUIRED, {}) + // const { isOpen: isVisible, onClose } = useDisclosure({ defaultIsOpen: true }) const location = useLocation() // Close websocket on user jwt change (refresh/logout) @@ -91,21 +85,21 @@ export function App() { Domains - - DMARC Summaries - )} + {isLoggedIn() && isEmailValidated() && currentTFAMethod() !== 'NONE' && ( + + DMARC Summaries + + )} + {isLoggedIn() && ( <> - - - - myTracker - - - + + myTracker + + Account Settings @@ -234,12 +228,7 @@ export function App() { )} - + {() => ( @@ -248,7 +237,7 @@ export function App() { - + {() => ( @@ -283,11 +272,7 @@ export function App() { {isLoggedIn() ? ( - - - - - + ) : ( - - - - Guidance - - - FAQ - - - - - {' '} - - Read Guidance - - - - - The Government of Canada’s (GC){' '} - - Directive on Service and Digital - {' '} - provides expectations on how GC organizations are to manage - their Information Technology (IT) services. The focus of the - Tracker tool is to help organizations stay in compliance with - the directives{' '} - - Email Management Service Configuration Requirements - {' '} - and the directives{' '} - - Web Site and Service Management Configuration Requirements - - . - + + + Getting Started + + + + + + The Government of Canada’s (GC){' '} + + Directive on Service and Digital + {' '} + provides expectations on how GC organizations are to manage their + Information Technology (IT) services. The focus of the Tracker tool + is to help organizations stay in compliance with the directives{' '} + + Email Management Service Configuration Requirements + {' '} + and the directives{' '} + + Web Site and Service Management Configuration Requirements + + . + + + + + Below are steps on how government organizations can leverage the + Tracker platform: + + + + {/* 1 */} + + + Getting an Account: - - - Below are steps on how government organizations can leverage the - Tracker platform: - - - - {/* 1 */} + - - Identify resources required to act as central point(s) of - contact with Treasury Board of Canada Secretariat (TBS). Share - the contact list with{' '} - - TBS Cyber Security - - , as required. - + + + Identify any current affiliated Tracker users within your + organization and develop a plan with them. + + - {/* 2 */} - - Perform an inventory of all organizational domains and - subdomains. Sources of information include: - + + + If your organization has no affiliated users within Tracker, + contact the{' '} + + TBS Cyber Security + {' '} + to assist in onboarding. + + - - The{' '} - - Tracker - {' '} - platform - - - - - Application Portfolio Management (APM) systems; and - - - - Business units within your organization. + + + Once access is given to your department by the TBS Cyber + team, they will be able to invite and manage other users + within the organization and manage the domain list. + + - {/* 3 */} + + + {/* 2 */} + + + Managing Your Domains: + + - - Provide an up-to-date list of all domain and sub-domains of - publicly accessible websites and web services to TBS Cyber - Security. The TBS Cyber Security team is responsible for - updating the domain and sub-domain lists within Tracker. - + + + Each organization’s domain list should include every + internet-facing service. It is the responsibility of + organization admins to manage the current list and identify + new domains to add. + + - {/* 4 */} - - Use Tracker and{' '} - - ITSP.40.062 Transport Layer Security (TLS) guidance - {' '} - to monitor the domains and sub-domains of your organization. - Other tools available to support this activity include,{' '} - - SSL Labs - - ,{' '} - - Hardenize - - ,{' '} - - SSLShopper - - , etc.. - + + + To receive DKIM scan results and guidance, you must add the + DKIM selectors used for each domain. Organization + administrators can add selectors in the “Admin Profile” by + clicking the edit button of the domain for which they wish + to add the selector. Common selectors to keep an for are + “selector1”, and “selector2”. + + + + + + + Domains are only to be removed from your list when 1) they + no longer exist, meaning they are deleted from the DNS + returning an error code of NX DOMAIN (domain name does not + exist); or 2) if you have identified that they do not belong + to your organization. + + - Tracker results refresh every 24 hours. + + + If a domain is no longer in use but still exists on the + DNS, it is still vulnerable to email spoofing attacks, + where an attacker can send an email that appears to be + coming from your domain. + + - {/* 5 */} + + + {/* 3 */} + + + Understanding Scan Metrics: + + - - Develop a prioritized schedule to address any failings. - Consider prioritizing websites and web services that exchange - Protected data. - - - - - Where necessary adjust IT Plans and budget estimates where - work is expected. - - - - - It is recommended that Shared Service Canada (SSC) - partners contact their SSC Service Delivery Manager to - discuss action plans and required steps to submit a - request for change. - - + + + The summary cards show two metrics that Tracker scans: + + + - Obtain certificates from a GC-approved certificate source - as outlined in the Recommendations for TLS Server - Certificates for GC public facing web services + The percentage of web-hosting services that strongly + enforce HTTPS - Obtain the configuration guidance for the appropriate - endpoints (e.g., web server, network/security appliances, - etc.) and implement recommended configurations. + The percentage of internet-facing services that have a + DMARC policy of at least p=”none” - + - - - - - Frequently Asked Questions - - - - {/* 1 */} - It is not clear to me why a domain has failed? - - - - Please contact{' '} - - TBS Cyber Security - {' '} - for help. - - - + + + These metrics are an important first step in securing your + services and should be treated as minimum requirements. + Further metrics are available in your organization's domain + list. + + - {/* 2 */} - How can I edit my domain list? - - - - Please direct all updates to TBS Cyber Security. - - - + Tracker results refresh every 24 hours. - {/* 3 */} + + + {/* 4 */} + + + Develop a prioritized schedule to address any failings: + + - Why do other tools ( - - Hardenize - - ,{' '} - - SSL Labs - - , etc.) show positive results for a domain while Tracker shows - negative results? + Consider prioritizing websites and web services that exchange + Protected data. - - - - While other tools are useful to work alongside Tracker, - they do not specifically adhere to the configuration - requirements specified in the{' '} - - Email Management Service Configuration Requirements - {' '} - and the{' '} - - Web Site and Service Management Configuration - Requirements - - . For a list of allowed protocols, ciphers, and curves - review the{' '} - - ITSP.40.062 TLS guidance - - . - - - - {/* 4 */} - - What does it mean if a domain is “unreachable”? - - - - - By default our scanners check domains ending in “.gc.ca” - and “.canada.ca”. If your domain is outside that set, you - need to contact us to let us know. Send an email to TBS - Cyber Security to confirm your ownership of that domain. - - - - - Another possibility is that your domain is not internet - facing. - - - + + Where necessary adjust IT Plans and budget estimates where + work is expected. + - Where can I get a GC-approved TLS certificate? - - - - Options include contacting the{' '} - - SSC WebSSL services team - {' '} - and/or using{' '} - - Let's Encrypt - - . For more information, please refer to the guidance on{' '} - - Recommendations for TLS Server Certificates - - . - - - + + It is recommended that Shared Service Canada (SSC) partners + contact their SSC Service Delivery Manager to discuss action + plans and required steps to submit a request for change. + + + + + Obtain certificates from a GC-approved certificate source as + outlined in the Recommendations for TLS Server Certificates + for GC public facing web services + + + + + Obtain the configuration guidance for the appropriate + endpoints (e.g., web server, network/security appliances, + etc.) and implement recommended configurations. + - {/* 6 */} + + + + + Links to Review: + + - References: + Tracker: - - Domain Name System (DNS) Services Management - Configuration Requirements - Canada.ca - + Wiki + List of guidance tags + + + + + + + Web Security: + + + + - Email Management Services Configuration Requirements - - Canada.ca + Requirements:{' '} + + Web Sites and Services Management Configuration + Requirements + - + - + - Implementation guidance: email domain protection - (ITSP.40.065 v1.1) - Canadian Centre for Cyber Security + Implementation:{' '} + + Guidance on securely configuring network protocols + (ITSP.40.062) + - + + + + + + + Email Security: + + + + + + Requirements:{' '} + + Email Management Services Configuration Requirements + + + + + + + + Implementation:{' '} + + Implementation guidance: email domain protection + (ITSP.40.065 v1.1) + + + - - - - + + + + - + + Frequently Asked Questions + + + + + {/* 1 */} + + It is not clear to me why a domain has failed? + + + + Please contact{' '} + + TBS Cyber Security + {' '} + for help. + + + + + {/* 2 */} + + How can I edit my domain list? + + + + Admins of an organization can add domains to their list. + + + + + Requests for updates can be sent directly to{' '} + + TBS Cyber Security + + . + + + + + Only{' '} + + TBS Cyber Security + {' '} + can remove domains from your organization. Domains are only to + be removed from your list when 1) they no longer exist, + meaning they are deleted from the DNS returning an error code + of NX DOMAIN (domain name does not exist); or 2) if you have + identified that they do not belong to your organization. + + + + + {/* 3 */} + + + Why do other tools show positive results for a domain while + Tracker shows negative results? + + + + + While other tools are useful to work alongside Tracker, they + do not specifically adhere to the configuration requirements + specified in the{' '} + + Email Management Service Configuration Requirements + {' '} + and the{' '} + + Web Site and Service Management Configuration Requirements + + . For a list of allowed protocols, ciphers, and curves review + the{' '} + + ITSP.40.062 TLS guidance + + . + + + + + {/* 4 */} + + + What does it mean if a domain is “unreachable”? + + + + + By default our scanners check domains ending in “.gc.ca” and + “.canada.ca”. If your domain is outside that set, you need to + contact us to let us know. Send an email to{' '} + + TBS Cyber Security + {' '} + to confirm your ownership of that domain. + + + + + Another possibility is that your domain is not internet + facing. + + + + + {/* 5 */} + + Where can I get a GC-approved TLS certificate? + + + + Options include contacting the{' '} + + SSC WebSSL services team + {' '} + and/or using{' '} + + Let's Encrypt + + . For more information, please refer to the guidance on{' '} + + Recommendations for TLS Server Certificates + + . + + + + + + + Why does the guidance page not show the domain’s DKIM selectors + even though they exist? + + + + + Tracker does not automatically add selectors, so it is likely + that they are not in the system yet. More information can be + found above in Getting Started. + + + + + {/* 7 */} + + References: + + + + + Domain Name System (DNS) Services Management Configuration + Requirements - Canada.ca + + + + + + + Email Management Services Configuration Requirements - + Canada.ca + + + + + + + Implementation guidance: email domain protection + (ITSP.40.065 v1.1) - Canadian Centre for Cyber Security + + + + + + Mozilla SSL Configuration Generator + + + + + + Protect domains that do not send email - GOV.UK (www.gov.uk) + + + + + + + + For any questions or concerns, please contact{' '} diff --git a/frontend/src/app/TopBanner.js b/frontend/src/app/TopBanner.js index 986dec746d..801bcec1d9 100644 --- a/frontend/src/app/TopBanner.js +++ b/frontend/src/app/TopBanner.js @@ -16,8 +16,7 @@ import { useUserVar } from '../utilities/userState' import { SIGN_OUT } from '../graphql/mutations' import { PhaseBanner } from './PhaseBanner' import { useLingui } from '@lingui/react' -import { ABTestingWrapper } from './ABTestWrapper' -import { ABTestVariant } from './ABTestVariant' +import { ABTestingWrapper, ABTestVariant } from './ABTestWrapper' export const TopBanner = (props) => { const { isLoggedIn, logout } = useUserVar() @@ -53,12 +52,7 @@ export const TopBanner = (props) => { - + { - + {t`Tracker - + {t`Tracker @@ -97,7 +81,9 @@ export const TopBanner = (props) => { BETA - INSIDER + + PREVIEW + } ml={{ base: '0', md: 'auto' }} diff --git a/frontend/src/app/__tests__/ReadGuidancePage.test.js b/frontend/src/app/__tests__/ReadGuidancePage.test.js index 86d226a012..0f9c919045 100644 --- a/frontend/src/app/__tests__/ReadGuidancePage.test.js +++ b/frontend/src/app/__tests__/ReadGuidancePage.test.js @@ -22,7 +22,7 @@ const i18n = setupI18n({ }, }) -describe('', () => { +describe('', () => { afterEach(cleanup) it('renders the page', async () => { @@ -45,6 +45,8 @@ describe('', () => { , ) - await waitFor(() => expect(getByText(/Read guidance/i))) + await waitFor(() => + expect(getByText('Getting Started')).toBeInTheDocument(), + ) }) }) diff --git a/frontend/src/components/InfoPanel.js b/frontend/src/components/InfoPanel.js index 33a0347c90..82793ff3ca 100644 --- a/frontend/src/components/InfoPanel.js +++ b/frontend/src/components/InfoPanel.js @@ -1,25 +1,39 @@ import React from 'react' import { any, bool, func, string } from 'prop-types' import { Trans } from '@lingui/macro' -import { Box, IconButton, Slide, Stack, Text } from '@chakra-ui/react' -import { ArrowDownIcon, ArrowUpIcon } from '@chakra-ui/icons' +import { + Box, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerFooter, + DrawerHeader, + DrawerOverlay, + IconButton, + Stack, + Text, +} from '@chakra-ui/react' +import { QuestionOutlineIcon } from '@chakra-ui/icons' export function InfoPanel({ isOpen, onToggle, children }) { + const btnRef = React.useRef() return ( - - - - {children} - - + + + + + + Glossary + + + + {children} + + + + + ) } @@ -35,25 +49,15 @@ export function InfoBox({ title, info }) { ) } -export function InfoButton({ isOpen, onToggle, ...props }) { +export function InfoButton({ onToggle, ...props }) { return ( - ) : ( - - ) - } - aria-label={isOpen ? 'Close glossary' : 'Open glossary'} - onClick={onToggle} - color="black" - bg="white" - borderColor="black" - borderWidth="2px" - isRound - my="2" {...props} + icon={} + aria-label="Open glossary" + variant="primaryOutline" + mx="2" + onClick={onToggle} /> ) } @@ -70,6 +74,5 @@ InfoBox.propTypes = { } InfoButton.propTypes = { - isOpen: bool, onToggle: func, } diff --git a/frontend/src/components/RelayPaginationControls.js b/frontend/src/components/RelayPaginationControls.js index acb60b6621..7b4d25bd50 100644 --- a/frontend/src/components/RelayPaginationControls.js +++ b/frontend/src/components/RelayPaginationControls.js @@ -38,6 +38,7 @@ export function RelayPaginationControls({ aria-label="Items per page" isDisabled={isLoadingMore} value={selectedDisplayLimit} + borderColor="black" onChange={(e) => { setSelectedDisplayLimit(parseInt(e.target.value)) resetToFirstPage() // Make sure to provide this as a prop if !onlyPagination diff --git a/frontend/src/components/SearchBox.js b/frontend/src/components/SearchBox.js index d4899648e7..68f343bc4c 100644 --- a/frontend/src/components/SearchBox.js +++ b/frontend/src/components/SearchBox.js @@ -15,6 +15,7 @@ import { Trans } from '@lingui/macro' import { ArrowDownIcon, ArrowUpIcon, SearchIcon } from '@chakra-ui/icons' import { RelayPaginationControls } from './RelayPaginationControls' import { array, bool, func, number, string } from 'prop-types' +import { InfoButton } from './InfoPanel' export function SearchBox({ selectedDisplayLimit, @@ -31,37 +32,15 @@ export function SearchBox({ resetToFirstPage, orderByOptions, placeholder, + onToggle, ...props }) { - const orderIconName = - orderDirection === 'ASC' ? : + const orderIconName = orderDirection === 'ASC' ? : return ( - - - - + + + + Search: @@ -81,20 +60,16 @@ export function SearchBox({ /> + - + Sort by: { - setSearchTerm(e.target.value) - resetToFirstPage() - }} + + + + + + { + setSearchTerm(e.target.value) + resetToFirstPage() + }} + /> + + + - + {tableDisplay} @@ -307,7 +320,6 @@ export default function DmarcByDomainPage() { previous={previous} isLoadingMore={isLoadingMore} /> - + + + Error while retrieving DMARC data for {domainSlug}.
+ This could be due to insufficient user privileges or the domain does not exist in the system. +
+
+
+ ) + } + + if (graphData?.findDomainByDomain?.hasDMARCReport === false) { return ( @@ -128,16 +130,14 @@ export default function DmarcReportPage() { } const formattedGraphData = { - periods: graphData.findDomainByDomain.yearlyDmarcSummaries.map( - (entry) => { - return { - month: entry.month, - year: entry.year, - ...entry.categoryTotals, - ...entry.categoryPercentages, - } - }, - ), + periods: graphData.findDomainByDomain.yearlyDmarcSummaries.map((entry) => { + return { + month: entry.month, + year: entry.year, + ...entry.categoryTotals, + ...entry.categoryPercentages, + } + }), } formattedGraphData.strengths = strengths graphDisplay = ( @@ -214,6 +214,74 @@ export default function DmarcReportPage() { accessor: 'disposition', } + const glossary = { + sourceIpAddress: { + title: sourceIpAddress.Header, + info: t`The IP address of sending server.`, + }, + envelopeFrom: { + title: envelopeFrom.Header, + info: t`Domain from Simple Mail Transfer Protocol (SMTP) banner message.`, + }, + dkimDomains: { + title: dkimDomains.Header, + info: t`The domains used for DKIM validation.`, + }, + dkimSelectors: { + title: dkimSelectors.Header, + info: t`Pointer to a DKIM public key record in DNS.`, + }, + totalMessages: { + title: totalMessages.Header, + info: t`The Total Messages from this sender.`, + }, + dnsHost: { + title: dnsHost.Header, + info: t`Host from reverse DNS of source IP address.`, + }, + spfDomains: { + title: spfDomains.Header, + info: t`Domains used for SPF validation.`, + }, + headerFrom: { + title: headerFrom.Header, + info: t`The address/domain used in the "From" field.`, + }, + guidance: { + title: guidance.Header, + info: t`Details for a given guidance tag can be found on the wiki, see below.`, + }, + spfAligned: { + title: spfAligned.Header, + info: t`Is SPF aligned. Can be true or false.`, + }, + spfResults: { + title: spfResults.Header, + info: t`The results of DKIM verification of the message. Can be pass, fail, neutral, soft-fail, temp-error, or perm-error.`, + }, + dkimAligned: { + title: dkimAligned.Header, + info: t`Is DKIM aligned. Can be true or false.`, + }, + dkimResults: { + title: dkimResults.Header, + info: t`The results of DKIM verification of the message. Can be pass, fail, neutral, temp-error, or perm-error.`, + }, + disposition: { + title: disposition.Header, + info: t`The DMARC enforcement action that the receiver took, either none, quarantine, or reject.`, + }, + } + + const generalGlossary = ( + <> + + + + + + ) + const dataToCsv = (columns, data) => { let csvOutput = columns.map((column) => column.Header).join(',') data.forEach((entry) => { @@ -247,10 +315,7 @@ export default function DmarcReportPage() { ) } // DKIM Failure query no longer loading, check if data exists - else if ( - tableData?.findDomainByDomain?.dmarcSummaryByPeriod?.detailTables - ?.dkimFailure?.edges.length > 0 - ) { + else if (tableData?.findDomainByDomain?.dmarcSummaryByPeriod?.detailTables?.dkimFailure?.edges.length > 0) { const dkimFailureColumns = [ { Header: t`DKIM Failures by IP Address`, @@ -271,17 +336,16 @@ export default function DmarcReportPage() { ] // Convert boolean values to string and properly format - const dkimFailureNodes = - tableData.findDomainByDomain.dmarcSummaryByPeriod.detailTables.dkimFailure.edges.map( - (edge) => { - const node = { ...edge.node } - node.dkimAligned = node.dkimAligned.toString() - node.dkimDomains = node.dkimDomains.replace(/,/g, ', ') - node.dkimSelectors = node.dkimSelectors.replace(/,/g, ', ') - node.dkimResults = node.dkimResults.replace(/,/g, ', ') - return node - }, - ) + const dkimFailureNodes = tableData.findDomainByDomain.dmarcSummaryByPeriod.detailTables.dkimFailure.edges.map( + (edge) => { + const node = { ...edge.node } + node.dkimAligned = node.dkimAligned.toString() + node.dkimDomains = node.dkimDomains.replace(/,/g, ', ') + node.dkimSelectors = node.dkimSelectors.replace(/,/g, ', ') + node.dkimResults = node.dkimResults.replace(/,/g, ', ') + return node + }, + ) dkimFailureTable = ( @@ -293,9 +357,8 @@ export default function DmarcReportPage() { frontendPagination={true} searchPlaceholder={t`Search DKIM Failing Items`} fileName={fileName} - exportDataFunction={() => - dataToCsv(dkimFailureColumns[0].columns, dkimFailureNodes) - } + exportDataFunction={() => dataToCsv(dkimFailureColumns[0].columns, dkimFailureNodes)} + onToggle={failDkimToggle} /> ) @@ -325,10 +388,7 @@ export default function DmarcReportPage() { ) } // Full pass query no longer loading, check if data exists - else if ( - tableData?.findDomainByDomain?.dmarcSummaryByPeriod?.detailTables?.fullPass - ?.edges.length > 0 - ) { + else if (tableData?.findDomainByDomain?.dmarcSummaryByPeriod?.detailTables?.fullPass?.edges.length > 0) { const fullPassColumns = [ { Header: t`Fully Aligned by IP Address`, @@ -347,16 +407,13 @@ export default function DmarcReportPage() { ] // Convert boolean values to string and properly format - const fullPassNodes = - tableData.findDomainByDomain.dmarcSummaryByPeriod.detailTables.fullPass.edges.map( - (edge) => { - const node = { ...edge.node } - node.spfDomains = node.spfDomains.replace(/,/g, ', ') - node.dkimDomains = node.dkimDomains.replace(/,/g, ', ') - node.dkimSelectors = node.dkimSelectors.replace(/,/g, ', ') - return node - }, - ) + const fullPassNodes = tableData.findDomainByDomain.dmarcSummaryByPeriod.detailTables.fullPass.edges.map((edge) => { + const node = { ...edge.node } + node.spfDomains = node.spfDomains.replace(/,/g, ', ') + node.dkimDomains = node.dkimDomains.replace(/,/g, ', ') + node.dkimSelectors = node.dkimSelectors.replace(/,/g, ', ') + return node + }) fullPassTable = ( @@ -368,9 +425,8 @@ export default function DmarcReportPage() { frontendPagination={true} searchPlaceholder={t`Search Fully Aligned Items`} fileName={fileName} - exportDataFunction={() => - dataToCsv(fullPassColumns[0].columns, fullPassNodes) - } + exportDataFunction={() => dataToCsv(fullPassColumns[0].columns, fullPassNodes)} + onToggle={fullPassToggle} /> ) @@ -400,10 +456,7 @@ export default function DmarcReportPage() { ) } // SPF Failure query no longer loading, check if data exists - else if ( - tableData?.findDomainByDomain?.dmarcSummaryByPeriod?.detailTables - ?.spfFailure?.edges.length > 0 - ) { + else if (tableData?.findDomainByDomain?.dmarcSummaryByPeriod?.detailTables?.spfFailure?.edges.length > 0) { const spfFailureColumns = [ { Header: t`SPF Failures by IP Address`, @@ -422,15 +475,14 @@ export default function DmarcReportPage() { }, ] // Convert boolean values to string and properly format - const spfFailureNodes = - tableData.findDomainByDomain.dmarcSummaryByPeriod.detailTables.spfFailure.edges.map( - (edge) => { - const node = { ...edge.node } - node.spfAligned = node.spfAligned.toString() - node.spfDomains = node.spfDomains.replace(/,/g, ', ') - return node - }, - ) + const spfFailureNodes = tableData.findDomainByDomain.dmarcSummaryByPeriod.detailTables.spfFailure.edges.map( + (edge) => { + const node = { ...edge.node } + node.spfAligned = node.spfAligned.toString() + node.spfDomains = node.spfDomains.replace(/,/g, ', ') + return node + }, + ) spfFailureTable = ( @@ -442,9 +494,8 @@ export default function DmarcReportPage() { frontendPagination={true} searchPlaceholder={t`Search SPF Failing Items`} fileName={fileName} - exportDataFunction={() => - dataToCsv(spfFailureColumns[0].columns, spfFailureNodes) - } + exportDataFunction={() => dataToCsv(spfFailureColumns[0].columns, spfFailureNodes)} + onToggle={failSpfToggle} /> ) @@ -479,10 +530,7 @@ export default function DmarcReportPage() { ) } // DMARC Failure query no longer loading, check if data exists - else if ( - tableData?.findDomainByDomain?.dmarcSummaryByPeriod?.detailTables - ?.dmarcFailure?.edges.length > 0 - ) { + else if (tableData?.findDomainByDomain?.dmarcSummaryByPeriod?.detailTables?.dmarcFailure?.edges.length > 0) { const dmarcFailureColumns = [ { Header: t`DMARC Failures by IP Address`, @@ -502,20 +550,19 @@ export default function DmarcReportPage() { ] // Convert boolean values to string and properly format - const dmarcFailureNodes = - tableData.findDomainByDomain.dmarcSummaryByPeriod.detailTables.dmarcFailure.edges.map( - (edge) => { - const node = { ...edge.node } + const dmarcFailureNodes = tableData.findDomainByDomain.dmarcSummaryByPeriod.detailTables.dmarcFailure.edges.map( + (edge) => { + const node = { ...edge.node } - // calculate dmarcFailStats totals - dmarcFailStats[node.disposition] += node.totalMessages + // calculate dmarcFailStats totals + dmarcFailStats[node.disposition] += node.totalMessages - node.spfDomains = node.spfDomains.replace(/,/g, ', ') - node.dkimDomains = node.dkimDomains.replace(/,/g, ', ') - node.dkimSelectors = node.dkimSelectors.replace(/,/g, ', ') - return node - }, - ) + node.spfDomains = node.spfDomains.replace(/,/g, ', ') + node.dkimDomains = node.dkimDomains.replace(/,/g, ', ') + node.dkimSelectors = node.dkimSelectors.replace(/,/g, ', ') + return node + }, + ) dmarcFailureTable = ( @@ -527,9 +574,8 @@ export default function DmarcReportPage() { frontendPagination={true} searchPlaceholder={t`Search DMARC Failing Items`} fileName={fileName} - exportDataFunction={() => - dataToCsv(dmarcFailureColumns[0].columns, dmarcFailureNodes) - } + exportDataFunction={() => dataToCsv(dmarcFailureColumns[0].columns, dmarcFailureNodes)} + onToggle={failDmarcToggle} /> ) @@ -547,22 +593,15 @@ export default function DmarcReportPage() { ) } - const fakeEmailDomainBlocks = - dmarcFailStats.reject + dmarcFailStats.quarantine + const fakeEmailDomainBlocks = dmarcFailStats.reject + dmarcFailStats.quarantine const domainSpoofingVolume = fakeEmailDomainBlocks + dmarcFailStats.none const tableDisplay = ( - - {fullPassTable} - - - {dkimFailureTable} - - - {spfFailureTable} - + {fullPassTable} + {dkimFailureTable} + {spfFailureTable} {dmarcFailureTable} @@ -574,10 +613,7 @@ export default function DmarcReportPage() {
- - Volume of messages spoofing domain (reject + quarantine + - none): - + Volume of messages spoofing domain (reject + quarantine + none): {domainSpoofingVolume} @@ -595,14 +631,7 @@ export default function DmarcReportPage() { {domainSlug.toUpperCase()} - + Guidance
@@ -209,20 +204,21 @@ export function DomainCard({ justifyContent="center" ml={{ base: 0, md: '4' }} > - - + {isEmailValidated() && userHasPermission && ( + + )} {hasDMARCReport && ( + + + )}
- + DMARC Phases - + - + {data?.organization?.userHasPermission && ( + { + const result = await getOrgDomainStatuses() + return result.data?.findOrganizationBySlug?.toCsv + }} + isLoading={orgDomainStatusesLoading} + /> + )} + {!isNaN(data?.organization?.affiliations?.totalCount) && ( - + )} diff --git a/frontend/src/organizationDetails/OrganizationDomains.js b/frontend/src/organizationDetails/OrganizationDomains.js index 01e369e2a3..23510ff4b0 100644 --- a/frontend/src/organizationDetails/OrganizationDomains.js +++ b/frontend/src/organizationDetails/OrganizationDomains.js @@ -20,19 +20,14 @@ import { ListOf } from '../components/ListOf' import { LoadingMessage } from '../components/LoadingMessage' import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' import { RelayPaginationControls } from '../components/RelayPaginationControls' -import { InfoButton, InfoBox, InfoPanel } from '../components/InfoPanel' +import { InfoBox, InfoPanel } from '../components/InfoPanel' import { usePaginatedCollection } from '../utilities/usePaginatedCollection' import { useDebouncedFunction } from '../utilities/useDebouncedFunction' import { PAGINATED_ORG_DOMAINS as FORWARD, MY_TRACKER_DOMAINS } from '../graphql/queries' import { SearchBox } from '../components/SearchBox' import { Formik } from 'formik' -import { - getRequirement, - schemaToValidation, -} from '../utilities/fieldRequirements' +import { getRequirement, schemaToValidation } from '../utilities/fieldRequirements' import { CheckCircleIcon, InfoIcon, WarningIcon } from '@chakra-ui/icons' -import { ABTestingWrapper } from '../app/ABTestWrapper' -import { ABTestVariant } from '../app/ABTestVariant' export function OrganizationDomains({ orgSlug }) { const [orderDirection, setOrderDirection] = useState('ASC') @@ -84,6 +79,7 @@ export function OrganizationDomains({ orgSlug }) { const orderByOptions = [ { value: 'HTTPS_STATUS', text: t`HTTPS Status` }, { value: 'HSTS_STATUS', text: t`HSTS Status` }, + { value: 'CERTIFICATES_STATUS', text: t`Certificates Status` }, { value: 'CIPHERS_STATUS', text: t`Ciphers Status` }, { value: 'CURVES_STATUS', text: t`Curves Status` }, { value: 'PROTOCOLS_STATUS', text: t`Protocols Status` }, @@ -99,6 +95,12 @@ export function OrganizationDomains({ orgSlug }) { { value: t`TEST`, text: t`Test` }, { value: t`WEB`, text: t`Web` }, { value: t`INACTIVE`, text: t`Inactive` }, + { value: `NXDOMAIN`, text: `NXDOMAIN` }, + { value: `BLOCKED`, text: t`Blocked` }, + { value: `SCAN_PENDING`, text: t`Scan Pending` }, + ] + + const hiddenFilterOptions = [ { value: `HIDDEN`, text: t`Hidden` }, { value: `ARCHIVED`, text: t`Archived` }, ] @@ -109,163 +111,170 @@ export function OrganizationDomains({ orgSlug }) { ) : ( - - - - { - setFilters([ - ...new Map( - [...filters, values].map((item) => { - if (item['filterCategory'] !== 'TAGS') - return [item['filterCategory'], item] - else return [item['filterValue'], item] - }), - ).values(), - ]) - resetForm() - }} - > - {({ handleChange, handleSubmit, values, errors }) => { - return ( - - - - Filters: + {orgSlug !== 'my-tracker' && ( + + { + setFilters([ + ...new Map( + [...filters, values].map((item) => { + if (item['filterCategory'] !== 'TAGS') return [item['filterCategory'], item] + else return [item['filterValue'], item] + }), + ).values(), + ]) + resetForm() + }} + > + {({ handleChange, handleSubmit, values, errors }) => { + return ( + + + + Filters: + + + + + {errors.filterCategory} - - - - {errors.filterCategory} - - - - - - {errors.comparison} - - - - + + + + + + {errors.comparison} + + + + - - {errors.filterValue} - - - - - - ) - }} - - - - - ( - - No Domains - + })} + {hiddenFilterOptions.map(({ value, text }, idx) => { + return ( + + ) + })} + + ) : ( + <> + + + + + )} + + + {errors.filterValue} + + + +
+ + ) + }} +
+ )} - mb="4" - > - {({ id, domain, status, hasDMARCReport, claimTags, hidden, archived, rcode, blocked, webScanPending }, index) => ( + ( + + No Domains + + )} + mb="4" + > + {( + { + id, + domain, + status, + hasDMARCReport, + claimTags, + hidden, + archived, + rcode, + blocked, + webScanPending, + userHasPermission, + }, + index, + ) => ( @@ -278,14 +287,17 @@ export function OrganizationDomains({ orgSlug }) { - - - + {/* Web statuses */} + + + + + {/* Email statuses */} + {/* Tags */} + + + + + + + - - - - {filters.map(({ filterCategory, comparison, filterValue }, idx) => { - const statuses = { - HTTPS_STATUS: `HTTPS`, - HSTS_STATUS: `HSTS`, - CIPHERS_STATUS: `Ciphers`, - CURVES_STATUS: t`Curves`, - PROTOCOLS_STATUS: t`Protocols`, - SPF_STATUS: `SPF`, - DKIM_STATUS: `DKIM`, - DMARC_STATUS: `DMARC`, - } - return ( - - {comparison === 'NOT_EQUAL' && !} - {filterCategory === 'TAGS' ? ( - {filterValue} - ) : ( - <> - {statuses[filterCategory]} - - - )} + {orgSlug !== 'my-tracker' && ( + + {filters.map(({ filterCategory, comparison, filterValue }, idx) => { + const statuses = { + HTTPS_STATUS: `HTTPS`, + HSTS_STATUS: `HSTS`, + CERTIFICATES_STATUS: `Certificates`, + CIPHERS_STATUS: `Ciphers`, + CURVES_STATUS: t`Curves`, + PROTOCOLS_STATUS: t`Protocols`, + SPF_STATUS: `SPF`, + DKIM_STATUS: `DKIM`, + DMARC_STATUS: `DMARC`, + } + return ( + + {comparison === 'NOT_EQUAL' && !} + {filterCategory === 'TAGS' ? ( + {filterValue} + ) : ( + <> + {statuses[filterCategory]} + + + )} - - setFilters(filters.filter((_, i) => i !== idx)) - } - /> - - ) - })} - - - + setFilters(filters.filter((_, i) => i !== idx))} /> +
+ ) + })} + + )} {domainList} @@ -400,7 +407,6 @@ export function OrganizationDomains({ orgSlug }) { previous={previous} isLoadingMore={isLoadingMore} /> - ) } diff --git a/frontend/src/organizationDetails/OrganizationSummary.js b/frontend/src/organizationDetails/OrganizationSummary.js deleted file mode 100644 index 133c47da12..0000000000 --- a/frontend/src/organizationDetails/OrganizationSummary.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' -import { Box } from '@chakra-ui/react' -import { SummaryGroup } from '../summaries/SummaryGroup' -import { number, object, string } from 'prop-types' - -export function OrganizationSummary({ summaries }) { - return ( - - - - ) -} - -OrganizationSummary.propTypes = { - summaries: object, - domainCount: number, - userCount: number, - city: string, - province: string, -} diff --git a/frontend/src/organizations/OrganizationCard.js b/frontend/src/organizations/OrganizationCard.js index db6a2738e6..7b81c30827 100644 --- a/frontend/src/organizations/OrganizationCard.js +++ b/frontend/src/organizations/OrganizationCard.js @@ -1,28 +1,11 @@ import React from 'react' -import { - Box, - Button, - Flex, - ListItem, - Progress, - Stack, - Text, - useBreakpointValue, -} from '@chakra-ui/react' +import { Box, Button, Flex, ListItem, Progress, Stack, Text, useBreakpointValue } from '@chakra-ui/react' import { CheckCircleIcon } from '@chakra-ui/icons' import { Link as RouteLink, useRouteMatch } from 'react-router-dom' import { bool, number, object, string } from 'prop-types' import { Trans } from '@lingui/macro' -export function OrganizationCard({ - name, - acronym, - slug, - domainCount, - verified, - summaries, - ...rest -}) { +export function OrganizationCard({ name, acronym, slug, domainCount, verified, summaries, ...rest }) { const { path, _url } = useRouteMatch() let httpsValue = 0 let dmarcValue = 0 @@ -75,24 +58,13 @@ export function OrganizationCard({ maxWidth="100%" > - + {name} ({acronym}) - {verified && ( - - )} + {verified && } - + HTTPS Configured @@ -121,11 +88,7 @@ export function OrganizationCard({ - + DMARC Configured @@ -133,13 +96,7 @@ export function OrganizationCard({ {hasButton && ( - + + + + ) +} + +RequestOrgInviteModal.propTypes = { + isOpen: bool, + onClose: func, + orgId: string, + orgName: string, +} diff --git a/frontend/src/organizations/__tests__/Organizations.test.js b/frontend/src/organizations/__tests__/Organizations.test.js index cf73515738..d7186563a6 100644 --- a/frontend/src/organizations/__tests__/Organizations.test.js +++ b/frontend/src/organizations/__tests__/Organizations.test.js @@ -75,6 +75,7 @@ describe('', () => { direction: 'ASC', search: '', includeSuperAdminOrg: false, + isVerified: true, }, }, result: { @@ -89,7 +90,7 @@ describe('', () => { name: 'organization one', slug: 'organization-one', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -103,7 +104,7 @@ describe('', () => { name: 'organization two', slug: 'organization-two', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -135,10 +136,7 @@ describe('', () => { > - + @@ -148,9 +146,7 @@ describe('', () => { ) // expect(getByText(/organization two/i)).toBeInTheDocument(), - await waitFor(() => - expect(getByText(/organization one/i)).toBeInTheDocument(), - ) + await waitFor(() => expect(getByText(/organization one/i)).toBeInTheDocument()) }) it('navigates to an organization detail page when a link is clicked', async () => { @@ -164,6 +160,7 @@ describe('', () => { direction: 'ASC', search: '', includeSuperAdminOrg: false, + isVerified: true, }, }, result: { @@ -178,7 +175,7 @@ describe('', () => { name: 'organization one', slug: 'organization-one', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -220,7 +217,7 @@ describe('', () => { name: 'organization two', slug: 'organization-two', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -262,7 +259,7 @@ describe('', () => { name: 'organization two', slug: 'organization-two', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -319,10 +316,7 @@ describe('', () => { - } - /> + } /> @@ -334,11 +328,7 @@ describe('', () => { const cardLink = await findByRole('link', /organization one/i) userEvent.click(cardLink) - await waitFor(() => - expect(history.location.pathname).toEqual( - '/organizations/organization-one', - ), - ) + await waitFor(() => expect(history.location.pathname).toEqual('/organizations/organization-one')) }) }) @@ -355,6 +345,7 @@ describe('', () => { direction: 'ASC', search: '', includeSuperAdminOrg: false, + isVerified: true, }, }, result: { @@ -369,7 +360,7 @@ describe('', () => { name: 'organization one', slug: 'organization-one', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -398,6 +389,7 @@ describe('', () => { direction: 'ASC', search: '', includeSuperAdminOrg: false, + isVerified: true, }, }, result: { @@ -412,7 +404,7 @@ describe('', () => { name: 'organization two', slug: 'organization-two', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -453,10 +445,7 @@ describe('', () => { - } - /> + } /> @@ -465,25 +454,19 @@ describe('', () => { , ) - await waitFor(() => - expect(getByText(/organization one/)).toBeInTheDocument(), - ) + await waitFor(() => expect(getByText(/organization one/)).toBeInTheDocument()) const next = await waitFor(() => getAllByLabelText('Next page')) fireEvent.click(next[0]) - await waitFor(() => - expect(getByText(/organization two/)).toBeInTheDocument(), - ) + await waitFor(() => expect(getByText(/organization two/)).toBeInTheDocument()) const previous = await waitFor(() => getAllByLabelText('Previous page')) fireEvent.click(previous[0]) - await waitFor(() => - expect(getByText(/organization one/)).toBeInTheDocument(), - ) + await waitFor(() => expect(getByText(/organization one/)).toBeInTheDocument()) }) }) @@ -498,6 +481,7 @@ describe('', () => { direction: 'ASC', search: '', includeSuperAdminOrg: false, + isVerified: true, }, data: { findMyOrganizations: { @@ -510,7 +494,7 @@ describe('', () => { name: 'organization one', slug: 'organization-one', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -539,6 +523,7 @@ describe('', () => { direction: 'ASC', search: '', includeSuperAdminOrg: false, + isVerified: true, }, }, result: { @@ -553,7 +538,7 @@ describe('', () => { name: 'organization one', slug: 'organization-one', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -582,6 +567,7 @@ describe('', () => { direction: 'ASC', search: '', includeSuperAdminOrg: false, + isVerified: true, }, }, result: { @@ -596,7 +582,7 @@ describe('', () => { name: 'organization two', slug: 'organization-two', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -625,6 +611,7 @@ describe('', () => { direction: 'ASC', search: '', includeSuperAdminOrg: false, + isVerified: true, }, }, result: { @@ -639,7 +626,7 @@ describe('', () => { name: 'organization two', slug: 'organization-two', domainCount: 5, - verified: false, + verified: true, summaries, __typename: 'Organizations', }, @@ -678,10 +665,7 @@ describe('', () => { - } - /> + } /> @@ -690,17 +674,13 @@ describe('', () => { , ) - await waitFor(() => - expect(queryByText(/organization one/)).toBeInTheDocument(), - ) + await waitFor(() => expect(queryByText(/organization one/)).toBeInTheDocument()) const next = getAllByLabelText('Next page') fireEvent.click(next[0]) - await waitFor(() => - expect(queryByText(/organization two/)).toBeInTheDocument(), - ) + await waitFor(() => expect(queryByText(/organization two/)).toBeInTheDocument()) }) }) }) diff --git a/frontend/src/organizations/__tests__/RequestOrgInviteModal.test.js b/frontend/src/organizations/__tests__/RequestOrgInviteModal.test.js new file mode 100644 index 0000000000..0b5ba5eefc --- /dev/null +++ b/frontend/src/organizations/__tests__/RequestOrgInviteModal.test.js @@ -0,0 +1,234 @@ +import React from 'react' +import { render, waitFor } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' +import { theme, ChakraProvider, useDisclosure } from '@chakra-ui/react' +import { I18nProvider } from '@lingui/react' +import { MockedProvider } from '@apollo/client/testing' +import { setupI18n } from '@lingui/core' +import { en } from 'make-plural/plurals' +import userEvent from '@testing-library/user-event' +import { RequestOrgInviteModal } from '../RequestOrgInviteModal' +import { REQUEST_INVITE_TO_ORG } from '../../graphql/mutations' +import { matchMediaSize } from '../../helpers/matchMedia' +import { createCache } from '../../client' +import { makeVar } from '@apollo/client' +import { UserVarProvider } from '../../utilities/userState' + +matchMediaSize() + +const i18n = setupI18n({ + locale: 'en', + messages: { + en: {}, + }, + localeData: { + en: { plurals: en }, + }, +}) + +const orgId = 'test-id' +const orgName = 'test-org-name' + +const RequestModal = () => { + const { isOpen, onOpen, onClose } = useDisclosure() + + return ( + <> + + + + ) +} + +describe('', () => { + it("successfully renders modal when 'Open Modal' btn is clicked", async () => { + const { queryByText, getByRole } = render( + + + + + + + + + + + , + ) + + // modal closed + const openModalButton = getByRole('button', { name: /Open Modal/ }) + expect(queryByText(/test-org-name/)).not.toBeInTheDocument() + + // modal opened + userEvent.click(openModalButton) + + await waitFor(() => { + expect(queryByText(/test-org-name/)).toBeVisible() + }) + const closeModalButton = getByRole('button', { name: /Close/ }) + + // modal closed + userEvent.click(closeModalButton) + + await waitFor(() => expect(queryByText(/test-org-name/)).not.toBeInTheDocument()) + }) + + describe('when confirm btn is clicked', () => { + it('successfully requests invite', async () => { + const mocks = [ + { + request: { + query: REQUEST_INVITE_TO_ORG, + variables: { + orgId: orgId, + }, + }, + result: { + data: { + requestOrgAffiliation: { + result: { + status: 'Hello World', + __typename: 'InviteUserToOrgResult', + }, + }, + }, + }, + }, + ] + const { queryByText, getByRole } = render( + + + + + + + + + + + , + ) + + // modal closed + const openModalButton = getByRole('button', { name: /Open Modal/ }) + expect(queryByText(/test-org-name/)).not.toBeInTheDocument() + + // modal opened + userEvent.click(openModalButton) + + await waitFor(() => { + expect(queryByText(/test-org-name/)).toBeVisible() + }) + const confirmButton = getByRole('button', { name: /Confirm/ }) + + // modal closed + userEvent.click(confirmButton) + + await waitFor(() => + expect(queryByText(/Your request has been sent to the organization administrators./)).toBeInTheDocument(), + ) + }) + describe('fails to request invite', () => { + it('a server-side error occurs', async () => { + const mocks = [ + { + request: { + query: REQUEST_INVITE_TO_ORG, + variables: { + orgId: orgId, + }, + }, + result: { + error: { errors: [{ message: 'error' }] }, + }, + }, + ] + const { queryByText, getByRole } = render( + + + + + + + + + + + , + ) + + // modal closed + const openModalButton = getByRole('button', { name: /Open Modal/ }) + expect(queryByText(/test-org-name/)).not.toBeInTheDocument() + + // modal opened + userEvent.click(openModalButton) + + await waitFor(() => { + expect(queryByText(/test-org-name/)).toBeVisible() + }) + const confirmButton = getByRole('button', { name: /Confirm/ }) + + // modal closed + userEvent.click(confirmButton) + + await waitFor(() => expect(queryByText(/Unable to request invite, please try again./)).toBeInTheDocument()) + }) + it('a client-side error occurs', async () => { + const mocks = [ + { + request: { + query: REQUEST_INVITE_TO_ORG, + variables: { + orgId: orgId, + }, + }, + result: { + data: { + requestOrgAffiliation: { + result: { + code: 92, + description: 'Hello World', + __typename: 'AffiliationError', + }, + }, + __typename: 'RequestOrgAffiliationPayload', + }, + }, + }, + ] + const { queryByText, getByRole } = render( + + + + + + + + + + + , + ) + + // modal closed + const openModalButton = getByRole('button', { name: /Open Modal/ }) + expect(queryByText(/test-org-name/)).not.toBeInTheDocument() + + // modal opened + userEvent.click(openModalButton) + + await waitFor(() => { + expect(queryByText(/test-org-name/)).toBeVisible() + }) + const confirmButton = getByRole('button', { name: /Confirm/ }) + + // modal closed + userEvent.click(confirmButton) + + await waitFor(() => expect(queryByText(/Unable to request invite, please try again./)).toBeInTheDocument()) + }) + }) + }) +}) diff --git a/frontend/src/summaries/Doughnut.js b/frontend/src/summaries/Doughnut.js index 88960f0f64..74aec42401 100644 --- a/frontend/src/summaries/Doughnut.js +++ b/frontend/src/summaries/Doughnut.js @@ -12,6 +12,7 @@ export const Doughnut = ({ height, width, title, + id, valueAccessor = (d) => d, innerRadius = Math.ceil(width / 2.8), outerRadius = Math.ceil(width / 2.2), @@ -28,7 +29,7 @@ export const Doughnut = ({ }) const { i18n } = useLingui() - const domainContext = title.includes('DMARC') ? ( + const domainContext = ['email', 'spf', 'dkim'].includes(id) ? ( Internet-facing ) : ( Web-hosting @@ -42,9 +43,7 @@ export const Doughnut = ({ y={15} textAnchor="middle" dominantBaseline="central" - fontSize={ - i18n.locale === 'en' ? `${width / 256}rem` : `${width / 300}rem` - } + fontSize={i18n.locale === 'en' ? `${width / 256}rem` : `${width / 300}rem`} transform={`translate(${width / 2}, ${height / 2})`} > Domains @@ -54,9 +53,7 @@ export const Doughnut = ({ y={i18n.locale === 'en' ? 20 : 30} textAnchor="middle" dominantBaseline="central" - fontSize={ - i18n.locale === 'en' ? `${width / 256}rem` : `${width / 300}rem` - } + fontSize={i18n.locale === 'en' ? `${width / 256}rem` : `${width / 300}rem`} transform={`translate(${width / 2}, ${height / 2})`} > {domainContext} @@ -67,9 +64,7 @@ export const Doughnut = ({ y={40} textAnchor="middle" dominantBaseline="central" - fontSize={ - i18n.locale === 'en' ? `${width / 256}rem` : `${width / 512}rem` - } + fontSize={i18n.locale === 'en' ? `${width / 256}rem` : `${width / 512}rem`} transform={`translate(${width / 2}, ${height / 2})`} > Domains @@ -116,11 +111,7 @@ export const Doughnut = ({
{chartContent} {arcs.map(({ title, count, percentage }, index) => { - if ( - (percentage % 1 === 0.5 && - ['Compliant', 'Implemented'].includes(title)) || - percentage % 1 > 0.5 - ) { + if ((percentage % 1 === 0.5 && ['Compliant', 'Implemented'].includes(title)) || percentage % 1 > 0.5) { percentage = Math.ceil(percentage) } else { percentage = Math.floor(percentage) @@ -136,20 +127,9 @@ export const Doughnut = ({ py={arcs.length > 2 ? '2' : '5'} overflow="hidden" > -

{ - if (name !== 'not implemented') { - dmarcCompliantCount += count - dmarcCompliantPercentage += percentage - } - }) - categories = [ - { - name: 'implemented', - count: dmarcCompliantCount, - percentage: dmarcCompliantPercentage, - }, - categories[0], - ] + + const descriptionMb = { + httpsStatus: ['2', '2', i18n.locale === 'fr' ? '8' : '2', '8'], + dmarc: '2', + spf: { base: 2, md: '14' }, + dkim: { base: 2, md: i18n.locale === 'fr' ? '14' : '8' }, + web: { base: 2, md: i18n.locale === 'fr' ? '8' : '2' }, + email: '2', } return ( @@ -44,21 +29,8 @@ export function SummaryCard({ mx="4" {...props} > - - + + {title} @@ -81,9 +53,7 @@ export function SummaryCard({ width={320} valueAccessor={(d) => d.count} > - {(segmentProps, index) => ( - - )} + {(segmentProps, index) => } diff --git a/frontend/src/summaries/SummaryGroup.js b/frontend/src/summaries/SummaryGroup.js index c8bf476e93..7c50d6219c 100644 --- a/frontend/src/summaries/SummaryGroup.js +++ b/frontend/src/summaries/SummaryGroup.js @@ -1,82 +1,53 @@ import React from 'react' -import { t, Trans } from '@lingui/macro' -import { Flex, Text } from '@chakra-ui/react' -import { object } from 'prop-types' +import { t } from '@lingui/macro' +import { Flex } from '@chakra-ui/react' +import { array } from 'prop-types' import { SummaryCard } from './SummaryCard' import theme from '../theme/canada' -export function SummaryGroup({ https, dmarcPhases }) { - const { colors } = theme +export function SummaryGroup({ summaries }) { + const { fail, pass } = theme.colors.summaries + const webCategoryDisplay = { + fail: { + name: t`Non-compliant`, + color: fail, + }, + pass: { + name: t`Compliant`, + color: pass, + }, + } - const httpsCard = https ? ( - - ) : ( - - - No HTTPS configuration information available for this organization. - - - ) - - const dmarcPhaseCard = dmarcPhases ? ( - - ) : ( - - No DMARC phase information available for this organization. - - ) + const mailcategoryDisplay = { + fail: { + name: t`Not Implemented`, + color: fail, + }, + pass: { + name: t`Implemented`, + color: pass, + }, + } return ( - - {httpsCard} - {dmarcPhaseCard} + + {summaries.map(({ id, title, description, data }) => ( + + ))} ) } SummaryGroup.propTypes = { - https: object, - dmarcPhases: object, + summaries: array, } diff --git a/frontend/src/summaries/TierOneSummaries.js b/frontend/src/summaries/TierOneSummaries.js new file mode 100644 index 0000000000..df03973cb6 --- /dev/null +++ b/frontend/src/summaries/TierOneSummaries.js @@ -0,0 +1,29 @@ +import React from 'react' +import { t } from '@lingui/macro' + +import { object } from 'prop-types' +import { SummaryGroup } from './SummaryGroup' + +export function TierOneSummaries({ https, dmarc }) { + const summaries = [ + { + id: 'httpsStatus', + title: t`HTTPS Configuration Summary`, + description: t`HTTPS is configured and HTTP connections redirect to HTTPS`, + data: https, + }, + { + id: 'email', + title: t`DMARC Configuration Summary`, + description: t`A minimum DMARC policy of “p=none” with at least one address defined as a recipient of aggregate reports`, + data: dmarc, + }, + ] + + return +} + +TierOneSummaries.propTypes = { + https: object, + dmarc: object, +} diff --git a/frontend/src/summaries/TierThreeSummaries.js b/frontend/src/summaries/TierThreeSummaries.js new file mode 100644 index 0000000000..01c5fac685 --- /dev/null +++ b/frontend/src/summaries/TierThreeSummaries.js @@ -0,0 +1,28 @@ +import React from 'react' + +import { t } from '@lingui/macro' +import { object } from 'prop-types' +import { SummaryGroup } from './SummaryGroup' + +export function TierThreeSummaries({ web, mail }) { + const summaries = [ + { + id: 'web', + title: t`Web Summary`, + description: t`Configuration requirements for web sites and services completely met`, + data: web, + }, + { + id: 'email', + title: t`Email Summary`, + description: t`Configuration requirements for email services completely met`, + data: mail, + }, + ] + return +} + +TierThreeSummaries.propTypes = { + web: object, + mail: object, +} diff --git a/frontend/src/summaries/TierTwoSummaries.js b/frontend/src/summaries/TierTwoSummaries.js new file mode 100644 index 0000000000..6511930f7c --- /dev/null +++ b/frontend/src/summaries/TierTwoSummaries.js @@ -0,0 +1,76 @@ +import React from 'react' +import { Box } from '@chakra-ui/react' + +import { t } from '@lingui/macro' +import { object } from 'prop-types' +import { SummaryGroup } from './SummaryGroup' + +export function TierTwoSummaries({ webConnections, ssl, spf, dkim, dmarcPhases }) { + const makeDmarcPhases = () => { + let dmarcFailCount = 0 + let dmarcFailPercentage = 0 + dmarcPhases.categories.forEach(({ name, count, percentage }) => { + if (name !== 'maintain') { + dmarcFailCount += count + dmarcFailPercentage += percentage + } + }) + const maintain = dmarcPhases.categories.find(({ name }) => name === 'maintain') + return { + categories: [ + { + name: 'pass', + count: maintain.count, + percentage: maintain.percentage, + }, + { + name: 'fail', + count: dmarcFailCount, + percentage: dmarcFailPercentage, + }, + ], + total: dmarcPhases.total, + } + } + + const webSummaries = [ + { + id: 'webConnections', + title: t`Web Connections Summary`, + description: t`HTTPS is configured, HTTP redirects, and HSTS is enabled`, + data: webConnections, + }, + { + id: 'tls', + title: t`TLS Summary`, + description: `Certificate is valid and configured to use strong protocols, ciphers, and curves`, + data: ssl, + }, + ] + + const mailSummaries = [ + { id: 'spf', title: t`SPF Summary`, description: t`SPF record is deployed and valid`, data: spf }, + { id: 'dkim', title: t`DKIM Summary`, description: t`DKIM record and keys are deployed and valid`, data: dkim }, + { + id: 'email', + title: t`DMARC Summary`, + description: t`DMARC policy of quarantine or reject, and all messages from non-mail domain is rejected`, + data: makeDmarcPhases(), + }, + ] + + return ( + + + + + ) +} + +TierTwoSummaries.propTypes = { + webConnections: object, + ssl: object, + spf: object, + dkim: object, + dmarcPhases: object, +} diff --git a/frontend/src/summaries/TieredSummaries.js b/frontend/src/summaries/TieredSummaries.js new file mode 100644 index 0000000000..b20781bd74 --- /dev/null +++ b/frontend/src/summaries/TieredSummaries.js @@ -0,0 +1,122 @@ +import React, { useState } from 'react' +import { + Flex, + IconButton, + Box, + Tooltip, + Accordion, + AccordionItem, + AccordionButton, + AccordionPanel, + AccordionIcon, +} from '@chakra-ui/react' +import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons' + +import { TierOneSummaries } from './TierOneSummaries' +import { TierTwoSummaries } from './TierTwoSummaries' +import { TierThreeSummaries } from './TierThreeSummaries' +import { Trans, t } from '@lingui/macro' +import { object } from 'prop-types' +import { ABTestingWrapper, ABTestVariant } from '../app/ABTestWrapper' + +export function TieredSummaries({ summaries }) { + const [show, setShow] = useState(false) + const { https, dmarc, webConnections, ssl, spf, dkim, dmarcPhase, web, mail } = summaries + + let hidden = null + if (typeof summaries?.httpsIncludeHidden !== 'undefined') + hidden = { + https: summaries?.httpsIncludeHidden, + dmarc: summaries?.dmarcIncludeHidden, + } + + return ( + + + + + + {hidden && ( + + )} + + + + + + + + + Tier 1: Minimum Requirements + + + + + + {hidden && ( + + )} + + + + + + + + Tier 2: Improved Posture + + + + + + + + + + + Tier 3: Compliance + + + + + + + + + + + + ) +} + +TieredSummaries.propTypes = { + summaries: object, +} diff --git a/frontend/src/summaries/__tests__/SummaryGroup.test.js b/frontend/src/summaries/__tests__/SummaryGroup.test.js index a5f4756669..710f901642 100644 --- a/frontend/src/summaries/__tests__/SummaryGroup.test.js +++ b/frontend/src/summaries/__tests__/SummaryGroup.test.js @@ -17,38 +17,49 @@ const i18n = setupI18n({ }, }) -const data = { - httpsSummary: { - categories: [ - { - name: 'pass', - count: 7468, - percentage: 56.6, - }, - { - name: 'fail', - count: 5738, - percentage: 43.4, - }, - ], - total: 13206, +const data = [ + { + id: 'httpsStatus', + title: 'HTTPS Configuration Summary', + description: 'HTTPS is configured and HTTP connections redirect to HTTPS', + data: { + categories: [ + { + name: 'pass', + count: 7468, + percentage: 56.6, + }, + { + name: 'fail', + count: 5738, + percentage: 43.4, + }, + ], + total: 13206, + }, }, - dmarcPhaseSummary: { - categories: [ - { - name: 'not implemented', - count: 200, - percentage: 20, - }, - { - name: 'implemented', - count: 800, - percentage: 80, - }, - ], - total: 1000, + { + id: 'email', + title: 'DMARC Configuration Summary', + description: + 'A minimum DMARC policy of “p=none” with at least one address defined as a recipient of aggregate reports', + data: { + categories: [ + { + name: 'pass', + count: 200, + percentage: 20, + }, + { + name: 'fail', + count: 800, + percentage: 80, + }, + ], + total: 1000, + }, }, -} +] describe('', () => { describe('given the data for HTTPS and email summaries', () => { @@ -57,19 +68,12 @@ describe('', () => { - + , ) - expect( - getByText( - /HTTPS is configured and HTTP connections redirect to HTTPS/i, - ), - ).toBeInTheDocument() + expect(getByText(/HTTPS is configured and HTTP connections redirect to HTTPS/i)).toBeInTheDocument() expect( getByText( /A minimum DMARC policy of “p=none” with at least one address defined as a recipient of aggregate reports/i, diff --git a/frontend/src/summaries/__tests__/TieredSummaries.test.js b/frontend/src/summaries/__tests__/TieredSummaries.test.js new file mode 100644 index 0000000000..cf696af2c0 --- /dev/null +++ b/frontend/src/summaries/__tests__/TieredSummaries.test.js @@ -0,0 +1,222 @@ +import React from 'react' +import { MockedProvider } from '@apollo/client/testing' +import { render } from '@testing-library/react' +import { theme, ChakraProvider } from '@chakra-ui/react' +import { I18nProvider } from '@lingui/react' +import { setupI18n } from '@lingui/core' + +import { TieredSummaries } from '../TieredSummaries' +import { UserVarProvider } from '../../utilities/userState' +import { makeVar } from '@apollo/client' + +const i18n = setupI18n({ + locale: 'en', + messages: { + en: {}, + }, + localeData: { + en: {}, + }, +}) + +const data = { + httpsIncludeHidden: { + total: 8200, + categories: [ + { + name: 'pass', + count: 372, + percentage: 4.536585365853658, + }, + { + name: 'fail', + count: 7828, + percentage: 95.46341463414635, + }, + ], + }, + dmarcIncludeHidden: { + total: 7465, + categories: [ + { + name: 'pass', + count: 1143, + percentage: 15.311453449430676, + }, + { + name: 'fail', + count: 6322, + percentage: 84.68854655056933, + }, + ], + }, + https: { + total: 8992, + categories: [ + { + name: 'pass', + count: 3036, + percentage: 33.763345195729535, + }, + { + name: 'fail', + count: 5956, + percentage: 66.23665480427047, + }, + ], + }, + dmarc: { + total: 9274, + categories: [ + { + name: 'pass', + count: 8805, + percentage: 94.94285098123787, + }, + { + name: 'fail', + count: 469, + percentage: 5.057149018762132, + }, + ], + }, + webConnections: { + total: 4199, + categories: [ + { + name: 'pass', + count: 3648, + percentage: 86.87782805429865, + }, + { + name: 'fail', + count: 551, + percentage: 13.122171945701353, + }, + ], + }, + ssl: { + total: 4204, + categories: [ + { + name: 'pass', + count: 296, + percentage: 7.040913415794481, + }, + { + name: 'fail', + count: 3908, + percentage: 92.95908658420552, + }, + ], + }, + spf: { + total: 8509, + categories: [ + { + name: 'pass', + count: 2641, + percentage: 31.037724762016687, + }, + { + name: 'fail', + count: 5868, + percentage: 68.9622752379833, + }, + ], + }, + dkim: { + total: 5692, + categories: [ + { + name: 'pass', + count: 1883, + percentage: 33.08151791988756, + }, + { + name: 'fail', + count: 3809, + percentage: 66.91848208011244, + }, + ], + }, + dmarcPhase: { + total: 3348, + categories: [ + { + name: 'not implemented', + count: 312, + percentage: 9.31899641577061, + }, + { + name: 'assess', + count: 1351, + percentage: 40.35244922341696, + }, + { + name: 'deploy', + count: 204, + percentage: 6.093189964157706, + }, + { + name: 'enforce', + count: 57, + percentage: 1.702508960573477, + }, + { + name: 'maintain', + count: 1424, + percentage: 42.53285543608124, + }, + ], + }, + web: { + total: 5911, + categories: [ + { + name: 'pass', + count: 5848, + percentage: 98.93419049230249, + }, + { + name: 'fail', + count: 63, + percentage: 1.0658095076975087, + }, + ], + }, + mail: { + total: 4531, + categories: [ + { + name: 'pass', + count: 1905, + percentage: 42.04369896270139, + }, + { + name: 'fail', + count: 2626, + percentage: 57.95630103729861, + }, + ], + }, +} + +describe('', () => { + describe('given the data for the tiered summaries', () => { + it('displays tier 1 summary cards', async () => { + const { getByText } = render( + + + + + + + + + , + ) + expect(getByText(/HTTPS is configured and HTTP connections redirect to HTTPS/i)).toBeInTheDocument() + }) + }) +}) diff --git a/frontend/src/theme/Icons.js b/frontend/src/theme/Icons.js index 7656f37622..7c6090cf01 100644 --- a/frontend/src/theme/Icons.js +++ b/frontend/src/theme/Icons.js @@ -43,11 +43,7 @@ export const TwoFactorIcon = createIcon({ viewBox: '0 0 100 118.23771', // path can also be an array of elements, if you have multiple paths, lines, shapes, etc. path: ( - + - + { + toast({ + title: t`An error occurred while updating your email update preference.`, + description: message, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + }, + onCompleted({ updateUserProfile }) { + if (updateUserProfile.result.__typename === 'UpdateUserProfileResult') { + toast({ + title: t`Email Updates status changed`, + description: t`You have successfully updated your email update preference.`, + status: 'success', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + login({ + ...currentUser, + receiveUpdateEmails: updateUserProfile.result.user.receiveUpdateEmails, + }) + } else if (updateUserProfile.result.__typename === 'UpdateUserProfileError') { + toast({ + title: t`Unable to update to your Email Updates status, please try again.`, + description: updateUserProfile.result.description, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + } else { + toast({ + title: t`Incorrect update method received.`, + description: t`Incorrect updateUserProfile.result typename.`, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + console.log('Incorrect updateUserProfile.result typename.') + } + }, + }) + return ( + + + + + + await updateUserProfile({ + variables: { receiveUpdateEmails: e.target.checked }, + }) + } + /> + + Email Updates + + + ) +} + +EmailUpdatesSwitch.propTypes = { + receiveUpdateEmails: bool.isRequired, +} diff --git a/frontend/src/user/InsideUserSwitch.js b/frontend/src/user/InsideUserSwitch.js index cfc8398ec8..cb6e5c62b2 100644 --- a/frontend/src/user/InsideUserSwitch.js +++ b/frontend/src/user/InsideUserSwitch.js @@ -16,7 +16,7 @@ export function InsideUserSwitch({ insideUser }) { { onError: ({ message }) => { toast({ - title: t`An error occurred while updating your insider preference.`, + title: t`An error occurred while updating your inside user preference.`, description: message, status: 'error', duration: 9000, @@ -25,11 +25,10 @@ export function InsideUserSwitch({ insideUser }) { }) }, onCompleted({ updateUserProfile }) { - console.log(updateUserProfile) if (updateUserProfile.result.__typename === 'UpdateUserProfileResult') { toast({ - title: t`Insider status changed`, - description: t`You have successfully updated your insider preference.`, + title: t`Inside user status changed`, + description: t`You have successfully updated your inside user preference.`, status: 'success', duration: 9000, isClosable: true, @@ -43,7 +42,7 @@ export function InsideUserSwitch({ insideUser }) { updateUserProfile.result.__typename === 'UpdateUserProfileError' ) { toast({ - title: t`Unable to update to your insider status, please try again.`, + title: t`Unable to update to your inside user status, please try again.`, description: updateUserProfile.result.description, status: 'error', duration: 9000, @@ -75,7 +74,7 @@ export function InsideUserSwitch({ insideUser }) { isFocusable={true} id="Inside User" name="Inside User" - aria-label="Inside User" + aria-label="Feature Preview" mx="2" defaultChecked={insideUser} onChange={async (e) => diff --git a/frontend/src/user/MyTrackerPage.js b/frontend/src/user/MyTrackerPage.js index 808af4671d..918461401b 100644 --- a/frontend/src/user/MyTrackerPage.js +++ b/frontend/src/user/MyTrackerPage.js @@ -1,27 +1,17 @@ import React, { useEffect } from 'react' import { useQuery } from '@apollo/client' import { Trans } from '@lingui/macro' -import { - Box, - Flex, - Heading, - Tab, - TabList, - TabPanel, - TabPanels, - Tabs, - Text, -} from '@chakra-ui/react' +import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@chakra-ui/react' import { useParams, useHistory } from 'react-router-dom' import { ErrorBoundary } from 'react-error-boundary' import { OrganizationDomains } from '../organizationDetails/OrganizationDomains' -import { OrganizationSummary } from '../organizationDetails/OrganizationSummary' import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' import { LoadingMessage } from '../components/LoadingMessage' import { MY_TRACKER_SUMMARY } from '../graphql/queries' import { RadialBarChart } from '../summaries/RadialBarChart' +import { TierOneSummaries } from '../summaries/TierOneSummaries' export default function OrganizationDetails() { const { activeTab } = useParams() @@ -29,7 +19,7 @@ export default function OrganizationDetails() { const tabNames = ['summary', 'dmarc_phases', 'domains'] const defaultActiveTab = tabNames[0] - const { loading, error, data } = useQuery(MY_TRACKER_SUMMARY, {}) + const { loading, error, data } = useQuery(MY_TRACKER_SUMMARY) useEffect(() => { if (!activeTab) { @@ -58,12 +48,7 @@ export default function OrganizationDetails() { return ( - + - Welcome to your personal view of Tracker. Moderate the security - posture of domains of interest across multiple organizations. To add - domains to this view, use the star icon buttons available on domain - lists. + Welcome to your personal view of Tracker. Moderate the security posture of domains of interest across multiple + organizations. To add domains to this view, use the star icon buttons available on domain lists. - diff --git a/frontend/src/user/UserPage.js b/frontend/src/user/UserPage.js index 137b0403cb..94e2899c25 100644 --- a/frontend/src/user/UserPage.js +++ b/frontend/src/user/UserPage.js @@ -36,13 +36,10 @@ import { LoadingMessage } from '../components/LoadingMessage' import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' import { createValidationSchema } from '../utilities/fieldRequirements' import { useUserVar } from '../utilities/userState' -import { - SEND_EMAIL_VERIFICATION, - CLOSE_ACCOUNT, - SIGN_OUT, -} from '../graphql/mutations' +import { SEND_EMAIL_VERIFICATION, CLOSE_ACCOUNT, SIGN_OUT } from '../graphql/mutations' import { NotificationBanner } from '../app/NotificationBanner' -// import { InsideUserSwitch } from './InsideUserSwitch' +import { InsideUserSwitch } from './InsideUserSwitch' +import { EmailUpdatesSwitch } from './EmailUpdatesSwitch' export default function UserPage() { const toast = useToast() @@ -50,81 +47,75 @@ export default function UserPage() { const { i18n } = useLingui() const [emailSent, setEmailSent] = useState(false) const { logout } = useUserVar() - const [sendEmailVerification, { loading: loadEmailVerification }] = - useMutation(SEND_EMAIL_VERIFICATION, { - onError(error) { + const [sendEmailVerification, { loading: loadEmailVerification }] = useMutation(SEND_EMAIL_VERIFICATION, { + onError(error) { + toast({ + title: error.message, + description: t`Unable to send verification email`, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + }, + onCompleted() { + toast({ + title: t`Email successfully sent`, + description: t`Check your associated Tracker email for the verification link`, + status: 'success', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + setEmailSent(true) + }, + }) + + const [closeAccount, { loading: loadingCloseAccount }] = useMutation(CLOSE_ACCOUNT, { + onError(error) { + toast({ + title: i18n._(t`An error occurred.`), + description: error.message, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + }, + onCompleted({ closeAccount }) { + if (closeAccount.result.__typename === 'CloseAccountResult') { toast({ - title: error.message, - description: t`Unable to send verification email`, - status: 'error', + title: i18n._(t`Account Closed Successfully`), + description: i18n._(t`Tracker account has been successfully closed.`), + status: 'success', duration: 9000, isClosable: true, position: 'top-left', }) - }, - onCompleted() { + closeAccountOnClose() + history.push('/') + } else if (closeAccount.result.__typename === 'CloseAccountError') { toast({ - title: t`Email successfully sent`, - description: t`Check your associated Tracker email for the verification link`, - status: 'success', + title: i18n._(t`Unable to close the account.`), + description: closeAccount.result.description, + status: 'error', duration: 9000, isClosable: true, position: 'top-left', }) - setEmailSent(true) - }, - }) - - const [closeAccount, { loading: loadingCloseAccount }] = useMutation( - CLOSE_ACCOUNT, - { - onError(error) { + } else { toast({ - title: i18n._(t`An error occurred.`), - description: error.message, + title: i18n._(t`Incorrect send method received.`), + description: i18n._(t`Incorrect closeAccount.result typename.`), status: 'error', duration: 9000, isClosable: true, position: 'top-left', }) - }, - onCompleted({ closeAccount }) { - if (closeAccount.result.__typename === 'CloseAccountResult') { - toast({ - title: i18n._(t`Account Closed Successfully`), - description: i18n._( - t`Tracker account has been successfully closed.`, - ), - status: 'success', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - closeAccountOnClose() - history.push('/') - } else if (closeAccount.result.__typename === 'CloseAccountError') { - toast({ - title: i18n._(t`Unable to close the account.`), - description: closeAccount.result.description, - status: 'error', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - } else { - toast({ - title: i18n._(t`Incorrect send method received.`), - description: i18n._(t`Incorrect closeAccount.result typename.`), - status: 'error', - duration: 9000, - isClosable: true, - position: 'top-left', - }) - console.log('Incorrect closeAccount.result typename.') - } - }, + console.log('Incorrect closeAccount.result typename.') + } }, - ) + }) const [signOut] = useMutation(SIGN_OUT, { onCompleted() { @@ -132,11 +123,7 @@ export default function UserPage() { }, }) - const { - isOpen: closeAccountIsOpen, - onOpen: closeAccountOnOpen, - onClose: closeAccountOnClose, - } = useDisclosure() + const { isOpen: closeAccountIsOpen, onOpen: closeAccountOnOpen, onClose: closeAccountOnClose } = useDisclosure() const { loading: queryUserLoading, @@ -164,7 +151,8 @@ export default function UserPage() { tfaSendMethod, emailValidated, phoneValidated, - // insideUser, + insideUser, + receiveUpdateEmails, } = queryUserData?.userPage return ( @@ -172,9 +160,7 @@ export default function UserPage() { {tfaSendMethod === 'NONE' && queryUserData?.isUserAdmin && ( - - Admin accounts must activate a multi-factor authentication option. - + Admin accounts must activate a multi-factor authentication option. )} @@ -214,7 +200,8 @@ export default function UserPage() { )} - {/* */} + + - diff --git a/frontend/src/user/__tests__/InsideUserSwitch.test.js b/frontend/src/user/__tests__/InsideUserSwitch.test.js index cb143b66f5..d98f04b610 100644 --- a/frontend/src/user/__tests__/InsideUserSwitch.test.js +++ b/frontend/src/user/__tests__/InsideUserSwitch.test.js @@ -98,9 +98,9 @@ describe('', () => { await waitFor(() => expect(getByText(/Feature Preview/i)).toBeInTheDocument(), ) - const betaSwitch = getByLabelText(/Inside User/i) + const betaSwitch = getByLabelText(/Feature Preview/i) userEvent.click(betaSwitch) - await waitFor(() => expect(getByText(/Insider status changed/i))) + await waitFor(() => expect(getByText(/Inside user status changed/i))) }) it("fails when changing the user's status", async () => { const mocks = [ @@ -146,12 +146,12 @@ describe('', () => { await waitFor(() => expect(getByText(/Feature Preview/i)).toBeInTheDocument(), ) - const betaSwitch = getByLabelText(/Inside User/i) + const betaSwitch = getByLabelText(/Feature Preview/i) userEvent.click(betaSwitch) await waitFor(() => expect( getByText( - /Unable to update to your insider status, please try again./i, + /Unable to update to your inside user status, please try again./i, ), ), ) diff --git a/frontend/src/utilities/useDebouncedFunction.js b/frontend/src/utilities/useDebouncedFunction.js index d2aa52fbf5..08c9ca866c 100644 --- a/frontend/src/utilities/useDebouncedFunction.js +++ b/frontend/src/utilities/useDebouncedFunction.js @@ -2,7 +2,9 @@ import { useEffect } from 'react' export const useDebouncedFunction = (functionToCall, delay) => { useEffect(() => { - const timeoutID = setTimeout(functionToCall, delay) + const timeoutID = setTimeout(function () { + functionToCall() + }, delay) return () => { clearTimeout(timeoutID) diff --git a/k8s/apps/bases/api/deployment.yaml b/k8s/apps/bases/api/deployment.yaml index 9a1dae6be9..ec72ee22e7 100644 --- a/k8s/apps/bases/api/deployment.yaml +++ b/k8s/apps/bases/api/deployment.yaml @@ -54,7 +54,7 @@ spec: - name: database-config mountPath: /app containers: - - image: gcr.io/track-compliance/api-js:master-a802403-1680796913 # {"$imagepolicy": "flux-system:api"} + - image: gcr.io/track-compliance/api-js:master-af5d359-1686937994 # {"$imagepolicy": "flux-system:api"} name: api ports: - containerPort: 4000 @@ -244,6 +244,16 @@ spec: secretKeyRef: name: api key: NOTIFICATION_VERIFICATION_EMAIL_FR + - name: NOTIFICATION_ORG_INVITE_REQUEST_EN + valueFrom: + secretKeyRef: + name: api + key: NOTIFICATION_ORG_INVITE_REQUEST_EN + - name: NOTIFICATION_ORG_INVITE_REQUEST_FR + valueFrom: + secretKeyRef: + name: api + key: NOTIFICATION_ORG_INVITE_REQUEST_FR - name: TRACING_ENABLED valueFrom: secretKeyRef: diff --git a/k8s/apps/bases/api/kustomization.yaml b/k8s/apps/bases/api/kustomization.yaml index 9dd33452de..aba83d6b73 100644 --- a/k8s/apps/bases/api/kustomization.yaml +++ b/k8s/apps/bases/api/kustomization.yaml @@ -2,8 +2,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: api resources: -- namespace.yaml -- deployment.yaml -- service.yaml -- virtual-service.yaml -- database-configmap.yaml + - namespace.yaml + - deployment.yaml + - service.yaml + - virtual-service.yaml + - database-configmap.yaml + - org-footprint-cronjob.yaml diff --git a/k8s/apps/bases/api/org-footprint-cronjob.yaml b/k8s/apps/bases/api/org-footprint-cronjob.yaml new file mode 100644 index 0000000000..3b5f217e00 --- /dev/null +++ b/k8s/apps/bases/api/org-footprint-cronjob.yaml @@ -0,0 +1,63 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: org-footprint + namespace: api +spec: + schedule: "30 10 * * *" + concurrencyPolicy: Replace + startingDeadlineSeconds: 180 + jobTemplate: + spec: + template: + spec: + containers: + - name: org-footprint + image: gcr.io/track-compliance/services/org-footprint:master-8f0fa7c-1686767950 # {"$imagepolicy": "flux-system:org-footprint"} + env: + - name: DB_PASS + valueFrom: + secretKeyRef: + name: api + key: DB_PASS + - name: DB_URL + valueFrom: + secretKeyRef: + name: api + key: DB_URL + - name: DB_NAME + valueFrom: + secretKeyRef: + name: api + key: DB_NAME + - name: NOTIFICATION_API_KEY + valueFrom: + secretKeyRef: + name: api + key: NOTIFICATION_API_KEY + - name: NOTIFICATION_API_URL + valueFrom: + secretKeyRef: + name: api + key: NOTIFICATION_API_URL + - name: NOTIFICATION_ORG_FOOTPRINT_EN + valueFrom: + secretKeyRef: + name: api + key: NOTIFICATION_ORG_FOOTPRINT_EN + - name: NOTIFICATION_ORG_FOOTPRINT_FR + valueFrom: + secretKeyRef: + name: api + key: NOTIFICATION_ORG_FOOTPRINT_FR + - name: SERVICE_ACCOUNT_EMAIL + valueFrom: + secretKeyRef: + name: api + key: SERVICE_ACCOUNT_EMAIL + - name: REDIRECT_TO_SERVICE_ACCOUNT_EMAIL + valueFrom: + secretKeyRef: + name: api + key: REDIRECT_TO_SERVICE_ACCOUNT_EMAIL + restartPolicy: OnFailure diff --git a/k8s/apps/bases/frontend/deployment.yaml b/k8s/apps/bases/frontend/deployment.yaml index 0180e35d38..f9ae0d6e8c 100644 --- a/k8s/apps/bases/frontend/deployment.yaml +++ b/k8s/apps/bases/frontend/deployment.yaml @@ -20,7 +20,7 @@ spec: app: tracker-frontend spec: containers: - - image: gcr.io/track-compliance/frontend:master-a802403-1680797001 # {"$imagepolicy": "flux-system:frontend"} + - image: gcr.io/track-compliance/frontend:master-af5d359-1687177261 # {"$imagepolicy": "flux-system:frontend"} name: frontend resources: limits: diff --git a/k8s/apps/bases/scanners/dmarc-report-cronjob/cronjob.yaml b/k8s/apps/bases/scanners/dmarc-report-cronjob/cronjob.yaml index ec6022c045..cd9611245a 100644 --- a/k8s/apps/bases/scanners/dmarc-report-cronjob/cronjob.yaml +++ b/k8s/apps/bases/scanners/dmarc-report-cronjob/cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: name: dmarc-report @@ -13,7 +13,7 @@ spec: spec: containers: - name: dmarc-report - image: gcr.io/track-compliance/dmarc-report:master-770f2e2-1674589724 # {"$imagepolicy": "flux-system:dmarc-report"} + image: gcr.io/track-compliance/dmarc-report:master-927f20c-1685459197 # {"$imagepolicy": "flux-system:dmarc-report"} env: - name: DB_NAME valueFrom: diff --git a/k8s/apps/bases/scanners/dns-processor/deployment.yaml b/k8s/apps/bases/scanners/dns-processor/deployment.yaml index 0745864eb4..35d8c6c9be 100644 --- a/k8s/apps/bases/scanners/dns-processor/deployment.yaml +++ b/k8s/apps/bases/scanners/dns-processor/deployment.yaml @@ -18,7 +18,7 @@ spec: spec: containers: - name: dns-processor - image: gcr.io/track-compliance/dns-processor:master-c111971-1681319424 # {"$imagepolicy": "flux-system:dns-processor"} + image: gcr.io/track-compliance/dns-processor:master-cdbea07-1682441075 # {"$imagepolicy": "flux-system:dns-processor"} env: - name: DB_NAME value: track_dmarc diff --git a/k8s/apps/bases/scanners/dns-scanner/deployment.yaml b/k8s/apps/bases/scanners/dns-scanner/deployment.yaml index c93779bfc7..d15f4a048d 100644 --- a/k8s/apps/bases/scanners/dns-scanner/deployment.yaml +++ b/k8s/apps/bases/scanners/dns-scanner/deployment.yaml @@ -17,7 +17,7 @@ spec: spec: containers: - name: dns-scanner - image: gcr.io/track-compliance/dns-scanner:master-5474a22-1681310508 # {"$imagepolicy": "flux-system:dns-scanner"} + image: gcr.io/track-compliance/dns-scanner:master-0ca5e9b-1684842738 # {"$imagepolicy": "flux-system:dns-scanner"} env: - name: PYTHONWARNINGS value: ignore diff --git a/k8s/apps/bases/scanners/domain-dispatcher-cronjob/cronjob.yaml b/k8s/apps/bases/scanners/domain-dispatcher-cronjob/cronjob.yaml index adf79210f0..f4408db2ac 100644 --- a/k8s/apps/bases/scanners/domain-dispatcher-cronjob/cronjob.yaml +++ b/k8s/apps/bases/scanners/domain-dispatcher-cronjob/cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: name: domain-dispatcher @@ -16,7 +16,7 @@ spec: spec: containers: - name: domain-dispatcher - image: gcr.io/track-compliance/domain-dispatcher:master-bdfe07c-1678454331 # {"$imagepolicy": "flux-system:domain-dispatcher"} + image: gcr.io/track-compliance/domain-dispatcher:master-927f20c-1685459197 # {"$imagepolicy": "flux-system:domain-dispatcher"} env: - name: DB_PASS valueFrom: diff --git a/k8s/apps/bases/scanners/log4shell-scanner/deployment.yaml b/k8s/apps/bases/scanners/log4shell-scanner/deployment.yaml index 83f613e6d8..c913b16e6f 100644 --- a/k8s/apps/bases/scanners/log4shell-scanner/deployment.yaml +++ b/k8s/apps/bases/scanners/log4shell-scanner/deployment.yaml @@ -15,7 +15,7 @@ spec: spec: containers: - name: log4shell-scanner - image: gcr.io/track-compliance/log4shell-scanner:master-74c49c3-1670863247 # {"$imagepolicy": "flux-system:log4shell-scanner"} + image: gcr.io/track-compliance/log4shell-scanner:master-8d91653-1685369491 # {"$imagepolicy": "flux-system:log4shell-scanner"} env: - name: PYTHONWARNINGS value: ignore diff --git a/k8s/apps/bases/scanners/summaries-cronjob/cronjob.yaml b/k8s/apps/bases/scanners/summaries-cronjob/cronjob.yaml index 8f47d8d971..35ef98f9de 100644 --- a/k8s/apps/bases/scanners/summaries-cronjob/cronjob.yaml +++ b/k8s/apps/bases/scanners/summaries-cronjob/cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: name: summaries @@ -13,7 +13,7 @@ spec: spec: containers: - name: summaries - image: gcr.io/track-compliance/services/summaries:master-cbbb2fd-1681235183 # {"$imagepolicy": "flux-system:summaries"} + image: gcr.io/track-compliance/services/summaries:master-955c53b-1685969471 # {"$imagepolicy": "flux-system:summaries"} env: - name: DB_USER valueFrom: diff --git a/k8s/apps/bases/scanners/web-processor/deployment.yaml b/k8s/apps/bases/scanners/web-processor/deployment.yaml index 9e9f2ca57c..71e957e1e9 100644 --- a/k8s/apps/bases/scanners/web-processor/deployment.yaml +++ b/k8s/apps/bases/scanners/web-processor/deployment.yaml @@ -18,7 +18,7 @@ spec: spec: containers: - name: web-processor - image: gcr.io/track-compliance/web-processor:master-1f34ed3-1681232023 # {"$imagepolicy": "flux-system:web-processor"} + image: gcr.io/track-compliance/web-processor:master-e9d5cd3-1685369546 # {"$imagepolicy": "flux-system:web-processor"} env: - name: DB_NAME value: track_dmarc diff --git a/k8s/apps/bases/scanners/web-scanner/deployment.yaml b/k8s/apps/bases/scanners/web-scanner/deployment.yaml index 2b205a989c..7ac6603989 100644 --- a/k8s/apps/bases/scanners/web-scanner/deployment.yaml +++ b/k8s/apps/bases/scanners/web-scanner/deployment.yaml @@ -19,7 +19,7 @@ spec: spec: containers: - name: web-scanner - image: gcr.io/track-compliance/web-scanner:master-1175b1e-1681325623 # {"$imagepolicy": "flux-system:web-scanner"} + image: gcr.io/track-compliance/web-scanner:master-769f1ba-1682958826 # {"$imagepolicy": "flux-system:web-scanner"} env: - name: PYTHONWARNINGS value: ignore diff --git a/k8s/apps/overlays/gke/domain-dispatcher-suspend-patch.yaml b/k8s/apps/overlays/gke/domain-dispatcher-suspend-patch.yaml index 8a5968f08f..f54d1d5eab 100644 --- a/k8s/apps/overlays/gke/domain-dispatcher-suspend-patch.yaml +++ b/k8s/apps/overlays/gke/domain-dispatcher-suspend-patch.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: name: domain-dispatcher diff --git a/k8s/apps/overlays/staging/domain-dispatcher-suspend-patch.yaml b/k8s/apps/overlays/staging/domain-dispatcher-suspend-patch.yaml index 8a5968f08f..f54d1d5eab 100644 --- a/k8s/apps/overlays/staging/domain-dispatcher-suspend-patch.yaml +++ b/k8s/apps/overlays/staging/domain-dispatcher-suspend-patch.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: name: domain-dispatcher diff --git a/k8s/clusters/auto-image-update/bases/image-repo-policies/kustomization.yaml b/k8s/clusters/auto-image-update/bases/image-repo-policies/kustomization.yaml index 3cd864e296..7cc8ac0388 100644 --- a/k8s/clusters/auto-image-update/bases/image-repo-policies/kustomization.yaml +++ b/k8s/clusters/auto-image-update/bases/image-repo-policies/kustomization.yaml @@ -10,6 +10,7 @@ resources: - guidance-image-repo-policy.yaml - log4shell-processor-image-repo-policy.yaml - log4shell-scanner-image-repo-policy.yaml + - org-footprint-image-repo-policy.yaml - spring4shell-scanner-image-repo-policy.yaml - summaries-image-repo-policy.yaml - super-admin-image-repo-policy.yaml diff --git a/k8s/clusters/auto-image-update/bases/image-repo-policies/org-footprint-image-repo-policy.yaml b/k8s/clusters/auto-image-update/bases/image-repo-policies/org-footprint-image-repo-policy.yaml new file mode 100644 index 0000000000..72e33e4e6a --- /dev/null +++ b/k8s/clusters/auto-image-update/bases/image-repo-policies/org-footprint-image-repo-policy.yaml @@ -0,0 +1,23 @@ +apiVersion: image.toolkit.fluxcd.io/v1alpha2 +kind: ImageRepository +metadata: + name: org-footprint + namespace: flux-system +spec: + image: gcr.io/track-compliance/services/org-footprint + interval: 1m0s +--- +apiVersion: image.toolkit.fluxcd.io/v1alpha1 +kind: ImagePolicy +metadata: + name: org-footprint + namespace: flux-system +spec: + filterTags: + extract: $ts + pattern: ^master-[a-fA-F0-9]+-(?P.*) + imageRepositoryRef: + name: org-footprint + policy: + numerical: + order: asc diff --git a/k8s/infrastructure/bases/arangodb/arangodb-deployment.yaml b/k8s/infrastructure/bases/arangodb/arangodb-deployment.yaml index 84bcf28b51..6b141312a8 100644 --- a/k8s/infrastructure/bases/arangodb/arangodb-deployment.yaml +++ b/k8s/infrastructure/bases/arangodb/arangodb-deployment.yaml @@ -6,7 +6,7 @@ metadata: labels: app: 'arangodb' spec: - image: arangodb/arangodb:3.10.4 + image: arangodb/arangodb:3.10.5 environment: Production mode: Cluster tls: diff --git a/k8s/infrastructure/bases/arangodb/platform/namespace.yaml b/k8s/infrastructure/bases/arangodb/platform/namespace.yaml index 7b87ddcc0a..2dc4428732 100644 --- a/k8s/infrastructure/bases/arangodb/platform/namespace.yaml +++ b/k8s/infrastructure/bases/arangodb/platform/namespace.yaml @@ -3,4 +3,4 @@ kind: Namespace metadata: name: db labels: - istio-injection: enabled + istio-injection: disabled diff --git a/k8s/infrastructure/overlays/gke/backup-cronjob.yaml b/k8s/infrastructure/overlays/gke/backup-cronjob.yaml index 45e6cef7dd..06ce3410e2 100644 --- a/k8s/infrastructure/overlays/gke/backup-cronjob.yaml +++ b/k8s/infrastructure/overlays/gke/backup-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: labels: diff --git a/k8s/infrastructure/overlays/production/backup-cronjob.yaml b/k8s/infrastructure/overlays/production/backup-cronjob.yaml index 10b2e602bd..20c0620d50 100644 --- a/k8s/infrastructure/overlays/production/backup-cronjob.yaml +++ b/k8s/infrastructure/overlays/production/backup-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: labels: diff --git a/k8s/infrastructure/overlays/staging/backup-cronjob.yaml b/k8s/infrastructure/overlays/staging/backup-cronjob.yaml index 10b2e602bd..20c0620d50 100644 --- a/k8s/infrastructure/overlays/staging/backup-cronjob.yaml +++ b/k8s/infrastructure/overlays/staging/backup-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: labels: diff --git a/k8s/infrastructure/overlays/test/backup-cronjob.yaml b/k8s/infrastructure/overlays/test/backup-cronjob.yaml index e86cb5ba37..5c4ae9b263 100644 --- a/k8s/infrastructure/overlays/test/backup-cronjob.yaml +++ b/k8s/infrastructure/overlays/test/backup-cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: batch/v1 kind: CronJob metadata: labels: diff --git a/k8s/jobs/bases/super-admin/super-admin-job.yaml b/k8s/jobs/bases/super-admin/super-admin-job.yaml index 9540e02330..5c5bc801e2 100644 --- a/k8s/jobs/bases/super-admin/super-admin-job.yaml +++ b/k8s/jobs/bases/super-admin/super-admin-job.yaml @@ -8,7 +8,7 @@ spec: spec: containers: - name: super-admin - image: gcr.io/track-compliance/super-admin:master-a802403-1680796755 # {"$imagepolicy": "flux-system:super-admin"} + image: gcr.io/track-compliance/super-admin:master-927f20c-1685459193 # {"$imagepolicy": "flux-system:super-admin"} env: - name: DB_PASS valueFrom: diff --git a/scanners/dns-processor/service.py b/scanners/dns-processor/service.py index a5cd136e6b..c3c4d4eb22 100644 --- a/scanners/dns-processor/service.py +++ b/scanners/dns-processor/service.py @@ -82,6 +82,7 @@ async def reconnected_cb(): logger.info(f"Connected to NATS at {nc.connected_url.netloc}...") async def subscribe_handler(msg): + await asyncio.sleep(0.01) subject = msg.subject reply = msg.reply data = msg.data.decode() diff --git a/scanners/dns-scanner/requirements.txt b/scanners/dns-scanner/requirements.txt index 7436805f76..3a80baa147 100644 --- a/scanners/dns-scanner/requirements.txt +++ b/scanners/dns-scanner/requirements.txt @@ -13,7 +13,7 @@ pycparser==2.21 pyleri==1.4.1 PyNaCl==1.5.0 python-dotenv==0.21.0 -requests==2.28.1 +requests==2.31.0 requests-file==1.5.1 six==1.16.0 timeout-decorator==0.5.0 diff --git a/scanners/dns-scanner/service.py b/scanners/dns-scanner/service.py index 23cf836692..d804ec4f29 100644 --- a/scanners/dns-scanner/service.py +++ b/scanners/dns-scanner/service.py @@ -52,6 +52,7 @@ async def reconnected_cb(): logger.info(f"Connected to NATS at {nc.connected_url.netloc}...") async def subscribe_handler(msg): + await asyncio.sleep(0.01) subject = msg.subject reply = msg.reply data = msg.data.decode() diff --git a/scanners/domain-dispatcher/cloudbuild.yaml b/scanners/domain-dispatcher/cloudbuild.yaml index a7a7b49e8b..a0410c0d10 100644 --- a/scanners/domain-dispatcher/cloudbuild.yaml +++ b/scanners/domain-dispatcher/cloudbuild.yaml @@ -6,7 +6,7 @@ steps: args: [ '-c', - 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb', + 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb/arangodb:3.10.4', ] - name: mikewilliamson/wait-for diff --git a/scanners/domain-dispatcher/domain-dispatcher-job.yaml b/scanners/domain-dispatcher/domain-dispatcher-job.yaml index 0f1c6406cf..eda30a4794 100644 --- a/scanners/domain-dispatcher/domain-dispatcher-job.yaml +++ b/scanners/domain-dispatcher/domain-dispatcher-job.yaml @@ -12,7 +12,7 @@ spec: spec: containers: - name: domain-dispatcher - image: gcr.io/track-compliance/domain-dispatcher:master-bdfe07c-1678454331 # {"$imagepolicy": "flux-system:domain-dispatcher"} + image: gcr.io/track-compliance/domain-dispatcher:master-927f20c-1685459197 # {"$imagepolicy": "flux-system:domain-dispatcher"} env: - name: DB_PASS valueFrom: diff --git a/scanners/log4shell-scanner/Pipfile.lock b/scanners/log4shell-scanner/Pipfile.lock index f1f6bb0f70..06d2221aef 100644 --- a/scanners/log4shell-scanner/Pipfile.lock +++ b/scanners/log4shell-scanner/Pipfile.lock @@ -23,26 +23,99 @@ }, "certifi": { "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", + "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" ], "markers": "python_version >= '3.6'", - "version": "==2022.12.7" + "version": "==2023.5.7" }, "charset-normalizer": { "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", + "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", + "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", + "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", + "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", + "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", + "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", + "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", + "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", + "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", + "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", + "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", + "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", + "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", + "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", + "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", + "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", + "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", + "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", + "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", + "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", + "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", + "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", + "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", + "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", + "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", + "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", + "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", + "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", + "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", + "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", + "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", + "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", + "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", + "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", + "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", + "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", + "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", + "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", + "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", + "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", + "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", + "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", + "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", + "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", + "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", + "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", + "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", + "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", + "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", + "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", + "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", + "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", + "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", + "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", + "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", + "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", + "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", + "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", + "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", + "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", + "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", + "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", + "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", + "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", + "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", + "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", + "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", + "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", + "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", + "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", + "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", + "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", + "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", + "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" ], - "markers": "python_version >= '3'", - "version": "==2.0.12" + "markers": "python_full_version >= '3.7.0'", + "version": "==3.1.0" }, "idna": { "hashes": [ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '3'", + "markers": "python_version >= '3.5'", "version": "==3.4" }, "python-dotenv": { @@ -55,19 +128,19 @@ }, "requests": { "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "version": "==2.26.0" + "version": "==2.31.0" }, "urllib3": { "hashes": [ - "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", - "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" + "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc", + "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.13" + "markers": "python_full_version >= '3.7.0'", + "version": "==2.0.2" } }, "develop": { @@ -86,120 +159,121 @@ }, "mypy-extensions": { "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" ], - "version": "==0.4.3" + "markers": "python_version >= '3.5'", + "version": "==1.0.0" }, "pathspec": { "hashes": [ - "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6", - "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6" + "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", + "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" ], "markers": "python_version >= '3.7'", - "version": "==0.10.3" + "version": "==0.11.1" }, "platformdirs": { "hashes": [ - "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca", - "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e" + "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f", + "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5" ], "markers": "python_version >= '3.7'", - "version": "==2.6.0" + "version": "==3.5.1" }, "regex": { "hashes": [ - "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad", - "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4", - "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd", - "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc", - "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d", - "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066", - "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec", - "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9", - "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e", - "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8", - "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e", - "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783", - "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6", - "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1", - "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c", - "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4", - "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1", - "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1", - "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7", - "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8", - "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe", - "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d", - "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b", - "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8", - "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c", - "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af", - "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49", - "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714", - "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542", - "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318", - "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e", - "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5", - "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc", - "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144", - "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453", - "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5", - "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61", - "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11", - "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a", - "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54", - "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73", - "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc", - "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347", - "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c", - "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66", - "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c", - "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93", - "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443", - "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc", - "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1", - "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892", - "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8", - "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001", - "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa", - "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90", - "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c", - "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0", - "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692", - "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4", - "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5", - "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690", - "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83", - "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66", - "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f", - "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f", - "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4", - "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee", - "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81", - "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95", - "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9", - "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff", - "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e", - "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5", - "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6", - "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7", - "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1", - "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394", - "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6", - "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742", - "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57", - "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b", - "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7", - "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b", - "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244", - "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af", - "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185", - "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8", - "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5" + "sha256:02f4541550459c08fdd6f97aa4e24c6f1932eec780d58a2faa2068253df7d6ff", + "sha256:0a69cf0c00c4d4a929c6c7717fd918414cab0d6132a49a6d8fc3ded1988ed2ea", + "sha256:0bbd5dcb19603ab8d2781fac60114fb89aee8494f4505ae7ad141a3314abb1f9", + "sha256:10250a093741ec7bf74bcd2039e697f519b028518f605ff2aa7ac1e9c9f97423", + "sha256:10374c84ee58c44575b667310d5bbfa89fb2e64e52349720a0182c0017512f6c", + "sha256:1189fbbb21e2c117fda5303653b61905aeeeea23de4a94d400b0487eb16d2d60", + "sha256:1307aa4daa1cbb23823d8238e1f61292fd07e4e5d8d38a6efff00b67a7cdb764", + "sha256:144b5b017646b5a9392a5554a1e5db0000ae637be4971c9747566775fc96e1b2", + "sha256:171c52e320fe29260da550d81c6b99f6f8402450dc7777ef5ced2e848f3b6f8f", + "sha256:18196c16a584619c7c1d843497c069955d7629ad4a3fdee240eb347f4a2c9dbe", + "sha256:18f05d14f14a812fe9723f13afafefe6b74ca042d99f8884e62dbd34dcccf3e2", + "sha256:1ecf3dcff71f0c0fe3e555201cbe749fa66aae8d18f80d2cc4de8e66df37390a", + "sha256:21e90a288e6ba4bf44c25c6a946cb9b0f00b73044d74308b5e0afd190338297c", + "sha256:23d86ad2121b3c4fc78c58f95e19173790e22ac05996df69b84e12da5816cb17", + "sha256:256f7f4c6ba145f62f7a441a003c94b8b1af78cee2cccacfc1e835f93bc09426", + "sha256:290fd35219486dfbc00b0de72f455ecdd63e59b528991a6aec9fdfc0ce85672e", + "sha256:2e9c4f778514a560a9c9aa8e5538bee759b55f6c1dcd35613ad72523fd9175b8", + "sha256:338994d3d4ca4cf12f09822e025731a5bdd3a37aaa571fa52659e85ca793fb67", + "sha256:33d430a23b661629661f1fe8395be2004006bc792bb9fc7c53911d661b69dd7e", + "sha256:385992d5ecf1a93cb85adff2f73e0402dd9ac29b71b7006d342cc920816e6f32", + "sha256:3d45864693351c15531f7e76f545ec35000d50848daa833cead96edae1665559", + "sha256:40005cbd383438aecf715a7b47fe1e3dcbc889a36461ed416bdec07e0ef1db66", + "sha256:4035d6945cb961c90c3e1c1ca2feb526175bcfed44dfb1cc77db4fdced060d3e", + "sha256:445d6f4fc3bd9fc2bf0416164454f90acab8858cd5a041403d7a11e3356980e8", + "sha256:48c9ec56579d4ba1c88f42302194b8ae2350265cb60c64b7b9a88dcb7fbde309", + "sha256:4a5059bd585e9e9504ef9c07e4bc15b0a621ba20504388875d66b8b30a5c4d18", + "sha256:4a6e4b0e0531223f53bad07ddf733af490ba2b8367f62342b92b39b29f72735a", + "sha256:4b870b6f632fc74941cadc2a0f3064ed8409e6f8ee226cdfd2a85ae50473aa94", + "sha256:50fd2d9b36938d4dcecbd684777dd12a407add4f9f934f235c66372e630772b0", + "sha256:53e22e4460f0245b468ee645156a4f84d0fc35a12d9ba79bd7d79bdcd2f9629d", + "sha256:586a011f77f8a2da4b888774174cd266e69e917a67ba072c7fc0e91878178a80", + "sha256:59597cd6315d3439ed4b074febe84a439c33928dd34396941b4d377692eca810", + "sha256:59e4b729eae1a0919f9e4c0fc635fbcc9db59c74ad98d684f4877be3d2607dd6", + "sha256:5a0f874ee8c0bc820e649c900243c6d1e6dc435b81da1492046716f14f1a2a96", + "sha256:5ac2b7d341dc1bd102be849d6dd33b09701223a851105b2754339e390be0627a", + "sha256:5e3f4468b8c6fd2fd33c218bbd0a1559e6a6fcf185af8bb0cc43f3b5bfb7d636", + "sha256:6164d4e2a82f9ebd7752a06bd6c504791bedc6418c0196cd0a23afb7f3e12b2d", + "sha256:6893544e06bae009916a5658ce7207e26ed17385149f35a3125f5259951f1bbe", + "sha256:690a17db524ee6ac4a27efc5406530dd90e7a7a69d8360235323d0e5dafb8f5b", + "sha256:6b8d0c153f07a953636b9cdb3011b733cadd4178123ef728ccc4d5969e67f3c2", + "sha256:72a28979cc667e5f82ef433db009184e7ac277844eea0f7f4d254b789517941d", + "sha256:72aa4746993a28c841e05889f3f1b1e5d14df8d3daa157d6001a34c98102b393", + "sha256:732176f5427e72fa2325b05c58ad0b45af341c459910d766f814b0584ac1f9ac", + "sha256:7918a1b83dd70dc04ab5ed24c78ae833ae8ea228cef84e08597c408286edc926", + "sha256:7923470d6056a9590247ff729c05e8e0f06bbd4efa6569c916943cb2d9b68b91", + "sha256:7d76a8a1fc9da08296462a18f16620ba73bcbf5909e42383b253ef34d9d5141e", + "sha256:811040d7f3dd9c55eb0d8b00b5dcb7fd9ae1761c454f444fd9f37fe5ec57143a", + "sha256:821a88b878b6589c5068f4cc2cfeb2c64e343a196bc9d7ac68ea8c2a776acd46", + "sha256:84397d3f750d153ebd7f958efaa92b45fea170200e2df5e0e1fd4d85b7e3f58a", + "sha256:844671c9c1150fcdac46d43198364034b961bd520f2c4fdaabfc7c7d7138a2dd", + "sha256:890a09cb0a62198bff92eda98b2b507305dd3abf974778bae3287f98b48907d3", + "sha256:8f08276466fedb9e36e5193a96cb944928301152879ec20c2d723d1031cd4ddd", + "sha256:8f5e06df94fff8c4c85f98c6487f6636848e1dc85ce17ab7d1931df4a081f657", + "sha256:921473a93bcea4d00295799ab929522fc650e85c6b9f27ae1e6bb32a790ea7d3", + "sha256:941b3f1b2392f0bcd6abf1bc7a322787d6db4e7457be6d1ffd3a693426a755f2", + "sha256:9b320677521aabf666cdd6e99baee4fb5ac3996349c3b7f8e7c4eee1c00dfe3a", + "sha256:9c3efee9bb53cbe7b285760c81f28ac80dc15fa48b5fe7e58b52752e642553f1", + "sha256:9fda3e50abad8d0f48df621cf75adc73c63f7243cbe0e3b2171392b445401550", + "sha256:a4c5da39bca4f7979eefcbb36efea04471cd68db2d38fcbb4ee2c6d440699833", + "sha256:a56c18f21ac98209da9c54ae3ebb3b6f6e772038681d6cb43b8d53da3b09ee81", + "sha256:a623564d810e7a953ff1357f7799c14bc9beeab699aacc8b7ab7822da1e952b8", + "sha256:a8906669b03c63266b6a7693d1f487b02647beb12adea20f8840c1a087e2dfb5", + "sha256:a99757ad7fe5c8a2bb44829fc57ced11253e10f462233c1255fe03888e06bc19", + "sha256:aa7d032c1d84726aa9edeb6accf079b4caa87151ca9fabacef31fa028186c66d", + "sha256:aad5524c2aedaf9aa14ef1bc9327f8abd915699dea457d339bebbe2f0d218f86", + "sha256:afb1c70ec1e594a547f38ad6bf5e3d60304ce7539e677c1429eebab115bce56e", + "sha256:b6365703e8cf1644b82104cdd05270d1a9f043119a168d66c55684b1b557d008", + "sha256:b8b942d8b3ce765dbc3b1dad0a944712a89b5de290ce8f72681e22b3c55f3cc8", + "sha256:ba73a14e9c8f9ac409863543cde3290dba39098fc261f717dc337ea72d3ebad2", + "sha256:bd7b68fd2e79d59d86dcbc1ccd6e2ca09c505343445daaa4e07f43c8a9cc34da", + "sha256:bd966475e963122ee0a7118ec9024388c602d12ac72860f6eea119a3928be053", + "sha256:c2ce65bdeaf0a386bb3b533a28de3994e8e13b464ac15e1e67e4603dd88787fa", + "sha256:c64d5abe91a3dfe5ff250c6bb267ef00dbc01501518225b45a5f9def458f31fb", + "sha256:c8c143a65ce3ca42e54d8e6fcaf465b6b672ed1c6c90022794a802fb93105d22", + "sha256:cd46f30e758629c3ee91713529cfbe107ac50d27110fdcc326a42ce2acf4dafc", + "sha256:ced02e3bd55e16e89c08bbc8128cff0884d96e7f7a5633d3dc366b6d95fcd1d6", + "sha256:cf123225945aa58b3057d0fba67e8061c62d14cc8a4202630f8057df70189051", + "sha256:d19e57f888b00cd04fc38f5e18d0efbd91ccba2d45039453ab2236e6eec48d4d", + "sha256:d1cbe6b5be3b9b698d8cc4ee4dee7e017ad655e83361cd0ea8e653d65e469468", + "sha256:db09e6c18977a33fea26fe67b7a842f706c67cf8bda1450974d0ae0dd63570df", + "sha256:de2f780c3242ea114dd01f84848655356af4dd561501896c751d7b885ea6d3a1", + "sha256:e2205a81f815b5bb17e46e74cc946c575b484e5f0acfcb805fb252d67e22938d", + "sha256:e645c757183ee0e13f0bbe56508598e2d9cd42b8abc6c0599d53b0d0b8dd1479", + "sha256:f2910502f718828cecc8beff004917dcf577fc5f8f5dd40ffb1ea7612124547b", + "sha256:f764e4dfafa288e2eba21231f455d209f4709436baeebb05bdecfb5d8ddc3d35", + "sha256:f83fe9e10f9d0b6cf580564d4d23845b9d692e4c91bd8be57733958e4c602956", + "sha256:fb2b495dd94b02de8215625948132cc2ea360ae84fe6634cd19b6567709c8ae2", + "sha256:fee0016cc35a8a91e8cc9312ab26a6fe638d484131a7afa79e1ce6165328a135" ], "markers": "python_version >= '3.6'", - "version": "==2022.10.31" + "version": "==2023.5.5" }, "tomli": { "hashes": [ @@ -211,11 +285,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c", + "sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98" ], "markers": "python_version >= '3.7'", - "version": "==4.4.0" + "version": "==4.6.2" } } } diff --git a/scanners/log4shell-scanner/requirements.txt b/scanners/log4shell-scanner/requirements.txt index e96158bda6..a27e669a99 100644 --- a/scanners/log4shell-scanner/requirements.txt +++ b/scanners/log4shell-scanner/requirements.txt @@ -7,9 +7,9 @@ -i https://pypi.org/simple/ asyncio-nats-client==0.11.4 -certifi==2022.12.7; python_version >= '3.6' -charset-normalizer==2.0.12; python_version >= '3' -idna==3.4; python_version >= '3' +certifi==2023.5.7; python_version >= '3.6' +charset-normalizer==3.1.0; python_full_version >= '3.7.0' +idna==3.4; python_version >= '3.5' python-dotenv==0.19.2 -requests==2.26.0 -urllib3==1.26.13; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' +requests==2.31.0 +urllib3==2.0.2; python_full_version >= '3.7.0' diff --git a/scanners/spring4shell/domain-dispatcher-job.yaml b/scanners/spring4shell/domain-dispatcher-job.yaml index 0f1c6406cf..eda30a4794 100644 --- a/scanners/spring4shell/domain-dispatcher-job.yaml +++ b/scanners/spring4shell/domain-dispatcher-job.yaml @@ -12,7 +12,7 @@ spec: spec: containers: - name: domain-dispatcher - image: gcr.io/track-compliance/domain-dispatcher:master-bdfe07c-1678454331 # {"$imagepolicy": "flux-system:domain-dispatcher"} + image: gcr.io/track-compliance/domain-dispatcher:master-927f20c-1685459197 # {"$imagepolicy": "flux-system:domain-dispatcher"} env: - name: DB_PASS valueFrom: diff --git a/scanners/web-processor/Dockerfile b/scanners/web-processor/Dockerfile index 5c99dced9f..dd03a9244a 100644 --- a/scanners/web-processor/Dockerfile +++ b/scanners/web-processor/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.7-slim-bullseye AS python-builder +FROM python:3.10.11-slim-bullseye AS python-builder # Copy local code to the container image. ENV PYTHONUNBUFFERED 1 @@ -23,7 +23,7 @@ RUN pip3 install --prefix=/working/install -r /requirements.txt #=============================================================================================== -FROM python:3.10.7-slim-bullseye +FROM python:3.10.11-slim-bullseye # Copy local code to the container image. ENV PYTHONWARNINGS ignore diff --git a/scanners/web-processor/requirements.txt b/scanners/web-processor/requirements.txt index 1bb8c7f8aa..566f794452 100644 --- a/scanners/web-processor/requirements.txt +++ b/scanners/web-processor/requirements.txt @@ -5,6 +5,6 @@ nats-py==2.1.3 PyJWT==2.4.0 python-arango==7.3.4 python-dotenv==0.20.0 -requests==2.28.0 +requests==2.31.0 requests-toolbelt==0.9.1 urllib3==1.26.9 diff --git a/scanners/web-processor/service.py b/scanners/web-processor/service.py index c25f8a0f0a..971c73260a 100644 --- a/scanners/web-processor/service.py +++ b/scanners/web-processor/service.py @@ -85,6 +85,7 @@ async def reconnected_cb(): logger.info(f"Connected to NATS at {nc.connected_url.netloc}...") async def subscribe_handler(msg): + await asyncio.sleep(0.01) subject = msg.subject reply = msg.reply data = msg.data.decode() diff --git a/scanners/web-processor/web_processor/web_processor.py b/scanners/web-processor/web_processor/web_processor.py index cd1035356e..bf0bc72e0f 100644 --- a/scanners/web-processor/web_processor/web_processor.py +++ b/scanners/web-processor/web_processor/web_processor.py @@ -306,7 +306,10 @@ def check_https_downgrades(connections): "preload": preload } - hsts_status = "pass" if hsts and max_age > 0 and "https14" not in negative_tags else "fail" + if hsts and isinstance(max_age, int) and max_age > 0 and "https14" not in negative_tags: + hsts_status = "pass" + else: + hsts_status = "fail" http_down_or_redirect = not http_live or http_immediately_upgrades diff --git a/scanners/web-scanner/Dockerfile b/scanners/web-scanner/Dockerfile index 313b2541fe..22bb5ef163 100644 --- a/scanners/web-scanner/Dockerfile +++ b/scanners/web-scanner/Dockerfile @@ -4,7 +4,7 @@ RUN cargo install --git https://github.com/mozilla/crlite rust-query-crlite --re #=============================================================================================== #=============================================================================================== -FROM python:3.10.7-slim-bullseye AS python-builder +FROM python:3.10.11-slim-bullseye AS python-builder # Copy local code to the container image. ENV PYTHONUNBUFFERED 1 @@ -26,7 +26,7 @@ RUN pip3 install --prefix=/working/install -r /requirements.txt #=============================================================================================== #=============================================================================================== -FROM python:3.10.7-slim-bullseye +FROM python:3.10.11-slim-bullseye # Copy local code to the container image. ENV PYTHONUNBUFFERED 1 diff --git a/scanners/web-scanner/requirements.txt b/scanners/web-scanner/requirements.txt index be6cfb2cd1..77bf8a8c9a 100644 --- a/scanners/web-scanner/requirements.txt +++ b/scanners/web-scanner/requirements.txt @@ -1,14 +1,14 @@ certifi==2022.12.7 cffi==1.15.1 charset-normalizer==2.1.1 -cryptography==39.0.1 +cryptography==41.0.0 idna==3.4 nassl==5.0.1 nats-py==2.2.0 pycparser==2.21 pydantic==1.9.2 python-dotenv==0.21.0 -requests==2.28.1 +requests==2.31.0 requests-toolbelt==0.10.1 sslyze==5.1.3 tls-parser==2.0.0 diff --git a/scanners/web-scanner/scan/endpoint_chain_scanner/endpoint_chain_scanner.py b/scanners/web-scanner/scan/endpoint_chain_scanner/endpoint_chain_scanner.py index dcec0e6214..9a6b6e8059 100644 --- a/scanners/web-scanner/scan/endpoint_chain_scanner/endpoint_chain_scanner.py +++ b/scanners/web-scanner/scan/endpoint_chain_scanner/endpoint_chain_scanner.py @@ -10,9 +10,12 @@ logger = logging.getLogger(__name__) -TIMEOUT = 10 +# Set the default timeout for requests (connect, read) +TIMEOUT = (1.0, 10) CONNECTION_ERROR = "CONNECTION_ERROR" +CONNECTION_TIMEOUT_ERROR = "CONNECTION_TIMEOUT_ERROR" +READ_TIMEOUT_ERROR = "READ_TIMEOUT_ERROR" TIMEOUT_ERROR = "TIMEOUT_ERROR" UNKNOWN_ERROR = "UNKNOWN_ERROR" @@ -128,6 +131,20 @@ def request_connection(uri: Optional[str] = None, connection = HTTPSConnectionRequest(uri=uri, http_response=response) return {"connection": connection, "response": response} + except requests.exceptions.ConnectTimeout as e: + logger.error(f"Connection timeout error {context}: {str(e)}") + if scheme.lower() == "http": + connection = HTTPConnectionRequest(uri=uri, error=CONNECTION_TIMEOUT_ERROR) + elif scheme.lower() == "https": + connection = HTTPSConnectionRequest(uri=uri, error=CONNECTION_TIMEOUT_ERROR) + return {"connection": connection, "response": response} + except requests.exceptions.ReadTimeout as e: + logger.error(f"Read timeout error {context}: {str(e)}") + if scheme.lower() == "http": + connection = HTTPConnectionRequest(uri=uri, error=READ_TIMEOUT_ERROR) + elif scheme.lower() == "https": + connection = HTTPSConnectionRequest(uri=uri, error=READ_TIMEOUT_ERROR) + return {"connection": connection, "response": response} except requests.exceptions.Timeout as e: logger.error(f"Timeout error {context}: {str(e)}") if scheme.lower() == "http": diff --git a/scanners/web-scanner/scan/tls_scanner/tls_scanner.py b/scanners/web-scanner/scan/tls_scanner/tls_scanner.py index 9e5906a19c..2dc5fa3495 100644 --- a/scanners/web-scanner/scan/tls_scanner/tls_scanner.py +++ b/scanners/web-scanner/scan/tls_scanner/tls_scanner.py @@ -24,8 +24,7 @@ logger = logging.getLogger() -TIMEOUT = int(os.getenv("SCAN_TIMEOUT", "80")) - +CONNECT_TIMEOUT = 1 @dataclass class AcceptedCipherSuites: @@ -184,9 +183,15 @@ def __init__(self, domain: str, ip_address: str = None): try: scan_request = ServerScanRequest( - server_location=ServerNetworkLocation(hostname=domain, - ip_address=ip_address), - scan_commands=designated_scans + server_location=ServerNetworkLocation( + hostname=domain, + ip_address=ip_address + ), + scan_commands=designated_scans, + network_configuration=ServerNetworkConfiguration( + tls_server_name_indication=domain, + network_timeout=CONNECT_TIMEOUT + ) ) except ServerHostnameCouldNotBeResolved as e: logger.info(f"Server hostname could not be resolved for domain '{domain}': {str(e)}") diff --git a/scripts/domain-loader/cloudbuild.yaml b/scripts/domain-loader/cloudbuild.yaml index a97845babb..5a54899298 100644 --- a/scripts/domain-loader/cloudbuild.yaml +++ b/scripts/domain-loader/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: [ '-c', - 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb', + 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb/arangodb:3.10.4', ] - name: mikewilliamson/wait-for diff --git a/scripts/downgrade-users/README.md b/scripts/downgrade-users/README.md new file mode 100644 index 0000000000..d3a5560b33 --- /dev/null +++ b/scripts/downgrade-users/README.md @@ -0,0 +1,10 @@ +Use in Chromium-based browsers (Chrome, Edge, etc.): +1. Open the console (F12) +2. Copy the code from the file `downgradeUsers.js` and paste it into the console +3. Enter auth token into `AUTH_TOKEN`. The token can be obtained from a Tracker browser request. Log into Tracker, open the console (F12), go to the Network tab, refresh the page, find the latest request with the name `graphql`, go to the Headers tab, copy the value of the `Authorization` header. +4. Enter the URI for the Tracker instance into `TRACKER_URI` +5. Press Enter +6. Wait for the script to finish +7. Enjoy + +CSV file format given in the example file `example.csv`. diff --git a/scripts/downgrade-users/downgradeUsers.js b/scripts/downgrade-users/downgradeUsers.js new file mode 100644 index 0000000000..fb733649ec --- /dev/null +++ b/scripts/downgrade-users/downgradeUsers.js @@ -0,0 +1,102 @@ +const AUTH_TOKEN = ""; +const TRACKER_GRAPHQL_URI = ""; + +if (!AUTH_TOKEN) { + console.error("You must enter your auth token!"); +} + +if (!TRACKER_GRAPHQL_URI) { + console.error("You must enter the Tracker graphql URI!"); +} + +const csv2json = (str, delimiter = ",") => { + const titles = str + .slice(0, str.indexOf("\n")) + .replace(/["']/g, "") + .split(delimiter) + .map((str) => str.trim()); + const rows = str.slice(str.indexOf("\n") + 1).split("\n"); + return rows.map((row) => { + const values = row.split(delimiter); + return titles.reduce((object, curr, i) => { + object[curr] = values[i].replace(/["']/g, "").trim(); + return object; + }, {}); + }); +}; + +const changeUserRole = async ({ email, role = "USER", orgId }) => { + const res = await fetch(TRACKER_GRAPHQL_URI, { + body: JSON.stringify({ + query: `mutation { + updateUserRole(input: { + userName: "${email}", + role: ${role}, + orgId: "${orgId}", + }) { + result { + ...on UpdateUserRoleResult { + status + } + ...on AffiliationError { + code + description + } + __typename + } + } + }`, + }), + headers: { + Accept: "application/json", + Authorization: AUTH_TOKEN, + "Content-Type": "application/json", + }, + method: "POST", + }); + + return await res.json(); +}; + +const [fileHandle] = await window.showOpenFilePicker(); +const file = await fileHandle.getFile(); +const content = (await file.text()).trim(); +const downgradeUserList = csv2json(content, ","); + +for await (const [key, val] of downgradeUserList.entries()) { + try { + const updateRoleRes = await changeUserRole({ email: val.userName, orgId: val.organizationId, role: "USER" }); + if (updateRoleRes.data.updateUserRole.result["__typename"] === "AffiliationError") { + console.error( + `Error while updating "${val.email}" permission for organization "${val.organizationId}" to "USER": `, + val + ); + downgradeUserList[key].success = false; + downgradeUserList[key].error = updateRoleRes.data.updateUserRole.result.description; + continue; + } + } catch (e) { + console.error( + `Error while updating "${val.email}" permission for organization "${val.organizationId}" to "USER": `, + val + ); + downgradeUserList[key].success = false; + downgradeUserList[key].error = e; + } + downgradeUserList[key].success = true; + console.log( + `Successfully changed "${val.email}" permission for organization "${val.organizationId}" to "USER": `, + val + ); +} + +for (const val of downgradeUserList) { + if (val.error) { + console.error( + `Error while updating "${val.email}" permission for organization "${val.organizationId}" to "USER": `, + val + ); + } +} + +console.table(downgradeUserList.filter((val) => val.success === false)); diff --git a/scripts/downgrade-users/example.csv b/scripts/downgrade-users/example.csv new file mode 100644 index 0000000000..f1ec6cf5a1 --- /dev/null +++ b/scripts/downgrade-users/example.csv @@ -0,0 +1,3 @@ +userId,displayName,userName,totalCount,affiliationId,permission,organizationId,acronym,name,slug +exampleIdqd2a=,example name ,example1@example.com,2,e21e12eaw,ADMIN,2e21e12e,EO,Example Org,example-org +exampleIdqd2a=,example name ,example1@example.com,2,e21e12eaw,ADMIN,anrk30a=,AEO,Another Example,another-example diff --git a/scripts/mass-invite-users/README.md b/scripts/mass-invite-users/README.md new file mode 100644 index 0000000000..75ed428ffa --- /dev/null +++ b/scripts/mass-invite-users/README.md @@ -0,0 +1,10 @@ +Use in Chromium-based browsers (Chrome, Edge, etc.): +1. Open the console (F12) +2. Copy the code from the file `mass-invite-users.js` and paste it into the console +3. Enter auth token into `AUTH_TOKEN`. The token can be obtained from a Tracker browser request. Log into Tracker, open the console (F12), go to the Network tab, refresh the page, find the latest request with the name `graphql`, go to the Headers tab, copy the value of the `Authorization` header. +4. Enter the URI for the Tracker instance into `TRACKER_URI` +5. Press Enter +6. Wait for the script to finish +7. Enjoy + +CSV file format given in the example file `example.csv`. diff --git a/scripts/mass-invite-users/example.csv b/scripts/mass-invite-users/example.csv new file mode 100644 index 0000000000..f231fd63d1 --- /dev/null +++ b/scripts/mass-invite-users/example.csv @@ -0,0 +1,3 @@ +email,role,orgSlug +email@example.com,USER,example-slug +adminEmail@example.com,ADMIN,another-example-slug diff --git a/scripts/mass-invite-users/massInviteUsers.js b/scripts/mass-invite-users/massInviteUsers.js new file mode 100644 index 0000000000..f770aace68 --- /dev/null +++ b/scripts/mass-invite-users/massInviteUsers.js @@ -0,0 +1,116 @@ +const AUTH_TOKEN = ""; +const TRACKER_GRAPHQL_URI = ""; + +if (!AUTH_TOKEN) { + console.error("You must enter your auth token!"); +} + +if (!TRACKER_GRAPHQL_URI) { + console.error("You must enter the Tracker graphql URI!"); +} + +const csv2json = (str, delimiter = ",") => { + const titles = str + .slice(0, str.indexOf("\n")) + .replace(/["']/g, "") + .split(delimiter) + .map((str) => str.trim()); + const rows = str.slice(str.indexOf("\n") + 1).split("\n"); + return rows.map((row) => { + const values = row.split(delimiter); + return titles.reduce((object, curr, i) => { + object[curr] = values[i].replace(/["']/g, "").trim(); + return object; + }, {}); + }); +}; + +const getOrg = async (orgSlug) => { + const res = await fetch(TRACKER_GRAPHQL_URI, { + body: JSON.stringify({ + query: `query { + findOrganizationBySlug(orgSlug: "${orgSlug}") { + id + name + slug + } + }`, + }), + headers: { + Accept: "application/json", + Authorization: AUTH_TOKEN, + "Content-Type": "application/json", + }, + method: "POST", + }); + return (await res.json()).data; +}; + +const inviteUser = async ({ email, role, orgId }) => { + const res = await fetch(TRACKER_GRAPHQL_URI, { + body: JSON.stringify({ + query: `mutation { + inviteUserToOrg(input: { + userName: "${email}", + requestedRole: ${role}, + orgId: "${orgId}", + preferredLang: ENGLISH + }) { + result { + ... on InviteUserToOrgResult { + status + } + ... on AffiliationError { + code + description + } + __typename + } + } + }`, + }), + headers: { + Accept: "application/json", + Authorization: AUTH_TOKEN, + "Content-Type": "application/json", + }, + method: "POST", + }); + + const json = await res.json(); + + return json; +}; + +const [fileHandle] = await window.showOpenFilePicker(); +const file = await fileHandle.getFile(); +const content = (await file.text()).trim(); +const inviteList = csv2json(content, ","); + +for await (const [key, inv] of inviteList.entries()) { + try { + const data = await getOrg(inv.orgSlug); + inviteList[key].orgId = data.findOrganizationBySlug.id; + const inviteRes = await inviteUser({ email: inv.email, orgId: inv.orgId, role: inv.role }); + if (inviteRes.data.inviteUserToOrg.result["__typename"] === "AffiliationError") { + console.error(`Error while inviting ${inv.email} to ${inv.orgSlug}: `, inv); + inviteList[key].success = false; + inviteList[key].error = inviteRes.data.inviteUserToOrg.result.description; + continue; + } + } catch (e) { + console.error(`Error while inviting ${inv.email} to ${inv.orgSlug}: `, inv); + inviteList[key].success = false; + inviteList[key].error = e; + } + inviteList[key].success = true; + console.log(`Successfully invited ${inv.email} to ${inv.orgSlug}: `, inv); +} + +for (const inv of inviteList) { + if (inv.error) { + console.error(`Error while inviting ${inv.email} to ${inv.orgSlug}: `, inv); + } +} + +console.table(inviteList.filter((inv) => inv.success === false)); diff --git a/services/dmarc-report/cloudbuild.yaml b/services/dmarc-report/cloudbuild.yaml index 0aa77ebec6..dcbf6e146b 100644 --- a/services/dmarc-report/cloudbuild.yaml +++ b/services/dmarc-report/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: [ '-c', - 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb', + 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb/arangodb:3.10.4', ] - name: mikewilliamson/wait-for diff --git a/services/org-footprint/.dockerignore b/services/org-footprint/.dockerignore new file mode 100644 index 0000000000..463aee45ce --- /dev/null +++ b/services/org-footprint/.dockerignore @@ -0,0 +1,6 @@ +node_modules +cloudbuild.yaml +coverage +**/*.test.js +**/*/.env +.git diff --git a/services/org-footprint/.env.example b/services/org-footprint/.env.example new file mode 100644 index 0000000000..004c818b65 --- /dev/null +++ b/services/org-footprint/.env.example @@ -0,0 +1,11 @@ +DB_PASS= +DB_URL= +DB_NAME= + +NOTIFICATION_API_KEY= +NOTIFICATION_API_URL= +NOTIFICATION_ORG_FOOTPRINT_EN= +NOTIFICATION_ORG_FOOTPRINT_FR= + +SERVICE_ACCOUNT_EMAIL= +REDIRECT_TO_SERVICE_ACCOUNT_EMAIL= \ No newline at end of file diff --git a/services/org-footprint/.eslintrc b/services/org-footprint/.eslintrc new file mode 100644 index 0000000000..c82ccfe77d --- /dev/null +++ b/services/org-footprint/.eslintrc @@ -0,0 +1,46 @@ +{ + "extends": [ + "standard", + "prettier" + ], + "plugins": ["jest"], + "env": { + "browser": false, + "node": true, + "jest/globals": true + }, + "globals": { + "fetch": true, + "fetchMock": true + }, + "rules": { + "no-control-regex": "off", + "semi": ["error", "never"], + "comma-dangle": ["error", "always-multiline"], + "no-unused-vars": [ + "error", + { + "vars": "all", + "args": "all", + "varsIgnorePattern": "_", + "argsIgnorePattern": "_" + } + ], + "camelcase": [ + "error", + { + "ignoreDestructuring": true, + "properties": "never" + } + ], + "new-cap": [ + "error", + { + "capIsNewExceptions": [ + "ArangoTools" + ] + } + ] + } + } + \ No newline at end of file diff --git a/services/org-footprint/.prettierrc b/services/org-footprint/.prettierrc new file mode 100644 index 0000000000..18e3a08569 --- /dev/null +++ b/services/org-footprint/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all" +} + \ No newline at end of file diff --git a/services/org-footprint/Dockerfile b/services/org-footprint/Dockerfile new file mode 100644 index 0000000000..a09fcf9540 --- /dev/null +++ b/services/org-footprint/Dockerfile @@ -0,0 +1,14 @@ +FROM node:alpine + +ENV NODE_ENV production + +WORKDIR /app +COPY src ./src +COPY index.js ./index.js +COPY package* ./ +COPY database-options.js ./ +COPY .env.example ./ +RUN npm ci + +USER node +CMD ["npm", "start"] diff --git a/services/org-footprint/README.md b/services/org-footprint/README.md new file mode 100644 index 0000000000..a460e9c8a0 --- /dev/null +++ b/services/org-footprint/README.md @@ -0,0 +1,76 @@ +# Organization Footprint Notification Service + +The purpose of this service is to search the database for recent audit logs in each organization, and alert affiliated admins of the changes. + +## Install dependencies + +Like all other JavaScript applications, dependencies are installed with npm. + +```bash +npm i +``` + +## One time setup + +This application uses the [dotenv-safe](https://github.com/rolodato/dotenv-safe) library to enforce that the environment variables it is counting on actually exist. This makes the application fail immediately with a descriptive error if anything important is missing. + +To set up the environment, duplicate the `.env.example` file and fill in the values. Example values are shown. + +```bash +cp .env.example .env +cat .env +DB_PASS=test +DB_URL=http://localhost:8529 +DB_NAME=track_dmarc +NOTIFICATION_API_KEY=asdf1234 +NOTIFICATION_API_URL=https://api.notification.alpha.canada.ca +NOTIFICATION_ORG_FOOTPRINT_EN=test_id +NOTIFICATION_ORG_FOOTPRINT_FR=test_id +SERVICE_ACCOUNT_EMAIL=test@email.ca +REDIRECT_TO_SERVICE_ACCOUNT_EMAIL=false +``` + +## Testing + +### Local development + +The tests require a copy of [ArangoDB](https://www.arangodb.com/) to be running locally. ArangoDB should have it's own .env file, and the value of the root password should align with the value of `DB_PASS` in the services `.env` file. + +```bash +# Write the arango test credentials into an env file: +echo ARANGO_ROOT_PASSWORD=test > arangodb.env +# Run a detached arangodb container using the root password from the env: +docker run -d -p=8529:8529 --env-file arangodb.env --name=arango arangodb +# Run the tests: +npm test +``` + +With ArangoDB running you will need to create a `test.env` file, with values to be used during the test run. + +``` +[mike@ouroboros org-footprint]$ cat test.env +DB_PASS=test +DB_URL=http://localhost:8529 +DB_NAME=track_dmarc +NOTIFICATION_API_KEY=asdf1234 +NOTIFICATION_API_URL=https://api.notification.alpha.canada.ca +NOTIFICATION_ORG_FOOTPRINT_EN=test_id +NOTIFICATION_ORG_FOOTPRINT_FR=test_id +SERVICE_ACCOUNT_EMAIL=test@email.ca +REDIRECT_TO_SERVICE_ACCOUNT_EMAIL=false +``` + +### Cloudbuild + +Cloudbuild is used for CI, and one of the reasons for that is that it has great tooling for running jobs locally which is very helpful for debugging. To see how this will behave in CI you can run something like the following to run the tests with cloud-build-local. + +```bash +cloud-build-local --config cloudbuild.yaml --substitutions=BRANCH_NAME=foo,SHORT_SHA=asdf1234,_DB_PASS=test,_DB_URL=http://arangodb:8529,_DB_NAME=track_dmarc,NOTIFICATION_API_KEY=asdf1234, NOTIFICATION_API_URL=https://api.notification.alpha.canada.ca, NOTIFICATION_ORG_FOOTPRINT_EN=test_id, NOTIFICATION_ORG_FOOTPRINT_FR=test_id, SERVICE_ACCOUNT_EMAIL=test@email.ca, REDIRECT_TO_SERVICE_ACCOUNT_EMAIL=false --dryrun=false . +``` + +Because of the way the cloudbuild config spins up and detaches a copy of ArangoDB, you will need to run the following commands to clean up after. + +```bash +docker stop $(docker ps -q) +docker rm arangodb +``` diff --git a/services/org-footprint/cloudbuild.yaml b/services/org-footprint/cloudbuild.yaml new file mode 100644 index 0000000000..77addb8aab --- /dev/null +++ b/services/org-footprint/cloudbuild.yaml @@ -0,0 +1,79 @@ +steps: + - name: 'gcr.io/cloud-builders/docker' + id: start_arango + entrypoint: /bin/sh + args: + [ + '-c', + 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb/arangodb:3.10.4', + ] + + - name: mikewilliamson/wait-for + id: wait + args: ['arangodb:8529'] + + - name: node:alpine + id: install-dependencies + dir: services/org-footprint + entrypoint: npm + args: ['ci', '--no-optional'] + + - name: node:alpine + id: run-eslint + dir: services/org-footprint + entrypoint: npm + args: ['run', lint] + + - name: node:alpine + id: test + dir: services/org-footprint + entrypoint: npm + args: ['test'] + env: + - DB_URL=$_DB_URL + - DB_PASS=$_DB_PASS + - DB_NAME=$_DB_NAME + - NOTIFICATION_API_KEY=$_NOTIFICATION_API_KEY + - NOTIFICATION_API_URL=$_NOTIFICATION_API_URL + - NOTIFICATION_ORG_FOOTPRINT_EN=$_NOTIFICATION_TEST_TEMPLATE_ID + - NOTIFICATION_ORG_FOOTPRINT_FR=$_NOTIFICATION_TEST_TEMPLATE_ID + - SERVICE_ACCOUNT_EMAIL=$_SERVICE_ACCOUNT_EMAIL + - REDIRECT_TO_SERVICE_ACCOUNT_EMAIL=$_REDIRECT_TO_SERVICE_ACCOUNT_EMAIL + - name: 'gcr.io/cloud-builders/docker' + id: generate-image-name + entrypoint: 'bash' + dir: services/org-footprint + args: + - '-c' + - | + echo "gcr.io/$PROJECT_ID/services/org-footprint:$BRANCH_NAME-$SHORT_SHA-$(date +%s)" > /workspace/imagename + + - name: 'gcr.io/cloud-builders/docker' + id: build-if-master + entrypoint: 'bash' + dir: services/org-footprint + args: + - '-c' + - | + if [[ "$BRANCH_NAME" == "master" ]] + then + image=$(cat /workspace/imagename) + docker build -t $image . + else + exit 0 + fi + + - name: 'gcr.io/cloud-builders/docker' + id: push-if-master + entrypoint: 'bash' + dir: services/org-footprint + args: + - '-c' + - | + if [[ "$BRANCH_NAME" == "master" ]] + then + image=$(cat /workspace/imagename) + docker push $image + else + exit 0 + fi diff --git a/services/org-footprint/database-options.js b/services/org-footprint/database-options.js new file mode 100644 index 0000000000..363492fc96 --- /dev/null +++ b/services/org-footprint/database-options.js @@ -0,0 +1,27 @@ +const databaseOptions = ({ rootPass }) => [ + { type: 'user', username: 'root', password: rootPass }, + { + type: 'documentcollection', + name: 'users', + options: { replicationFactor: 3, writeConcern: 1, numberOfShards: 6 }, + }, + { + type: 'documentcollection', + name: 'organizations', + options: { replicationFactor: 3, writeConcern: 1, numberOfShards: 6 }, + }, + { + type: 'edgecollection', + name: 'affiliations', + options: { replicationFactor: 3, writeConcern: 1, numberOfShards: 6 }, + }, + { + type: 'documentcollection', + name: 'auditLogs', + options: { replicationFactor: 3, writeConcern: 1, numberOfShards: 6 }, + }, +] + +module.exports = { + databaseOptions, +} diff --git a/services/org-footprint/index.js b/services/org-footprint/index.js new file mode 100644 index 0000000000..2479fb3506 --- /dev/null +++ b/services/org-footprint/index.js @@ -0,0 +1,33 @@ +require('dotenv-safe').config({ + allowEmptyValues: true, + example: '.env.example', +}) + +const { + DB_PASS: rootPass, + DB_URL: url, + DB_NAME: databaseName, + NOTIFICATION_API_KEY, + NOTIFICATION_API_URL, +} = process.env + +const { ensure } = require('arango-tools') +const { NotifyClient } = require('notifications-node-client') +const { databaseOptions } = require('./database-options') + +const { orgFootprintService } = require('./src') + +const notifyClient = new NotifyClient(NOTIFICATION_API_URL, NOTIFICATION_API_KEY) + +;(async () => { + // Generate Database information + const { query } = await ensure({ + type: 'database', + name: databaseName, + url, + rootPassword: rootPass, + options: databaseOptions({ rootPass }), + }) + + await orgFootprintService({ query, log: console.log, notifyClient }) +})() diff --git a/services/org-footprint/jest.config.js b/services/org-footprint/jest.config.js new file mode 100644 index 0000000000..36e161369e --- /dev/null +++ b/services/org-footprint/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + setupFiles: ['/src/setupEnv.js'], + collectCoverage: false, + collectCoverageFrom: ['src/**/*.js'], + coveragePathIgnorePatterns: [ + 'node_modules', + 'test-config', + 'jestGlobalMocks.js', + '.module.js', + ], + testTimeout: 10000, + verbose: true, +} diff --git a/services/org-footprint/package-lock.json b/services/org-footprint/package-lock.json new file mode 100644 index 0000000000..bdcae2dff4 --- /dev/null +++ b/services/org-footprint/package-lock.json @@ -0,0 +1,10866 @@ +{ + "name": "org-footprint", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "org-footprint", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "arango-tools": "^0.6.0", + "dotenv-safe": "^8.2.0", + "notifications-node-client": "^7.0.0" + }, + "devDependencies": { + "eslint": "^8.40.0", + "eslint-config-prettier": "^8.8.0", + "eslint-config-standard": "^17.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^27.2.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-standard": "^5.0.0", + "jest": "^29.5.0", + "jest-fetch-mock": "^3.0.3", + "jest-matcher-utils": "^29.5.0", + "prettier": "^2.8.8" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.21.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.7.tgz", + "integrity": "sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.8.tgz", + "integrity": "sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.5", + "@babel/helper-compilation-targets": "^7.21.5", + "@babel/helper-module-transforms": "^7.21.5", + "@babel/helpers": "^7.21.5", + "@babel/parser": "^7.21.8", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@babel/generator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", + "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz", + "integrity": "sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.21.5", + "@babel/helper-validator-option": "^7.21.0", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", + "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", + "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz", + "integrity": "sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-module-imports": "^7.21.4", + "@babel/helper-simple-access": "^7.21.5", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", + "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", + "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", + "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.5.tgz", + "integrity": "sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", + "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", + "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz", + "integrity": "sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", + "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.5", + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.21.5", + "@babel/types": "^7.21.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", + "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.21.5", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", + "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", + "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", + "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/reporters": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.5.0", + "jest-config": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-resolve-dependencies": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "jest-watcher": "^29.5.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", + "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "dev": true, + "dependencies": { + "expect": "^29.5.0", + "jest-snapshot": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", + "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", + "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/types": "^29.5.0", + "jest-mock": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", + "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", + "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.15", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", + "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", + "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", + "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.18.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.5.tgz", + "integrity": "sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.3.0" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.1.tgz", + "integrity": "sha512-uKBEevTNb+l6/aCQaKVnUModfEMjAl98lw2Si9P5y4hLu9tm6AlX2ZIoXZX6Wh9lJueYPrGPKk5WMCNHg/u6/A==" + }, + "node_modules/@types/prettier": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", + "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz", + "integrity": "sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/visitor-keys": "5.59.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz", + "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz", + "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/visitor-keys": "5.59.5", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.5.tgz", + "integrity": "sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/typescript-estree": "5.59.5", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz", + "integrity": "sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.5", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arango-tools": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/arango-tools/-/arango-tools-0.6.0.tgz", + "integrity": "sha512-2XjPPddz7Vc07JrOyXFYGzXI+QZOAR+I0kiyklKBevWVTTDh1lYRetZh3X1fN+smibgL2DY3jKwHK14aSzVkSw==", + "dependencies": { + "arangojs": "^7.2.0", + "assign-deep": "^1.0.1", + "json-placeholder-replacer": "^1.0.35" + } + }, + "node_modules/arangojs": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/arangojs/-/arangojs-7.8.0.tgz", + "integrity": "sha512-aJFlMKlVr4sIO5GDMuykBVNVxWeZTkWDgYbbl9cIuxVctp8Lhs6dW5fr5MYlwAndnOEyi3bvbrhZIucly2IpWQ==", + "dependencies": { + "@types/node": ">=13.13.4", + "es6-error": "^4.0.1", + "multi-part": "^3.0.0", + "x3-linkedlist": "1.2.0", + "xhr": "^2.4.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assign-deep": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/assign-deep/-/assign-deep-1.0.1.tgz", + "integrity": "sha512-CSXAX79mibneEYfqLT5FEmkqR5WXF+xDRjgQQuVf6wSCXCYU8/vHttPidNar7wJ5BFmKAo8Wei0rCtzb+M/yeA==", + "dependencies": { + "assign-symbols": "^2.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/assign-symbols": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-2.0.2.tgz", + "integrity": "sha512-9sBQUQZMKFKcO/C3Bo6Rx4CQany0R0UeVcefNGRRdW2vbmaMOhV1sbmlXcQLcD56juLXbSGTBm0GGuvmrAF8pA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "dependencies": { + "follow-redirects": "^1.14.7" + } + }, + "node_modules/babel-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.5.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.5.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.5.0", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "peer": true, + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/builtins/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/builtins/node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/builtins/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "peer": true + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001486", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz", + "integrity": "sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-safe": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv-safe/-/dotenv-safe-8.2.0.tgz", + "integrity": "sha512-uWwWWdUQkSs5a3mySDB22UtNwyEYi0JtEQu+vDzIqr9OjbDdC2Ip13PnSpi/fctqlYmzkxCeabiyCAOROuAIaA==", + "dependencies": { + "dotenv": "^8.2.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.387", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.387.tgz", + "integrity": "sha512-tutLf+alr1/0YqJwKPdstVvDLmxmLb5xNyDLNS0RZmenHcEYk9qKfpKDCVZEKJ00JVbnayJm1MZAbYhYDFpcOw==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", + "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.40.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-config-standard": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz", + "integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0", + "eslint-plugin-promise": "^6.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "peer": true, + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "peer": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "27.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", + "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "peer": true, + "dependencies": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-n/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "peer": true + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-node/node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-node/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-node/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", + "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-standard": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-5.0.0.tgz", + "integrity": "sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg==", + "deprecated": "standard 16.0.0 and eslint-config-standard 16.0.0 no longer require the eslint-plugin-standard package. You can remove it from your dependencies with 'npm rm eslint-plugin-standard'. More info here: https://github.com/standard/standard/issues/1316", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "peer": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "12.4.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz", + "integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "dev": true, + "dependencies": { + "@jest/core": "^29.5.0", + "@jest/types": "^29.5.0", + "import-local": "^3.0.2", + "jest-cli": "^29.5.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", + "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", + "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.5.0", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.5.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", + "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", + "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.5.0", + "@jest/types": "^29.5.0", + "babel-jest": "^29.5.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.5.0", + "jest-environment-node": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", + "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", + "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "jest-util": "^29.5.0", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", + "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, + "node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", + "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", + "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", + "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", + "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.4.3", + "jest-snapshot": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", + "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.5.0", + "@jest/environment": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.4.3", + "jest-environment-node": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-leak-detector": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-resolve": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-util": "^29.5.0", + "jest-watcher": "^29.5.0", + "jest-worker": "^29.5.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", + "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/globals": "^29.5.0", + "@jest/source-map": "^29.4.3", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", + "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.5.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", + "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "leven": "^3.1.0", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", + "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.5.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-sdsl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-placeholder-replacer": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/json-placeholder-replacer/-/json-placeholder-replacer-1.0.35.tgz", + "integrity": "sha512-edlSWqcFVUpKPshaIcJfXpQ8eu0//gk8iU6XHWkCZIp5QEp4hoCFR7uk+LrIzhLTSqmBQ9VBs+EYK8pvWGEpRg==", + "bin": { + "jpr": "dist/index.js", + "json-placeholder-replacer": "dist/index.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsonwebtoken/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-kind": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime-kind/-/mime-kind-3.0.0.tgz", + "integrity": "sha512-sx9lClVP7GXY2mO3aVDWTQLhfvAdDvNhGi3o3g7+ae3aKaoybeGbEIlnreoRKjrbDpvlPltlkIryxOtatojeXQ==", + "dependencies": { + "file-type": "^12.1.0", + "mime-types": "^2.1.24" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multi-part": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/multi-part/-/multi-part-3.0.0.tgz", + "integrity": "sha512-pDbdYQ6DLDxAsD83w9R7r7rlW56cETL7hIB5bCWX7FJYw0K+kL5JwHr0I8tRk9lGeFcAzf+2OEzXWlG/4wCnFw==", + "dependencies": { + "mime-kind": "^3.0.0", + "multi-part-lite": "^1.0.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/multi-part-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/multi-part-lite/-/multi-part-lite-1.0.0.tgz", + "integrity": "sha512-KxIRbBZZ45hoKX1ROD/19wJr0ql1bef1rE8Y1PCwD3PuNXV42pp7Wo8lEHYuAajoT4vfAFcd3rPjlkyEEyt1nw==", + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/notifications-node-client": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/notifications-node-client/-/notifications-node-client-7.0.0.tgz", + "integrity": "sha512-lVVUAlllj7HWRTXJMXoLGdNi5XXYtQyuU50m/m9bRKt/3TK0aKa2cyzYjseElW9kFpQRx2rM3SgaEvPrVcnGqg==", + "dependencies": { + "axios": "^0.25.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=14.17.3", + "npm": ">=6.14.13" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-headers": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", + "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/x3-linkedlist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/x3-linkedlist/-/x3-linkedlist-1.2.0.tgz", + "integrity": "sha512-mH/YwxpYSKNa8bDNF1yOuZCMuV+K80LtDN8vcLDUAwNazCxptDNsYt+zA/EJeYiGbdtKposhKLZjErGVOR8mag==", + "engines": { + "node": ">=10" + } + }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.21.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.7.tgz", + "integrity": "sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==", + "dev": true + }, + "@babel/core": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.8.tgz", + "integrity": "sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.5", + "@babel/helper-compilation-targets": "^7.21.5", + "@babel/helper-module-transforms": "^7.21.5", + "@babel/helpers": "^7.21.5", + "@babel/parser": "^7.21.8", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", + "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "dev": true, + "requires": { + "@babel/types": "^7.21.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz", + "integrity": "sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.21.5", + "@babel/helper-validator-option": "^7.21.0", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", + "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dev": true, + "requires": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-imports": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", + "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "dev": true, + "requires": { + "@babel/types": "^7.21.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz", + "integrity": "sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-module-imports": "^7.21.4", + "@babel/helper-simple-access": "^7.21.5", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", + "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", + "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", + "dev": true, + "requires": { + "@babel/types": "^7.21.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", + "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "dev": true + }, + "@babel/helpers": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.5.tgz", + "integrity": "sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==", + "dev": true, + "requires": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.5", + "@babel/types": "^7.21.5" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", + "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", + "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz", + "integrity": "sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/traverse": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", + "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.5", + "@babel/helper-environment-visitor": "^7.21.5", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.21.5", + "@babel/types": "^7.21.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", + "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.21.5", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@eslint/js": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", + "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", + "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", + "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "dev": true, + "requires": { + "@jest/console": "^29.5.0", + "@jest/reporters": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.5.0", + "jest-config": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-resolve-dependencies": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "jest-watcher": "^29.5.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", + "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0" + } + }, + "@jest/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "dev": true, + "requires": { + "expect": "^29.5.0", + "jest-snapshot": "^29.5.0" + } + }, + "@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "requires": { + "jest-get-type": "^29.4.3" + } + }, + "@jest/fake-timers": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", + "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + } + }, + "@jest/globals": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", + "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/types": "^29.5.0", + "jest-mock": "^29.5.0" + } + }, + "@jest/reporters": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", + "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + } + }, + "@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.25.16" + } + }, + "@jest/source-map": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", + "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.15", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", + "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "dev": true, + "requires": { + "@jest/console": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", + "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "dev": true, + "requires": { + "@jest/test-result": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.5.0", + "@jridgewell/trace-mapping": "^0.3.15", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0" + } + }, + "@types/babel__core": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", + "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.18.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.5.tgz", + "integrity": "sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/node": { + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.1.tgz", + "integrity": "sha512-uKBEevTNb+l6/aCQaKVnUModfEMjAl98lw2Si9P5y4hLu9tm6AlX2ZIoXZX6Wh9lJueYPrGPKk5WMCNHg/u6/A==" + }, + "@types/prettier": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", + "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", + "dev": true + }, + "@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@typescript-eslint/scope-manager": { + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz", + "integrity": "sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/visitor-keys": "5.59.5" + } + }, + "@typescript-eslint/types": { + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz", + "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz", + "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/visitor-keys": "5.59.5", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@typescript-eslint/utils": { + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.5.tgz", + "integrity": "sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.5", + "@typescript-eslint/types": "5.59.5", + "@typescript-eslint/typescript-estree": "5.59.5", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.59.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz", + "integrity": "sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.5", + "eslint-visitor-keys": "^3.3.0" + } + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arango-tools": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/arango-tools/-/arango-tools-0.6.0.tgz", + "integrity": "sha512-2XjPPddz7Vc07JrOyXFYGzXI+QZOAR+I0kiyklKBevWVTTDh1lYRetZh3X1fN+smibgL2DY3jKwHK14aSzVkSw==", + "requires": { + "arangojs": "^7.2.0", + "assign-deep": "^1.0.1", + "json-placeholder-replacer": "^1.0.35" + } + }, + "arangojs": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/arangojs/-/arangojs-7.8.0.tgz", + "integrity": "sha512-aJFlMKlVr4sIO5GDMuykBVNVxWeZTkWDgYbbl9cIuxVctp8Lhs6dW5fr5MYlwAndnOEyi3bvbrhZIucly2IpWQ==", + "requires": { + "@types/node": ">=13.13.4", + "es6-error": "^4.0.1", + "multi-part": "^3.0.0", + "x3-linkedlist": "1.2.0", + "xhr": "^2.4.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "assign-deep": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/assign-deep/-/assign-deep-1.0.1.tgz", + "integrity": "sha512-CSXAX79mibneEYfqLT5FEmkqR5WXF+xDRjgQQuVf6wSCXCYU8/vHttPidNar7wJ5BFmKAo8Wei0rCtzb+M/yeA==", + "requires": { + "assign-symbols": "^2.0.2" + } + }, + "assign-symbols": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-2.0.2.tgz", + "integrity": "sha512-9sBQUQZMKFKcO/C3Bo6Rx4CQany0R0UeVcefNGRRdW2vbmaMOhV1sbmlXcQLcD56juLXbSGTBm0GGuvmrAF8pA==" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "axios": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "requires": { + "follow-redirects": "^1.14.7" + } + }, + "babel-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "dev": true, + "requires": { + "@jest/transform": "^29.5.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.5.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.5.0", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "peer": true, + "requires": { + "semver": "^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "peer": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "peer": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "peer": true + } + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001486", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz", + "integrity": "sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "requires": { + "node-fetch": "2.6.7" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" + }, + "dotenv-safe": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv-safe/-/dotenv-safe-8.2.0.tgz", + "integrity": "sha512-uWwWWdUQkSs5a3mySDB22UtNwyEYi0JtEQu+vDzIqr9OjbDdC2Ip13PnSpi/fctqlYmzkxCeabiyCAOROuAIaA==", + "requires": { + "dotenv": "^8.2.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "electron-to-chromium": { + "version": "1.4.387", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.387.tgz", + "integrity": "sha512-tutLf+alr1/0YqJwKPdstVvDLmxmLb5xNyDLNS0RZmenHcEYk9qKfpKDCVZEKJ00JVbnayJm1MZAbYhYDFpcOw==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", + "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.40.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + } + }, + "eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "requires": {} + }, + "eslint-config-standard": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz", + "integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==", + "dev": true, + "requires": {} + }, + "eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "peer": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "peer": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "peer": true + } + } + }, + "eslint-plugin-import": { + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-plugin-jest": { + "version": "27.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", + "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "^5.10.0" + } + }, + "eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "peer": true, + "requires": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "peer": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "peer": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "peer": true + } + } + }, + "eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "requires": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "dependencies": { + "eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-plugin-promise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", + "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", + "dev": true, + "requires": {} + }, + "eslint-plugin-standard": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-5.0.0.tgz", + "integrity": "sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg==", + "dev": true, + "requires": {} + }, + "eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "peer": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "peer": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true + }, + "espree": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "file-type": { + "version": "12.4.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz", + "integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "dev": true, + "requires": { + "@jest/core": "^29.5.0", + "@jest/types": "^29.5.0", + "import-local": "^3.0.2", + "jest-cli": "^29.5.0" + } + }, + "jest-changed-files": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", + "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", + "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/expect": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.5.0", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.5.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", + "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "dev": true, + "requires": { + "@jest/core": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", + "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.5.0", + "@jest/types": "^29.5.0", + "babel-jest": "^29.5.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.5.0", + "jest-environment-node": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-runner": "^29.5.0", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + } + }, + "jest-docblock": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", + "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", + "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "jest-util": "^29.5.0", + "pretty-format": "^29.5.0" + } + }, + "jest-environment-node": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", + "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-mock": "^29.5.0", + "jest-util": "^29.5.0" + } + }, + "jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "requires": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, + "jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true + }, + "jest-haste-map": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", + "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "dev": true, + "requires": { + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + } + }, + "jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + } + }, + "jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", + "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "jest-util": "^29.5.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "dev": true + }, + "jest-resolve": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", + "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.5.0", + "jest-validate": "^29.5.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", + "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "dev": true, + "requires": { + "jest-regex-util": "^29.4.3", + "jest-snapshot": "^29.5.0" + } + }, + "jest-runner": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", + "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "dev": true, + "requires": { + "@jest/console": "^29.5.0", + "@jest/environment": "^29.5.0", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.4.3", + "jest-environment-node": "^29.5.0", + "jest-haste-map": "^29.5.0", + "jest-leak-detector": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-resolve": "^29.5.0", + "jest-runtime": "^29.5.0", + "jest-util": "^29.5.0", + "jest-watcher": "^29.5.0", + "jest-worker": "^29.5.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + } + }, + "jest-runtime": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", + "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", + "dev": true, + "requires": { + "@jest/environment": "^29.5.0", + "@jest/fake-timers": "^29.5.0", + "@jest/globals": "^29.5.0", + "@jest/source-map": "^29.4.3", + "@jest/test-result": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.5.0", + "jest-snapshot": "^29.5.0", + "jest-util": "^29.5.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", + "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.5.0", + "@jest/transform": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.5.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.5.0", + "semver": "^7.3.5" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", + "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "leven": "^3.1.0", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", + "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "dev": true, + "requires": { + "@jest/test-result": "^29.5.0", + "@jest/types": "^29.5.0", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.5.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-sdsl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-placeholder-replacer": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/json-placeholder-replacer/-/json-placeholder-replacer-1.0.35.tgz", + "integrity": "sha512-edlSWqcFVUpKPshaIcJfXpQ8eu0//gk8iU6XHWkCZIp5QEp4hoCFR7uk+LrIzhLTSqmBQ9VBs+EYK8pvWGEpRg==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-kind": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime-kind/-/mime-kind-3.0.0.tgz", + "integrity": "sha512-sx9lClVP7GXY2mO3aVDWTQLhfvAdDvNhGi3o3g7+ae3aKaoybeGbEIlnreoRKjrbDpvlPltlkIryxOtatojeXQ==", + "requires": { + "file-type": "^12.1.0", + "mime-types": "^2.1.24" + } + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "requires": { + "dom-walk": "^0.1.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multi-part": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/multi-part/-/multi-part-3.0.0.tgz", + "integrity": "sha512-pDbdYQ6DLDxAsD83w9R7r7rlW56cETL7hIB5bCWX7FJYw0K+kL5JwHr0I8tRk9lGeFcAzf+2OEzXWlG/4wCnFw==", + "requires": { + "mime-kind": "^3.0.0", + "multi-part-lite": "^1.0.0" + } + }, + "multi-part-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/multi-part-lite/-/multi-part-lite-1.0.0.tgz", + "integrity": "sha512-KxIRbBZZ45hoKX1ROD/19wJr0ql1bef1rE8Y1PCwD3PuNXV42pp7Wo8lEHYuAajoT4vfAFcd3rPjlkyEEyt1nw==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "notifications-node-client": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/notifications-node-client/-/notifications-node-client-7.0.0.tgz", + "integrity": "sha512-lVVUAlllj7HWRTXJMXoLGdNi5XXYtQyuU50m/m9bRKt/3TK0aKa2cyzYjseElW9kFpQRx2rM3SgaEvPrVcnGqg==", + "requires": { + "axios": "^0.25.0", + "jsonwebtoken": "^9.0.0" + } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-headers": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==" + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + }, + "pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "pure-rand": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", + "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + } + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "peer": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + } + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "x3-linkedlist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/x3-linkedlist/-/x3-linkedlist-1.2.0.tgz", + "integrity": "sha512-mH/YwxpYSKNa8bDNF1yOuZCMuV+K80LtDN8vcLDUAwNazCxptDNsYt+zA/EJeYiGbdtKposhKLZjErGVOR8mag==" + }, + "xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "requires": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/services/org-footprint/package.json b/services/org-footprint/package.json new file mode 100644 index 0000000000..c34e26b100 --- /dev/null +++ b/services/org-footprint/package.json @@ -0,0 +1,34 @@ +{ + "name": "org-footprint", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "jest", + "test-coverage": "jest --coverage", + "lint": "eslint src", + "prettier": "prettier --write \"**/*.js\"" + }, + "author": "", + "license": "ISC", + "dependencies": { + "arango-tools": "^0.6.0", + "dotenv-safe": "^8.2.0", + "notifications-node-client": "^7.0.0" + }, + "devDependencies": { + "eslint": "^8.40.0", + "eslint-config-prettier": "^8.8.0", + "eslint-config-standard": "^17.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^27.2.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-standard": "^5.0.0", + "jest": "^29.5.0", + "jest-fetch-mock": "^3.0.3", + "jest-matcher-utils": "^29.5.0", + "prettier": "^2.8.8" + } +} diff --git a/services/org-footprint/src/__tests__/get-all-org-keys.test.js b/services/org-footprint/src/__tests__/get-all-org-keys.test.js new file mode 100644 index 0000000000..ae8983ede9 --- /dev/null +++ b/services/org-footprint/src/__tests__/get-all-org-keys.test.js @@ -0,0 +1,103 @@ +const { DB_PASS: rootPass, DB_URL: url } = process.env + +const { ensure, dbNameFromFile } = require('arango-tools') +const { databaseOptions } = require('../../database-options') + +const { getAllOrgKeys } = require('../database') + +describe('given the getAllOrgKeys function', () => { + const consoleErrorOutput = [] + const consoleInfoOutput = [] + const mockedError = (output) => consoleErrorOutput.push(output) + const mockedInfo = (output) => consoleInfoOutput.push(output) + + let query, drop, truncate, collections + + beforeAll(async () => { + // Generate DB Items + ;({ query, drop, truncate, collections } = await ensure({ + type: 'database', + name: dbNameFromFile(__filename), + url, + rootPassword: rootPass, + options: databaseOptions({ rootPass }), + })) + }) + + beforeEach(async () => { + console.error = mockedError + console.info = mockedInfo + consoleErrorOutput.length = 0 + consoleInfoOutput.length = 0 + + await truncate() + }) + + afterAll(async () => { + await drop() + }) + + describe('given a successful query', () => { + let org, org2 + beforeEach(async () => { + org = await collections.organizations.save({ + orgDetails: { + en: { + name: 'test org', + }, + fr: { + name: 'test org', + }, + }, + }) + org2 = await collections.organizations.save({ + orgDetails: { + en: { + name: 'test org 2', + }, + fr: { + name: 'test org 2', + }, + }, + }) + }) + it('returns the org keys', async () => { + const orgKeys = await getAllOrgKeys({ query }) + + const expectedOrgKeys = [org._key, org2._key] + + expect(orgKeys).toEqual(expectedOrgKeys) + }) + }) + describe('given an unsuccessful query', () => { + describe('when the query fails', () => { + it('throws an error', async () => { + const mockQuery = jest.fn().mockRejectedValue(new Error('Database error occurred.')) + try { + await getAllOrgKeys({ query: mockQuery }) + } catch (err) { + expect(err).toEqual( + new Error('Database error occurred while trying to find org keys: Error: Database error occurred.'), + ) + } + }) + }) + describe('when the cursor fails', () => { + it('throws an error', async () => { + const cursor = { + all() { + throw new Error('Cursor error occurred.') + }, + } + const mockQuery = jest.fn().mockReturnValue(cursor) + try { + await getAllOrgKeys({ query: mockQuery }) + } catch (err) { + expect(err).toEqual( + new Error('Cursor error occurred while trying to find org keys: Error: Cursor error occurred.'), + ) + } + }) + }) + }) +}) diff --git a/services/org-footprint/src/__tests__/get-bilingual-org-names.test.js b/services/org-footprint/src/__tests__/get-bilingual-org-names.test.js new file mode 100644 index 0000000000..ec105ba7ef --- /dev/null +++ b/services/org-footprint/src/__tests__/get-bilingual-org-names.test.js @@ -0,0 +1,99 @@ +const { DB_PASS: rootPass, DB_URL: url } = process.env + +const { ensure, dbNameFromFile } = require('arango-tools') +const { databaseOptions } = require('../../database-options') + +const { getBilingualOrgNames } = require('../database') + +describe('given the getBilingualOrgNames function', () => { + const consoleErrorOutput = [] + const consoleInfoOutput = [] + const mockedError = (output) => consoleErrorOutput.push(output) + const mockedInfo = (output) => consoleInfoOutput.push(output) + + let query, drop, truncate, collections + + beforeAll(async () => { + // Generate DB Items + ;({ query, drop, truncate, collections } = await ensure({ + type: 'database', + name: dbNameFromFile(__filename), + url, + rootPassword: rootPass, + options: databaseOptions({ rootPass }), + })) + }) + + beforeEach(async () => { + console.error = mockedError + console.info = mockedInfo + consoleErrorOutput.length = 0 + consoleInfoOutput.length = 0 + + await truncate() + }) + + afterAll(async () => { + await drop() + }) + + describe('given a successful query', () => { + let org + beforeEach(async () => { + org = await collections.organizations.save({ + orgDetails: { + en: { + name: 'test org', + }, + fr: { + name: 'test org', + }, + }, + }) + }) + it('returns the org names', async () => { + const orgNames = await getBilingualOrgNames({ + query, + orgKey: org._key, + }) + + const expectedOrgNames = { + en: 'test org', + fr: 'test org', + } + + expect(orgNames).toEqual(expectedOrgNames) + }) + }) + describe('given an unsuccessful query', () => { + describe('when the query fails', () => { + it('throws an error', async () => { + const mockQuery = jest.fn().mockRejectedValue(new Error('Database error occurred.')) + try { + await getBilingualOrgNames({ query: mockQuery }) + } catch (err) { + expect(err).toEqual( + new Error('Database error occurred while trying to find org names: Error: Database error occurred.'), + ) + } + }) + }) + describe('when the cursor fails', () => { + it('throws an error', async () => { + const cursor = { + next() { + throw new Error('Cursor error occurred.') + }, + } + const mockQuery = jest.fn().mockReturnValue(cursor) + try { + await getBilingualOrgNames({ query: mockQuery }) + } catch (err) { + expect(err).toEqual( + new Error('Cursor error occurred while trying to find org names: Error: Cursor error occurred.'), + ) + } + }) + }) + }) +}) diff --git a/services/org-footprint/src/__tests__/get-new-audit-logs.test.js b/services/org-footprint/src/__tests__/get-new-audit-logs.test.js new file mode 100644 index 0000000000..07e96e8425 --- /dev/null +++ b/services/org-footprint/src/__tests__/get-new-audit-logs.test.js @@ -0,0 +1,157 @@ +const { DB_PASS: rootPass, DB_URL: url } = process.env + +const { ensure, dbNameFromFile } = require('arango-tools') +const { databaseOptions } = require('../../database-options') + +const { getNewAuditLogs } = require('../database') + +describe('given the getNewAuditLogs function', () => { + const consoleErrorOutput = [] + const consoleInfoOutput = [] + const mockedError = (output) => consoleErrorOutput.push(output) + const mockedInfo = (output) => consoleInfoOutput.push(output) + + let query, drop, truncate, collections + + beforeAll(async () => { + // Generate DB Items + ;({ query, drop, truncate, collections } = await ensure({ + type: 'database', + name: dbNameFromFile(__filename), + url, + rootPassword: rootPass, + options: databaseOptions({ rootPass }), + })) + }) + + beforeEach(async () => { + console.error = mockedError + console.info = mockedInfo + consoleErrorOutput.length = 0 + consoleInfoOutput.length = 0 + + await truncate() + }) + + afterAll(async () => { + await drop() + }) + + describe('given a successful query', () => { + const time = new Date() + let log, log2 + beforeEach(async () => { + log = await collections.auditLogs.save({ + timestamp: time - 1000, + initiatedBy: { + id: '123', + userName: 'user@test.ca', + role: 'admin', + }, + action: 'add', + target: { + resource: 'test.domain.gc.ca', + updatedProperties: [], + organization: { + id: '123', + name: 'test org', + }, // name of resource being acted upon + resourceType: 'domain', // user, org, domain + }, + }) + log2 = await collections.auditLogs.save({ + timestamp: time - 1000, + initiatedBy: { + id: '123', + userName: 'user@test.ca', + role: 'admin', + }, + action: 'remove', + target: { + resource: 'removed-user@test.ca', + updatedProperties: [], + organization: { + id: '123', + name: 'test org', + }, // name of resource being acted upon + resourceType: 'user', // user, org, domain + }, + }) + }) + it('returns the org admins', async () => { + const auditLogs = await getNewAuditLogs({ query, orgKey: '123' }) + const expectedAuditLogs = [ + { + ...log, + timestamp: time - 1000, + initiatedBy: { + id: '123', + userName: 'user@test.ca', + role: 'admin', + }, + action: 'add', + target: { + resource: 'test.domain.gc.ca', + updatedProperties: [], + organization: { + id: '123', + name: 'test org', + }, // name of resource being acted upon + resourceType: 'domain', // user, org, domain + }, + }, + { + ...log2, + timestamp: time - 1000, + initiatedBy: { + id: '123', + userName: 'user@test.ca', + role: 'admin', + }, + action: 'remove', + target: { + resource: 'removed-user@test.ca', + updatedProperties: [], + organization: { + id: '123', + name: 'test org', + }, // name of resource being acted upon + resourceType: 'user', // user, org, domain + }, + }, + ] + expect(auditLogs).toEqual(expectedAuditLogs) + }) + }) + describe('given an unsuccessful query', () => { + describe('when the query fails', () => { + it('throws an error', async () => { + const mockQuery = jest.fn().mockRejectedValue(new Error('Database error occurred.')) + try { + await getNewAuditLogs({ query: mockQuery }) + } catch (err) { + expect(err).toEqual( + new Error('Database error occurred while trying to find audit logs: Error: Database error occurred.'), + ) + } + }) + }) + describe('when the cursor fails', () => { + it('throws an error', async () => { + const cursor = { + all() { + throw new Error('Cursor error occurred.') + }, + } + const mockQuery = jest.fn().mockReturnValue(cursor) + try { + await getNewAuditLogs({ query: mockQuery }) + } catch (err) { + expect(err).toEqual( + new Error('Cursor error occurred while trying to find audit logs: Error: Cursor error occurred.'), + ) + } + }) + }) + }) +}) diff --git a/services/org-footprint/src/__tests__/get-org-admins.test.js b/services/org-footprint/src/__tests__/get-org-admins.test.js new file mode 100644 index 0000000000..525b68ee69 --- /dev/null +++ b/services/org-footprint/src/__tests__/get-org-admins.test.js @@ -0,0 +1,93 @@ +const { DB_PASS: rootPass, DB_URL: url } = process.env + +const { ensure, dbNameFromFile } = require('arango-tools') +const { databaseOptions } = require('../../database-options') + +const { getOrgAdmins } = require('../database') + +describe('given the getOrgAdmins function', () => { + const consoleErrorOutput = [] + const consoleInfoOutput = [] + const mockedError = (output) => consoleErrorOutput.push(output) + const mockedInfo = (output) => consoleInfoOutput.push(output) + + let query, drop, truncate, collections + + beforeAll(async () => { + // Generate DB Items + ;({ query, drop, truncate, collections } = await ensure({ + type: 'database', + name: dbNameFromFile(__filename), + url, + rootPassword: rootPass, + options: databaseOptions({ rootPass }), + })) + }) + + beforeEach(async () => { + console.error = mockedError + console.info = mockedInfo + consoleErrorOutput.length = 0 + consoleInfoOutput.length = 0 + + await truncate() + }) + + afterAll(async () => { + await drop() + }) + + describe('given a successful query', () => { + // eslint-disable-next-line no-unused-vars + let org, user, affiliation + beforeEach(async () => { + org = await collections.organizations.save({}) + user = await collections.users.save({ + userName: 'user@test.ca', + }) + affiliation = await collections.affiliations.save({ + _from: org._id, + _to: user._id, + permission: 'admin', + }) + }) + it('returns the org admins', async () => { + const orgAdmins = await getOrgAdmins({ query, orgKey: org._key }) + + const expectedOrgAdmins = [{ ...user, userName: 'user@test.ca' }] + + expect(orgAdmins).toEqual(expectedOrgAdmins) + }) + }) + describe('given an unsuccessful query', () => { + describe('when the query fails', () => { + it('throws an error', async () => { + const mockQuery = jest.fn().mockRejectedValue(new Error('Database error occurred.')) + try { + await getOrgAdmins({ query: mockQuery }) + } catch (err) { + expect(err).toEqual( + new Error('Database error occurred while trying to find org admins: Error: Database error occurred.'), + ) + } + }) + }) + describe('when the cursor fails', () => { + it('throws an error', async () => { + const cursor = { + all() { + throw new Error('Cursor error occurred.') + }, + } + const mockQuery = jest.fn().mockReturnValue(cursor) + try { + await getOrgAdmins({ query: mockQuery }) + } catch (err) { + expect(err).toEqual( + new Error('Cursor error occurred while trying to find org admins: Error: Cursor error occurred.'), + ) + } + }) + }) + }) +}) diff --git a/services/org-footprint/src/__tests__/notify-send-org-footprint-email.test.js b/services/org-footprint/src/__tests__/notify-send-org-footprint-email.test.js new file mode 100644 index 0000000000..87c78dc012 --- /dev/null +++ b/services/org-footprint/src/__tests__/notify-send-org-footprint-email.test.js @@ -0,0 +1,110 @@ +const { sendOrgFootprintEmail } = require('../notify') +const { NOTIFICATION_ORG_FOOTPRINT_EN, NOTIFICATION_ORG_FOOTPRINT_FR } = process.env + +describe('given the sendOrgFootprintEmail function', () => { + let consoleOutput = [] + const mockedError = (output) => consoleOutput.push(output) + beforeAll(async () => { + console.error = mockedError + }) + beforeEach(async () => { + consoleOutput = [] + }) + describe('email successfully sent', () => { + it('returns nothing', async () => { + const sendEmail = jest.fn() + const notifyClient = { + sendEmail, + } + const user = { + userName: 'test@email.ca', + displayName: 'Test Account', + preferredLang: 'english', + } + + const auditLogs = [ + { + action: 'add', + target: { + resourceType: 'user', + }, + }, + { + action: 'add', + target: { + resourceType: 'user', + }, + }, + { + action: 'remove', + target: { + resource: 'domain1', + resourceType: 'domain', + }, + }, + { + action: 'update', + target: { + resource: 'domain2', + resourceType: 'domain', + }, + }, + ] + + await sendOrgFootprintEmail({ + notifyClient, + user, + auditLogs, + orgNames: { + en: 'Test Org', + fr: 'Le Test Org', + }, + }) + + expect(notifyClient.sendEmail).toHaveBeenCalledWith(NOTIFICATION_ORG_FOOTPRINT_EN, user.userName, { + personalisation: { + display_name: user.displayName, + organization_name: 'Test Org', + add_users_count: 2, + update_users_count: 0, + remove_users_count: 0, + add_domains_count: 0, + add_domains_list: '', + update_domains_count: 1, + update_domains_list: 'domain2', + remove_domains_count: 1, + remove_domains_list: 'domain1', + export_count: 0, + }, + }) + }) + }) + describe('an error occurs while sending email', () => { + it('throws an error message', async () => { + const sendEmail = jest.fn().mockRejectedValue(new Error('Notification error occurred.')) + const notifyClient = { + sendEmail, + } + + const user = { + userName: 'test@email.ca', + displayName: 'Test Account', + preferredLang: 'english', + } + + await sendOrgFootprintEmail({ + notifyClient, + user, + auditLogs: [], + orgNames: { + en: 'Test Org', + fr: 'Le Test Org', + }, + }) + + expect(consoleOutput).toEqual([ + `Error occurred when sending org footprint changes via email for ${user._key}: Error: Notification error occurred.`, + ]) + }) + }) +}) diff --git a/services/org-footprint/src/database/get-all-org-keys.js b/services/org-footprint/src/database/get-all-org-keys.js new file mode 100644 index 0000000000..71a2745b75 --- /dev/null +++ b/services/org-footprint/src/database/get-all-org-keys.js @@ -0,0 +1,24 @@ +const getAllOrgKeys = async ({ query }) => { + let cursor + try { + cursor = await query` + FOR org IN organizations + RETURN org._key + ` + } catch (err) { + throw new Error(`Database error occurred while trying to find org keys: ${err}`) + } + + let orgKeys + try { + orgKeys = await cursor.all() + } catch (err) { + throw new Error(`Cursor error occurred while trying to find org keys: ${err}`) + } + + return orgKeys +} + +module.exports = { + getAllOrgKeys, +} diff --git a/services/org-footprint/src/database/get-bilingual-org-names.js b/services/org-footprint/src/database/get-bilingual-org-names.js new file mode 100644 index 0000000000..4293a29908 --- /dev/null +++ b/services/org-footprint/src/database/get-bilingual-org-names.js @@ -0,0 +1,25 @@ +const getBilingualOrgNames = async ({ query, orgKey }) => { + let cursor + try { + cursor = await query` + FOR org IN organizations + FILTER org._key == ${orgKey} + RETURN { "en": org.orgDetails.en.name, "fr": org.orgDetails.fr.name } + ` + } catch (err) { + throw new Error(`Database error occurred while trying to find org names: ${err}`) + } + + let orgNames + try { + orgNames = await cursor.next() + } catch (err) { + throw new Error(`Cursor error occurred while trying to find org names: ${err}`) + } + + return orgNames +} + +module.exports = { + getBilingualOrgNames, +} diff --git a/services/org-footprint/src/database/get-new-audit-logs.js b/services/org-footprint/src/database/get-new-audit-logs.js new file mode 100644 index 0000000000..08cb36f750 --- /dev/null +++ b/services/org-footprint/src/database/get-new-audit-logs.js @@ -0,0 +1,30 @@ +const getNewAuditLogs = async ({ query, orgKey, days = 1 }) => { + const twentyFourHours = 1000 * 60 * 60 * 24 + let cursor + try { + cursor = await query` + LET timeframe = ${twentyFourHours} * ${days} + let currentTime = DATE_NOW() + FOR log IN auditLogs + let logTime = DATE_TIMESTAMP(log.timestamp) + FILTER log.target.organization.id == ${orgKey} + FILTER (currentTime - logTime) < timeframe + RETURN log + ` + } catch (err) { + throw new Error(`Database error occurred while trying to find audit logs: ${err}`) + } + + let auditLogs + try { + auditLogs = await cursor.all() + } catch (err) { + throw new Error(`Cursor error occurred while trying to find audit logs: ${err}`) + } + + return auditLogs +} + +module.exports = { + getNewAuditLogs, +} diff --git a/services/org-footprint/src/database/get-org-admins.js b/services/org-footprint/src/database/get-org-admins.js new file mode 100644 index 0000000000..6643b55e88 --- /dev/null +++ b/services/org-footprint/src/database/get-org-admins.js @@ -0,0 +1,27 @@ +const getOrgAdmins = async ({ query, orgKey }) => { + let cursor + const orgId = `organizations/${orgKey}` + try { + cursor = await query` + FOR v, e IN 1..1 OUTBOUND ${orgId} affiliations + FILTER e.permission == "admin" OR e.permission == "owner" + FILTER v.receiveUpdateEmails != false + RETURN v + ` + } catch (err) { + throw new Error(`Database error occurred while trying to find org admins: ${err}`) + } + + let orgAdmins + try { + orgAdmins = await cursor.all() + } catch (err) { + throw new Error(`Cursor error occurred while trying to find org admins: ${err}`) + } + + return orgAdmins +} + +module.exports = { + getOrgAdmins, +} diff --git a/services/org-footprint/src/database/index.js b/services/org-footprint/src/database/index.js new file mode 100644 index 0000000000..3fa210c3f7 --- /dev/null +++ b/services/org-footprint/src/database/index.js @@ -0,0 +1,6 @@ +const { getOrgAdmins } = require('./get-org-admins') +const { getAllOrgKeys } = require('./get-all-org-keys') +const { getNewAuditLogs } = require('./get-new-audit-logs') +const { getBilingualOrgNames } = require('./get-bilingual-org-names') + +module.exports = { getOrgAdmins, getAllOrgKeys, getNewAuditLogs, getBilingualOrgNames } diff --git a/services/org-footprint/src/index.js b/services/org-footprint/src/index.js new file mode 100644 index 0000000000..fb7893d7af --- /dev/null +++ b/services/org-footprint/src/index.js @@ -0,0 +1,5 @@ +const { orgFootprintService } = require('./org-footprint-service') + +module.exports = { + orgFootprintService, +} diff --git a/services/org-footprint/src/notify/index.js b/services/org-footprint/src/notify/index.js new file mode 100644 index 0000000000..bef4dbdf1a --- /dev/null +++ b/services/org-footprint/src/notify/index.js @@ -0,0 +1,2 @@ +const { sendOrgFootprintEmail } = require('./notify-send-org-footprint-email') +module.exports = { sendOrgFootprintEmail } diff --git a/services/org-footprint/src/notify/notify-send-org-footprint-email.js b/services/org-footprint/src/notify/notify-send-org-footprint-email.js new file mode 100644 index 0000000000..37ae6aa765 --- /dev/null +++ b/services/org-footprint/src/notify/notify-send-org-footprint-email.js @@ -0,0 +1,62 @@ +const { NOTIFICATION_ORG_FOOTPRINT_EN, NOTIFICATION_ORG_FOOTPRINT_FR } = process.env + +const sendOrgFootprintEmail = async ({ notifyClient, user, auditLogs, orgNames }) => { + let templateId = NOTIFICATION_ORG_FOOTPRINT_EN + if (user.preferredLang === 'french') { + templateId = NOTIFICATION_ORG_FOOTPRINT_FR + } + + // Get stats for user changes + const usersAdded = auditLogs.filter((log) => log.action === 'add' && log.target.resourceType === 'user') + const usersUpdated = auditLogs.filter((log) => log.action === 'update' && log.target.resourceType === 'user') + const usersRemoved = auditLogs.filter((log) => log.action === 'remove' && log.target.resourceType === 'user') + + // Get stats for domain changes + const domainsAdded = auditLogs.filter((log) => log.action === 'add' && log.target.resourceType === 'domain') + const domainsUpdated = auditLogs.filter((log) => log.action === 'update' && log.target.resourceType === 'domain') + const domainsRemoved = auditLogs.filter((log) => log.action === 'remove' && log.target.resourceType === 'domain') + + let addDomainsList = '' + let updateDomainsList = '' + let removeDomainsList = '' + + // Get list of domains added + if (domainsAdded.length > 0) { + addDomainsList = domainsAdded.map((log) => log.target.resource).join(', ') + } + // Get list of domains updated + if (domainsUpdated.length > 0) { + updateDomainsList = domainsUpdated.map((log) => log.target.resource).join(', ') + } + // Get list of domains removed + if (domainsRemoved.length > 0) { + removeDomainsList = domainsRemoved.map((log) => log.target.resource).join(', ') + } + + const exportsToCsv = auditLogs.filter((log) => log.action === 'export') + + try { + await notifyClient.sendEmail(templateId, user.userName, { + personalisation: { + display_name: user.displayName, + organization_name: user.preferredLang === 'french' ? orgNames.fr : orgNames.en, + add_users_count: usersAdded.length, + update_users_count: usersUpdated.length, + remove_users_count: usersRemoved.length, + add_domains_count: domainsAdded.length, + add_domains_list: addDomainsList, + update_domains_count: domainsUpdated.length, + update_domains_list: updateDomainsList, + remove_domains_count: domainsRemoved.length, + remove_domains_list: removeDomainsList, + export_count: exportsToCsv.length, + }, + }) + } catch (err) { + console.error(`Error occurred when sending org footprint changes via email for ${user._key}: ${err}`) + } +} + +module.exports = { + sendOrgFootprintEmail, +} diff --git a/services/org-footprint/src/org-footprint-service.js b/services/org-footprint/src/org-footprint-service.js new file mode 100644 index 0000000000..6f636aec7a --- /dev/null +++ b/services/org-footprint/src/org-footprint-service.js @@ -0,0 +1,44 @@ +const { getOrgAdmins, getAllOrgKeys, getNewAuditLogs, getBilingualOrgNames } = require('./database') +const { sendOrgFootprintEmail } = require('./notify') + +const { SERVICE_ACCOUNT_EMAIL, REDIRECT_TO_SERVICE_ACCOUNT_EMAIL } = process.env + +const orgFootprintService = async ({ query, log, notifyClient }) => { + // get list of all orgs + const orgKeys = await getAllOrgKeys({ query }) + for (const orgKey of orgKeys) { + // check for new audit logs + const auditLogs = await getNewAuditLogs({ query, orgKey }) + // if new audit logs exist + if (auditLogs.length > 0) { + if (REDIRECT_TO_SERVICE_ACCOUNT_EMAIL) { + const orgNames = await getBilingualOrgNames({ query, orgKey }) + log(`Sending recent activity email to service account: ${orgKey}`) + // send email to service account + const user = { + userName: SERVICE_ACCOUNT_EMAIL, + displayName: 'Service Account', + preferredLang: 'en', + _key: 'service-account', + } + await sendOrgFootprintEmail({ notifyClient, user, auditLogs, orgNames }) + } else { + // get list of org admins + const orgAdmins = await getOrgAdmins({ query, orgKey }) + // if org admins exist + if (orgAdmins.length > 0) { + const orgNames = await getBilingualOrgNames({ query, orgKey }) + log(`Sending recent activity email to admins of org: ${orgKey}`) + // send email to org admins + for (const user of orgAdmins) { + await sendOrgFootprintEmail({ notifyClient, user, auditLogs, orgNames }) + } + } + } + } + } +} + +module.exports = { + orgFootprintService, +} diff --git a/services/org-footprint/src/setupEnv.js b/services/org-footprint/src/setupEnv.js new file mode 100644 index 0000000000..487e141b83 --- /dev/null +++ b/services/org-footprint/src/setupEnv.js @@ -0,0 +1,4 @@ +require('dotenv-safe').config({ + example: './.env.example', + path: './test.env', +}) diff --git a/services/summaries/Dockerfile b/services/summaries/Dockerfile index 656d0cd084..729468fbe0 100644 --- a/services/summaries/Dockerfile +++ b/services/summaries/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11.2-slim-bullseye +FROM python:3.11.3-slim-bullseye ENV PYTHONUNBUFFERED 1 ENV PYTHONDONTWRITEBYTECODE 1 diff --git a/services/summaries/cloudbuild.yaml b/services/summaries/cloudbuild.yaml index 38116c1f13..e28d878524 100644 --- a/services/summaries/cloudbuild.yaml +++ b/services/summaries/cloudbuild.yaml @@ -2,7 +2,7 @@ steps: - name: 'gcr.io/cloud-builders/docker' id: start_testdb entrypoint: /bin/sh - args: ["-c", "docker run --net cloudbuild --name testdb -e ARANGO_NO_AUTH=1 -d -p 127.0.0.1:8529:8529 arangodb"] + args: ["-c", "docker run --net cloudbuild --name testdb -e ARANGO_NO_AUTH=1 -d -p 127.0.0.1:8529:8529 arangodb/arangodb:3.10.4"] - name: mikewilliamson/wait-for id: wait_testdb diff --git a/services/summaries/requirements.txt b/services/summaries/requirements.txt index 148fec0845..e0dbd979fe 100644 --- a/services/summaries/requirements.txt +++ b/services/summaries/requirements.txt @@ -12,7 +12,7 @@ PyJWT==2.6.0 pytest==7.2.2 python-arango==7.5.7 python-dotenv==1.0.0 -requests==2.28.2 +requests==2.31.0 requests-toolbelt==0.10.1 tomli==2.0.1 urllib3==1.26.14 diff --git a/services/summaries/summaries-job.yaml b/services/summaries/summaries-job.yaml index 7a39149b2f..dde3a984f6 100644 --- a/services/summaries/summaries-job.yaml +++ b/services/summaries/summaries-job.yaml @@ -8,7 +8,7 @@ spec: spec: containers: - name: summaries - image: gcr.io/track-compliance/services/summaries:master-cbbb2fd-1681235183 # {"$imagepolicy": "flux-system:summaries"} + image: gcr.io/track-compliance/services/summaries:master-955c53b-1685969471 # {"$imagepolicy": "flux-system:summaries"} env: - name: DB_USER valueFrom: diff --git a/services/summaries/summaries.py b/services/summaries/summaries.py index 4e87fdd830..62eb621f93 100644 --- a/services/summaries/summaries.py +++ b/services/summaries/summaries.py @@ -1,12 +1,6 @@ import os -import re import sys -import time -import json import logging -import traceback -import random -import datetime from arango import ArangoClient from dotenv import load_dotenv @@ -17,12 +11,23 @@ DB_NAME = os.getenv("DB_NAME") DB_URL = os.getenv("DB_URL") -SCAN_TYPES = ["https", "ssl", "dkim", "spf", "dmarc"] -CHARTS = {"mail": ["dmarc", "spf", "dkim"], "web": ["https", "ssl"], - "https": ["https"]} +CHARTS = { + # tier 1 + "https": ["https"], + "dmarc": ["dmarc"], + # tier 2 + "web_connections": ["https", "hsts"], + "ssl": ["ssl"], + "spf": ["spf"], + "dkim": ["dkim"], + # tier 3 + "mail": ["dmarc", "spf", "dkim"], + "web": ["https", "hsts", "ssl"], +} logging.basicConfig(stream=sys.stdout, level=logging.INFO) + def is_domain_hidden(domain, db): """Check if a domain is hidden @@ -39,87 +44,71 @@ def is_domain_hidden(domain, db): return False -def update_scan_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, - password=DB_PASS): - logging.info(f"Updating scan summaries...") - - # Establish DB connection - client = ArangoClient(hosts=host) - db = client.db(name, username=user, password=password) - - for scan_type in SCAN_TYPES: - scan_pass = 0 - scan_fail = 0 - scan_total = 0 - for domain in db.collection("domains"): - archived = domain.get("archived") - hidden = is_domain_hidden(domain, db) - if archived != True and hidden != True: - # We don't want to count domains not passing or failing - # (i.e unreachable or unscanned) towards the total. - if domain.get("status", {}).get(scan_type) == "fail": - scan_total = scan_total + 1 - scan_fail = scan_fail + 1 - - elif domain.get("status", {}).get(scan_type): - scan_total = scan_total + 1 - scan_pass = scan_pass + 1 - - current_summary = db.collection("scanSummaries").get( - {"_key": scan_type}) - - summary_exists = current_summary is not None - - if not summary_exists: - db.collection("scanSummaries").insert( - { - "_key": scan_type, - "pass": scan_pass, - "fail": scan_fail, - "total": scan_total, - } - ) - else: - db.collection("scanSummaries").update_match( - {"_key": scan_type}, - {"pass": scan_pass, "fail": scan_fail, "total": scan_total}, - ) +def ignore_domain(domain): + """Check if a domain should be ignored - logging.info(f"{scan_type} scan summary updated.") + :param domain: domain to check + :return: True if domain should be ignored, False otherwise + """ + if ( + domain.get("archived") is True + or domain.get("blocked") is True + or domain.get("rcode") == "NXDOMAIN" + ): + return True + return False - logging.info(f"Scan summary update completed.") +def update_chart_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, password=DB_PASS): + logging.info(f"Updating chart summaries...") -def update_dmarc_phase_chart_summaries(db): - """Update the dmarc phase chart summaries in the database + # Establish DB connection + client = ArangoClient(hosts=host) + db = client.db(name, username=user, password=password) - :param db: active arangodb connection - """ + # Gather summaries from domain statuses + chartSummaries = {} + for chart_type, scan_types in CHARTS.items(): + chartSummaries[chart_type] = { + "scan_types": scan_types, + "pass_count": 0, + "fail_count": 0, + "domain_total": 0, + } # DMARC phases: # 0. Not Implemented - # 1. Assess - # 2. Deploy - # 3. Enforce - # 4. Maintain - not_implemented_count = 0 + # 1. Assess assess_count = 0 + # 2. Deploy deploy_count = 0 + # 3. Enforce enforce_count = 0 + # 4. Maintain maintain_count = 0 - domain_total = 0 - for domain in db.collection("domains"): - archived = domain.get("archived") - hidden = is_domain_hidden(domain, db) - if archived != True and hidden != True: - phase = domain.get("phase") + if ignore_domain(domain) is False: + # Update chart summaries + for chart_type in chartSummaries: + chart = chartSummaries[chart_type] + category_status = [] + for scan_type in chart["scan_types"]: + category_status.append(domain.get("status", {}).get(scan_type)) + if "fail" in category_status: + chart["fail_count"] += 1 + chart["domain_total"] += 1 + elif "info" not in category_status: + chart["pass_count"] += 1 + chart["domain_total"] += 1 + # Update DMARC phase summaries + phase = domain.get("phase") if phase is None: logging.info( - f"Property \"phase\" does not exist for domain \"{domain['domain']}\".") + f"Property \"phase\" does not exist for domain \"{domain['domain']}\"." + ) continue if phase == "not implemented": @@ -133,12 +122,41 @@ def update_dmarc_phase_chart_summaries(db): elif phase == "maintain": maintain_count = maintain_count + 1 - domain_total = not_implemented_count + assess_count + deploy_count + \ - enforce_count + maintain_count + # Update chart summaries in DB + for chart_type in chartSummaries: + chart = chartSummaries[chart_type] + current_summary = db.collection("chartSummaries").get({"_key": chart_type}) - current_summary = db.collection("chartSummaries").get( - {"_key": "dmarc_phase"}) + summary_exists = current_summary is not None + if not summary_exists: + db.collection("chartSummaries").insert( + { + "_key": chart_type, + "pass": chart["pass_count"], + "fail": chart["fail_count"], + "total": chart["domain_total"], + } + ) + else: + db.collection("chartSummaries").update_match( + {"_key": chart_type}, + { + "pass": chart["pass_count"], + "fail": chart["fail_count"], + "total": chart["domain_total"], + }, + ) + + # Update DMARC phase summaries in DB + domain_total = ( + not_implemented_count + + assess_count + + deploy_count + + enforce_count + + maintain_count + ) + current_summary = db.collection("chartSummaries").get({"_key": "dmarc_phase"}) summary_exists = current_summary is not None if not summary_exists: @@ -162,68 +180,14 @@ def update_dmarc_phase_chart_summaries(db): "deploy": deploy_count, "enforce": enforce_count, "maintain": maintain_count, - "total": domain_total, }, + "total": domain_total, + }, ) - logging.info("DMARC phase scan summary updated.") - - -def update_chart_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, - password=DB_PASS): - logging.info(f"Updating chart summaries...") - - # Establish DB connection - client = ArangoClient(hosts=host) - db = client.db(name, username=user, password=password) - - for chart_type, scan_types in CHARTS.items(): - pass_count = 0 - fail_count = 0 - domain_total = 0 - for domain in db.collection("domains"): - archived = domain.get("archived") - hidden = is_domain_hidden(domain, db) - if archived != True and hidden != True: - category_status = [] - for scan_type in scan_types: - category_status.append(domain.get("status", {}).get(scan_type)) - - if "fail" in category_status: - fail_count = fail_count + 1 - elif "info" not in category_status: - pass_count = pass_count + 1 - - domain_total = pass_count + fail_count - current_summary = db.collection("chartSummaries").get( - {"_key": chart_type}) - - summary_exists = current_summary is not None - - if not summary_exists: - db.collection("chartSummaries").insert( - { - "_key": chart_type, - "pass": pass_count, - "fail": fail_count, - "total": domain_total, - } - ) - else: - db.collection("chartSummaries").update_match( - {"_key": chart_type}, - {"pass": pass_count, "fail": fail_count, "total": domain_total}, - ) - - logging.info(f"{chart_type} scan summary updated.") - - # handle DMARC phase summary - update_dmarc_phase_chart_summaries(db) - logging.info(f"Chart summary update completed.") -def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, - password=DB_PASS): +def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, password=DB_PASS): logging.info(f"Updating organization summary values...") # Establish DB connection @@ -239,74 +203,125 @@ def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, web_pass = 0 mail_fail = 0 mail_pass = 0 + web_connections_fail = 0 + web_connections_pass = 0 + ssl_fail = 0 + ssl_pass = 0 + spf_fail = 0 + spf_pass = 0 + dkim_fail = 0 + dkim_pass = 0 dmarc_phase_not_implemented = 0 dmarc_phase_assess = 0 dmarc_phase_deploy = 0 dmarc_phase_enforce = 0 dmarc_phase_maintain = 0 + + hidden_https_pass = 0 + hidden_https_fail = 0 + hidden_dmarc_pass = 0 + hidden_dmarc_fail = 0 + domain_total = 0 claims = db.collection("claims").find({"_from": org["_id"]}) for claim in claims: domain = db.collection("domains").get({"_id": claim["_to"]}) - archived = domain.get("archived") - hidden = claim.get("hidden") - if hidden != True and archived != True: - domain_total = domain_total + 1 - if domain.get("status", {}).get("dmarc") == "pass": - dmarc_pass = dmarc_pass + 1 - else: - dmarc_fail = dmarc_fail + 1 - - if ( - domain.get("status", {}).get("ssl") == "pass" - and domain.get("status", {}).get("https") == "pass" - ): - web_pass = web_pass + 1 - elif ( - domain.get("status", {}).get("ssl") == "fail" - or domain.get("status", {}).get("https") == "fail" - ): - web_fail = web_fail + 1 - - if domain.get("status", {}).get("https") == "pass": - https_pass = https_pass + 1 - if domain.get("status", {}).get("https") == "fail": - https_fail = https_fail + 1 - - if ( - domain.get("status", {}).get("dmarc") == "pass" - and domain.get("status", {}).get("spf") == "pass" - and domain.get("status", {}).get("dkim") == "pass" - ): - mail_pass = mail_pass + 1 + if ignore_domain(domain) is False: + hidden = claim.get("hidden") + if hidden != True: + domain_total = domain_total + 1 + if domain.get("status", {}).get("dmarc") == "pass": + dmarc_pass = dmarc_pass + 1 + else: + dmarc_fail = dmarc_fail + 1 + + if ( + domain.get("status", {}).get("ssl") == "pass" + and domain.get("status", {}).get("https") == "pass" + ): + web_pass = web_pass + 1 + elif ( + domain.get("status", {}).get("ssl") == "fail" + or domain.get("status", {}).get("https") == "fail" + ): + web_fail = web_fail + 1 + + if domain.get("status", {}).get("https") == "pass": + https_pass = https_pass + 1 + if domain.get("status", {}).get("https") == "fail": + https_fail = https_fail + 1 + + if ( + domain.get("status", {}).get("dmarc") == "pass" + and domain.get("status", {}).get("spf") == "pass" + and domain.get("status", {}).get("dkim") == "pass" + ): + mail_pass = mail_pass + 1 + else: + mail_fail = mail_fail + 1 + + if domain.get("status", {}).get("spf") == "pass": + spf_pass = spf_pass + 1 + else: + spf_fail = spf_fail + 1 + + if domain.get("status", {}).get("dkim") == "pass": + dkim_pass = dkim_pass + 1 + else: + dkim_fail = dkim_fail + 1 + + if domain.get("status", {}).get("ssl") == "pass": + ssl_pass = ssl_pass + 1 + elif domain.get("status", {}).get("ssl") == "fail": + ssl_fail = ssl_fail + 1 + + if ( + domain.get("status", {}).get("https") == "pass" + and domain.get("status", {}).get("hsts") == "pass" + ): + web_connections_pass = web_connections_pass + 1 + elif ( + domain.get("status", {}).get("https") == "fail" + or domain.get("status", {}).get("hsts") == "fail" + ): + web_connections_fail = web_connections_fail + 1 + + phase = domain.get("phase") + + if phase is None: + logging.info( + f"Property \"phase\" does not exist for domain \"${domain['domain']}\"." + ) + continue + + if phase == "not implemented": + dmarc_phase_not_implemented = dmarc_phase_not_implemented + 1 + elif phase == "assess": + dmarc_phase_assess = dmarc_phase_assess + 1 + elif phase == "deploy": + dmarc_phase_deploy = dmarc_phase_deploy + 1 + elif phase == "enforce": + dmarc_phase_enforce = dmarc_phase_enforce + 1 + elif phase == "maintain": + dmarc_phase_maintain = dmarc_phase_maintain + 1 else: - mail_fail = mail_fail + 1 - - phase = domain.get("phase") - - if phase is None: - logging.info( - f"Property \"phase\" does not exist for domain \"${domain['domain']}\".") - continue - - if phase == "not implemented": - dmarc_phase_not_implemented = dmarc_phase_not_implemented + 1 - elif phase == "assess": - dmarc_phase_assess = dmarc_phase_assess + 1 - elif phase == "deploy": - dmarc_phase_deploy = dmarc_phase_deploy + 1 - elif phase == "enforce": - dmarc_phase_enforce = dmarc_phase_enforce + 1 - elif phase == "maintain": - dmarc_phase_maintain = dmarc_phase_maintain + 1 + if domain.get("status", {}).get("dmarc") == "pass": + hidden_dmarc_pass = hidden_dmarc_pass + 1 + else: + hidden_dmarc_fail = hidden_dmarc_fail + 1 + + if domain.get("status", {}).get("https") == "pass": + hidden_https_pass = hidden_https_pass + 1 + elif domain.get("status", {}).get("https") == "fail": + hidden_https_fail = hidden_https_fail + 1 summary_data = { "summaries": { "dmarc": { "pass": dmarc_pass, "fail": dmarc_fail, - "total": dmarc_pass + dmarc_fail + "total": dmarc_pass + dmarc_fail, }, "web": { "pass": web_pass, @@ -332,7 +347,41 @@ def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, "fail": https_fail, "total": https_pass + https_fail # Don't count non web-hosting domains - } + }, + "ssl": { + "pass": ssl_pass, + "fail": ssl_fail, + "total": ssl_pass + ssl_fail + # Don't count non web-hosting domains + }, + "spf": { + "pass": spf_pass, + "fail": spf_fail, + "total": spf_pass + spf_fail, + }, + "dkim": { + "pass": dkim_pass, + "fail": dkim_fail, + "total": dkim_pass + dkim_fail, + }, + "web_connections": { + "pass": web_connections_pass, + "fail": web_connections_fail, + "total": web_connections_pass + web_connections_fail + # Don't count non web-hosting domains + }, + "hidden": { + "dmarc": { + "pass": hidden_dmarc_pass, + "fail": hidden_dmarc_fail, + "total": hidden_dmarc_pass + hidden_dmarc_fail, + }, + "https": { + "pass": hidden_https_pass, + "fail": hidden_https_fail, + "total": hidden_https_pass + hidden_https_fail, + }, + }, } } @@ -344,7 +393,6 @@ def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, if __name__ == "__main__": logging.info("Summary service started") - update_scan_summaries() update_chart_summaries() update_org_summaries() logging.info(f"Summary service shutting down...") diff --git a/services/summaries/tests/test_summaries.py b/services/summaries/tests/test_summaries.py index 270311939a..5b863beef5 100644 --- a/services/summaries/tests/test_summaries.py +++ b/services/summaries/tests/test_summaries.py @@ -32,9 +32,17 @@ "web": {"pass": 0, "fail": 0, "total": 0}, "mail": {"pass": 0, "fail": 0, "total": 0}, "https": {"pass": 0, "fail": 0, "total": 0}, - "dmarc_phase": {"not_implemented": 0, "assess": 0, "deploy": 0, - "enforce": 0, "maintain": 0}, - + "web_connections": {"pass": 0, "fail": 0, "total": 0}, + "ssl": {"pass": 0, "fail": 0, "total": 0}, + "dkim": {"pass": 0, "fail": 0, "total": 0}, + "spf": {"pass": 0, "fail": 0, "total": 0}, + "dmarc_phase": { + "not_implemented": 0, + "assess": 0, + "deploy": 0, + "enforce": 0, + "maintain": 0, + }, }, "orgDetails": { "en": { @@ -112,60 +120,6 @@ claims.insert({"_from": org["_id"], "_to": domain3["_id"], "hidden": False}) -def test_update_scan_summaries(): - update_scan_summaries(host="http://testdb:8529", name="test", user="", password="") - - httpsScanSummary = db.collection("scanSummaries").get({"_key": "https"}) - assert httpsScanSummary == { - "_id": "scanSummaries/https", - "_rev": httpsScanSummary["_rev"], - "_key": "https", - "pass": 2, - "fail": 1, - "total": 3, - } - - sslScanSummary = db.collection("scanSummaries").get({"_key": "ssl"}) - assert sslScanSummary == { - "_id": "scanSummaries/ssl", - "_rev": sslScanSummary["_rev"], - "_key": "ssl", - "pass": 2, - "fail": 1, - "total": 3, - } - - dmarcScanSummary = db.collection("scanSummaries").get({"_key": "dmarc"}) - assert dmarcScanSummary == { - "_id": "scanSummaries/dmarc", - "_rev": dmarcScanSummary["_rev"], - "_key": "dmarc", - "pass": 2, - "fail": 1, - "total": 3, - } - - spfScanSummary = db.collection("scanSummaries").get({"_key": "spf"}) - assert spfScanSummary == { - "_id": "scanSummaries/spf", - "_rev": spfScanSummary["_rev"], - "_key": "spf", - "pass": 2, - "fail": 1, - "total": 3, - } - - dkimScanSummary = db.collection("scanSummaries").get({"_key": "dkim"}) - assert dkimScanSummary == { - "_id": "scanSummaries/dkim", - "_rev": dkimScanSummary["_rev"], - "_key": "dkim", - "pass": 1, - "fail": 2, - "total": 3, - } - - def test_update_chart_summaries(): update_chart_summaries(host="http://testdb:8529", name="test", user="", password="") @@ -222,6 +176,20 @@ def test_update_org_summaries(): "https": {"pass": 2, "fail": 1, "total": 3}, "web": {"pass": 2, "fail": 1, "total": 3}, "mail": {"pass": 1, "fail": 2, "total": 3}, - "dmarc_phase": {"not_implemented": 1, "assess": 0, "deploy": 0, - "enforce": 0, "maintain": 2, "total": 3}, + "web_connections": {"pass": 0, "fail": 1, "total": 1}, + "ssl": {"pass": 2, "fail": 1, "total": 3}, + "dkim": {"pass": 1, "fail": 2, "total": 3}, + "spf": {"pass": 2, "fail": 1, "total": 3}, + "hidden": { + "https": {"pass": 0, "fail": 0, "total": 0}, + "dmarc": {"pass": 0, "fail": 0, "total": 0}, + }, + "dmarc_phase": { + "not_implemented": 1, + "assess": 0, + "deploy": 0, + "enforce": 0, + "maintain": 2, + "total": 3, + }, } diff --git a/services/super-admin/cloudbuild.yaml b/services/super-admin/cloudbuild.yaml index 3a2cbd6022..35a65b28d3 100644 --- a/services/super-admin/cloudbuild.yaml +++ b/services/super-admin/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: [ '-c', - 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb', + 'docker run -d --network=cloudbuild -p=8529:8529 -e ARANGO_ROOT_PASSWORD=$_DB_PASS --name=arangodb arangodb/arangodb:3.10.4', ] - name: mikewilliamson/wait-for