diff --git a/client/components/comments.vue b/client/components/comments.vue index a27cd41f..77b14b77 100644 --- a/client/components/comments.vue +++ b/client/components/comments.vue @@ -51,7 +51,7 @@ v-icon(left) mdi-comment span.text-none Post Comment v-divider.mt-3(v-if='permissions.write') - .pa-5.d-flex.align-center.justify-center(v-if='isLoading') + .pa-5.d-flex.align-center.justify-center(v-if='isLoading && !hasLoadedOnce') v-progress-circular( indeterminate size='20' @@ -63,22 +63,38 @@ dense v-else-if='comments && comments.length > 0' ) - v-timeline-item( + v-timeline-item.comments-post( color='pink darken-4' large v-for='cm of comments' :key='`comment-` + cm.id' + :id='`comment-post-id-` + cm.id' ) template(v-slot:icon) - v-avatar - v-img(src='http://i.pravatar.cc/64') + v-avatar(color='blue-grey') + //- v-img(src='http://i.pravatar.cc/64') + span.white--text.title {{cm.initials}} v-card.elevation-1 v-card-text - .caption: strong {{cm.authorName}} - .overline.grey--text 3 minutes ago - .mt-3 {{cm.render}} + .comments-post-actions(v-if='permissions.manage && !isBusy') + v-icon.mr-3(small, @click='editComment(cm)') mdi-pencil + v-icon(small, @click='deleteCommentConfirm(cm)') mdi-delete + .comments-post-name.caption: strong {{cm.authorName}} + .comments-post-date.overline.grey--text {{cm.createdAt | moment('from') }} #[em(v-if='cm.createdAt !== cm.updatedAt') - modified {{cm.updatedAt | moment('from') }}] + .comments-post-content.mt-3(v-html='cm.render') .pt-5.text-center.body-2.blue-grey--text(v-else-if='permissions.write') Be the first to comment. .text-center.body-2.blue-grey--text(v-else) No comments yet. + + v-dialog(v-model='deleteCommentDialogShown', max-width='500') + v-card + .dialog-header.is-red Confirm Delete + v-card-text.pt-5 + span Are you sure you want to permanently delete this comment? + .caption: strong This action cannot be undone! + v-card-chin + v-spacer + v-btn(text, @click='deleteCommentDialogShown = false') {{$t('common:actions.cancel')}} + v-btn(color='red', dark, @click='deleteComment') {{$t('common:actions.delete')}} diff --git a/client/themes/default/scss/app.scss b/client/themes/default/scss/app.scss index 2647745a..8a888d01 100644 --- a/client/themes/default/scss/app.scss +++ b/client/themes/default/scss/app.scss @@ -823,7 +823,7 @@ border-radius: 7px 7px 0 0; @at-root .theme--dark & { - background-color: lighten(mc('grey', '900'), 5%); + background-color: lighten(mc('blue-grey', '900'), 5%); } } diff --git a/server/graph/resolvers/comment.js b/server/graph/resolvers/comment.js index 5e836b4c..f5c1edbe 100644 --- a/server/graph/resolvers/comment.js +++ b/server/graph/resolvers/comment.js @@ -40,7 +40,25 @@ module.exports = { * Fetch list of comments for a page */ async list (obj, args, context) { - return [] + const page = await WIKI.models.pages.getPage(args) + if (page) { + if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], { + path: page.path, + locale: page.localeCode + })) { + const comments = await WIKI.models.comments.query().where('pageId', page.id) + return comments.map(c => ({ + ...c, + authorName: c.name, + authorEmail: c.email, + authorIP: c.ip + })) + } else { + throw new WIKI.Error.PageViewForbidden() + } + } else { + return [] + } } }, CommentMutation: { @@ -49,12 +67,31 @@ module.exports = { */ async create (obj, args, context) { try { - // WIKI.data.commentProvider.create({ - // ...args, - // user: context.req.user - // }) + const cmId = await WIKI.models.comments.postNewComment({ + ...args, + user: context.req.user, + ip: context.req.ip + }) return { - responseResult: graphHelper.generateSuccess('New comment posted successfully') + responseResult: graphHelper.generateSuccess('New comment posted successfully'), + id: cmId + } + } catch (err) { + return graphHelper.generateError(err) + } + }, + /** + * Delete an Existing Comment + */ + async delete (obj, args, context) { + try { + await WIKI.models.comments.deleteComment({ + id: args.id, + user: context.req.user, + ip: context.req.ip + }) + return { + responseResult: graphHelper.generateSuccess('Comment deleted successfully') } } catch (err) { return graphHelper.generateError(err) diff --git a/server/graph/schemas/comment.graphql b/server/graph/schemas/comment.graphql index d99c4066..74c9d629 100644 --- a/server/graph/schemas/comment.graphql +++ b/server/graph/schemas/comment.graphql @@ -18,7 +18,8 @@ type CommentQuery { providers: [CommentProvider] @auth(requires: ["manage:system"]) list( - pageId: Int! + locale: String! + path: String! ): [CommentPost]! @auth(requires: ["read:comments", "manage:system"]) single( @@ -41,7 +42,7 @@ type CommentMutation { content: String! guestName: String guestEmail: String - ): DefaultResponse @auth(requires: ["write:comments", "manage:system"]) + ): CommentCreateResponse @auth(requires: ["write:comments", "manage:system"]) update( id: Int! @@ -85,3 +86,8 @@ type CommentPost { createdAt: Date! updatedAt: Date! } + +type CommentCreateResponse { + responseResult: ResponseStatus + id: Int +} diff --git a/server/helpers/error.js b/server/helpers/error.js index 05b6e244..d7093a2a 100644 --- a/server/helpers/error.js +++ b/server/helpers/error.js @@ -97,6 +97,26 @@ module.exports = { message: 'Too many attempts! Try again later.', code: 1008 }), + CommentGenericError: CustomError('CommentGenericError', { + message: 'An unexpected error occured.', + code: 8001 + }), + CommentPostForbidden: CustomError('CommentPostForbidden', { + message: 'You are not authorized to post a comment on this page.', + code: 8002 + }), + CommentContentMissing: CustomError('CommentContentMissing', { + message: 'Comment content is missing or too short.', + code: 8003 + }), + CommentManageForbidden: CustomError('CommentManageForbidden', { + message: 'You are not authorized to manage comments on this page.', + code: 8004 + }), + CommentNotFound: CustomError('CommentNotFound', { + message: 'This comment does not exist.', + code: 8005 + }), InputInvalid: CustomError('InputInvalid', { message: 'Input data is invalid.', code: 1012 diff --git a/server/models/comments.js b/server/models/comments.js index f938ba43..228b18af 100644 --- a/server/models/comments.js +++ b/server/models/comments.js @@ -1,4 +1,8 @@ const Model = require('objection').Model +const validate = require('validate.js') +const _ = require('lodash') + +/* global WIKI */ /** * Comments model @@ -52,4 +56,102 @@ module.exports = class Comment extends Model { this.createdAt = new Date().toISOString() this.updatedAt = new Date().toISOString() } + + /** + * Post New Comment + */ + static async postNewComment ({ pageId, replyTo, content, guestName, guestEmail, user, ip }) { + // -> Input validation + if (user.id === 2) { + const validation = validate({ + email: _.toLower(guestEmail), + name: guestName + }, { + email: { + email: true, + length: { + maximum: 255 + } + }, + name: { + presence: { + allowEmpty: false + }, + length: { + minimum: 2, + maximum: 255 + } + } + }, { format: 'flat' }) + + if (validation && validation.length > 0) { + throw new WIKI.Error.InputInvalid(validation[0]) + } + } + + content = _.trim(content) + if (content.length < 2) { + throw new WIKI.Error.CommentContentMissing() + } + + // -> Load Page + const page = await WIKI.models.pages.getPageFromDb(pageId) + if (page) { + if (!WIKI.auth.checkAccess(user, ['write:comments'], { + path: page.path, + locale: page.localeCode + })) { + throw new WIKI.Error.CommentPostForbidden() + } + } else { + throw new WIKI.Error.PageNotFound() + } + + // -> Process by comment provider + return WIKI.data.commentProvider.create({ + page, + replyTo, + content, + user: { + ...user, + ...(user.id === 2) ? { + name: guestName, + email: guestEmail + } : {}, + ip + } + }) + } + + /** + * Delete an Existing Comment + */ + static async deleteComment ({ id, user, ip }) { + // -> Load Page + const pageId = await WIKI.data.commentProvider.getPageIdFromCommentId(id) + if (!pageId) { + throw new WIKI.Error.CommentNotFound() + } + const page = await WIKI.models.pages.getPageFromDb(pageId) + if (page) { + if (!WIKI.auth.checkAccess(user, ['manage:comments'], { + path: page.path, + locale: page.localeCode + })) { + throw new WIKI.Error.CommentManageForbidden() + } + } else { + throw new WIKI.Error.PageNotFound() + } + + // -> Process by comment provider + await WIKI.data.commentProvider.remove({ + id, + page, + user: { + ...user, + ip + } + }) + } } diff --git a/server/modules/comments/default/comment.js b/server/modules/comments/default/comment.js index 3064b178..e56b63c7 100644 --- a/server/modules/comments/default/comment.js +++ b/server/modules/comments/default/comment.js @@ -74,7 +74,7 @@ module.exports = { ip: user.ip } - // Check for Spam with Akismet + // -> Check for Spam with Akismet if (akismetClient) { let userRole = 'user' if (user.groups.indexOf(1) >= 0) { @@ -106,16 +106,33 @@ module.exports = { } } - // Save Comment - await WIKI.models.comments.query().insert(newComment) + // -> Save Comment to DB + const cm = await WIKI.models.comments.query().insert(newComment) + + // -> Return Comment ID + return cm.id }, async update ({ id, content, user, ip }) { }, + /** + * Delete an existing comment by ID + */ async remove ({ id, user, ip }) { - + return WIKI.models.comments.query().findById(id).delete() }, - async count ({ pageId }) { - + /** + * Get the page ID from a comment ID + */ + async getPageIdFromCommentId (id) { + const result = await WIKI.models.comments.query().select('pageId').findById(id) + return (result) ? result.pageId : false + }, + /** + * Get the total comments count for a page ID + */ + async count (pageId) { + const result = await WIKI.models.comments.query().count('* as total').where('pageId', pageId).first() + return _.toSafeInteger(result.total) } }