separated author and publisher permissions

master
rob 2 years ago
parent f09fc0a496
commit 30a8c3b48e

@ -54,7 +54,7 @@ class PageController extends SiteController {
async pageUpdatePage (req, res, next) { async pageUpdatePage (req, res, next) {
const { page: pageService } = this.dtp.services; const { page: pageService } = this.dtp.services;
try { try {
await pageService.update(res.locals.page, req.body); await pageService.update(req.user, res.locals.page, req.body);
res.redirect('/admin/page'); res.redirect('/admin/page');
} catch (error) { } catch (error) {
this.log.error('failed to update page', { newletterId: res.locals.page._id, error }); this.log.error('failed to update page', { newletterId: res.locals.page._id, error });

@ -195,7 +195,7 @@ class PostController extends SiteController {
if (!req.user._id.equals(res.locals.post.author._id)) { if (!req.user._id.equals(res.locals.post.author._id)) {
throw new SiteError(403, 'This is not your post'); 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}`); res.redirect(`/post/${res.locals.post.slug}`);
} catch (error) { } catch (error) {
this.log.error('failed to update post', { newletterId: res.locals.post._id, 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) { async getView (req, res, next) {
const { comment: commentService, resource: resourceService } = this.dtp.services; const { comment: commentService, resource: resourceService } = this.dtp.services;
try { 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); await resourceService.recordView(req, 'Post', res.locals.post._id);
res.locals.countPerPage = 20; res.locals.countPerPage = 20;

@ -22,6 +22,8 @@ module.exports.UserPermissionsSchema = new Schema({
canReport: { type: Boolean, default: true, required: true }, canReport: { type: Boolean, default: true, required: true },
canAuthorPages: { type: Boolean, default: false, required: true }, canAuthorPages: { type: Boolean, default: false, required: true },
canAuthorPosts: { 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({ module.exports.UserOptInSchema = new Schema({

@ -29,6 +29,23 @@ const UserSchema = new Schema({
optIn: { type: UserOptInSchema, required: true, select: false }, optIn: { type: UserOptInSchema, required: true, select: false },
theme: { type: String, enum: DTP_THEME_LIST, default: 'dtp-light', required: true }, theme: { type: String, enum: DTP_THEME_LIST, default: 'dtp-light', required: true },
stats: { type: ResourceStats, default: ResourceStatsDefaults, 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); module.exports = mongoose.model('User', UserSchema);

@ -431,12 +431,17 @@ class CoreNodeService extends SiteService {
} }
async getUserByLocalId (userId) { async getUserByLocalId (userId) {
const user = await CoreUser const { user: userService } = this.dtp.services;
let user = await CoreUser
.findOne({ _id: userId }) .findOne({ _id: userId })
.select('+flags +permissions +optIn') .select('+flags +permissions +optIn')
.populate(this.populateCoreUser) .populate(this.populateCoreUser)
.lean(); .lean();
user.type = 'CoreUser'; user.type = 'CoreUser';
userService.decorateUserObject(user);
return user; return user;
} }
@ -470,6 +475,8 @@ class CoreNodeService extends SiteService {
'permissions.canReport': settings.canReport === 'on', 'permissions.canReport': settings.canReport === 'on',
'permissions.canAuthorPages': settings.canAuthorPages === 'on', 'permissions.canAuthorPages': settings.canAuthorPages === 'on',
'permissions.canAuthorPosts': settings.canAuthorPosts === 'on', 'permissions.canAuthorPosts': settings.canAuthorPosts === 'on',
'permissions.canPublishPages': settings.canPublishPages === 'on',
'permissions.canPublishPosts': settings.canPublishPosts === 'on',
'optIn.system': settings.optInSystem === 'on', 'optIn.system': settings.optInSystem === 'on',
'optIn.marketing': settings.optInMarketing === 'on', 'optIn.marketing': settings.optInMarketing === 'on',

@ -7,7 +7,7 @@
const striptags = require('striptags'); const striptags = require('striptags');
const slug = require('slug'); const slug = require('slug');
const { SiteService } = require('../../lib/site-lib'); const { SiteService, SiteError } = require('../../lib/site-lib');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const ObjectId = mongoose.Types.ObjectId; const ObjectId = mongoose.Types.ObjectId;
@ -49,6 +49,10 @@ class PageService extends SiteService {
} }
async create (author, pageDefinition) { async create (author, pageDefinition) {
if (!author.permissions.canAuthorPages) {
throw new SiteError(403, 'You are not permitted to author pages');
}
const page = new Page(); const page = new Page();
page.title = striptags(pageDefinition.title.trim()); page.title = striptags(pageDefinition.title.trim());
page.slug = this.createPageSlug(page._id, page.title); page.slug = this.createPageSlug(page._id, page.title);
@ -69,7 +73,7 @@ class PageService extends SiteService {
return page.toObject(); return page.toObject();
} }
async update (page, pageDefinition) { async update (user, page, pageDefinition) {
const NOW = new Date(); const NOW = new Date();
const updateOp = { const updateOp = {
$set: { $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) { if (pageDefinition.title) {
updateOp.$set.title = striptags(pageDefinition.title.trim()); updateOp.$set.title = striptags(pageDefinition.title.trim());
} }
@ -95,9 +103,6 @@ class PageService extends SiteService {
if (pageDefinition.content) { if (pageDefinition.content) {
updateOp.$set.content = pageDefinition.content.trim(); updateOp.$set.content = pageDefinition.content.trim();
} }
if (pageDefinition.status) {
updateOp.$set.status = striptags(pageDefinition.status.trim());
}
updateOp.$set.menu = { updateOp.$set.menu = {
icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()), icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()),
@ -109,6 +114,15 @@ class PageService extends SiteService {
updateOp.$set.menu.parent = pageDefinition.parentPageId; 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( await Page.updateOne(
{ _id: page._id }, { _id: page._id },
updateOp, updateOp,

@ -42,6 +42,10 @@ class PostService extends SiteService {
async createPlaceholder (author) { async createPlaceholder (author) {
const NOW = new Date(); const NOW = new Date();
if (!author.permissions.canAuthorPosts) {
throw new SiteError(403, 'You are not permitted to author posts');
}
let post = new Post(); let post = new Post();
post.created = NOW; post.created = NOW;
post.authorType = author.type; post.authorType = author.type;
@ -51,7 +55,7 @@ class PostService extends SiteService {
await post.save(); await post.save();
post = post.toObject(); post = post.toObject();
post.author = author; // I'll populate it myself post.author = author; // self-populate instead of calling db
return post; return post;
} }
@ -59,6 +63,13 @@ class PostService extends SiteService {
async create (author, postDefinition) { async create (author, postDefinition) {
const NOW = new Date(); 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(); const post = new Post();
post.created = NOW; post.created = NOW;
post.authorType = author.type; post.authorType = author.type;
@ -78,9 +89,13 @@ class PostService extends SiteService {
return post.toObject(); return post.toObject();
} }
async update (post, postDefinition) { async update (user, post, postDefinition) {
const { coreNode: coreNodeService } = this.dtp.services; 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 NOW = new Date();
const updateOp = { const updateOp = {
$setOnInsert: { $setOnInsert: {
@ -113,6 +128,12 @@ class PostService extends SiteService {
if (!postDefinition.status) { if (!postDefinition.status) {
throw new SiteError(406, 'Must include post 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.status = striptags(postDefinition.status.trim());
updateOp.$set['flags.enableComments'] = postDefinition.enableComments === 'on'; updateOp.$set['flags.enableComments'] = postDefinition.enableComments === 'on';
@ -149,6 +170,11 @@ class PostService extends SiteService {
async updateImage (user, post, file) { async updateImage (user, post, file) {
const { image: imageService } = this.dtp.services; const { image: imageService } = this.dtp.services;
if (!user.permissions.canAuthorPosts) {
throw new SiteError(403, 'You are not permitted to change posts');
}
const images = [ const images = [
{ {
width: 960, width: 960,

@ -107,6 +107,8 @@ class UserService extends SiteService {
canReport: userDefinition.canReport || true, canReport: userDefinition.canReport || true,
canAuthorPosts: userDefinition.canAuthorPosts || false, canAuthorPosts: userDefinition.canAuthorPosts || false,
canAuthorPages: userDefinition.canAuthorPages || false, canAuthorPages: userDefinition.canAuthorPages || false,
canPublishPosts: userDefinition.canPublishPosts || false,
canPublishPages: userDefinition.canPublishPages || false,
}; };
user.optIn = { user.optIn = {
@ -248,6 +250,8 @@ class UserService extends SiteService {
'permissions.canReport': userDefinition.canReport === 'on', 'permissions.canReport': userDefinition.canReport === 'on',
'permissions.canAuthorPages': userDefinition.canAuthorPages === 'on', 'permissions.canAuthorPages': userDefinition.canAuthorPages === 'on',
'permissions.canAuthorPosts': userDefinition.canAuthorPosts === 'on', 'permissions.canAuthorPosts': userDefinition.canAuthorPosts === 'on',
'permissions.canPublishPages': userDefinition.canPublishPages === 'on',
'permissions.canPublishPosts': userDefinition.canPublishPosts === 'on',
'optIn.system': userDefinition.optInSystem === 'on', 'optIn.system': userDefinition.optInSystem === 'on',
'optIn.marketing': userDefinition.optInMarketing === 'on', 'optIn.marketing': userDefinition.optInMarketing === 'on',
@ -290,19 +294,22 @@ class UserService extends SiteService {
const accountUsername = await this.filterUsername(accountEmail); const accountUsername = await this.filterUsername(accountEmail);
this.log.debug('locating user record', { accountEmail, accountUsername }); this.log.debug('locating user record', { accountEmail, accountUsername });
const user = await User let user = await User
.findOne({ .findOne({
$or: [ $or: [
{ email: accountEmail }, { email: accountEmail },
{ username_lc: accountUsername }, { username_lc: accountUsername },
] ]
}) })
.select('+passwordSalt +password +flags +optIn +permissions') .select('+passwordSalt +password +flags +optIn +permissions');
.lean();
if (!user) { if (!user) {
throw new SiteError(404, 'Member credentials are invalid'); throw new SiteError(404, 'Member credentials are invalid');
} }
user = user.toObject();
this.log.debug('user debug', { user });
const maskedPassword = crypto.maskPassword( const maskedPassword = crypto.maskPassword(
user.passwordSalt, user.passwordSalt,
account.password, account.password,
@ -407,10 +414,19 @@ class UserService extends SiteService {
if (!user) { if (!user) {
throw new SiteError(404, 'Member account not found'); throw new SiteError(404, 'Member account not found');
} }
user.type = 'User'; user.type = 'User';
this.decorateUserObject(user);
return 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) { async getUserAccounts (pagination, username) {
let search = { }; let search = { };
if (username) { if (username) {

@ -55,6 +55,12 @@ block content
label label
input(id="can-author-posts", name="canAuthorPosts", type="checkbox", checked= userAccount.permissions.canAuthorPosts) input(id="can-author-posts", name="canAuthorPosts", type="checkbox", checked= userAccount.permissions.canAuthorPosts)
| Can Author Posts | 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 .uk-margin
label.uk-form-label Opt-Ins label.uk-form-label Opt-Ins

@ -61,6 +61,12 @@ block content
label label
input(id="can-author-posts", name="canAuthorPosts", type="checkbox", checked= userAccount.permissions.canAuthorPosts) input(id="can-author-posts", name="canAuthorPosts", type="checkbox", checked= userAccount.permissions.canAuthorPosts)
| Can Author Posts | 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 .uk-margin
label.uk-form-label Opt-Ins label.uk-form-label Opt-Ins

@ -28,7 +28,7 @@ mixin renderMenuItem (iconClass, label)
if user if user
li.uk-nav-header Member Menu li.uk-nav-header Member Menu
if user.permissions.canAuthorPosts if user.hasAuthorDashboard
li(class={ "uk-active": (currentView === 'author') }) li(class={ "uk-active": (currentView === 'author') })
a(href='/author').uk-display-block a(href='/author').uk-display-block
div(uk-grid).uk-grid-collapse div(uk-grid).uk-grid-collapse

@ -80,7 +80,7 @@ block viewjs
'bold italic backcolor', 'bold italic backcolor',
'alignleft aligncenter alignright alignjustify', 'alignleft aligncenter alignright alignjustify',
'bullist numlist outdent indent removeformat', 'bullist numlist outdent indent removeformat',
'link image code', 'link image media code',
'help' 'help'
]; ];
const pluginItems = [ const pluginItems = [

@ -30,10 +30,13 @@ block content
div(uk-grid).uk-grid-small.uk-flex-top div(uk-grid).uk-grid-small.uk-flex-top
.uk-width-expand .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] 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) if user && user.hasAuthorDashboard
.uk-width-auto .uk-width-auto= post.status
a(href=`/post/${post._id}/edit`)
+renderButtonIcon('fa-pen', 'edit') if post.author._id.equals(user._id)
.uk-width-auto
a(href=`/post/${post._id}/edit`)
+renderButtonIcon('fa-pen', 'edit')
.uk-width-auto .uk-width-auto
+renderButtonIcon('fa-eye', displayIntegerValue(post.stats.totalViewCount)) +renderButtonIcon('fa-eye', displayIntegerValue(post.stats.totalViewCount))
.uk-width-auto .uk-width-auto

Loading…
Cancel
Save