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__/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/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/remove-user-from-org.js b/api/src/affiliation/mutations/remove-user-from-org.js index 237461d3aa..971d7069fd 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,14 +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}) + 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.`, @@ -87,9 +84,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 +101,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 +111,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,20 +122,13 @@ 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.`)) } let canRemove - if ( - permission === 'super_admin' && - (affiliation.permission === 'admin' || affiliation.permission === 'user') - ) { + if (permission === 'super_admin' && ['pending', 'user', 'admin'].includes(affiliation.permission)) { canRemove = true - } else if (permission === 'admin' && affiliation.permission === 'user') { + } else if (permission === 'admin' && ['pending', 'user'].includes(affiliation.permission)) { canRemove = true } else { canRemove = false @@ -171,11 +153,7 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ 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.`, - ), - ) + throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`)) } try { @@ -184,16 +162,10 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ 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.`, - ), - ) + 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}.`, - ) + console.info(`User: ${userKey} successfully removed user: ${requestedUser._key} from org: ${requestedOrg._key}.`) await logActivity({ transaction, collections, @@ -229,9 +201,7 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ return { _type: 'error', code: 400, - description: i18n._( - t`Permission Denied: Please contact organization admin for help with removing users.`, - ), + description: i18n._(t`Permission Denied: Please contact organization admin for help with removing users.`), } } }, 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..ee9f733ba7 --- /dev/null +++ b/api/src/affiliation/mutations/request-org-affiliation.js @@ -0,0 +1,201 @@ +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", + owner: false + } 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" + 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/update-user-role.js b/api/src/affiliation/mutations/update-user-role.js index df985043b9..8850d51c54 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,16 @@ 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') { + if (!['admin', 'super_admin'].includes(permission) || typeof permission === 'undefined') { 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 +122,7 @@ 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 +132,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,9 +143,7 @@ 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.`), - ) + throw new Error(i18n._(t`Unable to update user's role. Please try again.`)) } // Setup Transaction @@ -170,10 +157,7 @@ given organization.`, _to: requestedUser._id, permission: 'super_admin', } - } else if ( - role === 'admin' && - (permission === 'admin' || permission === 'super_admin') - ) { + } else if (role === 'admin' && ['admin', 'super_admin'].includes(permission)) { // If requested user's permission is super admin, make sure they don't get downgraded if (affiliation.permission === 'super_admin') { console.warn( @@ -223,9 +207,7 @@ given organization.`, 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 updating user roles.`), } } @@ -243,9 +225,7 @@ 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 +234,10 @@ 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/auth/check-permission.js b/api/src/auth/check-permission.js index 814b1735e0..dad9a94eaf 100644 --- a/api/src/auth/check-permission.js +++ b/api/src/auth/check-permission.js @@ -1,63 +1,53 @@ -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 + 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 + } else { + // 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 ` - } 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/enums/roles.js b/api/src/enums/roles.js index ae3a65a4a3..d6ae2420f1 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.', @@ -14,8 +18,7 @@ export const RoleEnums = new GraphQLEnumType({ }, 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/locale/en/messages.po b/api/src/locale/en/messages.po index 3ec44d61fc..c777614f32 100644 --- a/api/src/locale/en/messages.po +++ b/api/src/locale/en/messages.po @@ -11,15 +11,15 @@ 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:42 #: 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:161 msgid "Cannot query affiliations on organization without admin permission or higher." msgstr "Cannot query affiliations on organization without admin permission or higher." @@ -93,7 +93,7 @@ msgstr "Organization name already in use. Please try again with a different name 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." @@ -227,7 +227,7 @@ 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:204 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,9 +235,9 @@ 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 +#: 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." @@ -245,7 +245,7 @@ msgstr "Permission Denied: Please contact organization admin for help with updat 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:108 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." @@ -295,7 +295,7 @@ 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." @@ -320,7 +320,7 @@ msgstr "Phone number has been successfully set, you will receive a verification 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." @@ -447,7 +447,7 @@ msgstr "Successfully removed domain: {0} from {1}." 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:191 msgid "Successfully removed user from organization." msgstr "Successfully removed user from organization." @@ -455,6 +455,10 @@ msgstr "Successfully removed user from organization." msgid "Successfully removed {domainCount} domain(s) from {0}." msgstr "Successfully removed {domainCount} domain(s) from {0}." +#: src/affiliation/mutations/request-org-affiliation.js:201 +msgid "Successfully requested invite to organization, and sent notification email." +msgstr "Successfully requested invite to organization, and sent notification email." + #: src/domain/mutations/remove-organizations-domains.js:530 #~ msgid "Successfully removed {domainCount} domains from {0}." #~ msgstr "Successfully removed {domainCount} domains from {0}." @@ -518,8 +522,8 @@ msgstr "Unable to add domains in unknown organization." 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:49 #: src/auth/check-super-admin.js:20 #: src/auth/check-super-admin.js:30 msgid "Unable to check permission. Please try again." @@ -697,8 +701,9 @@ msgstr "Unable to invite user to organization. User is already affiliated with o msgid "Unable to invite user to unknown organization." msgstr "Unable to invite user to unknown organization." -#: src/affiliation/mutations/invite-user-to-org.js:212 -#: src/affiliation/mutations/invite-user-to-org.js:231 +#: src/affiliation/mutations/invite-user-to-org.js:194 +#: src/affiliation/mutations/invite-user-to-org.js:209 +#: src/affiliation/mutations/request-org-affiliation.js:134 msgid "Unable to invite user. Please try again." msgstr "Unable to invite user. Please try again." @@ -816,11 +821,11 @@ 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/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." @@ -932,7 +937,7 @@ 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." @@ -960,7 +965,7 @@ 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:114 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." @@ -1015,22 +1020,22 @@ 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:87 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 +#: 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:104 +#: src/affiliation/mutations/remove-user-from-org.js:125 +#: src/affiliation/mutations/remove-user-from-org.js:156 +#: src/affiliation/mutations/remove-user-from-org.js:165 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." @@ -1046,6 +1051,25 @@ msgstr "Unable to request a one time scan on an unknown domain." 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:109 +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:83 +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:73 +#: src/affiliation/mutations/request-org-affiliation.js:99 +#: src/affiliation/mutations/request-org-affiliation.js:150 +#: src/affiliation/mutations/request-org-affiliation.js:169 +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." @@ -1079,6 +1103,10 @@ msgstr "Unable to send authentication text message. Please try again." 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." @@ -1207,15 +1235,15 @@ msgstr "Unable to update password. Please try again." 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:135 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." @@ -1227,14 +1255,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:125 +#: src/affiliation/mutations/update-user-role.js:146 +#: src/affiliation/mutations/update-user-role.js:228 +#: src/affiliation/mutations/update-user-role.js:237 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." @@ -1271,7 +1299,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:270 msgid "User role was updated successfully." msgstr "User role was updated successfully." @@ -1287,7 +1315,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." @@ -1406,7 +1434,7 @@ msgstr "You must provide at most one pagination method (`before`, `after`, `offs 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/dmarc-summaries/loaders/load-dkim-failure-connections-by-sum-id.js:83 @@ -1433,7 +1461,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." diff --git a/api/src/locale/fr/messages.po b/api/src/locale/fr/messages.po index 6c5ec1fde5..cf5b442e4e 100644 --- a/api/src/locale/fr/messages.po +++ b/api/src/locale/fr/messages.po @@ -11,15 +11,15 @@ 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:42 #: 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:161 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." @@ -93,7 +93,7 @@ msgstr "Le nom de l'organisation est déjà utilisé. Veuillez réessayer avec u 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é." @@ -227,7 +227,7 @@ 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:204 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,9 +235,9 @@ 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 +#: 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." @@ -245,7 +245,7 @@ msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisat 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:108 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." @@ -295,7 +295,7 @@ 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." @@ -320,7 +320,7 @@ msgstr "Le numéro de téléphone a été configuré avec succès, vous recevrez 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." @@ -447,7 +447,7 @@ msgstr "A réussi à supprimer le domaine : {0} de {1}." 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:191 msgid "Successfully removed user from organization." msgstr "L'utilisateur a été retiré de l'organisation avec succès." @@ -455,6 +455,10 @@ msgstr "L'utilisateur a été retiré de l'organisation avec succès." msgid "Successfully removed {domainCount} domain(s) from {0}." msgstr "Supprimé avec succès le(s) domaine(s) {domainCount} de {0}." +#: src/affiliation/mutations/request-org-affiliation.js:201 +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/domain/mutations/remove-organizations-domains.js:530 #~ msgid "Successfully removed {domainCount} domains from {0}." #~ msgstr "Suppression réussie des domaines {domainCount} de {0}." @@ -518,8 +522,8 @@ msgstr "Impossible d'ajouter des domaines dans une organisation inconnue." 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:49 #: src/auth/check-super-admin.js:20 #: src/auth/check-super-admin.js:30 msgid "Unable to check permission. Please try again." @@ -697,8 +701,9 @@ msgstr "Impossible d'inviter un utilisateur dans une organisation. L'utilisateur 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:212 -#: src/affiliation/mutations/invite-user-to-org.js:231 +#: src/affiliation/mutations/invite-user-to-org.js:194 +#: src/affiliation/mutations/invite-user-to-org.js:209 +#: src/affiliation/mutations/request-org-affiliation.js:134 msgid "Unable to invite user. Please try again." msgstr "Impossible d'inviter un utilisateur. Veuillez réessayer." @@ -816,11 +821,11 @@ msgstr "Impossible de charger le(s) tag(s) d'orientation SSL. Veuillez réessaye #~ msgid "Unable to load SSL scan(s). Please try again." #~ msgstr "Impossible de charger le(s) scan(s) 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." @@ -932,7 +937,7 @@ msgstr "Impossible de charger le(s) scan(s) web. Veuillez réessayer." 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." @@ -960,7 +965,7 @@ 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:114 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." @@ -1015,22 +1020,22 @@ 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:87 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 +#: 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:104 +#: src/affiliation/mutations/remove-user-from-org.js:125 +#: src/affiliation/mutations/remove-user-from-org.js:156 +#: src/affiliation/mutations/remove-user-from-org.js:165 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." @@ -1046,6 +1051,25 @@ msgstr "Impossible de demander un scan unique sur un domaine inconnu." 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:109 +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:83 +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:73 +#: src/affiliation/mutations/request-org-affiliation.js:99 +#: src/affiliation/mutations/request-org-affiliation.js:150 +#: src/affiliation/mutations/request-org-affiliation.js:169 +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." @@ -1079,6 +1103,10 @@ msgstr "Impossible d'envoyer un message texte d'authentification. Veuillez rées 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." @@ -1207,15 +1235,15 @@ msgstr "Impossible de mettre à jour le mot de passe. Veuillez réessayer." 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:135 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." @@ -1227,14 +1255,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:125 +#: src/affiliation/mutations/update-user-role.js:146 +#: src/affiliation/mutations/update-user-role.js:228 +#: src/affiliation/mutations/update-user-role.js:237 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." @@ -1271,7 +1299,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:270 msgid "User role was updated successfully." msgstr "Le rôle de l'utilisateur a été mis à jour avec succès." @@ -1287,7 +1315,7 @@ 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`." @@ -1406,7 +1434,7 @@ msgstr "Vous devez fournir au plus une valeur de méthode de pagination (`before 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: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/dmarc-summaries/loaders/load-dkim-failure-connections-by-sum-id.js:83 @@ -1433,7 +1461,7 @@ msgstr "Vous devez fournir au plus une valeur de méthode de pagination (`before 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." 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/organization/objects/organization.js b/api/src/organization/objects/organization.js index d97fba897a..beee776b98 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' @@ -67,8 +61,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: { @@ -80,25 +73,11 @@ export const organizationType = new GraphQLObjectType({ type: GraphQLString, description: 'CSV formatted output of all domains in the organization including their email and web scan statuses.', - resolve: async ( - { _id }, - _args, - { loaders: { loadOrganizationDomainStatuses } }, - ) => { + resolve: async ({ _id }, _args, { loaders: { loadOrganizationDomainStatuses } }) => { const domains = await loadOrganizationDomainStatuses({ orgId: _id, }) - const headers = [ - 'domain', - 'https', - 'hsts', - 'ciphers', - 'curves', - 'protocols', - 'spf', - 'dkim', - 'dmarc', - ] + const headers = ['domain', 'https', 'hsts', 'ciphers', 'curves', 'protocols', 'spf', 'dkim', 'dmarc'] let csvOutput = headers.join(',') domains.forEach((domain) => { let csvLine = `${domain.domain}` @@ -120,8 +99,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 +115,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,16 +139,16 @@ 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') { @@ -183,15 +158,10 @@ export const organizationType = new GraphQLObjectType({ }) return affiliations } - 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.`)) }, }, }), interfaces: [nodeInterface], - description: - 'Organization object containing information for a given Organization.', + description: 'Organization object containing information for a given Organization.', }) diff --git a/frontend/mocking/faked_schema.js b/frontend/mocking/faked_schema.js index 872a472583..5c60a562c8 100644 --- a/frontend/mocking/faked_schema.js +++ b/frontend/mocking/faked_schema.js @@ -308,8 +308,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 +324,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 +347,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 +456,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 +545,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 +584,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 +611,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 +622,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 +655,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 +665,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 @@ -766,6 +784,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 +808,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 @@ -880,6 +904,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 +936,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 +1260,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 +1290,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 +1423,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 +1518,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 +1526,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 +1539,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 +1565,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 +1586,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 +1850,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. @@ -2413,7 +2518,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 +2734,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 +2745,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 +2757,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 +2828,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. @@ -2751,12 +2865,12 @@ export const getTypeNames = () => gql` } 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. @@ -2772,12 +2886,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. @@ -2798,13 +2912,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 @@ -2824,12 +2950,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. @@ -2853,16 +2979,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 { @@ -2873,6 +2999,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! @@ -2885,6 +3046,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 } @@ -2925,10 +3092,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 } @@ -2940,19 +3113,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. @@ -2971,6 +3144,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 @@ -2984,7 +3178,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 } @@ -2996,12 +3190,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 @@ -3021,16 +3215,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 @@ -3089,12 +3289,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 @@ -3115,12 +3315,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 @@ -3173,12 +3373,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 @@ -3190,12 +3390,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. @@ -3226,12 +3426,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. @@ -3256,12 +3456,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 { @@ -3269,12 +3469,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. @@ -3297,12 +3497,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. @@ -3357,12 +3557,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. @@ -3390,12 +3590,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. @@ -3439,12 +3639,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. @@ -3481,12 +3681,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. @@ -3517,12 +3717,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. @@ -3562,12 +3762,12 @@ export const getTypeNames = () => gql` } 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. @@ -3592,12 +3792,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/src/admin/AdminPanel.js b/frontend/src/admin/AdminPanel.js index ce2337721a..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' @@ -36,17 +29,13 @@ export function AdminPanel({ activeMenu, orgSlug, permission, orgId }) { - + @@ -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 af2c5ad963..7f113fcc70 100644 --- a/frontend/src/admin/UserListModal.js +++ b/frontend/src/admin/UserListModal.js @@ -38,10 +38,13 @@ export function UserListModal({ const toast = useToast() const initialFocusRef = useRef() - const [addUser, { loading: _addUserLoading }] = useMutation(INVITE_USER_TO_ORG, { - refetchQueries: ['PaginatedOrgAffiliations', 'FindAuditLogs'], + const refetchQueriesValues = { + refetchQueries: ['PaginatedOrgAffiliations', 'FindAuditLogs', 'FindMyUsers'], awaitRefetchQueries: true, + } + const [addUser, { loading: _addUserLoading }] = useMutation(INVITE_USER_TO_ORG, { + ...refetchQueriesValues, onError(error) { toast({ title: t`An error occurred.`, @@ -86,9 +89,7 @@ export function UserListModal({ }) const [updateUserRole, { loading: _updateLoading, error: _updateError }] = useMutation(UPDATE_USER_ROLE, { - refetchQueries: ['FindMyUsers', 'FindAuditLogs'], - awaitRefetchQueries: true, - + ...refetchQueriesValues, onError(updateError) { toast({ title: updateError.message, @@ -133,9 +134,7 @@ export function UserListModal({ }) const [removeUser, { loading: _removeUserLoading }] = useMutation(REMOVE_USER_FROM_ORG, { - refetchQueries: ['FindMyUsers', 'FindAuditLogs'], - awaitRefetchQueries: true, - + ...refetchQueriesValues, onError(error) { toast({ title: t`An error occurred.`, @@ -253,11 +252,12 @@ export function UserListModal({ defaultValue={editingUserRole} onChange={handleChange} > - {(editingUserRole === 'USER' || + {editingUserRole === 'PENDING' && } + {(['PENDING', 'USER'].includes(editingUserRole) || (permission === 'SUPER_ADMIN' && editingUserRole === 'ADMIN')) && ( )} - {(editingUserRole === 'USER' || editingUserRole === 'ADMIN') && ( + {['PENDING', 'USER', 'ADMIN'].includes(editingUserRole) && ( )} {(editingUserRole === 'SUPER_ADMIN' || 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( ', () => { ', () => { ', () => { + {userName} @@ -16,7 +12,7 @@ export function UserCard({ userName, displayName, role, ...props }) { {role && ( Sign up" #~ msgid "Dploy DKIM records and keys for all domains and senders; and" #~ msgstr "Dploy DKIM records and keys for all domains and senders; and" -#: src/organizationDetails/OrganizationDomains.js:178 +#: src/organizationDetails/OrganizationDomains.js:179 msgid "EQUALS" msgstr "EQUALS" @@ -1061,7 +1062,7 @@ msgstr "Email Sent" msgid "Email Validated" msgstr "Email Validated" -#: src/app/App.js:288 +#: src/app/App.js:296 msgid "Email Verification" msgstr "Email Verification" @@ -1071,7 +1072,7 @@ msgstr "Email Verification" msgid "Email cannot be empty" msgstr "Email cannot be empty" -#: src/admin/UserListModal.js:59 +#: src/admin/UserListModal.js:62 msgid "Email invitation sent" msgstr "Email invitation sent" @@ -1172,7 +1173,7 @@ msgstr "Export to CSV" #: src/dmarc/DmarcReportPage.js:129 #: src/dmarc/DmarcReportPage.js:130 -#: src/organizationDetails/OrganizationDomains.js:223 +#: src/organizationDetails/OrganizationDomains.js:211 msgid "Fail" msgstr "Fail" @@ -1221,7 +1222,7 @@ msgstr "February" #~ msgid "Filters" #~ msgstr "Filters" -#: src/organizationDetails/OrganizationDomains.js:138 +#: src/organizationDetails/OrganizationDomains.js:137 msgid "Filters:" msgstr "Filters:" @@ -1261,7 +1262,7 @@ msgstr "For details related to terms pertaining to privacy, please refer to" msgid "For users interested in using new features that are still in progress." msgstr "For users interested in using new features that are still in progress." -#: src/app/App.js:176 +#: src/app/App.js:180 #: src/auth/ForgotPasswordPage.js:75 msgid "Forgot Password" msgstr "Forgot Password" @@ -1302,7 +1303,7 @@ msgstr "Fully Aligned Table" msgid "Fully Aligned by IP Address" msgstr "Fully Aligned by IP Address" -#: src/organizations/Organizations.js:130 +#: src/organizations/Organizations.js:143 msgid "Further details for each organization can be found by clicking on its row." msgstr "Further details for each organization can be found by clicking on its row." @@ -1346,7 +1347,7 @@ msgstr "Government of Canada Employees" #~ msgid "Graph direction:" #~ msgstr "Graph direction:" -#: src/app/App.js:337 +#: src/app/App.js:345 #: src/dmarc/DmarcReportPage.js:196 #: src/dmarc/DmarcReportPage.js:690 msgid "Guidance" @@ -1369,9 +1370,9 @@ msgstr "Guidance results" msgid "HIDDEN" msgstr "HIDDEN" -#: src/domains/DomainCard.js:185 +#: src/domains/DomainCard.js:186 #: src/domains/DomainsPage.js:157 -#: src/organizationDetails/OrganizationDomains.js:282 +#: src/organizationDetails/OrganizationDomains.js:270 msgid "HSTS" msgstr "HSTS" @@ -1396,7 +1397,7 @@ msgid "HSTS Preloaded" msgstr "HSTS Preloaded" #: src/domains/DomainsPage.js:70 -#: src/organizationDetails/OrganizationDomains.js:82 +#: src/organizationDetails/OrganizationDomains.js:83 msgid "HSTS Status" msgstr "HSTS Status" @@ -1416,9 +1417,9 @@ msgstr "HTTP Live" msgid "HTTP Upgrades" msgstr "HTTP Upgrades" -#: src/domains/DomainCard.js:184 +#: src/domains/DomainCard.js:185 #: src/domains/DomainsPage.js:159 -#: src/organizationDetails/OrganizationDomains.js:284 +#: src/organizationDetails/OrganizationDomains.js:272 msgid "HTTPS" msgstr "HTTPS" @@ -1431,7 +1432,7 @@ msgid "HTTPS Configuration Summary" msgstr "HTTPS Configuration Summary" #: src/organizations/OrganizationCard.js:118 -#: src/organizations/Organizations.js:122 +#: src/organizations/Organizations.js:135 msgid "HTTPS Configured" msgstr "HTTPS Configured" @@ -1448,7 +1449,7 @@ msgid "HTTPS Scan Complete" msgstr "HTTPS Scan Complete" #: src/domains/DomainsPage.js:69 -#: src/organizationDetails/OrganizationDomains.js:81 +#: src/organizationDetails/OrganizationDomains.js:82 msgid "HTTPS Status" msgstr "HTTPS Status" @@ -1484,8 +1485,8 @@ msgstr "Heartbleed Vulnerable" #~ msgid "Help us make government websites more secure. Please complete the following steps to become compliant with the Government of Canada's web security standards. If you have any questions about this process, please <0>contact us." #~ msgstr "Help us make government websites more secure. Please complete the following steps to become compliant with the Government of Canada's web security standards. If you have any questions about this process, please <0>contact us." -#: src/admin/AdminDomainCard.js:44 -#: src/organizationDetails/OrganizationDomains.js:102 +#: src/admin/AdminDomainCard.js:68 +#: src/organizationDetails/OrganizationDomains.js:100 msgid "Hidden" msgstr "Hidden" @@ -1493,8 +1494,8 @@ msgstr "Hidden" msgid "Hide domain" msgstr "Hide domain" -#: src/app/App.js:77 -#: src/app/App.js:146 +#: src/app/App.js:78 +#: src/app/App.js:150 #: src/app/FloatingMenu.js:175 msgid "Home" msgstr "Home" @@ -1523,7 +1524,7 @@ msgstr "How can I edit my domain list?" msgid "I agree to all <0>Terms, Privacy Policy & Code of Conduct Guidelines <1/>" msgstr "I agree to all <0>Terms, Privacy Policy & Code of Conduct Guidelines <1/>" -#: src/organizationDetails/OrganizationDomains.js:98 +#: src/organizationDetails/OrganizationDomains.js:99 msgid "INACTIVE" msgstr "INACTIVE" @@ -1621,7 +1622,7 @@ msgstr "Implementation: <0>Implementation guidance: email domain protection (ITS msgid "Implemented" msgstr "Implemented" -#: src/organizationDetails/OrganizationDomains.js:98 +#: src/organizationDetails/OrganizationDomains.js:99 msgid "Inactive" msgstr "Inactive" @@ -1642,7 +1643,7 @@ msgstr "Incorrect createDomain.result typename." msgid "Incorrect createOrganization.result typename." msgstr "Incorrect createOrganization.result typename." -#: src/admin/UserListModal.js:78 +#: src/admin/UserListModal.js:81 msgid "Incorrect inviteUserToOrg.result typename." msgstr "Incorrect inviteUserToOrg.result typename." @@ -1667,8 +1668,8 @@ msgstr "Incorrect resetPassword.result typename." #: src/admin/AdminDomainModal.js:142 #: src/admin/AdminDomains.js:133 #: src/admin/SuperAdminUserList.js:110 -#: src/admin/UserListModal.js:77 -#: src/admin/UserListModal.js:124 +#: src/admin/UserListModal.js:80 +#: src/admin/UserListModal.js:125 #: src/auth/CreateUserPage.js:83 #: src/auth/ResetPasswordPage.js:60 #: src/auth/SignInPage.js:100 @@ -1726,7 +1727,7 @@ msgstr "Incorrect updateUserPassword.result typename." msgid "Incorrect updateUserProfile.result typename." msgstr "Incorrect updateUserProfile.result typename." -#: src/admin/UserListModal.js:125 +#: src/admin/UserListModal.js:126 msgid "Incorrect updateUserRole.result typename." msgstr "Incorrect updateUserRole.result typename." @@ -1750,7 +1751,7 @@ msgstr "Individuals from a departmental information technology group may contact #~ msgid "Individuals with questions about the accuracy of their domain’s compliance data may contact the TBS Cyber Security mailbox." #~ msgstr "Individuals with questions about the accuracy of their domain’s compliance data may contact the TBS Cyber Security mailbox." -#: src/organizationDetails/OrganizationDomains.js:220 +#: src/organizationDetails/OrganizationDomains.js:208 msgid "Info" msgstr "Info" @@ -1810,7 +1811,11 @@ msgstr "Internet-facing" msgid "Invalid email" msgstr "Invalid email" -#: src/admin/UserList.js:173 +#: src/organizations/RequestOrgInviteModal.js:36 +msgid "Invite Requested" +msgstr "Invite Requested" + +#: src/admin/UserList.js:147 msgid "Invite User" msgstr "Invite User" @@ -1988,14 +1993,14 @@ msgstr "Must Staple" #~ msgid "My Tracker" #~ msgstr "My Tracker" -#: src/organizationDetails/OrganizationDomains.js:93 +#: src/organizationDetails/OrganizationDomains.js:94 msgid "NEW" msgstr "NEW" #: src/admin/WebCheckPage.js:60 #: src/createOrganization/CreateOrganizationPage.js:173 #: src/createOrganization/CreateOrganizationPage.js:178 -#: src/organizations/Organizations.js:60 +#: src/organizations/Organizations.js:57 msgid "Name" msgstr "Name" @@ -2037,7 +2042,7 @@ msgstr "Negative" msgid "Never" msgstr "Never" -#: src/organizationDetails/OrganizationDomains.js:93 +#: src/organizationDetails/OrganizationDomains.js:94 msgid "New" msgstr "New" @@ -2102,7 +2107,7 @@ msgstr "No DMARC phase information available for this organization." #: src/admin/AdminDomains.js:156 #: src/domains/DomainsPage.js:89 -#: src/organizationDetails/OrganizationDomains.js:246 +#: src/organizationDetails/OrganizationDomains.js:235 msgid "No Domains" msgstr "No Domains" @@ -2111,7 +2116,7 @@ msgid "No HTTPS configuration information available for this organization." msgstr "No HTTPS configuration information available for this organization." #: src/admin/WebCheckPage.js:94 -#: src/organizations/Organizations.js:81 +#: src/organizations/Organizations.js:78 msgid "No Organizations" msgstr "No Organizations" @@ -2181,7 +2186,7 @@ msgid "No scan data is currently available for this service. You may request a s msgstr "No scan data is currently available for this service. You may request a scan using the refresh button, or wait up to 24 hours for data to refresh." #: src/admin/SuperAdminUserList.js:161 -#: src/admin/UserList.js:76 +#: src/admin/UserList.js:69 msgid "No users" msgstr "No users" @@ -2277,7 +2282,7 @@ msgstr "Options include contacting the <0>SSC WebSSL services team and/or us msgid "Organization" msgstr "Organization" -#: src/organizationDetails/OrganizationDetails.js:63 +#: src/organizationDetails/OrganizationDetails.js:67 msgid "Organization Details" msgstr "Organization Details" @@ -2286,7 +2291,7 @@ msgid "Organization Information" msgstr "Organization Information" #: src/admin/OrganizationInformation.js:509 -#: src/organizations/Organizations.js:114 +#: src/organizations/Organizations.js:130 msgid "Organization Name" msgstr "Organization Name" @@ -2318,15 +2323,19 @@ msgid "Organization:" msgstr "Organization:" #: src/admin/AdminPage.js:189 -#: src/app/App.js:83 -#: src/app/App.js:186 +#: src/app/App.js:84 +#: src/app/App.js:190 #: src/app/FloatingMenu.js:103 -#: src/organizations/Organizations.js:72 -#: src/organizations/Organizations.js:109 +#: src/organizations/Organizations.js:69 +#: src/organizations/Organizations.js:125 msgid "Organizations" msgstr "Organizations" -#: src/organizationDetails/OrganizationDomains.js:94 +#: src/admin/UserListModal.js:256 +msgid "PENDING" +msgstr "PENDING" + +#: src/organizationDetails/OrganizationDomains.js:95 msgid "PROD" msgstr "PROD" @@ -2336,7 +2345,7 @@ msgstr "Page {0} of {1}" #: src/dmarc/DmarcReportPage.js:120 #: src/dmarc/DmarcReportPage.js:121 -#: src/organizationDetails/OrganizationDomains.js:217 +#: src/organizationDetails/OrganizationDomains.js:205 msgid "Pass" msgstr "Pass" @@ -2489,7 +2498,7 @@ msgstr "Prevent this domain from being visible, scanned, and being counted in an #~ msgid "Previous" #~ msgstr "Previous" -#: src/app/App.js:321 +#: src/app/App.js:329 #: src/app/FloatingMenu.js:219 #: src/app/SlideMessage.js:88 #: src/termsConditions/TermsConditionsPage.js:41 @@ -2504,7 +2513,7 @@ msgstr "Privacy Act." msgid "Privacy Notice Statement" msgstr "Privacy Notice Statement" -#: src/organizationDetails/OrganizationDomains.js:94 +#: src/organizationDetails/OrganizationDomains.js:95 msgid "Prod" msgstr "Prod" @@ -2512,16 +2521,16 @@ msgstr "Prod" msgid "Protect domains that do not send email - GOV.UK (www.gov.uk)" msgstr "Protect domains that do not send email - GOV.UK (www.gov.uk)" -#: src/domains/DomainCard.js:187 +#: src/domains/DomainCard.js:188 #: src/domains/DomainsPage.js:162 #: src/guidance/WebTLSResults.js:52 -#: src/organizationDetails/OrganizationDomains.js:287 -#: src/organizationDetails/OrganizationDomains.js:326 +#: src/organizationDetails/OrganizationDomains.js:275 +#: src/organizationDetails/OrganizationDomains.js:314 msgid "Protocols" msgstr "Protocols" #: src/domains/DomainsPage.js:74 -#: src/organizationDetails/OrganizationDomains.js:86 +#: src/organizationDetails/OrganizationDomains.js:87 msgid "Protocols Status" msgstr "Protocols Status" @@ -2562,7 +2571,7 @@ msgstr "ROBOT Vulnerable" #~ msgid "Read Guidance" #~ msgstr "Read Guidance" -#: src/app/App.js:184 +#: src/app/App.js:188 msgid "Read guidance" msgstr "Read guidance" @@ -2623,12 +2632,17 @@ msgstr "Remove User" msgid "Removed Organization" msgstr "Removed Organization" -#: src/app/App.js:329 +#: src/app/App.js:337 #: src/app/FloatingMenu.js:230 #: src/app/SlideMessage.js:99 msgid "Report an Issue" msgstr "Report an Issue" +#: src/organizationDetails/OrganizationDetails.js:112 +#: src/organizations/RequestOrgInviteModal.js:62 +msgid "Request Invite" +msgstr "Request Invite" + #: src/domains/ScanDomain.js:167 msgid "Request a domain to be scanned:" msgstr "Request a domain to be scanned:" @@ -2653,7 +2667,7 @@ msgstr "Requirements: <0>Email Management Services Configuration RequirementsWeb Sites and Services Management Configuration Requirements" msgstr "Requirements: <0>Web Sites and Services Management Configuration Requirements" -#: src/app/App.js:178 +#: src/app/App.js:182 msgid "Reset Password" msgstr "Reset Password" @@ -2696,7 +2710,7 @@ msgstr "Results for scans of web technologies (TLS, HTTPS)." msgid "Revoked:" msgstr "Revoked:" -#: src/admin/UserListModal.js:105 +#: src/admin/UserListModal.js:106 msgid "Role updated" msgstr "Role updated" @@ -2714,7 +2728,7 @@ msgid "SAN List:" msgstr "SAN List:" #: src/domains/DomainsPage.js:163 -#: src/organizationDetails/OrganizationDomains.js:288 +#: src/organizationDetails/OrganizationDomains.js:276 msgid "SPF" msgstr "SPF" @@ -2741,7 +2755,7 @@ msgid "SPF Results" msgstr "SPF Results" #: src/domains/DomainsPage.js:75 -#: src/organizationDetails/OrganizationDomains.js:87 +#: src/organizationDetails/OrganizationDomains.js:88 msgid "SPF Status" msgstr "SPF Status" @@ -2762,11 +2776,11 @@ msgstr "SPF Status" #~ msgid "SSL scan for domain \"{0}\" has completed." #~ msgstr "SSL scan for domain \"{0}\" has completed." -#: src/organizationDetails/OrganizationDomains.js:95 +#: src/organizationDetails/OrganizationDomains.js:96 msgid "STAGING" msgstr "STAGING" -#: src/admin/UserListModal.js:265 +#: src/admin/UserListModal.js:266 msgid "SUPER_ADMIN" msgstr "SUPER_ADMIN" @@ -2783,7 +2797,7 @@ msgstr "Save" msgid "Scan Domain" msgstr "Scan Domain" -#: src/domains/DomainCard.js:141 +#: src/domains/DomainCard.js:143 #: src/guidance/GuidancePage.js:140 msgid "Scan Pending" msgstr "Scan Pending" @@ -2823,7 +2837,7 @@ msgstr "Search by initiated by, resource name" #: src/dmarc/DmarcByDomainPage.js:221 #: src/dmarc/DmarcByDomainPage.js:292 #: src/domains/DomainsPage.js:189 -#: src/organizationDetails/OrganizationDomains.js:313 +#: src/organizationDetails/OrganizationDomains.js:301 msgid "Search for a domain" msgstr "Search for a domain" @@ -2839,12 +2853,12 @@ msgstr "Search for a user (email)" #~ msgid "Search for an activity" #~ msgstr "Search for an activity" -#: src/organizations/Organizations.js:151 +#: src/organizations/Organizations.js:161 msgid "Search for an organization" msgstr "Search for an organization" #: src/admin/AdminDomains.js:252 -#: src/admin/UserList.js:149 +#: src/admin/UserList.js:131 #: src/components/ReactTableGlobalFilter.js:36 #: src/components/SearchBox.js:44 msgid "Search:" @@ -2904,8 +2918,8 @@ msgstr "September" msgid "Serial:" msgstr "Serial:" -#: src/organizations/Organizations.js:62 -#: src/organizations/Organizations.js:118 +#: src/organizations/Organizations.js:59 +#: src/organizations/Organizations.js:133 msgid "Services" msgstr "Services" @@ -2978,42 +2992,42 @@ msgstr "Shows if the domain has a valid SSL certificate." #~ msgstr "Shows if the domain is policy compliant." #: src/domains/DomainsPage.js:166 -#: src/organizationDetails/OrganizationDomains.js:291 +#: src/organizationDetails/OrganizationDomains.js:279 msgid "Shows if the domain meets the DomainKeys Identified Mail (DKIM) requirements." msgstr "Shows if the domain meets the DomainKeys Identified Mail (DKIM) requirements." #: src/domains/DomainsPage.js:157 -#: src/organizationDetails/OrganizationDomains.js:282 +#: src/organizationDetails/OrganizationDomains.js:270 msgid "Shows if the domain meets the HSTS requirements." msgstr "Shows if the domain meets the HSTS requirements." #: src/domains/DomainsPage.js:160 -#: src/organizationDetails/OrganizationDomains.js:285 +#: src/organizationDetails/OrganizationDomains.js:273 msgid "Shows if the domain meets the Hypertext Transfer Protocol Secure (HTTPS) requirements." msgstr "Shows if the domain meets the Hypertext Transfer Protocol Secure (HTTPS) requirements." #: src/domains/DomainsPage.js:170 -#: src/organizationDetails/OrganizationDomains.js:295 +#: src/organizationDetails/OrganizationDomains.js:283 msgid "Shows if the domain meets the Message Authentication, Reporting, and Conformance (DMARC) requirements." msgstr "Shows if the domain meets the Message Authentication, Reporting, and Conformance (DMARC) requirements." #: src/domains/DomainsPage.js:163 -#: src/organizationDetails/OrganizationDomains.js:288 +#: src/organizationDetails/OrganizationDomains.js:276 msgid "Shows if the domain meets the Sender Policy Framework (SPF) requirements." msgstr "Shows if the domain meets the Sender Policy Framework (SPF) requirements." #: src/domains/DomainsPage.js:162 -#: src/organizationDetails/OrganizationDomains.js:287 +#: src/organizationDetails/OrganizationDomains.js:275 msgid "Shows if the domain uses acceptable protocols." msgstr "Shows if the domain uses acceptable protocols." #: src/domains/DomainsPage.js:155 -#: src/organizationDetails/OrganizationDomains.js:280 +#: src/organizationDetails/OrganizationDomains.js:268 msgid "Shows if the domain uses only ciphers that are strong or acceptable." msgstr "Shows if the domain uses only ciphers that are strong or acceptable." #: src/domains/DomainsPage.js:156 -#: src/organizationDetails/OrganizationDomains.js:281 +#: src/organizationDetails/OrganizationDomains.js:269 msgid "Shows if the domain uses only curves that are strong or acceptable." msgstr "Shows if the domain uses only curves that are strong or acceptable." @@ -3049,11 +3063,11 @@ msgstr "Shows if the server was found to be vulnerable to the ROBOT vulnerabilit msgid "Shows the duration of time, in seconds, that the HSTS header is valid." msgstr "Shows the duration of time, in seconds, that the HSTS header is valid." -#: src/organizations/Organizations.js:119 +#: src/organizations/Organizations.js:133 msgid "Shows the number of domains that the organization is in control of." msgstr "Shows the number of domains that the organization is in control of." -#: src/organizations/Organizations.js:123 +#: src/organizations/Organizations.js:136 msgid "Shows the percentage of domains which have HTTPS configured and upgrade HTTP connections to HTTPS" msgstr "Shows the percentage of domains which have HTTPS configured and upgrade HTTP connections to HTTPS" @@ -3061,7 +3075,7 @@ msgstr "Shows the percentage of domains which have HTTPS configured and upgrade #~ msgid "Shows the percentage of domains which have HTTPS configured and upgrade HTTP connections to HTTPS (ITPIN 6.1.1)" #~ msgstr "Shows the percentage of domains which have HTTPS configured and upgrade HTTP connections to HTTPS (ITPIN 6.1.1)" -#: src/organizations/Organizations.js:127 +#: src/organizations/Organizations.js:140 msgid "Shows the percentage of domains which have a valid DMARC policy configuration." msgstr "Shows the percentage of domains which have a valid DMARC policy configuration." @@ -3089,7 +3103,7 @@ msgstr "Shows the total number of emails that have been sent by this domain duri #~ msgid "Siganture Hash:" #~ msgstr "Siganture Hash:" -#: src/app/App.js:156 +#: src/app/App.js:160 #: src/app/FloatingMenu.js:197 #: src/app/TopBanner.js:118 #: src/auth/SignInPage.js:189 @@ -3119,7 +3133,7 @@ msgstr "Sign Out." msgid "Signature Hash:" msgstr "Signature Hash:" -#: src/app/App.js:71 +#: src/app/App.js:72 msgid "Skip to main content" msgstr "Skip to main content" @@ -3135,7 +3149,7 @@ msgstr "Sort by:" msgid "Source IP Address" msgstr "Source IP Address" -#: src/organizationDetails/OrganizationDomains.js:95 +#: src/organizationDetails/OrganizationDomains.js:96 msgid "Staging" msgstr "Staging" @@ -3143,7 +3157,7 @@ msgstr "Staging" #~ msgid "Status" #~ msgstr "Status" -#: src/organizationDetails/OrganizationDomains.js:191 +#: src/organizationDetails/OrganizationDomains.js:192 msgid "Status or tag" msgstr "Status or tag" @@ -3172,11 +3186,11 @@ msgstr "Subject:" msgid "Submit" msgstr "Submit" -#: src/admin/UserListModal.js:154 +#: src/admin/UserListModal.js:153 msgid "Successfully removed user {0}." msgstr "Successfully removed user {0}." -#: src/organizationDetails/OrganizationDetails.js:133 +#: src/organizationDetails/OrganizationDetails.js:132 #: src/user/MyTrackerPage.js:93 msgid "Summary" msgstr "Summary" @@ -3209,7 +3223,7 @@ msgstr "TBS be identified as the source; and" msgid "TBS reserves the right to refuse service, and may reject your application for an account, or cancel an existing account, for any reason, at our sole discretion." msgstr "TBS reserves the right to refuse service, and may reject your application for an account, or cancel an existing account, for any reason, at our sole discretion." -#: src/organizationDetails/OrganizationDomains.js:96 +#: src/organizationDetails/OrganizationDomains.js:97 msgid "TEST" msgstr "TEST" @@ -3241,11 +3255,11 @@ msgstr "Technical implementation guidance:" msgid "Termination" msgstr "Termination" -#: src/app/App.js:180 +#: src/app/App.js:184 msgid "Terms & Conditions" msgstr "Terms & Conditions" -#: src/app/App.js:325 +#: src/app/App.js:333 #: src/app/FloatingMenu.js:225 #: src/app/SlideMessage.js:92 msgid "Terms & conditions" @@ -3259,7 +3273,7 @@ msgstr "Terms and Conditions" msgid "Terms of Use" msgstr "Terms of Use" -#: src/organizationDetails/OrganizationDomains.js:96 +#: src/organizationDetails/OrganizationDomains.js:97 msgid "Test" msgstr "Test" @@ -3293,7 +3307,7 @@ msgstr "The advice, guidance or services provided to you by TBS will be provided #: src/dmarc/DmarcByDomainPage.js:324 #: src/domains/DomainsPage.js:154 -#: src/organizationDetails/OrganizationDomains.js:278 +#: src/organizationDetails/OrganizationDomains.js:267 msgid "The domain address." msgstr "The domain address." @@ -3341,7 +3355,7 @@ msgstr "The results of DKIM verification of the message. Can be pass, fail, neut msgid "The summary cards show two metrics that Tracker scans:" msgstr "The summary cards show two metrics that Tracker scans:" -#: src/admin/UserListModal.js:106 +#: src/admin/UserListModal.js:107 msgid "The user's role has been successfully updated" msgstr "The user's role has been successfully updated" @@ -3411,11 +3425,11 @@ msgstr "Time Generated (UTC)" #~ msgid "Timestamp" #~ msgstr "Timestamp" -#: src/app/App.js:118 +#: src/app/App.js:122 msgid "To enable full app functionality and maximize your account's security, <0>please verify your account." msgstr "To enable full app functionality and maximize your account's security, <0>please verify your account." -#: src/app/App.js:132 +#: src/app/App.js:136 msgid "To maximize your account's security, <0>please activate a multi-factor authentication option." msgstr "To maximize your account's security, <0>please activate a multi-factor authentication option." @@ -3492,11 +3506,11 @@ msgstr "Two-Factor Authentication:" msgid "URL:" msgstr "URL:" -#: src/admin/UserListModal.js:258 +#: src/admin/UserListModal.js:259 msgid "USER" msgstr "USER" -#: src/admin/UserListModal.js:95 +#: src/admin/UserListModal.js:96 msgid "Unable to change user role, please try again." msgstr "Unable to change user role, please try again." @@ -3525,7 +3539,7 @@ msgstr "Unable to create new organization." msgid "Unable to create your account, please try again." msgstr "Unable to create your account, please try again." -#: src/admin/UserListModal.js:68 +#: src/admin/UserListModal.js:71 msgid "Unable to invite user." msgstr "Unable to invite user." @@ -3542,10 +3556,15 @@ msgstr "Unable to remove domain." msgid "Unable to remove this organization." msgstr "Unable to remove this organization." -#: src/admin/UserListModal.js:162 +#: src/admin/UserListModal.js:161 msgid "Unable to remove user." msgstr "Unable to remove user." +#: src/organizations/RequestOrgInviteModal.js:26 +#: src/organizations/RequestOrgInviteModal.js:46 +msgid "Unable to request invite, please try again." +msgstr "Unable to request invite, please try again." + #: src/domains/ScanDomain.js:44 msgid "Unable to request scan, please try again." msgstr "Unable to request scan, please try again." @@ -3601,7 +3620,7 @@ msgstr "Unable to update to your preferred language, please try again." msgid "Unable to update to your username, please try again." msgstr "Unable to update to your username, please try again." -#: src/admin/UserListModal.js:115 +#: src/admin/UserListModal.js:116 msgid "Unable to update user role." msgstr "Unable to update user role." @@ -3621,7 +3640,7 @@ msgstr "Unable to verify your phone number, please try again." msgid "Understanding Scan Metrics:" msgstr "Understanding Scan Metrics:" -#: src/domains/DomainCard.js:83 +#: src/domains/DomainCard.js:85 msgid "Unfavourited Domain" msgstr "Unfavourited Domain" @@ -3690,7 +3709,7 @@ msgid "User Email" msgstr "User Email" #: src/admin/SuperAdminUserList.js:157 -#: src/admin/UserList.js:72 +#: src/admin/UserList.js:65 msgid "User List" msgstr "User List" @@ -3698,11 +3717,11 @@ msgstr "User List" msgid "User email does not match" msgstr "User email does not match" -#: src/admin/UserListModal.js:58 +#: src/admin/UserListModal.js:61 msgid "User invited" msgstr "User invited" -#: src/admin/UserListModal.js:153 +#: src/admin/UserListModal.js:152 msgid "User removed." msgstr "User removed." @@ -3711,8 +3730,8 @@ msgid "User:" msgstr "User:" #: src/admin/AdminPage.js:190 -#: src/admin/AdminPanel.js:29 -#: src/organizationDetails/OrganizationDetails.js:143 +#: src/admin/AdminPanel.js:24 +#: src/organizationDetails/OrganizationDetails.js:142 msgid "Users" msgstr "Users" @@ -3720,7 +3739,7 @@ msgstr "Users" msgid "Users exercise due diligence in ensuring the accuracy of the materials reproduced;" msgstr "Users exercise due diligence in ensuring the accuracy of the materials reproduced;" -#: src/organizationDetails/OrganizationDomains.js:155 +#: src/organizationDetails/OrganizationDomains.js:154 msgid "Value" msgstr "Value" @@ -3730,7 +3749,7 @@ msgstr "Verification code must only contains numbers" #: src/admin/SuperAdminUserList.js:150 #: src/admin/SuperAdminUserList.js:341 -#: src/organizations/Organizations.js:63 +#: src/organizations/Organizations.js:60 msgid "Verified" msgstr "Verified" @@ -3774,7 +3793,7 @@ msgstr "Volume of messages spoofing domain (reject + quarantine + none):" msgid "Vulnerability Scan Dashboard" msgstr "Vulnerability Scan Dashboard" -#: src/organizationDetails/OrganizationDomains.js:97 +#: src/organizationDetails/OrganizationDomains.js:98 msgid "WEB" msgstr "WEB" @@ -3810,7 +3829,7 @@ msgstr "We've sent you an email with an authentication code to sign into Tracker #~ msgid "Weak Curves:" #~ msgstr "Weak Curves:" -#: src/organizationDetails/OrganizationDomains.js:97 +#: src/organizationDetails/OrganizationDomains.js:98 msgid "Web" msgstr "Web" @@ -3904,6 +3923,10 @@ msgstr "Why does the guidance page not show the domain’s DKIM selectors even t msgid "Wiki" msgstr "Wiki" +#: src/organizations/RequestOrgInviteModal.js:66 +msgid "Would you like to request an invite to {orgName}?" +msgstr "Would you like to request an invite to {orgName}?" + #: src/guidance/WebConnectionResults.js:126 #: src/guidance/WebConnectionResults.js:166 #: src/guidance/WebConnectionResults.js:188 @@ -4005,7 +4028,7 @@ msgstr "You may now sign in with your new password" msgid "You will need a Tracker account to use certain products and services. You are responsible for maintaining the confidentiality of your account, password and for restricting access to your account. You also agree to accept responsibility for all activities that occur under your account or password. TBS accepts no liability for any loss or damage arising from your failure to maintain the security of your account or password." msgstr "You will need a Tracker account to use certain products and services. You are responsible for maintaining the confidentiality of your account, password and for restricting access to your account. You also agree to accept responsibility for all activities that occur under your account or password. TBS accepts no liability for any loss or damage arising from your failure to maintain the security of your account or password." -#: src/app/App.js:262 +#: src/app/App.js:266 msgid "Your Account" msgstr "Your Account" @@ -4021,6 +4044,10 @@ msgstr "Your account email was successfully verified" msgid "Your account will be fully activated the next time you log in" msgstr "Your account will be fully activated the next time you log in" +#: src/organizations/RequestOrgInviteModal.js:37 +msgid "Your request has been sent to the organization administrators." +msgstr "Your request has been sent to the organization administrators." + #: src/admin/OrganizationInformation.js:421 msgid "Zone:" msgstr "Zone:" @@ -4041,8 +4068,8 @@ msgstr "contact us" #~ msgid "https://https-everywhere.canada.ca/en/help/" #~ msgstr "https://https-everywhere.canada.ca/en/help/" -#: src/app/App.js:97 -#: src/app/App.js:275 +#: src/app/App.js:100 +#: src/app/App.js:279 #: src/user/MyTrackerPage.js:43 #: src/user/MyTrackerPage.js:74 msgid "myTracker" @@ -4076,7 +4103,7 @@ msgstr "sp:" msgid "strong" msgstr "strong" -#: src/admin/UserList.js:162 +#: src/admin/UserList.js:140 msgid "user email" msgstr "user email" diff --git a/frontend/src/locales/fr.po b/frontend/src/locales/fr.po index 20c9d5dda7..141e371a9c 100644 --- a/frontend/src/locales/fr.po +++ b/frontend/src/locales/fr.po @@ -77,7 +77,7 @@ msgstr "Une ventilation plus détaillée de chaque domaine peut être trouvée e msgid "A verification link has been sent to your email account" msgstr "Un lien de vérification a été envoyé à votre compte de messagerie." -#: src/admin/UserListModal.js:261 +#: src/admin/UserListModal.js:262 msgid "ADMIN" msgstr "ADMIN" @@ -110,7 +110,7 @@ msgstr "Compte" msgid "Account Closed Successfully" msgstr "Compte clôturé avec succès" -#: src/app/App.js:101 +#: src/app/App.js:105 #: src/app/FloatingMenu.js:177 #: src/user/UserPage.js:150 msgid "Account Settings" @@ -123,7 +123,7 @@ msgstr "Compte créé" #: src/admin/WebCheckPage.js:61 #: src/createOrganization/CreateOrganizationPage.js:184 #: src/createOrganization/CreateOrganizationPage.js:189 -#: src/organizations/Organizations.js:61 +#: src/organizations/Organizations.js:58 msgid "Acronym" msgstr "Acronyme" @@ -155,7 +155,7 @@ msgstr "Action" msgid "Action:" msgstr "Action :" -#: src/admin/AdminPanel.js:32 +#: src/admin/AdminPanel.js:29 msgid "Activity" msgstr "Activité" @@ -175,7 +175,7 @@ msgstr "Ajouter les détails du domaine" msgid "Add User" msgstr "Ajouter un utilisateur" -#: src/app/App.js:207 +#: src/app/App.js:211 msgid "Admin" msgstr "Administrateur" @@ -183,7 +183,7 @@ msgstr "Administrateur" msgid "Admin Portal" msgstr "Portail Admin" -#: src/app/App.js:109 +#: src/app/App.js:113 msgid "Admin Profile" msgstr "Profil de l'administrateur" @@ -283,8 +283,8 @@ msgstr "Une erreur s'est produite lors de la mise à jour de votre numéro de t #: src/admin/AdminDomainModal.js:63 #: src/admin/AdminDomainModal.js:110 #: src/admin/AdminDomains.js:103 -#: src/admin/UserListModal.js:47 -#: src/admin/UserListModal.js:141 +#: src/admin/UserListModal.js:50 +#: src/admin/UserListModal.js:140 #: src/auth/TwoFactorAuthenticatePage.js:29 #: src/createOrganization/CreateOrganizationPage.js:56 #: src/user/UserPage.js:83 @@ -307,7 +307,7 @@ msgstr "Tous les produits ou services connexes qui vous sont fournis par le SCT #~ msgid "Application Portfolio Management (APM) systems; and" #~ msgstr "les systèmes de gestion du portefeuille d’applications (GPA);" -#: src/organizationDetails/OrganizationDomains.js:233 +#: src/organizationDetails/OrganizationDomains.js:221 msgid "Apply" msgstr "Appliquer" @@ -320,8 +320,8 @@ msgstr "Avril" msgid "Archive domain" msgstr "Archiver ce domaine" -#: src/admin/AdminDomainCard.js:51 -#: src/organizationDetails/OrganizationDomains.js:103 +#: src/admin/AdminDomainCard.js:80 +#: src/organizationDetails/OrganizationDomains.js:101 msgid "Archived" msgstr "Archivé" @@ -352,7 +352,7 @@ msgstr "Journaux d'audit" msgid "August" msgstr "Août" -#: src/app/App.js:173 +#: src/app/App.js:177 msgid "Authenticate" msgstr "Authentifier" @@ -430,9 +430,9 @@ msgid "Certificates" msgstr "Certificats" #: src/domains/DomainsPage.js:71 -#: src/organizationDetails/OrganizationDomains.js:83 +#: src/organizationDetails/OrganizationDomains.js:84 msgid "Certificates Status" -msgstr "" +msgstr "Statut des certificats" #: src/auth/ResetPasswordPage.js:126 #: src/user/EditableUserPassword.js:153 @@ -475,15 +475,15 @@ msgstr "Changements requis pour la mise en conformité ITPIN" msgid "Check your associated Tracker email for the verification link" msgstr "Vérifiez le lien de vérification dans votre courriel de suivi associé." -#: src/domains/DomainCard.js:188 +#: src/domains/DomainCard.js:189 #: src/domains/DomainsPage.js:155 #: src/guidance/WebTLSResults.js:101 -#: src/organizationDetails/OrganizationDomains.js:280 +#: src/organizationDetails/OrganizationDomains.js:268 msgid "Ciphers" msgstr "Ciphers" #: src/domains/DomainsPage.js:72 -#: src/organizationDetails/OrganizationDomains.js:84 +#: src/organizationDetails/OrganizationDomains.js:85 msgid "Ciphers Status" msgstr "État du chiffrement" @@ -529,7 +529,7 @@ msgstr "Le champ de code ne doit pas être vide" msgid "Collect and analyze DMARC reports." msgstr "Recueillir et analyser les rapports DMARC." -#: src/organizationDetails/OrganizationDomains.js:175 +#: src/organizationDetails/OrganizationDomains.js:176 msgid "Comparison" msgstr "Comparaison" @@ -542,7 +542,8 @@ msgstr "Conforme" #: src/admin/OrganizationInformation.js:393 #: src/admin/OrganizationInformation.js:520 #: src/admin/SuperAdminUserList.js:441 -#: src/admin/UserListModal.js:274 +#: src/admin/UserListModal.js:275 +#: src/organizations/RequestOrgInviteModal.js:75 #: src/user/EditableUserDisplayName.js:168 #: src/user/EditableUserEmail.js:168 #: src/user/EditableUserPassword.js:182 @@ -580,8 +581,8 @@ msgstr "Envisagez de donner la priorité aux sites web et aux services web qui msgid "Contact" msgstr "Contact" -#: src/app/App.js:182 -#: src/app/App.js:333 +#: src/app/App.js:186 +#: src/app/App.js:341 #: src/app/ContactUsPage.js:39 #: src/app/SlideMessage.js:103 msgid "Contact Us" @@ -633,12 +634,12 @@ msgid "Create Account" msgstr "Créer un compte" #: src/admin/AdminPage.js:130 -#: src/app/App.js:292 +#: src/app/App.js:300 #: src/createOrganization/CreateOrganizationPage.js:237 msgid "Create Organization" msgstr "Créer une organisation" -#: src/app/App.js:150 +#: src/app/App.js:154 msgid "Create an Account" msgstr "Créer un compte" @@ -666,21 +667,21 @@ msgstr "Mot de passe actuel:" msgid "Current Phone Number:" msgstr "Numéro de téléphone actuel:" -#: src/domains/DomainCard.js:189 +#: src/domains/DomainCard.js:190 #: src/domains/DomainsPage.js:156 #: src/guidance/WebTLSResults.js:155 -#: src/organizationDetails/OrganizationDomains.js:281 -#: src/organizationDetails/OrganizationDomains.js:325 +#: src/organizationDetails/OrganizationDomains.js:269 +#: src/organizationDetails/OrganizationDomains.js:313 msgid "Curves" msgstr "Courbes" #: src/domains/DomainsPage.js:73 -#: src/organizationDetails/OrganizationDomains.js:85 +#: src/organizationDetails/OrganizationDomains.js:86 msgid "Curves Status" msgstr "État des courbes" #: src/domains/DomainsPage.js:165 -#: src/organizationDetails/OrganizationDomains.js:290 +#: src/organizationDetails/OrganizationDomains.js:278 msgid "DKIM" msgstr "DKIM" @@ -719,7 +720,7 @@ msgid "DKIM Selectors:" msgstr "Sélecteurs DKIM:" #: src/domains/DomainsPage.js:76 -#: src/organizationDetails/OrganizationDomains.js:88 +#: src/organizationDetails/OrganizationDomains.js:89 msgid "DKIM Status" msgstr "Statut DKIM" @@ -728,11 +729,11 @@ msgstr "Statut DKIM" #~ msgstr "Un enregistrement DKIM n'a pas pu être trouvé pour ce sélecteur." #: src/domains/DomainsPage.js:169 -#: src/organizationDetails/OrganizationDomains.js:294 +#: src/organizationDetails/OrganizationDomains.js:282 msgid "DMARC" msgstr "DMARC" -#: src/organizations/Organizations.js:126 +#: src/organizations/Organizations.js:139 msgid "DMARC Configuration" msgstr "Configuration de DMARC" @@ -759,13 +760,13 @@ msgstr "Défaillances du DMARC par adresse IP" msgid "DMARC Implementation Phase: {0}" msgstr "Phase de mise en œuvre de DMARC: {0}" -#: src/organizationDetails/OrganizationDetails.js:136 +#: src/organizationDetails/OrganizationDetails.js:135 #: src/user/MyTrackerPage.js:96 msgid "DMARC Phases" msgstr "Phases DMARC" #: src/dmarc/DmarcReportPage.js:95 -#: src/domains/DomainCard.js:232 +#: src/domains/DomainCard.js:233 #: src/guidance/GuidancePage.js:152 msgid "DMARC Report" msgstr "Rapport DMARC " @@ -775,12 +776,12 @@ msgid "DMARC Report for {domainSlug}" msgstr "Rapport DMARC pour {domainSlug}" #: src/domains/DomainsPage.js:77 -#: src/organizationDetails/OrganizationDomains.js:89 +#: src/organizationDetails/OrganizationDomains.js:90 msgid "DMARC Status" msgstr "Statut DMARC" -#: src/app/App.js:89 -#: src/app/App.js:254 +#: src/app/App.js:90 +#: src/app/App.js:258 #: src/app/FloatingMenu.js:131 #: src/dmarc/DmarcByDomainPage.js:181 #: src/dmarc/DmarcByDomainPage.js:241 @@ -812,7 +813,7 @@ msgstr "Scan DNS terminé" msgid "DNS scan for domain \"{0}\" has completed." msgstr "Le scan DNS du domaine \"{0}\" est terminé." -#: src/organizationDetails/OrganizationDomains.js:181 +#: src/organizationDetails/OrganizationDomains.js:182 msgid "DOES NOT EQUAL" msgstr "N'EST PAS ÉGAL" @@ -890,7 +891,7 @@ msgstr "Nom d'affichage:" msgid "Display name cannot be empty" msgstr "Le nom d'affichage ne peut pas être vide" -#: src/organizations/Organizations.js:115 +#: src/organizations/Organizations.js:131 msgid "Displays the Name of the organization, its acronym, and a blue check mark if it is a verified organization." msgstr "Affiche le nom de l'organisation, son acronyme et une coche bleue s'il s'agit d'une organisation vérifiée." @@ -903,8 +904,8 @@ msgstr "Disposition" #: src/dmarc/DmarcByDomainPage.js:324 #: src/domains/DomainsPage.js:68 #: src/domains/DomainsPage.js:154 -#: src/organizationDetails/OrganizationDomains.js:278 -#: src/organizationDetails/OrganizationDomains.js:312 +#: src/organizationDetails/OrganizationDomains.js:267 +#: src/organizationDetails/OrganizationDomains.js:300 msgid "Domain" msgstr "Domaine" @@ -949,21 +950,21 @@ msgstr "Domaine mis à jour" msgid "Domain url field must not be empty" msgstr "Le champ de l'url du domaine ne doit pas être vide" -#: src/admin/AdminDomainCard.js:16 +#: src/admin/AdminDomainCard.js:29 #: src/admin/WebCheckPage.js:129 -#: src/domains/DomainCard.js:127 +#: src/domains/DomainCard.js:129 #: src/domains/ScanDomain.js:211 msgid "Domain:" msgstr "Domaine:" -#: src/admin/AdminPanel.js:26 -#: src/app/App.js:86 -#: src/app/App.js:220 +#: src/admin/AdminPanel.js:21 +#: src/app/App.js:87 +#: src/app/App.js:224 #: src/app/FloatingMenu.js:116 #: src/domains/DomainsPage.js:82 #: src/domains/DomainsPage.js:116 -#: src/organizationDetails/OrganizationDetails.js:139 -#: src/organizationDetails/OrganizationDomains.js:108 +#: src/organizationDetails/OrganizationDetails.js:138 +#: src/organizationDetails/OrganizationDomains.js:106 #: src/summaries/Doughnut.js:50 #: src/summaries/Doughnut.js:75 #: src/user/MyTrackerPage.js:99 @@ -982,7 +983,7 @@ msgstr "Domaines utilisés pour la validation SPF." msgid "Don't have an account? <0>Sign up" msgstr "Vous n'avez pas de compte ? <0>S'inscrire" -#: src/organizationDetails/OrganizationDomains.js:178 +#: src/organizationDetails/OrganizationDomains.js:179 msgid "EQUALS" msgstr "ÉGAUX" @@ -1053,7 +1054,7 @@ msgstr "Courriel envoyé" msgid "Email Validated" msgstr "Courriel validé" -#: src/app/App.js:288 +#: src/app/App.js:296 msgid "Email Verification" msgstr "Vérification de l'e-mail" @@ -1063,7 +1064,7 @@ msgstr "Vérification de l'e-mail" msgid "Email cannot be empty" msgstr "Le courriel ne peut être vide" -#: src/admin/UserListModal.js:59 +#: src/admin/UserListModal.js:62 msgid "Email invitation sent" msgstr "Envoi d'une invitation par courriel" @@ -1156,7 +1157,7 @@ msgstr "Exportation vers CSV" #: src/dmarc/DmarcReportPage.js:129 #: src/dmarc/DmarcReportPage.js:130 -#: src/organizationDetails/OrganizationDomains.js:223 +#: src/organizationDetails/OrganizationDomains.js:211 msgid "Fail" msgstr "Échec" @@ -1201,7 +1202,7 @@ msgstr "Février" #~ msgid "Filters" #~ msgstr "Filtres" -#: src/organizationDetails/OrganizationDomains.js:138 +#: src/organizationDetails/OrganizationDomains.js:137 msgid "Filters:" msgstr "Filtres :" @@ -1233,7 +1234,7 @@ msgstr "Pour plus de détails concernant les termes relatifs à la vie privée, msgid "For users interested in using new features that are still in progress." msgstr "Pour les utilisateurs intéressés par l'utilisation de nouvelles fonctionnalités qui sont encore en cours de développement." -#: src/app/App.js:176 +#: src/app/App.js:180 #: src/auth/ForgotPasswordPage.js:75 msgid "Forgot Password" msgstr "Mot de passe oublié" @@ -1274,7 +1275,7 @@ msgstr "Tableau entièrement aligné" msgid "Fully Aligned by IP Address" msgstr "Entièrement aligné par adresse IP" -#: src/organizations/Organizations.js:130 +#: src/organizations/Organizations.js:143 msgid "Further details for each organization can be found by clicking on its row." msgstr "Vous trouverez de plus amples informations sur chaque organisation en cliquant sur sa ligne." @@ -1318,7 +1319,7 @@ msgstr "Employés du gouvernement du Canada" #~ msgid "Graph direction:" #~ msgstr "Direction du graphique :" -#: src/app/App.js:337 +#: src/app/App.js:345 #: src/dmarc/DmarcReportPage.js:196 #: src/dmarc/DmarcReportPage.js:690 msgid "Guidance" @@ -1341,9 +1342,9 @@ msgstr "Résultats de l'orientation" msgid "HIDDEN" msgstr "CACHÉ" -#: src/domains/DomainCard.js:185 +#: src/domains/DomainCard.js:186 #: src/domains/DomainsPage.js:157 -#: src/organizationDetails/OrganizationDomains.js:282 +#: src/organizationDetails/OrganizationDomains.js:270 msgid "HSTS" msgstr "HSTS" @@ -1368,7 +1369,7 @@ msgid "HSTS Preloaded" msgstr "HSTS préchargé" #: src/domains/DomainsPage.js:70 -#: src/organizationDetails/OrganizationDomains.js:82 +#: src/organizationDetails/OrganizationDomains.js:83 msgid "HSTS Status" msgstr "Statut HSTS" @@ -1388,9 +1389,9 @@ msgstr "HTTP Live" msgid "HTTP Upgrades" msgstr "Mises à jour HTTP" -#: src/domains/DomainCard.js:184 +#: src/domains/DomainCard.js:185 #: src/domains/DomainsPage.js:159 -#: src/organizationDetails/OrganizationDomains.js:284 +#: src/organizationDetails/OrganizationDomains.js:272 msgid "HTTPS" msgstr "HTTPS" @@ -1403,7 +1404,7 @@ msgid "HTTPS Configuration Summary" msgstr "Résumé de la configuration HTTPS" #: src/organizations/OrganizationCard.js:118 -#: src/organizations/Organizations.js:122 +#: src/organizations/Organizations.js:135 msgid "HTTPS Configured" msgstr "HTTPS configuré" @@ -1420,7 +1421,7 @@ msgid "HTTPS Scan Complete" msgstr "Scan HTTPS terminé" #: src/domains/DomainsPage.js:69 -#: src/organizationDetails/OrganizationDomains.js:81 +#: src/organizationDetails/OrganizationDomains.js:82 msgid "HTTPS Status" msgstr "Statut HTTPS" @@ -1450,14 +1451,14 @@ msgstr "En-tête De" #: src/guidance/WebTLSResults.js:229 msgid "Heartbleed Vulnerable" -msgstr "" +msgstr "Vulnérabilité Heartbleed" #: src/app/ReadGuidancePage.js:23 #~ msgid "Help us make government websites more secure. Please complete the following steps to become compliant with the Government of Canada's web security standards. If you have any questions about this process, please <0>contact us." #~ msgstr "Aidez-nous à rendre les sites Web du gouvernement plus sûrs. Veuillez suivre les étapes suivantes pour vous conformer aux normes de sécurité Web du gouvernement du Canada. Si vous avez des questions sur ce processus, veuillez <0>nous contacter." -#: src/admin/AdminDomainCard.js:44 -#: src/organizationDetails/OrganizationDomains.js:102 +#: src/admin/AdminDomainCard.js:68 +#: src/organizationDetails/OrganizationDomains.js:100 msgid "Hidden" msgstr "Caché" @@ -1465,8 +1466,8 @@ msgstr "Caché" msgid "Hide domain" msgstr "Cacher ce domaine" -#: src/app/App.js:77 -#: src/app/App.js:146 +#: src/app/App.js:78 +#: src/app/App.js:150 #: src/app/FloatingMenu.js:175 msgid "Home" msgstr "Accueil" @@ -1495,7 +1496,7 @@ msgstr "Comment puis-je modifier ma liste de domaines?" msgid "I agree to all <0>Terms, Privacy Policy & Code of Conduct Guidelines <1/>" msgstr "J'accepte toutes les <0>Conditions générales, la politique de confidentialité et les directives du code de conduite<1/>." -#: src/organizationDetails/OrganizationDomains.js:98 +#: src/organizationDetails/OrganizationDomains.js:99 msgid "INACTIVE" msgstr "INACTIF" @@ -1593,7 +1594,7 @@ msgstr "Mise en œuvre : <0>Conseils de mise en œuvre : protection du domaine d msgid "Implemented" msgstr "Mis en œuvre" -#: src/organizationDetails/OrganizationDomains.js:98 +#: src/organizationDetails/OrganizationDomains.js:99 msgid "Inactive" msgstr "Inactif" @@ -1614,7 +1615,7 @@ msgstr "Incorrect createDomain.result typename." msgid "Incorrect createOrganization.result typename." msgstr "createOrganization.result incorrecte typename." -#: src/admin/UserListModal.js:78 +#: src/admin/UserListModal.js:81 msgid "Incorrect inviteUserToOrg.result typename." msgstr "Incorrect inviteUserToOrg.result typename." @@ -1639,8 +1640,8 @@ msgstr "Incorrect resetPassword.result typename." #: src/admin/AdminDomainModal.js:142 #: src/admin/AdminDomains.js:133 #: src/admin/SuperAdminUserList.js:110 -#: src/admin/UserListModal.js:77 -#: src/admin/UserListModal.js:124 +#: src/admin/UserListModal.js:80 +#: src/admin/UserListModal.js:125 #: src/auth/CreateUserPage.js:83 #: src/auth/ResetPasswordPage.js:60 #: src/auth/SignInPage.js:100 @@ -1698,7 +1699,7 @@ msgstr "Incorrect updateUserPassword.result typename." msgid "Incorrect updateUserProfile.result typename." msgstr "Incorrect updateUserProfile.result typename." -#: src/admin/UserListModal.js:125 +#: src/admin/UserListModal.js:126 msgid "Incorrect updateUserRole.result typename." msgstr "Incorrect updateUserRole.result typename." @@ -1722,7 +1723,7 @@ msgstr "Les personnes d'un groupe ministériel de technologie de l'information p #~ msgid "Individuals with questions about the accuracy of their domain’s compliance data may contact the TBS Cyber Security mailbox." #~ msgstr "Les personnes ayant des questions sur l'exactitude des données de conformité de leur domaine peuvent contacter la boîte aux lettres de la cybersécurité du SCT." -#: src/organizationDetails/OrganizationDomains.js:220 +#: src/organizationDetails/OrganizationDomains.js:208 msgid "Info" msgstr "Info" @@ -1782,7 +1783,11 @@ msgstr "orientés vers l'Internet" msgid "Invalid email" msgstr "Courriel non valide" -#: src/admin/UserList.js:173 +#: src/organizations/RequestOrgInviteModal.js:36 +msgid "Invite Requested" +msgstr "Invitation demandée" + +#: src/admin/UserList.js:147 msgid "Invite User" msgstr "Inviter l'utilisateur" @@ -1956,14 +1961,14 @@ msgstr "Générateur de configuration SSL de Mozilla" msgid "Must Staple" msgstr "Agrafe obligatoire" -#: src/organizationDetails/OrganizationDomains.js:93 +#: src/organizationDetails/OrganizationDomains.js:94 msgid "NEW" msgstr "NOUVEAU" #: src/admin/WebCheckPage.js:60 #: src/createOrganization/CreateOrganizationPage.js:173 #: src/createOrganization/CreateOrganizationPage.js:178 -#: src/organizations/Organizations.js:60 +#: src/organizations/Organizations.js:57 msgid "Name" msgstr "Nom" @@ -2005,7 +2010,7 @@ msgstr "Négatif" msgid "Never" msgstr "Jamais" -#: src/organizationDetails/OrganizationDomains.js:93 +#: src/organizationDetails/OrganizationDomains.js:94 msgid "New" msgstr "Nouveau" @@ -2070,7 +2075,7 @@ msgstr "Aucune information sur la phase DMARC n'est disponible pour cette organi #: src/admin/AdminDomains.js:156 #: src/domains/DomainsPage.js:89 -#: src/organizationDetails/OrganizationDomains.js:246 +#: src/organizationDetails/OrganizationDomains.js:235 msgid "No Domains" msgstr "Aucun domaine" @@ -2079,7 +2084,7 @@ msgid "No HTTPS configuration information available for this organization." msgstr "Aucune information de configuration HTTPS disponible pour cette organisation." #: src/admin/WebCheckPage.js:94 -#: src/organizations/Organizations.js:81 +#: src/organizations/Organizations.js:78 msgid "No Organizations" msgstr "Aucune organisation" @@ -2149,7 +2154,7 @@ msgid "No scan data is currently available for this service. You may request a s msgstr "Aucune donnée de balayage n'est actuellement disponible pour ce service. Vous pouvez demander un scan en utilisant le bouton d'actualisation, ou attendre jusqu'à 24 heures pour que les données soient actualisées." #: src/admin/SuperAdminUserList.js:161 -#: src/admin/UserList.js:76 +#: src/admin/UserList.js:69 msgid "No users" msgstr "Aucun utilisateur" @@ -2245,7 +2250,7 @@ msgstr "Vous pouvez notamment communiquer avec l’<0>équipe responsable des se msgid "Organization" msgstr "Organisation" -#: src/organizationDetails/OrganizationDetails.js:63 +#: src/organizationDetails/OrganizationDetails.js:67 msgid "Organization Details" msgstr "Détails de l'organisation" @@ -2254,7 +2259,7 @@ msgid "Organization Information" msgstr "Informations sur l'organisation" #: src/admin/OrganizationInformation.js:509 -#: src/organizations/Organizations.js:114 +#: src/organizations/Organizations.js:130 msgid "Organization Name" msgstr "Nom de l'organisation" @@ -2286,15 +2291,19 @@ msgid "Organization:" msgstr "Organisation:" #: src/admin/AdminPage.js:189 -#: src/app/App.js:83 -#: src/app/App.js:186 +#: src/app/App.js:84 +#: src/app/App.js:190 #: src/app/FloatingMenu.js:103 -#: src/organizations/Organizations.js:72 -#: src/organizations/Organizations.js:109 +#: src/organizations/Organizations.js:69 +#: src/organizations/Organizations.js:125 msgid "Organizations" msgstr "Organisations" -#: src/organizationDetails/OrganizationDomains.js:94 +#: src/admin/UserListModal.js:256 +msgid "PENDING" +msgstr "EN ATTENTE" + +#: src/organizationDetails/OrganizationDomains.js:95 msgid "PROD" msgstr "PROD" @@ -2304,7 +2313,7 @@ msgstr "Page {0} de {1}" #: src/dmarc/DmarcReportPage.js:120 #: src/dmarc/DmarcReportPage.js:121 -#: src/organizationDetails/OrganizationDomains.js:217 +#: src/organizationDetails/OrganizationDomains.js:205 msgid "Pass" msgstr "Passez" @@ -2457,7 +2466,7 @@ msgstr "Empêchez ce domaine d'être visible, d'être scanné et d'être compté #~ msgid "Previous" #~ msgstr "Précédent" -#: src/app/App.js:321 +#: src/app/App.js:329 #: src/app/FloatingMenu.js:219 #: src/app/SlideMessage.js:88 #: src/termsConditions/TermsConditionsPage.js:41 @@ -2472,7 +2481,7 @@ msgstr "Loi sur la protection de la vie privée." msgid "Privacy Notice Statement" msgstr "Déclaration de confidentialité" -#: src/organizationDetails/OrganizationDomains.js:94 +#: src/organizationDetails/OrganizationDomains.js:95 msgid "Prod" msgstr "Prod" @@ -2480,16 +2489,16 @@ msgstr "Prod" msgid "Protect domains that do not send email - GOV.UK (www.gov.uk)" msgstr "Protéger les domaines qui n'envoient pas de courrier électronique - GOV.UK (www.gov.uk)" -#: src/domains/DomainCard.js:187 +#: src/domains/DomainCard.js:188 #: src/domains/DomainsPage.js:162 #: src/guidance/WebTLSResults.js:52 -#: src/organizationDetails/OrganizationDomains.js:287 -#: src/organizationDetails/OrganizationDomains.js:326 +#: src/organizationDetails/OrganizationDomains.js:275 +#: src/organizationDetails/OrganizationDomains.js:314 msgid "Protocols" msgstr "Protocoles" #: src/domains/DomainsPage.js:74 -#: src/organizationDetails/OrganizationDomains.js:86 +#: src/organizationDetails/OrganizationDomains.js:87 msgid "Protocols Status" msgstr "Statut des protocoles" @@ -2520,13 +2529,13 @@ msgstr "Province:" #: src/guidance/WebTLSResults.js:253 msgid "ROBOT Vulnerable" -msgstr "" +msgstr "ROBOT Vulnérable" #: src/app/ReadGuidancePage.js:259 #~ msgid "Read Guidance" #~ msgstr "Conseils de lecture" -#: src/app/App.js:184 +#: src/app/App.js:188 msgid "Read guidance" msgstr "Conseils de lecture" @@ -2587,12 +2596,17 @@ msgstr "Supprimer l'utilisateur" msgid "Removed Organization" msgstr "Organisation supprimée" -#: src/app/App.js:329 +#: src/app/App.js:337 #: src/app/FloatingMenu.js:230 #: src/app/SlideMessage.js:99 msgid "Report an Issue" msgstr "Signaler un problème" +#: src/organizationDetails/OrganizationDetails.js:112 +#: src/organizations/RequestOrgInviteModal.js:62 +msgid "Request Invite" +msgstr "Demande d'invitation" + #: src/domains/ScanDomain.js:167 msgid "Request a domain to be scanned:" msgstr "Demander qu'un domaine soit scanné:" @@ -2617,7 +2631,7 @@ msgstr "Exigences : <0>Configuration requise pour les services de gestion du cou msgid "Requirements: <0>Web Sites and Services Management Configuration Requirements" msgstr "Exigences : <0>Exigences de configuration de la gestion des sites et services web" -#: src/app/App.js:178 +#: src/app/App.js:182 msgid "Reset Password" msgstr "Réinitialiser le mot de passe" @@ -2660,7 +2674,7 @@ msgstr "Résultats pour les analyses des technologies web (TLS, HTTPS)." msgid "Revoked:" msgstr "Révoqué :" -#: src/admin/UserListModal.js:105 +#: src/admin/UserListModal.js:106 msgid "Role updated" msgstr "Rôle mis à jour" @@ -2678,7 +2692,7 @@ msgid "SAN List:" msgstr "Liste des SAN :" #: src/domains/DomainsPage.js:163 -#: src/organizationDetails/OrganizationDomains.js:288 +#: src/organizationDetails/OrganizationDomains.js:276 msgid "SPF" msgstr "SPF" @@ -2705,7 +2719,7 @@ msgid "SPF Results" msgstr "Résultats du SPF" #: src/domains/DomainsPage.js:75 -#: src/organizationDetails/OrganizationDomains.js:87 +#: src/organizationDetails/OrganizationDomains.js:88 msgid "SPF Status" msgstr "Statut SPF" @@ -2726,11 +2740,11 @@ msgstr "Statut SPF" #~ msgid "SSL scan for domain \"{0}\" has completed." #~ msgstr "Le scan SSL pour le domaine \"{0}\" est terminé." -#: src/organizationDetails/OrganizationDomains.js:95 +#: src/organizationDetails/OrganizationDomains.js:96 msgid "STAGING" msgstr "DEV" -#: src/admin/UserListModal.js:265 +#: src/admin/UserListModal.js:266 msgid "SUPER_ADMIN" msgstr "SUPER_ADMIN" @@ -2747,7 +2761,7 @@ msgstr "Sauvez" msgid "Scan Domain" msgstr "Domaine de balayage" -#: src/domains/DomainCard.js:141 +#: src/domains/DomainCard.js:143 #: src/guidance/GuidancePage.js:140 msgid "Scan Pending" msgstr "Scan en attente" @@ -2787,7 +2801,7 @@ msgstr "Recherche par initié par, nom de la ressource" #: src/dmarc/DmarcByDomainPage.js:221 #: src/dmarc/DmarcByDomainPage.js:292 #: src/domains/DomainsPage.js:189 -#: src/organizationDetails/OrganizationDomains.js:313 +#: src/organizationDetails/OrganizationDomains.js:301 msgid "Search for a domain" msgstr "Rechercher un domaine" @@ -2803,12 +2817,12 @@ msgstr "Recherche d'un utilisateur (email)" #~ msgid "Search for an activity" #~ msgstr "Recherche d'une activité" -#: src/organizations/Organizations.js:151 +#: src/organizations/Organizations.js:161 msgid "Search for an organization" msgstr "Rechercher une organisation" #: src/admin/AdminDomains.js:252 -#: src/admin/UserList.js:149 +#: src/admin/UserList.js:131 #: src/components/ReactTableGlobalFilter.js:36 #: src/components/SearchBox.js:44 msgid "Search:" @@ -2868,8 +2882,8 @@ msgstr "Septembre" msgid "Serial:" msgstr "En série :" -#: src/organizations/Organizations.js:62 -#: src/organizations/Organizations.js:118 +#: src/organizations/Organizations.js:59 +#: src/organizations/Organizations.js:133 msgid "Services" msgstr "Services" @@ -2942,17 +2956,17 @@ msgstr "" #~ msgstr "Indique si le domaine est conforme à la politique." #: src/domains/DomainsPage.js:166 -#: src/organizationDetails/OrganizationDomains.js:291 +#: src/organizationDetails/OrganizationDomains.js:279 msgid "Shows if the domain meets the DomainKeys Identified Mail (DKIM) requirements." msgstr "Indique si le domaine répond aux exigences de DomainKeys Identified Mail (DKIM)." #: src/domains/DomainsPage.js:157 -#: src/organizationDetails/OrganizationDomains.js:282 +#: src/organizationDetails/OrganizationDomains.js:270 msgid "Shows if the domain meets the HSTS requirements." msgstr "Indique si le domaine répond aux exigences du HSTS." #: src/domains/DomainsPage.js:160 -#: src/organizationDetails/OrganizationDomains.js:285 +#: src/organizationDetails/OrganizationDomains.js:273 msgid "Shows if the domain meets the Hypertext Transfer Protocol Secure (HTTPS) requirements." msgstr "Indique si le domaine répond aux exigences du protocole de transfert hypertexte sécurisé (HTTPS)." @@ -2963,27 +2977,27 @@ msgstr "Indique si le domaine répond aux exigences du protocole de transfert hy #~ msgstr "Indique si le domaine répond aux exigences de Hypertext Transfer ol Secure (HTTPS)." #: src/domains/DomainsPage.js:170 -#: src/organizationDetails/OrganizationDomains.js:295 +#: src/organizationDetails/OrganizationDomains.js:283 msgid "Shows if the domain meets the Message Authentication, Reporting, and Conformance (DMARC) requirements." msgstr "Indique si le domaine répond aux exigences de Message Authentication, Reporting, and Conformance (DMARC)." #: src/domains/DomainsPage.js:163 -#: src/organizationDetails/OrganizationDomains.js:288 +#: src/organizationDetails/OrganizationDomains.js:276 msgid "Shows if the domain meets the Sender Policy Framework (SPF) requirements." msgstr "Indique si le domaine répond aux exigences du Sender Policy Framework (SPF)." #: src/domains/DomainsPage.js:162 -#: src/organizationDetails/OrganizationDomains.js:287 +#: src/organizationDetails/OrganizationDomains.js:275 msgid "Shows if the domain uses acceptable protocols." msgstr "Indique si le domaine utilise des protocoles acceptables." #: src/domains/DomainsPage.js:155 -#: src/organizationDetails/OrganizationDomains.js:280 +#: src/organizationDetails/OrganizationDomains.js:268 msgid "Shows if the domain uses only ciphers that are strong or acceptable." msgstr "Indique si le domaine utilise uniquement des ciphers forts ou acceptables." #: src/domains/DomainsPage.js:156 -#: src/organizationDetails/OrganizationDomains.js:281 +#: src/organizationDetails/OrganizationDomains.js:269 msgid "Shows if the domain uses only curves that are strong or acceptable." msgstr "Indique si le domaine utilise uniquement des courbes fortes ou acceptables" @@ -3009,21 +3023,21 @@ msgstr "Indique si les certificats reçus ne reposent pas sur un certificat raci #: src/guidance/WebTLSResults.js:224 msgid "Shows if the server was found to be vulnerable to the Heartbleed vulnerability." -msgstr "" +msgstr "Indique si le serveur s'est avéré vulnérable à la faille Heartbleed." #: src/guidance/WebTLSResults.js:237 msgid "Shows if the server was found to be vulnerable to the ROBOT vulnerability." -msgstr "" +msgstr "Indique si le serveur a été jugé vulnérable à la vulnérabilité ROBOT." #: src/guidance/WebConnectionResults.js:191 msgid "Shows the duration of time, in seconds, that the HSTS header is valid." msgstr "Indique la durée, en secondes, pendant laquelle l'en-tête HSTS est valide." -#: src/organizations/Organizations.js:119 +#: src/organizations/Organizations.js:133 msgid "Shows the number of domains that the organization is in control of." msgstr "Indique le nombre de domaines dont l'organisation a le contrôle." -#: src/organizations/Organizations.js:123 +#: src/organizations/Organizations.js:136 msgid "Shows the percentage of domains which have HTTPS configured and upgrade HTTP connections to HTTPS" msgstr "Indique le pourcentage de domaines qui ont configuré HTTPS et qui mettent à niveau les connexions HTTP vers HTTPS." @@ -3031,7 +3045,7 @@ msgstr "Indique le pourcentage de domaines qui ont configuré HTTPS et qui mette #~ msgid "Shows the percentage of domains which have HTTPS configured and upgrade HTTP connections to HTTPS (ITPIN 6.1.1)" #~ msgstr "Indique le pourcentage de domaines qui ont configuré HTTPS et qui mettent à niveau les connexions HTTP vers HTTPS (ITPIN 6.1.1)." -#: src/organizations/Organizations.js:127 +#: src/organizations/Organizations.js:140 msgid "Shows the percentage of domains which have a valid DMARC policy configuration." msgstr "Indique le pourcentage de domaines qui ont une configuration de politique DMARC valide." @@ -3059,7 +3073,7 @@ msgstr "Indique le nombre total d'e-mails qui ont été envoyés par ce domaine #~ msgid "Siganture Hash:" #~ msgstr "Siganture Hash :" -#: src/app/App.js:156 +#: src/app/App.js:160 #: src/app/FloatingMenu.js:197 #: src/app/TopBanner.js:118 #: src/auth/SignInPage.js:189 @@ -3089,7 +3103,7 @@ msgstr "Déconnexion." msgid "Signature Hash:" msgstr "Signature Hash :" -#: src/app/App.js:71 +#: src/app/App.js:72 msgid "Skip to main content" msgstr "Passer au contenu principal" @@ -3105,11 +3119,11 @@ msgstr "Trier par:" msgid "Source IP Address" msgstr "Adresse IP source" -#: src/organizationDetails/OrganizationDomains.js:95 +#: src/organizationDetails/OrganizationDomains.js:96 msgid "Staging" msgstr "Dév" -#: src/organizationDetails/OrganizationDomains.js:191 +#: src/organizationDetails/OrganizationDomains.js:192 msgid "Status or tag" msgstr "Statut ou étiquette" @@ -3134,11 +3148,11 @@ msgstr "Sujet :" msgid "Submit" msgstr "Soumettre" -#: src/admin/UserListModal.js:154 +#: src/admin/UserListModal.js:153 msgid "Successfully removed user {0}." msgstr "L'utilisateur {0} a été supprimé." -#: src/organizationDetails/OrganizationDetails.js:133 +#: src/organizationDetails/OrganizationDetails.js:132 #: src/user/MyTrackerPage.js:93 msgid "Summary" msgstr "Résumé" @@ -3171,7 +3185,7 @@ msgstr "le SCT soit identifié comme la source; et" msgid "TBS reserves the right to refuse service, and may reject your application for an account, or cancel an existing account, for any reason, at our sole discretion." msgstr "TBS se réserve le droit de refuser le service, de rejeter votre demande de compte ou d'annuler un compte existant, pour quelque raison que ce soit, à sa seule discrétion." -#: src/organizationDetails/OrganizationDomains.js:96 +#: src/organizationDetails/OrganizationDomains.js:97 msgid "TEST" msgstr "TEST" @@ -3203,11 +3217,11 @@ msgstr "Conseils techniques de mise en œuvre :" msgid "Termination" msgstr "Terminaison" -#: src/app/App.js:180 +#: src/app/App.js:184 msgid "Terms & Conditions" msgstr "Termes et conditions" -#: src/app/App.js:325 +#: src/app/App.js:333 #: src/app/FloatingMenu.js:225 #: src/app/SlideMessage.js:92 msgid "Terms & conditions" @@ -3221,7 +3235,7 @@ msgstr "Termes et conditions" msgid "Terms of Use" msgstr "Conditions d'utilisation" -#: src/organizationDetails/OrganizationDomains.js:96 +#: src/organizationDetails/OrganizationDomains.js:97 msgid "Test" msgstr "Test" @@ -3255,7 +3269,7 @@ msgstr "Les conseils, orientations ou services qui vous sont fournis par le SCT #: src/dmarc/DmarcByDomainPage.js:324 #: src/domains/DomainsPage.js:154 -#: src/organizationDetails/OrganizationDomains.js:278 +#: src/organizationDetails/OrganizationDomains.js:267 msgid "The domain address." msgstr "L'adresse du domaine." @@ -3303,7 +3317,7 @@ msgstr "Résultats de la vérification DKIM du message. Il peut s'agir d'un succ msgid "The summary cards show two metrics that Tracker scans:" msgstr "Les cartes récapitulatives présentent deux mesures que Suivi analyse :" -#: src/admin/UserListModal.js:106 +#: src/admin/UserListModal.js:107 msgid "The user's role has been successfully updated" msgstr "Le rôle de l'utilisateur a été mis à jour avec succès" @@ -3369,11 +3383,11 @@ msgstr "Temps généré" msgid "Time Generated (UTC)" msgstr "Heure générée (UTC)" -#: src/app/App.js:118 +#: src/app/App.js:122 msgid "To enable full app functionality and maximize your account's security, <0>please verify your account." msgstr "Pour activer toutes les fonctionnalités de l'application et maximiser la sécurité de votre compte, <0>vous devez vérifier votre compte." -#: src/app/App.js:132 +#: src/app/App.js:136 msgid "To maximize your account's security, <0>please activate a multi-factor authentication option." msgstr "Pour maximiser la sécurité de votre compte, <0>vous devez activer une option d'authentification multifactorielle." @@ -3446,11 +3460,11 @@ msgstr "Authentification à deux facteurs:" msgid "URL:" msgstr "URL :" -#: src/admin/UserListModal.js:258 +#: src/admin/UserListModal.js:259 msgid "USER" msgstr "UTILISATEUR" -#: src/admin/UserListModal.js:95 +#: src/admin/UserListModal.js:96 msgid "Unable to change user role, please try again." msgstr "Impossible de modifier le rôle de l'utilisateur, veuillez réessayer." @@ -3479,7 +3493,7 @@ msgstr "Impossible de créer une nouvelle organisation." msgid "Unable to create your account, please try again." msgstr "Impossible de créer votre compte, veuillez réessayer" -#: src/admin/UserListModal.js:68 +#: src/admin/UserListModal.js:71 msgid "Unable to invite user." msgstr "Impossible d'inviter un utilisateur." @@ -3496,10 +3510,15 @@ msgstr "Impossible de supprimer le domaine." msgid "Unable to remove this organization." msgstr "Impossible de supprimer cette organisation." -#: src/admin/UserListModal.js:162 +#: src/admin/UserListModal.js:161 msgid "Unable to remove user." msgstr "Impossible de supprimer l'utilisateur." +#: src/organizations/RequestOrgInviteModal.js:26 +#: src/organizations/RequestOrgInviteModal.js:46 +msgid "Unable to request invite, please try again." +msgstr "Impossible de demander une invitation, veuillez réessayer." + #: src/domains/ScanDomain.js:44 msgid "Unable to request scan, please try again." msgstr "Impossible de demander un balayage, veuillez réessayer." @@ -3555,7 +3574,7 @@ msgstr "Impossible de mettre à jour votre langue préférée, veuillez réessay msgid "Unable to update to your username, please try again." msgstr "Impossible de mettre à jour votre nom d'utilisateur, veuillez réessayer." -#: src/admin/UserListModal.js:115 +#: src/admin/UserListModal.js:116 msgid "Unable to update user role." msgstr "Impossible de mettre à jour le rôle de l'utilisateur." @@ -3575,14 +3594,14 @@ msgstr "Impossible de vérifier votre numéro de téléphone, veuillez réessaye msgid "Understanding Scan Metrics:" msgstr "Comprendre les métriques d'analyse :" -#: src/domains/DomainCard.js:83 +#: src/domains/DomainCard.js:85 msgid "Unfavourited Domain" msgstr "Domaine non favorisé" #: src/guidance/WebTLSResults.js:233 #: src/guidance/WebTLSResults.js:256 msgid "Unknown" -msgstr "" +msgstr "Inconnu" #: src/summaries/RadialBarChart.js:43 #: src/summaries/SummaryGroup.js:28 @@ -3644,7 +3663,7 @@ msgid "User Email" msgstr "Courriel de l'utilisateur" #: src/admin/SuperAdminUserList.js:157 -#: src/admin/UserList.js:72 +#: src/admin/UserList.js:65 msgid "User List" msgstr "Liste des utilisateurs" @@ -3652,11 +3671,11 @@ msgstr "Liste des utilisateurs" msgid "User email does not match" msgstr "L'email de l'utilisateur ne correspond pas" -#: src/admin/UserListModal.js:58 +#: src/admin/UserListModal.js:61 msgid "User invited" msgstr "Utilisateur invité" -#: src/admin/UserListModal.js:153 +#: src/admin/UserListModal.js:152 msgid "User removed." msgstr "Utilisateur supprimé." @@ -3665,8 +3684,8 @@ msgid "User:" msgstr "Utilisateur:" #: src/admin/AdminPage.js:190 -#: src/admin/AdminPanel.js:29 -#: src/organizationDetails/OrganizationDetails.js:143 +#: src/admin/AdminPanel.js:24 +#: src/organizationDetails/OrganizationDetails.js:142 msgid "Users" msgstr "Utilisateurs" @@ -3674,7 +3693,7 @@ msgstr "Utilisateurs" msgid "Users exercise due diligence in ensuring the accuracy of the materials reproduced;" msgstr "Les utilisateurs font preuve de diligence raisonnable en s'assurant de l'exactitude des documents reproduits;" -#: src/organizationDetails/OrganizationDomains.js:155 +#: src/organizationDetails/OrganizationDomains.js:154 msgid "Value" msgstr "Valeur" @@ -3684,7 +3703,7 @@ msgstr "Le code de vérification ne doit contenir que des chiffres" #: src/admin/SuperAdminUserList.js:150 #: src/admin/SuperAdminUserList.js:341 -#: src/organizations/Organizations.js:63 +#: src/organizations/Organizations.js:60 msgid "Verified" msgstr "Vérifié" @@ -3728,7 +3747,7 @@ msgstr "Volume de messages usurpant domaine (rejet + quarantaine + aucun) :" msgid "Vulnerability Scan Dashboard" msgstr "Tableau de bord de l'analyse des vulnérabilités" -#: src/organizationDetails/OrganizationDomains.js:97 +#: src/organizationDetails/OrganizationDomains.js:98 msgid "WEB" msgstr "WEB" @@ -3760,7 +3779,7 @@ msgstr "Nous vous avons envoyé un e-mail avec un code d'authentification pour v #~ msgid "Weak Curves:" #~ msgstr "Courbes faibles:" -#: src/organizationDetails/OrganizationDomains.js:97 +#: src/organizationDetails/OrganizationDomains.js:98 msgid "Web" msgstr "Web" @@ -3846,6 +3865,10 @@ msgstr "Pourquoi la page d'orientation n'affiche-t-elle pas les sélecteurs DKIM msgid "Wiki" msgstr "Wiki" +#: src/organizations/RequestOrgInviteModal.js:66 +msgid "Would you like to request an invite to {orgName}?" +msgstr "Souhaitez-vous demander une invitation à {orgName} ?" + #: src/guidance/WebConnectionResults.js:126 #: src/guidance/WebConnectionResults.js:166 #: src/guidance/WebConnectionResults.js:188 @@ -3947,7 +3970,7 @@ msgstr "Vous pouvez maintenant vous connecter avec votre nouveau mot de passe" msgid "You will need a Tracker account to use certain products and services. You are responsible for maintaining the confidentiality of your account, password and for restricting access to your account. You also agree to accept responsibility for all activities that occur under your account or password. TBS accepts no liability for any loss or damage arising from your failure to maintain the security of your account or password." msgstr "Vous aurez besoin d'un compte Suivi pour utiliser certains produits et services. Vous êtes responsable du maintien de la confidentialité de votre compte et de votre mot de passe et de la restriction de l'accès à votre compte. Vous acceptez également d'assumer la responsabilité de toutes les activités qui se déroulent sous votre compte ou votre mot de passe. Le SCT n'accepte aucune responsabilité pour toute perte ou tout dommage résultant de votre incapacité à maintenir la sécurité de votre compte ou de votre mot de passe." -#: src/app/App.js:262 +#: src/app/App.js:266 msgid "Your Account" msgstr "Votre compte" @@ -3963,6 +3986,10 @@ msgstr "L'email de votre compte a été vérifié avec succès" msgid "Your account will be fully activated the next time you log in" msgstr "Votre compte sera entièrement activé lors de votre prochaine connexion." +#: src/organizations/RequestOrgInviteModal.js:37 +msgid "Your request has been sent to the organization administrators." +msgstr "Votre demande a été envoyée aux administrateurs de l'organisation." + #: src/admin/OrganizationInformation.js:421 msgid "Zone:" msgstr "Zone:" @@ -3983,8 +4010,8 @@ msgstr "contactez-nous" #~ msgid "https://https-everywhere.canada.ca/en/help/" #~ msgstr "https://https-everywhere.canada.ca/en/help/" -#: src/app/App.js:97 -#: src/app/App.js:275 +#: src/app/App.js:100 +#: src/app/App.js:279 #: src/user/MyTrackerPage.js:43 #: src/user/MyTrackerPage.js:74 msgid "myTracker" @@ -4018,7 +4045,7 @@ msgstr "sp:" msgid "strong" msgstr "fort" -#: src/admin/UserList.js:162 +#: src/admin/UserList.js:140 msgid "user email" msgstr "e-mail de l'utilisateur" diff --git a/frontend/src/organizationDetails/OrganizationDetails.js b/frontend/src/organizationDetails/OrganizationDetails.js index 72c67877b4..4f23d46a91 100644 --- a/frontend/src/organizationDetails/OrganizationDetails.js +++ b/frontend/src/organizationDetails/OrganizationDetails.js @@ -3,6 +3,7 @@ import { useLazyQuery, useQuery } from '@apollo/client' import { Trans } from '@lingui/macro' import { Box, + Button, Flex, Heading, IconButton, @@ -12,8 +13,10 @@ import { TabPanels, Tabs, Text, + useDisclosure, } from '@chakra-ui/react' import { ArrowLeftIcon, CheckCircleIcon } from '@chakra-ui/icons' +import { UserIcon } from '../theme/Icons' import { Link as RouteLink, useParams, useHistory } from 'react-router-dom' import { ErrorBoundary } from 'react-error-boundary' @@ -24,16 +27,18 @@ import { OrganizationSummary } from './OrganizationSummary' import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' import { LoadingMessage } from '../components/LoadingMessage' import { useDocumentTitle } from '../utilities/useDocumentTitle' -import { - GET_ORGANIZATION_DOMAINS_STATUSES_CSV, - ORG_DETAILS_PAGE, -} from '../graphql/queries' +import { GET_ORGANIZATION_DOMAINS_STATUSES_CSV, ORG_DETAILS_PAGE } from '../graphql/queries' import { RadialBarChart } from '../summaries/RadialBarChart' import { ExportButton } from '../components/ExportButton' +import { RequestOrgInviteModal } from '../organizations/RequestOrgInviteModal' +import { useUserVar } from '../utilities/userState' +import { ABTestingWrapper, ABTestVariant } from '../app/ABTestWrapper' export default function OrganizationDetails() { + const { isLoggedIn } = useUserVar() const { orgSlug, activeTab } = useParams() const history = useHistory() + const { isOpen, onOpen, onClose } = useDisclosure() const tabNames = ['summary', 'dmarc_phases', 'domains', 'users'] const defaultActiveTab = tabNames[0] @@ -44,12 +49,12 @@ export default function OrganizationDetails() { // errorPolicy: 'ignore', // allow partial success }) - const [ - getOrgDomainStatuses, - { loading: orgDomainStatusesLoading, _error, _data }, - ] = useLazyQuery(GET_ORGANIZATION_DOMAINS_STATUSES_CSV, { - variables: { orgSlug: orgSlug }, - }) + const [getOrgDomainStatuses, { loading: orgDomainStatusesLoading, _error, _data }] = useLazyQuery( + GET_ORGANIZATION_DOMAINS_STATUSES_CSV, + { + variables: { orgSlug: orgSlug }, + }, + ) useEffect(() => { if (!activeTab) { @@ -79,12 +84,7 @@ export default function OrganizationDetails() { return ( - + } as={RouteLink} @@ -102,25 +102,29 @@ export default function OrganizationDetails() { order={{ base: 2, md: 1 }} flexBasis={{ base: '100%', md: 'auto' }} > - {orgName} - {data?.organization?.verified && ( - <> - {' '} - - - )} + + {orgName} + {data?.organization?.verified && } + - { - const result = await getOrgDomainStatuses() - return result.data?.findOrganizationBySlug?.toCsv - }} - isLoading={orgDomainStatusesLoading} - /> + + + {isLoggedIn && ( + <> + + + + )} + + DMARC Phases - + - + { + const result = await getOrgDomainStatuses() + return result.data?.findOrganizationBySlug?.toCsv + }} + isLoading={orgDomainStatusesLoading} + /> + {!isNaN(data?.organization?.affiliations?.totalCount) && ( - + )} diff --git a/frontend/src/organizations/Organizations.js b/frontend/src/organizations/Organizations.js index 9627b27dc8..216675ceb0 100644 --- a/frontend/src/organizations/Organizations.js +++ b/frontend/src/organizations/Organizations.js @@ -1,7 +1,7 @@ import React, { useCallback, useState } from 'react' import { t, Trans } from '@lingui/macro' import { ListOf } from '../components/ListOf' -import { Box, Divider, Heading, Text, useDisclosure } from '@chakra-ui/react' +import { Box, Divider, Flex, Heading, IconButton, Text, useDisclosure } from '@chakra-ui/react' import { ErrorBoundary } from 'react-error-boundary' import { OrganizationCard } from './OrganizationCard' @@ -14,13 +14,20 @@ import { usePaginatedCollection } from '../utilities/usePaginatedCollection' import { useDebouncedFunction } from '../utilities/useDebouncedFunction' import { PAGINATED_ORGANIZATIONS as FORWARD } from '../graphql/queries' import { SearchBox } from '../components/SearchBox' +import { UserIcon } from '../theme/Icons' +import { RequestOrgInviteModal } from './RequestOrgInviteModal' +import { useUserVar } from '../utilities/userState' +import { ABTestingWrapper, ABTestVariant } from '../app/ABTestWrapper' export default function Organizations() { + const { isLoggedIn } = useUserVar() const [orderDirection, setOrderDirection] = useState('ASC') const [orderField, setOrderField] = useState('NAME') const [searchTerm, setSearchTerm] = useState('') const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('') const [orgsPerPage, setOrgsPerPage] = useState(10) + const { isOpen: inviteRequestIsOpen, onOpen, onClose } = useDisclosure() + const [orgInfo, setOrgInfo] = useState({}) const memoizedSetDebouncedSearchTermCallback = useCallback(() => { setDebouncedSearchTerm(searchTerm) @@ -30,29 +37,20 @@ export default function Organizations() { const { isOpen, onToggle } = useDisclosure() - const { - loading, - isLoadingMore, - error, - nodes, - next, - previous, - resetToFirstPage, - hasNextPage, - hasPreviousPage, - } = usePaginatedCollection({ - fetchForward: FORWARD, - variables: { - field: orderField, - direction: orderDirection, - search: debouncedSearchTerm, - includeSuperAdminOrg: false, - }, - fetchPolicy: 'cache-and-network', - nextFetchPolicy: 'cache-first', - recordsPerPage: orgsPerPage, - relayRoot: 'findMyOrganizations', - }) + const { loading, isLoadingMore, error, nodes, next, previous, resetToFirstPage, hasNextPage, hasPreviousPage } = + usePaginatedCollection({ + fetchForward: FORWARD, + variables: { + field: orderField, + direction: orderDirection, + search: debouncedSearchTerm, + includeSuperAdminOrg: false, + }, + fetchPolicy: 'cache-and-network', + nextFetchPolicy: 'cache-first', + recordsPerPage: orgsPerPage, + relayRoot: 'findMyOrganizations', + }) if (error) return @@ -83,20 +81,43 @@ export default function Organizations() { )} mb="4" > - {({ name, slug, acronym, domainCount, verified, summaries }, index) => ( - - + {({ id, name, slug, acronym, domainCount, verified, summaries }, index) => ( + + + + + + {isLoggedIn && ( + <> + } + onClick={() => { + setOrgInfo({ id, name }) + onOpen() + }} + /> + + + )} + + + )} @@ -114,10 +135,7 @@ export default function Organizations() { title={t`Organization Name`} info={t`Displays the Name of the organization, its acronym, and a blue check mark if it is a verified organization.`} /> - + - - Further details for each organization can be found by clicking on its - row. - + Further details for each organization can be found by clicking on its row. diff --git a/frontend/src/organizations/RequestOrgInviteModal.js b/frontend/src/organizations/RequestOrgInviteModal.js new file mode 100644 index 0000000000..7fb9d39a15 --- /dev/null +++ b/frontend/src/organizations/RequestOrgInviteModal.js @@ -0,0 +1,88 @@ +import React from 'react' +import { REQUEST_INVITE_TO_ORG } from '../graphql/mutations' +import { useMutation } from '@apollo/client' +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Button, + useToast, +} from '@chakra-ui/react' +import { Trans, t } from '@lingui/macro' +import { bool } from 'prop-types' +import { func } from 'prop-types' +import { string } from 'prop-types' + +export function RequestOrgInviteModal({ isOpen, onClose, orgId, orgName }) { + const toast = useToast() + const [requestInviteToOrg, { loading }] = useMutation(REQUEST_INVITE_TO_ORG, { + onError(error) { + toast({ + title: error.message, + description: t`Unable to request invite, please try again.`, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + }, + onCompleted({ requestOrgAffiliation }) { + if (requestOrgAffiliation.result.__typename === 'InviteUserToOrgResult') { + toast({ + title: t`Invite Requested`, + description: t`Your request has been sent to the organization administrators.`, + status: 'success', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + onClose() + } else { + toast({ + title: t`Unable to request invite, please try again.`, + description: requestOrgAffiliation.result.description, + status: 'error', + duration: 9000, + isClosable: true, + position: 'top-left', + }) + } + }, + }) + + return ( + + + + + Request Invite + + + + Would you like to request an invite to {orgName}? + + + + + + + + ) +} + +RequestOrgInviteModal.propTypes = { + isOpen: bool, + onClose: func, + orgId: string, + orgName: string, +} 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/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: ( - + - +