diff --git a/packages/server/modules/activitystream/events/streamInviteListeners.ts b/packages/server/modules/activitystream/events/streamInviteListeners.ts index f8317c15b..90ed453d0 100644 --- a/packages/server/modules/activitystream/events/streamInviteListeners.ts +++ b/packages/server/modules/activitystream/events/streamInviteListeners.ts @@ -49,7 +49,7 @@ const addStreamInviteAcceptedActivityFactory = getProjectInviteProject: GetProjectInviteProject }) => async (payload: EventPayload) => { - const { invite } = payload.payload + const { invite, trueFinalizerUserId } = payload.payload const project = await deps.getProjectInviteProject({ invite }) if (!project) return @@ -58,14 +58,18 @@ const addStreamInviteAcceptedActivityFactory = getResourceTypeRole(invite.resource, ProjectInviteResourceType) || Roles.Stream.Contributor + const differentFinalizer = trueFinalizerUserId !== userTarget.userId + await deps.saveActivity({ streamId: project.id, resourceType: ResourceTypes.Stream, resourceId: project.id, actionType: ActionTypes.Stream.InviteAccepted, - userId: userTarget.userId!, - info: { inviterUser: invite.inviterId, role }, - message: `User ${userTarget.userId!} has accepted an invitation to become a ${role}` + userId: trueFinalizerUserId, + info: { inviterUser: invite.inviterId, role, targetUserId: userTarget.userId! }, + message: differentFinalizer + ? `User ${trueFinalizerUserId} has auto-accepted ${userTarget.userId!} invitation to become a ${role}` + : `User ${userTarget.userId!} has accepted an invitation to become a ${role}` }) } diff --git a/packages/server/modules/activitystream/tests/activitySummary.spec.ts b/packages/server/modules/activitystream/tests/activitySummary.spec.ts index e5363d3a3..d415fb922 100644 --- a/packages/server/modules/activitystream/tests/activitySummary.spec.ts +++ b/packages/server/modules/activitystream/tests/activitySummary.spec.ts @@ -20,7 +20,8 @@ import { db } from '@/db/knex' import { createStreamFactory, deleteStreamFactory, - getStreamFactory + getStreamFactory, + grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { createStreamReturnRecordFactory, @@ -29,8 +30,12 @@ import { import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' import { + deleteInvitesByTargetFactory, + deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, - insertInviteAndDeleteOldFactory + insertInviteAndDeleteOldFactory, + updateAllInviteTargetsFactory } from '@/modules/serverinvites/repositories/serverInvites' import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' @@ -38,6 +43,29 @@ import { getEventBus } from '@/modules/shared/services/eventBus' import { createBranchFactory } from '@/modules/core/repositories/branches' import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users' import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' +import { + createUserEmailFactory, + ensureNoPrimaryEmailForUserFactory, + findEmailFactory +} from '@/modules/core/repositories/userEmails' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' const cleanup = async () => { await truncateTables([StreamActivity.name, Users.name]) @@ -53,6 +81,52 @@ const createActivitySummary = createActivitySummaryFactory({ getActivity: getActivityFactory({ db }), getUser }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -71,7 +145,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/auth/tests/auth.spec.ts b/packages/server/modules/auth/tests/auth.spec.ts index f8af76395..fc12fc899 100644 --- a/packages/server/modules/auth/tests/auth.spec.ts +++ b/packages/server/modules/auth/tests/auth.spec.ts @@ -12,7 +12,8 @@ import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory + updateAllInviteTargetsFactory, + deleteInvitesByTargetFactory } from '@/modules/serverinvites/repositories/serverInvites' import { db } from '@/db/knex' import { @@ -24,7 +25,8 @@ import { createAndSendInviteFactory } from '@/modules/serverinvites/services/cre import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' import { getStreamFactory, - createStreamFactory + createStreamFactory, + grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' import { getEventBus } from '@/modules/shared/services/eventBus' @@ -48,7 +50,10 @@ import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { createUserFactory } from '@/modules/core/services/users/management' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' -import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { getServerInfoFactory, updateServerInfoFactory @@ -57,6 +62,15 @@ import { temporarilyEnableRateLimiter } from '@/modules/core/tests/ratelimiter.s import { passportAuthenticationCallbackFactory } from '@/modules/auth/services/passportService' import { testLogger as logger } from '@/observability/logging' import { Application } from 'express' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) @@ -64,6 +78,52 @@ const getUsers = getUsersFactory({ db }) const createInviteDirectly = createStreamInviteDirectly const findInvite = findInviteFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -82,7 +142,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), @@ -205,7 +266,7 @@ describe('Auth @auth', () => { }@speckle.systems` const inviterUser = await getUserByEmail({ email: registeredUserEmail }) - const { token, inviteId } = await createInviteDirectly( + const { token, id: inviteId } = await createInviteDirectly( streamInvite ? { email: targetEmail, diff --git a/packages/server/modules/blobstorage/tests/blobstorage.graph.spec.js b/packages/server/modules/blobstorage/tests/blobstorage.graph.spec.js deleted file mode 100644 index dc5469f9b..000000000 --- a/packages/server/modules/blobstorage/tests/blobstorage.graph.spec.js +++ /dev/null @@ -1,193 +0,0 @@ -const { buildApolloServer } = require('@/app') -const { truncateTables } = require('@/test/hooks') -const { gql } = require('graphql-tag') -const { createBlobs } = require('@/modules/blobstorage/tests/helpers') -const { expect } = require('chai') -const { Users, Streams } = require('@/modules/core/dbSchema') -const { createAuthedTestContext, executeOperation } = require('@/test/graphqlHelper') -const { - getStreamFactory, - createStreamFactory -} = require('@/modules/core/repositories/streams') -const { db } = require('@/db/knex') -const { - legacyCreateStreamFactory, - createStreamReturnRecordFactory -} = require('@/modules/core/services/streams/management') -const { - inviteUsersToProjectFactory -} = require('@/modules/serverinvites/services/projectInviteManagement') -const { - createAndSendInviteFactory -} = require('@/modules/serverinvites/services/creation') -const { - findUserByTargetFactory, - insertInviteAndDeleteOldFactory, - deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory -} = require('@/modules/serverinvites/repositories/serverInvites') -const { - collectAndValidateCoreTargetsFactory -} = require('@/modules/serverinvites/services/coreResourceCollection') -const { - buildCoreInviteEmailContentsFactory -} = require('@/modules/serverinvites/services/coreEmailContents') -const { getEventBus } = require('@/modules/shared/services/eventBus') -const { createBranchFactory } = require('@/modules/core/repositories/branches') -const { - getUsersFactory, - getUserFactory, - storeUserFactory, - countAdminUsersFactory, - storeUserAclFactory -} = require('@/modules/core/repositories/users') -const { - findEmailFactory, - createUserEmailFactory, - ensureNoPrimaryEmailForUserFactory -} = require('@/modules/core/repositories/userEmails') -const { - requestNewEmailVerificationFactory -} = require('@/modules/emails/services/verification/request') -const { - deleteOldAndInsertNewVerificationFactory -} = require('@/modules/emails/repositories') -const { renderEmail } = require('@/modules/emails/services/emailRendering') -const { sendEmail } = require('@/modules/emails/services/sending') -const { createUserFactory } = require('@/modules/core/services/users/management') -const { - validateAndCreateUserEmailFactory -} = require('@/modules/core/services/userEmails') -const { - finalizeInvitedServerRegistrationFactory -} = require('@/modules/serverinvites/services/processing') -const { getServerInfoFactory } = require('@/modules/core/repositories/server') - -const getServerInfo = getServerInfoFactory({ db }) -const getUser = getUserFactory({ db }) -const getUsers = getUsersFactory({ db }) -const getStream = getStreamFactory({ db }) -const createStream = legacyCreateStreamFactory({ - createStreamReturnRecord: createStreamReturnRecordFactory({ - inviteUsersToProject: inviteUsersToProjectFactory({ - createAndSendInvite: createAndSendInviteFactory({ - findUserByTarget: findUserByTargetFactory({ db }), - insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), - collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ - getStream - }), - buildInviteEmailContents: buildCoreInviteEmailContentsFactory({ - getStream - }), - emitEvent: ({ eventName, payload }) => - getEventBus().emit({ - eventName, - payload - }), - getUser, - getServerInfo - }), - getUsers - }), - createStream: createStreamFactory({ db }), - createBranch: createBranchFactory({ db }), - emitEvent: getEventBus().emit - }) -}) - -const findEmail = findEmailFactory({ db }) -const requestNewEmailVerification = requestNewEmailVerificationFactory({ - findEmail, - getUser: getUserFactory({ db }), - getServerInfo, - deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }), - renderEmail, - sendEmail -}) -const createUser = createUserFactory({ - getServerInfo, - findEmail, - storeUser: storeUserFactory({ db }), - countAdminUsers: countAdminUsersFactory({ db }), - storeUserAcl: storeUserAclFactory({ db }), - validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ - createUserEmail: createUserEmailFactory({ db }), - ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), - findEmail, - updateEmailInvites: finalizeInvitedServerRegistrationFactory({ - deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), - updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) - }), - requestNewEmailVerification - }), - emitEvent: getEventBus().emit -}) - -describe('Blobs graphql @blobstorage', () => { - /** @type {import('@/test/graphqlHelper').ServerAndContext} */ - let graphqlServer - - const user = { - name: 'Baron Von Blubba', - email: 'zebarron@bubble.bobble', - password: 'bubblesAreMyBlobs' - } - before(async () => { - await truncateTables(['blob_storage', Users.name, Streams.name]) - user.id = await createUser(user) - graphqlServer = { - apollo: await buildApolloServer(), - context: await createAuthedTestContext(user.id) - } - }) - - it('Stream has blob metadata for a single blob', async () => { - const query = gql` - query ($streamId: String!, $blobId: String!) { - stream(id: $streamId) { - id - blob(id: $blobId) { - id - fileName - uploadStatus - fileSize - fileHash - } - } - } - ` - const streamId = await createStream({ ownerId: user.id }) - const [blob] = await createBlobs({ streamId, number: 1 }) - - const result = await executeOperation(graphqlServer, query, { - streamId, - blobId: blob.id - }) - - const blobMetadata = result.data.stream.blob - expect(blobMetadata.id).to.equal(blob.id) - expect(blobMetadata.fileSize).to.equal(blob.fileSize) - expect(blobMetadata.fileHash).to.equal(blob.fileHash) - }) - - it('Blob metadata collection returns proper summary values', async () => { - const query = gql` - query ($streamId: String!) { - stream(id: $streamId) { - id - blobs { - totalCount - totalSize - } - } - } - ` - const streamId = await createStream({ ownerId: user.id }) - const number = 10 - const fileSize = 123 - await createBlobs({ streamId, number, fileSize }) - const result = await executeOperation(graphqlServer, query, { streamId }) - expect(result.data.stream.blobs.totalCount).to.equal(number) - expect(result.data.stream.blobs.totalSize).to.equal(number * fileSize) - }) -}) diff --git a/packages/server/modules/blobstorage/tests/blobstorage.graph.spec.ts b/packages/server/modules/blobstorage/tests/blobstorage.graph.spec.ts new file mode 100644 index 000000000..8e427a877 --- /dev/null +++ b/packages/server/modules/blobstorage/tests/blobstorage.graph.spec.ts @@ -0,0 +1,244 @@ +import { buildApolloServer } from '@/app' +import { truncateTables } from '@/test/hooks' +import gql from 'graphql-tag' +import { createBlobs } from '@/modules/blobstorage/tests/helpers' +import { expect } from 'chai' +import { Users, Streams } from '@/modules/core/dbSchema' +import { + createAuthedTestContext, + executeOperation, + ServerAndContext +} from '@/test/graphqlHelper' +import { + getStreamFactory, + createStreamFactory, + grantStreamPermissionsFactory +} from '@/modules/core/repositories/streams' +import { db } from '@/db/knex' +import { + legacyCreateStreamFactory, + createStreamReturnRecordFactory +} from '@/modules/core/services/streams/management' +import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' +import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' +import { + findUserByTargetFactory, + insertInviteAndDeleteOldFactory, + deleteServerOnlyInvitesFactory, + updateAllInviteTargetsFactory, + findInviteFactory, + deleteInvitesByTargetFactory +} from '@/modules/serverinvites/repositories/serverInvites' +import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' +import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { createBranchFactory } from '@/modules/core/repositories/branches' +import { + getUsersFactory, + getUserFactory, + storeUserFactory, + countAdminUsersFactory, + storeUserAclFactory +} from '@/modules/core/repositories/users' +import { + findEmailFactory, + createUserEmailFactory, + ensureNoPrimaryEmailForUserFactory +} from '@/modules/core/repositories/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' +import { createUserFactory } from '@/modules/core/services/users/management' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' + +const getServerInfo = getServerInfoFactory({ db }) +const getUser = getUserFactory({ db }) +const getUsers = getUsersFactory({ db }) +const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + +const createStream = legacyCreateStreamFactory({ + createStreamReturnRecord: createStreamReturnRecordFactory({ + inviteUsersToProject: inviteUsersToProjectFactory({ + createAndSendInvite: createAndSendInviteFactory({ + findUserByTarget: findUserByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + buildInviteEmailContents: buildCoreInviteEmailContentsFactory({ + getStream + }), + emitEvent: ({ eventName, payload }) => + getEventBus().emit({ + eventName, + payload + }), + getUser, + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() + }), + getUsers + }), + createStream: createStreamFactory({ db }), + createBranch: createBranchFactory({ db }), + emitEvent: getEventBus().emit + }) +}) + +const findEmail = findEmailFactory({ db }) +const requestNewEmailVerification = requestNewEmailVerificationFactory({ + findEmail, + getUser: getUserFactory({ db }), + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }), + renderEmail, + sendEmail +}) +const createUser = createUserFactory({ + getServerInfo, + findEmail, + storeUser: storeUserFactory({ db }), + countAdminUsers: countAdminUsersFactory({ db }), + storeUserAcl: storeUserAclFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail, + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification + }), + emitEvent: getEventBus().emit +}) + +describe('Blobs graphql @blobstorage', () => { + let graphqlServer: ServerAndContext + + const user = { + name: 'Baron Von Blubba', + email: 'zebarron@bubble.bobble', + password: 'bubblesAreMyBlobs', + id: '' + } + + before(async () => { + await truncateTables(['blob_storage', Users.name, Streams.name]) + user.id = await createUser(user) + graphqlServer = { + apollo: await buildApolloServer(), + context: await createAuthedTestContext(user.id) + } + }) + + it('Stream has blob metadata for a single blob', async () => { + const query = gql` + query ($streamId: String!, $blobId: String!) { + stream(id: $streamId) { + id + blob(id: $blobId) { + id + fileName + uploadStatus + fileSize + fileHash + } + } + } + ` + const streamId = await createStream({ ownerId: user.id }) + const [blob] = await createBlobs({ streamId, number: 1 }) + + const result = await executeOperation(graphqlServer, query, { + streamId, + blobId: blob.id + }) + + const blobMetadata = result.data!.stream.blob + expect(blobMetadata.id).to.equal(blob.id) + expect(blobMetadata.fileSize).to.equal(blob.fileSize) + expect(blobMetadata.fileHash).to.equal(blob.fileHash) + }) + + it('Blob metadata collection returns proper summary values', async () => { + const query = gql` + query ($streamId: String!) { + stream(id: $streamId) { + id + blobs { + totalCount + totalSize + } + } + } + ` + const streamId = await createStream({ ownerId: user.id }) + const number = 10 + const fileSize = 123 + await createBlobs({ streamId, number, fileSize }) + const result = await executeOperation(graphqlServer, query, { streamId }) + expect(result.data!.stream.blobs.totalCount).to.equal(number) + expect(result.data!.stream.blobs.totalSize).to.equal(number * fileSize) + }) +}) diff --git a/packages/server/modules/comments/tests/comments.graph.spec.js b/packages/server/modules/comments/tests/comments.graph.spec.ts similarity index 79% rename from packages/server/modules/comments/tests/comments.graph.spec.js rename to packages/server/modules/comments/tests/comments.graph.spec.ts index 8960838fe..60cf702f8 100644 --- a/packages/server/modules/comments/tests/comments.graph.spec.js +++ b/packages/server/modules/comments/tests/comments.graph.spec.ts @@ -1,120 +1,117 @@ -const expect = require('chai').expect +import { expect } from 'chai' -const crs = require('crypto-random-string') -const { buildApolloServer } = require('@/app') -const { beforeEachContext } = require('@/test/hooks') -const { Roles } = require('@/modules/core/helpers/mainConstants') -const { gql } = require('graphql-tag') -const { - convertBasicStringToDocument -} = require('@/modules/core/services/richTextEditorService') -const { +import crs from 'crypto-random-string' +import { buildApolloServer } from '@/app' +import { beforeEachContext } from '@/test/hooks' +import { Roles } from '@/modules/core/helpers/mainConstants' +import gql from 'graphql-tag' +import { convertBasicStringToDocument } from '@/modules/core/services/richTextEditorService' +import { createTestContext, createAuthedTestContext, - executeOperation -} = require('@/test/graphqlHelper') -const { + executeOperation, + ServerAndContext, + ExecuteOperationResponse +} from '@/test/graphqlHelper' +import { streamResourceCheckFactory, createCommentFactory -} = require('@/modules/comments/services') -const { +} from '@/modules/comments/services' +import { checkStreamResourceAccessFactory, markCommentViewedFactory, insertCommentsFactory, insertCommentLinksFactory, deleteCommentFactory, getCommentsResourcesFactory -} = require('@/modules/comments/repositories/comments') -const { db } = require('@/db/knex') -const { - validateInputAttachmentsFactory -} = require('@/modules/comments/services/commentTextService') -const { getBlobsFactory } = require('@/modules/blobstorage/repositories') -const { +} from '@/modules/comments/repositories/comments' +import { db } from '@/db/knex' +import { validateInputAttachmentsFactory } from '@/modules/comments/services/commentTextService' +import { getBlobsFactory } from '@/modules/blobstorage/repositories' +import { createCommitByBranchIdFactory, createCommitByBranchNameFactory -} = require('@/modules/core/services/commit/management') -const { +} from '@/modules/core/services/commit/management' +import { createCommitFactory, insertStreamCommitsFactory, insertBranchCommitsFactory, getCommitsAndTheirBranchIdsFactory -} = require('@/modules/core/repositories/commits') -const { +} from '@/modules/core/repositories/commits' +import { getBranchByIdFactory, markCommitBranchUpdatedFactory, getStreamBranchByNameFactory, createBranchFactory -} = require('@/modules/core/repositories/branches') -const { +} from '@/modules/core/repositories/branches' +import { getStreamFactory, createStreamFactory, updateStreamFactory, grantStreamPermissionsFactory, markCommitStreamUpdatedFactory -} = require('@/modules/core/repositories/streams') -const { +} from '@/modules/core/repositories/streams' +import { getObjectFactory, storeSingleObjectIfNotFoundFactory, getStreamObjectsFactory -} = require('@/modules/core/repositories/objects') -const { +} from '@/modules/core/repositories/objects' +import { legacyCreateStreamFactory, createStreamReturnRecordFactory, legacyUpdateStreamFactory -} = require('@/modules/core/services/streams/management') -const { - inviteUsersToProjectFactory -} = require('@/modules/serverinvites/services/projectInviteManagement') -const { - createAndSendInviteFactory -} = require('@/modules/serverinvites/services/creation') -const { +} from '@/modules/core/services/streams/management' +import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' +import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' +import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory -} = require('@/modules/serverinvites/repositories/serverInvites') -const { - collectAndValidateCoreTargetsFactory -} = require('@/modules/serverinvites/services/coreResourceCollection') -const { - buildCoreInviteEmailContentsFactory -} = require('@/modules/serverinvites/services/coreEmailContents') -const { getEventBus } = require('@/modules/shared/services/eventBus') -const { + updateAllInviteTargetsFactory, + findInviteFactory, + deleteInvitesByTargetFactory +} from '@/modules/serverinvites/repositories/serverInvites' +import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' +import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { getUsersFactory, getUserFactory, storeUserFactory, countAdminUsersFactory, storeUserAclFactory -} = require('@/modules/core/repositories/users') -const { +} from '@/modules/core/repositories/users' +import { findEmailFactory, ensureNoPrimaryEmailForUserFactory, createUserEmailFactory -} = require('@/modules/core/repositories/userEmails') -const { - requestNewEmailVerificationFactory -} = require('@/modules/emails/services/verification/request') -const { - deleteOldAndInsertNewVerificationFactory -} = require('@/modules/emails/repositories') -const { renderEmail } = require('@/modules/emails/services/emailRendering') -const { sendEmail } = require('@/modules/emails/services/sending') -const { createUserFactory } = require('@/modules/core/services/users/management') -const { - validateAndCreateUserEmailFactory -} = require('@/modules/core/services/userEmails') -const { - finalizeInvitedServerRegistrationFactory -} = require('@/modules/serverinvites/services/processing') -const { getServerInfoFactory } = require('@/modules/core/repositories/server') -const { createObjectFactory } = require('@/modules/core/services/objects/management') -const { +} from '@/modules/core/repositories/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' +import { createUserFactory } from '@/modules/core/services/users/management' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { createObjectFactory } from '@/modules/core/services/objects/management' +import { getViewerResourcesFromLegacyIdentifiersFactory, getViewerResourcesForCommentsFactory -} = require('@/modules/core/services/commit/viewerResources') +} from '@/modules/core/services/commit/viewerResources' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' +import { SetNonNullable } from 'type-fest' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) @@ -168,6 +165,51 @@ const createCommitByBranchName = createCommitByBranchNameFactory({ }) const getStream = getStreamFactory({ db }) +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -186,7 +228,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), @@ -232,45 +275,54 @@ const createObject = createObjectFactory({ storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({ db }) }) -function buildCommentInputFromString(textString) { +function buildCommentInputFromString(textString: string) { return convertBasicStringToDocument(textString) } -const testForbiddenResponse = (result) => { +const testForbiddenResponse = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result: ExecuteOperationResponse> +) => { expect(result.errors, 'This should have failed').to.exist - expect(result.errors.length).to.be.above(0) - expect(result.errors[0].extensions.code).to.match( + expect(result.errors!.length).to.be.above(0) + expect(result.errors![0].extensions!.code).to.match( /(STREAM_INVALID_ACCESS_ERROR|FORBIDDEN|UNAUTHORIZED_ACCESS_ERROR)/ ) } -const testResult = (shouldSucceed, result, successTests) => { +const testResult = ( + shouldSucceed: boolean, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result: ExecuteOperationResponse>, + successTests: ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result: SetNonNullable>, 'data'> + ) => void +) => { if (shouldSucceed) { expect(result.errors, 'This should not have failed').to.not.exist - successTests(result) + successTests( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result as SetNonNullable>, 'data'> + ) } else { testForbiddenResponse(result) } } -/** - * @typedef {{ - * apollo: import('@/test/graphqlHelper').ServerAndContext, - * resources: { - * streamId: string, - * objectId: string, - * commentId: string, - * testActorId: string - * }, - * shouldSucceed: boolean, - * streamId: string - * }} TestContext - */ +type TestContext = { + apollo: ServerAndContext + resources: { + streamId: string + objectId: string + commentId: string + testActorId: string + } + shouldSucceed: boolean + streamId: string +} -/** - * @param {TestContext} param0 - */ -const writeComment = async ({ apollo, resources, shouldSucceed }) => { +const writeComment = async ({ apollo, resources, shouldSucceed }: TestContext) => { const res = await executeOperation( apollo, gql` @@ -294,10 +346,11 @@ const writeComment = async ({ apollo, resources, shouldSucceed }) => { }) } -/** - * @param {TestContext} param0 - */ -const broadcastViewerActivity = async ({ apollo, resources, shouldSucceed }) => { +const broadcastViewerActivity = async ({ + apollo, + resources, + shouldSucceed +}: TestContext) => { const res = await executeOperation( apollo, gql` @@ -320,10 +373,11 @@ const broadcastViewerActivity = async ({ apollo, resources, shouldSucceed }) => }) } -/** - * @param {TestContext} param0 - */ -const broadcastCommentActivity = async ({ apollo, resources, shouldSucceed }) => { +const broadcastCommentActivity = async ({ + apollo, + resources, + shouldSucceed +}: TestContext) => { const res = await executeOperation( apollo, gql` @@ -346,10 +400,7 @@ const broadcastCommentActivity = async ({ apollo, resources, shouldSucceed }) => }) } -/** - * @param {TestContext} param0 - */ -const viewAComment = async ({ apollo, resources, shouldSucceed }) => { +const viewAComment = async ({ apollo, resources, shouldSucceed }: TestContext) => { const res = await executeOperation( apollo, gql` @@ -367,13 +418,10 @@ const viewAComment = async ({ apollo, resources, shouldSucceed }) => { }) } -/** - * @param {TestContext} param0 - */ -const archiveMyComment = async ({ apollo, resources, shouldSucceed }) => { +const archiveMyComment = async ({ apollo, resources, shouldSucceed }: TestContext) => { const context = apollo.context const { id: commentId } = await createComment({ - userId: context.userId, + userId: context!.userId!, input: { streamId: resources.streamId, text: buildCommentInputFromString('i wrote this myself'), @@ -399,10 +447,11 @@ const archiveMyComment = async ({ apollo, resources, shouldSucceed }) => { }) } -/** - * @param {TestContext} param0 - */ -const archiveOthersComment = async ({ apollo, resources, shouldSucceed }) => { +const archiveOthersComment = async ({ + apollo, + resources, + shouldSucceed +}: TestContext) => { const res = await executeOperation( apollo, gql` @@ -420,12 +469,9 @@ const archiveOthersComment = async ({ apollo, resources, shouldSucceed }) => { }) } -/** - * @param {TestContext} param0 - */ -const editMyComment = async ({ apollo, resources, shouldSucceed }) => { +const editMyComment = async ({ apollo, resources, shouldSucceed }: TestContext) => { const { id: commentId } = await createComment({ - userId: apollo.context.userId, + userId: apollo.context!.userId!, input: { streamId: resources.streamId, text: buildCommentInputFromString('i wrote this myself'), @@ -458,10 +504,7 @@ const editMyComment = async ({ apollo, resources, shouldSucceed }) => { }) } -/** - * @param {TestContext} param0 - */ -const editOthersComment = async ({ apollo, resources, shouldSucceed }) => { +const editOthersComment = async ({ apollo, resources, shouldSucceed }: TestContext) => { const res = await executeOperation( apollo, gql` @@ -485,10 +528,7 @@ const editOthersComment = async ({ apollo, resources, shouldSucceed }) => { }) } -/** - * @param {TestContext} param0 - */ -const replyToAComment = async ({ apollo, resources, shouldSucceed }) => { +const replyToAComment = async ({ apollo, resources, shouldSucceed }: TestContext) => { const res = await executeOperation( apollo, gql` @@ -514,10 +554,7 @@ const replyToAComment = async ({ apollo, resources, shouldSucceed }) => { }) } -/** - * @param {TestContext} param0 - */ -const queryComment = async ({ apollo, resources, shouldSucceed }) => { +const queryComment = async ({ apollo, resources, shouldSucceed }: TestContext) => { const res = await executeOperation( apollo, gql` @@ -547,10 +584,7 @@ const queryComment = async ({ apollo, resources, shouldSucceed }) => { }) } -/** - * @param {TestContext} param0 - */ -const queryComments = async ({ apollo, resources, shouldSucceed }) => { +const queryComments = async ({ apollo, resources, shouldSucceed }: TestContext) => { const object = { foo: 123, bar: crs({ length: 5 }) @@ -599,14 +633,17 @@ const queryComments = async ({ apollo, resources, shouldSucceed }) => { ) testResult(shouldSucceed, res, (res) => { expect(res.data.comments.totalCount).to.be.equal(numberOfComments) - expect(res.data.comments.items.map((i) => i.id)).to.be.equalInAnyOrder(commentIds) + expect( + res.data.comments.items.map((i: { id: string }) => i.id) + ).to.deep.equalInAnyOrder(commentIds) }) } -/** - * @param {TestContext} param0 - */ -const queryStreamCommentCount = async ({ apollo, resources, shouldSucceed }) => { +const queryStreamCommentCount = async ({ + apollo, + resources, + shouldSucceed +}: TestContext) => { await createComment({ userId: resources.testActorId, input: { @@ -635,10 +672,11 @@ const queryStreamCommentCount = async ({ apollo, resources, shouldSucceed }) => }) } -/** - * @param {TestContext} param0 - */ -const queryObjectCommentCount = async ({ apollo, resources, shouldSucceed }) => { +const queryObjectCommentCount = async ({ + apollo, + resources, + shouldSucceed +}: TestContext) => { const objectId = await createObject({ streamId: resources.streamId, object: { @@ -675,10 +713,11 @@ const queryObjectCommentCount = async ({ apollo, resources, shouldSucceed }) => }) } -/** - * @param {TestContext} param0 - */ -const queryCommitCommentCount = async ({ apollo, resources, shouldSucceed }) => { +const queryCommitCommentCount = async ({ + apollo, + resources, + shouldSucceed +}: TestContext) => { const objectId = await createObject({ streamId: resources.streamId, object: { @@ -722,14 +761,11 @@ const queryCommitCommentCount = async ({ apollo, resources, shouldSucceed }) => }) } -/** - * @param {TestContext} param0 - */ const queryCommitCollectionCommentCount = async ({ apollo, resources, shouldSucceed -}) => { +}: TestContext) => { const objectId = await createObject({ streamId: resources.streamId, object: { @@ -772,16 +808,13 @@ const queryCommitCollectionCommentCount = async ({ ) testResult(shouldSucceed, res, (res) => { res.data.otherUser.commits.items - .map((i) => i.commentCount) - .map((commentCount) => { + .map((i: { commentCount: number }) => i.commentCount) + .map((commentCount: number) => { expect(commentCount).to.be.greaterThanOrEqual(1) }) }) } -// eslint-disable-next-line no-unused-vars -const actions = ['queryCommitCommentCount', 'queryCommitCollectionCommentCount'] - describe('Graphql @comments', () => { // this user will be admin by default // it will be used to create all resources, that the other actors can @@ -789,59 +822,69 @@ describe('Graphql @comments', () => { const myTestActor = { name: 'Gergo Jedlicska', email: 'gergo@jedlicska.com', - password: 'sn3aky-1337-b1m' + password: 'sn3aky-1337-b1m', + id: '' } const chadTheEngineer = { name: 'Chad the Engineer', email: 'chad@engineering.acme', password: 'tryingNotToBeACadMonkey', - role: Roles.Server.User + role: Roles.Server.User, + id: '' } const archived = { name: 'The Balrog of Morgoth', email: 'durinsbane@moria.bridge', - role: Roles.Server.ArchivedUser + password: 'tryingNotToBeACadMonkey', + role: Roles.Server.ArchivedUser, + id: '' } const ownedStream = { name: 'stream owner', isPublic: false, - role: Roles.Stream.Owner + role: Roles.Stream.Owner, + id: '' } const contributorStream = { name: 'contributions are welcome', isPublic: false, - role: Roles.Stream.Contributor + role: Roles.Stream.Contributor, + id: '' } const reviewerStream = { name: 'no work, just talk', isPublic: false, - role: Roles.Stream.Reviewer + role: Roles.Stream.Reviewer, + id: '' } const noAccessStream = { name: 'aint nobody canna cross it', isPublic: false, - role: null + role: null, + id: '' } const publicStream = { name: 'come take a look', isPublic: true, - role: null + role: null, + id: '' } const publicStreamWithPublicComments = { name: 'the gossip protocol', isPublic: true, - role: null + role: null, + id: '' } - const testData = [ + const testData = [ { user: chadTheEngineer, streamData: [ @@ -1116,11 +1159,8 @@ describe('Graphql @comments', () => { }`, () => { userContext.streamData.forEach((streamContext) => { const stream = streamContext.stream - let resources - /** - * @type {import('@/test/graphqlHelper').ServerAndContext} - */ - let apollo + let resources: TestContext['resources'] + let apollo: ServerAndContext before(async () => { apollo = { diff --git a/packages/server/modules/comments/tests/comments.spec.ts b/packages/server/modules/comments/tests/comments.spec.ts index 8a2c3f954..db41f952a 100644 --- a/packages/server/modules/comments/tests/comments.spec.ts +++ b/packages/server/modules/comments/tests/comments.spec.ts @@ -55,7 +55,8 @@ import { getBlobsFactory } from '@/modules/blobstorage/repositories' import { getStreamFactory, createStreamFactory, - markCommitStreamUpdatedFactory + markCommitStreamUpdatedFactory, + grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { createCommitByBranchIdFactory, @@ -88,7 +89,9 @@ import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory + updateAllInviteTargetsFactory, + findInviteFactory, + deleteInvitesByTargetFactory } from '@/modules/serverinvites/repositories/serverInvites' import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' @@ -111,7 +114,10 @@ import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { createUserFactory } from '@/modules/core/services/users/management' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' -import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { getServerInfoFactory } from '@/modules/core/repositories/server' import { createObjectFactory } from '@/modules/core/services/objects/management' import type express from 'express' @@ -130,6 +136,15 @@ import { getViewerResourcesFromLegacyIdentifiersFactory } from '@/modules/core/services/commit/viewerResources' import { StreamRecord } from '@/modules/core/helpers/types' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' type LegacyCommentRecord = CommentRecord & { total_count: string @@ -223,6 +238,51 @@ const createCommitByBranchName = createCommitByBranchNameFactory({ getBranchById: getBranchByIdFactory({ db }) }) +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -241,7 +301,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/core/graph/resolvers/projects.ts b/packages/server/modules/core/graph/resolvers/projects.ts index e71fc24a7..c4880b738 100644 --- a/packages/server/modules/core/graph/resolvers/projects.ts +++ b/packages/server/modules/core/graph/resolvers/projects.ts @@ -77,8 +77,12 @@ import { } from '@/modules/multiregion/utils/dbSelector' import { deleteAllResourceInvitesFactory, + deleteInvitesByTargetFactory, + deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, - insertInviteAndDeleteOldFactory + insertInviteAndDeleteOldFactory, + updateAllInviteTargetsFactory } from '@/modules/serverinvites/repositories/serverInvites' import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' @@ -94,11 +98,75 @@ import { import { has } from 'lodash' import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper' import { withOperationLogging } from '@/observability/domain/businessLogging' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + createUserEmailFactory, + ensureNoPrimaryEmailForUserFactory, + findEmailFactory +} from '@/modules/core/repositories/userEmails' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' const getServerInfo = getServerInfoFactory({ db }) const getUsers = getUsersFactory({ db }) const getUser = getUserFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStreamReturnRecord = createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ createAndSendInvite: createAndSendInviteFactory({ @@ -116,7 +184,8 @@ const createStreamReturnRecord = createStreamReturnRecordFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/core/graph/resolvers/streams.ts b/packages/server/modules/core/graph/resolvers/streams.ts index 164d2bc77..4619cfed4 100644 --- a/packages/server/modules/core/graph/resolvers/streams.ts +++ b/packages/server/modules/core/graph/resolvers/streams.ts @@ -48,9 +48,13 @@ import { } from '@/modules/core/graph/generated/graphql' import { deleteAllResourceInvitesFactory, + deleteInvitesByTargetFactory, + deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, insertInviteAndDeleteOldFactory, - queryAllResourceInvitesFactory + queryAllResourceInvitesFactory, + updateAllInviteTargetsFactory } from '@/modules/serverinvites/repositories/serverInvites' import db from '@/db/knex' import { getInvitationTargetUsersFactory } from '@/modules/serverinvites/services/retrieval' @@ -75,6 +79,24 @@ import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/use import { getServerInfoFactory } from '@/modules/core/repositories/server' import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper' import { withOperationLogging } from '@/observability/domain/businessLogging' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + createUserEmailFactory, + ensureNoPrimaryEmailForUserFactory, + findEmailFactory +} from '@/modules/core/repositories/userEmails' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' const getServerInfo = getServerInfoFactory({ db }) const getUsers = getUsersFactory({ db }) @@ -84,6 +106,52 @@ const getFavoriteStreamsCollection = getFavoriteStreamsCollectionFactory({ getFavoritedStreamsPage: getFavoritedStreamsPageFactory({ db }) }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStreamReturnRecord = createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ createAndSendInvite: createAndSendInviteFactory({ @@ -101,7 +169,8 @@ const createStreamReturnRecord = createStreamReturnRecordFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/core/services/streams/access.ts b/packages/server/modules/core/services/streams/access.ts index 935458495..0459ac6ad 100644 --- a/packages/server/modules/core/services/streams/access.ts +++ b/packages/server/modules/core/services/streams/access.ts @@ -208,8 +208,9 @@ export const addOrUpdateStreamCollaboratorFactory = eventName: ServerInvitesEvents.Finalized, payload: { invite: fromInvite, - finalizerUserId: addedById, - accept: true + finalizerUserId: userId, + accept: true, + trueFinalizerUserId: addedById } }) } else { diff --git a/packages/server/modules/core/tests/branches.spec.ts b/packages/server/modules/core/tests/branches.spec.ts index 09c16817e..01e7bd0ee 100644 --- a/packages/server/modules/core/tests/branches.spec.ts +++ b/packages/server/modules/core/tests/branches.spec.ts @@ -27,7 +27,8 @@ import { getStreamFactory, createStreamFactory, markBranchStreamUpdatedFactory, - markCommitStreamUpdatedFactory + markCommitStreamUpdatedFactory, + grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { createCommitByBranchIdFactory, @@ -52,7 +53,9 @@ import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory + updateAllInviteTargetsFactory, + findInviteFactory, + deleteInvitesByTargetFactory } from '@/modules/serverinvites/repositories/serverInvites' import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' @@ -75,12 +78,24 @@ import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { createUserFactory } from '@/modules/core/services/users/management' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' -import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { getServerInfoFactory } from '@/modules/core/repositories/server' import { getPaginatedStreamBranchesFactory } from '@/modules/core/services/branch/retrieval' import { createObjectFactory } from '@/modules/core/services/objects/management' import { ensureError } from '@speckle/shared' import { ModelEvents } from '@/modules/core/domain/branches/events' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' const db = knex const Commits = () => knex('commits') @@ -125,6 +140,51 @@ const createCommitByBranchName = createCommitByBranchNameFactory({ getBranchById: getBranchByIdFactory({ db }) }) +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -143,7 +203,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/core/tests/commits.spec.ts b/packages/server/modules/core/tests/commits.spec.ts index 3bd275480..6b43f358b 100644 --- a/packages/server/modules/core/tests/commits.spec.ts +++ b/packages/server/modules/core/tests/commits.spec.ts @@ -37,7 +37,8 @@ import { getStreamFactory, getCommitStreamFactory, createStreamFactory, - markCommitStreamUpdatedFactory + markCommitStreamUpdatedFactory, + grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { getObjectFactory, @@ -53,7 +54,9 @@ import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory + updateAllInviteTargetsFactory, + findInviteFactory, + deleteInvitesByTargetFactory } from '@/modules/serverinvites/repositories/serverInvites' import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' @@ -76,7 +79,10 @@ import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { createUserFactory } from '@/modules/core/services/users/management' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' -import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { getServerInfoFactory } from '@/modules/core/repositories/server' import { getBranchCommitsTotalCountByNameFactory, @@ -85,6 +91,15 @@ import { import { createObjectFactory } from '@/modules/core/services/objects/management' import { ensureError } from '@speckle/shared' import { VersionEvents } from '@/modules/core/domain/commits/events' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) @@ -139,6 +154,51 @@ const updateCommitAndNotify = updateCommitAndNotifyFactory({ }) const getStreamCommitCount = getStreamCommitCountFactory({ db }) +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -157,7 +217,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/core/tests/favoriteStreams.spec.js b/packages/server/modules/core/tests/favoriteStreams.spec.ts similarity index 65% rename from packages/server/modules/core/tests/favoriteStreams.spec.js rename to packages/server/modules/core/tests/favoriteStreams.spec.ts index dc60442be..f3e282692 100644 --- a/packages/server/modules/core/tests/favoriteStreams.spec.js +++ b/packages/server/modules/core/tests/favoriteStreams.spec.ts @@ -1,78 +1,124 @@ /* instanbul ignore file */ -const expect = require('chai').expect +import { expect } from 'chai' -const { buildApolloServer } = require('@/app') -const { StreamFavorites, Streams, Users } = require('@/modules/core/dbSchema') -const { truncateTables } = require('@/test/hooks') -const { gql } = require('graphql-tag') -const { sleep } = require('@/test/helpers') -const { +import { buildApolloServer } from '@/app' +import { StreamFavorites, Streams, Users } from '@/modules/core/dbSchema' +import { truncateTables } from '@/test/hooks' +import gql from 'graphql-tag' +import { sleep } from '@/test/helpers' +import { createAuthedTestContext, createTestContext, - executeOperation -} = require('@/test/graphqlHelper') -const { + executeOperation, + ServerAndContext +} from '@/test/graphqlHelper' +import { getStreamFactory, - createStreamFactory -} = require('@/modules/core/repositories/streams') -const { db } = require('@/db/knex') -const { + createStreamFactory, + grantStreamPermissionsFactory +} from '@/modules/core/repositories/streams' +import { db } from '@/db/knex' +import { legacyCreateStreamFactory, createStreamReturnRecordFactory -} = require('@/modules/core/services/streams/management') -const { - inviteUsersToProjectFactory -} = require('@/modules/serverinvites/services/projectInviteManagement') -const { - createAndSendInviteFactory -} = require('@/modules/serverinvites/services/creation') -const { +} from '@/modules/core/services/streams/management' +import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' +import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' +import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory -} = require('@/modules/serverinvites/repositories/serverInvites') -const { - collectAndValidateCoreTargetsFactory -} = require('@/modules/serverinvites/services/coreResourceCollection') -const { - buildCoreInviteEmailContentsFactory -} = require('@/modules/serverinvites/services/coreEmailContents') -const { getEventBus } = require('@/modules/shared/services/eventBus') -const { createBranchFactory } = require('@/modules/core/repositories/branches') -const { + updateAllInviteTargetsFactory, + findInviteFactory, + deleteInvitesByTargetFactory +} from '@/modules/serverinvites/repositories/serverInvites' +import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' +import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { createBranchFactory } from '@/modules/core/repositories/branches' +import { getUsersFactory, getUserFactory, storeUserFactory, countAdminUsersFactory, storeUserAclFactory -} = require('@/modules/core/repositories/users') -const { +} from '@/modules/core/repositories/users' +import { findEmailFactory, createUserEmailFactory, ensureNoPrimaryEmailForUserFactory -} = require('@/modules/core/repositories/userEmails') -const { - requestNewEmailVerificationFactory -} = require('@/modules/emails/services/verification/request') -const { - deleteOldAndInsertNewVerificationFactory -} = require('@/modules/emails/repositories') -const { renderEmail } = require('@/modules/emails/services/emailRendering') -const { sendEmail } = require('@/modules/emails/services/sending') -const { createUserFactory } = require('@/modules/core/services/users/management') -const { - validateAndCreateUserEmailFactory -} = require('@/modules/core/services/userEmails') -const { - finalizeInvitedServerRegistrationFactory -} = require('@/modules/serverinvites/services/processing') -const { getServerInfoFactory } = require('@/modules/core/repositories/server') +} from '@/modules/core/repositories/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' +import { createUserFactory } from '@/modules/core/services/users/management' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) const getUsers = getUsersFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -91,7 +137,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), @@ -205,29 +252,35 @@ const totalOwnedStreamsFavoritesNew = gql` describe('Favorite streams', () => { const myPubStream = { name: 'My Stream 1', - isPublic: false + isPublic: false, + id: '' } const myStream = { name: 'My Stream 2', - isPublic: true + isPublic: true, + id: '' } const notMyStream = { name: 'Not My Stream 1', - isPublic: false + isPublic: false, + id: '' } const notMyPubStream = { name: 'Not My Stream 2', - isPublic: true + isPublic: true, + id: '' } const me = { name: 'Itsa Me', email: 'me@example.org', - password: 'sn3aky-1337-b1m' + password: 'sn3aky-1337-b1m', + id: '' } const otherGuy = { name: 'Some Other DUde', email: 'otherguy@example.org', - password: 'sn3aky-1337-b1m' + password: 'sn3aky-1337-b1m', + id: '' } before(async function () { @@ -259,9 +312,9 @@ describe('Favorite streams', () => { describe('when authenticated', () => { /** @type {import('@/test/graphqlHelper').ServerAndContext} */ - let apollo + let apollo: ServerAndContext - const favoriteStream = async (sid, favorited) => + const favoriteStream = async (sid: string, favorited: boolean) => await executeOperation(apollo, favoriteMutationGql, { sid, favorited }) before(async () => { @@ -274,7 +327,7 @@ describe('Favorite streams', () => { await StreamFavorites.knex().truncate() }) - const accessibleStreamIds = [ + const accessibleStreamIds = [ [() => myPubStream.id, 'owned and public'], [() => myStream.id, 'owned and not public'], [() => notMyPubStream.id, 'not owned, but public'] @@ -292,7 +345,7 @@ describe('Favorite streams', () => { expect(result.errors).to.not.be.ok expect(result.data?.streamFavorite?.favoritedDate).to.be.a('date') expect(result.data?.streamFavorite?.favoritedDate.getTime()).to.satisfy( - (t) => t > beforeTime && t < afterTime + (t: number) => t > beforeTime && t < afterTime ) expect(result.data?.streamFavorite?.id).to.equal(streamId) expect(result.data?.streamFavorite?.favoritesCount).to.equal(1) @@ -302,19 +355,19 @@ describe('Favorite streams', () => { it("can't be favorited if not owned and not public", async () => { const result = await favoriteStream(notMyStream.id, true) - expect(result.data.streamFavorite).to.not.be.ok + expect(result.data!.streamFavorite).to.not.be.ok expect(result.errors).to.have.lengthOf(1) - expect(result.errors.at(0).message).to.contain("doesn't have access") + expect(result.errors!.at(0)!.message).to.contain("doesn't have access") }) describe('and favorited', () => { const favoritedStream = { name: 'Favorited Stream', - isPublic: true + isPublic: true, + id: '' } - /** @type {{favoritedDate: Date, favoritesCount: number, id: string}} */ - let favoritingResults + let favoritingResults: { favoritedDate: Date; favoritesCount: number; id: string } before(async () => { favoritedStream.id = await createStream({ ...favoritedStream, ownerId: me.id }) @@ -345,12 +398,12 @@ describe('Favorite streams', () => { describe('and being queried', () => { const favoritableStreams = [ - { name: 'Random 1', isPublic: true }, - { name: 'Random 2', isPublic: true }, - { name: 'Random 2', isPublic: true } + { name: 'Random 1', isPublic: true, id: '' }, + { name: 'Random 2', isPublic: true, id: '' }, + { name: 'Random 2', isPublic: true, id: '' } ] - const getFavorites = async (cursor, limit = 10) => + const getFavorites = async (cursor: string | null, limit = 10) => await executeOperation(apollo, favoriteStreamsQueryGql, { cursor, limit }) const favoritedStreamIds = () => favoritableStreams.map((s) => s.id) @@ -380,7 +433,7 @@ describe('Favorite streams', () => { ) expect(data).to.be.ok - expect(data.otherUser?.favoriteStreams).to.not.be.ok + expect(data!.otherUser?.favoriteStreams).to.not.be.ok expect((errors || []).map((e) => e.message).join()).to.match( /cannot view another user's favorite streams/i ) @@ -394,22 +447,24 @@ describe('Favorite streams', () => { expect(results.data?.activeUser?.favoriteStreams?.items).to.have.lengthOf( ids.length ) - expect(results.data.activeUser.favoriteStreams.totalCount).to.equal(ids.length) - expect(results.data.activeUser.favoriteStreams.cursor).to.be.a('string') + expect(results.data!.activeUser.favoriteStreams.totalCount).to.equal(ids.length) + expect(results.data!.activeUser.favoriteStreams.cursor).to.be.a('string') }) it('are paginated correctly', async () => { let nextCursor = null - let returnedStreamIds = [] + let returnedStreamIds: string[] = [] - const getPaginatedAndAssert = async (nextCursor) => { + const getPaginatedAndAssert = async (nextCursor: string | null) => { const results = await getFavorites(nextCursor, 1) expect(results.errors).to.not.be.ok expect(results.data?.activeUser?.favoriteStreams).to.be.ok return { - cursor: results.data.activeUser.favoriteStreams.cursor, - sids: results.data.activeUser.favoriteStreams.items.map((i) => i.id) + cursor: results.data!.activeUser.favoriteStreams.cursor, + sids: results.data!.activeUser.favoriteStreams.items.map( + (i: { id: string }) => i.id + ) } } @@ -456,8 +511,7 @@ describe('Favorite streams', () => { }) describe('when not authenticated', () => { - /** @type {import('@/test/graphqlHelper').ServerAndContext} */ - let apollo + let apollo: ServerAndContext before(async () => { apollo = { @@ -472,15 +526,15 @@ describe('Favorite streams', () => { favorited: true }) - expect(result.data.streamFavorite).to.not.be.ok + expect(result.data!.streamFavorite).to.not.be.ok expect(result.errors).to.have.lengthOf(1) - expect(result.errors.at(0).message).to.contain('Must provide an auth token') + expect(result.errors!.at(0)!.message).to.contain('Must provide an auth token') }) it("can't be retrieved", async () => { const result = await executeOperation(apollo, favoriteStreamsQueryGql) - expect(result.data.activeUser).to.be.null + expect(result.data!.activeUser).to.be.null expect(result.errors).to.not.be.ok }) }) diff --git a/packages/server/modules/core/tests/generic.spec.js b/packages/server/modules/core/tests/generic.spec.ts similarity index 59% rename from packages/server/modules/core/tests/generic.spec.js rename to packages/server/modules/core/tests/generic.spec.ts index 8802c2d4a..02fba6e32 100644 --- a/packages/server/modules/core/tests/generic.spec.js +++ b/packages/server/modules/core/tests/generic.spec.ts @@ -1,71 +1,115 @@ /* istanbul ignore file */ -const expect = require('chai').expect +import { expect } from 'chai' -const { beforeEachContext } = require('@/test/hooks') +import { beforeEachContext } from '@/test/hooks' -const { validateScopes, authorizeResolver } = require('@/modules/shared') -const { buildContext } = require('@/modules/shared/middleware') -const { Roles, Scopes } = require('@speckle/shared') -const { throwForNotHavingServerRole } = require('@/modules/shared/authz') -const { ForbiddenError } = require('@/modules/shared/errors') -const { +import { validateScopes, authorizeResolver } from '@/modules/shared' +import { buildContext } from '@/modules/shared/middleware' +import { AvailableRoles, Roles, Scopes, ServerRoles } from '@speckle/shared' +import { throwForNotHavingServerRole } from '@/modules/shared/authz' +import { ForbiddenError } from '@/modules/shared/errors' +import { getStreamFactory, - createStreamFactory -} = require('@/modules/core/repositories/streams') -const { db } = require('@/db/knex') -const { + createStreamFactory, + grantStreamPermissionsFactory +} from '@/modules/core/repositories/streams' +import { db } from '@/db/knex' +import { legacyCreateStreamFactory, createStreamReturnRecordFactory -} = require('@/modules/core/services/streams/management') -const { - inviteUsersToProjectFactory -} = require('@/modules/serverinvites/services/projectInviteManagement') -const { - createAndSendInviteFactory -} = require('@/modules/serverinvites/services/creation') -const { +} from '@/modules/core/services/streams/management' +import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' +import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' +import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory -} = require('@/modules/serverinvites/repositories/serverInvites') -const { - collectAndValidateCoreTargetsFactory -} = require('@/modules/serverinvites/services/coreResourceCollection') -const { - buildCoreInviteEmailContentsFactory -} = require('@/modules/serverinvites/services/coreEmailContents') -const { getEventBus } = require('@/modules/shared/services/eventBus') -const { createBranchFactory } = require('@/modules/core/repositories/branches') -const { + updateAllInviteTargetsFactory, + findInviteFactory, + deleteInvitesByTargetFactory +} from '@/modules/serverinvites/repositories/serverInvites' +import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' +import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { createBranchFactory } from '@/modules/core/repositories/branches' +import { getUsersFactory, getUserFactory, storeUserFactory, countAdminUsersFactory, storeUserAclFactory -} = require('@/modules/core/repositories/users') -const { +} from '@/modules/core/repositories/users' +import { findEmailFactory, createUserEmailFactory, ensureNoPrimaryEmailForUserFactory -} = require('@/modules/core/repositories/userEmails') -const { - requestNewEmailVerificationFactory -} = require('@/modules/emails/services/verification/request') -const { - deleteOldAndInsertNewVerificationFactory -} = require('@/modules/emails/repositories') -const { renderEmail } = require('@/modules/emails/services/emailRendering') -const { sendEmail } = require('@/modules/emails/services/sending') -const { createUserFactory } = require('@/modules/core/services/users/management') -const { - validateAndCreateUserEmailFactory -} = require('@/modules/core/services/userEmails') -const { - finalizeInvitedServerRegistrationFactory -} = require('@/modules/serverinvites/services/processing') -const { getServerInfoFactory } = require('@/modules/core/repositories/server') -const { mockAdminOverride } = require('@/test/mocks/global') +} from '@/modules/core/repositories/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' +import { createUserFactory } from '@/modules/core/services/users/management' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { mockAdminOverride } from '@/test/mocks/global' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { Request } from 'express' + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) @@ -89,7 +133,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), @@ -134,7 +179,7 @@ describe('Generic AuthN & AuthZ controller tests', () => { }) it('Validate scopes', async () => { - await validateScopes() + await validateScopes(undefined, undefined as unknown as string) .then(() => { throw new Error('This should have been rejected') }) @@ -156,14 +201,17 @@ describe('Generic AuthN & AuthZ controller tests', () => { await validateScopes(['a', 'b'], 'b') // should pass }) - ;[ - ['BS header', { req: { headers: { authorization: 'Bearer BS' } } }], - ['Null header', { req: { headers: { authorization: null } } }], - ['Undefined header', { req: { headers: { authorization: undefined } } }], + ;([ + ['BS header', { req: { headers: { authorization: 'Bearer BS' } } as Request }], + [ + 'Null header', + { req: { headers: { authorization: null as string | null } } as Request } + ], + ['Undefined header', { req: { headers: { authorization: undefined } } as Request }], ['BS token', { token: 'Bearer BS' }], ['Null token', { token: null }], ['Undefined token', { token: undefined }] - ].map(([caseName, contextInput]) => + ]).map(([caseName, contextInput]) => it(`Should create proper context ${caseName}`, async () => { const res = await buildContext(contextInput) expect(res.auth).to.equal(false) @@ -182,7 +230,10 @@ describe('Generic AuthN & AuthZ controller tests', () => { expect('You do not have the required server role').to.equal(err.message) ) - await throwForNotHavingServerRole({ auth: true, role: 'HACZOR' }, '133TCR3w') + await throwForNotHavingServerRole( + { auth: true, role: 'HACZOR' as ServerRoles }, + '133TCR3w' as ServerRoles + ) .then(() => { throw new Error('This should have been rejected') }) @@ -192,7 +243,7 @@ describe('Generic AuthN & AuthZ controller tests', () => { await throwForNotHavingServerRole( { auth: true, role: Roles.Server.Admin }, - '133TCR3w' + '133TCR3w' as ServerRoles ) .then(() => { throw new Error('This should have been rejected') @@ -209,14 +260,19 @@ describe('Generic AuthN & AuthZ controller tests', () => { }) it('Resolver Authorization Should fail nicely when roles & resources are wanky', async () => { - await authorizeResolver(null, 'foo', 'bar') + await authorizeResolver(null, 'foo', 'bar' as AvailableRoles, null) .then(() => { throw new Error('This should have been rejected') }) .catch((err) => expect('Unknown role: bar').to.equal(err.message)) // this caught me out, but streams:read is not a valid role for now - await authorizeResolver('foo', 'bar', Scopes.Streams.Read) + await authorizeResolver( + 'foo', + 'bar' as AvailableRoles, + Scopes.Streams.Read as AvailableRoles, + null + ) .then(() => { throw new Error('This should have been rejected') }) @@ -226,21 +282,25 @@ describe('Generic AuthN & AuthZ controller tests', () => { describe('Authorize resolver ', () => { const myStream = { name: 'My Stream 2', - isPublic: true + isPublic: true, + id: '' } const notMyStream = { name: 'Not My Stream 1', - isPublic: false + isPublic: false, + id: '' } const serverOwner = { name: 'Itsa Me', email: 'me@example.org', - password: 'sn3aky-1337-b1m' + password: 'sn3aky-1337-b1m', + id: '' } const otherGuy = { name: 'Some Other DUde', email: 'otherguy@example.org', - password: 'sn3aky-1337-b1m' + password: 'sn3aky-1337-b1m', + id: '' } before(async function () { @@ -290,7 +350,7 @@ describe('Generic AuthN & AuthZ controller tests', () => { Roles.Stream.Contributor, null ) - throw 'This should have thrown' + throw new Error('This should have thrown') } catch (e) { expect(e instanceof ForbiddenError) } @@ -315,7 +375,7 @@ describe('Generic AuthN & AuthZ controller tests', () => { Roles.Stream.Contributor, null ) - throw 'This should have thrown' + throw new Error('This should have thrown') } catch (e) { expect(e instanceof ForbiddenError) } @@ -331,7 +391,7 @@ describe('Generic AuthN & AuthZ controller tests', () => { Roles.Stream.Contributor, null ) - throw 'This should have thrown' + throw new Error('This should have thrown') } catch (e) { expect(e instanceof ForbiddenError) } diff --git a/packages/server/modules/core/tests/objects.spec.js b/packages/server/modules/core/tests/objects.spec.ts similarity index 70% rename from packages/server/modules/core/tests/objects.spec.js rename to packages/server/modules/core/tests/objects.spec.ts index 02546a52b..2cb8e55ba 100644 --- a/packages/server/modules/core/tests/objects.spec.js +++ b/packages/server/modules/core/tests/objects.spec.ts @@ -1,75 +1,65 @@ /* istanbul ignore file */ /* eslint-disable camelcase */ -const expect = require('chai').expect -const assert = require('assert') -const { cloneDeep, times, random, padStart } = require('lodash') +import { expect } from 'chai' +import assert from 'assert' +import { cloneDeep, times, random, padStart } from 'lodash' -const { beforeEachContext } = require('@/test/hooks') -const { getAnIdForThisOnePlease } = require('@/test/helpers') +import { beforeEachContext } from '@/test/hooks' +import { getAnIdForThisOnePlease } from '@/test/helpers' -const { +import { getStreamFactory, - createStreamFactory -} = require('@/modules/core/repositories/streams') -const { db } = require('@/db/knex') -const { + createStreamFactory, + grantStreamPermissionsFactory +} from '@/modules/core/repositories/streams' +import { db } from '@/db/knex' +import { legacyCreateStreamFactory, createStreamReturnRecordFactory -} = require('@/modules/core/services/streams/management') -const { - inviteUsersToProjectFactory -} = require('@/modules/serverinvites/services/projectInviteManagement') -const { - createAndSendInviteFactory -} = require('@/modules/serverinvites/services/creation') -const { +} from '@/modules/core/services/streams/management' +import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' +import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' +import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory -} = require('@/modules/serverinvites/repositories/serverInvites') -const { - collectAndValidateCoreTargetsFactory -} = require('@/modules/serverinvites/services/coreResourceCollection') -const { - buildCoreInviteEmailContentsFactory -} = require('@/modules/serverinvites/services/coreEmailContents') -const { getEventBus } = require('@/modules/shared/services/eventBus') -const { createBranchFactory } = require('@/modules/core/repositories/branches') -const { + updateAllInviteTargetsFactory, + findInviteFactory, + deleteInvitesByTargetFactory +} from '@/modules/serverinvites/repositories/serverInvites' +import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' +import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { createBranchFactory } from '@/modules/core/repositories/branches' +import { getUsersFactory, getUserFactory, storeUserFactory, countAdminUsersFactory, storeUserAclFactory -} = require('@/modules/core/repositories/users') -const { +} from '@/modules/core/repositories/users' +import { findEmailFactory, createUserEmailFactory, ensureNoPrimaryEmailForUserFactory -} = require('@/modules/core/repositories/userEmails') -const { - requestNewEmailVerificationFactory -} = require('@/modules/emails/services/verification/request') -const { - deleteOldAndInsertNewVerificationFactory -} = require('@/modules/emails/repositories') -const { renderEmail } = require('@/modules/emails/services/emailRendering') -const { sendEmail } = require('@/modules/emails/services/sending') -const { createUserFactory } = require('@/modules/core/services/users/management') -const { - validateAndCreateUserEmailFactory -} = require('@/modules/core/services/userEmails') -const { - finalizeInvitedServerRegistrationFactory -} = require('@/modules/serverinvites/services/processing') -const { getServerInfoFactory } = require('@/modules/core/repositories/server') -const { +} from '@/modules/core/repositories/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' +import { createUserFactory } from '@/modules/core/services/users/management' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { createObjectFactory, createObjectsBatchedAndNoClosuresFactory, createObjectsFactory -} = require('@/modules/core/services/objects/management') -const { +} from '@/modules/core/services/objects/management' +import { storeSingleObjectIfNotFoundFactory, storeObjectsIfNotFoundFactory, getFormattedObjectFactory, @@ -77,7 +67,17 @@ const { getObjectChildrenFactory, getObjectChildrenQueryFactory, getStreamObjectsFactory -} = require('@/modules/core/repositories/objects') +} from '@/modules/core/repositories/objects' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' +import { ObjectRecord } from '@/modules/core/helpers/types' const sampleCommit = JSON.parse(`{ "Objects": [ @@ -106,6 +106,52 @@ const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) const getUsers = getUsersFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -124,7 +170,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), @@ -180,12 +227,14 @@ describe('Objects @core-objects', () => { const userOne = { name: 'Dimitrie Stefanescu', email: 'didimitrie43@example.org', - password: 'sn3aky-1337-b1m' + password: 'sn3aky-1337-b1m', + id: '' } const stream = { name: 'Test Streams', - description: 'Whatever goes in here usually...' + description: 'Whatever goes in here usually...', + id: '' } before(async () => { @@ -202,8 +251,8 @@ describe('Objects @core-objects', () => { const objCount_1 = 10 const objCount_2 = 1000 - const objs = [] - const objs2 = [] + const objs: Array & { id?: string }> = [] + const objs2: Array & { id?: string }> = [] it(`Should create ${objCount_1} objects`, async () => { for (let i = 0; i < objCount_1; i++) { @@ -258,7 +307,7 @@ describe('Objects @core-objects', () => { ]).reduce((obj, [key, value]) => { obj[key] = value return obj - }, {}) + }, {} as Record) } const id = await createObject({ streamId: stream.id, object: obj }) expect(id).to.be.ok @@ -272,20 +321,20 @@ describe('Objects @core-objects', () => { it('Should get more objects', async () => { const myObjs = await getObjects( stream.id, - objs.map((o) => o.id) + objs.map((o) => o.id!) ) expect(myObjs).to.have.lengthOf(objs.length) const match1 = myObjs.find((o) => o.id === objs[0].id) expect(match1).to.not.be.null - expect(match1.id).to.equal(objs[0].id) + expect(match1!.id).to.equal(objs[0].id) const match2 = myObjs.find((o) => o.id === objs[2].id) expect(match2).to.not.be.null - expect(match2.id).to.equal(objs[2].id) + expect(match2!.id).to.equal(objs[2].id) }) - let parentObjectId + let parentObjectId: string it('Should get object children', async () => { const objs_1 = createManyObjects(100, 'noise__') @@ -368,7 +417,7 @@ describe('Objects @core-objects', () => { { field: 'test.value', operator: '<', value: 24 }, { verb: 'OR', field: 'test.value', operator: '=', value: 42 } ], - orderBy: { field: 'test.value', direction: 'asc' } + orderBy: { field: 'test.value' as keyof ObjectRecord, direction: 'asc' } }) const test2 = await getObjectChildrenQuery({ @@ -381,7 +430,7 @@ describe('Objects @core-objects', () => { { field: 'test.value', operator: '<', value: 24 }, { verb: 'OR', field: 'test.value', operator: '=', value: 42 } ], - orderBy: { field: 'test.value', direction: 'asc' }, + orderBy: { field: 'test.value' as keyof ObjectRecord, direction: 'asc' }, cursor: test.cursor }) @@ -403,14 +452,19 @@ describe('Objects @core-objects', () => { expect(test.totalCount).to.equal(23) expect(test2.totalCount).to.equal(23) - expect(test.objects[0].data.test.value).to.be.below(test.objects[1].data.test.value) - expect(test2.objects[0].data.test.value).to.be.below( - test2.objects[1].data.test.value - ) + const testObjects = test.objects as unknown as Array<{ + data: { test: { value: number } } + }> + const test2Objects = test2.objects as unknown as Array<{ + data: { test: { value: number } } + }> + + expect(testObjects[0].data.test.value).to.be.below(testObjects[1].data.test.value) + expect(test2Objects[0].data.test.value).to.be.below(test2Objects[1].data.test.value) // continuity - expect(test.objects[test.objects.length - 1].data.test.value + 1).to.equal( - test2.objects[0].data.test.value + expect(testObjects[testObjects.length - 1].data.test.value + 1).to.equal( + test2Objects[0].data.test.value ) }) @@ -424,7 +478,7 @@ describe('Objects @core-objects', () => { { field: 'similar', operator: '>=', value: 0 }, { field: 'similar', operator: '<', value: 100 } ], - orderBy: { field: 'similar', direction: 'asc' }, + orderBy: { field: 'similar' as keyof ObjectRecord, direction: 'asc' }, limit: 5 }) @@ -436,7 +490,7 @@ describe('Objects @core-objects', () => { { field: 'similar', operator: '>=', value: 0 }, { field: 'similar', operator: '<', value: 100 } ], - orderBy: { field: 'similar', direction: 'asc' }, + orderBy: { field: 'similar' as keyof ObjectRecord, direction: 'asc' }, cursor: test3.cursor, limit: 5 }) @@ -453,17 +507,24 @@ describe('Objects @core-objects', () => { expect(test3.totalCount).to.equal(100) expect(test4.totalCount).to.equal(100) - expect(test3.objects[0].data.similar).to.be.below(test3.objects[1].data.similar) // 0, 1, 1, 1, ... - expect(test4.objects[0].data.similar).to.be.below(test4.objects[3].data.similar) + const test3Objects = test3.objects as unknown as Array<{ + data: { similar: number } + }> + const test4Objects = test4.objects as unknown as Array<{ + data: { similar: number } + }> + + expect(test3Objects[0].data.similar).to.be.below(test3Objects[1].data.similar) // 0, 1, 1, 1, ... + expect(test4Objects[0].data.similar).to.be.below(test4Objects[3].data.similar) // continuity (in reverse) - expect(test3.objects[test3.objects.length - 1].data.similar).to.equal( - test3.objects[test3.objects.length - 2].data.similar + 1 + expect(test3Objects[test3Objects.length - 1].data.similar).to.equal( + test3Objects[test3Objects.length - 2].data.similar + 1 ) - expect(test3.objects[test3.objects.length - 1].data.similar).to.equal( - test4.objects[0].data.similar + expect(test3Objects[test3Objects.length - 1].data.similar).to.equal( + test4Objects[0].data.similar ) - expect(test4.objects[1].data.similar).to.equal(test4.objects[2].data.similar - 1) + expect(test4Objects[1].data.similar).to.equal(test4Objects[2].data.similar - 1) }) it('should query object children with no results ', async () => { @@ -474,7 +535,7 @@ describe('Objects @core-objects', () => { { field: 'test.value', operator: '>=', value: 10 }, { field: 'test.value', operator: '<', value: 9 } ], - orderBy: { field: 'test.value', direction: 'desc' } + orderBy: { field: 'test.value' as keyof ObjectRecord, direction: 'desc' } }) expect(test.totalCount).to.equal(0) @@ -494,7 +555,7 @@ describe('Objects @core-objects', () => { }, { field: 'test.value', operator: '<', value: 9 } ], - orderBy: { field: 'test.value', direction: 'desc' } + orderBy: { field: 'test.value' as keyof ObjectRecord, direction: 'desc' } }) assert.fail('sql injections are bad for health') } catch { @@ -509,7 +570,7 @@ describe('Objects @core-objects', () => { limit: 5, select: ['test.value', 'nest.duck'], query: [{ field: 'test.value', operator: '<', value: 10 }], - orderBy: { field: 'nest.duck', direction: 'desc' } + orderBy: { field: 'nest.duck' as keyof ObjectRecord, direction: 'desc' } }) const test2 = await getObjectChildrenQuery({ @@ -518,12 +579,18 @@ describe('Objects @core-objects', () => { limit: 5, select: ['test.value', 'nest.duck'], query: [{ field: 'test.value', operator: '<', value: 10 }], - orderBy: { field: 'nest.duck', direction: 'desc' }, + orderBy: { field: 'nest.duck' as keyof ObjectRecord, direction: 'desc' }, cursor: test.cursor }) - expect(test.objects[0].data.nest.duck).to.equal(true) - expect(test2.objects[test2.objects.length - 1].data.nest.duck).to.equal(false) // last duck should be false + const testObjects = test.objects as unknown as Array<{ + data: { test: { value: number }; nest: { duck: boolean } } + }> + const test2Objects = test2.objects as unknown as Array<{ + data: { test: { value: number }; nest: { duck: boolean } } + }> + expect(testObjects[0].data.nest.duck).to.equal(true) + expect(test2Objects[test2Objects.length - 1].data.nest.duck).to.equal(false) // last duck should be false }) it('should query children and sort them by a string value ', async () => { @@ -534,7 +601,7 @@ describe('Objects @core-objects', () => { objectId: parentObjectId, limit: 5, query: [{ field: 'test.value', operator: '<', value: limVal }], - orderBy: { field: 'name', direction: 'asc' } + orderBy: { field: 'name' as keyof ObjectRecord, direction: 'asc' } }) const test2 = await getObjectChildrenQuery({ @@ -542,18 +609,25 @@ describe('Objects @core-objects', () => { objectId: parentObjectId, limit: 5, query: [{ field: 'test.value', operator: '<', value: limVal }], - orderBy: { field: 'name', direction: 'asc' }, + orderBy: { field: 'name' as keyof ObjectRecord, direction: 'asc' }, cursor: test.cursor }) expect(test.objects.length).to.equal(5) expect(test.cursor).to.be.a('string') - expect(test.objects[0].data.name).to.equal('mr. 0') - expect(test.objects[1].data.name).to.equal('mr. 1') - expect(test.objects[2].data.name).to.equal('mr. 10') // remember kids, this is a lexicographical sort - expect(test.objects[4].data.name).to.equal('mr. 12') - expect(test2.objects[0].data.name).to.equal('mr. 13') + const testObjects = test.objects as unknown as Array<{ + data: { name: string; test: { value: number } } + }> + const test2Objects = test2.objects as unknown as Array<{ + data: { name: string; test: { value: number } } + }> + + expect(testObjects[0].data.name).to.equal('mr. 0') + expect(testObjects[1].data.name).to.equal('mr. 1') + expect(testObjects[2].data.name).to.equal('mr. 10') // remember kids, this is a lexicographical sort + expect(testObjects[4].data.name).to.equal('mr. 12') + expect(test2Objects[0].data.name).to.equal('mr. 13') }) it('should query children and sort them by id by default ', async () => { @@ -588,41 +662,53 @@ describe('Objects @core-objects', () => { streamId: stream.id, objectId: parentObjectId, limit: 2, - orderBy: { field: 'test.value', direction: 'desc' } + orderBy: { field: 'test.value' as keyof ObjectRecord, direction: 'desc' } }) const test2 = await getObjectChildrenQuery({ streamId: stream.id, objectId: parentObjectId, limit: 2, - orderBy: { field: 'test.value', direction: 'desc' }, + orderBy: { field: 'test.value' as keyof ObjectRecord, direction: 'desc' }, cursor: test.cursor }) - expect(test.objects[1].data.test.value).to.equal( - test2.objects[0].data.test.value + 1 - ) // continuity check + const testObjects = test.objects as unknown as Array<{ + data: { test: { value: number } } + }> + const test2Objects = test2.objects as unknown as Array<{ + data: { test: { value: number } } + }> + + expect(testObjects[1].data.test.value).to.equal(test2Objects[0].data.test.value + 1) // continuity check const test3 = await getObjectChildrenQuery({ streamId: stream.id, objectId: parentObjectId, limit: 50, - orderBy: { field: 'nest.duck', direction: 'desc' } + orderBy: { field: 'nest.duck' as keyof ObjectRecord, direction: 'desc' } }) const test4 = await getObjectChildrenQuery({ streamId: stream.id, objectId: parentObjectId, limit: 50, - orderBy: { field: 'nest.duck', direction: 'desc' }, + orderBy: { field: 'nest.duck' as keyof ObjectRecord, direction: 'desc' }, cursor: test3.cursor }) - expect(test3.objects[49].data.nest.duck).to.equal(true) - expect(test4.objects[0].data.nest.duck).to.equal(false) + const test3Objects = test3.objects as unknown as Array<{ + data: { nest: { duck: boolean } } + }> + const test4Objects = test4.objects as unknown as Array<{ + data: { nest: { duck: boolean } } + }> + + expect(test3Objects[49].data.nest.duck).to.equal(true) + expect(test4Objects[0].data.nest.duck).to.equal(false) }) - let commitId + let commitId: string it('should batch create objects', async () => { const objs = createManyObjects(3333, 'perlin merlin magic') commitId = objs[0].id @@ -641,6 +727,7 @@ describe('Objects @core-objects', () => { it('should stream objects back', (done) => { let tcount = 0 + // eslint-disable-next-line @typescript-eslint/no-floating-promises getObjectChildrenStream({ streamId: stream.id, objectId: commitId }).then( (stream) => { stream.on('data', () => tcount++) @@ -656,7 +743,7 @@ describe('Objects @core-objects', () => { this.timeout(5000) const objs = createManyObjects(5000, 'perlin merlin magic') - function shuffleArray(array) { + function shuffleArray(array: Array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[array[i], array[j]] = [array[j], array[i]] @@ -686,13 +773,18 @@ describe('Objects @core-objects', () => { }) }) -function createManyObjects(num, noise) { +function createManyObjects(num: number, noise: string | number) { num = num || 10000 noise = noise || Math.random() * 100 const objs = [] - const base = { name: 'base bastard 2', noise, __closure: {} } + const base = { + name: 'base bastard 2', + noise, + __closure: {} as Record, + id: '' + } objs.push(base) let k = 0 @@ -706,7 +798,8 @@ function createManyObjects(num, noise) { objArr: [{ a: i }, { b: i * i }, { c: true }], noise, sortValueA: i, - sortValueB: i * 0.42 * i + sortValueB: i * 0.42 * i, + id: '' } if (i % 3 === 0) k++ diff --git a/packages/server/modules/core/tests/rest.spec.js b/packages/server/modules/core/tests/rest.spec.ts similarity index 71% rename from packages/server/modules/core/tests/rest.spec.js rename to packages/server/modules/core/tests/rest.spec.ts index 051540bd4..f427bd523 100644 --- a/packages/server/modules/core/tests/rest.spec.js +++ b/packages/server/modules/core/tests/rest.spec.ts @@ -1,84 +1,130 @@ /* istanbul ignore file */ -const expect = require('chai').expect -const request = require('supertest') +import { expect } from 'chai' +import request from 'supertest' -const assert = require('assert') -const crypto = require('crypto') +import assert from 'assert' +import crypto from 'crypto' -const { beforeEachContext } = require('@/test/hooks') -const { createManyObjects } = require('@/test/helpers') +import { beforeEachContext } from '@/test/hooks' +import { createManyObjects } from '@/test/helpers' -const { Scopes } = require('@speckle/shared') -const { +import { Scopes } from '@speckle/shared' +import { getStreamFactory, - createStreamFactory -} = require('@/modules/core/repositories/streams') -const { db } = require('@/db/knex') -const { + createStreamFactory, + grantStreamPermissionsFactory +} from '@/modules/core/repositories/streams' +import { db } from '@/db/knex' +import { legacyCreateStreamFactory, createStreamReturnRecordFactory -} = require('@/modules/core/services/streams/management') -const { - inviteUsersToProjectFactory -} = require('@/modules/serverinvites/services/projectInviteManagement') -const { - createAndSendInviteFactory -} = require('@/modules/serverinvites/services/creation') -const { +} from '@/modules/core/services/streams/management' +import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' +import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' +import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory -} = require('@/modules/serverinvites/repositories/serverInvites') -const { - collectAndValidateCoreTargetsFactory -} = require('@/modules/serverinvites/services/coreResourceCollection') -const { - buildCoreInviteEmailContentsFactory -} = require('@/modules/serverinvites/services/coreEmailContents') -const { getEventBus } = require('@/modules/shared/services/eventBus') -const { createBranchFactory } = require('@/modules/core/repositories/branches') -const { + updateAllInviteTargetsFactory, + findInviteFactory, + deleteInvitesByTargetFactory +} from '@/modules/serverinvites/repositories/serverInvites' +import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' +import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { createBranchFactory } from '@/modules/core/repositories/branches' +import { getUsersFactory, getUserFactory, storeUserFactory, countAdminUsersFactory, storeUserAclFactory -} = require('@/modules/core/repositories/users') -const { +} from '@/modules/core/repositories/users' +import { findEmailFactory, createUserEmailFactory, ensureNoPrimaryEmailForUserFactory -} = require('@/modules/core/repositories/userEmails') -const { - requestNewEmailVerificationFactory -} = require('@/modules/emails/services/verification/request') -const { - deleteOldAndInsertNewVerificationFactory -} = require('@/modules/emails/repositories') -const { renderEmail } = require('@/modules/emails/services/emailRendering') -const { sendEmail } = require('@/modules/emails/services/sending') -const { createUserFactory } = require('@/modules/core/services/users/management') -const { - validateAndCreateUserEmailFactory -} = require('@/modules/core/services/userEmails') -const { - finalizeInvitedServerRegistrationFactory -} = require('@/modules/serverinvites/services/processing') -const { createPersonalAccessTokenFactory } = require('@/modules/core/services/tokens') -const { +} from '@/modules/core/repositories/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' +import { createUserFactory } from '@/modules/core/services/users/management' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { createPersonalAccessTokenFactory } from '@/modules/core/services/tokens' +import { storeTokenScopesFactory, storeApiTokenFactory, storeTokenResourceAccessDefinitionsFactory, storePersonalApiTokenFactory -} = require('@/modules/core/repositories/tokens') -const { getServerInfoFactory } = require('@/modules/core/repositories/server') -const cryptoRandomString = require('crypto-random-string') +} from '@/modules/core/repositories/tokens' +import { getServerInfoFactory } from '@/modules/core/repositories/server' +import cryptoRandomString from 'crypto-random-string' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' +import type Express from 'express' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) const getUsers = getUsersFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -97,7 +143,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), @@ -147,22 +194,33 @@ describe('Upload/Download Routes @api-rest', () => { const userA = { name: 'd1', email: 'd.1@speckle.systems', - password: 'wowwow8charsplease' + password: 'wowwow8charsplease', + id: '', + token: '' } const userB = { name: 'd2', email: 'd.2@speckle.systems', - password: 'wowwow8charsplease' + password: 'wowwow8charsplease', + id: '', + token: '' } const testStream = { name: 'Test Stream 01', - description: 'wonderful test stream' + description: 'wonderful test stream', + id: '', + ownerId: '' } - const privateTestStream = { name: 'Private Test Stream', isPublic: false } + const privateTestStream = { + name: 'Private Test Stream', + isPublic: false, + id: '', + ownerId: '' + } - let app + let app: Express.Express before(async () => { ;({ app } = await beforeEachContext()) @@ -277,7 +335,12 @@ describe('Upload/Download Routes @api-rest', () => { .post(`/objects/${testStream.id}`) .set('Authorization', userA.token) .set('Content-type', 'application/json') - .attach(Buffer.from(JSON.stringify(objBatches[0]), 'utf8')) + .attach( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Buffer.from(JSON.stringify(objBatches[0]), 'utf8') as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + undefined as any + ) expect(res).to.have.status(400) expect(res.text).to.equal( 'Failed to parse request headers and body content as valid multipart/form-data.' @@ -289,7 +352,8 @@ describe('Upload/Download Routes @api-rest', () => { .post(`/objects/${testStream.id}`) .set('Authorization', userA.token) .set('Content-type', 'multipart/form-data') - .attach(JSON.stringify(objBatches[0], 'utf8')) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .attach(JSON.stringify(objBatches[0]) as any, undefined as any) expect(res).to.have.status(400) expect(res.text).to.equal( 'Failed to parse request headers and body content as valid multipart/form-data.' @@ -341,7 +405,8 @@ describe('Upload/Download Routes @api-rest', () => { it('Should not allow upload with invalid body (not contained within array)', async () => { //creating a single valid object const objectToPost = { - name: 'yet again cannot believe i have to create this' + name: 'yet again cannot believe i have to create this', + id: '' } const objectId = crypto .createHash('md5') @@ -384,7 +449,7 @@ describe('Upload/Download Routes @api-rest', () => { // expect(res.text).contains('Object too large') // }) - let parentId + let parentId: string const numObjs = 5000 const objBatches = [ createManyObjects(numObjs), @@ -412,19 +477,22 @@ describe('Upload/Download Routes @api-rest', () => { }) it('Should properly download an object, with all its children, into a application/json response', (done) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises new Promise((resolve) => setTimeout(resolve, 1500)) // avoids race condition .then(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises request(app) .get(`/objects/${testStream.id}/${parentId}`) .set('Authorization', userA.token) .buffer() .parse((res, cb) => { - res.data = '' - res.on('data', (chunk) => { - res.data += chunk.toString() + const resTyped = res as typeof res & { data: string } + resTyped.data = '' + resTyped.on('data', (chunk) => { + resTyped.data += chunk.toString() }) - res.on('end', () => { - cb(null, res.data) + resTyped.on('end', () => { + cb(null, resTyped.data) }) }) .end((err, res) => { @@ -442,24 +510,27 @@ describe('Upload/Download Routes @api-rest', () => { }) it('Should properly download an object, with all its children, into a text/plain response', (done) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises request(app) .get(`/objects/${testStream.id}/${parentId}`) .set('Authorization', userA.token) .set('Accept', 'text/plain') .buffer() .parse((res, cb) => { - res.data = '' - res.on('data', (chunk) => { - res.data += chunk.toString() + const resTyped = res as typeof res & { data: string } + + resTyped.data = '' + resTyped.on('data', (chunk) => { + resTyped.data += chunk.toString() }) - res.on('end', () => { - cb(null, res.data) + resTyped.on('end', () => { + cb(null, resTyped.data) }) }) .end((err, res) => { if (err) done(err) try { - const o = res.body.split('\n').filter((l) => l !== '') + const o = res.body.split('\n').filter((l: string) => l !== '') expect(o.length).to.equal(numObjs + 1) expect(res).to.be.text done() @@ -474,6 +545,7 @@ describe('Upload/Download Routes @api-rest', () => { for (let i = 0; i < objBatches[0].length; i++) { objectIds.push(objBatches[0][i].id) } + // eslint-disable-next-line @typescript-eslint/no-floating-promises request(app) .post(`/api/getobjects/${testStream.id}`) .set('Authorization', userA.token) @@ -481,18 +553,20 @@ describe('Upload/Download Routes @api-rest', () => { .send({ objects: JSON.stringify(objectIds) }) .buffer() .parse((res, cb) => { - res.data = '' - res.on('data', (chunk) => { - res.data += chunk.toString() + const resTyped = res as typeof res & { data: string } + + resTyped.data = '' + resTyped.on('data', (chunk) => { + resTyped.data += chunk.toString() }) - res.on('end', () => { - cb(null, res.data) + resTyped.on('end', () => { + cb(null, resTyped.data) }) }) .end((err, res) => { if (err) done(err) try { - const o = res.body.split('\n').filter((l) => l !== '') + const o = res.body.split('\n').filter((l: string) => l !== '') expect(o.length).to.equal(objectIds.length) expect(res).to.be.text done() @@ -530,7 +604,7 @@ describe('Upload/Download Routes @api-rest', () => { for (let i = 0; i < objBatches[0].length; i++) { objectIds.push(objBatches[0][i].id) } - const fakeIds = [] + const fakeIds: string[] = [] for (let i = 0; i < 100; i++) { const fakeId = crypto .createHash('md5') @@ -540,18 +614,21 @@ describe('Upload/Download Routes @api-rest', () => { objectIds.push(fakeId) } + // eslint-disable-next-line @typescript-eslint/no-floating-promises request(app) .post(`/api/diff/${testStream.id}`) .set('Authorization', userA.token) .send({ objects: JSON.stringify(objectIds) }) .buffer() .parse((res, cb) => { - res.data = '' - res.on('data', (chunk) => { - res.data += chunk.toString() + const resTyped = res as typeof res & { data: string } + + resTyped.data = '' + resTyped.on('data', (chunk) => { + resTyped.data += chunk.toString() }) - res.on('end', () => { - cb(null, res.data) + resTyped.on('end', () => { + cb(null, resTyped.data) }) }) .end((err, res) => { @@ -590,7 +667,7 @@ describe('Upload/Download Routes @api-rest', () => { }) describe('Express @core-rest', () => { - let app + let app: Express.Express before(async () => { ;({ app } = await beforeEachContext()) }) diff --git a/packages/server/modules/core/tests/streams.spec.ts b/packages/server/modules/core/tests/streams.spec.ts index 2faa2bd59..b482495fb 100644 --- a/packages/server/modules/core/tests/streams.spec.ts +++ b/packages/server/modules/core/tests/streams.spec.ts @@ -73,8 +73,12 @@ import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/pr import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' import { deleteAllResourceInvitesFactory, + deleteInvitesByTargetFactory, + deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, - insertInviteAndDeleteOldFactory + insertInviteAndDeleteOldFactory, + updateAllInviteTargetsFactory } from '@/modules/serverinvites/repositories/serverInvites' import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' @@ -94,6 +98,24 @@ import { import { changeUserRoleFactory } from '@/modules/core/services/users/management' import { getServerInfoFactory } from '@/modules/core/repositories/server' import { createObjectFactory } from '@/modules/core/services/objects/management' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + createUserEmailFactory, + ensureNoPrimaryEmailForUserFactory, + findEmailFactory +} from '@/modules/core/repositories/userEmails' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) @@ -129,6 +151,51 @@ const createCommitByBranchName = createCommitByBranchNameFactory({ getBranchById: getBranchByIdFactory({ db }) }) +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -147,7 +214,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/core/tests/users.spec.ts b/packages/server/modules/core/tests/users.spec.ts index f2395bc8d..22f0e92d5 100644 --- a/packages/server/modules/core/tests/users.spec.ts +++ b/packages/server/modules/core/tests/users.spec.ts @@ -54,7 +54,9 @@ import { insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, updateAllInviteTargetsFactory, - deleteAllUserInvitesFactory + deleteAllUserInvitesFactory, + findInviteFactory, + deleteInvitesByTargetFactory } from '@/modules/serverinvites/repositories/serverInvites' import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' @@ -95,7 +97,10 @@ import { changeUserRoleFactory } from '@/modules/core/services/users/management' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' -import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { dbLogger } from '@/observability/logging' import { storeApiTokenFactory, @@ -114,6 +119,15 @@ import { getServerInfoFactory } from '@/modules/core/repositories/server' import { getPaginatedBranchCommitsItemsByNameFactory } from '@/modules/core/services/commit/retrieval' import { getPaginatedStreamBranchesFactory } from '@/modules/core/services/branch/retrieval' import { createObjectFactory } from '@/modules/core/services/objects/management' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' const getServerInfo = getServerInfoFactory({ db }) const getUser = legacyGetUserFactory({ db }) @@ -141,6 +155,51 @@ const createCommitByBranchName = createCommitByBranchNameFactory({ getBranchById: getBranchByIdFactory({ db }) }) +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -159,7 +218,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser: getUserFactory({ db }), - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/core/tests/usersAdminList.spec.ts b/packages/server/modules/core/tests/usersAdminList.spec.ts index bbcfde3d4..5b7376970 100644 --- a/packages/server/modules/core/tests/usersAdminList.spec.ts +++ b/packages/server/modules/core/tests/usersAdminList.spec.ts @@ -11,7 +11,8 @@ import { wait } from '@speckle/shared' import { createAuthedTestContext, ServerAndContext } from '@/test/graphqlHelper' import { createStreamFactory, - getStreamFactory + getStreamFactory, + grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { db } from '@/db/knex' import { @@ -21,7 +22,9 @@ import { import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' import { + deleteInvitesByTargetFactory, deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, insertInviteAndDeleteOldFactory, updateAllInviteTargetsFactory @@ -48,8 +51,20 @@ import { sendEmail } from '@/modules/emails/services/sending' import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' import { createUserFactory } from '@/modules/core/services/users/management' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' -import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' // To ensure that the invites are created in the correct order, we need to wait a bit between each creation const WAIT_TIMEOUT = 5 @@ -58,6 +73,52 @@ const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) const getUsers = getUsersFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -76,7 +137,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), @@ -264,7 +326,10 @@ describe('[Admin users list]', () => { userId }, ownerId - ) + ).then((invite) => ({ + inviteId: invite.id, + token: invite.token + })) ) } diff --git a/packages/server/modules/fileuploads/tests/fileuploads.integration.spec.ts b/packages/server/modules/fileuploads/tests/fileuploads.integration.spec.ts index 81b4338e1..bbe020c3d 100644 --- a/packages/server/modules/fileuploads/tests/fileuploads.integration.spec.ts +++ b/packages/server/modules/fileuploads/tests/fileuploads.integration.spec.ts @@ -11,7 +11,8 @@ import cryptoRandomString from 'crypto-random-string' import { noErrors } from '@/test/helpers' import { createStreamFactory, - getStreamFactory + getStreamFactory, + grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { db } from '@/db/knex' import { @@ -21,7 +22,9 @@ import { import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' import { + deleteInvitesByTargetFactory, deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, insertInviteAndDeleteOldFactory, updateAllInviteTargetsFactory @@ -47,7 +50,10 @@ import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repos import { renderEmail } from '@/modules/emails/services/emailRendering' import { createUserFactory } from '@/modules/core/services/users/management' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' -import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { sendEmail } from '@/modules/emails/services/sending' import { createTokenFactory } from '@/modules/core/services/tokens' import { @@ -57,11 +63,66 @@ import { } from '@/modules/core/repositories/tokens' import { getServerInfoFactory } from '@/modules/core/repositories/server' import { TIME_MS } from '@speckle/shared' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) const getUsers = getUsersFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -80,7 +141,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/fileuploads/tests/fileuploads.spec.ts b/packages/server/modules/fileuploads/tests/fileuploads.spec.ts index 45257347a..da4d3ee28 100644 --- a/packages/server/modules/fileuploads/tests/fileuploads.spec.ts +++ b/packages/server/modules/fileuploads/tests/fileuploads.spec.ts @@ -1,7 +1,8 @@ import cryptoRandomString from 'crypto-random-string' import { createStreamFactory, - getStreamFactory + getStreamFactory, + grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { db } from '@/db/knex' import { @@ -11,7 +12,9 @@ import { import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' import { + deleteInvitesByTargetFactory, deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, insertInviteAndDeleteOldFactory, updateAllInviteTargetsFactory @@ -40,7 +43,10 @@ import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repos import { renderEmail } from '@/modules/emails/services/emailRendering' import { createUserFactory } from '@/modules/core/services/users/management' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' -import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { sendEmail } from '@/modules/emails/services/sending' import { getServerInfoFactory } from '@/modules/core/repositories/server' import { manageFileImportExpiryFactory } from '@/modules/fileuploads/services/tasks' @@ -59,11 +65,66 @@ import { sleep } from '@/test/helpers' import { expect } from 'chai' import { FileUploadConvertedStatus } from '@/modules/fileuploads/helpers/types' import { TIME } from '@speckle/shared' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) const getUsers = getUsersFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -82,7 +143,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/serverinvites/domain/events.ts b/packages/server/modules/serverinvites/domain/events.ts index ec1bb1df9..9c54962ad 100644 --- a/packages/server/modules/serverinvites/domain/events.ts +++ b/packages/server/modules/serverinvites/domain/events.ts @@ -18,6 +18,11 @@ export type ServerInvitesEventsPayloads = { invite: ServerInviteRecord finalizerUserId: string accept: boolean + /** + * finalizerUserId will always be the invite target. This field will be the actual person triggering the action, + * which in auto-accept flows will be the initial inviter. Use this for reporting. + */ + trueFinalizerUserId: string } [ServerInvitesEvents.Canceled]: { invite: ServerInviteRecord diff --git a/packages/server/modules/serverinvites/domain/types.ts b/packages/server/modules/serverinvites/domain/types.ts index ba7b0d074..6016cf019 100644 --- a/packages/server/modules/serverinvites/domain/types.ts +++ b/packages/server/modules/serverinvites/domain/types.ts @@ -35,6 +35,12 @@ export type PrimaryInviteResourceTarget< * If invite also has secondary resource targets, you can specify the expected roles here */ secondaryResourceRoles?: Partial + + /** + * Whether the invite should be auto accepted or not. If this is true, no invite is actually created or email sent, + * and the accept process is done automatically without user involvement. + */ + autoAccept?: boolean } export type ServerInviteResourceTarget = InviteResourceTarget< diff --git a/packages/server/modules/serverinvites/graph/resolvers/serverInvites.ts b/packages/server/modules/serverinvites/graph/resolvers/serverInvites.ts index 4f69ab782..41ab38a66 100644 --- a/packages/server/modules/serverinvites/graph/resolvers/serverInvites.ts +++ b/packages/server/modules/serverinvites/graph/resolvers/serverInvites.ts @@ -110,6 +110,35 @@ const buildCollectAndValidateResourceTargets = () => getStream }) +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaborator + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification + }), + collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), + getUser, + getServerInfo + }) + const buildCreateAndSendServerOrProjectInvite = () => createAndSendInviteFactory({ findUserByTarget: findUserByTargetFactory({ db }), @@ -124,7 +153,8 @@ const buildCreateAndSendServerOrProjectInvite = () => payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }) export = { @@ -375,33 +405,7 @@ export = { streamId: projectId //legacy }) const useProjectInvite = useProjectInviteAndNotifyFactory({ - finalizeInvite: finalizeResourceInviteFactory({ - findInvite: findInviteFactory({ db }), - validateInvite: validateProjectInviteBeforeFinalizationFactory({ - getProject: getStream - }), - processInvite: processFinalizedProjectInviteFactory({ - getProject: getStream, - addProjectRole: addOrUpdateStreamCollaborator - }), - deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), - insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), - emitEvent: (...args) => getEventBus().emit(...args), - findEmail: findEmailFactory({ db }), - validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ - createUserEmail: createUserEmailFactory({ db }), - ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), - findEmail: findEmailFactory({ db }), - updateEmailInvites: finalizeInvitedServerRegistrationFactory({ - deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), - updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) - }), - requestNewEmailVerification - }), - collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), - getUser, - getServerInfo - }) + finalizeInvite: buildFinalizeProjectInvite() }) await withOperationLogging( @@ -617,33 +621,7 @@ export = { async use(_parent, args, ctx) { const logger = ctx.log const useProjectInvite = useProjectInviteAndNotifyFactory({ - finalizeInvite: finalizeResourceInviteFactory({ - findInvite: findInviteFactory({ db }), - validateInvite: validateProjectInviteBeforeFinalizationFactory({ - getProject: getStream - }), - processInvite: processFinalizedProjectInviteFactory({ - getProject: getStream, - addProjectRole: addOrUpdateStreamCollaborator - }), - deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), - insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), - emitEvent: (...args) => getEventBus().emit(...args), - findEmail: findEmailFactory({ db }), - validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ - createUserEmail: createUserEmailFactory({ db }), - ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), - findEmail: findEmailFactory({ db }), - updateEmailInvites: finalizeInvitedServerRegistrationFactory({ - deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), - updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) - }), - requestNewEmailVerification - }), - collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), - getUser, - getServerInfo - }) + finalizeInvite: buildFinalizeProjectInvite() }) await withOperationLogging( diff --git a/packages/server/modules/serverinvites/helpers/core.ts b/packages/server/modules/serverinvites/helpers/core.ts index 19ee4c9f2..737bd379a 100644 --- a/packages/server/modules/serverinvites/helpers/core.ts +++ b/packages/server/modules/serverinvites/helpers/core.ts @@ -69,6 +69,10 @@ export const isProjectResourceTarget = ( ): target is ProjectInviteResourceTarget => target.resourceType === ProjectInviteResourceType +export const isPrimaryResourceTarget = ( + target: InviteResourceTarget +): target is PrimaryInviteResourceTarget => 'primary' in target && !!target.primary + export interface ResourceTargetTypeRoleTypeMap { [ServerInviteResourceType]: ServerRoles [ProjectInviteResourceType]: StreamRoles diff --git a/packages/server/modules/serverinvites/services/creation.ts b/packages/server/modules/serverinvites/services/creation.ts index b6a809dba..fb8a7b9e4 100644 --- a/packages/server/modules/serverinvites/services/creation.ts +++ b/packages/server/modules/serverinvites/services/creation.ts @@ -19,6 +19,7 @@ import { BuildInviteEmailContents, CollectAndValidateResourceTargets, CreateAndSendInvite, + FinalizeInvite, ResendInviteEmail } from '@/modules/serverinvites/services/operations' import { renderEmail } from '@/modules/emails/services/emailRendering' @@ -91,7 +92,8 @@ export const createAndSendInviteFactory = buildInviteEmailContents, emitEvent, getUser, - getServerInfo + getServerInfo, + finalizeInvite }: { findUserByTarget: FindUserByTarget insertInviteAndDeleteOld: InsertInviteAndDeleteOld @@ -100,6 +102,7 @@ export const createAndSendInviteFactory = emitEvent: EventBusEmit getUser: GetUser getServerInfo: GetServerInfo + finalizeInvite: FinalizeInvite }): CreateAndSendInvite => async (params, inviterResourceAccessLimits?) => { const sendInviteEmail = sendInviteEmailFactory({ buildInviteEmailContents }) @@ -165,6 +168,19 @@ export const createAndSendInviteFactory = targetUser ? [targetUser.email, buildUserTarget(targetUser.id)!] : [] ) + const autoAccept = finalPrimaryResource.autoAccept + if (autoAccept && targetUser?.id) { + await finalizeInvite({ + finalizerUserId: targetUser.id, + finalizerResourceAccessLimits: inviterResourceAccessLimits, + accept: true, + token: invite.token, + resourceType: finalPrimaryResource.resourceType, + trueFinalizerId: inviterId + }) + return + } + // generate and send email await sendInviteEmail({ invite: finalInvite, @@ -180,11 +196,6 @@ export const createAndSendInviteFactory = invite: finalInvite } }) - - return { - inviteId: invite.id, - token: invite.token - } } /** diff --git a/packages/server/modules/serverinvites/services/operations.ts b/packages/server/modules/serverinvites/services/operations.ts index d042d18a4..c539e4cc0 100644 --- a/packages/server/modules/serverinvites/services/operations.ts +++ b/packages/server/modules/serverinvites/services/operations.ts @@ -21,7 +21,7 @@ export type InviteResult = { export type CreateAndSendInvite = ( params: CreateInviteParams, inviterResourceAccessLimits: MaybeNullOrUndefined -) => Promise +) => Promise export type FinalizeInvite = (params: { finalizerUserId: string @@ -35,6 +35,11 @@ export type FinalizeInvite = (params: { * If the invite is accepted, the email will be attached to the user account as well in a verified state. */ allowAttachingNewEmail?: boolean + /** + * Allow someone else besides the target user to finalize the invite. Used in auto-accept flows. The finalizerUserId + * must be the target of the invite, but this different one will be used in reporting/activityStream actions + */ + trueFinalizerId?: string }) => Promise export type ResendInviteEmail = (params: { @@ -80,6 +85,9 @@ export enum InviteFinalizationAction { */ export type ValidateResourceInviteBeforeFinalization = (params: { invite: ServerInviteRecord + /** + * Not necessarily the invite target, can also be the inviter in case of auto-accept + */ finalizerUserId: string finalizerResourceAccessLimits: MaybeNullOrUndefined action: InviteFinalizationAction diff --git a/packages/server/modules/serverinvites/services/processing.ts b/packages/server/modules/serverinvites/services/processing.ts index 6d2887cce..cc669092b 100644 --- a/packages/server/modules/serverinvites/services/processing.ts +++ b/packages/server/modules/serverinvites/services/processing.ts @@ -202,7 +202,8 @@ export const finalizeResourceInviteFactory = token, resourceType, finalizerResourceAccessLimits, - allowAttachingNewEmail + allowAttachingNewEmail, + trueFinalizerId } = params const finalizerUserTarget = buildUserTarget(finalizerUserId) @@ -318,7 +319,8 @@ export const finalizeResourceInviteFactory = payload: { invite, accept, - finalizerUserId + finalizerUserId, + trueFinalizerUserId: trueFinalizerId || finalizerUserId } }) } diff --git a/packages/server/modules/serverinvites/tests/invites.spec.ts b/packages/server/modules/serverinvites/tests/invites.spec.ts index e9fb9ddaf..ea9b2e0a4 100644 --- a/packages/server/modules/serverinvites/tests/invites.spec.ts +++ b/packages/server/modules/serverinvites/tests/invites.spec.ts @@ -490,14 +490,12 @@ describe('[Stream & Server Invites]', () => { }) // Creating some invites - await Promise.all( - invites.map((i) => - createInviteDirectly(i, me.id).then((o) => { - i.inviteId = o.inviteId - i.token = o.token - }) - ) - ) + for (const invite of invites) { + await createInviteDirectly(invite, me.id).then((o) => { + invite.inviteId = o.id + invite.token = o.token + }) + } }) it('they can resend pre-existing invites irregardless of type', async () => { @@ -568,14 +566,12 @@ describe('[Stream & Server Invites]', () => { } ] - await Promise.all( - deletableInvites.map((i) => - createInviteDirectly(i, me.id).then((o) => { - i.inviteId = o.inviteId - i.token = o.token - }) - ) - ) + for (const deletableInvite of deletableInvites) { + await createInviteDirectly(deletableInvite, me.id).then((o) => { + deletableInvite.inviteId = o.id + deletableInvite.token = o.token + }) + } // Delete all invites for (const invite of deletableInvites) { @@ -695,7 +691,7 @@ describe('[Stream & Server Invites]', () => { // Create an invite before each test so that we can mutate them // in each test as needed await createInviteDirectly(inviteFromOtherGuy, otherGuy.id).then((o) => { - inviteFromOtherGuy.inviteId = o.inviteId + inviteFromOtherGuy.inviteId = o.id inviteFromOtherGuy.token = o.token }) }) @@ -804,23 +800,21 @@ describe('[Stream & Server Invites]', () => { ]) // Create a couple of static invites that shouldn't be mutated in tests - await Promise.all([ - createInviteDirectly(myInvite, me.id).then((o) => { - myInvite.inviteId = o.inviteId - myInvite.token = o.token - }), - createInviteDirectly(otherGuysInvite, otherGuy.id).then((o) => { - otherGuysInvite.inviteId = o.inviteId - otherGuysInvite.token = o.token - }) - ]) + await createInviteDirectly(myInvite, me.id).then((o) => { + myInvite.inviteId = o.id + myInvite.token = o.token + }) + await createInviteDirectly(otherGuysInvite, otherGuy.id).then((o) => { + otherGuysInvite.inviteId = o.id + otherGuysInvite.token = o.token + }) }) beforeEach(async () => { // Create an invite before each test so that we can mutate them // in each test as needed await createInviteDirectly(dynamicInvite, me.id).then((o) => { - dynamicInvite.inviteId = o.inviteId + dynamicInvite.inviteId = o.id dynamicInvite.token = o.token }) }) @@ -895,24 +889,22 @@ describe('[Stream & Server Invites]', () => { await createTestUser(ownInvitesGuy) // Invite him to a few streams - await Promise.all([ - createInviteDirectly( - { - stream: myPrivateStream, - // SPecifically w/ email - email: ownInvitesGuy.email - }, - me.id - ), - createInviteDirectly( - { - // Specifically w/ id - userId: ownInvitesGuy.id, - stream: otherGuysStream - }, - otherGuy.id - ) - ]) + await createInviteDirectly( + { + stream: myPrivateStream, + // SPecifically w/ email + email: ownInvitesGuy.email + }, + me.id + ) + await createInviteDirectly( + { + // Specifically w/ id + userId: ownInvitesGuy.id, + stream: otherGuysStream + }, + otherGuy.id + ) // Build authenticated apollo instance apollo = await testApolloServer({ authUserId: ownInvitesGuy.id }) diff --git a/packages/server/modules/stats/tests/stats.spec.ts b/packages/server/modules/stats/tests/stats.spec.ts index dce68352c..e8aa12d0a 100644 --- a/packages/server/modules/stats/tests/stats.spec.ts +++ b/packages/server/modules/stats/tests/stats.spec.ts @@ -27,6 +27,7 @@ import { import { createStreamFactory, getStreamFactory, + grantStreamPermissionsFactory, markCommitStreamUpdatedFactory } from '@/modules/core/repositories/streams' import { @@ -40,7 +41,9 @@ import { import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' import { + deleteInvitesByTargetFactory, deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, insertInviteAndDeleteOldFactory, updateAllInviteTargetsFactory @@ -66,7 +69,10 @@ import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { createUserFactory } from '@/modules/core/services/users/management' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' -import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { createPersonalAccessTokenFactory } from '@/modules/core/services/tokens' import { storeApiTokenFactory, @@ -76,9 +82,19 @@ import { } from '@/modules/core/repositories/tokens' import { getServerInfoFactory } from '@/modules/core/repositories/server' import { createObjectsFactory } from '@/modules/core/services/objects/management' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' const getServerInfo = getServerInfoFactory({ db }) const getUsers = getUsersFactory({ db }) +const getUser = getUserFactory({ db }) const markCommitStreamUpdated = markCommitStreamUpdatedFactory({ db }) const getObject = getObjectFactory({ db }) const createCommitByBranchId = createCommitByBranchIdFactory({ @@ -99,6 +115,50 @@ const createCommitByBranchName = createCommitByBranchNameFactory({ }) const getStream = getStreamFactory({ db }) +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -117,7 +177,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser: getUserFactory({ db }), - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/webhooks/services/webhooks.ts b/packages/server/modules/webhooks/services/webhooks.ts index 50ade93b5..b9dd30552 100644 --- a/packages/server/modules/webhooks/services/webhooks.ts +++ b/packages/server/modules/webhooks/services/webhooks.ts @@ -124,6 +124,7 @@ export const dispatchStreamEventFactory = stream?: StreamWithOptionalRole userId?: string | null user?: Partial | null + test?: string } }) => { const payload: typeof eventPayload & { diff --git a/packages/server/modules/webhooks/tests/cleanup.spec.ts b/packages/server/modules/webhooks/tests/cleanup.spec.ts index 9598d1972..3fe486d96 100644 --- a/packages/server/modules/webhooks/tests/cleanup.spec.ts +++ b/packages/server/modules/webhooks/tests/cleanup.spec.ts @@ -7,7 +7,8 @@ import { createBranchFactory } from '@/modules/core/repositories/branches' import { getServerInfoFactory } from '@/modules/core/repositories/server' import { createStreamFactory, - getStreamFactory + getStreamFactory, + grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { createUserEmailFactory, @@ -32,7 +33,9 @@ import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' import { + deleteInvitesByTargetFactory, deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, insertInviteAndDeleteOldFactory, updateAllInviteTargetsFactory @@ -40,13 +43,25 @@ import { import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' -import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' import { getEventBus } from '@/modules/shared/services/eventBus' import { truncateTables } from '@/test/hooks' import { expect } from 'chai' import crs from 'crypto-random-string' import { cleanOrphanedWebhookConfigsFactory } from '@/modules/webhooks/repositories/cleanup' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' const WEBHOOKS_CONFIG_TABLE = 'webhooks_config' const WEBHOOKS_EVENTS_TABLE = 'webhooks_events' @@ -59,6 +74,52 @@ const getServerInfo = getServerInfoFactory({ db }) const getUsers = getUsersFactory({ db }) const getUser = getUserFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -77,7 +138,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), diff --git a/packages/server/modules/webhooks/tests/webhooks.spec.js b/packages/server/modules/webhooks/tests/webhooks.spec.ts similarity index 74% rename from packages/server/modules/webhooks/tests/webhooks.spec.js rename to packages/server/modules/webhooks/tests/webhooks.spec.ts index 99d6e83c7..e1028e7af 100644 --- a/packages/server/modules/webhooks/tests/webhooks.spec.js +++ b/packages/server/modules/webhooks/tests/webhooks.spec.ts @@ -1,11 +1,11 @@ /* istanbul ignore file */ -const expect = require('chai').expect -const assert = require('assert') +import { expect } from 'chai' +import assert from 'assert' -const { beforeEachContext, initializeTestServer } = require('@/test/hooks') -const { noErrors } = require('@/test/helpers') -const { Scopes, Roles } = require('@speckle/shared') -const { +import { beforeEachContext, initializeTestServer } from '@/test/hooks' +import { noErrors } from '@/test/helpers' +import { Scopes, Roles, ensureError } from '@speckle/shared' +import { createWebhookConfigFactory, countWebhooksByStreamIdFactory, getWebhookByIdFactory, @@ -14,78 +14,77 @@ const { getStreamWebhooksFactory, createWebhookEventFactory, getLastWebhookEventsFactory -} = require('@/modules/webhooks/repositories/webhooks') -const { db } = require('@/db/knex') -const { +} from '@/modules/webhooks/repositories/webhooks' +import { db } from '@/db/knex' +import { createWebhookFactory, updateWebhookFactory, deleteWebhookFactory, dispatchStreamEventFactory -} = require('@/modules/webhooks/services/webhooks') -const { +} from '@/modules/webhooks/services/webhooks' +import { getStreamFactory, createStreamFactory, grantStreamPermissionsFactory -} = require('@/modules/core/repositories/streams') -const { +} from '@/modules/core/repositories/streams' +import { legacyCreateStreamFactory, createStreamReturnRecordFactory -} = require('@/modules/core/services/streams/management') -const { - inviteUsersToProjectFactory -} = require('@/modules/serverinvites/services/projectInviteManagement') -const { - createAndSendInviteFactory -} = require('@/modules/serverinvites/services/creation') -const { +} from '@/modules/core/services/streams/management' +import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' +import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' +import { findUserByTargetFactory, insertInviteAndDeleteOldFactory, deleteServerOnlyInvitesFactory, - updateAllInviteTargetsFactory -} = require('@/modules/serverinvites/repositories/serverInvites') -const { - collectAndValidateCoreTargetsFactory -} = require('@/modules/serverinvites/services/coreResourceCollection') -const { - buildCoreInviteEmailContentsFactory -} = require('@/modules/serverinvites/services/coreEmailContents') -const { getEventBus } = require('@/modules/shared/services/eventBus') -const { createBranchFactory } = require('@/modules/core/repositories/branches') -const { + updateAllInviteTargetsFactory, + findInviteFactory, + deleteInvitesByTargetFactory +} from '@/modules/serverinvites/repositories/serverInvites' +import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' +import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { createBranchFactory } from '@/modules/core/repositories/branches' +import { getUserFactory, getUsersFactory, storeUserFactory, countAdminUsersFactory, storeUserAclFactory -} = require('@/modules/core/repositories/users') -const { +} from '@/modules/core/repositories/users' +import { findEmailFactory, createUserEmailFactory, ensureNoPrimaryEmailForUserFactory -} = require('@/modules/core/repositories/userEmails') -const { - requestNewEmailVerificationFactory -} = require('@/modules/emails/services/verification/request') -const { - deleteOldAndInsertNewVerificationFactory -} = require('@/modules/emails/repositories') -const { renderEmail } = require('@/modules/emails/services/emailRendering') -const { sendEmail } = require('@/modules/emails/services/sending') -const { createUserFactory } = require('@/modules/core/services/users/management') -const { - validateAndCreateUserEmailFactory -} = require('@/modules/core/services/userEmails') -const { - finalizeInvitedServerRegistrationFactory -} = require('@/modules/serverinvites/services/processing') -const { createPersonalAccessTokenFactory } = require('@/modules/core/services/tokens') -const { +} from '@/modules/core/repositories/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' +import { createUserFactory } from '@/modules/core/services/users/management' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { createPersonalAccessTokenFactory } from '@/modules/core/services/tokens' +import { storeApiTokenFactory, storeTokenScopesFactory, storeTokenResourceAccessDefinitionsFactory, storePersonalApiTokenFactory -} = require('@/modules/core/repositories/tokens') -const { getServerInfoFactory } = require('@/modules/core/repositories/server') +} from '@/modules/core/repositories/tokens' +import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' +import { omit } from 'lodash' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) @@ -95,6 +94,52 @@ const updateWebhook = updateWebhookFactory({ updateWebhookConfig: updateWebhookConfigFactory({ db }) }) const getStreamWebhooks = getStreamWebhooksFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -113,7 +158,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }), @@ -161,27 +207,32 @@ const createPersonalAccessToken = createPersonalAccessTokenFactory({ describe('Webhooks @webhooks', () => { const getWebhook = getWebhookByIdFactory({ db }) - let sendRequest + let sendRequest: Awaited>['sendRequest'] const userOne = { name: 'User', email: 'user@example.org', - password: 'jdsadjsadasfdsa' + password: 'jdsadjsadasfdsa', + id: '', + token: '' } const streamOne = { name: 'streamOne', description: 'stream', - isPublic: true + isPublic: true, + ownerId: '', + id: '' } const webhookOne = { - streamId: null, // filled in `before` + streamId: '', // filled in `before` url: 'http://localhost:42/non-existent', description: 'test wh', secret: 'secret', enabled: true, - triggers: ['commit_create', 'commit_update'] + triggers: ['commit_create', 'commit_update'], + id: '' } before(async () => { @@ -209,7 +260,7 @@ describe('Webhooks @webhooks', () => { const webhook = await getWebhook({ id: webhookOne.id }) expect(webhook).to.not.be.null expect(webhook).to.have.property('url') - expect(webhook.url).to.equal(webhookOne.url) + expect(webhook!.url).to.equal(webhookOne.url) }) it('Should update a webhook', async () => { @@ -223,7 +274,7 @@ describe('Webhooks @webhooks', () => { const webhook = await getWebhook({ id: webhookId }) expect(webhook).to.not.be.null expect(webhook).to.have.property('url') - expect(webhook.url).to.equal(newUrl) + expect(webhook!.url).to.equal(newUrl) }) it('Should delete a webhook', async () => { @@ -240,7 +291,8 @@ describe('Webhooks @webhooks', () => { description: 'test wh', secret: 'secret', enabled: true, - triggers: ['commit_create', 'commit_update'] + triggers: ['commit_create', 'commit_update'], + id: '' } webhook.id = await createWebhookFactory({ createWebhookConfig: createWebhookConfigFactory({ db }), @@ -307,7 +359,6 @@ describe('Webhooks @webhooks', () => { countWebhooksByStreamId: countWebhooksByStreamIdFactory({ db }) })(webhook) await dispatchStreamEventFactory({ - db, getServerInfo, getStream, createWebhookEvent: createWebhookEventFactory({ db }), @@ -328,22 +379,27 @@ describe('Webhooks @webhooks', () => { const userTwo = { name: 'User2', email: 'user2@example.org', - password: 'jdsadjsadasfdsa' + password: 'jdsadjsadasfdsa', + id: '', + token: '' } const webhookTwo = { - streamId: null, + streamId: '', url: 'http://localhost:42/non-existent-two', description: 'test wh no 2', secret: 'secret', enabled: true, - triggers: ['commit_create', 'commit_update'] + triggers: ['commit_create', 'commit_update'], + id: '' } const streamTwo = { name: 'streamTwo', description: 'stream', - isPublic: true + isPublic: true, + ownerId: '', + id: '' } before(async () => { @@ -373,7 +429,7 @@ describe('Webhooks @webhooks', () => { const res = await sendRequest(userTwo.token, { query: 'mutation createWebhook($webhook: WebhookCreateInput!) { webhookCreate( webhook: $webhook ) }', - variables: { webhook: webhookTwo } + variables: { webhook: omit(webhookTwo, ['id']) } }) expect(noErrors(res)) expect(res.body.data.webhookCreate).to.not.be.null @@ -382,7 +438,6 @@ describe('Webhooks @webhooks', () => { it('Should get stream webhooks and the previous events', async () => { await dispatchStreamEventFactory({ - db, getServerInfo, getStream, getStreamWebhooks: getStreamWebhooksFactory({ db }), @@ -420,9 +475,9 @@ describe('Webhooks @webhooks', () => { }) const webhook = await getWebhook({ id: webhookTwo.id }) expect(noErrors(res)) - expect(res.body.data.webhookUpdate).to.equal(webhook.id) - expect(webhook.description).to.equal('updated webhook') - expect(webhook.enabled).to.equal(false) + expect(res.body.data.webhookUpdate).to.equal(webhook!.id) + expect(webhook!.description).to.equal('updated webhook') + expect(webhook!.enabled).to.equal(false) }) it('Should *not* update or delete a webhook if the stream id and webhook id do not match', async () => { @@ -461,7 +516,8 @@ describe('Webhooks @webhooks', () => { description: 'test wh', secret: 'secret', enabled: true, - triggers: ['commit_create', 'commit_update'] + triggers: ['commit_create', 'commit_update'], + id: '' } webhook.id = await createWebhookFactory({ createWebhookConfig: createWebhookConfigFactory({ db }), @@ -475,11 +531,11 @@ describe('Webhooks @webhooks', () => { }) it('Should *not* create a webhook if user is not a stream owner', async () => { - delete webhookTwo.id + webhookTwo.id = '' const res = await sendRequest(userOne.token, { query: 'mutation createWebhook($webhook: WebhookCreateInput!) { webhookCreate( webhook: $webhook ) }', - variables: { webhook: webhookTwo } + variables: { webhook: omit(webhookTwo, ['id']) } }) expect(res.body.errors).to.exist expect(res.body.errors[0].extensions.code).to.equal('FORBIDDEN') @@ -525,7 +581,7 @@ describe('Webhooks @webhooks', () => { countWebhooksByStreamId: countWebhooksByStreamIdFactory({ db }) })(webhook) } catch (err) { - if (err.toString().indexOf('Maximum') > -1) return + if (ensureError(err).toString().indexOf('Maximum') > -1) return } assert.fail('Configured more webhooks than the limit') diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index 0aea2092b..9267c59a8 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -217,6 +217,10 @@ import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper' import { withOperationLogging } from '@/observability/domain/businessLogging' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' import { WorkspaceInvitesLimit } from '@/modules/workspaces/domain/constants' const eventBus = getEventBus() @@ -255,6 +259,90 @@ const buildCollectAndValidateResourceTargets = () => }) }) +const buildFinalizeWorkspaceInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ + db, + filterQuery: workspaceInviteValidityFilter + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: ({ eventName, payload }) => + getEventBus().emit({ + eventName, + payload + }), + validateInvite: validateWorkspaceInviteBeforeFinalizationFactory({ + getWorkspace: getWorkspaceFactory({ db }) + }), + processInvite: processFinalizedWorkspaceInviteFactory({ + getWorkspace: getWorkspaceFactory({ db }), + updateWorkspaceRole: updateWorkspaceRoleFactory({ + getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }), + findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }), + getWorkspaceRoles: getWorkspaceRolesFactory({ db }), + upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), + emitWorkspaceEvent: getEventBus().emit, + ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({ + createWorkspaceSeat: createWorkspaceSeatFactory({ db }), + getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }), + eventEmit: getEventBus().emit + }) + }) + }), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification + }), + collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), + getUser, + getServerInfo + }) + +const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver }) +const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess, + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit +}) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaborator + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification + }), + collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), + getUser, + getServerInfo + }) + const buildCreateAndSendServerOrProjectInvite = () => createAndSendInviteFactory({ findUserByTarget: findUserByTargetFactory({ db }), @@ -269,7 +357,8 @@ const buildCreateAndSendServerOrProjectInvite = () => payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }) const buildCreateAndSendWorkspaceInvite = () => @@ -287,9 +376,9 @@ const buildCreateAndSendWorkspaceInvite = () => payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeWorkspaceInvite() }) -const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver }) const isStreamCollaborator = isStreamCollaboratorFactory({ getStream }) @@ -1226,52 +1315,7 @@ export = FF_WORKSPACES_MODULE_ENABLED use: async (_parent, args, ctx) => { const logger = ctx.log - const finalizeInvite = finalizeResourceInviteFactory({ - findInvite: findInviteFactory({ - db, - filterQuery: workspaceInviteValidityFilter - }), - deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), - insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), - emitEvent: ({ eventName, payload }) => - getEventBus().emit({ - eventName, - payload - }), - validateInvite: validateWorkspaceInviteBeforeFinalizationFactory({ - getWorkspace: getWorkspaceFactory({ db }) - }), - processInvite: processFinalizedWorkspaceInviteFactory({ - getWorkspace: getWorkspaceFactory({ db }), - updateWorkspaceRole: updateWorkspaceRoleFactory({ - getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }), - findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }), - getWorkspaceRoles: getWorkspaceRolesFactory({ db }), - upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), - emitWorkspaceEvent: getEventBus().emit, - ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({ - createWorkspaceSeat: createWorkspaceSeatFactory({ db }), - getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }), - eventEmit: getEventBus().emit - }) - }) - }), - findEmail: findEmailFactory({ db }), - validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ - createUserEmail: createUserEmailFactory({ db }), - ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), - findEmail: findEmailFactory({ db }), - updateEmailInvites: finalizeInvitedServerRegistrationFactory({ - deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), - updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) - }), - requestNewEmailVerification - }), - collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), - getUser, - getServerInfo - }) - + const finalizeInvite = buildFinalizeWorkspaceInvite() await withOperationLogging( async () => await finalizeInvite({ diff --git a/packages/server/modules/workspaces/services/invites.ts b/packages/server/modules/workspaces/services/invites.ts index 0073e849c..e5804cd8f 100644 --- a/packages/server/modules/workspaces/services/invites.ts +++ b/packages/server/modules/workspaces/services/invites.ts @@ -30,6 +30,7 @@ import { } from '@/modules/serverinvites/errors' import { buildUserTarget, + isPrimaryResourceTarget, isProjectResourceTarget, resolveInviteTargetTitle, resolveTarget @@ -225,6 +226,11 @@ export const collectAndValidateWorkspaceTargetsFactory = userId: targetUser.id, projectRole }) + + // If project target is primary and user target is already a workspace member, mark invite as auto-acceptable + if (isPrimaryResourceTarget(projectTarget) && workspaceRole) { + projectTarget.autoAccept = true + } } // Do further validation only if we're actually planning to invite to a workspace diff --git a/packages/server/modules/workspaces/tests/helpers/creation.ts b/packages/server/modules/workspaces/tests/helpers/creation.ts index 347bc6261..14dd78ac9 100644 --- a/packages/server/modules/workspaces/tests/helpers/creation.ts +++ b/packages/server/modules/workspaces/tests/helpers/creation.ts @@ -1,11 +1,18 @@ import { db } from '@/db/knex' import { + createUserEmailFactory, + ensureNoPrimaryEmailForUserFactory, + findEmailFactory, findEmailsByUserIdFactory, findVerifiedEmailsByUserIdFactory } from '@/modules/core/repositories/userEmails' import { + deleteInvitesByTargetFactory, + deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, - insertInviteAndDeleteOldFactory + insertInviteAndDeleteOldFactory, + updateAllInviteTargetsFactory } from '@/modules/serverinvites/repositories/serverInvites' import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' import { getEventBus } from '@/modules/shared/services/eventBus' @@ -19,12 +26,15 @@ import { getWorkspaceDomainsFactory, storeWorkspaceDomainFactory, getWorkspaceBySlugFactory, - getWorkspaceRoleForUserFactory + getWorkspaceRoleForUserFactory, + workspaceInviteValidityFilter } from '@/modules/workspaces/repositories/workspaces' import { buildWorkspaceInviteEmailContentsFactory, collectAndValidateWorkspaceTargetsFactory, - createWorkspaceInviteFactory + createWorkspaceInviteFactory, + processFinalizedWorkspaceInviteFactory, + validateWorkspaceInviteBeforeFinalizationFactory } from '@/modules/workspaces/services/invites' import { createWorkspaceFactory, @@ -92,6 +102,16 @@ import { getWorkspaceSeatTypeToProjectRoleMappingFactory, validateWorkspaceMemberProjectRoleFactory } from '@/modules/workspaces/services/projects' +import { captureCreatedInvite } from '@/test/speckle-helpers/inviteHelper' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() @@ -378,10 +398,9 @@ export const createWorkspaceInviteDirectly = async ( const getServerInfo = getServerInfoFactory({ db }) const getStream = getStreamFactory({ db }) const getUser = getUserFactory({ db }) - const createAndSendInvite = createAndSendInviteFactory({ - findUserByTarget: findUserByTargetFactory({ db }), - insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), - collectAndValidateResourceTargets: collectAndValidateWorkspaceTargetsFactory({ + + const buildCollectAndValidateResourceTargets = () => + collectAndValidateWorkspaceTargetsFactory({ getStream, getWorkspace: getWorkspaceFactory({ db }), getWorkspaceDomains: getWorkspaceDomainsFactory({ db }), @@ -400,7 +419,68 @@ export const createWorkspaceInviteDirectly = async ( getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db }) }) }) - }), + }) + + const buildFinalizeWorkspaceInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ + db, + filterQuery: workspaceInviteValidityFilter + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: ({ eventName, payload }) => + getEventBus().emit({ + eventName, + payload + }), + validateInvite: validateWorkspaceInviteBeforeFinalizationFactory({ + getWorkspace: getWorkspaceFactory({ db }) + }), + processInvite: processFinalizedWorkspaceInviteFactory({ + getWorkspace: getWorkspaceFactory({ db }), + updateWorkspaceRole: updateWorkspaceRoleFactory({ + getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }), + findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }), + getWorkspaceRoles: getWorkspaceRolesFactory({ db }), + upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), + emitWorkspaceEvent: getEventBus().emit, + ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({ + createWorkspaceSeat: createWorkspaceSeatFactory({ db }), + getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }), + eventEmit: getEventBus().emit + }) + }) + }), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), + getUser, + getServerInfo + }) + + const createAndSendInvite = createAndSendInviteFactory({ + findUserByTarget: findUserByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), buildInviteEmailContents: buildWorkspaceInviteEmailContentsFactory({ getStream, getWorkspace: getWorkspaceFactory({ db }) @@ -411,18 +491,22 @@ export const createWorkspaceInviteDirectly = async ( payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeWorkspaceInvite() }) const createInvite = createWorkspaceInviteFactory({ createAndSendInvite }) - return await createInvite({ - ...args, - inviterId, - inviterResourceAccessRules: null - }) + return await captureCreatedInvite( + async () => + await createInvite({ + ...args, + inviterId, + inviterResourceAccessRules: null + }) + ) } export const createTestOidcProvider = async ( diff --git a/packages/server/modules/workspaces/tests/integration/invites.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/invites.graph.spec.ts index 44f924d1b..bd9a98245 100644 --- a/packages/server/modules/workspaces/tests/integration/invites.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/invites.graph.spec.ts @@ -559,7 +559,9 @@ describe('Workspaces Invites GQL', () => { Roles.Stream.Owner, me.id ) + }) + beforeEach(async () => { // Remove all project access from workspaceMemberWithNoProjectAccess await Promise.all([ leaveStream( @@ -630,6 +632,38 @@ describe('Workspaces Invites GQL', () => { expect(res.data?.projectMutations.invites.createForWorkspace.id).to.not.be.ok }) + it('can invite to workspace project as admin, even if target doesnt belong to workspace', async () => { + const sendEmailInvocations = EmailSendingServiceMock.hijackFunction( + 'sendEmail', + async () => true + ) + + const res = await gqlHelpers.createWorkspaceProjectInvite({ + projectId: myProjectInviteTargetWorkspaceProject.id, + inputs: [ + { + userId: otherGuy.id, + role: Roles.Stream.Reviewer + } + ] + }) + + expect(res).to.not.haveGraphQLErrors() + expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok + + // no auto-accept, since target is not a workspace member + expect(sendEmailInvocations.args).to.have.lengthOf(1) + const emailParams = sendEmailInvocations.args[0][0] + await validateInviteExistanceFromEmail(emailParams) + + await gqlHelpers.validateResourceAccess({ + shouldHaveAccess: false, + userId: otherGuy.id, + workspaceId: myProjectInviteTargetWorkspace.id, + streamId: myProjectInviteTargetWorkspaceProject.id + }) + }) + it('can invite to workspace project even if not workspace admin, if target already belongs to workspace', async () => { const res = await gqlHelpers.createWorkspaceProjectInvite( { @@ -652,6 +686,38 @@ describe('Workspaces Invites GQL', () => { expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok }) + it('invite auto-accepted if both users already belong to the workspace', async () => { + const sendEmailInvocations = EmailSendingServiceMock.hijackFunction( + 'sendEmail', + async () => true + ) + + const res = await gqlHelpers.createWorkspaceProjectInvite({ + projectId: myProjectInviteTargetWorkspaceProject.id, + inputs: [ + { + userId: workspaceMemberWithNoProjectAccess.id, + role: Roles.Stream.Reviewer + } + ] + }) + + expect(res).to.not.haveGraphQLErrors() + expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok + + // No invite email should be sent out, due to auto-accept + expect(sendEmailInvocations.length()).to.eq(0) + + // Should have project role + await gqlHelpers.validateResourceAccess({ + shouldHaveAccess: true, + userId: workspaceMemberWithNoProjectAccess.id, + workspaceId: myProjectInviteTargetWorkspace.id, + streamId: myProjectInviteTargetWorkspaceProject.id, + expectedProjectRole: Roles.Stream.Reviewer + }) + }) + it("can't invite a workspace guest to be a workspace project owner", async () => { const res = await gqlHelpers.createWorkspaceProjectInvite({ projectId: myProjectInviteTargetWorkspaceProject.id, @@ -1097,7 +1163,7 @@ describe('Workspaces Invites GQL', () => { }, me.id ) - expect(brokenInvite.inviteId).to.be.ok + expect(brokenInvite.id).to.be.ok // Db query directly, cause this isn't a supported use case await Workspaces.knex() @@ -1569,8 +1635,7 @@ describe('Workspaces Invites GQL', () => { expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations.invites.use).to.be.ok - expect(await findInviteFactory({ db })({ inviteId: invite.inviteId })).to.be.not - .ok + expect(await findInviteFactory({ db })({ inviteId: invite.id })).to.be.not.ok await gqlHelpers.validateResourceAccess({ shouldHaveAccess: true, diff --git a/packages/server/scripts/streamObjects.js b/packages/server/scripts/streamObjects.js deleted file mode 100644 index dd6f48b8e..000000000 --- a/packages/server/scripts/streamObjects.js +++ /dev/null @@ -1,152 +0,0 @@ -require('../bootstrap') -const { createManyObjects } = require('@/test/helpers') -const { fetch } = require('undici') -const { init } = require(`@/app`) -const request = require('supertest') -const { exit } = require('yargs') -const { logger } = require('@/observability/logging') -const { Scopes } = require('@speckle/shared') -const { - getStreamFactory, - createStreamFactory -} = require('@/modules/core/repositories/streams') -const { db } = require('@/db/knex') -const { - legacyCreateStreamFactory, - createStreamReturnRecordFactory -} = require('@/modules/core/services/streams/management') -const { - inviteUsersToProjectFactory -} = require('@/modules/serverinvites/services/projectInviteManagement') -const { - createAndSendInviteFactory -} = require('@/modules/serverinvites/services/creation') -const { - findUserByTargetFactory, - insertInviteAndDeleteOldFactory -} = require('@/modules/serverinvites/repositories/serverInvites') -const { - collectAndValidateCoreTargetsFactory -} = require('@/modules/serverinvites/services/coreResourceCollection') -const { - buildCoreInviteEmailContentsFactory -} = require('@/modules/serverinvites/services/coreEmailContents') -const { getEventBus } = require('@/modules/shared/services/eventBus') -const { createBranchFactory } = require('@/modules/core/repositories/branches') -const { - getUsersFactory, - getUserFactory, - legacyGetUserByEmailFactory -} = require('@/modules/core/repositories/users') -const { createPersonalAccessTokenFactory } = require('@/modules/core/services/tokens') -const { - storeApiTokenFactory, - storeTokenScopesFactory, - storeTokenResourceAccessDefinitionsFactory, - storePersonalApiTokenFactory -} = require('@/modules/core/repositories/tokens') -const { getServerInfoFactory } = require('@/modules/core/repositories/server') - -const getServerInfo = getServerInfoFactory({ db }) -const getUsers = getUsersFactory({ db }) -const getUser = getUserFactory({ db }) -const getStream = getStreamFactory({ db }) -const createStream = legacyCreateStreamFactory({ - createStreamReturnRecord: createStreamReturnRecordFactory({ - inviteUsersToProject: inviteUsersToProjectFactory({ - createAndSendInvite: createAndSendInviteFactory({ - findUserByTarget: findUserByTargetFactory({ db }), - insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), - collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ - getStream - }), - buildInviteEmailContents: buildCoreInviteEmailContentsFactory({ - getStream - }), - emitEvent: ({ eventName, payload }) => - getEventBus().emit({ - eventName, - payload - }), - getUser, - getServerInfo - }), - getUsers - }), - createStream: createStreamFactory({ db }), - createBranch: createBranchFactory({ db }), - emitEvent: getEventBus().emit - }) -}) -const getUserByEmail = legacyGetUserByEmailFactory({ db }) -const createPersonalAccessToken = createPersonalAccessTokenFactory({ - storeApiToken: storeApiTokenFactory({ db }), - storeTokenScopes: storeTokenScopesFactory({ db }), - storeTokenResourceAccessDefinitions: storeTokenResourceAccessDefinitionsFactory({ - db - }), - storePersonalApiToken: storePersonalApiTokenFactory({ db }) -}) - -const main = async () => { - const testStream = { - name: 'Test Stream 01', - description: 'wonderful test stream' - } - - // const userA = { - // name: 'd1', - // email: 'd.1@speckle.systems', - // password: 'wowwow8charsplease' - // } - // userA.id = await createUser(userA) - - const userA = await getUserByEmail({ - email: 'd.1@speckle.systems' - }) - userA.token = `Bearer ${await createPersonalAccessToken( - userA.id, - 'test token user A', - [ - Scopes.Streams.Read, - Scopes.Streams.Write, - Scopes.Users.Read, - Scopes.Users.Email, - Scopes.Tokens.Write, - Scopes.Tokens.Read, - Scopes.Profile.Read, - Scopes.Profile.Email - ] - )}` - - testStream.id = await createStream({ ...testStream, ownerId: userA.id }) - - const { app } = await init() - - const numObjs = 5000 - const objBatch = createManyObjects(numObjs) - - const uploadRes = await request(app) - .post(`/objects/${testStream.id}`) - .set('Authorization', userA.token) - .set('Content-type', 'multipart/form-data') - .attach('batch1', Buffer.from(JSON.stringify(objBatch), 'utf8')) - - logger.info(uploadRes.status) - const objectIds = objBatch.map((obj) => obj.id) - - const res = await fetch(`http://127.0.0.1:3000/api/getobjects/${testStream.id}`, { - method: 'POST', - headers: { - Authorization: userA.token, - 'Content-Type': 'application/json', - Accept: 'text/plain' - }, - body: JSON.stringify({ objects: JSON.stringify(objectIds) }) - }) - const data = await res.body.getReader().read() - logger.info(data) - exit(0) -} - -main().then(logger.info('created')).catch(logger.error('failed')) diff --git a/packages/server/scripts/streamObjects.ts b/packages/server/scripts/streamObjects.ts new file mode 100644 index 000000000..ca15761a1 --- /dev/null +++ b/packages/server/scripts/streamObjects.ts @@ -0,0 +1,226 @@ +// eslint-disable-next-line no-restricted-imports +import '../bootstrap' +import { createManyObjects } from '@/test/helpers' +import { fetch } from 'undici' +import { init } from '@/app' +import request from 'supertest' +import { logger } from '@/observability/logging' +import { Scopes } from '@speckle/shared' +import { + getStreamFactory, + createStreamFactory, + grantStreamPermissionsFactory +} from '@/modules/core/repositories/streams' +import { db } from '@/db/knex' +import { + legacyCreateStreamFactory, + createStreamReturnRecordFactory +} from '@/modules/core/services/streams/management' +import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' +import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' +import { + deleteInvitesByTargetFactory, + deleteServerOnlyInvitesFactory, + findInviteFactory, + findUserByTargetFactory, + insertInviteAndDeleteOldFactory, + updateAllInviteTargetsFactory +} from '@/modules/serverinvites/repositories/serverInvites' +import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' +import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' +import { getEventBus } from '@/modules/shared/services/eventBus' +import { createBranchFactory } from '@/modules/core/repositories/branches' +import { + getUsersFactory, + getUserFactory, + legacyGetUserByEmailFactory +} from '@/modules/core/repositories/users' +import { createPersonalAccessTokenFactory } from '@/modules/core/services/tokens' +import { + storeApiTokenFactory, + storeTokenScopesFactory, + storeTokenResourceAccessDefinitionsFactory, + storePersonalApiTokenFactory +} from '@/modules/core/repositories/tokens' +import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' +import { + createUserEmailFactory, + ensureNoPrimaryEmailForUserFactory, + findEmailFactory +} from '@/modules/core/repositories/userEmails' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' + +const getServerInfo = getServerInfoFactory({ db }) +const getUsers = getUsersFactory({ db }) +const getUser = getUserFactory({ db }) +const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + +const createStream = legacyCreateStreamFactory({ + createStreamReturnRecord: createStreamReturnRecordFactory({ + inviteUsersToProject: inviteUsersToProjectFactory({ + createAndSendInvite: createAndSendInviteFactory({ + findUserByTarget: findUserByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + buildInviteEmailContents: buildCoreInviteEmailContentsFactory({ + getStream + }), + emitEvent: ({ eventName, payload }) => + getEventBus().emit({ + eventName, + payload + }), + getUser, + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() + }), + getUsers + }), + createStream: createStreamFactory({ db }), + createBranch: createBranchFactory({ db }), + emitEvent: getEventBus().emit + }) +}) +const getUserByEmail = legacyGetUserByEmailFactory({ db }) +const createPersonalAccessToken = createPersonalAccessTokenFactory({ + storeApiToken: storeApiTokenFactory({ db }), + storeTokenScopes: storeTokenScopesFactory({ db }), + storeTokenResourceAccessDefinitions: storeTokenResourceAccessDefinitionsFactory({ + db + }), + storePersonalApiToken: storePersonalApiTokenFactory({ db }) +}) + +const main = async () => { + const testStream = { + name: 'Test Stream 01', + description: 'wonderful test stream', + id: '' + } + + // const userA = { + // name: 'd1', + // email: 'd.1@speckle.systems', + // password: 'wowwow8charsplease' + // } + // userA.id = await createUser(userA) + + const userA = { + ...(await getUserByEmail({ + email: 'd.1@speckle.systems' + }))!, + token: '' + } + + userA.token = `Bearer ${await createPersonalAccessToken( + userA.id, + 'test token user A', + [ + Scopes.Streams.Read, + Scopes.Streams.Write, + Scopes.Users.Read, + Scopes.Users.Email, + Scopes.Tokens.Write, + Scopes.Tokens.Read, + Scopes.Profile.Read, + Scopes.Profile.Email + ] + )}` + + testStream.id = await createStream({ ...testStream, ownerId: userA.id }) + + const { app } = await init() + + const numObjs = 5000 + const objBatch = createManyObjects(numObjs) + + const uploadRes = await request(app) + .post(`/objects/${testStream.id}`) + .set('Authorization', userA.token) + .set('Content-type', 'multipart/form-data') + .attach('batch1', Buffer.from(JSON.stringify(objBatch), 'utf8')) + + logger.info(uploadRes.status) + const objectIds = objBatch.map((obj) => obj.id) + + const res = await fetch(`http://127.0.0.1:3000/api/getobjects/${testStream.id}`, { + method: 'POST', + headers: { + Authorization: userA.token, + 'Content-Type': 'application/json', + Accept: 'text/plain' + }, + body: JSON.stringify({ objects: JSON.stringify(objectIds) }) + }) + const data = await res.body!.getReader().read() + logger.info(data) + process.exit(0) +} + +main() + .then(() => logger.info('created')) + .catch((err) => logger.error('failed', err)) diff --git a/packages/server/test/speckle-helpers/inviteHelper.ts b/packages/server/test/speckle-helpers/inviteHelper.ts index f618daf49..07115600a 100644 --- a/packages/server/test/speckle-helpers/inviteHelper.ts +++ b/packages/server/test/speckle-helpers/inviteHelper.ts @@ -1,11 +1,14 @@ import { MaybeAsync, Roles, StreamRoles } from '@speckle/shared' import { buildUserTarget } from '@/modules/serverinvites/helpers/core' -import { InviteResult } from '@/modules/serverinvites/services/operations' import { + deleteInvitesByTargetFactory, + deleteServerOnlyInvitesFactory, findInviteByTokenFactory, + findInviteFactory, findUserByTargetFactory, - insertInviteAndDeleteOldFactory + insertInviteAndDeleteOldFactory, + updateAllInviteTargetsFactory } from '@/modules/serverinvites/repositories/serverInvites' import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' import { BasicTestUser } from '@/test/authHelper' @@ -17,21 +20,93 @@ import { ProjectInviteResourceType, ServerInviteResourceType } from '@/modules/serverinvites/domain/constants' -import { SendEmailParams } from '@/modules/emails/services/sending' +import { sendEmail, SendEmailParams } from '@/modules/emails/services/sending' import { db } from '@/db/knex' import { expect } from 'chai' import { PrimaryInviteResourceTarget, + ServerInviteRecord, ServerInviteResourceTarget } from '@/modules/serverinvites/domain/types' import { EmailSendingServiceMock } from '@/test/mocks/global' -import { getStreamFactory } from '@/modules/core/repositories/streams' +import { + getStreamFactory, + grantStreamPermissionsFactory +} from '@/modules/core/repositories/streams' import { getUserFactory } from '@/modules/core/repositories/users' import { getServerInfoFactory } from '@/modules/core/repositories/server' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' +import { + addOrUpdateStreamCollaboratorFactory, + validateStreamAccessFactory +} from '@/modules/core/services/streams/access' +import { authorizeResolver } from '@/modules/shared' +import { + createUserEmailFactory, + ensureNoPrimaryEmailForUserFactory, + findEmailFactory +} from '@/modules/core/repositories/userEmails' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createAndSendInvite = createAndSendInviteFactory({ findUserByTarget: findUserByTargetFactory({ db }), insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), @@ -47,7 +122,8 @@ const createAndSendInvite = createAndSendInviteFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }) export const createServerInviteDirectly = async ( @@ -89,7 +165,7 @@ export const createStreamInviteDirectly = async ( role?: StreamRoles }, creatorId: string -): Promise => { +): Promise => { const userId = invite.userId || invite.user?.id || null const email = invite.email || null if (!userId && !email) throw new Error('Either user/userId or email must be set') @@ -100,19 +176,24 @@ export const createStreamInviteDirectly = async ( const target = email || buildUserTarget(userId!) if (!target) throw new Error('Cannot create invite without a target') - return await createAndSendInvite( - { - target, - inviterId: creatorId, - message: invite.message, - primaryResourceTarget: { - resourceType: streamId ? ProjectInviteResourceType : ServerInviteResourceType, - resourceId: streamId || '', - role: streamId ? streamRole : Roles.Server.User, - primary: true - } - }, - null + return await captureCreatedInvite( + async () => + await createAndSendInvite( + { + target, + inviterId: creatorId, + message: invite.message, + primaryResourceTarget: { + resourceType: streamId + ? ProjectInviteResourceType + : ServerInviteResourceType, + resourceId: streamId || '', + role: streamId ? streamRole : Roles.Server.User, + primary: true + } + }, + null + ) ) } diff --git a/packages/server/test/speckle-helpers/streamHelper.ts b/packages/server/test/speckle-helpers/streamHelper.ts index b42d87e90..2af82afe4 100644 --- a/packages/server/test/speckle-helpers/streamHelper.ts +++ b/packages/server/test/speckle-helpers/streamHelper.ts @@ -10,6 +10,11 @@ import { grantStreamPermissionsFactory, revokeStreamPermissionsFactory } from '@/modules/core/repositories/streams' +import { + createUserEmailFactory, + ensureNoPrimaryEmailForUserFactory, + findEmailFactory +} from '@/modules/core/repositories/userEmails' import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users' import { addOrUpdateStreamCollaboratorFactory, @@ -21,13 +26,30 @@ import { createStreamReturnRecordFactory, legacyCreateStreamFactory } from '@/modules/core/services/streams/management' +import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' +import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' +import { renderEmail } from '@/modules/emails/services/emailRendering' +import { sendEmail } from '@/modules/emails/services/sending' +import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' import { + deleteInvitesByTargetFactory, + deleteServerOnlyInvitesFactory, + findInviteFactory, findUserByTargetFactory, - insertInviteAndDeleteOldFactory + insertInviteAndDeleteOldFactory, + updateAllInviteTargetsFactory } from '@/modules/serverinvites/repositories/serverInvites' import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' +import { + processFinalizedProjectInviteFactory, + validateProjectInviteBeforeFinalizationFactory +} from '@/modules/serverinvites/services/coreFinalization' import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection' import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation' +import { + finalizeInvitedServerRegistrationFactory, + finalizeResourceInviteFactory +} from '@/modules/serverinvites/services/processing' import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement' import { authorizeResolver } from '@/modules/shared' import { Nullable } from '@/modules/shared/helpers/typeHelper' @@ -44,6 +66,52 @@ const getServerInfo = getServerInfoFactory({ db }) const getUsers = getUsersFactory({ db }) const getUser = getUserFactory({ db }) const getStream = getStreamFactory({ db }) + +const buildFinalizeProjectInvite = () => + finalizeResourceInviteFactory({ + findInvite: findInviteFactory({ db }), + validateInvite: validateProjectInviteBeforeFinalizationFactory({ + getProject: getStream + }), + processInvite: processFinalizedProjectInviteFactory({ + getProject: getStream, + addProjectRole: addOrUpdateStreamCollaboratorFactory({ + validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }), + getUser, + grantStreamPermissions: grantStreamPermissionsFactory({ db }), + emitEvent: getEventBus().emit + }) + }), + deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }), + insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), + emitEvent: (...args) => getEventBus().emit(...args), + findEmail: findEmailFactory({ db }), + validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ + createUserEmail: createUserEmailFactory({ db }), + ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), + findEmail: findEmailFactory({ db }), + updateEmailInvites: finalizeInvitedServerRegistrationFactory({ + deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), + updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) + }), + requestNewEmailVerification: requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo, + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail + }) + }), + collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({ + getStream + }), + getUser, + getServerInfo + }) + const createStream = legacyCreateStreamFactory({ createStreamReturnRecord: createStreamReturnRecordFactory({ inviteUsersToProject: inviteUsersToProjectFactory({ @@ -62,7 +130,8 @@ const createStream = legacyCreateStreamFactory({ payload }), getUser, - getServerInfo + getServerInfo, + finalizeInvite: buildFinalizeProjectInvite() }), getUsers }),