Files
speckle-server/packages/server/modules/workspaces/services/workspaceJoinRequests.ts
T
Kristaps Fabians Geikins d903e8ffc4 feat(server): support editor -> viewer seat downgrades (#4181)
* new seat based project role checks implemented

* everything done

* minor bugfix
2025-03-14 14:21:25 +02:00

219 lines
6.3 KiB
TypeScript

import {
WorkspaceNotDiscoverableError,
WorkspaceNotFoundError,
WorkspaceNotJoinableError,
WorkspaceProtectedError
} from '@/modules/workspaces/errors/workspace'
import { GetUser } from '@/modules/core/domain/users/operations'
import { NotFoundError } from '@/modules/shared/errors'
import {
ApproveWorkspaceJoinRequest,
CreateWorkspaceJoinRequest,
DenyWorkspaceJoinRequest,
EnsureValidWorkspaceRoleSeat,
GetWorkspace,
GetWorkspaceJoinRequest,
GetWorkspaceWithDomains,
SendWorkspaceJoinRequestApprovedEmail,
SendWorkspaceJoinRequestDeniedEmail,
SendWorkspaceJoinRequestReceivedEmail,
UpdateWorkspaceJoinRequestStatus,
UpsertWorkspaceRole
} from '@/modules/workspaces/domain/operations'
import { Roles } from '@speckle/shared'
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic'
import { EventBus } from '@/modules/shared/services/eventBus'
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
export const dismissWorkspaceJoinRequestFactory =
({
getWorkspace,
updateWorkspaceJoinRequestStatus
}: {
getWorkspace: GetWorkspace
updateWorkspaceJoinRequestStatus: UpdateWorkspaceJoinRequestStatus
}) =>
async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => {
const workspace = await getWorkspace({ workspaceId })
if (!workspace) {
throw new WorkspaceNotFoundError()
}
await updateWorkspaceJoinRequestStatus({
userId,
workspaceId,
status: 'dismissed'
})
return true
}
export const requestToJoinWorkspaceFactory =
({
createWorkspaceJoinRequest,
sendWorkspaceJoinRequestReceivedEmail,
getUserById,
getWorkspaceWithDomains,
getUserEmails
}: {
createWorkspaceJoinRequest: CreateWorkspaceJoinRequest
sendWorkspaceJoinRequestReceivedEmail: SendWorkspaceJoinRequestReceivedEmail
getUserById: GetUser
getWorkspaceWithDomains: GetWorkspaceWithDomains
getUserEmails: FindEmailsByUserId
}) =>
async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => {
const requester = await getUserById(userId)
if (!requester) {
throw new NotFoundError('User not found')
}
const workspace = await getWorkspaceWithDomains({ id: workspaceId })
if (!workspace) {
throw new WorkspaceNotFoundError('Workspace not found')
}
if (!workspace?.discoverabilityEnabled) throw new WorkspaceNotDiscoverableError()
const workspaceDomains = workspace.domains.filter((domain) => domain.verified)
if (!workspaceDomains.length) throw new WorkspaceNotJoinableError()
const userEmails = await getUserEmails({ userId })
const canJoinWorkspace = userEmailsCompliantWithWorkspaceDomains({
workspaceDomains: workspace.domains,
userEmails
})
if (!canJoinWorkspace) {
throw new WorkspaceProtectedError()
}
await createWorkspaceJoinRequest({
workspaceJoinRequest: {
userId,
workspaceId,
status: 'pending'
}
})
await sendWorkspaceJoinRequestReceivedEmail({
workspace,
requester
})
return true
}
export const approveWorkspaceJoinRequestFactory =
({
updateWorkspaceJoinRequestStatus,
sendWorkspaceJoinRequestApprovedEmail,
getUserById,
getWorkspace,
getWorkspaceJoinRequest,
upsertWorkspaceRole,
emit,
ensureValidWorkspaceRoleSeat
}: {
updateWorkspaceJoinRequestStatus: UpdateWorkspaceJoinRequestStatus
sendWorkspaceJoinRequestApprovedEmail: SendWorkspaceJoinRequestApprovedEmail
getUserById: GetUser
getWorkspace: GetWorkspace
getWorkspaceJoinRequest: GetWorkspaceJoinRequest
upsertWorkspaceRole: UpsertWorkspaceRole
emit: EventBus['emit']
ensureValidWorkspaceRoleSeat: EnsureValidWorkspaceRoleSeat
}): ApproveWorkspaceJoinRequest =>
async ({ userId, workspaceId, approvedByUserId }) => {
const requester = await getUserById(userId)
if (!requester) {
throw new NotFoundError('User not found')
}
const workspace = await getWorkspace({ workspaceId })
if (!workspace) {
throw new WorkspaceNotFoundError('Workspace not found')
}
const request = await getWorkspaceJoinRequest({
userId,
workspaceId,
status: 'pending'
})
if (!request) {
throw new NotFoundError('Workspace join request not found')
}
await updateWorkspaceJoinRequestStatus({
userId,
workspaceId,
status: 'approved'
})
const role = Roles.Workspace.Member
await upsertWorkspaceRole({ userId, workspaceId, role, createdAt: new Date() })
const { type } = await ensureValidWorkspaceRoleSeat({ userId, workspaceId, role })
await emit({ eventName: WorkspaceEvents.Updated, payload: { workspace } })
await emit({
eventName: WorkspaceEvents.RoleUpdated,
payload: {
acl: { workspaceId, userId, role },
seatType: type,
updatedByUserId: approvedByUserId
}
})
await sendWorkspaceJoinRequestApprovedEmail({
workspace,
requester
})
return true
}
export const denyWorkspaceJoinRequestFactory =
({
updateWorkspaceJoinRequestStatus,
sendWorkspaceJoinRequestDeniedEmail,
getUserById,
getWorkspace,
getWorkspaceJoinRequest
}: {
updateWorkspaceJoinRequestStatus: UpdateWorkspaceJoinRequestStatus
sendWorkspaceJoinRequestDeniedEmail: SendWorkspaceJoinRequestDeniedEmail
getUserById: GetUser
getWorkspace: GetWorkspace
getWorkspaceJoinRequest: GetWorkspaceJoinRequest
}): DenyWorkspaceJoinRequest =>
async ({ userId, workspaceId }) => {
const requester = await getUserById(userId)
if (!requester) {
throw new NotFoundError('User not found')
}
const workspace = await getWorkspace({ workspaceId })
if (!workspace) {
throw new WorkspaceNotFoundError('Workspace not found')
}
const request = await getWorkspaceJoinRequest({
userId,
workspaceId,
status: 'pending'
})
if (!request) {
throw new NotFoundError('Workspace join request not found')
}
await updateWorkspaceJoinRequestStatus({
userId,
workspaceId,
status: 'denied'
})
await sendWorkspaceJoinRequestDeniedEmail({
workspace,
requester
})
return true
}