Files
speckle-server/packages/server/modules/notifications/services/handlers/newStreamAccessRequest.ts
T
Kristaps Fabians Geikins b406d0e32d chore(server): node16 & export maps support for server (#4698)
* chore(server): node16 & export maps support for server

* moar cleanup

* lint fixc
2025-05-12 14:27:34 +03:00

163 lines
5.0 KiB
TypeScript

import {
AccessRequestType,
getPendingAccessRequestFactory
} from '@/modules/accessrequests/repositories'
import {
NewStreamAccessRequestMessage,
NotificationHandler
} from '@/modules/notifications/helpers/types'
import { NotificationValidationError } from '@/modules/notifications/errors'
import { Roles } from '@/modules/core/helpers/mainConstants'
import {
buildAbsoluteFrontendUrlFromPath,
getStreamCollaboratorsRoute
} from '@/modules/core/helpers/routeHelper'
import { sendEmail } from '@/modules/emails/services/sending'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { db } from '@/db/knex'
import { GetPendingAccessRequest } from '@/modules/accessrequests/domain/operations'
import { GetStream } from '@/modules/core/domain/streams/operations'
import { getStreamFactory } from '@/modules/core/repositories/streams'
import { GetUser } from '@/modules/core/domain/users/operations'
import { getUserFactory } from '@/modules/core/repositories/users'
import { GetServerInfo } from '@/modules/core/domain/server/operations'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { EmailTemplateParams } from '@/modules/emails/domain/operations'
type ValidateMessageDeps = {
getPendingAccessRequest: GetPendingAccessRequest
getUser: GetUser
getStream: GetStream
}
const validateMessageFactory =
(deps: ValidateMessageDeps) => async (msg: NewStreamAccessRequestMessage) => {
const {
targetUserId,
data: { requestId }
} = msg
const [request, user] = await Promise.all([
deps.getPendingAccessRequest(requestId, AccessRequestType.Stream),
deps.getUser(targetUserId)
])
if (!request)
throw new NotificationValidationError('Nonexistant stream access request')
if (!user) throw new NotificationValidationError('Nonexistant user')
const [streamWithRole, requester] = await Promise.all([
deps.getStream({
streamId: request.resourceId,
userId: targetUserId
}),
deps.getUser(request.requesterId)
])
if (!streamWithRole) throw new NotificationValidationError('Nonexistant stream')
if (streamWithRole.role !== Roles.Stream.Owner)
throw new NotificationValidationError(
'Only stream owners can receive notifications about stream access requests'
)
if (!requester)
throw new NotificationValidationError(
'User who made the request no longer exists'
)
return {
request,
stream: streamWithRole,
targetUser: user,
requester
}
}
type ValidatedMessageState = Awaited<
ReturnType<ReturnType<typeof validateMessageFactory>>
>
function buildEmailTemplateHtml(
state: ValidatedMessageState
): EmailTemplateParams['mjml'] {
const { requester, stream } = state
return {
bodyStart: `<mj-text align="center" line-height="2" >
Hello,<br/>
<br/>
<b>${requester.name}</b> requested access to the <b>${stream.name}</b> project.
You can add them as a collaborator by clicking the button below.
</mj-text>
`,
bodyEnd: `<mj-text align="center" padding-bottom="0px" line-height="2">
You received this email because you are an owner on <b>${stream.name}</b>.
</mj-text>`
}
}
function buildEmailTemplateText(
state: ValidatedMessageState
): EmailTemplateParams['text'] {
const { requester, stream } = state
return {
bodyStart: `Hello,\n\n${requester.name} requested access to the ${stream.name} project. You can add them as a collaborator by opening the link below.`,
bodyEnd: `You received this email because you are an owner on ${stream.name}`
}
}
function buildEmailTemplateParams(state: ValidatedMessageState): EmailTemplateParams {
const { stream } = state
return {
mjml: buildEmailTemplateHtml(state),
text: buildEmailTemplateText(state),
cta: {
title: 'Review Request',
url: buildAbsoluteFrontendUrlFromPath(getStreamCollaboratorsRoute(stream.id))
}
}
}
const newStreamAccessRequestHandlerFactory =
(
deps: {
getServerInfo: GetServerInfo
renderEmail: typeof renderEmail
sendEmail: typeof sendEmail
} & ValidateMessageDeps
): NotificationHandler<NewStreamAccessRequestMessage> =>
async (msg) => {
const state = await validateMessageFactory(deps)(msg)
const htmlTemplateParams = buildEmailTemplateParams(state)
const serverInfo = await deps.getServerInfo()
const { html, text } = await deps.renderEmail(
htmlTemplateParams,
serverInfo,
state.targetUser
)
await deps.sendEmail({
to: state.targetUser.email,
text,
html,
subject: 'A user requested access to your project'
})
}
export const handler: NotificationHandler<NewStreamAccessRequestMessage> = (
...args
) => {
const newStreamAccessRequestHandler = newStreamAccessRequestHandlerFactory({
getServerInfo: getServerInfoFactory({ db }),
renderEmail,
sendEmail,
getUser: getUserFactory({ db }),
getStream: getStreamFactory({ db }),
getPendingAccessRequest: getPendingAccessRequestFactory({ db })
})
return newStreamAccessRequestHandler(...args)
}
export default handler