From 30a8c3b48e057f52d950561f9b25175f9eefef93 Mon Sep 17 00:00:00 2001 From: rob Date: Wed, 13 Jul 2022 02:35:38 -0400 Subject: [PATCH] separated author and publisher permissions --- app/controllers/admin/page.js | 2 +- app/controllers/post.js | 7 ++++++- app/models/lib/user-types.js | 2 ++ app/models/user.js | 17 ++++++++++++++++ app/services/core-node.js | 9 ++++++++- app/services/page.js | 24 ++++++++++++++++++----- app/services/post.js | 30 +++++++++++++++++++++++++++-- app/services/user.js | 22 ++++++++++++++++++--- app/views/admin/core-user/form.pug | 6 ++++++ app/views/admin/user/form.pug | 6 ++++++ app/views/components/off-canvas.pug | 2 +- app/views/post/editor.pug | 2 +- app/views/post/view.pug | 11 +++++++---- 13 files changed, 121 insertions(+), 19 deletions(-) diff --git a/app/controllers/admin/page.js b/app/controllers/admin/page.js index 7b327aa..7418a04 100644 --- a/app/controllers/admin/page.js +++ b/app/controllers/admin/page.js @@ -54,7 +54,7 @@ class PageController extends SiteController { async pageUpdatePage (req, res, next) { const { page: pageService } = this.dtp.services; try { - await pageService.update(res.locals.page, req.body); + await pageService.update(req.user, res.locals.page, req.body); res.redirect('/admin/page'); } catch (error) { this.log.error('failed to update page', { newletterId: res.locals.page._id, error }); diff --git a/app/controllers/post.js b/app/controllers/post.js index a77478a..d30bb8f 100644 --- a/app/controllers/post.js +++ b/app/controllers/post.js @@ -195,7 +195,7 @@ class PostController extends SiteController { if (!req.user._id.equals(res.locals.post.author._id)) { throw new SiteError(403, 'This is not your post'); } - await postService.update(res.locals.post, req.body); + await postService.update(req.user, res.locals.post, req.body); res.redirect(`/post/${res.locals.post.slug}`); } catch (error) { this.log.error('failed to update post', { newletterId: res.locals.post._id, error }); @@ -259,6 +259,11 @@ class PostController extends SiteController { async getView (req, res, next) { const { comment: commentService, resource: resourceService } = this.dtp.services; try { + if ((res.locals.post.status !== 'published') && + !res.locals.post.author._id.equals(req.user._id) && + !req.user.hasAuthorDashboard) { + throw new SiteError(403, 'The post is not published'); + } await resourceService.recordView(req, 'Post', res.locals.post._id); res.locals.countPerPage = 20; diff --git a/app/models/lib/user-types.js b/app/models/lib/user-types.js index 9c2d294..f1ea468 100644 --- a/app/models/lib/user-types.js +++ b/app/models/lib/user-types.js @@ -22,6 +22,8 @@ module.exports.UserPermissionsSchema = new Schema({ canReport: { type: Boolean, default: true, required: true }, canAuthorPages: { type: Boolean, default: false, required: true }, canAuthorPosts: { type: Boolean, default: false, required: true }, + canPublishPages: { type: Boolean, default: false, required: true }, + canPublishPosts: { type: Boolean, default: false, required: true }, }); module.exports.UserOptInSchema = new Schema({ diff --git a/app/models/user.js b/app/models/user.js index ee98939..0b31f5b 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -29,6 +29,23 @@ const UserSchema = new Schema({ optIn: { type: UserOptInSchema, required: true, select: false }, theme: { type: String, enum: DTP_THEME_LIST, default: 'dtp-light', required: true }, stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true }, +}, { + toObject: { virtuals: true }, +}); + +UserSchema.virtual('hasAuthorPermissions').get( function ( ) { + return this.permissions.canAuthorPages || this.permissions.canAuthorPosts; +}); + +UserSchema.virtual('hasPublishPermissions').get( function ( ) { + return this.permissions.canPublishPages || this.permissions.canPublishPosts; +}); + +UserSchema.virtual('hasAuthorDashboard').get( function ( ) { + return this.permissions.canAuthorPages || + this.permissions.cahAuthorPosts || + this.permissions.canPublishPages || + this.permissions.canPublishPosts; }); module.exports = mongoose.model('User', UserSchema); diff --git a/app/services/core-node.js b/app/services/core-node.js index 1f4e4fb..249c10a 100644 --- a/app/services/core-node.js +++ b/app/services/core-node.js @@ -431,12 +431,17 @@ class CoreNodeService extends SiteService { } async getUserByLocalId (userId) { - const user = await CoreUser + const { user: userService } = this.dtp.services; + + let user = await CoreUser .findOne({ _id: userId }) .select('+flags +permissions +optIn') .populate(this.populateCoreUser) .lean(); + user.type = 'CoreUser'; + userService.decorateUserObject(user); + return user; } @@ -470,6 +475,8 @@ class CoreNodeService extends SiteService { 'permissions.canReport': settings.canReport === 'on', 'permissions.canAuthorPages': settings.canAuthorPages === 'on', 'permissions.canAuthorPosts': settings.canAuthorPosts === 'on', + 'permissions.canPublishPages': settings.canPublishPages === 'on', + 'permissions.canPublishPosts': settings.canPublishPosts === 'on', 'optIn.system': settings.optInSystem === 'on', 'optIn.marketing': settings.optInMarketing === 'on', diff --git a/app/services/page.js b/app/services/page.js index 2b04970..7433a4e 100644 --- a/app/services/page.js +++ b/app/services/page.js @@ -7,7 +7,7 @@ const striptags = require('striptags'); const slug = require('slug'); -const { SiteService } = require('../../lib/site-lib'); +const { SiteService, SiteError } = require('../../lib/site-lib'); const mongoose = require('mongoose'); const ObjectId = mongoose.Types.ObjectId; @@ -49,6 +49,10 @@ class PageService extends SiteService { } async create (author, pageDefinition) { + if (!author.permissions.canAuthorPages) { + throw new SiteError(403, 'You are not permitted to author pages'); + } + const page = new Page(); page.title = striptags(pageDefinition.title.trim()); page.slug = this.createPageSlug(page._id, page.title); @@ -69,7 +73,7 @@ class PageService extends SiteService { return page.toObject(); } - async update (page, pageDefinition) { + async update (user, page, pageDefinition) { const NOW = new Date(); const updateOp = { $set: { @@ -77,6 +81,10 @@ class PageService extends SiteService { }, }; + if (!user.permissions.canAuthorPages) { + throw new SiteError(403, 'You are not permitted to author or change pages.'); + } + if (pageDefinition.title) { updateOp.$set.title = striptags(pageDefinition.title.trim()); } @@ -95,9 +103,6 @@ class PageService extends SiteService { if (pageDefinition.content) { updateOp.$set.content = pageDefinition.content.trim(); } - if (pageDefinition.status) { - updateOp.$set.status = striptags(pageDefinition.status.trim()); - } updateOp.$set.menu = { icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()), @@ -109,6 +114,15 @@ class PageService extends SiteService { updateOp.$set.menu.parent = pageDefinition.parentPageId; } + // if old status is not published and new status is published, we have to + // verify that the calling user has Publisher privileges. + if ((page.status !== 'published') && (pageDefinition.status === 'published')) { + if (!user.permissions.canPublishPages) { + throw new SiteError(403, 'You are not permitted to publish pages'); + } + updateOp.$set.status = striptags(pageDefinition.status.trim()); + } + await Page.updateOne( { _id: page._id }, updateOp, diff --git a/app/services/post.js b/app/services/post.js index 9e3f534..8c7222d 100644 --- a/app/services/post.js +++ b/app/services/post.js @@ -42,6 +42,10 @@ class PostService extends SiteService { async createPlaceholder (author) { const NOW = new Date(); + if (!author.permissions.canAuthorPosts) { + throw new SiteError(403, 'You are not permitted to author posts'); + } + let post = new Post(); post.created = NOW; post.authorType = author.type; @@ -51,7 +55,7 @@ class PostService extends SiteService { await post.save(); post = post.toObject(); - post.author = author; // I'll populate it myself + post.author = author; // self-populate instead of calling db return post; } @@ -59,6 +63,13 @@ class PostService extends SiteService { async create (author, postDefinition) { const NOW = new Date(); + if (!author.permissions.canAuthorPosts) { + throw new SiteError(403, 'You are not permitted to author posts'); + } + if ((postDefinition.status === 'published') && !author.permisstions.canPublishPosts) { + throw new SiteError(403, 'You are not permitted to publish posts'); + } + const post = new Post(); post.created = NOW; post.authorType = author.type; @@ -78,9 +89,13 @@ class PostService extends SiteService { return post.toObject(); } - async update (post, postDefinition) { + async update (user, post, postDefinition) { const { coreNode: coreNodeService } = this.dtp.services; + if (!user.permissions.canAuthorPosts) { + throw new SiteError(403, 'You are not permitted to author posts'); + } + const NOW = new Date(); const updateOp = { $setOnInsert: { @@ -113,6 +128,12 @@ class PostService extends SiteService { if (!postDefinition.status) { throw new SiteError(406, 'Must include post status'); } + + if (post.status !== 'published' && postDefinition.status === 'published') { + if (!user.permissions.canPublishPosts) { + throw new SiteError(403, 'You are not permitted to publish posts'); + } + } updateOp.$set.status = striptags(postDefinition.status.trim()); updateOp.$set['flags.enableComments'] = postDefinition.enableComments === 'on'; @@ -149,6 +170,11 @@ class PostService extends SiteService { async updateImage (user, post, file) { const { image: imageService } = this.dtp.services; + + if (!user.permissions.canAuthorPosts) { + throw new SiteError(403, 'You are not permitted to change posts'); + } + const images = [ { width: 960, diff --git a/app/services/user.js b/app/services/user.js index 9cb5c73..98dc238 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -107,6 +107,8 @@ class UserService extends SiteService { canReport: userDefinition.canReport || true, canAuthorPosts: userDefinition.canAuthorPosts || false, canAuthorPages: userDefinition.canAuthorPages || false, + canPublishPosts: userDefinition.canPublishPosts || false, + canPublishPages: userDefinition.canPublishPages || false, }; user.optIn = { @@ -248,6 +250,8 @@ class UserService extends SiteService { 'permissions.canReport': userDefinition.canReport === 'on', 'permissions.canAuthorPages': userDefinition.canAuthorPages === 'on', 'permissions.canAuthorPosts': userDefinition.canAuthorPosts === 'on', + 'permissions.canPublishPages': userDefinition.canPublishPages === 'on', + 'permissions.canPublishPosts': userDefinition.canPublishPosts === 'on', 'optIn.system': userDefinition.optInSystem === 'on', 'optIn.marketing': userDefinition.optInMarketing === 'on', @@ -290,19 +294,22 @@ class UserService extends SiteService { const accountUsername = await this.filterUsername(accountEmail); this.log.debug('locating user record', { accountEmail, accountUsername }); - const user = await User + let user = await User .findOne({ $or: [ { email: accountEmail }, { username_lc: accountUsername }, ] }) - .select('+passwordSalt +password +flags +optIn +permissions') - .lean(); + .select('+passwordSalt +password +flags +optIn +permissions'); + if (!user) { throw new SiteError(404, 'Member credentials are invalid'); } + user = user.toObject(); + this.log.debug('user debug', { user }); + const maskedPassword = crypto.maskPassword( user.passwordSalt, account.password, @@ -407,10 +414,19 @@ class UserService extends SiteService { if (!user) { throw new SiteError(404, 'Member account not found'); } + user.type = 'User'; + this.decorateUserObject(user); + return user; } + decorateUserObject (user) { + user.hasAuthorPermissions = user.permissions.canAuthorPages || user.permissions.canAuthorPosts; + user.hasPublishPermissions = user.permissions.canPublishPages || user.permissions.canPublishPosts; + user.hasAuthorDashboard = user.hasAuthorPermissions || user.hasPublishPermissions; + } + async getUserAccounts (pagination, username) { let search = { }; if (username) { diff --git a/app/views/admin/core-user/form.pug b/app/views/admin/core-user/form.pug index 438d0e2..06009a6 100644 --- a/app/views/admin/core-user/form.pug +++ b/app/views/admin/core-user/form.pug @@ -55,6 +55,12 @@ block content label input(id="can-author-posts", name="canAuthorPosts", type="checkbox", checked= userAccount.permissions.canAuthorPosts) | Can Author Posts + label + input(id="can-publish-pages", name="canPublishPages", type="checkbox", checked= userAccount.permissions.canPublishPages) + | Can Publish Pages + label + input(id="can-publish-posts", name="canPublishPosts", type="checkbox", checked= userAccount.permissions.canPublishPosts) + | Can Publish Posts .uk-margin label.uk-form-label Opt-Ins diff --git a/app/views/admin/user/form.pug b/app/views/admin/user/form.pug index e71c2a0..d56e656 100644 --- a/app/views/admin/user/form.pug +++ b/app/views/admin/user/form.pug @@ -61,6 +61,12 @@ block content label input(id="can-author-posts", name="canAuthorPosts", type="checkbox", checked= userAccount.permissions.canAuthorPosts) | Can Author Posts + label + input(id="can-publish-pages", name="canPublishPages", type="checkbox", checked= userAccount.permissions.canPublishPages) + | Can Publish Pages + label + input(id="can-publish-posts", name="canPublishPosts", type="checkbox", checked= userAccount.permissions.canPublishPosts) + | Can Publish Posts .uk-margin label.uk-form-label Opt-Ins diff --git a/app/views/components/off-canvas.pug b/app/views/components/off-canvas.pug index 46ffe46..2e183fa 100644 --- a/app/views/components/off-canvas.pug +++ b/app/views/components/off-canvas.pug @@ -28,7 +28,7 @@ mixin renderMenuItem (iconClass, label) if user li.uk-nav-header Member Menu - if user.permissions.canAuthorPosts + if user.hasAuthorDashboard li(class={ "uk-active": (currentView === 'author') }) a(href='/author').uk-display-block div(uk-grid).uk-grid-collapse diff --git a/app/views/post/editor.pug b/app/views/post/editor.pug index aa0505c..c37f5a5 100644 --- a/app/views/post/editor.pug +++ b/app/views/post/editor.pug @@ -80,7 +80,7 @@ block viewjs 'bold italic backcolor', 'alignleft aligncenter alignright alignjustify', 'bullist numlist outdent indent removeformat', - 'link image code', + 'link image media code', 'help' ]; const pluginItems = [ diff --git a/app/views/post/view.pug b/app/views/post/view.pug index c253963..5595e75 100644 --- a/app/views/post/view.pug +++ b/app/views/post/view.pug @@ -30,10 +30,13 @@ block content div(uk-grid).uk-grid-small.uk-flex-top .uk-width-expand div #{moment(post.created).format('MMM DD, YYYY, hh:mm a')}, by #[a(href=`/user/${post.author._id}`)= post.author.displayName || post.author.username] - if user && user.permissions.canAuthorPages && post.author._id.equals(user._id) - .uk-width-auto - a(href=`/post/${post._id}/edit`) - +renderButtonIcon('fa-pen', 'edit') + if user && user.hasAuthorDashboard + .uk-width-auto= post.status + + if post.author._id.equals(user._id) + .uk-width-auto + a(href=`/post/${post._id}/edit`) + +renderButtonIcon('fa-pen', 'edit') .uk-width-auto +renderButtonIcon('fa-eye', displayIntegerValue(post.stats.totalViewCount)) .uk-width-auto