Commit 12223550 authored by NGPixel's avatar NGPixel

feat: comments - default provider create (wip) + permissions

parent 2fe2e621
...@@ -126,7 +126,7 @@ export default { ...@@ -126,7 +126,7 @@ export default {
permission: 'write:comments', permission: 'write:comments',
hint: 'Can post new comments, as specified in the Page Rules', hint: 'Can post new comments, as specified in the Page Rules',
warning: false, warning: false,
restrictedForSystem: true, restrictedForSystem: false,
disabled: false disabled: false
}, },
{ {
......
<template lang="pug"> <template lang="pug">
div(v-intersect.once.quiet='onIntersect') div(v-intersect.once='onIntersect')
v-textarea#discussion-new( v-textarea#discussion-new(
outlined outlined
flat flat
...@@ -11,11 +11,37 @@ ...@@ -11,11 +11,37 @@
v-model='newcomment' v-model='newcomment'
color='blue-grey darken-2' color='blue-grey darken-2'
:background-color='$vuetify.theme.dark ? `grey darken-5` : `white`' :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'
v-if='permissions.write'
) )
.d-flex.align-center.pt-3 v-row.mt-2(dense, v-if='!isAuthenticated && permissions.write')
v-col(cols='12', lg='6')
v-text-field(
outlined
color='blue-grey darken-2'
:background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'
placeholder='Your Name'
hide-details
dense
autocomplete='name'
v-model='guestName'
)
v-col(cols='12', lg='6')
v-text-field(
outlined
color='blue-grey darken-2'
:background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'
placeholder='Your Email Address'
hide-details
type='email'
dense
autocomplete='email'
v-model='guestEmail'
)
.d-flex.align-center.pt-3(v-if='permissions.write')
v-icon.mr-1(color='blue-grey') mdi-language-markdown-outline v-icon.mr-1(color='blue-grey') mdi-language-markdown-outline
.caption.blue-grey--text Markdown Format .caption.blue-grey--text Markdown Format
v-spacer v-spacer
.caption.mr-3(v-if='isAuthenticated') Posting as #[strong {{userDisplayName}}]
v-btn( v-btn(
dark dark
color='blue-grey darken-2' color='blue-grey darken-2'
...@@ -24,7 +50,7 @@ ...@@ -24,7 +50,7 @@
) )
v-icon(left) mdi-comment v-icon(left) mdi-comment
span.text-none Post Comment span.text-none Post Comment
v-divider.mt-3 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')
v-progress-circular( v-progress-circular(
indeterminate indeterminate
...@@ -48,15 +74,18 @@ ...@@ -48,15 +74,18 @@
v-img(src='http://i.pravatar.cc/64') v-img(src='http://i.pravatar.cc/64')
v-card.elevation-1 v-card.elevation-1
v-card-text v-card-text
.caption: strong John Smith .caption: strong {{cm.authorName}}
.overline.grey--text 3 minutes ago .overline.grey--text 3 minutes ago
.mt-3 {{cm.render}} .mt-3 {{cm.render}}
.pt-5.text-center.body-2.blue-grey--text(v-else) Be the first to comment. .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.
</template> </template>
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { get } from 'vuex-pathify' import { get } from 'vuex-pathify'
import validate from 'validate.js'
import _ from 'lodash'
export default { export default {
data () { data () {
...@@ -64,19 +93,118 @@ export default { ...@@ -64,19 +93,118 @@ export default {
newcomment: '', newcomment: '',
isLoading: true, isLoading: true,
canFetch: false, canFetch: false,
comments: [] comments: [],
guestName: '',
guestEmail: ''
} }
}, },
computed: { computed: {
pageId: get('page/id') pageId: get('page/id'),
permissions: get('page/commentsPermissions'),
isAuthenticated: get('user/authenticated'),
userDisplayName: get('user/name')
}, },
methods: { methods: {
onIntersect () { onIntersect (entries, observer, isIntersecting) {
if (isIntersecting) {
this.isLoading = true this.isLoading = true
this.canFetch = true this.canFetch = true
}
}, },
async postComment () { async postComment () {
let rules = {
comment: {
presence: {
allowEmpty: false
},
length: {
minimum: 2
}
}
}
if (!this.isAuthenticated && this.permissions.write) {
rules.name = {
presence: {
allowEmpty: false
},
length: {
minimum: 2,
maximum: 255
}
}
rules.email = {
presence: {
allowEmpty: false
},
email: true
}
}
const validationResults = validate({
comment: this.newcomment,
name: this.guestName,
email: this.guestEmail
}, rules, { format: 'flat' })
if (validationResults) {
this.$store.commit('showNotification', {
style: 'red',
message: validationResults[0],
icon: 'alert'
})
return
}
const resp = await this.$apollo.mutate({
mutation: gql`
mutation (
$pageId: Int!
$replyTo: Int
$content: String!
$guestName: String
$guestEmail: String
) {
comments {
create (
pageId: $pageId
replyTo: $replyTo
content: $content
guestName: $guestName
guestEmail: $guestEmail
) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`,
variables: {
pageId: this.pageId,
replyTo: 0,
content: this.newcomment,
guestName: this.guestName,
guestEmail: this.guestEmail
}
})
if (_.get(resp, 'data.comments.create.responseResult.succeeded', false)) {
this.$store.commit('showNotification', {
style: 'success',
message: 'New comment posted successfully.',
icon: 'check'
})
this.newcomment = ''
} else {
this.$store.commit('showNotification', {
style: 'red',
message: _.get(resp, 'data.comments.create.responseResult.message', 'An unexpected error occured.'),
icon: 'alert'
})
}
} }
}, },
apollo: { apollo: {
......
...@@ -15,9 +15,9 @@ const state = { ...@@ -15,9 +15,9 @@ const state = {
title: '', title: '',
updatedAt: '', updatedAt: '',
mode: '', mode: '',
comments: { commentsPermissions: {
view: false, read: false,
post: false, write: false,
manage: false manage: false
}, },
commentsCount: 0 commentsCount: 0
......
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
) )
v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', size='20') mdi-tag-multiple v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', size='20') mdi-tag-multiple
v-card.mb-5(v-if='commentsEnabled') v-card.mb-5(v-if='commentsEnabled && commentsPerms.read')
.pa-5 .pa-5
.overline.pb-2.blue-grey--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : `text--darken-2`') .overline.pb-2.blue-grey--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : `text--darken-2`')
span Talk span Talk
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
small small
) )
span.blue-grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') View Discussion span.blue-grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') View Discussion
v-tooltip(right, v-if='isAuthenticated') v-tooltip(right, v-if='commentsPerms.write')
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn.ml-2( v-btn.ml-2(
@click='goToComments(true)' @click='goToComments(true)'
...@@ -261,7 +261,7 @@ ...@@ -261,7 +261,7 @@
span {{$t('common:page.editPage')}} span {{$t('common:page.editPage')}}
.contents(ref='container') .contents(ref='container')
slot(name='contents') slot(name='contents')
.comments-container#discussion .comments-container#discussion(v-if='commentsEnabled && commentsPerms.read')
.comments-header .comments-header
v-icon.mr-2(dark) mdi-comment-text-outline v-icon.mr-2(dark) mdi-comment-text-outline
span Comments span Comments
...@@ -446,6 +446,7 @@ export default { ...@@ -446,6 +446,7 @@ export default {
computed: { computed: {
isAuthenticated: get('user/authenticated'), isAuthenticated: get('user/authenticated'),
commentsCount: get('page/commentsCount'), commentsCount: get('page/commentsCount'),
commentsPerms: get('page/commentsPermissions'),
rating: { rating: {
get () { get () {
return 3.5 return 3.5
...@@ -491,7 +492,7 @@ export default { ...@@ -491,7 +492,7 @@ export default {
this.$store.set('page/title', this.title) this.$store.set('page/title', this.title)
this.$store.set('page/updatedAt', this.updatedAt) this.$store.set('page/updatedAt', this.updatedAt)
if (this.commentsPermissions) { if (this.commentsPermissions) {
this.$store.set('page/comments', JSON.parse(atob(this.commentsPermissions))) this.$store.set('page/commentsPermissions', JSON.parse(atob(this.commentsPermissions)))
} }
this.$store.set('page/mode', 'view') this.$store.set('page/mode', 'view')
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
"@root/keypairs": "0.9.0", "@root/keypairs": "0.9.0",
"@root/pem": "1.0.4", "@root/pem": "1.0.4",
"acme": "3.0.3", "acme": "3.0.3",
"akismet-api": "5.0.0",
"algoliasearch": "4.2.0", "algoliasearch": "4.2.0",
"apollo-fetch": "0.7.0", "apollo-fetch": "0.7.0",
"apollo-server": "2.13.1", "apollo-server": "2.13.1",
......
...@@ -447,12 +447,24 @@ router.get('/*', async (req, res, next) => { ...@@ -447,12 +447,24 @@ router.get('/*', async (req, res, next) => {
}) })
} }
// -> Comments Permissions
const commentsPermissions = WIKI.config.features.featurePageComments ? {
read: WIKI.auth.checkAccess(req.user, ['read:comments'], pageArgs),
write: WIKI.auth.checkAccess(req.user, ['write:comments'], pageArgs),
manage: WIKI.auth.checkAccess(req.user, ['manage:comments'], pageArgs)
} : {
read: false,
write: false,
manage: false
}
// -> Render view // -> Render view
res.render('page', { res.render('page', {
page, page,
sidebar, sidebar,
injectCode, injectCode,
comments: WIKI.data.commentProvider comments: WIKI.data.commentProvider,
commentsPermissions
}) })
} }
} else if (pageArgs.path === 'home') { } else if (pageArgs.path === 'home') {
......
exports.up = knex => {
return knex.schema
.alterTable('comments', table => {
table.integer('replyTo').unsigned().notNullable().defaultTo(0)
})
}
exports.down = knex => { }
exports.up = knex => {
return knex.schema
.alterTable('comments', table => {
table.integer('replyTo').unsigned().notNullable().defaultTo(0)
})
}
exports.down = knex => { }
...@@ -11,6 +11,9 @@ module.exports = { ...@@ -11,6 +11,9 @@ module.exports = {
async comments() { return {} } async comments() { return {} }
}, },
CommentQuery: { CommentQuery: {
/**
* Fetch list of Comments Providers
*/
async providers(obj, args, context, info) { async providers(obj, args, context, info) {
const providers = await WIKI.models.commentProviders.getProviders() const providers = await WIKI.models.commentProviders.getProviders()
return providers.map(provider => { return providers.map(provider => {
...@@ -33,11 +36,33 @@ module.exports = { ...@@ -33,11 +36,33 @@ module.exports = {
} }
}) })
}, },
/**
* Fetch list of comments for a page
*/
async list (obj, args, context) { async list (obj, args, context) {
return [] return []
} }
}, },
CommentMutation: { CommentMutation: {
/**
* Create New Comment
*/
async create (obj, args, context) {
try {
// WIKI.data.commentProvider.create({
// ...args,
// user: context.req.user
// })
return {
responseResult: graphHelper.generateSuccess('New comment posted successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* Update Comments Providers
*/
async updateProviders(obj, args, context) { async updateProviders(obj, args, context) {
try { try {
for (let provider of args.providers) { for (let provider of args.providers) {
......
...@@ -39,12 +39,18 @@ type CommentMutation { ...@@ -39,12 +39,18 @@ type CommentMutation {
pageId: Int! pageId: Int!
replyTo: Int replyTo: Int
content: String! content: String!
guestName: String
guestEmail: String
): DefaultResponse @auth(requires: ["write:comments", "manage:system"]) ): DefaultResponse @auth(requires: ["write:comments", "manage:system"])
update( update(
id: Int! id: Int!
content: String! content: String!
): DefaultResponse @auth(requires: ["write:comments", "manage:comments", "manage:system"]) ): DefaultResponse @auth(requires: ["write:comments", "manage:comments", "manage:system"])
delete(
id: Int!
): DefaultResponse @auth(requires: ["manage:comments", "manage:system"])
} }
# ----------------------------------------------- # -----------------------------------------------
......
const Model = require('objection').Model
/**
* Comments model
*/
module.exports = class Comment extends Model {
static get tableName() { return 'comments' }
static get jsonSchema () {
return {
type: 'object',
required: [],
properties: {
id: {type: 'integer'},
content: {type: 'string'},
render: {type: 'string'},
name: {type: 'string'},
email: {type: 'string'},
ip: {type: 'string'},
createdAt: {type: 'string'},
updatedAt: {type: 'string'}
}
}
}
static get relationMappings() {
return {
author: {
relation: Model.BelongsToOneRelation,
modelClass: require('./users'),
join: {
from: 'comments.authorId',
to: 'users.id'
}
},
page: {
relation: Model.BelongsToOneRelation,
modelClass: require('./pages'),
join: {
from: 'comments.pageId',
to: 'pages.id'
}
}
}
}
$beforeUpdate() {
this.updatedAt = new Date().toISOString()
}
$beforeInsert() {
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
}
const md = require('markdown-it')
const mdEmoji = require('markdown-it-emoji')
const { JSDOM } = require('jsdom')
const createDOMPurify = require('dompurify')
const _ = require('lodash')
const { AkismetClient } = require('akismet-api')
/* global WIKI */ /* global WIKI */
const window = new JSDOM('').window
const DOMPurify = createDOMPurify(window)
md.use(mdEmoji)
let akismetClient = null
// ------------------------------------ // ------------------------------------
// Default Comment Provider // Default Comment Provider
// ------------------------------------ // ------------------------------------
module.exports = { module.exports = {
add (args) { /**
* Init
*/
async init (config) {
if (WIKI.data.commentProvider.config.akismet && WIKI.data.commentProvider.config.akismet.length > 2) {
akismetClient = new AkismetClient({
key: WIKI.data.commentProvider.config.akismet,
blog: WIKI.config.host,
lang: WIKI.config.lang.namespacing ? WIKI.config.lang.namespaces.join(', ') : WIKI.config.lang.code,
charset: 'UTF-8'
})
try {
const isValid = await akismetClient.verifyKey()
if (!isValid) {
WIKI.logger.warn('Akismet Key is invalid!')
}
} catch (err) {
WIKI.logger.warn('Unable to verify Akismet Key: ' + err.message)
}
} else {
akismetClient = null
}
},
/**
* Create New Comment
*/
async create ({ page, replyTo, content, user }) {
// -> Render Markdown
const mkdown = md({
html: false,
breaks: true,
linkify: true,
highlight(str, lang) {
return `<pre><code class="language-${lang}">${_.escape(str)}</code></pre>`
}
})
// -> Build New Comment
const newComment = {
content,
render: DOMPurify.sanitize(mkdown.render(content)),
replyTo,
pageId: page.id,
authorId: user.id,
name: user.name,
email: user.email,
ip: user.ip
}
// Check for Spam with Akismet
if (akismetClient) {
let userRole = 'user'
if (user.groups.indexOf(1) >= 0) {
userRole = 'administrator'
} else if (user.groups.indexOf(2) >= 0) {
userRole = 'guest'
}
let isSpam = false
try {
isSpam = await akismetClient.checkSpam({
ip: user.ip,
useragent: user.agentagent,
content,
name: user.name,
email: user.email,
permalink: `${WIKI.config.host}/${page.localeCode}/${page.path}`,
permalinkDate: page.updatedAt,
type: (replyTo > 0) ? 'reply' : 'comment',
role: userRole
})
} catch (err) {
WIKI.logger.warn('Akismet Comment Validation: [ FAILED ]')
WIKI.logger.warn(err)
}
if (isSpam) {
throw new Error('Comment was rejected because it is marked as spam.')
}
}
// Save Comment
await WIKI.models.comments.query().insert(newComment)
},
async update ({ id, content, user, ip }) {
},
async remove ({ id, user, ip }) {
},
async count ({ pageId }) {
} }
} }
...@@ -26,7 +26,7 @@ block body ...@@ -26,7 +26,7 @@ block body
sidebar=Buffer.from(JSON.stringify(sidebar)).toString('base64') sidebar=Buffer.from(JSON.stringify(sidebar)).toString('base64')
nav-mode=config.nav.mode nav-mode=config.nav.mode
comments-enabled=config.features.featurePageComments comments-enabled=config.features.featurePageComments
comments-provider=comments.key comments-permissions=Buffer.from(JSON.stringify(commentsPermissions)).toString('base64')
comments-external=comments.codeTemplate comments-external=comments.codeTemplate
) )
template(slot='contents') template(slot='contents')
......
This diff was suppressed by a .gitattributes entry.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment