const crs = require('crypto-random-string')
const { getServerInfo } = require('@/modules/core/services/generic')
const { sendEmail } = require('@/modules/emails')
const { InviteCreateValidationError } = require('@/modules/serverinvites/errors')
const { authorizeResolver } = require('@/modules/shared')
const {
insertInviteAndDeleteOld,
getUserFromTarget,
getResource
} = require('@/modules/serverinvites/repositories')
const { getStreamCollaborator } = require('@/modules/core/repositories/streams')
const { Roles } = require('@/modules/core/helpers/mainConstants')
const sanitizeHtml = require('sanitize-html')
const {
getRegistrationRoute,
getStreamRoute
} = require('@/modules/core/helpers/routeHelper')
const {
isServerInvite,
resolveTarget,
buildUserTarget,
ResourceTargets
} = require('@/modules/serverinvites/helpers/inviteHelper')
const { getUsers, getUser } = require('@/modules/core/repositories/users')
const {
addStreamInviteSentOutActivity
} = require('@/modules/activitystream/services/streamActivityService')
/**
* @typedef {{
* target: string;
* inviterId: string;
* message?: string;
* resourceTarget?: string;
* resourceId?: string;
* role?: string;
* }} CreateInviteParams
*/
/**
* @typedef {CreateInviteParams|import('@/modules/serverinvites/repositories').ServerInviteRecord} InviteOrInputParams
*/
/**
* @param {InviteOrInputParams} params
* @param {Object | null} resource invite resource (e.g. stream)
*/
function resolveResourceName(params, resource) {
const { resourceTarget } = params
if (resourceTarget === ResourceTargets.Streams) {
return resource.name
}
return null
}
/**
* Validate that the inviter has access to the resources he's trying to invite people to
* @param {CreateInviteParams} params
* @param {import('@/modules/core/helpers/userHelper').UserRecord} inviter
*/
async function validateInviter(params, inviter) {
const { resourceId, resourceTarget } = params
if (!inviter) throw new InviteCreateValidationError('Invalid inviter')
if (isServerInvite(params)) return
try {
if (resourceTarget === ResourceTargets.Streams) {
await authorizeResolver(inviter.id, resourceId, Roles.Stream.Owner)
} else {
throw new InviteCreateValidationError('Unexpected resource target type')
}
} catch (e) {
throw new InviteCreateValidationError(
"Inviter doesn't have proper access to the resource",
{ cause: e }
)
}
}
/**
* Validate the target
* @param {CreateInviteParams} params
* @param {import('@/modules/core/helpers/userHelper').UserRecord | undefined} targetUser
*/
function validateTargetUser(params, targetUser) {
const { target } = params
const { userId } = resolveTarget(target)
if (userId && !targetUser) {
throw new InviteCreateValidationError('Attempting to invite an invalid user')
}
if (isServerInvite(params) && targetUser) {
throw new InviteCreateValidationError(
'This email is already associated with an account on this server'
)
}
}
/**
* Validate the target resource
* @param {CreateInviteParams} params
* @param {Object | null} resource
* @param {import('@/modules/core/helpers/userHelper').UserRecord | undefined} targetUser Target user, if one exists in our DB
*/
async function validateResource(params, resource, targetUser) {
const { resourceId, resourceTarget, role } = params
if (resourceId && !resource) {
throw new InviteCreateValidationError("Couldn't resolve invite resource")
}
if (resourceTarget === ResourceTargets.Streams) {
if (targetUser) {
// Check if user isn't already associated with the stream
const isStreamCollaborator = !!(await getStreamCollaborator(
resourceId,
targetUser.id
))
if (isStreamCollaborator) {
throw new InviteCreateValidationError(
'The target user is already a collaborator of the specified stream'
)
}
}
if (!Object.values(Roles.Stream).includes(role)) {
throw new InviteCreateValidationError('Unexpected stream invite role')
}
}
}
/**
* Validate invite creation input data
* @param {CreateInviteParams} params
* @param {import('@/modules/core/helpers/userHelper').UserRecord} inviter Inviter, resolved from DB
* @param {import('@/modules/core/helpers/userHelper').UserRecord | undefined} targetUser Target user, if one exists in our DB
* @param {Object | null} resource Invite resource (stream or null)
*/
async function validateInput(params, inviter, targetUser, resource) {
const { message } = params
// validate inviter & invitee
validateTargetUser(params, targetUser)
await validateInviter(params, inviter)
// validate resource
await validateResource(params, resource, targetUser)
// check if message too long
if (message) {
if (message.length >= 1024) {
throw new InviteCreateValidationError('Personal message too long')
}
}
}
/**
* Sanitize message that potentially has HTML in it
* @param {string} message
* @returns {string}
*/
function sanitizeMessage(message) {
return sanitizeHtml(message, {
allowedTags: ['b', 'i', 'em', 'strong']
})
}
/**
* Build email text version body
*/
function buildEmailTextBody(invite, inviter, serverInfo, inviteLink, resourceName) {
const { message } = invite
const forServer = isServerInvite(invite)
const dynamicText = forServer
? `join the ${serverInfo.name} Speckle Server (${process.env.CANONICAL_URL})`
: `become a collaborator on the ${serverInfo.name} Speckle Server (${process.env.CANONICAL_URL}) stream - "${resourceName}"`
return `
Hello!
${
inviter.name
} has just sent you this invitation to ${dynamicText}! To accept the invitation, open the following URL in your browser:
${inviteLink}
${message ? inviter.name + ' said: "' + message + '"' : ''}
Warm regards,
Speckle
---
This email was sent from ${serverInfo.name} at ${
process.env.CANONICAL_URL
}, deployed and managed by ${serverInfo.company}. Your admin contact is ${
serverInfo.adminContact ? serverInfo.adminContact : '[not provided]'
}.
`
}
/**
* Build email HTML version body
*/
function buildEmailHtmlBody(invite, inviter, serverInfo, inviteLink, resourceName) {
const { message } = invite
const forServer = isServerInvite(invite)
const dynamicText = forServer
? `join the ${serverInfo.name} Speckle Server`
: `become a collaborator on the ${serverInfo.name} Speckle Server stream - "${resourceName}"`
return `
Hello!
${inviter.name} has just sent you this invitation to ${dynamicText}!
To accept the invitation, click here!
${message ? inviter.name + ' said: "' + message + '"
' : ''}
Warm regards,
Speckle (on behalf of ${inviter.name})