a563fa27c7
* fix(fe2): clearer error msg on failed upload * fix(fe2): missing project.commentThreads access w/ admin override
288 lines
8.1 KiB
TypeScript
288 lines
8.1 KiB
TypeScript
import { ensureError, Roles, SpeckleViewer } from '@speckle/shared'
|
|
import { AuthContext } from '@/modules/shared/authz'
|
|
import { ForbiddenError } from '@/modules/shared/errors'
|
|
import { getStream } from '@/modules/core/repositories/streams'
|
|
import { StreamInvalidAccessError } from '@/modules/core/errors/stream'
|
|
import {
|
|
InsertCommentPayload,
|
|
getComment,
|
|
markCommentViewed,
|
|
insertComment,
|
|
insertCommentLinks,
|
|
markCommentUpdated,
|
|
updateComment
|
|
} from '@/modules/comments/repositories/comments'
|
|
import {
|
|
CreateCommentInput,
|
|
CreateCommentReplyInput,
|
|
EditCommentInput
|
|
} from '@/modules/core/graph/generated/graphql'
|
|
import { getViewerResourceItemsUngrouped } from '@/modules/core/services/commit/viewerResources'
|
|
import { CommentCreateError, CommentUpdateError } from '@/modules/comments/errors'
|
|
import {
|
|
buildCommentTextFromInput,
|
|
validateInputAttachments
|
|
} from '@/modules/comments/services/commentTextService'
|
|
import { knex } from '@/modules/core/dbSchema'
|
|
import {
|
|
CommentLinkRecord,
|
|
CommentLinkResourceType,
|
|
CommentRecord
|
|
} from '@/modules/comments/helpers/types'
|
|
import { CommentsEmitter, CommentsEvents } from '@/modules/comments/events/emitter'
|
|
import {
|
|
addCommentArchivedActivity,
|
|
addCommentCreatedActivity,
|
|
addReplyAddedActivity
|
|
} from '@/modules/activitystream/services/commentActivity'
|
|
import {
|
|
formatSerializedViewerState,
|
|
inputToDataStruct
|
|
} from '@/modules/comments/services/data'
|
|
import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper'
|
|
|
|
export async function authorizeProjectCommentsAccess(params: {
|
|
projectId: string
|
|
authCtx: AuthContext
|
|
requireProjectRole?: boolean
|
|
}) {
|
|
const { projectId, authCtx, requireProjectRole } = params
|
|
if (authCtx.role === Roles.Server.ArchivedUser) {
|
|
throw new ForbiddenError('You are not authorized')
|
|
}
|
|
|
|
const project = await getStream({ streamId: projectId, userId: authCtx.userId })
|
|
if (!project) {
|
|
throw new StreamInvalidAccessError('Stream not found')
|
|
}
|
|
|
|
let success = true
|
|
if (!project.isPublic && !authCtx.auth) success = false
|
|
if (!project.isPublic && !project.role) success = false
|
|
if (requireProjectRole && !project.role && !project.allowPublicComments)
|
|
success = false
|
|
if (adminOverrideEnabled() && authCtx.role === Roles.Server.Admin) success = true
|
|
|
|
if (!success) {
|
|
throw new StreamInvalidAccessError('You are not authorized')
|
|
}
|
|
|
|
return project
|
|
}
|
|
|
|
export async function authorizeCommentAccess(params: {
|
|
authCtx: AuthContext
|
|
commentId: string
|
|
requireProjectRole?: boolean
|
|
}) {
|
|
const { authCtx, commentId, requireProjectRole } = params
|
|
const comment = await getComment({ id: commentId, userId: authCtx.userId })
|
|
if (!comment) {
|
|
throw new StreamInvalidAccessError('Attempting to access a nonexistant comment')
|
|
}
|
|
|
|
return authorizeProjectCommentsAccess({
|
|
projectId: comment.streamId,
|
|
authCtx,
|
|
requireProjectRole
|
|
})
|
|
}
|
|
|
|
export async function markViewed(commentId: string, userId: string) {
|
|
await markCommentViewed(commentId, userId)
|
|
}
|
|
|
|
export async function createCommentThreadAndNotify(
|
|
input: CreateCommentInput,
|
|
userId: string
|
|
) {
|
|
const [resources] = await Promise.all([
|
|
getViewerResourceItemsUngrouped({ ...input, loadedVersionsOnly: true }),
|
|
validateInputAttachments(input.projectId, input.content.blobIds || [])
|
|
])
|
|
if (!resources.length) {
|
|
throw new CommentCreateError(
|
|
"Resource ID string doesn't resolve to any valid resources for the specified project/stream"
|
|
)
|
|
}
|
|
|
|
const state = SpeckleViewer.ViewerState.isSerializedViewerState(input.viewerState)
|
|
? formatSerializedViewerState(input.viewerState)
|
|
: null
|
|
const dataStruct = inputToDataStruct(state)
|
|
|
|
const commentPayload: InsertCommentPayload = {
|
|
streamId: input.projectId,
|
|
authorId: userId,
|
|
text: buildCommentTextFromInput({
|
|
doc: input.content.doc,
|
|
blobIds: input.content.blobIds || undefined
|
|
}),
|
|
screenshot: input.screenshot,
|
|
data: dataStruct
|
|
}
|
|
|
|
let comment: CommentRecord
|
|
try {
|
|
comment = await knex.transaction(async (trx) => {
|
|
const comment = await insertComment(commentPayload, { trx })
|
|
|
|
const links: CommentLinkRecord[] = resources.map((r) => {
|
|
let resourceId = r.objectId
|
|
let resourceType: CommentLinkResourceType = 'object'
|
|
if (r.versionId) {
|
|
resourceId = r.versionId
|
|
resourceType = 'commit'
|
|
}
|
|
|
|
return {
|
|
commentId: comment.id,
|
|
resourceId,
|
|
resourceType
|
|
}
|
|
})
|
|
await insertCommentLinks(links, { trx })
|
|
|
|
return comment
|
|
})
|
|
} catch (e) {
|
|
throw new CommentCreateError('Comment creation failed', { cause: ensureError(e) })
|
|
}
|
|
|
|
// Mark as viewed and emit events
|
|
await Promise.all([
|
|
markViewed(comment.id, userId),
|
|
CommentsEmitter.emit(CommentsEvents.Created, {
|
|
comment
|
|
}),
|
|
addCommentCreatedActivity({
|
|
streamId: input.projectId,
|
|
userId,
|
|
input: {
|
|
...input,
|
|
resolvedResourceItems: resources
|
|
},
|
|
comment
|
|
})
|
|
])
|
|
|
|
return comment
|
|
}
|
|
|
|
export async function createCommentReplyAndNotify(
|
|
input: CreateCommentReplyInput,
|
|
userId: string
|
|
) {
|
|
const thread = await getComment({ id: input.threadId, userId })
|
|
if (!thread) {
|
|
throw new CommentCreateError('Reply creation failed due to nonexistant thread')
|
|
}
|
|
await validateInputAttachments(thread.streamId, input.content.blobIds || [])
|
|
|
|
const commentPayload: InsertCommentPayload = {
|
|
streamId: thread.streamId,
|
|
authorId: userId,
|
|
text: buildCommentTextFromInput({
|
|
doc: input.content.doc,
|
|
blobIds: input.content.blobIds || undefined
|
|
}),
|
|
parentComment: thread.id,
|
|
data: null
|
|
}
|
|
|
|
let reply: CommentRecord
|
|
try {
|
|
reply = await knex.transaction(async (trx) => {
|
|
const reply = await insertComment(commentPayload, { trx })
|
|
const links: CommentLinkRecord[] = [
|
|
{ resourceType: 'comment', resourceId: thread.id, commentId: reply.id }
|
|
]
|
|
await insertCommentLinks(links, { trx })
|
|
|
|
return reply
|
|
})
|
|
} catch (e) {
|
|
throw new CommentCreateError('Reply creation failed', { cause: ensureError(e) })
|
|
}
|
|
|
|
// Mark parent comment updated and emit events
|
|
await Promise.all([
|
|
markCommentUpdated(thread.id),
|
|
CommentsEmitter.emit(CommentsEvents.Created, {
|
|
comment: reply
|
|
}),
|
|
addReplyAddedActivity({
|
|
streamId: thread.streamId,
|
|
input,
|
|
reply,
|
|
userId
|
|
})
|
|
])
|
|
|
|
return reply
|
|
}
|
|
|
|
export async function editCommentAndNotify(input: EditCommentInput, userId: string) {
|
|
const comment = await getComment({ id: input.commentId, userId })
|
|
if (!comment) {
|
|
throw new CommentUpdateError('Comment update failed due to nonexistant comment')
|
|
}
|
|
if (comment.authorId !== userId) {
|
|
throw new CommentUpdateError("You cannot edit someone else's comments")
|
|
}
|
|
|
|
await validateInputAttachments(comment.streamId, input.content.blobIds || [])
|
|
const updatedComment = await updateComment(comment.id, {
|
|
text: buildCommentTextFromInput({
|
|
doc: input.content.doc,
|
|
blobIds: input.content.blobIds || undefined
|
|
})
|
|
})
|
|
|
|
await Promise.all([
|
|
CommentsEmitter.emit(CommentsEvents.Updated, {
|
|
previousComment: comment,
|
|
newComment: updatedComment
|
|
})
|
|
])
|
|
|
|
return updatedComment
|
|
}
|
|
|
|
export async function archiveCommentAndNotify(
|
|
commentId: string,
|
|
userId: string,
|
|
archived = true
|
|
) {
|
|
const comment = await getComment({ id: commentId, userId })
|
|
if (!comment) {
|
|
throw new CommentUpdateError(
|
|
"Specified comment doesn't exist and thus it's archival status can't be changed"
|
|
)
|
|
}
|
|
|
|
const stream = await getStream({ streamId: comment.streamId, userId })
|
|
if (!stream || (comment.authorId !== userId && stream.role !== Roles.Stream.Owner)) {
|
|
throw new CommentUpdateError('You do not have permissions to archive this comment')
|
|
}
|
|
const updatedComment = await updateComment(comment.id, {
|
|
archived
|
|
})
|
|
|
|
await Promise.all([
|
|
addCommentArchivedActivity({
|
|
streamId: stream.id,
|
|
commentId,
|
|
userId,
|
|
input: {
|
|
archived,
|
|
streamId: stream.id,
|
|
commentId
|
|
},
|
|
comment: updatedComment
|
|
})
|
|
])
|
|
|
|
return updatedComment
|
|
}
|