Files
speckle-server/packages/server/modules/serverinvites/graph/resolvers/serverInvites.ts
T
Kristaps Fabians Geikins 4dae1569cd feat(fe2): invite + list workspace invites (#2629)
* list invites table

* invites list works

* update last reminded date on resend

* fix FE

* WIP invitedialog + updated debounced utility

* invite create works

* exclude users correctly

* more adjustments

* minor cleanup

* using workspace invite server role

* test fix

* fixed multiple root eslint issues

* minor adjustments
2024-08-12 11:30:01 +03:00

425 lines
14 KiB
TypeScript

import { Roles } from '@/modules/core/helpers/mainConstants'
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
import { InviteCreateValidationError } from '@/modules/serverinvites/errors'
import {
createAndSendInviteFactory,
resendInviteEmailFactory
} from '@/modules/serverinvites/services/creation'
import {
cancelResourceInviteFactory,
deleteInviteFactory,
finalizeResourceInviteFactory
} from '@/modules/serverinvites/services/processing'
import {
getInvitationTargetUsersFactory,
getServerInviteForTokenFactory
} from '@/modules/serverinvites/services/retrieval'
import { authorizeResolver } from '@/modules/shared'
import { chunk } from 'lodash'
import { Resolvers } from '@/modules/core/graph/generated/graphql'
import db from '@/db/knex'
import { ServerRoles } from '@speckle/shared'
import {
deleteInvitesByTargetFactory,
findInviteFactory,
findServerInviteFactory,
findUserByTargetFactory,
insertInviteAndDeleteOldFactory,
deleteInviteFactory as deleteInviteFromDbFactory,
queryAllUserResourceInvitesFactory,
queryAllResourceInvitesFactory,
markInviteUpdatedfactory
} from '@/modules/serverinvites/repositories/serverInvites'
import {
createProjectInviteFactory,
getPendingProjectCollaboratorsFactory,
getUserPendingProjectInviteFactory,
getUserPendingProjectInvitesFactory,
useProjectInviteAndNotifyFactory
} from '@/modules/serverinvites/services/projectInviteManagement'
import { getUser, getUsers } from '@/modules/core/repositories/users'
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import { getStream } from '@/modules/core/repositories/streams'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
PrimaryInviteResourceTarget,
ServerInviteResourceTarget
} from '@/modules/serverinvites/domain/types'
import {
ProjectInviteResourceType,
ServerInviteResourceType
} from '@/modules/serverinvites/domain/constants'
import {
processFinalizedProjectInviteFactory,
validateProjectInviteBeforeFinalizationFactory
} from '@/modules/serverinvites/services/coreFinalization'
import { addStreamInviteDeclinedActivity } from '@/modules/activitystream/services/streamActivity'
import { addOrUpdateStreamCollaborator } from '@/modules/core/services/streams/streamAccessService'
const buildCreateAndSendServerOrProjectInvite = () =>
createAndSendInviteFactory({
findUserByTarget: findUserByTargetFactory(),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
getStream
}),
emitEvent: ({ eventName, payload }) =>
getEventBus().emit({
eventName,
payload
})
})
export = {
Query: {
async streamInvite(_parent, args, context) {
const { streamId, token } = args
return getUserPendingProjectInviteFactory({
getUser,
findInvite: findInviteFactory({ db })
})(streamId, context.userId, token)
},
async projectInvite(_parent, args, context) {
const { projectId, token } = args
return getUserPendingProjectInviteFactory({
getUser,
findInvite: findInviteFactory({ db })
})(projectId, context.userId, token)
},
async streamInvites(_parent, _args, context) {
const { userId } = context
return getUserPendingProjectInvitesFactory({
getUser,
getUserResourceInvites: queryAllUserResourceInvitesFactory({ db })
})(userId!)
},
async serverInviteByToken(_parent, args) {
const { token } = args
return getServerInviteForTokenFactory({
findServerInvite: findServerInviteFactory({ db })
})(token)
}
},
User: {
async projectInvites(_parent, _args, context) {
const { userId } = context
const getUserPendingProjectInvites = getUserPendingProjectInvitesFactory({
getUser,
getUserResourceInvites: queryAllUserResourceInvitesFactory({ db })
})
return await getUserPendingProjectInvites(userId!)
}
},
Project: {
async invitedTeam(parent) {
const getPendingTeam = getPendingProjectCollaboratorsFactory({
queryAllResourceInvites: queryAllResourceInvitesFactory({ db }),
getInvitationTargetUsers: getInvitationTargetUsersFactory({ getUsers })
})
return await getPendingTeam(parent.id)
}
},
ServerInvite: {
async invitedBy(parent, _args, ctx) {
const { invitedById } = parent
if (!invitedById) return null
const user = await ctx.loaders.users.getUser.load(invitedById)
return user ? removePrivateFields(user) : null
}
},
Mutation: {
async serverInviteCreate(_parent, args, context) {
const createAndSendInvite = buildCreateAndSendServerOrProjectInvite()
const primaryResourceTarget: PrimaryInviteResourceTarget<ServerInviteResourceTarget> =
{
resourceId: '',
role: (args.input.serverRole as ServerRoles) || Roles.Server.User,
resourceType: ServerInviteResourceType,
primary: true
}
await createAndSendInvite(
{
target: args.input.email,
inviterId: context.userId!,
message: args.input.message,
primaryResourceTarget
},
context.resourceAccessRules
)
return true
},
async streamInviteCreate(_parent, args, context) {
const createProjectInvite = createProjectInviteFactory({
createAndSendInvite: buildCreateAndSendServerOrProjectInvite()
})
await createProjectInvite({
input: args.input,
inviterId: context.userId!,
inviterResourceAccessRules: context.resourceAccessRules
})
return true
},
async serverInviteBatchCreate(_parent, args, context) {
const { input: paramsArray } = args
const inviteCount = paramsArray.length
if (inviteCount > 10 && context.role !== Roles.Server.Admin) {
throw new InviteCreateValidationError(
'Maximum 10 invites can be sent at once by non admins'
)
}
const createAndSendInvite = buildCreateAndSendServerOrProjectInvite()
// Batch calls so that we don't kill the server
const batches = chunk(paramsArray, 50)
for (const paramsBatchArray of batches) {
await Promise.all(
paramsBatchArray.map((params) => {
const primaryResourceTarget: PrimaryInviteResourceTarget<ServerInviteResourceTarget> =
{
resourceId: '',
role: (params.serverRole as ServerRoles) || Roles.Server.User,
resourceType: ServerInviteResourceType,
primary: true
}
return createAndSendInvite(
{
target: params.email,
inviterId: context.userId!,
message: params.message,
primaryResourceTarget
},
context.resourceAccessRules
)
})
)
}
return true
},
async streamInviteBatchCreate(_parent, args, context) {
const { input: paramsArray } = args
// Validate params
for (const params of paramsArray) {
const { email, userId } = params
if (!email && !userId) {
throw new InviteCreateValidationError(
'Either email or userId must be specified'
)
}
}
const createProjectInvite = createProjectInviteFactory({
createAndSendInvite: buildCreateAndSendServerOrProjectInvite()
})
// Batch calls so that we don't kill the server
const batches = chunk(paramsArray, 50)
for (const paramsBatchArray of batches) {
await Promise.all(
paramsBatchArray.map((params) => {
return createProjectInvite({
input: params,
inviterId: context.userId!,
inviterResourceAccessRules: context.resourceAccessRules
})
})
)
}
return true
},
async streamInviteUse(_parent, args, ctx) {
const useProjectInvite = useProjectInviteAndNotifyFactory({
finalizeInvite: finalizeResourceInviteFactory({
findInvite: findInviteFactory({ db }),
validateInvite: validateProjectInviteBeforeFinalizationFactory({
getProject: getStream
}),
processInvite: processFinalizedProjectInviteFactory({
getProject: getStream,
addInviteDeclinedActivity: addStreamInviteDeclinedActivity,
addProjectRole: addOrUpdateStreamCollaborator
}),
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
emitEvent: (...args) => getEventBus().emit(...args)
})
})
await useProjectInvite(args, ctx.userId!, ctx.resourceAccessRules)
return true
},
async streamInviteCancel(_parent, args, ctx) {
const { streamId, inviteId } = args
const { userId, resourceAccessRules } = ctx
const cancelInvite = cancelResourceInviteFactory({
findInvite: findInviteFactory({ db }),
deleteInvite: deleteInviteFromDbFactory({ db }),
validateResourceAccess: validateProjectInviteBeforeFinalizationFactory({
getProject: getStream
})
})
await authorizeResolver(userId, streamId, Roles.Stream.Owner, resourceAccessRules)
await cancelInvite({
inviteId,
resourceId: streamId,
resourceType: ProjectInviteResourceType,
cancelerId: userId!,
cancelerResourceAccessLimits: resourceAccessRules
})
return true
},
async inviteResend(_parent, args) {
const { inviteId } = args
const resendInviteEmail = resendInviteEmailFactory({
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
getStream
}),
findUserByTarget: findUserByTargetFactory(),
findInvite: findInviteFactory({ db }),
markInviteUpdated: markInviteUpdatedfactory({ db })
})
await resendInviteEmail({ inviteId })
return true
},
async inviteDelete(_parent, args) {
const { inviteId } = args
await deleteInviteFactory({
findInvite: findInviteFactory({ db }),
deleteInvite: deleteInviteFromDbFactory({ db })
})(inviteId)
return true
}
},
ProjectInviteMutations: {
async create(_parent, args, ctx) {
const createProjectInvite = createProjectInviteFactory({
createAndSendInvite: buildCreateAndSendServerOrProjectInvite()
})
await createProjectInvite({
input: {
projectId: args.projectId,
...args.input
},
inviterId: ctx.userId!,
inviterResourceAccessRules: ctx.resourceAccessRules
})
return ctx.loaders.streams.getStream.load(args.projectId)
},
async batchCreate(_parent, args, ctx) {
await authorizeResolver(
ctx.userId,
args.projectId,
Roles.Stream.Owner,
ctx.resourceAccessRules
)
const inviteCount = args.input.length
if (inviteCount > 10 && ctx.role !== Roles.Server.Admin) {
throw new InviteCreateValidationError(
'Maximum 10 invites can be sent at once by non admins'
)
}
const createProjectInvite = createProjectInviteFactory({
createAndSendInvite: buildCreateAndSendServerOrProjectInvite()
})
const inputBatches = chunk(args.input, 10)
for (const batch of inputBatches) {
await Promise.all(
batch.map((i) =>
createProjectInvite({
input: {
...i,
projectId: args.projectId
},
inviterId: ctx.userId!,
inviterResourceAccessRules: ctx.resourceAccessRules
})
)
)
}
return ctx.loaders.streams.getStream.load(args.projectId)
},
async use(_parent, args, ctx) {
const useProjectInvite = useProjectInviteAndNotifyFactory({
finalizeInvite: finalizeResourceInviteFactory({
findInvite: findInviteFactory({ db }),
validateInvite: validateProjectInviteBeforeFinalizationFactory({
getProject: getStream
}),
processInvite: processFinalizedProjectInviteFactory({
getProject: getStream,
addInviteDeclinedActivity: addStreamInviteDeclinedActivity,
addProjectRole: addOrUpdateStreamCollaborator
}),
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
emitEvent: (...args) => getEventBus().emit(...args)
})
})
await useProjectInvite(args.input, ctx.userId!, ctx.resourceAccessRules)
return true
},
async cancel(_parent, args, ctx) {
await authorizeResolver(
ctx.userId,
args.projectId,
Roles.Stream.Owner,
ctx.resourceAccessRules
)
const cancelInvite = cancelResourceInviteFactory({
findInvite: findInviteFactory({ db }),
deleteInvite: deleteInviteFromDbFactory({ db }),
validateResourceAccess: validateProjectInviteBeforeFinalizationFactory({
getProject: getStream
})
})
await cancelInvite({
resourceId: args.projectId,
inviteId: args.inviteId,
cancelerId: ctx.userId!,
resourceType: ProjectInviteResourceType,
cancelerResourceAccessLimits: ctx.resourceAccessRules
})
return ctx.loaders.streams.getStream.load(args.projectId)
}
}
} as Resolvers