Files
speckle-server/packages/server/modules/serverinvites/services/inviteProcessingService.ts
T
Kristaps Fabians Geikins 6bfffca0a9 fix: various invite flow fixes & improvements (#2451)
* fix(fe2): show signup CTA for non-registered invitees [web-1144]

* fix: server invite signup error

* WIP project error access block

* feat(fe2): better 'no project access' error screens

* CR fix
2024-06-28 17:26:11 +02:00

218 lines
5.9 KiB
TypeScript

import { Roles } from '@/modules/core/helpers/mainConstants'
import { getStreamRoute } from '@/modules/core/helpers/routeHelper'
import { NoInviteFoundError } from '@/modules/serverinvites/errors'
import {
isStreamInvite,
buildUserTarget,
ResourceTargets
} from '@/modules/serverinvites/helpers/inviteHelper'
import { addOrUpdateStreamCollaborator } from '@/modules/core/services/streams/streamAccessService'
import { addStreamInviteDeclinedActivity } from '@/modules/activitystream/services/streamActivity'
import { getFrontendOrigin, useNewFrontend } from '@/modules/shared/helpers/envHelper'
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
import {
DeleteInvite,
DeleteInvitesByTarget,
DeleteServerOnlyInvites,
DeleteStreamInvite,
FindInvite,
FindServerInvite,
FindStreamInvite,
UpdateAllInviteTargets
} from '@/modules/serverinvites/domain/operations'
import {
FinalizeStreamInvite,
ResendInviteEmail
} from '@/modules/serverinvites/services/operations'
/**
* Resolve the relative auth redirect path, after registering with an invite
* Note: Important auth query string params like the access_code are added separately
* in auth middlewares
*/
export const resolveAuthRedirectPathFactory = () => (invite?: ServerInviteRecord) => {
if (useNewFrontend()) {
// All post-auth redirects are handled by the frontend itself
return getFrontendOrigin()
}
if (invite) {
const { resourceId } = invite
if (isStreamInvite(invite)) {
// TODO: check nullability
return `${getStreamRoute(resourceId!)}`
}
}
// Fall-back to base URL (for server invites)
return getFrontendOrigin()
}
/**
* Validate that the new user has a valid invite for registering to the server
*/
export const validateServerInviteFactory =
({ findServerInvite }: { findServerInvite: FindServerInvite }) =>
async (email: string, token: string): Promise<ServerInviteRecord> => {
const invite = await findServerInvite(email, token)
if (!invite) {
throw new NoInviteFoundError(
token
? "Wrong e-mail address or invite token. Make sure you're using the same e-mail address that received the invite."
: "Wrong e-mail address. Make sure you're using the same e-mail address that received the invite.",
{
info: {
email,
token
}
}
)
}
return invite
}
/**
* Finalize server registration by deleting unnecessary invites and updating
* the remaining ones
*/
export const finalizeInvitedServerRegistrationFactory =
({
deleteServerOnlyInvites,
updateAllInviteTargets
}: {
deleteServerOnlyInvites: DeleteServerOnlyInvites
updateAllInviteTargets: UpdateAllInviteTargets
}) =>
async (email: string, userId: string) => {
// Delete all server-only invites for this email
await deleteServerOnlyInvites(email)
// Update all remaining invites to use a userId target, not the e-mail
// (in case the user changes his e-mail right after)
await updateAllInviteTargets(email, buildUserTarget(userId)!)
}
/**
* Accept or decline a stream invite
*/
export const finalizeStreamInviteFactory =
({
findStreamInvite,
deleteInvitesByTarget
}: {
findStreamInvite: FindStreamInvite
deleteInvitesByTarget: DeleteInvitesByTarget
}): FinalizeStreamInvite =>
async (accept, streamId, token, userId) => {
const invite = await findStreamInvite(streamId, {
token,
target: buildUserTarget(userId)
})
if (!invite) {
throw new NoInviteFoundError('Attempted to finalize nonexistant stream invite', {
info: {
streamId,
token,
userId
}
})
}
// Invite found - accept or decline
if (accept) {
// Add access for user
const { role = Roles.Stream.Contributor, inviterId } = invite
// TODO: check role nullability
await addOrUpdateStreamCollaborator(streamId, userId, role!, inviterId, null, {
fromInvite: true
})
// Delete all invites to this stream
await deleteInvitesByTarget(
buildUserTarget(userId)!,
ResourceTargets.Streams,
streamId
)
} else {
await addStreamInviteDeclinedActivity({
streamId,
inviteTargetId: userId,
inviterId: invite.inviterId
})
}
// Delete all invites to this stream
await deleteInvitesByTarget(
buildUserTarget(userId)!,
ResourceTargets.Streams,
streamId
)
}
/**
* Cancel/decline a stream invite
*/
export const cancelStreamInviteFactory =
({
findStreamInvite,
deleteStreamInvite
}: {
findStreamInvite: FindStreamInvite
deleteStreamInvite: DeleteStreamInvite
}) =>
async (streamId: string, inviteId: string) => {
const invite = await findStreamInvite(streamId, {
inviteId
})
if (!invite) {
throw new NoInviteFoundError('Attempted to process nonexistant stream invite', {
info: {
streamId,
inviteId
}
})
}
await deleteStreamInvite(invite.id)
}
/**
* Re-send pending invite e-mail, without creating a new invite
*/
export const resendInviteFactory =
({
findInvite,
resendInviteEmail
}: {
resendInviteEmail: ResendInviteEmail
findInvite: FindInvite
}) =>
async (inviteId: string) => {
const invite = await findInvite(inviteId)
if (!invite) {
throw new NoInviteFoundError('Attempted to re-send a nonexistant invite')
}
await resendInviteEmail(invite)
}
/**
* Delete pending invite
*/
export const deleteInviteFactory =
({
findInvite,
deleteInvite
}: {
findInvite: FindInvite
deleteInvite: DeleteInvite
}) =>
async (inviteId: string) => {
const invite = await findInvite(inviteId)
if (!invite) {
throw new NoInviteFoundError('Attempted to delete a nonexistant invite')
}
await deleteInvite(invite.id)
}