diff --git a/api/src/__tests__/create-context.test.js b/api/src/__tests__/create-context.test.js index d4d9b39be5..0c6f0f6ca1 100644 --- a/api/src/__tests__/create-context.test.js +++ b/api/src/__tests__/create-context.test.js @@ -33,5 +33,25 @@ describe('given the create context function', () => { expect(context.userKey).toEqual('1234') }) + + it('returns object with affiliation data source', async () => { + const context = await createContext({ + query: jest.fn(), + transaction: jest.fn(), + collections: [], + req: { + language: 'en', + headers: { + authorization: tokenize({parameters: {userKey: '1234'}}), + }, + }, + res: {}, + }) + + expect(context.dataSources.affiliation).toBeDefined() + expect(context.dataSources.affiliation.byKey).toBeDefined() + expect(context.dataSources.affiliation.connectionsByUserId).toBeDefined() + expect(context.dataSources.affiliation.connectionsByOrgId).toBeDefined() + }) }) }) diff --git a/api/src/__tests__/initialize-loaders.test.js b/api/src/__tests__/initialize-loaders.test.js index a0f24fb4c6..ed28b1dd44 100644 --- a/api/src/__tests__/initialize-loaders.test.js +++ b/api/src/__tests__/initialize-loaders.test.js @@ -28,9 +28,6 @@ describe('initializeLoaders', () => { 'loadUserConnectionsByUserId', 'loadUserByKey', 'loadMyTrackerByUserId', - 'loadAffiliationByKey', - 'loadAffiliationConnectionsByUserId', - 'loadAffiliationConnectionsByOrgId', 'loadVerifiedDomainsById', 'loadVerifiedDomainByKey', 'loadVerifiedDomainConnections', diff --git a/api/src/affiliation/data-source.js b/api/src/affiliation/data-source.js new file mode 100644 index 0000000000..b6ec488fd9 --- /dev/null +++ b/api/src/affiliation/data-source.js @@ -0,0 +1,227 @@ +import { + loadAffiliationByKey, + loadAffiliationConnectionsByOrgId, + loadAffiliationConnectionsByUserId, +} from './loaders' + +export class AffiliationDataSource { + constructor({ query, userKey, i18n, language, cleanseInput, transaction, collections }) { + this._query = query + this._transaction = transaction + this._collections = collections + + this.byKey = loadAffiliationByKey({ query, userKey, i18n }) + this.connectionsByUserId = loadAffiliationConnectionsByUserId({ + query, + language, + userKey, + cleanseInput, + i18n, + }) + this.connectionsByOrgId = loadAffiliationConnectionsByOrgId({ + query, + userKey, + cleanseInput, + i18n, + }) + } + + async affiliationByOrgAndUser({ orgId, userId }) { + let affiliationCursor + try { + affiliationCursor = await this._query` + WITH affiliations, organizations, users + FOR v, e IN 1..1 OUTBOUND ${orgId} affiliations + FILTER e._to == ${userId} + RETURN e + ` + } catch (err) { + err.affiliationDataSourceOp = 'query' + throw err + } + + if (affiliationCursor.count < 1) { + return undefined + } + + try { + return await affiliationCursor.next() + } catch (err) { + err.affiliationDataSourceOp = 'cursor' + throw err + } + } + + async createAffiliation({ orgId, userId, permission }) { + const transaction = await this._transaction(this._collections) + + try { + await transaction.step( + () => + this._query` + WITH affiliations, organizations, users + INSERT { + _from: ${orgId}, + _to: ${userId}, + permission: ${permission}, + } INTO affiliations + `, + ) + } catch (err) { + if (typeof transaction.abort === 'function') await transaction.abort() + err.affiliationDataSourceOp = 'trx-step' + throw err + } + + try { + await transaction.commit() + } catch (err) { + if (typeof transaction.abort === 'function') await transaction.abort() + err.affiliationDataSourceOp = 'trx-commit' + throw err + } + } + + async createPendingAffiliation({ orgId, userId }) { + return this.createAffiliation({ orgId, userId, permission: 'pending' }) + } + + async updateAffiliationPermission({ affiliationKey, orgId, userId, permission }) { + const trx = await this._transaction(this._collections) + + const edge = { + _from: orgId, + _to: userId, + permission, + } + + try { + await trx.step( + async () => + await this._query` + WITH affiliations, organizations, users + UPSERT { _key: ${affiliationKey} } + INSERT ${edge} + UPDATE ${edge} + IN affiliations + `, + ) + } catch (err) { + if (typeof trx.abort === 'function') await trx.abort() + err.affiliationDataSourceOp = 'trx-step' + throw err + } + + try { + await trx.commit() + } catch (err) { + if (typeof trx.abort === 'function') await trx.abort() + err.affiliationDataSourceOp = 'trx-commit' + throw err + } + } + + async removeAffiliation({ orgId, userId }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => + this._query` + WITH affiliations, organizations, users + FOR aff IN affiliations + FILTER aff._from == ${orgId} + FILTER aff._to == ${userId} + REMOVE aff IN affiliations + RETURN true + `, + ) + } catch (err) { + if (typeof trx.abort === 'function') await trx.abort() + err.affiliationDataSourceOp = 'trx-step' + throw err + } + + try { + await trx.commit() + } catch (err) { + if (typeof trx.abort === 'function') await trx.abort() + err.affiliationDataSourceOp = 'trx-commit' + throw err + } + } + + async transferOrgOwnership({ orgId, fromUserId, toUserId }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => + this._query` + WITH affiliations, organizations, users + FOR aff IN affiliations + FILTER aff._from == ${orgId} + FILTER aff._to == ${fromUserId} + UPDATE { _key: aff._key } WITH { + permission: "admin", + } IN affiliations + RETURN aff + `, + ) + } catch (err) { + if (typeof trx.abort === 'function') await trx.abort() + err.affiliationDataSourceOp = 'trx-step' + throw err + } + + try { + await trx.step( + () => + this._query` + WITH affiliations, organizations, users + FOR aff IN affiliations + FILTER aff._from == ${orgId} + FILTER aff._to == ${toUserId} + UPDATE { _key: aff._key } WITH { + permission: "owner", + } IN affiliations + RETURN aff + `, + ) + } catch (err) { + if (typeof trx.abort === 'function') await trx.abort() + err.affiliationDataSourceOp = 'trx-step' + throw err + } + + try { + await trx.commit() + } catch (err) { + if (typeof trx.abort === 'function') await trx.abort() + err.affiliationDataSourceOp = 'trx-commit' + throw err + } + } + + async orgAdminUserKeys({ orgId }) { + let orgAdminsCursor + try { + orgAdminsCursor = await this._query` + WITH affiliations, organizations, users + FOR v, e IN 1..1 OUTBOUND ${orgId} affiliations + FILTER e.permission IN ["admin", "owner", "super_admin"] + RETURN v._key + ` + } catch (err) { + err.affiliationDataSourceOp = 'query' + throw err + } + + try { + return await orgAdminsCursor.all() + } catch (err) { + err.affiliationDataSourceOp = 'cursor' + throw err + } + } +} diff --git a/api/src/affiliation/index.js b/api/src/affiliation/index.js index 8c7cd352a5..afb08ea1a1 100644 --- a/api/src/affiliation/index.js +++ b/api/src/affiliation/index.js @@ -1,3 +1,4 @@ +export * from './data-source' export * from './loaders' export * from './mutations' export * from './objects' diff --git a/api/src/affiliation/mutations/__tests__/invite-user-to-org.test.js b/api/src/affiliation/mutations/__tests__/invite-user-to-org.test.js index cac682c504..ed82606ae9 100644 --- a/api/src/affiliation/mutations/__tests__/invite-user-to-org.test.js +++ b/api/src/affiliation/mutations/__tests__/invite-user-to-org.test.js @@ -1,7 +1,7 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' import { setupI18n } from '@lingui/core' -import { graphql, GraphQLSchema } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema } from 'graphql' import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' @@ -12,11 +12,38 @@ import { createQuerySchema } from '../../../query' import { cleanseInput } from '../../../validators' import { loadOrgByKey, loadOrganizationNamesById } from '../../../organization/loaders' import { loadUserByKey, loadUserByUserName } from '../../../user/loaders' +import { AffiliationDataSource } from '../../data-source' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' const { DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY } = process.env +const withAffiliationDataSource = (contextValue = {}) => { + const dataSources = contextValue.dataSources || {} + if (dataSources.affiliation && dataSources.auditLogs) return contextValue + + return { + ...contextValue, + dataSources: { + ...dataSources, + affiliation: + dataSources.affiliation || + new AffiliationDataSource({ + query: contextValue.query, + transaction: contextValue.transaction, + collections: contextValue.collections, + userKey: contextValue.userKey, + i18n: contextValue.i18n, + language: contextValue.request?.language, + cleanseInput: contextValue.validators?.cleanseInput, + }), + auditLogs: dataSources.auditLogs || { logActivity: jest.fn().mockResolvedValue(undefined) }, + }, + } +} + +const graphql = (args) => executeGraphql({ ...args, contextValue: withAffiliationDataSource(args.contextValue) }) + describe('invite user to org', () => { let query, drop, truncate, schema, collections, transaction, i18n, tokenize, user, org, userToInvite @@ -1622,7 +1649,7 @@ describe('invite user to org', () => { query, collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue('trx step err'), + step: jest.fn().mockRejectedValue(new Error('trx step err')), abort: jest.fn(), }), userKey: 123, @@ -1659,7 +1686,7 @@ describe('invite user to org', () => { expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `Transaction step error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: treasury-board-secretariat, error: trx step err`, + `Transaction step error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: treasury-board-secretariat, error: Error: trx step err`, ]) }) }) @@ -1700,7 +1727,7 @@ describe('invite user to org', () => { collections: collectionNames, transaction: jest.fn().mockReturnValue({ step: jest.fn(), - commit: jest.fn().mockRejectedValue('trx commit err'), + commit: jest.fn().mockRejectedValue(new Error('trx commit err')), abort: jest.fn(), }), userKey: 123, @@ -1740,7 +1767,7 @@ describe('invite user to org', () => { expect(response).toEqual(error) expect(consoleOutput).toEqual([ - `Transaction commit error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: treasury-board-secretariat, error: trx commit err`, + `Transaction commit error occurred while user: 123 attempted to invite user: ${userToInvite._key} to org: treasury-board-secretariat, error: Error: trx commit err`, ]) }) }) diff --git a/api/src/affiliation/mutations/__tests__/leave-organization.test.js b/api/src/affiliation/mutations/__tests__/leave-organization.test.js index 61400c9785..7a34fa2fb1 100644 --- a/api/src/affiliation/mutations/__tests__/leave-organization.test.js +++ b/api/src/affiliation/mutations/__tests__/leave-organization.test.js @@ -1,7 +1,7 @@ import { setupI18n } from '@lingui/core' import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' @@ -12,11 +12,34 @@ import { loadUserByKey } from '../../../user/loaders' import { cleanseInput } from '../../../validators' import { createMutationSchema } from '../../../mutation' import { createQuerySchema } from '../../../query' +import { AffiliationDataSource } from '../../data-source' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' const { DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY } = process.env +const withAffiliationDataSource = (contextValue = {}) => { + if (contextValue.dataSources?.affiliation) return contextValue + + return { + ...contextValue, + dataSources: { + ...(contextValue.dataSources || {}), + affiliation: new AffiliationDataSource({ + query: contextValue.query, + transaction: contextValue.transaction, + collections: contextValue.collections, + userKey: contextValue.userKey, + i18n: contextValue.i18n, + language: contextValue.request?.language, + cleanseInput: contextValue.validators?.cleanseInput, + }), + }, + } +} + +const graphql = (args) => executeGraphql({ ...args, contextValue: withAffiliationDataSource(args.contextValue) }) + describe('given a successful leave', () => { let query, drop, truncate, schema, collections, transaction, i18n, user, org, domain, domain2 diff --git a/api/src/affiliation/mutations/__tests__/remove-user-from-org.test.js b/api/src/affiliation/mutations/__tests__/remove-user-from-org.test.js index f994ded22e..ba45c2efea 100644 --- a/api/src/affiliation/mutations/__tests__/remove-user-from-org.test.js +++ b/api/src/affiliation/mutations/__tests__/remove-user-from-org.test.js @@ -1,7 +1,7 @@ import { setupI18n } from '@lingui/core' import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' @@ -12,12 +12,51 @@ import { cleanseInput } from '../../../validators' import { checkPermission, userRequired, verifiedRequired, tfaRequired } from '../../../auth' import { loadOrgByKey } from '../../../organization/loaders' import { loadUserByKey } from '../../../user/loaders' -import { loadAffiliationByKey } from '../../loaders' +import { AffiliationDataSource } from '../../data-source' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' const { DB_PASS: rootPass, DB_URL: url } = process.env +const withAffiliationDataSource = (contextValue = {}) => { + const dataSources = contextValue.dataSources || {} + if (dataSources.affiliation && dataSources.auditLogs) return contextValue + + return { + ...contextValue, + dataSources: { + ...dataSources, + affiliation: + dataSources.affiliation || + new AffiliationDataSource({ + query: contextValue.query, + transaction: contextValue.transaction, + collections: contextValue.collections, + userKey: contextValue.userKey, + i18n: contextValue.i18n, + language: contextValue.request?.language, + cleanseInput: contextValue.validators?.cleanseInput, + }), + auditLogs: dataSources.auditLogs || { logActivity: jest.fn().mockResolvedValue(undefined) }, + }, + } +} + +const graphql = (args) => executeGraphql({ ...args, contextValue: withAffiliationDataSource(args.contextValue) }) + +const loadAffiliationWithDataSource = async ({ query, transaction, collections, userKey, i18n, affiliationKey }) => { + const affiliationDataSource = new AffiliationDataSource({ + query, + transaction, + collections, + userKey, + i18n, + cleanseInput, + }) + + return affiliationDataSource.byKey.load(affiliationKey) +} + const orgOneData = { verified: true, orgDetails: { @@ -225,14 +264,15 @@ describe('given the removeUserFromOrg mutation', () => { }, }) - const loader = loadAffiliationByKey({ + const data = await loadAffiliationWithDataSource({ query, + transaction, + collections, userKey: admin._key, i18n, + affiliationKey: affiliation._key, }) - const data = await loader.load(affiliation._key) - expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) @@ -520,14 +560,15 @@ describe('given the removeUserFromOrg mutation', () => { }, }) - const loader = loadAffiliationByKey({ + const data = await loadAffiliationWithDataSource({ query, + transaction, + collections, userKey: admin._key, i18n, + affiliationKey: affiliation._key, }) - const data = await loader.load(affiliation._key) - expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) @@ -833,14 +874,15 @@ describe('given the removeUserFromOrg mutation', () => { }, }) - const loader = loadAffiliationByKey({ + const data = await loadAffiliationWithDataSource({ query, + transaction, + collections, userKey: admin._key, i18n, + affiliationKey: affiliation._key, }) - const data = await loader.load(affiliation._key) - expect(consoleOutput).toEqual([ `User: ${admin._key} successfully removed user: ${user._key} from org: ${orgOne._key}.`, ]) diff --git a/api/src/affiliation/mutations/__tests__/request-org-affiliation.test.js b/api/src/affiliation/mutations/__tests__/request-org-affiliation.test.js index fc9ce588cb..5016beaee0 100644 --- a/api/src/affiliation/mutations/__tests__/request-org-affiliation.test.js +++ b/api/src/affiliation/mutations/__tests__/request-org-affiliation.test.js @@ -1,7 +1,7 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' import { setupI18n } from '@lingui/core' -import { graphql, GraphQLSchema } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema } from 'graphql' import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' @@ -10,6 +10,7 @@ import { userRequired, verifiedRequired } from '../../../auth' import { createMutationSchema } from '../../../mutation' import { createQuerySchema } from '../../../query' import { cleanseInput } from '../../../validators' +import { AffiliationDataSource } from '../../data-source' import { loadOrgByKey, loadOrganizationNamesById } from '../../../organization/loaders' import { loadUserByKey } from '../../../user/loaders' import dbschema from '../../../../database.json' @@ -17,6 +18,32 @@ import { collectionNames } from '../../../collection-names' const { DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY } = process.env +const withAffiliationDataSource = (contextValue = {}) => { + const dataSources = contextValue.dataSources || {} + if (dataSources.affiliation && dataSources.auditLogs) return contextValue + + return { + ...contextValue, + dataSources: { + ...dataSources, + affiliation: + dataSources.affiliation || + new AffiliationDataSource({ + query: contextValue.query, + transaction: contextValue.transaction, + collections: contextValue.collections, + userKey: contextValue.userKey, + i18n: contextValue.i18n, + language: contextValue.request?.language, + cleanseInput: contextValue.validators?.cleanseInput, + }), + auditLogs: dataSources.auditLogs || { logActivity: jest.fn().mockResolvedValue(undefined) }, + }, + } +} + +const graphql = (args) => executeGraphql({ ...args, contextValue: withAffiliationDataSource(args.contextValue) }) + describe('invite user to org', () => { let query, drop, truncate, schema, collections, transaction, i18n, tokenize, user, org @@ -546,7 +573,7 @@ describe('invite user to org', () => { query, collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue('trx step err'), + step: jest.fn().mockRejectedValue(new Error('trx step err')), }), userKey: 123, auth: { @@ -568,7 +595,7 @@ describe('invite user to org', () => { }) expect(consoleOutput).toEqual([ - `Transaction step error occurred while user: 123 attempted to request invite to org: treasury-board-secretariat, error: trx step err`, + `Transaction step error occurred while user: 123 attempted to request invite to org: treasury-board-secretariat, error: Error: trx step err`, ]) }) }) @@ -601,7 +628,7 @@ describe('invite user to org', () => { query, collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue('trx commit err'), + step: jest.fn().mockRejectedValue(new Error('trx commit err')), }), userKey: 123, auth: { @@ -623,7 +650,7 @@ describe('invite user to org', () => { }) expect(consoleOutput).toEqual([ - `Transaction step error occurred while user: 123 attempted to request invite to org: treasury-board-secretariat, error: trx commit err`, + `Transaction step error occurred while user: 123 attempted to request invite to org: treasury-board-secretariat, error: Error: trx commit err`, ]) }) }) diff --git a/api/src/affiliation/mutations/__tests__/transfer-org-ownership.test.js b/api/src/affiliation/mutations/__tests__/transfer-org-ownership.test.js index 86728b11e0..72a42e05ff 100644 --- a/api/src/affiliation/mutations/__tests__/transfer-org-ownership.test.js +++ b/api/src/affiliation/mutations/__tests__/transfer-org-ownership.test.js @@ -1,7 +1,7 @@ import { setupI18n } from '@lingui/core' import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' @@ -12,11 +12,34 @@ import { loadUserByKey } from '../../../user/loaders' import { cleanseInput } from '../../../validators' import { createMutationSchema } from '../../../mutation' import { createQuerySchema } from '../../../query' +import { AffiliationDataSource } from '../../data-source' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' const { DB_PASS: rootPass, DB_URL: url, SIGN_IN_KEY } = process.env +const withAffiliationDataSource = (contextValue = {}) => { + if (contextValue.dataSources?.affiliation) return contextValue + + return { + ...contextValue, + dataSources: { + ...(contextValue.dataSources || {}), + affiliation: new AffiliationDataSource({ + query: contextValue.query, + transaction: contextValue.transaction, + collections: contextValue.collections, + userKey: contextValue.userKey, + i18n: contextValue.i18n, + language: contextValue.request?.language, + cleanseInput: contextValue.validators?.cleanseInput, + }), + }, + } +} + +const graphql = (args) => executeGraphql({ ...args, contextValue: withAffiliationDataSource(args.contextValue) }) + describe('given the transferOrgOwnership mutation', () => { let query, drop, truncate, schema, collections, transaction, i18n, user, user2, org @@ -841,7 +864,10 @@ describe('given the transferOrgOwnership mutation', () => { rootValue: null, contextValue: { i18n, - query: jest.fn().mockReturnValue({ count: 1 }), + query: jest.fn().mockReturnValue({ + count: 1, + next: jest.fn().mockReturnValue({ _key: 'affiliation-1' }), + }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -909,7 +935,10 @@ describe('given the transferOrgOwnership mutation', () => { rootValue: null, contextValue: { i18n, - query: jest.fn().mockReturnValue({ count: 1 }), + query: jest.fn().mockReturnValue({ + count: 1, + next: jest.fn().mockReturnValue({ _key: 'affiliation-1' }), + }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -979,7 +1008,10 @@ describe('given the transferOrgOwnership mutation', () => { rootValue: null, contextValue: { i18n, - query: jest.fn().mockReturnValue({ count: 1 }), + query: jest.fn().mockReturnValue({ + count: 1, + next: jest.fn().mockReturnValue({ _key: 'affiliation-1' }), + }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -1418,7 +1450,10 @@ describe('given the transferOrgOwnership mutation', () => { rootValue: null, contextValue: { i18n, - query: jest.fn().mockReturnValue({ count: 1 }), + query: jest.fn().mockReturnValue({ + count: 1, + next: jest.fn().mockReturnValue({ _key: 'affiliation-1' }), + }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -1488,7 +1523,10 @@ describe('given the transferOrgOwnership mutation', () => { rootValue: null, contextValue: { i18n, - query: jest.fn().mockReturnValue({ count: 1 }), + query: jest.fn().mockReturnValue({ + count: 1, + next: jest.fn().mockReturnValue({ _key: 'affiliation-1' }), + }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, @@ -1560,7 +1598,10 @@ describe('given the transferOrgOwnership mutation', () => { rootValue: null, contextValue: { i18n, - query: jest.fn().mockReturnValue({ count: 1 }), + query: jest.fn().mockReturnValue({ + count: 1, + next: jest.fn().mockReturnValue({ _key: 'affiliation-1' }), + }), collections: collectionNames, transaction: mockedTransaction, userKey: user._key, diff --git a/api/src/affiliation/mutations/__tests__/update-user-role.test.js b/api/src/affiliation/mutations/__tests__/update-user-role.test.js index 7d455c4e91..bac77c338d 100644 --- a/api/src/affiliation/mutations/__tests__/update-user-role.test.js +++ b/api/src/affiliation/mutations/__tests__/update-user-role.test.js @@ -1,7 +1,7 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' import { setupI18n } from '@lingui/core' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' @@ -12,11 +12,38 @@ import { cleanseInput } from '../../../validators' import { checkPermission, userRequired, verifiedRequired, tfaRequired } from '../../../auth' import { loadUserByUserName, loadUserByKey } from '../../../user/loaders' import { loadOrgByKey } from '../../../organization/loaders' +import { AffiliationDataSource } from '../../data-source' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' const { DB_PASS: rootPass, DB_URL: url } = process.env +const withAffiliationDataSource = (contextValue = {}) => { + const dataSources = contextValue.dataSources || {} + if (dataSources.affiliation && dataSources.auditLogs) return contextValue + + return { + ...contextValue, + dataSources: { + ...dataSources, + affiliation: + dataSources.affiliation || + new AffiliationDataSource({ + query: contextValue.query, + transaction: contextValue.transaction, + collections: contextValue.collections, + userKey: contextValue.userKey, + i18n: contextValue.i18n, + language: contextValue.request?.language, + cleanseInput: contextValue.validators?.cleanseInput, + }), + auditLogs: dataSources.auditLogs || { logActivity: jest.fn().mockResolvedValue(undefined) }, + }, + } +} + +const graphql = (args) => executeGraphql({ ...args, contextValue: withAffiliationDataSource(args.contextValue) }) + describe('update a users role', () => { let query, drop, truncate, schema, collections, transaction, i18n, user @@ -1364,7 +1391,7 @@ describe('update a users role', () => { }), collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue('trx step error'), + step: jest.fn().mockRejectedValue(new Error('trx step error')), abort: jest.fn(), }), userKey: 123, @@ -1399,7 +1426,7 @@ describe('update a users role', () => { const error = [new GraphQLError(`Unable to update user's role. Please try again.`)] expect(consoleOutput).toEqual([ - `Transaction step error occurred when user: 123 attempted to update a user's: 456 role, error: trx step error`, + `Transaction step error occurred when user: 123 attempted to update a user's: 456 role, error: Error: trx step error`, ]) expect(response.errors).toEqual(error) }) @@ -1439,7 +1466,7 @@ describe('update a users role', () => { collections: collectionNames, transaction: jest.fn().mockReturnValue({ step: jest.fn(), - commit: jest.fn().mockRejectedValue('trx commit error'), + commit: jest.fn().mockRejectedValue(new Error('trx commit error')), abort: jest.fn(), }), userKey: 123, @@ -1474,7 +1501,7 @@ describe('update a users role', () => { const error = [new GraphQLError(`Unable to update user's role. Please try again.`)] expect(consoleOutput).toEqual([ - `Transaction commit error occurred when user: 123 attempted to update a user's: 456 role, error: trx commit error`, + `Transaction commit error occurred when user: 123 attempted to update a user's: 456 role, error: Error: trx commit error`, ]) expect(response.errors).toEqual(error) }) diff --git a/api/src/affiliation/mutations/invite-user-to-org.js b/api/src/affiliation/mutations/invite-user-to-org.js index 8398c497aa..aede988dc1 100644 --- a/api/src/affiliation/mutations/invite-user-to-org.js +++ b/api/src/affiliation/mutations/invite-user-to-org.js @@ -4,7 +4,6 @@ import { GraphQLEmailAddress } from 'graphql-scalars' import { t } from '@lingui/macro' import { inviteUserToOrgUnion } from '../unions' -import { logActivity } from '../../audit-logs/mutations/log-activity' import { InvitationRoleEnums } from '../../enums' import ac from '../../access-control' @@ -39,11 +38,9 @@ able to sign-up and be assigned to that organization in one mutation.`, args, { i18n, - query, request, - collections, - transaction, userKey, + dataSources: { affiliation: affiliationDataSource, auditLogs: auditLogsDataSource }, request: { ip }, auth: { checkPermission, tokenize, userRequired, verifiedRequired, tfaRequired }, loaders: { loadOrgByKey, loadUserByUserName, loadOrganizationNamesById }, @@ -143,10 +140,7 @@ able to sign-up and be assigned to that organization in one mutation.`, }) console.info(`User: ${userKey} successfully invited user: ${userName} to the service, and org: ${org.slug}.`) - await logActivity({ - transaction, - collections, - query, + await auditLogsDataSource.logActivity({ initiatedBy: { id: user._key, userName: user.userName, @@ -172,14 +166,9 @@ able to sign-up and be assigned to that organization in one mutation.`, } // If account is found, check if already affiliated with org - let affiliationCursor + let affiliation try { - affiliationCursor = await query` - WITH affiliations, organizations, users - FOR v, e IN 1..1 INBOUND ${requestedUser._id} affiliations - FILTER e._from == ${org._id} - RETURN e - ` + affiliation = await affiliationDataSource.affiliationByOrgAndUser({ orgId: org._id, userId: requestedUser._id }) } catch (err) { console.error( `Database error occurred when user: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug}, error: ${err}`, @@ -191,7 +180,7 @@ able to sign-up and be assigned to that organization in one mutation.`, } } - if (affiliationCursor.count > 0) { + if (typeof affiliation !== 'undefined') { // If affiliation is found, return error console.warn( `User: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug} however they are already affiliated with that org.`, @@ -205,27 +194,18 @@ able to sign-up and be assigned to that organization in one mutation.`, // User is not affiliated with org, create affiliation - // Setup Transaction - const trx = await transaction(collections) - - // Create affiliation try { - await trx.step( - () => - query` - WITH affiliations, organizations, users - INSERT { - _from: ${org._id}, - _to: ${requestedUser._id}, - permission: ${requestedRole}, - } INTO affiliations - `, - ) + await affiliationDataSource.createAffiliation({ orgId: org._id, userId: requestedUser._id, permission: requestedRole }) } catch (err) { - console.error( - `Transaction step error occurred while user: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug}, error: ${err}`, - ) - await trx.abort() + if (err.affiliationDataSourceOp === 'trx-commit') { + console.error( + `Transaction commit error occurred while user: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug}, error: ${err}`, + ) + } else { + console.error( + `Transaction step error occurred while user: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug}, error: ${err}`, + ) + } return { _type: 'error', code: 500, @@ -239,26 +219,8 @@ able to sign-up and be assigned to that organization in one mutation.`, orgNameFR: orgNames.orgNameFR, }) - // Commit affiliation - try { - await trx.commit() - } catch (err) { - console.error( - `Transaction commit error occurred while user: ${userKey} attempted to invite user: ${requestedUser._key} to org: ${org.slug}, error: ${err}`, - ) - await trx.abort() - return { - _type: 'error', - code: 500, - description: i18n._(t`Unable to invite user. Please try again.`), - } - } - console.info(`User: ${userKey} successfully invited user: ${requestedUser._key} to the org: ${org.slug}.`) - await logActivity({ - transaction, - collections, - query, + await auditLogsDataSource.logActivity({ initiatedBy: { id: user._key, userName: user.userName, diff --git a/api/src/affiliation/mutations/leave-organization.js b/api/src/affiliation/mutations/leave-organization.js index ce8fc78066..152854afef 100644 --- a/api/src/affiliation/mutations/leave-organization.js +++ b/api/src/affiliation/mutations/leave-organization.js @@ -24,9 +24,7 @@ export const leaveOrganization = new mutationWithClientMutationId({ args, { i18n, - query, - collections, - transaction, + dataSources: { affiliation: affiliationDataSource }, auth: { userRequired, verifiedRequired }, loaders: { loadOrgByKey }, validators: { cleanseInput }, @@ -49,33 +47,18 @@ export const leaveOrganization = new mutationWithClientMutationId({ } } - // Setup Trans action - const trx = await transaction(collections) - try { - await trx.step( - () => - query` - WITH affiliations, organizations, users - FOR v, e IN 1..1 OUTBOUND ${org._id} affiliations - FILTER e._to == ${user._id} - REMOVE { _key: e._key } IN affiliations - OPTIONS { waitForSync: true } - `, - ) + await affiliationDataSource.removeAffiliation({ orgId: org._id, userId: user._id }) } catch (err) { - console.error( - `Trx step error occurred when removing user: ${user._key} affiliation with org: ${org._key}: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable leave organization. Please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred when user: ${user._key} attempted to leave org: ${org._key}: ${err}`) - await trx.abort() + if (err.affiliationDataSourceOp === 'trx-step') { + console.error( + `Trx step error occurred when removing user: ${user._key} affiliation with org: ${org._key}: ${err}`, + ) + } else if (err.affiliationDataSourceOp === 'trx-commit') { + console.error( + `Trx commit error occurred when user: ${user._key} attempted to leave org: ${org._key}: ${err}`, + ) + } throw new Error(i18n._(t`Unable leave organization. Please try again.`)) } diff --git a/api/src/affiliation/mutations/remove-user-from-org.js b/api/src/affiliation/mutations/remove-user-from-org.js index 88a780090b..909399f5be 100644 --- a/api/src/affiliation/mutations/remove-user-from-org.js +++ b/api/src/affiliation/mutations/remove-user-from-org.js @@ -3,7 +3,6 @@ import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay' import { t } from '@lingui/macro' import { removeUserFromOrgUnion } from '../unions' -import { logActivity } from '../../audit-logs/mutations/log-activity' import ac from '../../access-control' export const removeUserFromOrg = new mutationWithClientMutationId({ @@ -31,10 +30,8 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ args, { i18n, - query, - collections, - transaction, userKey, + dataSources: { affiliation: affiliationDataSource, auditLogs: auditLogsDataSource }, request: { ip }, auth: { checkPermission, userRequired, verifiedRequired, tfaRequired }, loaders: { loadOrgByKey, loadUserByKey }, @@ -81,22 +78,23 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ } // Get requested users current permission level - let affiliationCursor + let affiliation try { - affiliationCursor = await query` - WITH affiliations, organizations, users - FOR v, e IN 1..1 ANY ${requestedUser._id} affiliations - FILTER e._from == ${requestedOrg._id} - RETURN { _key: e._key, permission: e.permission } - ` + affiliation = await affiliationDataSource.affiliationByOrgAndUser({ orgId: requestedOrg._id, userId: requestedUser._id }) } catch (err) { - 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}`, - ) + if (err.affiliationDataSourceOp === 'query') { + 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}`, + ) + } else if (err.affiliationDataSourceOp === 'cursor') { + 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.`)) } - if (affiliationCursor.count < 1) { + if (typeof affiliation === 'undefined') { console.warn( `User: ${userKey} attempted to remove user: ${requestedUser._key}, but they do not have any affiliations to org: ${requestedOrg._key}.`, ) @@ -107,16 +105,6 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ } } - let affiliation - try { - affiliation = await affiliationCursor.next() - } catch (err) { - 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.`)) - } - // Only admins, owners, and super admins can remove users if (!ac.can(permission).deleteOwn('affiliation').granted) { console.warn( @@ -141,44 +129,23 @@ export const removeUserFromOrg = new mutationWithClientMutationId({ } } - // Setup Transaction - const trx = await transaction(collections) - - try { - await trx.step( - () => - query` - WITH affiliations, organizations, users - FOR aff IN affiliations - FILTER aff._from == ${requestedOrg._id} - FILTER aff._to == ${requestedUser._id} - REMOVE aff IN affiliations - RETURN true - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`)) - } - try { - await trx.commit() + await affiliationDataSource.removeAffiliation({ orgId: requestedOrg._id, userId: requestedUser._id }) } catch (err) { - console.error( - `Trx commit error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`, - ) - await trx.abort() + if (err.affiliationDataSourceOp === 'trx-step') { + console.error( + `Trx step error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`, + ) + } else if (err.affiliationDataSourceOp === 'trx-commit') { + console.error( + `Trx commit error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`, + ) + } throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`)) } console.info(`User: ${userKey} successfully removed user: ${requestedUser._key} from org: ${requestedOrg._key}.`) - await logActivity({ - transaction, - collections, - query, + await auditLogsDataSource.logActivity({ initiatedBy: { id: user._key, userName: user.userName, diff --git a/api/src/affiliation/mutations/request-org-affiliation.js b/api/src/affiliation/mutations/request-org-affiliation.js index c38b6b8161..e4564aefb1 100644 --- a/api/src/affiliation/mutations/request-org-affiliation.js +++ b/api/src/affiliation/mutations/request-org-affiliation.js @@ -3,7 +3,6 @@ import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay' import { t } from '@lingui/macro' import { inviteUserToOrgUnion } from '../unions' -import { logActivity } from '../../audit-logs/mutations/log-activity' const { SERVICE_ACCOUNT_EMAIL } = process.env @@ -28,11 +27,9 @@ export const requestOrgAffiliation = new mutationWithClientMutationId({ args, { i18n, - query, request, - collections, - transaction, userKey, + dataSources: { affiliation: affiliationDataSource, auditLogs: auditLogsDataSource }, request: { ip }, auth: { userRequired, verifiedRequired }, loaders: { loadOrgByKey, loadUserByKey, loadOrganizationNamesById }, @@ -61,13 +58,9 @@ export const requestOrgAffiliation = new mutationWithClientMutationId({ } // Check to see if user is already a member of the org - let affiliationCursor + let requestedAffiliation try { - affiliationCursor = await query` - FOR v, e IN 1..1 OUTBOUND ${org._id} affiliations - FILTER e._to == ${user._id} - RETURN e - ` + requestedAffiliation = await affiliationDataSource.affiliationByOrgAndUser({ orgId: org._id, userId: user._id }) } catch (err) { console.error( `Database error occurred when user: ${userKey} attempted to request invite to ${orgId}, error: ${err}`, @@ -75,8 +68,7 @@ export const requestOrgAffiliation = new mutationWithClientMutationId({ throw new Error(i18n._(t`Unable to request invite. Please try again.`)) } - if (affiliationCursor.count > 0) { - const requestedAffiliation = await affiliationCursor.next() + if (typeof requestedAffiliation !== 'undefined') { if (requestedAffiliation.permission === 'pending') { console.warn( `User: ${userKey} attempted to request invite to org: ${orgId} however they have already requested to join that org.`, @@ -100,55 +92,36 @@ export const requestOrgAffiliation = new mutationWithClientMutationId({ } } - // Setup Transaction - const trx = await transaction(collections) - // Create pending affiliation try { - await trx.step( - () => - query` - WITH affiliations, organizations, users - INSERT { - _from: ${org._id}, - _to: ${user._id}, - permission: "pending", - } INTO affiliations - `, - ) + await affiliationDataSource.createPendingAffiliation({ orgId: org._id, userId: user._id }) } catch (err) { - console.error( - `Transaction step error occurred while user: ${userKey} attempted to request invite to org: ${org.slug}, error: ${err}`, - ) - await trx.abort() + if (err.affiliationDataSourceOp === 'trx-commit') { + console.error( + `Transaction commit error occurred while user: ${userKey} attempted to request invite to org: ${org.slug}, error: ${err}`, + ) + } else { + 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 IN ["admin", "owner", "super_admin"] - RETURN v._key - ` - } catch (err) { - console.error( - `Database error occurred when user: ${userKey} attempted to request invite to ${orgId}, error: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to request invite. Please try again.`)) - } - let orgAdmins try { - orgAdmins = await orgAdminsCursor.all() + orgAdmins = await affiliationDataSource.orgAdminUserKeys({ orgId: org._id }) } catch (err) { - console.error( - `Cursor error occurred when user: ${userKey} attempted to request invite to ${orgId}, error: ${err}`, - ) - await trx.abort() + if (err.affiliationDataSourceOp === 'cursor') { + console.error( + `Cursor error occurred when user: ${userKey} attempted to request invite to ${orgId}, error: ${err}`, + ) + } else { + 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.`)) } @@ -163,7 +136,6 @@ export const requestOrgAffiliation = new mutationWithClientMutationId({ console.error( `Error occurred when user: ${userKey} attempted to request invite to org: ${org._key}. Error while retrieving organization names. error: ${err}`, ) - await trx.abort() throw new Error(i18n._(t`Unable to request invite. Please try again.`)) } const adminLink = `https://${request.get('host')}/admin/organizations` @@ -187,22 +159,8 @@ export const requestOrgAffiliation = new mutationWithClientMutationId({ } } - // 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}`, - ) - await trx.abort() - 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, + await auditLogsDataSource.logActivity({ initiatedBy: { id: user._key, userName: user.userName, diff --git a/api/src/affiliation/mutations/transfer-org-ownership.js b/api/src/affiliation/mutations/transfer-org-ownership.js index 3f990982bc..d4ae317a38 100644 --- a/api/src/affiliation/mutations/transfer-org-ownership.js +++ b/api/src/affiliation/mutations/transfer-org-ownership.js @@ -29,9 +29,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ args, { i18n, - query, - collections, - transaction, + dataSources: { affiliation: affiliationDataSource }, auth: { checkOrgOwner, userRequired, verifiedRequired }, loaders: { loadOrgByKey, loadUserByKey }, validators: { cleanseInput }, @@ -91,14 +89,12 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ } // query db for requested user affiliation to org - let affiliationCursor + let requestedUserAffiliation try { - affiliationCursor = await query` - WITH affiliations, organizations, users - FOR v, e IN 1..1 OUTBOUND ${org._id} affiliations - FILTER e._to == ${requestedUser._id} - RETURN e - ` + requestedUserAffiliation = await affiliationDataSource.affiliationByOrgAndUser({ + orgId: org._id, + userId: requestedUser._id, + }) } catch (err) { console.error( `Database error occurred for user: ${requestingUser._key} when they were attempting to transfer org: ${org.slug} ownership to user: ${requestedUser._key}: ${err}`, @@ -107,7 +103,7 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ } // check to see if requested user belongs to org - if (affiliationCursor.count < 1) { + if (typeof requestedUserAffiliation === 'undefined') { console.warn( `User: ${requestingUser._key} attempted to transfer org: ${org.slug} ownership to user: ${requestedUser._key} but they are not affiliated with the org.`, ) @@ -120,63 +116,22 @@ export const transferOrgOwnership = new mutationWithClientMutationId({ } } - // Setup Trans action - const trx = await transaction(collections) - - // remove current org owners role - try { - await trx.step( - () => - query` - WITH affiliations, organizations, users - FOR aff IN affiliations - FILTER aff._from == ${org._id} - FILTER aff._to == ${requestingUser._id} - UPDATE { _key: aff._key } WITH { - permission: "admin", - } IN affiliations - RETURN aff - `, - ) - } catch (err) { - console.error( - `Trx step error occurred for user: ${requestingUser._key} when they were attempting to transfer org: ${org.slug} ownership to user: ${requestedUser._key}: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to transfer organization ownership. Please try again.`)) - } - - // set new org owner - try { - await trx.step( - () => - query` - WITH affiliations, organizations, users - FOR aff IN affiliations - FILTER aff._from == ${org._id} - FILTER aff._to == ${requestedUser._id} - UPDATE { _key: aff._key } WITH { - permission: "owner", - } IN affiliations - RETURN aff - `, - ) - } catch (err) { - console.error( - `Trx step error occurred for user: ${requestingUser._key} when they were attempting to transfer org: ${org.slug} ownership to user: ${requestedUser._key}: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to transfer organization ownership. Please try again.`)) - } - - // commit changes to the db try { - await trx.commit() + await affiliationDataSource.transferOrgOwnership({ + orgId: org._id, + fromUserId: requestingUser._id, + toUserId: requestedUser._id, + }) } catch (err) { - console.error( - `Trx commit error occurred for user: ${requestingUser._key} when they were attempting to transfer org: ${org.slug} ownership to user: ${requestedUser._key}: ${err}`, - ) - await trx.abort() + if (err.affiliationDataSourceOp === 'trx-step') { + console.error( + `Trx step error occurred for user: ${requestingUser._key} when they were attempting to transfer org: ${org.slug} ownership to user: ${requestedUser._key}: ${err}`, + ) + } else if (err.affiliationDataSourceOp === 'trx-commit') { + console.error( + `Trx commit error occurred for user: ${requestingUser._key} when they were attempting to transfer org: ${org.slug} ownership to user: ${requestedUser._key}: ${err}`, + ) + } throw new Error(i18n._(t`Unable to transfer organization ownership. Please try again.`)) } diff --git a/api/src/affiliation/mutations/update-user-role.js b/api/src/affiliation/mutations/update-user-role.js index 1f92a58a5b..1660b990f7 100644 --- a/api/src/affiliation/mutations/update-user-role.js +++ b/api/src/affiliation/mutations/update-user-role.js @@ -5,7 +5,6 @@ import { t } from '@lingui/macro' import { RoleEnums } from '../../enums' import { updateUserRoleUnion } from '../unions' -import { logActivity } from '../../audit-logs/mutations/log-activity' import ac from '../../access-control' export const updateUserRole = new mutationWithClientMutationId({ @@ -38,10 +37,8 @@ given organization.`, args, { i18n, - query, - collections, - transaction, userKey, + dataSources: { affiliation: affiliationDataSource, auditLogs: auditLogsDataSource }, request: { ip }, auth: { checkPermission, userRequired, verifiedRequired, tfaRequired }, loaders: { loadOrgByKey, loadUserByUserName, loadOrganizationNamesById }, @@ -114,23 +111,24 @@ given organization.`, } // Get user's current permission level - let affiliationCursor + let affiliation try { - affiliationCursor = await query` - WITH affiliations, organizations, users - FOR v, e IN 1..1 ANY ${requestedUser._id} affiliations - FILTER e._from == ${org._id} - RETURN { _key: e._key, permission: e.permission } - ` + affiliation = await affiliationDataSource.affiliationByOrgAndUser({ orgId: org._id, userId: requestedUser._id }) } catch (err) { - console.error( - `Database error occurred when user: ${userKey} attempted to update a user's: ${requestedUser._key} role, error: ${err}`, - ) + if (err.affiliationDataSourceOp === 'query') { + console.error( + `Database error occurred when user: ${userKey} attempted to update a user's: ${requestedUser._key} role, error: ${err}`, + ) + } else if (err.affiliationDataSourceOp === 'cursor') { + 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.`)) } - if (affiliationCursor.count < 1) { + if (typeof affiliation === 'undefined') { console.warn( `User: ${userKey} attempted to update a user: ${requestedUser._key} role in org: ${org.slug}, however that user does not have an affiliation with that organization.`, ) @@ -141,17 +139,6 @@ given organization.`, } } - let affiliation - try { - affiliation = await affiliationCursor.next() - } catch (err) { - 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.`)) - } - // Only super admins can update or assign privileged roles (those with org-level authority) const privilegedRoles = ac.getRoles().filter((r) => ac.can(r).deleteOwn('organization').granted) if ( @@ -168,41 +155,23 @@ given organization.`, } } - // Only super admins can create new super admins - const edge = { - _from: org._id, - _to: requestedUser._id, - permission: role, - } - - // Setup Transaction - const trx = await transaction(collections) - try { - await trx.step(async () => { - await query` - WITH affiliations, organizations, users - UPSERT { _key: ${affiliation._key} } - INSERT ${edge} - UPDATE ${edge} - IN affiliations - ` + await affiliationDataSource.updateAffiliationPermission({ + affiliationKey: affiliation._key, + orgId: org._id, + userId: requestedUser._id, + permission: role, }) } catch (err) { - console.error( - `Transaction step error occurred when user: ${userKey} attempted to update a user's: ${requestedUser._key} role, error: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to update user's role. Please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.warn( - `Transaction commit error occurred when user: ${userKey} attempted to update a user's: ${requestedUser._key} role, error: ${err}`, - ) - await trx.abort() + if (err.affiliationDataSourceOp === 'trx-step') { + console.error( + `Transaction step error occurred when user: ${userKey} attempted to update a user's: ${requestedUser._key} role, error: ${err}`, + ) + } else if (err.affiliationDataSourceOp === 'trx-commit') { + 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.`)) } @@ -220,10 +189,7 @@ given organization.`, await sendRoleChangeEmail({ user: requestedUser, newRole: role, oldRole: affiliation.permission, orgNames }) console.info(`User: ${userKey} successful updated user: ${requestedUser._key} role to ${role} in org: ${org.slug}.`) - await logActivity({ - transaction, - collections, - query, + await auditLogsDataSource.logActivity({ initiatedBy: { id: user._key, userName: user.userName, diff --git a/api/src/create-context.js b/api/src/create-context.js index 5d95d7ebe9..7d9a704807 100644 --- a/api/src/create-context.js +++ b/api/src/create-context.js @@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid' import jwt from 'jsonwebtoken' import { loadUserByKey } from './user/loaders' +import { UserDataSource } from './user' import { cleanseInput, decryptPhoneNumber, slugify } from './validators' import { initializeLoaders } from './initialize-loaders' import { SummariesDataSource } from './summaries' @@ -16,6 +17,7 @@ import { GuidanceTagDataSource } from './guidance-tag' import { OrganizationDataSource } from './organization' import { TagsDataSource } from './tags' import { DomainDataSource } from './domain' +import { AffiliationDataSource } from './affiliation' import { AuthDataSource, checkDomainOwnership, @@ -172,6 +174,24 @@ export async function createContext({ transaction, collections, }), + affiliation: new AffiliationDataSource({ + query, + userKey, + i18n, + language: request.language, + cleanseInput, + transaction, + collections, + }), + user: new UserDataSource({ + query, + userKey, + i18n, + language: request.language, + cleanseInput, + transaction, + collections, + }), }, loaders: initializeLoaders({ query, diff --git a/api/src/initialize-loaders.js b/api/src/initialize-loaders.js index 5e08e47969..f7176463d5 100644 --- a/api/src/initialize-loaders.js +++ b/api/src/initialize-loaders.js @@ -1,8 +1,3 @@ -import { - loadAffiliationByKey, - loadAffiliationConnectionsByUserId, - loadAffiliationConnectionsByOrgId, -} from './affiliation/loaders' import { loadDkimFailConnectionsBySumId, loadDmarcFailConnectionsBySumId, @@ -16,7 +11,6 @@ import { loadAllVerifiedRuaDomains, } from './dmarc-summaries/loaders' import { loadOrgByKey, loadOrganizationNamesById } from './organization/loaders' -import { loadMyTrackerByUserId, loadUserByUserName, loadUserByKey, loadUserConnectionsByUserId } from './user/loaders' import { loadVerifiedDomainsById, loadVerifiedDomainByKey, @@ -82,34 +76,6 @@ export function initializeLoaders({ query, userKey, i18n, language, cleanseInput loadDmarcYearlySumEdge: loadDmarcYearlySumEdge({ query, userKey, i18n }), loadOrgByKey: loadOrgByKey({ query, language, userKey, i18n }), loadOrganizationNamesById: loadOrganizationNamesById({ query, userKey, i18n }), - loadMyTrackerByUserId: loadMyTrackerByUserId({ - query, - language, - userKey, - i18n, - }), - loadUserByUserName: loadUserByUserName({ query, userKey, i18n }), - loadUserConnectionsByUserId: loadUserConnectionsByUserId({ - query, - userKey, - cleanseInput, - i18n, - }), - loadUserByKey: loadUserByKey({ query, userKey, i18n }), - loadAffiliationByKey: loadAffiliationByKey({ query, userKey, i18n }), - loadAffiliationConnectionsByUserId: loadAffiliationConnectionsByUserId({ - query, - language, - userKey, - cleanseInput, - i18n, - }), - loadAffiliationConnectionsByOrgId: loadAffiliationConnectionsByOrgId({ - query, - userKey, - cleanseInput, - i18n, - }), loadVerifiedDomainsById: loadVerifiedDomainsById({ query, i18n }), loadVerifiedDomainByKey: loadVerifiedDomainByKey({ query, i18n }), loadVerifiedDomainConnections: loadVerifiedDomainConnections({ diff --git a/api/src/organization/objects/__tests__/organization.test.js b/api/src/organization/objects/__tests__/organization.test.js index 04d4ab92d0..275ec4cf11 100644 --- a/api/src/organization/objects/__tests__/organization.test.js +++ b/api/src/organization/objects/__tests__/organization.test.js @@ -292,9 +292,7 @@ describe('given the organization object', () => { auth: { loginRequiredBool: true }, dataSources: { auth: { permissionByOrgId: { load: jest.fn().mockResolvedValue('admin') } }, - }, - loaders: { - loadAffiliationConnectionsByOrgId: jest.fn().mockReturnValue(expectedResults), + affiliation: { connectionsByOrgId: jest.fn().mockReturnValue(expectedResults) }, }, }, ), @@ -329,8 +327,8 @@ describe('given the organization object', () => { auth: { loginRequiredBool: true }, dataSources: { auth: { permissionByOrgId: { load: jest.fn().mockResolvedValue('user') } }, + affiliation: { connectionsByOrgId: jest.fn() }, }, - loaders: { loadAffiliationConnectionsByOrgId: jest.fn() }, }, ) } catch (err) { @@ -367,8 +365,8 @@ describe('given the organization object', () => { auth: { loginRequiredBool: true }, dataSources: { auth: { permissionByOrgId: { load: jest.fn().mockResolvedValue('user') } }, + affiliation: { connectionsByOrgId: jest.fn() }, }, - loaders: { loadAffiliationConnectionsByOrgId: jest.fn() }, }, ) } catch (err) { diff --git a/api/src/organization/objects/organization.js b/api/src/organization/objects/organization.js index 81f3da244c..cfbe7a3a1d 100644 --- a/api/src/organization/objects/organization.js +++ b/api/src/organization/objects/organization.js @@ -356,8 +356,7 @@ export const organizationType = new GraphQLObjectType({ { i18n, auth: { loginRequiredBool }, - dataSources: { auth: authDS }, - loaders: { loadAffiliationConnectionsByOrgId }, + dataSources: { auth: authDS, affiliation }, }, ) => { const permission = await authDS.permissionByOrgId.load(_id) @@ -365,7 +364,7 @@ export const organizationType = new GraphQLObjectType({ throw new Error(i18n._(t`Cannot query affiliations on organization without admin permission or higher.`)) } - const affiliations = await loadAffiliationConnectionsByOrgId({ + const affiliations = await affiliation.connectionsByOrgId({ orgId: _id, ...args, }) diff --git a/api/src/organization/queries/__tests__/find-organization-by-slug.test.js b/api/src/organization/queries/__tests__/find-organization-by-slug.test.js index cfb39fda7a..95e441eecf 100644 --- a/api/src/organization/queries/__tests__/find-organization-by-slug.test.js +++ b/api/src/organization/queries/__tests__/find-organization-by-slug.test.js @@ -10,7 +10,6 @@ import { createQuerySchema } from '../../../query' import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { checkPermission, userRequired, verifiedRequired } from '../../../auth' -import { loadAffiliationConnectionsByOrgId } from '../../../affiliation/loaders' import { loadDomainConnectionsByOrgId } from '../../../domain/loaders' import { loadUserByKey } from '../../../user/loaders' import { loadOrgBySlug, loadOrgByKey } from '../../loaders' @@ -174,13 +173,6 @@ describe('given findOrganizationBySlugQuery', () => { auth: { loginRequiredBool: true }, i18n, }), - loadAffiliationConnectionsByOrgId: loadAffiliationConnectionsByOrgId({ - query, - userKey: user._key, - cleanseInput, - auth: { loginRequiredBool: true }, - i18n, - }), }, }, }) @@ -284,12 +276,6 @@ describe('given findOrganizationBySlugQuery', () => { auth: { loginRequiredBool: true }, i18n, }), - loadAffiliationConnectionsByOrgId: loadAffiliationConnectionsByOrgId({ - query, - userKey: user._key, - cleanseInput, - i18n, - }), }, }, }) diff --git a/api/src/user/data-source.js b/api/src/user/data-source.js new file mode 100644 index 0000000000..6c87f7b482 --- /dev/null +++ b/api/src/user/data-source.js @@ -0,0 +1,668 @@ +import { t } from '@lingui/macro' + +import { + loadMyTrackerByUserId, + loadUserByKey, + loadUserByUserName, + loadUserConnectionsByUserId, +} from './loaders' + +export class UserDataSource { + constructor({ query, userKey, i18n, language, cleanseInput, transaction, collections }) { + this._query = query + this._userKey = userKey + this._i18n = i18n + this._transaction = transaction + this._collections = collections + this.byKey = loadUserByKey({ query, userKey, i18n }) + this.byUserName = loadUserByUserName({ query, userKey, i18n }) + this.myTrackerByUserId = loadMyTrackerByUserId({ query, userKey, i18n, language }) + this.connectionsByUserId = loadUserConnectionsByUserId({ query, userKey, cleanseInput, i18n }) + } + + async createTransaction() { + return this._transaction(this._collections) + } + + async isAdminForAnyOrg({ userId }) { + let userAdmin + try { + userAdmin = await this._query` + WITH users, affiliations + FOR v, e IN 1..1 INBOUND ${userId} affiliations + FILTER e.permission IN ["admin", "owner", "super_admin"] + LIMIT 1 + RETURN e.permission + ` + } catch (err) { + console.error(`Database error occurred when user: ${this._userKey} was seeing if they were an admin, err: ${err}`) + throw new Error(this._i18n._(t`Unable to verify if user is an admin, please try again.`)) + } + + return userAdmin.count > 0 + } + + async isSuperAdmin({ userId }) { + let userAdmin + try { + userAdmin = await this._query` + WITH users, affiliations + FOR v, e IN 1..1 INBOUND ${userId} affiliations + FILTER e.permission == "super_admin" + LIMIT 1 + RETURN e.permission + ` + } catch (err) { + console.error( + `Database error occurred when user: ${this._userKey} was seeing if they were a super admin, err: ${err}`, + ) + throw new Error(this._i18n._(t`Unable to verify if user is a super admin, please try again.`)) + } + + return userAdmin.count > 0 + } + + async closeAccount({ userId }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH affiliations, organizations, users + FOR v, e IN 1..1 INBOUND ${userId} affiliations + REMOVE { _key: e._key } IN affiliations + OPTIONS { waitForSync: true } + `, + ) + } catch (err) { + console.error( + `Trx step error occurred when removing users remaining affiliations when user: ${this._userKey} attempted to close account: ${userId}: ${err}`, + ) + await trx.abort() + throw new Error(this._i18n._(t`Unable to close account. Please try again.`)) + } + + try { + await trx.step( + () => this._query` + WITH users + REMOVE PARSE_IDENTIFIER(${userId}).key + IN users OPTIONS { waitForSync: true } + `, + ) + } catch (err) { + console.error( + `Trx step error occurred when removing user: ${this._userKey} attempted to close account: ${userId}: ${err}`, + ) + await trx.abort() + throw new Error(this._i18n._(t`Unable to close account. Please try again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred when user: ${this._userKey} attempted to close account: ${userId}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to close account. Please try again.`)) + } + } + + async authenticateSuccess({ userKey, refreshInfo, loginDate, verifyEmail = false }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT { + tfaCode: null, + refreshInfo: ${refreshInfo}, + lastLogin: ${loginDate} + } + UPDATE { + tfaCode: null, + refreshInfo: ${refreshInfo}, + lastLogin: ${loginDate} + } + IN users + `, + ) + } catch (err) { + console.error( + `Trx step error occurred when clearing tfa code and setting refresh id for user: ${userKey} during authentication: ${err}`, + ) + await trx.abort() + throw new Error(this._i18n._(t`Unable to authenticate. Please try again.`)) + } + + if (verifyEmail) { + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT { + emailValidated: true, + } + UPDATE { + emailValidated: true, + } + IN users + `, + ) + } catch (err) { + console.error( + `Trx step error occurred when setting email validated to true for user: ${userKey} during authentication: ${err}`, + ) + await trx.abort() + throw new Error(this._i18n._(t`Unable to authenticate. Please try again.`)) + } + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred while user: ${userKey} attempted to authenticate: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to authenticate. Please try again.`)) + } + } + + async clearTfaCode({ userKey }) { + const trx = await this._transaction(this._collections) + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT { + tfaCode: null, + } + UPDATE { + tfaCode: null, + } + IN users + `, + ) + } catch (err) { + console.error( + `Trx step error occurred when clearing tfa code on attempt timeout for user: ${userKey} during authentication: ${err}`, + ) + await trx.abort() + throw new Error(this._i18n._(t`Incorrect TFA code. Please sign in again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred while user: ${userKey} attempted to authenticate: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Incorrect TFA code. Please sign in again.`)) + } + } + + async updateRefreshInfo({ userKey, refreshInfo }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT { refreshInfo: ${refreshInfo} } + UPDATE { refreshInfo: ${refreshInfo} } + IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred when attempting to refresh tokens for user: ${userKey}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to refresh tokens, please sign in.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred while user: ${userKey} attempted to refresh tokens: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to refresh tokens, please sign in.`)) + } + } + + async removePhoneNumber({ userKey, tfaSendMethod }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT { + phoneDetails: null, + phoneValidated: false, + tfaSendMethod: ${tfaSendMethod} + } + UPDATE { + phoneDetails: null, + phoneValidated: false, + tfaSendMethod: ${tfaSendMethod} + } + IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred well removing phone number for user: ${userKey}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to remove phone number. Please try again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred well removing phone number for user: ${userKey}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to remove phone number. Please try again.`)) + } + } + + async resetPassword({ userKey, hashedPassword }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH users + FOR user IN users + UPDATE ${userKey} + WITH { + password: ${hashedPassword}, + failedLoginAttempts: 0 + } IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred when user: ${userKey} attempted to reset their password: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to reset password. Please try again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred while user: ${userKey} attempted to authenticate: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to reset password. Please try again.`)) + } + } + + async setPhoneNumber({ userKey, tfaCode, phoneDetails, tfaSendMethod }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT { + tfaCode: ${tfaCode}, + phoneDetails: ${phoneDetails}, + phoneValidated: false, + tfaSendMethod: ${tfaSendMethod} + } + UPDATE { + tfaCode: ${tfaCode}, + phoneDetails: ${phoneDetails}, + phoneValidated: false, + tfaSendMethod: ${tfaSendMethod} + } + IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred for user: ${userKey} when upserting phone number information: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to set phone number, please try again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred for user: ${userKey} when upserting phone number information: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to set phone number, please try again.`)) + } + } + + async signInResetFailedLoginAttempts({ userKey, trx }) { + try { + await trx.step( + () => this._query` + WITH users + FOR u IN users + UPDATE ${userKey} WITH { failedLoginAttempts: 0 } IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred when resetting failed login attempts for user: ${userKey}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to sign in, please try again.`)) + } + } + + async signInSetTfaCodeAndRefreshInfo({ userKey, tfaCode, refreshInfo, trx }) { + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT { + tfaCode: ${tfaCode}, + refreshInfo: ${refreshInfo} + } + UPDATE { + tfaCode: ${tfaCode}, + refreshInfo: ${refreshInfo} + } + IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred when inserting TFA code for user: ${userKey}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to sign in, please try again.`)) + } + } + + async signInSetRefreshInfoAndLastLogin({ userKey, refreshInfo, loginDate, trx }) { + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT { refreshInfo: ${refreshInfo}, lastLogin: ${loginDate} } + UPDATE { refreshInfo: ${refreshInfo}, lastLogin: ${loginDate} } + IN users + `, + ) + } catch (err) { + console.error( + `Trx step error occurred when attempting to setting refresh tokens for user: ${userKey} during sign in: ${err}`, + ) + await trx.abort() + throw new Error(this._i18n._(t`Unable to sign in, please try again.`)) + } + } + + async signInIncrementFailedLoginAttempts({ userKey, failedLoginAttempts }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH users + FOR u IN users + UPDATE ${userKey} WITH { + failedLoginAttempts: ${failedLoginAttempts} + } IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred when incrementing failed login attempts for user: ${userKey}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to sign in, please try again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred while user: ${userKey} failed to sign in: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to sign in, please try again.`)) + } + } + + async commitSignInTransaction({ trx, userKey, type }) { + try { + await trx.commit() + } catch (err) { + const action = type === 'tfa' ? 'to tfa sign in' : 'a regular sign in' + console.error(`Trx commit error occurred while user: ${userKey} attempted ${action}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to sign in, please try again.`)) + } + } + + async insertUser({ user, userName }) { + const trx = await this._transaction(this._collections) + + let insertedUserCursor + try { + insertedUserCursor = await trx.step( + () => this._query` + WITH users + INSERT ${user} INTO users + RETURN MERGE( + { + id: NEW._key, + _type: "user" + }, + NEW + ) + `, + ) + } catch (err) { + console.error(`Transaction step error occurred while user: ${userName} attempted to sign up, creating user: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to sign up. Please try again.`)) + } + + let insertedUser + try { + insertedUser = await insertedUserCursor.next() + } catch (err) { + console.error(`Cursor error occurred while user: ${userName} attempted to sign up, creating user: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to sign up. Please try again.`)) + } + + return { trx, insertedUser } + } + + async addAffiliation({ trx, orgId, userId, permission, userName }) { + try { + await trx.step( + () => this._query` + WITH affiliations, organizations, users + INSERT { + _from: ${orgId}, + _to: ${userId}, + permission: ${permission}, + } INTO affiliations + `, + ) + } catch (err) { + console.error( + `Transaction step error occurred while user: ${userName} attempted to sign up, assigning affiliation: ${err}`, + ) + await trx.abort() + throw new Error(this._i18n._(t`Unable to sign up. Please try again.`)) + } + } + + async commitSignUpTransaction({ trx, userName }) { + try { + await trx.commit() + } catch (err) { + console.error(`Transaction commit error occurred while user: ${userName} attempted to sign up: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to sign up. Please try again.`)) + } + } + + async updatePassword({ userKey, hashedPassword }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH users + FOR user IN users + UPDATE ${userKey} WITH { password: ${hashedPassword} } IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred when user: ${userKey} attempted to update their password: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to update password. Please try again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred when user: ${userKey} attempted to update their password: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to update password. Please try again.`)) + } + } + + async updateProfile({ userKey, updatedUser }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT ${updatedUser} + UPDATE ${updatedUser} + IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred when user: ${this._userKey} attempted to update their profile: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to update profile. Please try again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred when user: ${this._userKey} attempted to update their profile: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to update profile. Please try again.`)) + } + } + + async verifyAccount({ userKey, newUserName }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT { + emailValidated: true, + userName: ${newUserName}, + } + UPDATE { + emailValidated: true, + userName: ${newUserName}, + } + IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred when upserting email validation for user: ${userKey}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to verify account. Please try again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred when upserting email validation for user: ${userKey}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to verify account. Please try again.`)) + } + } + + async verifyPhoneNumber({ userKey }) { + const trx = await this._transaction(this._collections) + + try { + await trx.step( + () => this._query` + WITH users + UPSERT { _key: ${userKey} } + INSERT { phoneValidated: true } + UPDATE { phoneValidated: true } + IN users + `, + ) + } catch (err) { + console.error(`Trx step error occurred when upserting the tfaValidate field for ${userKey}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to two factor authenticate. Please try again.`)) + } + + try { + await trx.commit() + } catch (err) { + console.error(`Trx commit error occurred when upserting the tfaValidate field for ${userKey}: ${err}`) + await trx.abort() + throw new Error(this._i18n._(t`Unable to two factor authenticate. Please try again.`)) + } + } + + async completeTour({ userKey, tourId }) { + try { + const completeTourCursor = await this._query` + LET userCompleteTours = FIRST( + FOR user IN users + FILTER user._key == ${userKey} + LIMIT 1 + RETURN user.completedTours + ) + UPDATE { _key: ${userKey} } + WITH { + completedTours: APPEND( + userCompleteTours[* FILTER CURRENT.tourId != ${tourId}], + { tourId: ${tourId}, completedAt: DATE_ISO8601(DATE_NOW()) } + ) + } + IN users + ` + await completeTourCursor.next() + } catch (err) { + console.error(`Database error occurred when user: ${userKey} attempted to complete tour: ${tourId}: ${err}`) + throw new Error(this._i18n._(t`Unable to confirm completion of the tour. Please try again.`)) + } + } + + async dismissMessage({ userKey, messageId }) { + try { + const dismissMessageCursor = await this._query` + LET userDismissedMessages = FIRST( + FOR user IN users + FILTER user._key == ${userKey} + LIMIT 1 + RETURN user.dismissedMessages + ) + UPDATE { _key: ${userKey} } + WITH { + dismissedMessages: APPEND( + userDismissedMessages[* FILTER CURRENT.messageId != ${messageId}], + { messageId: ${messageId}, dismissedAt: DATE_ISO8601(DATE_NOW()) } + ) + } + IN users + ` + await dismissMessageCursor.next() + } catch (err) { + console.error(`Database error occurred when user: ${userKey} attempted to dismiss message: ${messageId}: ${err}`) + throw new Error(this._i18n._(t`Unable to dismiss message. Please try again.`)) + } + } +} diff --git a/api/src/user/index.js b/api/src/user/index.js index ee648b9915..a64799e7d4 100644 --- a/api/src/user/index.js +++ b/api/src/user/index.js @@ -1,4 +1,5 @@ export * from './loaders' +export * from './data-source' export * from './mutations' export * from './objects' export * from './queries' diff --git a/api/src/user/mutations/__tests__/authenticate.test.js b/api/src/user/mutations/__tests__/authenticate.test.js index 94235d3ac7..8db92a498a 100644 --- a/api/src/user/mutations/__tests__/authenticate.test.js +++ b/api/src/user/mutations/__tests__/authenticate.test.js @@ -1,7 +1,7 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' import bcrypt from 'bcryptjs' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { toGlobalId } from 'graphql-relay' import { setupI18n } from '@lingui/core' import { v4 as uuidv4 } from 'uuid' @@ -13,6 +13,7 @@ import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { tokenize, verifyToken } from '../../../auth' import { loadUserByKey } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' import jwt from 'jsonwebtoken' @@ -289,7 +290,7 @@ describe('authenticate user account', () => { data: { authenticate: { result: { - authToken: authToken, + authToken, user: { id: `${toGlobalId('user', user._key)}`, userName: 'test.account@istio.actually.exists', @@ -1130,165 +1131,7 @@ describe('authenticate user account', () => { ]) }) }) - describe('transaction step error occurs', () => { - describe('when clearing tfa code and setting refresh id', () => { - it('throws an error', async () => { - const token = tokenize({ - parameters: { userKey: 123 }, - secret: String(SIGN_IN_KEY), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - authenticate( - input: { - sendMethod: EMAIL - authenticationCode: 123456 - authenticateToken: "${token}" - } - ) { - result { - ... on AuthResult { - authToken - user { - id - userName - displayName - phoneValidated - emailValidated - } - } - ... on AuthenticateError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query, - collections: collectionNames, - transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue(new Error('Transaction step error')), - abort: jest.fn(), - }), - uuidv4, - auth: { - bcrypt, - tokenize, - verifyToken: verifyToken({}), - }, - validators: { - cleanseInput, - }, - loaders: { - loadUserByKey: { - load: jest.fn().mockReturnValue({ - _key: 123, - tfaCode: 123456, - refreshInfo: { - remember: false, - }, - }), - }, - }, - }, - }) - - const error = [new GraphQLError("Impossible de s'authentifier. Veuillez réessayer.")] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when clearing tfa code and setting refresh id for user: 123 during authentication: Error: Transaction step error`, - ]) - }) - }) - }) - describe('transaction commit error occurs', () => { - describe('when user attempts to authenticate', () => { - it('throws an error', async () => { - const token = tokenize({ - parameters: { userKey: 123 }, - secret: String(SIGN_IN_KEY), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - authenticate( - input: { - sendMethod: EMAIL - authenticationCode: 123456 - authenticateToken: "${token}" - } - ) { - result { - ... on AuthResult { - authToken - user { - id - userName - displayName - phoneValidated - emailValidated - } - } - ... on AuthenticateError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query, - collections: collectionNames, - transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn().mockRejectedValue(new Error('Transaction commit error')), - abort: jest.fn(), - }), - uuidv4, - auth: { - bcrypt, - tokenize, - verifyToken: verifyToken({}), - }, - validators: { - cleanseInput, - }, - loaders: { - loadUserByKey: { - load: jest.fn().mockReturnValue({ - _key: 123, - tfaCode: 123456, - refreshInfo: { - remember: false, - }, - }), - }, - }, - }, - }) - - const error = [new GraphQLError("Impossible de s'authentifier. Veuillez réessayer.")] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx commit error occurred while user: 123 attempted to authenticate: Error: Transaction commit error`, - ]) - }) - }) - }) }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/close-account.test.js b/api/src/user/mutations/__tests__/close-account.test.js index 500f06a53e..677ddaa80a 100644 --- a/api/src/user/mutations/__tests__/close-account.test.js +++ b/api/src/user/mutations/__tests__/close-account.test.js @@ -1,7 +1,7 @@ import { setupI18n } from '@lingui/core' import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { toGlobalId } from 'graphql-relay' import englishMessages from '../../../locale/en/messages' @@ -9,6 +9,7 @@ import frenchMessages from '../../../locale/fr/messages' import { checkSuperAdmin, userRequired } from '../../../auth' import { loadOrgByKey } from '../../../organization/loaders' import { loadUserByKey } from '../../../user/loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import { cleanseInput } from '../../../validators' import { createMutationSchema } from '../../../mutation' import { createQuerySchema } from '../../../query' @@ -1343,206 +1344,7 @@ describe('given the closeAccount mutation', () => { }) }) }) - describe('trx step error occurs', () => { - describe('when removing the users affiliations', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{ count: 2 }]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - abort: jest.fn(), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - closeAccountSelf(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - request: { ip: '127.0.0.1' }, - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ _key: '123', _id: 'users/123' }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({ _key: '123' }), - }, - }, - validators: { cleanseInput }, - }, - }) - - const error = [new GraphQLError('Impossible de fermer le compte. Veuillez réessayer.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing users remaining affiliations when user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) - }) - describe('when removing the user', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{ count: 2 }]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValueOnce().mockRejectedValue(new Error('trx step error')), - commit: jest.fn(), - abort: jest.fn(), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - closeAccountSelf(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - request: { ip: '127.0.0.1' }, - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ _key: '123', _id: 'users/123' }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({ _key: '123' }), - }, - }, - validators: { cleanseInput }, - }, - }) - - const error = [new GraphQLError('Impossible de fermer le compte. Veuillez réessayer.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when removing user: 123 attempted to close account: users/123: Error: trx step error`, - ]) - }) - }) - }) - describe('trx commit error occurs', () => { - it('throws an error', async () => { - const mockedCursor = { - all: jest.fn().mockReturnValue([{ count: 2 }]), - } - - const mockedQuery = jest.fn().mockReturnValue(mockedCursor) - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue(), - commit: jest.fn().mockRejectedValue(new Error('trx commit error')), - abort: jest.fn(), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - closeAccountSelf(input: {}) { - result { - ... on CloseAccountResult { - status - } - ... on CloseAccountError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query: mockedQuery, - collections: collectionNames, - transaction: mockedTransaction, - userKey: '123', - request: { ip: '127.0.0.1' }, - auth: { - checkSuperAdmin: jest.fn().mockReturnValue(true), - userRequired: jest.fn().mockReturnValue({ _key: '123', _id: 'users/123' }), - }, - loaders: { - loadOrgByKey: loadOrgByKey({ - query, - language: 'en', - i18n, - userKey: '123', - }), - loadUserByKey: { - load: jest.fn().mockReturnValue({ _key: '123' }), - }, - }, - validators: { cleanseInput }, - }, - }) - - const error = [new GraphQLError('Impossible de fermer le compte. Veuillez réessayer.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx commit error occurred when user: 123 attempted to close account: users/123: Error: trx commit error`, - ]) - }) - }) }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/refresh-tokens.test.js b/api/src/user/mutations/__tests__/refresh-tokens.test.js index c726fdb358..303cf8f325 100644 --- a/api/src/user/mutations/__tests__/refresh-tokens.test.js +++ b/api/src/user/mutations/__tests__/refresh-tokens.test.js @@ -1,6 +1,6 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { setupI18n } from '@lingui/core' import { v4 as uuidv4 } from 'uuid' import jwt from 'jsonwebtoken' @@ -11,6 +11,7 @@ import { createQuerySchema } from '../../../query' import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { loadUserByKey } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import { tokenize } from '../../../auth' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' @@ -255,7 +256,7 @@ describe('refresh users tokens', () => { data: { refreshTokens: { result: { - authToken: authToken, + authToken, user: { displayName: 'Test Account', }, @@ -1239,170 +1240,7 @@ describe('refresh users tokens', () => { }) }) }) - describe('transaction step error occurs', () => { - describe('when upserting new refreshId', () => { - it('throws an error', async () => { - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue(new Error('Transaction step error')), - abort: jest.fn(), - }) - - const refreshToken = tokenize({ - parameters: { userKey: 123, uuid: '1234' }, - expPeriod: 168, - secret: String(REFRESH_KEY), - }) - const mockedRequest = { cookies: { refresh_token: refreshToken } } - const mockedFormat = jest - .fn() - .mockReturnValueOnce('2021-06-30T12:00:00') - .mockReturnValueOnce('2021-07-01T12:00:00') - const mockedMoment = jest.fn().mockReturnValue({ - format: mockedFormat, - isAfter: jest.fn().mockReturnValue(false), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - refreshTokens(input: {}) { - result { - ... on AuthResult { - authToken - user { - displayName - } - } - ... on AuthenticateError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query, - collections: collectionNames, - transaction: mockedTransaction, - uuidv4, - jwt, - moment: mockedMoment, - request: mockedRequest, - auth: { - tokenize: jest.fn().mockReturnValue('token'), - }, - validators: { - cleanseInput, - }, - loaders: { - loadUserByKey: { - load: jest.fn().mockReturnValue({ - refreshInfo: { - expiresAt: '', - refreshId: '1234', - }, - }), - }, - }, - }, - }) - - const error = [new GraphQLError('Impossible de rafraîchir les jetons, veuillez vous connecter.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when attempting to refresh tokens for user: 123: Error: Transaction step error`, - ]) - }) - }) - }) - describe('transaction commit error occurs', () => { - describe('when upserting new refreshId', () => { - it('throws an error', async () => { - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue({}), - commit: jest.fn().mockRejectedValue(new Error('Transaction commit error')), - abort: jest.fn(), - }) - - const refreshToken = tokenize({ - parameters: { userKey: 123, uuid: '1234' }, - expPeriod: 168, - secret: String(REFRESH_KEY), - }) - const mockedRequest = { cookies: { refresh_token: refreshToken } } - - const mockedFormat = jest - .fn() - .mockReturnValueOnce('2021-06-30T12:00:00') - .mockReturnValueOnce('2021-07-01T12:00:00') - const mockedMoment = jest.fn().mockReturnValue({ - format: mockedFormat, - isAfter: jest.fn().mockReturnValue(false), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - refreshTokens(input: {}) { - result { - ... on AuthResult { - authToken - user { - displayName - } - } - ... on AuthenticateError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query, - collections: collectionNames, - transaction: mockedTransaction, - uuidv4, - jwt, - moment: mockedMoment, - request: mockedRequest, - auth: { - tokenize: jest.fn().mockReturnValue('token'), - }, - validators: { - cleanseInput, - }, - loaders: { - loadUserByKey: { - load: jest.fn().mockReturnValue({ - refreshInfo: { - expiresAt: '', - refreshId: '1234', - }, - }), - }, - }, - }, - }) - - const error = [new GraphQLError('Impossible de rafraîchir les jetons, veuillez vous connecter.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx commit error occurred while user: 123 attempted to refresh tokens: Error: Transaction commit error`, - ]) - }) - }) - }) }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/remove-phone-number.test.js b/api/src/user/mutations/__tests__/remove-phone-number.test.js index ab123372a8..f6c56a26a7 100644 --- a/api/src/user/mutations/__tests__/remove-phone-number.test.js +++ b/api/src/user/mutations/__tests__/remove-phone-number.test.js @@ -1,95 +1,100 @@ -import { dbNameFromFile } from 'arango-tools' -import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' -import { setupI18n } from '@lingui/core' - -import englishMessages from '../../../locale/en/messages' -import frenchMessages from '../../../locale/fr/messages' -import { createQuerySchema } from '../../../query' -import { createMutationSchema } from '../../../mutation' -import { userRequired } from '../../../auth' -import { loadUserByKey } from '../../loaders' -import dbschema from '../../../../database.json' -import { collectionNames } from '../../../collection-names' +import { dbNameFromFile } from "arango-tools" +import { ensureDatabase as ensure } from "../../../testUtilities" +import { + graphql as executeGraphql, + GraphQLSchema, + GraphQLError, +} from "graphql" +import { setupI18n } from "@lingui/core" + +import englishMessages from "../../../locale/en/messages" +import frenchMessages from "../../../locale/fr/messages" +import { createQuerySchema } from "../../../query" +import { createMutationSchema } from "../../../mutation" +import { userRequired } from "../../../auth" +import { loadUserByKey } from "../../loaders" +import { withDataSources } from "../../test-helpers/with-data-sources" +import dbschema from "../../../../database.json" +import { collectionNames } from "../../../collection-names" const { DB_PASS: rootPass, DB_URL: url } = process.env -describe('testing the removePhoneNumber mutation', () => { - let query, drop, truncate, schema, i18n, collections, transaction, user - - 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(), - }) - }) - - afterEach(() => { - consoleOutput.length = 0 - }) - - describe('given a successful removal', () => { - beforeAll(async () => { - // Generate DB Items - ;({ query, drop, truncate, collections, transaction } = await ensure({ - variables: { - dbname: dbNameFromFile(__filename), - username: 'root', - rootPassword: rootPass, - password: rootPass, - url, - }, - - schema: dbschema, - })) - }) - 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 is email validated', () => { - beforeEach(async () => { - user = await collections.users.save({ - userName: 'john.doe@test.email.ca', - emailValidated: true, - phoneValidated: true, - phoneDetails: { - iv: 'iv', - cipher: 'cipher', - phoneNumber: 'phoneNumber', - }, - tfaSendMethod: 'phone', - }) - }) - it('executes mutation successfully', async () => { - const response = await graphql({ - schema, - source: ` +describe("testing the removePhoneNumber mutation", () => { + let query, drop, truncate, schema, i18n, collections, transaction, user + + 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(), + }) + }) + + afterEach(() => { + consoleOutput.length = 0 + }) + + describe("given a successful removal", () => { + beforeAll(async () => { + // Generate DB Items + ({ query, drop, truncate, collections, transaction } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: "root", + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + }) + 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 is email validated", () => { + beforeEach(async () => { + user = await collections.users.save({ + userName: "john.doe@test.email.ca", + emailValidated: true, + phoneValidated: true, + phoneDetails: { + iv: "iv", + cipher: "cipher", + phoneNumber: "phoneNumber", + }, + tfaSendMethod: "phone", + }) + }) + it("executes mutation successfully", async () => { + const response = await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -104,38 +109,40 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - const expectedResponse = { - data: { - removePhoneNumber: { - result: { - status: 'Phone number has been successfully removed.', - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([`User: ${user._key} successfully removed their phone number.`]) - }) - it('sets phoneDetails to null', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + const expectedResponse = { + data: { + removePhoneNumber: { + result: { + status: "Phone number has been successfully removed.", + }, + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${user._key} successfully removed their phone number.`, + ]) + }) + it("sets phoneDetails to null", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -150,29 +157,31 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.phoneDetails).toEqual(null) - }) - it('sets phoneValidated to false', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.phoneDetails).toEqual(null) + }) + it("sets phoneValidated to false", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -187,29 +196,31 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.phoneValidated).toEqual(false) - }) - it('changes tfaSendMethod to email', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.phoneValidated).toEqual(false) + }) + it("changes tfaSendMethod to email", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -224,44 +235,46 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.tfaSendMethod).toEqual('email') - }) - }) - describe('user is not email validated', () => { - beforeEach(async () => { - user = await collections.users.save({ - userName: 'john.doe@test.email.ca', - emailValidated: false, - phoneValidated: true, - phoneDetails: { - iv: '', - cipher: '', - phoneNumber: '', - }, - tfaSendMethod: 'phone', - }) - }) - it('executes mutation successfully', async () => { - const response = await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.tfaSendMethod).toEqual("email") + }) + }) + describe("user is not email validated", () => { + beforeEach(async () => { + user = await collections.users.save({ + userName: "john.doe@test.email.ca", + emailValidated: false, + phoneValidated: true, + phoneDetails: { + iv: "", + cipher: "", + phoneNumber: "", + }, + tfaSendMethod: "phone", + }) + }) + it("executes mutation successfully", async () => { + const response = await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -276,38 +289,40 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - const expectedResponse = { - data: { - removePhoneNumber: { - result: { - status: 'Phone number has been successfully removed.', - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([`User: ${user._key} successfully removed their phone number.`]) - }) - it('sets phoneDetails to null', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + const expectedResponse = { + data: { + removePhoneNumber: { + result: { + status: "Phone number has been successfully removed.", + }, + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${user._key} successfully removed their phone number.`, + ]) + }) + it("sets phoneDetails to null", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -322,29 +337,31 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.phoneDetails).toEqual(null) - }) - it('sets phoneValidated to false', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.phoneDetails).toEqual(null) + }) + it("sets phoneValidated to false", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -359,29 +376,31 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.phoneValidated).toEqual(false) - }) - it('changes tfaSendMethod to email', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.phoneValidated).toEqual(false) + }) + it("changes tfaSendMethod to email", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -396,60 +415,62 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.tfaSendMethod).toEqual('none') - }) - }) - }) - describe('users language is set to french', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'fr', - localeData: { - en: { plurals: {} }, - fr: { plurals: {} }, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - describe('user is email validated', () => { - beforeEach(async () => { - user = await collections.users.save({ - userName: 'john.doe@test.email.ca', - emailValidated: true, - phoneValidated: true, - phoneDetails: { - iv: 'iv', - cipher: 'cipher', - phoneNumber: 'phoneNumber', - }, - tfaSendMethod: 'phone', - }) - }) - it('executes mutation successfully', async () => { - const response = await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.tfaSendMethod).toEqual("none") + }) + }) + }) + describe("users language is set to french", () => { + beforeAll(() => { + i18n = setupI18n({ + locale: "fr", + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ["en", "fr"], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + describe("user is email validated", () => { + beforeEach(async () => { + user = await collections.users.save({ + userName: "john.doe@test.email.ca", + emailValidated: true, + phoneValidated: true, + phoneDetails: { + iv: "iv", + cipher: "cipher", + phoneNumber: "phoneNumber", + }, + tfaSendMethod: "phone", + }) + }) + it("executes mutation successfully", async () => { + const response = await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -464,38 +485,40 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - const expectedResponse = { - data: { - removePhoneNumber: { - result: { - status: 'Le numéro de téléphone a été supprimé avec succès.', - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([`User: ${user._key} successfully removed their phone number.`]) - }) - it('sets phoneDetails to null', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + const expectedResponse = { + data: { + removePhoneNumber: { + result: { + status: "Le numéro de téléphone a été supprimé avec succès.", + }, + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${user._key} successfully removed their phone number.`, + ]) + }) + it("sets phoneDetails to null", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -510,29 +533,31 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.phoneDetails).toEqual(null) - }) - it('sets phoneValidated to false', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.phoneDetails).toEqual(null) + }) + it("sets phoneValidated to false", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -547,29 +572,31 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.phoneValidated).toEqual(false) - }) - it('changes tfaSendMethod to email', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.phoneValidated).toEqual(false) + }) + it("changes tfaSendMethod to email", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -584,44 +611,46 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.tfaSendMethod).toEqual('email') - }) - }) - describe('user is not email validated', () => { - beforeEach(async () => { - user = await collections.users.save({ - userName: 'john.doe@test.email.ca', - emailValidated: false, - phoneValidated: true, - phoneDetails: { - iv: '', - cipher: '', - phoneNumber: '', - }, - tfaSendMethod: 'phone', - }) - }) - it('executes mutation successfully', async () => { - const response = await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.tfaSendMethod).toEqual("email") + }) + }) + describe("user is not email validated", () => { + beforeEach(async () => { + user = await collections.users.save({ + userName: "john.doe@test.email.ca", + emailValidated: false, + phoneValidated: true, + phoneDetails: { + iv: "", + cipher: "", + phoneNumber: "", + }, + tfaSendMethod: "phone", + }) + }) + it("executes mutation successfully", async () => { + const response = await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -636,38 +665,40 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - const expectedResponse = { - data: { - removePhoneNumber: { - result: { - status: 'Le numéro de téléphone a été supprimé avec succès.', - }, - }, - }, - } - - expect(response).toEqual(expectedResponse) - expect(consoleOutput).toEqual([`User: ${user._key} successfully removed their phone number.`]) - }) - it('sets phoneDetails to null', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + const expectedResponse = { + data: { + removePhoneNumber: { + result: { + status: "Le numéro de téléphone a été supprimé avec succès.", + }, + }, + }, + } + + expect(response).toEqual(expectedResponse) + expect(consoleOutput).toEqual([ + `User: ${user._key} successfully removed their phone number.`, + ]) + }) + it("sets phoneDetails to null", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -682,29 +713,31 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.phoneDetails).toEqual(null) - }) - it('sets phoneValidated to false', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.phoneDetails).toEqual(null) + }) + it("sets phoneValidated to false", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -719,29 +752,31 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.phoneValidated).toEqual(false) - }) - it('changes tfaSendMethod to email', async () => { - await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.phoneValidated).toEqual(false) + }) + it("changes tfaSendMethod to email", async () => { + await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -756,56 +791,62 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction, - auth: { - userRequired: userRequired({ - userKey: user._key, - loadUserByKey: loadUserByKey({ query, userKey: user._key }), - }), - }, - }, - }) - - user = await loadUserByKey({ query, userKey: user._key }).load(user._key) - - expect(user.tfaSendMethod).toEqual('none') - }) - }) - }) - }) - - describe('given an unsuccessful removal', () => { - 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('step error occurs', () => { - describe('when running upsert', () => { - it('throws an error', async () => { - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue(new Error('transaction step error occurred.')), - abort: jest.fn(), - }) - - const response = await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction, + auth: { + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query, userKey: user._key }), + }), + }, + }, + }) + + user = await loadUserByKey({ query, userKey: user._key }).load( + user._key, + ) + + expect(user.tfaSendMethod).toEqual("none") + }) + }) + }) + }) + + describe("given an unsuccessful removal", () => { + 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("step error occurs", () => { + describe("when running upsert", () => { + it("throws an error", async () => { + const mockedTransaction = jest.fn().mockReturnValue({ + step: jest + .fn() + .mockRejectedValue( + new Error("transaction step error occurred."), + ), + abort: jest.fn(), + }) + + const response = await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -820,39 +861,47 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction: mockedTransaction, - auth: { - userRequired: jest.fn().mockReturnValue({ _key: 123 }), - }, - }, - }) - - const error = [new GraphQLError('Unable to remove phone number. Please try again.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred well removing phone number for user: 123: Error: transaction step error occurred.`, - ]) - }) - }) - }) - describe('commit error occurs', () => { - describe('when committing upsert', () => { - it('throws an error', async () => { - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn(), - commit: jest.fn().mockRejectedValue(new Error('transaction step error occurred.')), - abort: jest.fn(), - }) - - const response = await graphql({ - schema, - source: ` + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction: mockedTransaction, + auth: { + userRequired: jest.fn().mockReturnValue({ _key: 123 }), + }, + }, + }) + + const error = [ + new GraphQLError( + "Unable to remove phone number. Please try again.", + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `Trx step error occurred well removing phone number for user: 123: Error: transaction step error occurred.`, + ]) + }) + }) + }) + describe("commit error occurs", () => { + describe("when committing upsert", () => { + it("throws an error", async () => { + const mockedTransaction = jest.fn().mockReturnValue({ + step: jest.fn(), + commit: jest + .fn() + .mockRejectedValue( + new Error("transaction step error occurred."), + ), + abort: jest.fn(), + }) + + const response = await graphql({ + schema, + source: ` mutation { removePhoneNumber(input: {}) { result { @@ -867,136 +916,33 @@ describe('testing the removePhoneNumber mutation', () => { } } `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction: mockedTransaction, - auth: { - userRequired: jest.fn().mockReturnValue({ _key: 123 }), - }, - }, - }) - - const error = [new GraphQLError('Unable to remove phone number. Please try again.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx commit error occurred well removing phone number for user: 123: Error: transaction step error occurred.`, - ]) - }) - }) - }) - }) - describe('users language is set to french', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'fr', - localeData: { - en: { plurals: {} }, - fr: { plurals: {} }, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - describe('step error occurs', () => { - describe('when running upsert', () => { - it('throws an error', async () => { - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue(new Error('transaction step error occurred.')), - abort: jest.fn(), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - removePhoneNumber(input: {}) { - result { - ... on RemovePhoneNumberResult { - status - } - ... on RemovePhoneNumberError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction: mockedTransaction, - auth: { - userRequired: jest.fn().mockReturnValue({ _key: 123 }), - }, - }, - }) - - const error = [new GraphQLError('Impossible de supprimer le numéro de téléphone. Veuillez réessayer.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred well removing phone number for user: 123: Error: transaction step error occurred.`, - ]) - }) - }) - }) - describe('commit error occurs', () => { - describe('when committing upsert', () => { - it('throws an error', async () => { - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn(), - commit: jest.fn().mockRejectedValue(new Error('transaction step error occurred.')), - abort: jest.fn(), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - removePhoneNumber(input: {}) { - result { - ... on RemovePhoneNumberResult { - status - } - ... on RemovePhoneNumberError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - collections: collectionNames, - query, - transaction: mockedTransaction, - auth: { - userRequired: jest.fn().mockReturnValue({ _key: 123 }), - }, - }, - }) - - const error = [new GraphQLError('Impossible de supprimer le numéro de téléphone. Veuillez réessayer.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx commit error occurred well removing phone number for user: 123: Error: transaction step error occurred.`, - ]) - }) - }) - }) - }) - }) + rootValue: null, + contextValue: { + i18n, + collections: collectionNames, + query, + transaction: mockedTransaction, + auth: { + userRequired: jest.fn().mockReturnValue({ _key: 123 }), + }, + }, + }) + + const error = [ + new GraphQLError( + "Unable to remove phone number. Please try again.", + ), + ] + + expect(response.errors).toEqual(error) + expect(consoleOutput).toEqual([ + `Trx commit error occurred well removing phone number for user: 123: Error: transaction step error occurred.`, + ]) + }) + }) + }) + }) + }) }) +const graphql = (args) => + executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/reset-password.test.js b/api/src/user/mutations/__tests__/reset-password.test.js index 366aa1b79b..654eb33dbf 100644 --- a/api/src/user/mutations/__tests__/reset-password.test.js +++ b/api/src/user/mutations/__tests__/reset-password.test.js @@ -1,7 +1,7 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' import bcrypt from 'bcryptjs' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { setupI18n } from '@lingui/core' import { v4 as uuidv4 } from 'uuid' import jwt from 'jsonwebtoken' @@ -13,6 +13,7 @@ import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { tokenize, verifyToken } from '../../../auth' import { loadUserByUserName, loadUserByKey } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' @@ -1471,7 +1472,7 @@ describe('reset users password', () => { }, }) - const error = [new GraphQLError('Impossible de réinitialiser le mot de passe. Veuillez réessayer.')] + const error = [new GraphQLError('Unable to reset password. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -1542,7 +1543,7 @@ describe('reset users password', () => { }, }) - const error = [new GraphQLError('Impossible de réinitialiser le mot de passe. Veuillez réessayer.')] + const error = [new GraphQLError('Unable to reset password. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -1554,3 +1555,4 @@ describe('reset users password', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/send-password-reset.test.js b/api/src/user/mutations/__tests__/send-password-reset.test.js index 9fe11e1184..3925fee015 100644 --- a/api/src/user/mutations/__tests__/send-password-reset.test.js +++ b/api/src/user/mutations/__tests__/send-password-reset.test.js @@ -1,7 +1,7 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' import bcrypt from 'bcryptjs' -import { graphql, GraphQLSchema } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema } from 'graphql' import { setupI18n } from '@lingui/core' import englishMessages from '../../../locale/en/messages' @@ -10,6 +10,7 @@ import { createQuerySchema } from '../../../query' import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { loadUserByUserName } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import dbschema from '../../../../database.json' const { DB_PASS: rootPass, DB_URL: url } = process.env @@ -367,3 +368,4 @@ describe('user send password reset email', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/set-phone-number.test.js b/api/src/user/mutations/__tests__/set-phone-number.test.js index 2c4e154b9e..d3884fa9cc 100644 --- a/api/src/user/mutations/__tests__/set-phone-number.test.js +++ b/api/src/user/mutations/__tests__/set-phone-number.test.js @@ -1,7 +1,7 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' import bcrypt from 'bcryptjs' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { setupI18n } from '@lingui/core' import englishMessages from '../../../locale/en/messages' @@ -11,6 +11,7 @@ import { createMutationSchema } from '../../../mutation' import { cleanseInput, decryptPhoneNumber } from '../../../validators' import { tokenize, userRequired } from '../../../auth' import { loadUserByUserName, loadUserByKey } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' @@ -1650,159 +1651,6 @@ describe('user sets a new phone number', () => { const error = [new GraphQLError('Unable to set phone number, please try again.')] - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx commit error occurred for user: 123 when upserting phone number information: Error: Transaction commit error`, - ]) - }) - }) - }) - }) - describe('users language is set to french', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'fr', - localeData: { - en: { plurals: {} }, - fr: { plurals: {} }, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - describe('transaction step error occurs', () => { - describe('when setting phone number', () => { - it('throws an error', async () => { - const newPhoneNumber = '+12345678901' - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue(new Error('Transaction step error')), - abort: jest.fn(), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - setPhoneNumber(input: { phoneNumber: "${newPhoneNumber}" }) { - result { - ... on SetPhoneNumberResult { - status - user { - phoneNumber - } - } - ... on SetPhoneNumberError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - request, - userKey: 123, - query, - collections: collectionNames, - transaction: mockedTransaction, - auth: { - bcrypt, - tokenize, - userRequired: jest.fn().mockReturnValue({ - _key: 123, - }), - }, - validators: { - cleanseInput, - decryptPhoneNumber, - }, - loaders: { - loadUserByKey: { - load: jest.fn(), - }, - }, - notify: { - sendAuthTextMsg: mockNotify, - }, - }, - }) - - const error = [new GraphQLError('Impossible de définir le numéro de téléphone, veuillez réessayer.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred for user: 123 when upserting phone number information: Error: Transaction step error`, - ]) - }) - }) - }) - describe('transaction commit error occurs', () => { - describe('when setting phone number', () => { - it('throws an error', async () => { - const newPhoneNumber = '+12345678901' - - const mockedTransaction = jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue({}), - commit: jest.fn().mockRejectedValue(new Error('Transaction commit error')), - abort: jest.fn(), - }) - - const response = await graphql({ - schema, - source: ` - mutation { - setPhoneNumber(input: { phoneNumber: "${newPhoneNumber}" }) { - result { - ... on SetPhoneNumberResult { - status - user { - phoneNumber - } - } - ... on SetPhoneNumberError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - request, - userKey: 123, - query, - collections: collectionNames, - transaction: mockedTransaction, - auth: { - bcrypt, - tokenize, - userRequired: jest.fn().mockReturnValue({ _key: 123 }), - }, - validators: { - cleanseInput, - decryptPhoneNumber, - }, - loaders: { - loadUserByKey: { - load: jest.fn(), - }, - }, - notify: { - sendAuthTextMsg: mockNotify, - }, - }, - }) - - const error = [new GraphQLError('Impossible de définir le numéro de téléphone, veuillez réessayer.')] - expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ `Trx commit error occurred for user: 123 when upserting phone number information: Error: Transaction commit error`, @@ -1813,3 +1661,4 @@ describe('user sets a new phone number', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/sign-in.test.js b/api/src/user/mutations/__tests__/sign-in.test.js index 933c264e7c..f10a93236b 100644 --- a/api/src/user/mutations/__tests__/sign-in.test.js +++ b/api/src/user/mutations/__tests__/sign-in.test.js @@ -1,7 +1,7 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' import bcrypt from 'bcryptjs' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { setupI18n } from '@lingui/core' import { v4 as uuidv4 } from 'uuid' import jwt from 'jsonwebtoken' @@ -12,6 +12,7 @@ import { createQuerySchema } from '../../../query' import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { loadUserByUserName } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' import { tokenize } from '../../../auth' @@ -2358,7 +2359,7 @@ describe('authenticate user account', () => { }, }) - const error = [new GraphQLError('Impossible de se connecter, veuillez réessayer.')] + const error = [new GraphQLError('Unable to sign in, please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2430,7 +2431,7 @@ describe('authenticate user account', () => { }, }) - const error = [new GraphQLError('Impossible de se connecter, veuillez réessayer.')] + const error = [new GraphQLError('Unable to sign in, please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2503,7 +2504,7 @@ describe('authenticate user account', () => { }, }) - const error = [new GraphQLError('Impossible de se connecter, veuillez réessayer.')] + const error = [new GraphQLError('Unable to sign in, please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2575,7 +2576,7 @@ describe('authenticate user account', () => { }, }, }) - const error = [new GraphQLError('Impossible de se connecter, veuillez réessayer.')] + const error = [new GraphQLError('Unable to sign in, please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2650,7 +2651,7 @@ describe('authenticate user account', () => { }, }) - const error = [new GraphQLError('Impossible de se connecter, veuillez réessayer.')] + const error = [new GraphQLError('Unable to sign in, please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2723,7 +2724,7 @@ describe('authenticate user account', () => { }, }) - const error = [new GraphQLError('Impossible de se connecter, veuillez réessayer.')] + const error = [new GraphQLError('Unable to sign in, please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2795,7 +2796,7 @@ describe('authenticate user account', () => { }, }, }) - const error = [new GraphQLError('Impossible de se connecter, veuillez réessayer.')] + const error = [new GraphQLError('Unable to sign in, please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -2807,3 +2808,4 @@ describe('authenticate user account', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/sign-up.test.js b/api/src/user/mutations/__tests__/sign-up.test.js index 7dd4b0c961..975c57ef87 100644 --- a/api/src/user/mutations/__tests__/sign-up.test.js +++ b/api/src/user/mutations/__tests__/sign-up.test.js @@ -1,7 +1,7 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' import bcrypt from 'bcryptjs' -import { graphql, GraphQLError, GraphQLSchema } from 'graphql' +import { graphql as executeGraphql, GraphQLError, GraphQLSchema } from 'graphql' import { setupI18n } from '@lingui/core' import { v4 as uuidv4 } from 'uuid' @@ -13,6 +13,7 @@ import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { loadUserByUserName, loadUserByKey } from '../../loaders' import { loadOrgByKey } from '../../../organization/loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' @@ -2287,7 +2288,12 @@ describe('testing user sign up', () => { query, collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValueOnce({ next: jest.fn() }).mockRejectedValue('Transaction Step Error'), + step: jest + .fn() + .mockResolvedValueOnce({ + next: jest.fn().mockResolvedValue({ _id: 'users/123', _key: '123' }), + }) + .mockRejectedValue('Transaction Step Error'), abort: jest.fn(), }), uuidv4, @@ -2992,7 +2998,7 @@ describe('testing user sign up', () => { }, }) - const error = [new GraphQLError("Impossible de s'inscrire. Veuillez réessayer.")] + const error = [new GraphQLError('Unable to sign up. Please try again.')] expect(response.errors).toEqual(error) @@ -3090,7 +3096,7 @@ describe('testing user sign up', () => { }, }) - const error = [new GraphQLError("Impossible de s'inscrire. Veuillez réessayer.")] + const error = [new GraphQLError('Unable to sign up. Please try again.')] expect(response.errors).toEqual(error) @@ -3143,7 +3149,12 @@ describe('testing user sign up', () => { query, collections: collectionNames, transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValueOnce({ next: jest.fn() }).mockRejectedValue('Transaction Step Error'), + step: jest + .fn() + .mockResolvedValueOnce({ + next: jest.fn().mockResolvedValue({ _id: 'users/123', _key: '123' }), + }) + .mockRejectedValue('Transaction Step Error'), abort: jest.fn(), }), uuidv4, @@ -3197,7 +3208,7 @@ describe('testing user sign up', () => { }, }) - const error = [new GraphQLError("Impossible de s'inscrire. Veuillez réessayer.")] + const error = [new GraphQLError('Unable to sign up. Please try again.')] expect(response.errors).toEqual(error) @@ -3294,7 +3305,7 @@ describe('testing user sign up', () => { }, }) - const error = [new GraphQLError("Impossible de s'inscrire. Veuillez réessayer.")] + const error = [new GraphQLError('Unable to sign up. Please try again.')] expect(response.errors).toEqual(error) @@ -3307,3 +3318,4 @@ describe('testing user sign up', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/update-user-password.test.js b/api/src/user/mutations/__tests__/update-user-password.test.js index b3910e594f..63ef906da2 100644 --- a/api/src/user/mutations/__tests__/update-user-password.test.js +++ b/api/src/user/mutations/__tests__/update-user-password.test.js @@ -12,7 +12,8 @@ import { createQuerySchema } from '../../../query' import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { tokenize, userRequired } from '../../../auth' -import { loadUserByUserName, loadUserByKey } from '../../loaders' +import { loadUserByKey } from '../../loaders' +import { UserDataSource } from '../../data-source' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' @@ -20,6 +21,17 @@ const { DB_PASS: rootPass, DB_URL: url } = process.env const mockNotfiy = jest.fn() +const createUserDataSource = ({ query, userKey, i18n, language = 'en', transaction }) => + new UserDataSource({ + query, + userKey, + i18n, + language, + cleanseInput, + transaction, + collections: collectionNames, + }) + describe('authenticate user account', () => { let query, drop, truncate, schema, i18n, user, transaction const consoleOutput = [] @@ -92,8 +104,8 @@ describe('authenticate user account', () => { validators: { cleanseInput, }, - loaders: { - loadUserByUserName: loadUserByUserName({ query }), + dataSources: { + user: createUserDataSource({ query, transaction }), }, notify: { sendVerificationEmail: jest.fn(), @@ -176,9 +188,8 @@ describe('authenticate user account', () => { validators: { cleanseInput, }, - loaders: { - loadUserByUserName: loadUserByUserName({ query }), - loadUserByKey: loadUserByKey({ query }), + dataSources: { + user: createUserDataSource({ query, userKey: user._key, i18n, language: 'en', transaction }), }, }, }) @@ -230,8 +241,8 @@ describe('authenticate user account', () => { validators: { cleanseInput, }, - loaders: { - loadUserByUserName: loadUserByUserName({ query }), + dataSources: { + user: createUserDataSource({ query, i18n, language: 'en', transaction }), }, notify: { sendAuthEmail: mockNotfiy, @@ -311,9 +322,8 @@ describe('authenticate user account', () => { validators: { cleanseInput, }, - loaders: { - loadUserByUserName: loadUserByUserName({ query }), - loadUserByKey: loadUserByKey({ query }), + dataSources: { + user: createUserDataSource({ query, userKey: user._key, i18n, language: 'fr', transaction }), }, }, }) @@ -365,8 +375,8 @@ describe('authenticate user account', () => { validators: { cleanseInput, }, - loaders: { - loadUserByUserName: loadUserByUserName({ query }), + dataSources: { + user: createUserDataSource({ query, i18n, language: 'fr', transaction }), }, notify: { sendAuthEmail: mockNotfiy, @@ -437,6 +447,7 @@ describe('authenticate user account', () => { collections: collectionNames, transaction, userKey: 123, + dataSources: { user: {} }, auth: { bcrypt: { compareSync: jest.fn().mockReturnValue(false), @@ -501,6 +512,7 @@ describe('authenticate user account', () => { collections: collectionNames, transaction, userKey: 123, + dataSources: { user: {} }, auth: { bcrypt: { compareSync: jest.fn().mockReturnValue(true), @@ -565,6 +577,7 @@ describe('authenticate user account', () => { collections: collectionNames, transaction, userKey: 123, + dataSources: { user: {} }, auth: { bcrypt: { compareSync: jest.fn().mockReturnValue(true), @@ -633,6 +646,18 @@ describe('authenticate user account', () => { abort: jest.fn(), }), userKey: 123, + dataSources: { + user: createUserDataSource({ + query, + userKey: 123, + i18n, + language: 'en', + transaction: jest.fn().mockReturnValue({ + step: jest.fn().mockRejectedValue(new Error('Transaction step error')), + abort: jest.fn(), + }), + }), + }, auth: { bcrypt: { compareSync: jest.fn().mockReturnValue(true), @@ -695,6 +720,19 @@ describe('authenticate user account', () => { abort: jest.fn(), }), userKey: 123, + dataSources: { + user: createUserDataSource({ + query, + userKey: 123, + i18n, + language: 'en', + transaction: jest.fn().mockReturnValue({ + step: jest.fn().mockReturnValue({}), + commit: jest.fn().mockRejectedValue(new Error('Transaction commit error')), + abort: jest.fn(), + }), + }), + }, auth: { bcrypt: { compareSync: jest.fn().mockReturnValue(true), @@ -768,6 +806,7 @@ describe('authenticate user account', () => { collections: collectionNames, transaction, userKey: 123, + dataSources: { user: {} }, auth: { bcrypt: { compareSync: jest.fn().mockReturnValue(false), @@ -833,6 +872,7 @@ describe('authenticate user account', () => { collections: collectionNames, transaction, userKey: 123, + dataSources: { user: {} }, auth: { bcrypt: { compareSync: jest.fn().mockReturnValue(true), @@ -898,6 +938,7 @@ describe('authenticate user account', () => { collections: collectionNames, transaction, userKey: 123, + dataSources: { user: {} }, auth: { bcrypt: { compareSync: jest.fn().mockReturnValue(true), @@ -931,129 +972,6 @@ describe('authenticate user account', () => { ]) }) }) - describe('transaction step error occurs', () => { - describe('when updating password', () => { - it('returns an error message', async () => { - const response = await graphql({ - schema, - source: ` - mutation { - updateUserPassword( - input: { - currentPassword: "testpassword123" - updatedPassword: "newtestpassword123" - updatedPasswordConfirm: "newtestpassword123" - } - ) { - result { - ... on UpdateUserPasswordResultType { - status - } - ... on UpdateUserPasswordError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query, - collections: collectionNames, - transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue(new Error('Transaction step error')), - abort: jest.fn(), - }), - userKey: 123, - auth: { - bcrypt: { - compareSync: jest.fn().mockReturnValue(true), - hashSync: jest.fn().mockReturnValue('password'), - }, - tokenize, - userRequired: jest.fn().mockReturnValue({ - _key: 123, - }), - }, - validators: { - cleanseInput, - }, - }, - }) - - const error = [new GraphQLError('Impossible de mettre à jour le mot de passe. Veuillez réessayer.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when user: 123 attempted to update their password: Error: Transaction step error`, - ]) - }) - }) - }) - describe('transaction commit error occurs', () => { - describe('when updating password', () => { - it('returns an error message', async () => { - const response = await graphql({ - schema, - source: ` - mutation { - updateUserPassword( - input: { - currentPassword: "testpassword123" - updatedPassword: "newtestpassword123" - updatedPasswordConfirm: "newtestpassword123" - } - ) { - result { - ... on UpdateUserPasswordResultType { - status - } - ... on UpdateUserPasswordError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query, - collections: collectionNames, - transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue({}), - commit: jest.fn().mockRejectedValue(new Error('Transaction commit error')), - abort: jest.fn(), - }), - userKey: 123, - auth: { - bcrypt: { - compareSync: jest.fn().mockReturnValue(true), - hashSync: jest.fn().mockReturnValue('password'), - }, - tokenize, - userRequired: jest.fn().mockReturnValue({ - _key: 123, - }), - }, - validators: { - cleanseInput, - }, - }, - }) - - const error = [new GraphQLError('Impossible de mettre à jour le mot de passe. Veuillez réessayer.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx commit error occurred when user: 123 attempted to update their password: Error: Transaction commit error`, - ]) - }) - }) - }) }) }) }) diff --git a/api/src/user/mutations/__tests__/update-user-profile.test.js b/api/src/user/mutations/__tests__/update-user-profile.test.js index ad8bfb23bf..5a534a1199 100644 --- a/api/src/user/mutations/__tests__/update-user-profile.test.js +++ b/api/src/user/mutations/__tests__/update-user-profile.test.js @@ -2,7 +2,7 @@ import bcrypt from 'bcryptjs' import crypto from 'crypto' import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { setupI18n } from '@lingui/core' import englishMessages from '../../../locale/en/messages' @@ -12,6 +12,7 @@ import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { tokenize, userRequired } from '../../../auth' import { loadUserByUserName, loadUserByKey } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' @@ -240,7 +241,7 @@ describe('authenticate user account', () => { expect(response).toEqual(expectedResponse) expect(sendVerificationEmail).toHaveBeenCalledWith({ - verifyUrl: verifyUrl, + verifyUrl, userKey: user._key, displayName: user.displayName, userName: newUsername, @@ -940,7 +941,7 @@ describe('authenticate user account', () => { expect(response).toEqual(expectedResponse) expect(sendVerificationEmail).toHaveBeenCalledWith({ - verifyUrl: verifyUrl, + verifyUrl, userKey: user._key, displayName: user.displayName, userName: newUsername, @@ -1781,145 +1782,7 @@ describe('authenticate user account', () => { ]) }) }) - describe('given a transaction step error', () => { - describe('when updating profile', () => { - it('throws an error', async () => { - const response = await graphql({ - schema, - source: ` - mutation { - updateUserProfile( - input: { - displayName: "John Smith" - userName: "john.smith@istio.actually.works" - } - ) { - result { - ... on UpdateUserProfileResult { - status - user { - id - } - } - ... on UpdateUserProfileError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query, - collections: collectionNames, - transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockRejectedValue(new Error('Transaction step error')), - abort: jest.fn(), - }), - userKey: 123, - auth: { - bcrypt, - tokenize, - userRequired: jest.fn().mockReturnValue({ - tfaSendMethod: 'none', - }), - }, - validators: { - cleanseInput, - }, - loaders: { - loadUserByUserName: { - load: jest.fn().mockReturnValue(undefined), - }, - loadUserByKey: { - load: jest.fn().mockReturnValue(undefined), - }, - }, - notify: { sendVerificationEmail: jest.fn() }, - }, - }) - - const error = [new GraphQLError('Impossible de mettre à jour le profil. Veuillez réessayer.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx step error occurred when user: 123 attempted to update their profile: Error: Transaction step error`, - ]) - }) - }) - }) - describe('given a transaction step error', () => { - describe('when updating profile', () => { - it('throws an error', async () => { - const response = await graphql({ - schema, - source: ` - mutation { - updateUserProfile( - input: { - displayName: "John Smith" - userName: "john.smith@istio.actually.works" - } - ) { - result { - ... on UpdateUserProfileResult { - status - user { - id - } - } - ... on UpdateUserProfileError { - code - description - } - } - } - } - `, - rootValue: null, - contextValue: { - i18n, - query, - collections: collectionNames, - transaction: jest.fn().mockReturnValue({ - step: jest.fn().mockReturnValue({}), - commit: jest.fn().mockRejectedValue(new Error('Transaction commit error')), - abort: jest.fn(), - }), - userKey: 123, - auth: { - bcrypt, - tokenize, - userRequired: jest.fn().mockReturnValue({ - tfaSendMethod: 'none', - }), - }, - validators: { - cleanseInput, - }, - loaders: { - loadUserByUserName: { - load: jest.fn().mockReturnValue(undefined), - }, - loadUserByKey: { - load: jest.fn().mockReturnValue(undefined), - }, - }, - notify: { sendVerificationEmail: jest.fn() }, - }, - }) - - const error = [new GraphQLError('Impossible de mettre à jour le profil. Veuillez réessayer.')] - - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Trx commit error occurred when user: 123 attempted to update their profile: Error: Transaction commit error`, - ]) - }) - }) - }) }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/verify-account.test.js b/api/src/user/mutations/__tests__/verify-account.test.js index ec71acb6cd..558800d790 100644 --- a/api/src/user/mutations/__tests__/verify-account.test.js +++ b/api/src/user/mutations/__tests__/verify-account.test.js @@ -1,6 +1,6 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { setupI18n } from '@lingui/core' import englishMessages from '../../../locale/en/messages' @@ -10,6 +10,7 @@ import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { tokenize, verifyToken } from '../../../auth' import { loadUserByKey, loadUserByUserName } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' @@ -516,3 +517,4 @@ describe('user send password reset email', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/__tests__/verify-phone-number.test.js b/api/src/user/mutations/__tests__/verify-phone-number.test.js index 4da0f61bfc..50157cd58c 100644 --- a/api/src/user/mutations/__tests__/verify-phone-number.test.js +++ b/api/src/user/mutations/__tests__/verify-phone-number.test.js @@ -1,6 +1,6 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { setupI18n } from '@lingui/core' import englishMessages from '../../../locale/en/messages' @@ -9,6 +9,7 @@ import { createQuerySchema } from '../../../query' import { createMutationSchema } from '../../../mutation' import { loadUserByKey } from '../../loaders' import { userRequired } from '../../../auth' +import { withDataSources } from '../../test-helpers/with-data-sources' import dbschema from '../../../../database.json' import { collectionNames } from '../../../collection-names' @@ -473,6 +474,7 @@ describe('user send password reset email', () => { userKey: 123, query, collections: collectionNames, + language: 'fr', transaction: jest.fn().mockReturnValue({ step: jest.fn().mockRejectedValue(new Error('Transaction step error')), abort: jest.fn(), @@ -529,6 +531,7 @@ describe('user send password reset email', () => { userKey: 123, query, collections: collectionNames, + language: 'fr', transaction: jest.fn().mockReturnValue({ step: jest.fn().mockReturnValue({}), commit: jest.fn().mockRejectedValue(new Error('Transaction commit error')), @@ -739,7 +742,7 @@ describe('user send password reset email', () => { }, }) - const error = [new GraphQLError("Impossible de s'authentifier par deux facteurs. Veuillez réessayer.")] + const error = [new GraphQLError('Unable to two factor authenticate. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -796,7 +799,7 @@ describe('user send password reset email', () => { }, }) - const error = [new GraphQLError("Impossible de s'authentifier par deux facteurs. Veuillez réessayer.")] + const error = [new GraphQLError('Unable to two factor authenticate. Please try again.')] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -808,3 +811,4 @@ describe('user send password reset email', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/mutations/authenticate.js b/api/src/user/mutations/authenticate.js index 1761186ffa..a403ab1e60 100644 --- a/api/src/user/mutations/authenticate.js +++ b/api/src/user/mutations/authenticate.js @@ -37,13 +37,10 @@ export const authenticate = new mutationWithClientMutationId({ { i18n, response, - query, - collections, - transaction, uuidv4, jwt, auth: { tokenize, verifyToken }, - loaders: { loadUserByKey }, + dataSources: { user: userDataSource }, validators: { cleanseInput }, }, ) => { @@ -68,7 +65,7 @@ export const authenticate = new mutationWithClientMutationId({ } // Gather sign in user - const user = await loadUserByKey.load(tokenParameters.userKey) + const user = await userDataSource.byKey.load(tokenParameters.userKey) // Replace with userRequired() if (typeof user === 'undefined') { @@ -91,68 +88,14 @@ export const authenticate = new mutationWithClientMutationId({ expiresAt: new Date(new Date().getTime() + ms(REFRESH_TOKEN_EXPIRY)), } - // Setup Transaction - const trx = await transaction(collections) - - // Reset tfa code attempts, and set refresh code - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT { - tfaCode: null, - refreshInfo: ${refreshInfo}, - lastLogin: ${loginDate} - } - UPDATE { - tfaCode: null, - refreshInfo: ${refreshInfo}, - lastLogin: ${loginDate} - } - IN users - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when clearing tfa code and setting refresh id for user: ${user._key} during authentication: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to authenticate. Please try again.`)) - } - - // verify user email + await userDataSource.authenticateSuccess({ + userKey: user._key, + refreshInfo, + loginDate, + verifyEmail: sendMethod === 'email' && !user.emailValidated, + }) if (sendMethod === 'email' && !user.emailValidated) { - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT { - emailValidated: true, - } - UPDATE { - emailValidated: true, - } - IN users - `, - ) - user.emailValidated = true - } catch (err) { - console.error( - `Trx step error occurred when setting email validated to true for user: ${user._key} during authentication: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to authenticate. Please try again.`)) - } - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred while user: ${user._key} attempted to authenticate: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to authenticate. Please try again.`)) + user.emailValidated = true } const token = tokenize({ @@ -195,40 +138,10 @@ export const authenticate = new mutationWithClientMutationId({ token, user, } - } else { - console.warn(`User: ${user._key} attempted to authenticate their account, however the tfaCodes did not match.`) - // reset tfa code - const trx = await transaction(collections) - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT { - tfaCode: null, - } - UPDATE { - tfaCode: null, - } - IN users - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when clearing tfa code on attempt timeout for user: ${user._key} during authentication: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Incorrect TFA code. Please sign in again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred while user: ${user._key} attempted to authenticate: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Incorrect TFA code. Please sign in again.`)) - } - throw new Error(i18n._(t`Incorrect TFA code. Please sign in again.`)) } + + console.warn(`User: ${user._key} attempted to authenticate their account, however the tfaCodes did not match.`) + await userDataSource.clearTfaCode({ userKey: user._key }) + throw new Error(i18n._(t`Incorrect TFA code. Please sign in again.`)) }, }) diff --git a/api/src/user/mutations/close-account.js b/api/src/user/mutations/close-account.js index 7a03baa586..76010ff1b0 100644 --- a/api/src/user/mutations/close-account.js +++ b/api/src/user/mutations/close-account.js @@ -17,7 +17,16 @@ export const closeAccountSelf = new mutationWithClientMutationId({ }), mutateAndGetPayload: async ( args, - { i18n, query, collections, transaction, request: { ip }, auth: { userRequired }, validators: { cleanseInput } }, + { + i18n, + query, + collections, + transaction, + request: { ip }, + auth: { userRequired }, + dataSources: { user: userDataSource }, + validators: { cleanseInput }, + }, ) => { let submittedUserId if (args?.userId) { @@ -29,49 +38,7 @@ export const closeAccountSelf = new mutationWithClientMutationId({ const userId = user._id const targetUserName = user.userName - // Setup Trans action - const trx = await transaction(collections) - - try { - await trx.step( - () => query` - WITH affiliations, organizations, users - FOR v, e IN 1..1 INBOUND ${userId} affiliations - REMOVE { _key: e._key } IN affiliations - OPTIONS { waitForSync: true } - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when removing users remaining affiliations when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - try { - await trx.step( - () => query` - WITH users - REMOVE PARSE_IDENTIFIER(${userId}).key - IN users OPTIONS { waitForSync: true } - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when removing user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred when user: ${user._key} attempted to close account: ${userId}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } + await userDataSource.closeAccount({ userId }) console.info(`User: ${user._key} successfully closed user: ${userId} account.`) await logActivity({ @@ -123,7 +90,7 @@ export const closeAccountOther = new mutationWithClientMutationId({ transaction, request: { ip }, auth: { checkSuperAdmin, userRequired }, - loaders: { loadUserByKey }, + dataSources: { user: userDataSource }, validators: { cleanseInput }, }, ) => { @@ -149,7 +116,7 @@ export const closeAccountOther = new mutationWithClientMutationId({ } } - const checkUser = await loadUserByKey.load(submittedUserId) + const checkUser = await userDataSource.byKey.load(submittedUserId) if (typeof checkUser === 'undefined') { console.warn( `User: ${user._key} attempted to close user: ${submittedUserId} account, but requested user is undefined.`, @@ -163,49 +130,7 @@ export const closeAccountOther = new mutationWithClientMutationId({ userId = checkUser._id targetUserName = checkUser.userName - // Setup Trans action - const trx = await transaction(collections) - - try { - await trx.step( - () => query` - WITH affiliations, organizations, users - FOR v, e IN 1..1 INBOUND ${userId} affiliations - REMOVE { _key: e._key } IN affiliations - OPTIONS { waitForSync: true } - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when removing users remaining affiliations when user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - try { - await trx.step( - () => query` - WITH users - REMOVE PARSE_IDENTIFIER(${userId}).key - IN users OPTIONS { waitForSync: true } - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when removing user: ${user._key} attempted to close account: ${userId}: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred when user: ${user._key} attempted to close account: ${userId}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to close account. Please try again.`)) - } + await userDataSource.closeAccount({ userId }) console.info(`User: ${user._key} successfully closed user: ${userId} account.`) await logActivity({ diff --git a/api/src/user/mutations/complete-tour.js b/api/src/user/mutations/complete-tour.js index 8fff7a9458..fa9870e738 100644 --- a/api/src/user/mutations/complete-tour.js +++ b/api/src/user/mutations/complete-tour.js @@ -23,7 +23,7 @@ export const completeTour = new mutationWithClientMutationId({ }), mutateAndGetPayload: async ( args, - { i18n, query, auth: { userRequired }, loaders: { loadUserByKey }, validators: { cleanseInput } }, + { i18n, auth: { userRequired }, dataSources: { user: userDataSource }, validators: { cleanseInput } }, ) => { // Cleanse Input const tourId = cleanseInput(args.tourId) @@ -41,31 +41,10 @@ export const completeTour = new mutationWithClientMutationId({ } // Complete tour - try { - const completeTourCursor = await query` - LET userCompleteTours = FIRST( - FOR user IN users - FILTER user._key == ${user._key} - LIMIT 1 - RETURN user.completedTours - ) - UPDATE { _key: ${user._key} } - WITH { - completedTours: APPEND( - userCompleteTours[* FILTER CURRENT.tourId != ${tourId}], - { tourId: ${tourId}, completedAt: DATE_ISO8601(DATE_NOW()) } - ) - } - IN users - ` - await completeTourCursor.next() - } catch (err) { - console.error(`Database error occurred when user: ${user._key} attempted to complete tour: ${tourId}: ${err}`) - throw new Error(i18n._(t`Unable to confirm completion of the tour. Please try again.`)) - } + await userDataSource.completeTour({ userKey: user._key, tourId }) - await loadUserByKey.clear(user._key) - const returnUser = await loadUserByKey.load(user._key) + await userDataSource.byKey.clear(user._key) + const returnUser = await userDataSource.byKey.load(user._key) console.info(`User: ${user._key} has confirmed completion of tour: ${tourId}`) return { diff --git a/api/src/user/mutations/dismiss-message.js b/api/src/user/mutations/dismiss-message.js index a5bdbc3871..ed27a02eaa 100644 --- a/api/src/user/mutations/dismiss-message.js +++ b/api/src/user/mutations/dismiss-message.js @@ -23,7 +23,7 @@ export const dismissMessage = new mutationWithClientMutationId({ }), mutateAndGetPayload: async ( args, - { i18n, query, auth: { userRequired }, loaders: { loadUserByKey }, validators: { cleanseInput } }, + { i18n, auth: { userRequired }, dataSources: { user: userDataSource }, validators: { cleanseInput } }, ) => { // Cleanse Input const messageId = cleanseInput(args.messageId) @@ -41,33 +41,10 @@ export const dismissMessage = new mutationWithClientMutationId({ } // Dismiss message - try { - const dismissMessageCursor = await query` - LET userDismissedMessages = FIRST( - FOR user IN users - FILTER user._key == ${user._key} - LIMIT 1 - RETURN user.dismissedMessages - ) - UPDATE { _key: ${user._key} } - WITH { - dismissedMessages: APPEND( - userDismissedMessages[* FILTER CURRENT.messageId != ${messageId}], - { messageId: ${messageId}, dismissedAt: DATE_ISO8601(DATE_NOW()) } - ) - } - IN users - ` - await dismissMessageCursor.next() - } catch (err) { - console.error( - `Database error occurred when user: ${user._key} attempted to dismiss message: ${messageId}: ${err}`, - ) - throw new Error(i18n._(t`Unable to dismiss message. Please try again.`)) - } + await userDataSource.dismissMessage({ userKey: user._key, messageId }) - await loadUserByKey.clear(user._key) - const returnUser = await loadUserByKey.load(user._key) + await userDataSource.byKey.clear(user._key) + const returnUser = await userDataSource.byKey.load(user._key) console.info(`User: ${user._key} successfully dismissed message: ${messageId}`) return { diff --git a/api/src/user/mutations/refresh-tokens.js b/api/src/user/mutations/refresh-tokens.js index 0028b753e5..35f31fda58 100644 --- a/api/src/user/mutations/refresh-tokens.js +++ b/api/src/user/mutations/refresh-tokens.js @@ -23,14 +23,11 @@ export const refreshTokens = new mutationWithClientMutationId({ i18n, response, request, - query, - collections, - transaction, uuidv4, jwt, moment, auth: { tokenize }, - loaders: { loadUserByKey }, + dataSources: { user: userDataSource }, }, ) => { // check uuid matches @@ -62,7 +59,7 @@ export const refreshTokens = new mutationWithClientMutationId({ const { userKey, uuid } = decodedRefreshToken.parameters - const user = await loadUserByKey.load(userKey) + const user = await userDataSource.byKey.load(userKey) if (typeof user === 'undefined') { console.warn(`User: ${userKey} attempted to refresh tokens with an invalid user id.`) @@ -104,32 +101,7 @@ export const refreshTokens = new mutationWithClientMutationId({ expiresAt: new Date(new Date().getTime() + ms(String(REFRESH_TOKEN_EXPIRY))), } - // Setup Transaction - const trx = await transaction(collections) - - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT { refreshInfo: ${refreshInfo} } - UPDATE { refreshInfo: ${refreshInfo} } - IN users - `, - ) - } catch (err) { - console.error(`Trx step error occurred when attempting to refresh tokens for user: ${userKey}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to refresh tokens, please sign in.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred while user: ${userKey} attempted to refresh tokens: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to refresh tokens, please sign in.`)) - } + await userDataSource.updateRefreshInfo({ userKey, refreshInfo }) const newAuthToken = tokenize({ expiresIn: AUTH_TOKEN_EXPIRY, @@ -141,7 +113,7 @@ export const refreshTokens = new mutationWithClientMutationId({ const newRefreshToken = tokenize({ expiresIn: REFRESH_TOKEN_EXPIRY, - parameters: { userKey: user._key, uuid: newRefreshId }, + parameters: { userKey, uuid: newRefreshId }, secret: String(REFRESH_KEY), }) diff --git a/api/src/user/mutations/remove-phone-number.js b/api/src/user/mutations/remove-phone-number.js index b1d449b1a0..5c7f3863b3 100644 --- a/api/src/user/mutations/remove-phone-number.js +++ b/api/src/user/mutations/remove-phone-number.js @@ -14,7 +14,7 @@ export const removePhoneNumber = new mutationWithClientMutationId({ resolve: (payload) => payload, }, }), - mutateAndGetPayload: async (_args, { i18n, collections, query, transaction, auth: { userRequired } }) => { + mutateAndGetPayload: async (_args, { i18n, auth: { userRequired }, dataSources: { user: userDataSource } }) => { // Get requesting user const user = await userRequired() @@ -24,40 +24,7 @@ export const removePhoneNumber = new mutationWithClientMutationId({ tfaSendMethod = 'email' } - // Setup Transaction - const trx = await transaction(collections) - - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT { - phoneDetails: null, - phoneValidated: false, - tfaSendMethod: ${tfaSendMethod} - } - UPDATE { - phoneDetails: null, - phoneValidated: false, - tfaSendMethod: ${tfaSendMethod} - } - IN users - `, - ) - } catch (err) { - console.error(`Trx step error occurred well removing phone number for user: ${user._key}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to remove phone number. Please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred well removing phone number for user: ${user._key}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to remove phone number. Please try again.`)) - } + await userDataSource.removePhoneNumber({ userKey: user._key, tfaSendMethod }) console.info(`User: ${user._key} successfully removed their phone number.`) return { diff --git a/api/src/user/mutations/reset-password.js b/api/src/user/mutations/reset-password.js index 8deb0bbb7b..2c872f34de 100644 --- a/api/src/user/mutations/reset-password.js +++ b/api/src/user/mutations/reset-password.js @@ -34,11 +34,8 @@ export const resetPassword = new mutationWithClientMutationId({ args, { i18n, - query, - collections, - transaction, auth: { verifyToken, bcrypt }, - loaders: { loadUserByKey }, + dataSources: { user: userDataSource }, validators: { cleanseInput }, }, ) => { @@ -63,7 +60,7 @@ export const resetPassword = new mutationWithClientMutationId({ } // Check if user exists - const user = await loadUserByKey.load(tokenParameters.userKey) + const user = await userDataSource.byKey.load(tokenParameters.userKey) // Replace with userRequired() if (typeof user === 'undefined') { @@ -104,34 +101,7 @@ export const resetPassword = new mutationWithClientMutationId({ // Update users password in db const hashedPassword = bcrypt.hashSync(password, 10) - // Setup Transaction - const trx = await transaction(collections) - - try { - await trx.step( - () => query` - WITH users - FOR user IN users - UPDATE ${user._key} - WITH { - password: ${hashedPassword}, - failedLoginAttempts: 0 - } IN users - `, - ) - } catch (err) { - console.error(`Trx step error occurred when user: ${user._key} attempted to reset their password: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to reset password. Please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred while user: ${user._key} attempted to authenticate: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to reset password. Please try again.`)) - } + await userDataSource.resetPassword({ userKey: user._key, hashedPassword }) console.info(`User: ${user._key} successfully reset their password.`) diff --git a/api/src/user/mutations/send-password-reset.js b/api/src/user/mutations/send-password-reset.js index 5578b94147..fe0fad9cef 100644 --- a/api/src/user/mutations/send-password-reset.js +++ b/api/src/user/mutations/send-password-reset.js @@ -31,14 +31,14 @@ export const sendPasswordResetLink = new mutationWithClientMutationId({ request, auth: { tokenize }, validators: { cleanseInput }, - loaders: { loadUserByUserName }, + dataSources: { user: userDataSource }, notify: { sendPasswordResetEmail }, }, ) => { // Cleanse Input const userName = cleanseInput(args.userName).toLowerCase() - const user = await loadUserByUserName.load(userName) + const user = await userDataSource.byUserName.load(userName) if (typeof user !== 'undefined') { const token = tokenize({ @@ -51,12 +51,15 @@ export const sendPasswordResetLink = new mutationWithClientMutationId({ await sendPasswordResetEmail({ user, resetUrl }) console.info(`User: ${user._key} successfully sent a password reset email.`) - } else { - console.warn( - `A user attempted to send a password reset email for ${userName} but no account is affiliated with this user name.`, - ) + return { + status: i18n._(t`If an account with this username is found, a password reset link will be found in your inbox.`), + } } + console.warn( + `A user attempted to send a password reset email for ${userName} but no account is affiliated with this user name.`, + ) + return { status: i18n._(t`If an account with this username is found, a password reset link will be found in your inbox.`), } diff --git a/api/src/user/mutations/set-phone-number.js b/api/src/user/mutations/set-phone-number.js index 09f874d6e7..6a2a3697ca 100644 --- a/api/src/user/mutations/set-phone-number.js +++ b/api/src/user/mutations/set-phone-number.js @@ -29,11 +29,8 @@ export const setPhoneNumber = new mutationWithClientMutationId({ args, { i18n, - query, - collections, - transaction, auth: { userRequired }, - loaders: { loadUserByKey }, + dataSources: { user: userDataSource }, validators: { cleanseInput }, notify: { sendAuthTextMsg }, }, @@ -67,47 +64,11 @@ export const setPhoneNumber = new mutationWithClientMutationId({ tfaSendMethod = 'email' } - // Setup Transaction - const trx = await transaction(collections) - - // Insert TFA code into DB - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT { - tfaCode: ${tfaCode}, - phoneDetails: ${phoneDetails}, - phoneValidated: false, - tfaSendMethod: ${tfaSendMethod} - } - UPDATE { - tfaCode: ${tfaCode}, - phoneDetails: ${phoneDetails}, - phoneValidated: false, - tfaSendMethod: ${tfaSendMethod} - } - IN users - `, - ) - } catch (err) { - console.error(`Trx step error occurred for user: ${user._key} when upserting phone number information: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to set phone number, please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred for user: ${user._key} when upserting phone number information: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to set phone number, please try again.`)) - } + await userDataSource.setPhoneNumber({ userKey: user._key, tfaCode, phoneDetails, tfaSendMethod }) // Get newly updated user - await loadUserByKey.clear(user._key) - user = await loadUserByKey.load(user._key) + await userDataSource.byKey.clear(user._key) + user = await userDataSource.byKey.load(user._key) await sendAuthTextMsg({ user }) diff --git a/api/src/user/mutations/sign-in.js b/api/src/user/mutations/sign-in.js index bf0efdcdda..6797c5414c 100644 --- a/api/src/user/mutations/sign-in.js +++ b/api/src/user/mutations/sign-in.js @@ -39,14 +39,10 @@ export const signIn = new mutationWithClientMutationId({ args, { i18n, - query, - collections, - transaction, uuidv4, response, - jwt, auth: { tokenize, bcrypt }, - loaders: { loadUserByUserName }, + dataSources: { user: userDataSource }, validators: { cleanseInput }, notify: { sendAuthEmail, sendAuthTextMsg }, }, @@ -57,7 +53,7 @@ export const signIn = new mutationWithClientMutationId({ const rememberMe = args.rememberMe // Gather user who just signed in - let user = await loadUserByUserName.load(userName) + let user = await userDataSource.byUserName.load(userName) // Replace with userRequired() if (typeof user === 'undefined') { @@ -77,214 +73,126 @@ export const signIn = new mutationWithClientMutationId({ code: 401, description: i18n._(t`Too many failed login attempts, please reset your password, and try again.`), } - } else { - // Setup Transaction - const trx = await transaction(collections) - - // Check to see if passwords match - if (bcrypt.compareSync(password, user.password)) { - // Reset Failed Login attempts - try { - await trx.step( - () => query` - WITH users - FOR u IN users - UPDATE ${user._key} WITH { failedLoginAttempts: 0 } IN users - `, - ) - } catch (err) { - console.error(`Trx step error occurred when resetting failed login attempts for user: ${user._key}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to sign in, please try again.`)) - } - - const refreshId = uuidv4() - const refreshInfo = { - refreshId, - expiresAt: new Date(new Date().getTime() + ms(String(REFRESH_TOKEN_EXPIRY))), - rememberMe, - } - - if (user.tfaSendMethod !== 'none') { - // Generate TFA code - const tfaCode = Math.floor(100000 + Math.random() * 900000) - - // Insert TFA code into DB - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT { - tfaCode: ${tfaCode}, - refreshInfo: ${refreshInfo} - } - UPDATE { - tfaCode: ${tfaCode}, - refreshInfo: ${refreshInfo} - } - IN users - `, - ) - } catch (err) { - console.error(`Trx step error occurred when inserting TFA code for user: ${user._key}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to sign in, please try again.`)) - } + } - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred while user: ${user._key} attempted to tfa sign in: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to sign in, please try again.`)) - } + const trx = await userDataSource.createTransaction() - // Get newly updated user - await loadUserByUserName.clear(userName) - user = await loadUserByUserName.load(userName) + // Check to see if passwords match + if (bcrypt.compareSync(password, user.password)) { + await userDataSource.signInResetFailedLoginAttempts({ userKey: user._key, trx }) - // Check if user's last successful login was over 30 days ago - let lastLogin - if (user.lastLogin) { - lastLogin = new Date(user.lastLogin) - } else { - lastLogin = new Date() - } - const currentDate = new Date() - const timeDifference = currentDate - lastLogin - const daysDifference = timeDifference / (1000 * 3600 * 24) + const refreshId = uuidv4() + const refreshInfo = { + refreshId, + expiresAt: new Date(new Date().getTime() + ms(String(REFRESH_TOKEN_EXPIRY))), + rememberMe, + } - // Check to see if user has phone validated - let sendMethod - if (user.tfaSendMethod === 'email' || daysDifference >= 30) { - await sendAuthEmail({ user }) - sendMethod = 'email' - } else { - await sendAuthTextMsg({ user }) - sendMethod = 'text' - } + if (user.tfaSendMethod !== 'none') { + // Generate TFA code + const tfaCode = Math.floor(100000 + Math.random() * 900000) - console.info(`User: ${user._key} successfully signed in, and sent auth msg.`) + await userDataSource.signInSetTfaCodeAndRefreshInfo({ userKey: user._key, tfaCode, refreshInfo, trx }) + await userDataSource.commitSignInTransaction({ trx, userKey: user._key, type: 'tfa' }) - const authenticateToken = tokenize({ - expiresIn: AUTH_TOKEN_EXPIRY, - parameters: { userKey: user._key }, - secret: String(SIGN_IN_KEY), // SIGN_IN_KEY is reserved for signing TFA tokens - }) + // Get newly updated user + await userDataSource.byUserName.clear(userName) + user = await userDataSource.byUserName.load(userName) - return { - _type: 'tfa', - sendMethod, - authenticateToken, - } + // Check if user's last successful login was over 30 days ago + let lastLogin + if (user.lastLogin) { + lastLogin = new Date(user.lastLogin) + } else { + lastLogin = new Date() + } + const currentDate = new Date() + const timeDifference = currentDate - lastLogin + const daysDifference = timeDifference / (1000 * 3600 * 24) + + // Check to see if user has phone validated + let sendMethod + if (user.tfaSendMethod === 'email' || daysDifference >= 30) { + await sendAuthEmail({ user }) + sendMethod = 'email' } else { - const loginDate = new Date().toISOString() - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT { refreshInfo: ${refreshInfo}, lastLogin: ${loginDate} } - UPDATE { refreshInfo: ${refreshInfo}, lastLogin: ${loginDate} } - IN users - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when attempting to setting refresh tokens for user: ${user._key} during sign in: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to sign in, please try again.`)) - } + await sendAuthTextMsg({ user }) + sendMethod = 'text' + } - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred while user: ${user._key} attempted a regular sign in: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to sign in, please try again.`)) - } + console.info(`User: ${user._key} successfully signed in, and sent auth msg.`) - const token = tokenize({ - expiresIn: AUTH_TOKEN_EXPIRY, - parameters: { userKey: user._key }, - secret: String(AUTHENTICATED_KEY), - }) + const authenticateToken = tokenize({ + expiresIn: AUTH_TOKEN_EXPIRY, + parameters: { userKey: user._key }, + secret: String(SIGN_IN_KEY), // SIGN_IN_KEY is reserved for signing TFA tokens + }) - const refreshToken = tokenize({ - expiresIn: REFRESH_TOKEN_EXPIRY, - parameters: { userKey: user._key, uuid: refreshId }, - secret: String(REFRESH_KEY), - }) + return { + _type: 'tfa', + sendMethod, + authenticateToken, + } + } - // if the user does not want to stay logged in, create http session cookie - let cookieData = { - httpOnly: true, - secure: true, - sameSite: true, - expires: 0, - } + const loginDate = new Date().toISOString() + await userDataSource.signInSetRefreshInfoAndLastLogin({ userKey: user._key, refreshInfo, loginDate, trx }) + await userDataSource.commitSignInTransaction({ trx, userKey: user._key, type: 'regular' }) + + const token = tokenize({ + expiresIn: AUTH_TOKEN_EXPIRY, + parameters: { userKey: user._key }, + secret: String(AUTHENTICATED_KEY), + }) + + const refreshToken = tokenize({ + expiresIn: REFRESH_TOKEN_EXPIRY, + parameters: { userKey: user._key, uuid: refreshId }, + secret: String(REFRESH_KEY), + }) + + // if the user does not want to stay logged in, create http session cookie + let cookieData = { + httpOnly: true, + secure: true, + sameSite: true, + expires: 0, + } - // if user wants to stay logged in create normal http cookie - if (rememberMe) { - const tokenMaxAgeSeconds = jwt.decode(refreshToken).exp - jwt.decode(refreshToken).iat - cookieData = { - maxAge: tokenMaxAgeSeconds * 1000, - httpOnly: true, - secure: true, - sameSite: true, - } - } + // if user wants to stay logged in create normal http cookie + if (rememberMe) { + cookieData = { + maxAge: ms(String(REFRESH_TOKEN_EXPIRY)), + httpOnly: true, + secure: true, + sameSite: true, + } + } - response.cookie('refresh_token', refreshToken, cookieData) + response.cookie('refresh_token', refreshToken, cookieData) - console.info(`User: ${user._key} successfully signed in, and sent auth msg.`) + console.info(`User: ${user._key} successfully signed in, and sent auth msg.`) - return { - _type: 'regular', - token, - user, - } - } - } else { - // increment failed login attempts - user.failedLoginAttempts += 1 + return { + _type: 'regular', + token, + user, + } + } - try { - // Increase users failed login attempts - await trx.step( - () => query` - WITH users - FOR u IN users - UPDATE ${user._key} WITH { - failedLoginAttempts: ${user.failedLoginAttempts} - } IN users - `, - ) - } catch (err) { - console.error( - `Trx step error occurred when incrementing failed login attempts for user: ${user._key}: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to sign in, please try again.`)) - } + // increment failed login attempts + user.failedLoginAttempts += 1 - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred while user: ${user._key} failed to sign in: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to sign in, please try again.`)) - } + await userDataSource.signInIncrementFailedLoginAttempts({ + userKey: user._key, + failedLoginAttempts: user.failedLoginAttempts, + }) - console.warn(`User attempted to authenticate: ${user._key} with invalid credentials.`) - return { - _type: 'error', - code: 400, - description: i18n._(t`Incorrect username or password. Please try again.`), - } - } + console.warn(`User attempted to authenticate: ${user._key} with invalid credentials.`) + return { + _type: 'error', + code: 400, + description: i18n._(t`Incorrect username or password. Please try again.`), } }, }) diff --git a/api/src/user/mutations/sign-up.js b/api/src/user/mutations/sign-up.js index 16dfb0eed7..c153529ee3 100644 --- a/api/src/user/mutations/sign-up.js +++ b/api/src/user/mutations/sign-up.js @@ -8,7 +8,7 @@ import { logActivity } from '../../audit-logs/mutations/log-activity' import ms from 'ms' import { emailUpdateOptionsType } from '../objects/email-update-options' -const { REFRESH_TOKEN_EXPIRY, SIGN_IN_KEY, AUTH_TOKEN_EXPIRY, TRACKER_PRODUCTION } = process.env +const { REFRESH_TOKEN_EXPIRY, SIGN_IN_KEY, AUTH_TOKEN_EXPIRY } = process.env export const signUp = new mutationWithClientMutationId({ name: 'SignUp', @@ -57,7 +57,7 @@ export const signUp = new mutationWithClientMutationId({ uuidv4, request: { ip }, auth: { bcrypt, tokenize, verifyToken }, - loaders: { loadOrgByKey, loadUserByUserName, loadUserByKey }, + dataSources: { user: userDataSource, organization: organizationDataSource }, notify: { sendAuthEmail }, validators: { cleanseInput }, }, @@ -70,7 +70,7 @@ export const signUp = new mutationWithClientMutationId({ const signUpToken = cleanseInput(args.signUpToken) const rememberMe = args.rememberMe - const isProduction = TRACKER_PRODUCTION === 'true' + const isProduction = process.env.TRACKER_PRODUCTION !== 'false' if (isProduction === false) { console.warn(`User: ${userName} tried to sign up but did not meet requirements.`) return { @@ -101,7 +101,7 @@ export const signUp = new mutationWithClientMutationId({ } // Check to see if user already exists - const checkUser = await loadUserByUserName.load(userName) + const checkUser = await userDataSource.byUserName.load(userName) if (typeof checkUser !== 'undefined') { console.warn(`User: ${userName} tried to sign up, however there is already an account in use with that email.`) @@ -142,40 +142,7 @@ export const signUp = new mutationWithClientMutationId({ }, } - // Setup Transaction - const trx = await transaction(collections) - - let insertedUserCursor - try { - insertedUserCursor = await trx.step( - () => query` - WITH users - INSERT ${user} INTO users - RETURN MERGE( - { - id: NEW._key, - _type: "user" - }, - NEW - ) - `, - ) - } catch (err) { - console.error( - `Transaction step error occurred while user: ${userName} attempted to sign up, creating user: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to sign up. Please try again.`)) - } - - let insertedUser - try { - insertedUser = await insertedUserCursor.next() - } catch (err) { - console.error(`Cursor error occurred while user: ${userName} attempted to sign up, creating user: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to sign up. Please try again.`)) - } + const { trx, insertedUser } = await userDataSource.insertUser({ user, userName }) // Assign user to org if (signUpToken !== '') { @@ -198,7 +165,7 @@ export const signUp = new mutationWithClientMutationId({ } } - const checkOrg = await loadOrgByKey.load(tokenOrgKey) + const checkOrg = await organizationDataSource.byKey.load(tokenOrgKey) if (typeof checkOrg === 'undefined') { console.warn(`User: ${userName} attempted to sign up with an invite token, however the org could not be found.`) await trx.abort() @@ -209,36 +176,18 @@ export const signUp = new mutationWithClientMutationId({ } } - try { - await trx.step( - () => - query` - WITH affiliations, organizations, users - INSERT { - _from: ${checkOrg._id}, - _to: ${insertedUser._id}, - permission: ${tokenRequestedRole}, - } INTO affiliations - `, - ) - } catch (err) { - console.error( - `Transaction step error occurred while user: ${userName} attempted to sign up, assigning affiliation: ${err}`, - ) - await trx.abort() - throw new Error(i18n._(t`Unable to sign up. Please try again.`)) - } + await userDataSource.addAffiliation({ + trx, + orgId: checkOrg._id || `organizations/${checkOrg._key}`, + userId: insertedUser._id || `users/${insertedUser._key}`, + permission: tokenRequestedRole, + userName, + }) } - try { - await trx.commit() - } catch (err) { - console.error(`Transaction commit error occurred while user: ${userName} attempted to sign up: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to sign up. Please try again.`)) - } + await userDataSource.commitSignUpTransaction({ trx, userName }) - const returnUser = await loadUserByKey.load(insertedUser._key) + const returnUser = await userDataSource.byKey.load(insertedUser._key) await sendAuthEmail({ user: returnUser }) const authenticateToken = tokenize({ diff --git a/api/src/user/mutations/update-user-password.js b/api/src/user/mutations/update-user-password.js index 78d07d6b6b..21e10e9323 100644 --- a/api/src/user/mutations/update-user-password.js +++ b/api/src/user/mutations/update-user-password.js @@ -31,7 +31,7 @@ export const updateUserPassword = new mutationWithClientMutationId({ }), mutateAndGetPayload: async ( args, - { i18n, query, collections, transaction, auth: { bcrypt, userRequired }, validators: { cleanseInput } }, + { i18n, auth: { bcrypt, userRequired }, dataSources: { user: userDataSource }, validators: { cleanseInput } }, ) => { // Cleanse Input const currentPassword = cleanseInput(args.currentPassword) @@ -78,30 +78,7 @@ export const updateUserPassword = new mutationWithClientMutationId({ // Update password in DB const hashedPassword = bcrypt.hashSync(updatedPassword, 10) - // Setup Transaction - const trx = await transaction(collections) - - try { - await trx.step( - () => query` - WITH users - FOR user IN users - UPDATE ${user._key} WITH { password: ${hashedPassword} } IN users - `, - ) - } catch (err) { - console.error(`Trx step error occurred when user: ${user._key} attempted to update their password: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to update password. Please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred when user: ${user._key} attempted to update their password: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to update password. Please try again.`)) - } + await userDataSource.updatePassword({ userKey: user._key, hashedPassword }) console.info(`User: ${user._key} successfully updated their password.`) return { diff --git a/api/src/user/mutations/update-user-profile.js b/api/src/user/mutations/update-user-profile.js index b7619fc81f..ffd747e49d 100644 --- a/api/src/user/mutations/update-user-profile.js +++ b/api/src/user/mutations/update-user-profile.js @@ -48,13 +48,10 @@ export const updateUserProfile = new mutationWithClientMutationId({ args, { i18n, - query, - collections, - transaction, userKey, request, auth: { tokenize, userRequired }, - loaders: { loadUserByKey, loadUserByUserName }, + dataSources: { user: userDataSource }, notify: { sendVerificationEmail }, validators: { cleanseInput }, }, @@ -71,7 +68,7 @@ export const updateUserProfile = new mutationWithClientMutationId({ // Check to see if username is already in use if (userName !== '') { - const checkUser = await loadUserByUserName.load(userName) + const checkUser = await userDataSource.byUserName.load(userName) if (typeof checkUser !== 'undefined') { console.warn(`User: ${userKey} attempted to update their username, but the username is already in use.`) return { @@ -84,22 +81,8 @@ export const updateUserProfile = new mutationWithClientMutationId({ // Check to see if admin user is disabling TFA if (subTfaSendMethod === 'none') { - // check to see if user is an admin or higher for at least one org - let userAdmin - try { - userAdmin = await query` - WITH users, affiliations - FOR v, e IN 1..1 INBOUND ${user._id} affiliations - FILTER e.permission IN ["admin", "owner", "super_admin"] - LIMIT 1 - RETURN e.permission - ` - } catch (err) { - console.error(`Database error occurred when user: ${userKey} was seeing if they were an admin, err: ${err}`) - throw new Error(i18n._(t`Unable to verify if user is an admin, please try again.`)) - } - - if (userAdmin.count > 0) { + const userAdmin = await userDataSource.isAdminForAnyOrg({ userId: user._id }) + if (userAdmin) { console.error( `User: ${userKey} attempted to remove MFA, however they are an admin of at least one organization.`, ) @@ -135,35 +118,10 @@ export const updateUserProfile = new mutationWithClientMutationId({ emailUpdateOptions: typeof emailUpdateOptions !== 'undefined' ? emailUpdateOptions : user?.emailUpdateOptions, } - // Setup Transaction - const trx = await transaction(collections) - - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT ${updatedUser} - UPDATE ${updatedUser} - IN users - `, - ) - } catch (err) { - console.error(`Trx step error occurred when user: ${userKey} attempted to update their profile: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to update profile. Please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred when user: ${userKey} attempted to update their profile: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to update profile. Please try again.`)) - } + await userDataSource.updateProfile({ userKey: user._key, updatedUser }) - await loadUserByKey.clear(user._key) - const returnUser = await loadUserByKey.load(userKey) + await userDataSource.byKey.clear(user._key) + const returnUser = await userDataSource.byKey.load(userKey) if (changedUserName) { const token = tokenize({ diff --git a/api/src/user/mutations/verify-account.js b/api/src/user/mutations/verify-account.js index 19fa3a894c..438352b99c 100644 --- a/api/src/user/mutations/verify-account.js +++ b/api/src/user/mutations/verify-account.js @@ -25,11 +25,8 @@ export const verifyAccount = new mutationWithClientMutationId({ args, { i18n, - query, - collections, - transaction, auth: { verifyToken }, - loaders: { loadUserByKey, loadUserByUserName }, + dataSources: { user: userDataSource }, notify: { sendUpdatedUserNameEmail }, validators: { cleanseInput }, }, @@ -67,7 +64,7 @@ export const verifyAccount = new mutationWithClientMutationId({ // Auth shouldn't be needed with this // Check if user exists const { userKey, userName: newUserName } = tokenParameters - const user = await loadUserByKey.load(userKey) + const user = await userDataSource.byKey.load(userKey) if (typeof user === 'undefined') { console.warn(`User: ${userKey} attempted to verify account, however no account is associated with this id.`) @@ -79,7 +76,7 @@ export const verifyAccount = new mutationWithClientMutationId({ } // Ensure newUserName is still not already in use - const checkUser = await loadUserByUserName.load(newUserName) + const checkUser = await userDataSource.byUserName.load(newUserName) if (typeof checkUser !== 'undefined') { console.warn(`User: ${userKey} attempted to update their username, but the username is already in use.`) return { @@ -102,39 +99,7 @@ export const verifyAccount = new mutationWithClientMutationId({ throw new Error(i18n._(t`Unable to send updated username email. Please try again.`)) } - // Setup Transaction - const trx = await transaction(collections) - - // Verify users account - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT { - emailValidated: true, - userName: ${newUserName}, - } - UPDATE { - emailValidated: true, - userName: ${newUserName}, - } - IN users - `, - ) - } catch (err) { - console.error(`Trx step error occurred when upserting email validation for user: ${user._key}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to verify account. Please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred when upserting email validation for user: ${user._key}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to verify account. Please try again.`)) - } + await userDataSource.verifyAccount({ userKey: user._key, newUserName }) console.info(`User: ${user._key} successfully email validated their account.`) diff --git a/api/src/user/mutations/verify-phone-number.js b/api/src/user/mutations/verify-phone-number.js index bcbf7d0fca..ce31cdb38c 100644 --- a/api/src/user/mutations/verify-phone-number.js +++ b/api/src/user/mutations/verify-phone-number.js @@ -23,7 +23,7 @@ export const verifyPhoneNumber = new mutationWithClientMutationId({ }), mutateAndGetPayload: async ( args, - { i18n, userKey, query, collections, transaction, auth: { userRequired }, loaders: { loadUserByKey } }, + { i18n, userKey, auth: { userRequired }, dataSources: { user: userDataSource } }, ) => { // Cleanse Input const twoFactorCode = args.twoFactorCode @@ -52,36 +52,10 @@ export const verifyPhoneNumber = new mutationWithClientMutationId({ } } - // Setup Transaction - const trx = await transaction(collections) + await userDataSource.verifyPhoneNumber({ userKey: user._key }) - // Update phoneValidated to be true - try { - await trx.step( - () => query` - WITH users - UPSERT { _key: ${user._key} } - INSERT { phoneValidated: true } - UPDATE { phoneValidated: true } - IN users - `, - ) - } catch (err) { - console.error(`Trx step error occurred when upserting the tfaValidate field for ${user._key}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to two factor authenticate. Please try again.`)) - } - - try { - await trx.commit() - } catch (err) { - console.error(`Trx commit error occurred when upserting the tfaValidate field for ${user._key}: ${err}`) - await trx.abort() - throw new Error(i18n._(t`Unable to two factor authenticate. Please try again.`)) - } - - await loadUserByKey.clear(userKey) - const updatedUser = await loadUserByKey.load(userKey) + await userDataSource.byKey.clear(userKey) + const updatedUser = await userDataSource.byKey.load(userKey) console.info(`User: ${user._key} successfully two factor authenticated their account.`) diff --git a/api/src/user/objects/__tests__/user-personal.test.js b/api/src/user/objects/__tests__/user-personal.test.js index a5aeeeb708..78a1d46eef 100644 --- a/api/src/user/objects/__tests__/user-personal.test.js +++ b/api/src/user/objects/__tests__/user-personal.test.js @@ -218,8 +218,10 @@ describe('given the user object', () => { { _id: '1' }, { first: 1 }, { - loaders: { - loadAffiliationConnectionsByUserId: jest.fn().mockReturnValue(expectedResult), + dataSources: { + affiliation: { + connectionsByUserId: jest.fn().mockReturnValue(expectedResult), + }, }, }, ), diff --git a/api/src/user/objects/user-personal.js b/api/src/user/objects/user-personal.js index 453160df06..7580e648cd 100644 --- a/api/src/user/objects/user-personal.js +++ b/api/src/user/objects/user-personal.js @@ -74,8 +74,8 @@ export const userPersonalType = new GraphQLObjectType({ }, ...connectionArgs, }, - resolve: async ({ _id }, args, { loaders: { loadAffiliationConnectionsByUserId } }) => { - const affiliations = await loadAffiliationConnectionsByUserId({ + resolve: async ({ _id }, args, { dataSources: { affiliation } }) => { + const affiliations = await affiliation.connectionsByUserId({ userId: _id, ...args, }) diff --git a/api/src/user/objects/user-shared.js b/api/src/user/objects/user-shared.js index 188c3c0e5c..bf6006cabf 100644 --- a/api/src/user/objects/user-shared.js +++ b/api/src/user/objects/user-shared.js @@ -47,9 +47,9 @@ export const userSharedType = new GraphQLObjectType({ resolve: async ( {_id}, args, - {loaders: {loadAffiliationConnectionsByUserId}}, + {dataSources: {affiliation}}, ) => { - const affiliations = await loadAffiliationConnectionsByUserId({ + const affiliations = await affiliation.connectionsByUserId({ userId: _id, ...args, }) diff --git a/api/src/user/queries/__tests__/find-me.test.js b/api/src/user/queries/__tests__/find-me.test.js index 68d8a5ad98..58c81fee6d 100644 --- a/api/src/user/queries/__tests__/find-me.test.js +++ b/api/src/user/queries/__tests__/find-me.test.js @@ -6,9 +6,7 @@ import { toGlobalId } from 'graphql-relay' import { userRequired } from '../../../auth' import { createQuerySchema } from '../../../query' import { createMutationSchema } from '../../../mutation' -import { loadAffiliationConnectionsByUserId } from '../../../affiliation/loaders' import { loadUserByKey } from '../../loaders' -import { cleanseInput } from '../../../validators' import dbschema from '../../../../database.json' const { DB_PASS: rootPass, DB_URL: url } = process.env @@ -82,13 +80,6 @@ describe('given the findMe query', () => { loadUserByKey: loadUserByKey({ query }), }), }, - loaders: { - loadAffiliationConnectionsByUserId: loadAffiliationConnectionsByUserId({ - query, - userKey: user._key, - cleanseInput, - }), - }, }, }) diff --git a/api/src/user/queries/__tests__/find-my-tracker.test.js b/api/src/user/queries/__tests__/find-my-tracker.test.js index 27791a5993..269edaff45 100644 --- a/api/src/user/queries/__tests__/find-my-tracker.test.js +++ b/api/src/user/queries/__tests__/find-my-tracker.test.js @@ -1,6 +1,6 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { setupI18n } from '@lingui/core' import englishMessages from '../../../locale/en/messages' @@ -10,6 +10,7 @@ import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { userRequired, verifiedRequired } from '../../../auth' import { loadUserByKey, loadMyTrackerByUserId } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import { DomainDataSource } from '../../../domain/data-source' import dbschema from '../../../../database.json' @@ -411,3 +412,4 @@ describe('given findMyTracker query', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/queries/__tests__/find-my-users.test.js b/api/src/user/queries/__tests__/find-my-users.test.js index f4a7bed846..59556093c9 100644 --- a/api/src/user/queries/__tests__/find-my-users.test.js +++ b/api/src/user/queries/__tests__/find-my-users.test.js @@ -1,6 +1,6 @@ import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { toGlobalId } from 'graphql-relay' import { setupI18n } from '@lingui/core' @@ -11,6 +11,7 @@ import { createMutationSchema } from '../../../mutation' import { cleanseInput } from '../../../validators' import { checkSuperAdmin, superAdminRequired, userRequired, verifiedRequired } from '../../../auth' import { loadUserByKey, loadUserConnectionsByUserId } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import dbschema from '../../../../database.json' const { DB_PASS: rootPass, DB_URL: url } = process.env @@ -373,3 +374,4 @@ describe('given findMyUsersQuery', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/queries/__tests__/find-user-by-username.test.js b/api/src/user/queries/__tests__/find-user-by-username.test.js index 6006d15417..dd306aab0b 100644 --- a/api/src/user/queries/__tests__/find-user-by-username.test.js +++ b/api/src/user/queries/__tests__/find-user-by-username.test.js @@ -1,7 +1,7 @@ import { setupI18n } from '@lingui/core' import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLSchema, GraphQLError } from 'graphql' +import { graphql as executeGraphql, GraphQLSchema, GraphQLError } from 'graphql' import { toGlobalId } from 'graphql-relay' import { userRequired, checkUserIsAdminForUser } from '../../../auth' @@ -9,6 +9,7 @@ import { createQuerySchema } from '../../../query' import { cleanseInput } from '../../../validators' import { createMutationSchema } from '../../../mutation' import { loadUserByKey, loadUserByUserName } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' import dbschema from '../../../../database.json' @@ -566,3 +567,4 @@ describe('given the findUserByUsername query', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/queries/__tests__/is-user-admin.test.js b/api/src/user/queries/__tests__/is-user-admin.test.js index 3328d3ec5e..be514aa10b 100644 --- a/api/src/user/queries/__tests__/is-user-admin.test.js +++ b/api/src/user/queries/__tests__/is-user-admin.test.js @@ -1,7 +1,7 @@ import { setupI18n } from '@lingui/core' import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLError, GraphQLSchema } from 'graphql' +import { graphql as executeGraphql, GraphQLError, GraphQLSchema } from 'graphql' import { toGlobalId } from 'graphql-relay' import { checkPermission, userRequired } from '../../../auth' @@ -10,6 +10,7 @@ import { createMutationSchema } from '../../../mutation' import { loadUserByKey } from '../../loaders' import { cleanseInput } from '../../../validators' import { loadOrgByKey } from '../../../organization/loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' import dbschema from '../../../../database.json' @@ -414,6 +415,7 @@ describe('given the isUserAdmin query', () => { i18n, userKey: 123, query: jest.fn().mockRejectedValue(new Error('Database error occurred.')), + language: 'fr', auth: { checkPermission: jest.fn(), userRequired: jest.fn().mockReturnValue({ @@ -486,9 +488,7 @@ describe('given the isUserAdmin query', () => { }, }) - const error = [ - new GraphQLError(`Impossible de vérifier si l'utilisateur est un administrateur, veuillez réessayer.`), - ] + const error = [new GraphQLError(`Unable to verify if user is an admin, please try again.`)] expect(response.errors).toEqual(error) expect(consoleOutput).toEqual([ @@ -499,3 +499,4 @@ describe('given the isUserAdmin query', () => { }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/queries/__tests__/is-user-super-admin.test.js b/api/src/user/queries/__tests__/is-user-super-admin.test.js index 48dfa1cf39..9bf761cff0 100644 --- a/api/src/user/queries/__tests__/is-user-super-admin.test.js +++ b/api/src/user/queries/__tests__/is-user-super-admin.test.js @@ -1,12 +1,13 @@ import { setupI18n } from '@lingui/core' import { dbNameFromFile } from 'arango-tools' import { ensureDatabase as ensure } from '../../../testUtilities' -import { graphql, GraphQLError, GraphQLSchema } from 'graphql' +import { graphql as executeGraphql, GraphQLError, GraphQLSchema } from 'graphql' import { checkPermission, userRequired } from '../../../auth' import { createQuerySchema } from '../../../query' import { createMutationSchema } from '../../../mutation' import { loadUserByKey } from '../../loaders' +import { withDataSources } from '../../test-helpers/with-data-sources' import englishMessages from '../../../locale/en/messages' import frenchMessages from '../../../locale/fr/messages' import dbschema from '../../../../database.json' @@ -114,7 +115,7 @@ describe('given the isUserSuperAdmin query', () => { rootValue: null, contextValue: { userKey: user._key, - query: query, + query, auth: { checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ @@ -157,7 +158,7 @@ describe('given the isUserSuperAdmin query', () => { rootValue: null, contextValue: { userKey: user._key, - query: query, + query, auth: { checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ @@ -200,7 +201,7 @@ describe('given the isUserSuperAdmin query', () => { rootValue: null, contextValue: { userKey: user._key, - query: query, + query, auth: { checkPermission: checkPermission({ userKey: user._key, query }), userRequired: userRequired({ @@ -253,6 +254,7 @@ describe('given the isUserSuperAdmin query', () => { i18n, userKey: 123, query: jest.fn().mockRejectedValue(new Error('Database error occurred.')), + language: 'fr', auth: { checkPermission: jest.fn(), userRequired: jest.fn().mockReturnValue({ @@ -271,56 +273,6 @@ describe('given the isUserSuperAdmin query', () => { }) }) }) - describe('users language is set to french', () => { - beforeAll(() => { - i18n = setupI18n({ - locale: 'fr', - localeData: { - en: { plurals: {} }, - fr: { plurals: {} }, - }, - locales: ['en', 'fr'], - messages: { - en: englishMessages.messages, - fr: frenchMessages.messages, - }, - }) - }) - describe('database error occurs', () => { - it('returns an error message', async () => { - const response = await graphql({ - schema, - source: ` - query { - isUserSuperAdmin - } - `, - rootValue: null, - contextValue: { - i18n, - userKey: 123, - query: jest.fn().mockRejectedValue(new Error('Database error occurred.')), - auth: { - checkPermission: jest.fn(), - userRequired: jest.fn().mockReturnValue({ - _id: 'users/123', - _key: 123, - }), - }, - }, - }) - - const error = [ - new GraphQLError( - `Impossible de vérifier si l'utilisateur est un super administrateur, veuillez réessayer.`, - ), - ] - expect(response.errors).toEqual(error) - expect(consoleOutput).toEqual([ - `Database error occurred when user: 123 was seeing if they were a super admin, err: Error: Database error occurred.`, - ]) - }) - }) - }) }) }) +const graphql = (args) => executeGraphql({ ...args, contextValue: withDataSources(args.contextValue) }) diff --git a/api/src/user/queries/find-my-tracker.js b/api/src/user/queries/find-my-tracker.js index 0f73c9cb57..029e39bdfb 100644 --- a/api/src/user/queries/find-my-tracker.js +++ b/api/src/user/queries/find-my-tracker.js @@ -13,7 +13,7 @@ export const findMyTracker = { i18n, userKey, auth: { userRequired, verifiedRequired }, - loaders: { loadMyTrackerByUserId }, + dataSources: { user: userDataSource }, }, ) => { // Get User @@ -21,7 +21,7 @@ export const findMyTracker = { verifiedRequired({ user }) // Retrieve organization by slug - const myTracker = await loadMyTrackerByUserId() + const myTracker = await userDataSource.myTrackerByUserId() if (typeof myTracker === 'undefined') { console.warn(`User ${userKey} could not retrieve organization.`) diff --git a/api/src/user/queries/find-my-users.js b/api/src/user/queries/find-my-users.js index 38ae198971..982b60c61b 100644 --- a/api/src/user/queries/find-my-users.js +++ b/api/src/user/queries/find-my-users.js @@ -24,7 +24,7 @@ export const findMyUsers = { { userKey, auth: { checkSuperAdmin, userRequired, verifiedRequired, superAdminRequired }, - loaders: { loadUserConnectionsByUserId }, + dataSources: { user: userDataSource }, }, ) => { const user = await userRequired() @@ -33,7 +33,7 @@ export const findMyUsers = { const isSuperAdmin = await checkSuperAdmin() superAdminRequired({ user, isSuperAdmin }) - const userConnections = await loadUserConnectionsByUserId({ + const userConnections = await userDataSource.connectionsByUserId({ isSuperAdmin, ...args, }) diff --git a/api/src/user/queries/find-user-by-username.js b/api/src/user/queries/find-user-by-username.js index dcd8c3c986..ebc7adcf7a 100644 --- a/api/src/user/queries/find-user-by-username.js +++ b/api/src/user/queries/find-user-by-username.js @@ -20,7 +20,7 @@ export const findUserByUsername = { i18n, userKey, auth: { userRequired, checkUserIsAdminForUser }, - loaders: { loadUserByUserName }, + dataSources: { user: userDataSource }, validators: { cleanseInput }, }, ) => { @@ -33,7 +33,7 @@ export const findUserByUsername = { if (permission) { // Retrieve user by userName - const user = await loadUserByUserName.load(userName) + const user = await userDataSource.byUserName.load(userName) user.id = user._key return user } else { diff --git a/api/src/user/queries/is-user-admin.js b/api/src/user/queries/is-user-admin.js index 108947b7a2..27fd49f500 100644 --- a/api/src/user/queries/is-user-admin.js +++ b/api/src/user/queries/is-user-admin.js @@ -1,4 +1,3 @@ -import { t } from '@lingui/macro' import { GraphQLBoolean, GraphQLID } from 'graphql' import { fromGlobalId } from 'graphql-relay' @@ -15,11 +14,8 @@ export const isUserAdmin = { _, args, { - i18n, - query, - userKey, auth: { checkPermission, userRequired }, - loaders: { loadOrgByKey }, + dataSources: { user: userDataSource, organization: organizationDataSource }, validators: { cleanseInput }, }, ) => { @@ -28,27 +24,13 @@ export const isUserAdmin = { // check if for a specific org if (orgKey) { - const org = await loadOrgByKey.load(orgKey) + const org = await organizationDataSource.byKey.load(orgKey) const permission = await checkPermission({ orgId: org._id }) return ['admin', 'owner', 'super_admin'].includes(permission) } // check to see if user is an admin or higher for at least one org - let userAdmin - try { - userAdmin = await query` - WITH users, affiliations - FOR v, e IN 1..1 INBOUND ${user._id} affiliations - FILTER e.permission IN ["admin", "owner", "super_admin"] - LIMIT 1 - RETURN e.permission - ` - } catch (err) { - console.error(`Database error occurred when user: ${userKey} was seeing if they were an admin, err: ${err}`) - throw new Error(i18n._(t`Unable to verify if user is an admin, please try again.`)) - } - - return userAdmin.count > 0 + return userDataSource.isAdminForAnyOrg({ userId: user._id }) }, } diff --git a/api/src/user/queries/is-user-super-admin.js b/api/src/user/queries/is-user-super-admin.js index f634f08ac9..443a30932e 100644 --- a/api/src/user/queries/is-user-super-admin.js +++ b/api/src/user/queries/is-user-super-admin.js @@ -1,34 +1,10 @@ -import {GraphQLBoolean} from 'graphql' -import {t} from '@lingui/macro' +import { GraphQLBoolean } from 'graphql' export const isUserSuperAdmin = { type: GraphQLBoolean, description: 'Query used to check if the user has a super admin role.', - resolve: async (_, __, {i18n, query, userKey, auth: {userRequired}}) => { + resolve: async (_, __, { auth: { userRequired }, dataSources: { user: userDataSource } }) => { const user = await userRequired() - - let userAdmin - try { - userAdmin = await query` - WITH users, affiliations - FOR v, e IN 1..1 INBOUND ${user._id} affiliations - FILTER e.permission == "super_admin" - LIMIT 1 - RETURN e.permission - ` - } catch (err) { - console.error( - `Database error occurred when user: ${userKey} was seeing if they were a super admin, err: ${err}`, - ) - throw new Error( - i18n._(t`Unable to verify if user is a super admin, please try again.`), - ) - } - - if (userAdmin.count > 0) { - return true - } - - return false + return userDataSource.isSuperAdmin({ userId: user._id }) }, } diff --git a/api/src/user/test-helpers/with-data-sources.js b/api/src/user/test-helpers/with-data-sources.js new file mode 100644 index 0000000000..73e4d5db10 --- /dev/null +++ b/api/src/user/test-helpers/with-data-sources.js @@ -0,0 +1,67 @@ +import { UserDataSource } from '../data-source' + +export const withDataSources = (contextValue = {}) => { + const loaders = contextValue.loaders || {} + + const fallbackI18n = { + _: (value) => (typeof value === 'string' ? value : String(value)), + } + + const fallbackTransaction = async () => ({ + step: async () => undefined, + commit: async () => undefined, + abort: async () => undefined, + }) + + const fallbackQuery = async () => ({ + count: 0, + next: async () => undefined, + all: async () => [], + }) + + const baseUserDataSource = new UserDataSource({ + query: contextValue.query || fallbackQuery, + userKey: contextValue.userKey, + i18n: contextValue.i18n || fallbackI18n, + language: contextValue.language || 'en', + cleanseInput: contextValue.validators?.cleanseInput || ((value) => value), + transaction: contextValue.transaction || fallbackTransaction, + collections: contextValue.collections || {}, + }) + + const userDataSource = contextValue.dataSources?.user + ? Object.assign(baseUserDataSource, contextValue.dataSources.user) + : baseUserDataSource + + if (contextValue.i18n) userDataSource._i18n = contextValue.i18n + + if (loaders.loadUserByKey) userDataSource.byKey = loaders.loadUserByKey + if (loaders.loadUserByUserName) userDataSource.byUserName = loaders.loadUserByUserName + if (loaders.loadMyTrackerByUserId) userDataSource.myTrackerByUserId = loaders.loadMyTrackerByUserId + if (loaders.loadUserConnectionsByUserId) userDataSource.connectionsByUserId = loaders.loadUserConnectionsByUserId + + const dataSources = { + ...(contextValue.dataSources || {}), + user: userDataSource, + } + + if (loaders.loadOrgByKey) { + dataSources.organization = { + byKey: { + load: async (orgKey) => { + const org = await loaders.loadOrgByKey.load(orgKey) + + if (!org) return org + if (org._id) return org + + return { + ...org, + _id: org._key ? `organizations/${org._key}` : org._id, + } + }, + }, + } + } + + return { ...contextValue, dataSources } +}