Files
speckle-server/packages/server/modules/comments/graph/resolvers/comments.js
T
Gergő Jedlicska 67cb97a262 gergo/testCommentsGQL (#775)
* yarn first go

* fix frontend build cache loader

* yarn workspaces built server Docker

* build(yarn): add workspaces plugin config

* chore(package defs): clean package*.json -s

* chore(gitignore): ignore yarn error log

* build(yarn): update yarn lock

* build(preview-service webpack): add extra resolved path to preview service webpack config

because of yarn package hoisting, there are no package level node_modules folder anymore.

* build(docker): update dockerignore with yarn specific configs

* build(docker): update Dockerfiles for yarn workspaces utilization

* ci(circleci): update server test job to yarn

* ci(circle): disable cache restore

* ci(circleci): trying the node orb yarn-run

* ci(circleci): yarn-run again

* ci(circleci): disable node orb

* ci(circleci): change base node image for tests

* ci(circleci): add yarn cache

* ci(circleci): remove node install step

* ci(circleci): add server specific cache archives

* ci(circleci): test build and publish

* ci(circleci): change npm auth method to suit yarn

* ci(circleci): trying new builder image

* ci(circleci): another base image, maybe this works

* ci(circleci): force a specific docker engine version

* ci(circleci): add yarn version plugin and its changes

* ci(circleci): cleanup and remove temp branch config

* chore(package defs): moving from npm run to yarn

* explicitly specifying webpack4 as a frontend dep

* chore(package defs): replace npm with yarn everywhere

* docs(root readme): update with some yarn specific docs

* test(server comments gql): add wip server comments gql tests

* test(server comments graphql): add missing test operations and generate a bunch of testcases

* test(server comments graphql api): fix all authz test cases for comments

* test(server comments service): fix comments service failing test

* fix(tests): do not look inside

Co-authored-by: Fabians <fabis94@live.com>
Co-authored-by: Dimitrie Stefanescu <didimitrie@gmail.com>
2022-06-02 11:15:27 +02:00

402 lines
12 KiB
JavaScript

const { pubsub } = require('@/modules/shared')
const { ForbiddenError, ApolloError, withFilter } = require('apollo-server-express')
const { Forbidden } = require('@/modules/shared/errors')
const { getStream } = require('@/modules/core/services/streams')
const { Roles } = require('@/modules/core/helpers/mainConstants')
const { saveActivity } = require('@/modules/activitystream/services')
const {
getComment,
getComments,
getResourceCommentCount,
getStreamCommentCount,
createComment,
createCommentReply,
viewComment,
archiveComment,
editComment,
streamResourceCheck,
formatCommentText
} = require('@/modules/comments/services')
const authorizeStreamAccess = async ({
streamId,
userId,
serverRole,
auth,
requireRole = false
}) => {
if (serverRole === Roles.Server.ArchivedUser)
throw new ForbiddenError('You are not authorized.')
const stream = await getStream({ streamId, userId })
if (!stream) throw new ApolloError('Stream not found')
let authZed = true
if (!stream.isPublic && auth === false) authZed = false
if (!stream.isPublic && !stream.role) authZed = false
if (stream.isPublic && requireRole && !stream.allowPublicComments && !stream.role)
authZed = false
if (!authZed) throw new ForbiddenError('You are not authorized.')
return stream
}
module.exports = {
Query: {
async comment(parent, args, context) {
await authorizeStreamAccess({
streamId: args.streamId,
userId: context.userId,
serverRole: context.role,
auth: context.auth
})
const comment = await getComment({ id: args.id, userId: context.userId })
if (comment.streamId !== args.streamId)
throw new ForbiddenError('You do not have access to this comment.')
return comment
},
async comments(parent, args, context) {
await authorizeStreamAccess({
streamId: args.streamId,
userId: context.userId,
serverRole: context.role,
auth: context.auth
})
return { ...(await getComments({ ...args, userId: context.userId })) }
}
},
Comment: {
async replies(parent, args) {
const resources = [{ resourceId: parent.id, resourceType: 'comment' }]
return await getComments({
resources,
replies: true,
limit: args.limit,
cursor: args.cursor
})
},
text(parent) {
return formatCommentText(parent)
}
},
Stream: {
async commentCount(parent, args, context) {
if (context.role === Roles.Server.ArchivedUser)
throw new ForbiddenError('You are not authorized.')
return await getStreamCommentCount({ streamId: parent.id })
}
},
Commit: {
async commentCount(parent, args, context) {
if (context.role === Roles.Server.ArchivedUser)
throw new ForbiddenError('You are not authorized.')
return await getResourceCommentCount({ resourceId: parent.id })
}
},
CommitCollectionUserNode: {
// urgh, i think we tripped our gql schemas in there a bit
async commentCount(parent, args, context) {
if (context.role === Roles.Server.ArchivedUser)
throw new ForbiddenError('You are not authorized.')
return await getResourceCommentCount({ resourceId: parent.id })
}
},
Object: {
async commentCount(parent, args, context) {
if (context.role === Roles.Server.ArchivedUser)
throw new ForbiddenError('You are not authorized.')
return await getResourceCommentCount({ resourceId: parent.id })
}
},
Mutation: {
// Used for broadcasting real time chat head bubbles and status. Does not persist anything!
async userViewerActivityBroadcast(parent, args, context) {
await authorizeStreamAccess({
streamId: args.streamId,
userId: context.userId,
serverRole: context.role,
auth: context.auth
})
// const stream = await getStream({
// streamId: args.streamId,
// userId: context.userId
// })
// if (!stream) {
// throw new ApolloError('Stream not found')
// }
// if (!stream.isPublic && !context.auth) {
// return false
// }
await pubsub.publish('VIEWER_ACTIVITY', {
userViewerActivity: args.data,
streamId: args.streamId,
resourceId: args.resourceId
})
return true
},
async userCommentThreadActivityBroadcast(parent, args, context) {
if (!context.userId) return false
const stream = await getStream({
streamId: args.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
await pubsub.publish('COMMENT_THREAD_ACTIVITY', {
commentThreadActivity: { eventType: 'reply-typing-status', data: args.data },
streamId: args.streamId,
commentId: args.commentId
})
return true
},
async commentCreate(parent, args, context) {
if (!context.userId)
throw new ForbiddenError('Only registered users can comment.')
const stream = await getStream({
streamId: args.input.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
const id = await createComment({ userId: context.userId, input: args.input })
await pubsub.publish('COMMENT_ACTIVITY', {
commentActivity: {
...args.input,
authorId: context.userId,
id,
replies: { totalCount: 0 },
updatedAt: Date.now(),
createdAt: Date.now(),
eventType: 'comment-added',
archived: false
},
streamId: args.input.streamId,
resourceIds: args.input.resources.map((res) => res.resourceId).join(',') // TODO: hack for now
})
await saveActivity({
streamId: args.input.streamId,
resourceType: 'comment',
resourceId: id,
actionType: 'comment_created',
userId: context.userId,
info: { input: args.input },
message: `Comment added: ${id} (${args.input})`
})
return id
},
async commentEdit(parent, args, context) {
// NOTE: This is NOT in use anywhere
const stream = await authorizeStreamAccess({
streamId: args.input.streamId,
userId: context.userId,
serverRole: context.role,
auth: context.auth,
//public, but not public comments needs a stream role to do this
requireRole: true
})
const matchUser = !stream.role
try {
await editComment({ userId: context.userId, input: args.input, matchUser })
return true
} catch (err) {
if (err instanceof Forbidden) throw new ForbiddenError(err.message)
throw err
}
},
// used for flagging a comment as viewed
async commentView(parent, args, context) {
await authorizeStreamAccess({
streamId: args.streamId,
userId: context.userId,
serverRole: context.role,
auth: context.auth
})
await viewComment({ userId: context.userId, commentId: args.commentId })
return true
},
async commentArchive(parent, args, context) {
await authorizeStreamAccess({
streamId: args.streamId,
userId: context.userId,
serverRole: context.role,
auth: context.auth,
//public, but not public comments needs a stream role to do this
requireRole: true
})
try {
await archiveComment({ ...args, userId: context.userId }) // NOTE: permissions check inside service
} catch (err) {
if (err instanceof Forbidden) throw new ForbiddenError(err.message)
throw err
}
await pubsub.publish('COMMENT_THREAD_ACTIVITY', {
commentThreadActivity: {
eventType: args.archived ? 'comment-archived' : 'comment-added'
},
streamId: args.streamId,
commentId: args.commentId
})
await saveActivity({
streamId: args.streamId,
resourceType: 'comment',
resourceId: args.commentId,
actionType: 'comment_archived',
userId: context.userId,
info: { input: args },
message: `Comment archived`
})
return true
},
async commentReply(parent, args, context) {
if (!context.userId)
throw new ForbiddenError('Only registered users can comment.')
const stream = await getStream({
streamId: args.input.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
const id = await createCommentReply({
authorId: context.userId,
parentCommentId: args.input.parentComment,
streamId: args.input.streamId,
text: args.input.text,
data: args.input.data
})
await pubsub.publish('COMMENT_THREAD_ACTIVITY', {
commentThreadActivity: {
eventType: 'reply-added',
...args.input,
id,
authorId: context.userId,
updatedAt: Date.now(),
createdAt: Date.now()
},
streamId: args.input.streamId,
commentId: args.input.parentComment
})
await saveActivity({
streamId: args.input.streamId,
resourceType: 'comment',
resourceId: args.input.parentComment,
actionType: 'comment_reply',
userId: context.userId,
info: { input: args.input },
message: `Comment reply created.`
})
return id
}
},
Subscription: {
userViewerActivity: {
subscribe: withFilter(
() => pubsub.asyncIterator(['VIEWER_ACTIVITY']),
async (payload, variables, context) => {
const stream = await getStream({
streamId: payload.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
return (
payload.streamId === variables.streamId &&
payload.resourceId === variables.resourceId
)
}
)
},
commentActivity: {
subscribe: withFilter(
() => pubsub.asyncIterator(['COMMENT_ACTIVITY']),
async (payload, variables, context) => {
const stream = await getStream({
streamId: payload.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
// if we're listening for a stream's root comments events
if (!variables.resourceIds) {
return payload.streamId === variables.streamId
}
// otherwise perform a deeper check
try {
// prevents comment exfiltration by listening in to a auth'ed stream, but different commit ("stream hopping" for subscriptions)
await streamResourceCheck({
streamId: variables.streamId,
resources: variables.resourceIds.map((resId) => {
return {
resourceId: resId,
resourceType: resId.length === 10 ? 'commit' : 'object'
}
})
})
for (const res of variables.resourceIds) {
if (
payload.resourceIds.includes(res) &&
payload.streamId === variables.streamId
) {
return true
}
}
} catch (e) {
return false
}
}
)
},
commentThreadActivity: {
subscribe: withFilter(
() => pubsub.asyncIterator(['COMMENT_THREAD_ACTIVITY']),
async (payload, variables, context) => {
const stream = await getStream({
streamId: payload.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
return (
payload.streamId === variables.streamId &&
payload.commentId === variables.commentId
)
}
)
}
}
}