mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
Merged v5.22.1 into main
v5.22.1
This commit is contained in:
commit
68689917e6
8 changed files with 137 additions and 80 deletions
|
@ -4,6 +4,7 @@ export default class MembersUtilsService extends Service {
|
||||||
@service config;
|
@service config;
|
||||||
@service settings;
|
@service settings;
|
||||||
@service feature;
|
@service feature;
|
||||||
|
@service session;
|
||||||
@service store;
|
@service store;
|
||||||
|
|
||||||
paidTiers = null;
|
paidTiers = null;
|
||||||
|
@ -29,16 +30,22 @@ export default class MembersUtilsService extends Service {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// contributors don't have permissions to fetch tiers
|
||||||
|
if (this.session.user && !this.session.user.isContributor) {
|
||||||
return this.store.query('tier', {filter: 'type:paid+active:true', limit: 'all'}).then((tiers) => {
|
return this.store.query('tier', {filter: 'type:paid+active:true', limit: 'all'}).then((tiers) => {
|
||||||
this.paidTiers = tiers;
|
this.paidTiers = tiers;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async reload() {
|
async reload() {
|
||||||
|
// contributors don't have permissions to fetch tiers
|
||||||
|
if (this.session.user && !this.session.user.isContributor) {
|
||||||
return this.store.query('tier', {filter: 'type:paid+active:true', limit: 'all'}).then((tiers) => {
|
return this.store.query('tier', {filter: 'type:paid+active:true', limit: 'all'}).then((tiers) => {
|
||||||
this.paidTiers = tiers;
|
this.paidTiers = tiers;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: always use paidMembersEnabled! Only use this getter for the Stripe Connection UI.
|
* Note: always use paidMembersEnabled! Only use this getter for the Stripe Connection UI.
|
||||||
|
|
|
@ -35,17 +35,17 @@
|
||||||
data-test-post-id={{post.id}}
|
data-test-post-id={{post.id}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<li class="no-posts-box">
|
<li class="no-posts-box" data-test-no-posts-box>
|
||||||
<div class="no-posts">
|
<div class="no-posts">
|
||||||
{{#if this.showingAll}}
|
{{#if this.showingAll}}
|
||||||
{{svg-jar "posts-placeholder" class="gh-posts-placeholder"}}
|
{{svg-jar "posts-placeholder" class="gh-posts-placeholder"}}
|
||||||
<h4>Start creating content.</h4>
|
<h4>Start creating content.</h4>
|
||||||
<LinkTo @route="editor.new" @model="post" class="gh-btn gh-btn-green">
|
<LinkTo @route="editor.new" @model="post" class="gh-btn gh-btn-green" data-test-link="write-a-new-post">
|
||||||
<span>Write a new post</span>
|
<span>Write a new post</span>
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
{{else}}
|
{{else}}
|
||||||
<h4>No posts match the current filter</h4>
|
<h4>No posts match the current filter</h4>
|
||||||
<LinkTo @route="posts" @query={{hash type=null author=null tag=null}} class="gh-btn">
|
<LinkTo @route="posts" @query={{hash type=null author=null tag=null}} class="gh-btn" data-test-link="show-all">
|
||||||
<span>Show all posts</span>
|
<span>Show all posts</span>
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -2,43 +2,20 @@ import faker from 'faker';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import nql from '@tryghost/nql';
|
import nql from '@tryghost/nql';
|
||||||
import {Response} from 'miragejs';
|
import {Response} from 'miragejs';
|
||||||
import {extractFilterParam, paginateModelCollection} from '../utils';
|
import {
|
||||||
|
extractFilterParam,
|
||||||
|
paginateModelCollection,
|
||||||
|
withPermissionsCheck
|
||||||
|
} from '../utils';
|
||||||
import {underscore} from '@ember/string';
|
import {underscore} from '@ember/string';
|
||||||
|
|
||||||
function hasInvalidPermissions() {
|
const ALLOWED_ROLES = [
|
||||||
const {schema, request} = this;
|
'Owner',
|
||||||
|
'Administrator'
|
||||||
// always allow dev requests through - the logged in user will be real so
|
];
|
||||||
// we can't check against it in the mocked db
|
|
||||||
if (!request.requestHeaders['X-Test-User']) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const invalidPermsResponse = new Response(403, {}, {
|
|
||||||
errors: [{
|
|
||||||
type: 'NoPermissionError',
|
|
||||||
message: 'You do not have permission to perform this action'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = schema.users.find(request.requestHeaders['X-Test-User']);
|
|
||||||
const adminRoles = user.roles.filter(role => ['Owner', 'Administrator'].includes(role.name));
|
|
||||||
|
|
||||||
if (adminRoles.length === 0) {
|
|
||||||
return invalidPermsResponse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function withPermissionsCheck(fn) {
|
|
||||||
return function () {
|
|
||||||
const boundPermsCheck = hasInvalidPermissions.bind(this);
|
|
||||||
const boundFn = fn.bind(this);
|
|
||||||
return boundPermsCheck() || boundFn(...arguments);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mockMembersStats(server) {
|
export function mockMembersStats(server) {
|
||||||
server.get('/members/stats/count', withPermissionsCheck(function (db, {queryParams}) {
|
server.get('/members/stats/count', withPermissionsCheck(ALLOWED_ROLES, function (db, {queryParams}) {
|
||||||
let {days} = queryParams;
|
let {days} = queryParams;
|
||||||
|
|
||||||
let firstSubscriberDays = faker.datatype.number({min: 30, max: 600});
|
let firstSubscriberDays = faker.datatype.number({min: 30, max: 600});
|
||||||
|
@ -94,12 +71,12 @@ export function mockMembersStats(server) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function mockMembers(server) {
|
export default function mockMembers(server) {
|
||||||
server.post('/members/', withPermissionsCheck(function ({members}) {
|
server.post('/members/', withPermissionsCheck(ALLOWED_ROLES, function ({members}) {
|
||||||
const attrs = this.normalizedRequestAttrs();
|
const attrs = this.normalizedRequestAttrs();
|
||||||
return members.create(attrs);
|
return members.create(attrs);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.get('/members/', withPermissionsCheck(function ({members}, {queryParams}) {
|
server.get('/members/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, {queryParams}) {
|
||||||
let {filter, search, page, limit} = queryParams;
|
let {filter, search, page, limit} = queryParams;
|
||||||
|
|
||||||
page = +page || 1;
|
page = +page || 1;
|
||||||
|
@ -164,7 +141,7 @@ export default function mockMembers(server) {
|
||||||
return paginateModelCollection('members', collection, page, limit);
|
return paginateModelCollection('members', collection, page, limit);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.del('/members/', withPermissionsCheck(function ({members}, {queryParams}) {
|
server.del('/members/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, {queryParams}) {
|
||||||
if (!queryParams.filter && !queryParams.search && queryParams.all !== 'true') {
|
if (!queryParams.filter && !queryParams.search && queryParams.all !== 'true') {
|
||||||
return new Response(422, {}, {errors: [{
|
return new Response(422, {}, {errors: [{
|
||||||
type: 'IncorrectUsageError',
|
type: 'IncorrectUsageError',
|
||||||
|
@ -200,7 +177,7 @@ export default function mockMembers(server) {
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.get('/members/:id/', withPermissionsCheck(function ({members}, {params}) {
|
server.get('/members/:id/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, {params}) {
|
||||||
let {id} = params;
|
let {id} = params;
|
||||||
let member = members.find(id);
|
let member = members.find(id);
|
||||||
|
|
||||||
|
@ -212,7 +189,7 @@ export default function mockMembers(server) {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.put('/members/:id/', withPermissionsCheck(function ({members, tiers, subscriptions}, {params}) {
|
server.put('/members/:id/', withPermissionsCheck(ALLOWED_ROLES, function ({members, tiers, subscriptions}, {params}) {
|
||||||
const attrs = this.normalizedRequestAttrs();
|
const attrs = this.normalizedRequestAttrs();
|
||||||
const member = members.find(params.id);
|
const member = members.find(params.id);
|
||||||
|
|
||||||
|
@ -282,12 +259,12 @@ export default function mockMembers(server) {
|
||||||
return member.update(attrs);
|
return member.update(attrs);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.del('/members/:id/', withPermissionsCheck(function ({members}, request) {
|
server.del('/members/:id/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, request) {
|
||||||
const id = request.params.id;
|
const id = request.params.id;
|
||||||
members.find(id).destroy();
|
members.find(id).destroy();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.get('/members/upload/', withPermissionsCheck(function () {
|
server.get('/members/upload/', withPermissionsCheck(ALLOWED_ROLES, function () {
|
||||||
return new Response(200, {
|
return new Response(200, {
|
||||||
'Content-Disposition': 'attachment',
|
'Content-Disposition': 'attachment',
|
||||||
filename: `members.${moment().format('YYYY-MM-DD')}.csv`,
|
filename: `members.${moment().format('YYYY-MM-DD')}.csv`,
|
||||||
|
@ -295,7 +272,7 @@ export default function mockMembers(server) {
|
||||||
}, '');
|
}, '');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.post('/members/upload/', withPermissionsCheck(function ({labels}, request) {
|
server.post('/members/upload/', withPermissionsCheck(ALLOWED_ROLES, function ({labels}, request) {
|
||||||
const label = labels.create();
|
const label = labels.create();
|
||||||
|
|
||||||
// TODO: parse CSV and create member records
|
// TODO: parse CSV and create member records
|
||||||
|
@ -312,7 +289,7 @@ export default function mockMembers(server) {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.get('/members/events/', withPermissionsCheck(function ({memberActivityEvents}, {queryParams}) {
|
server.get('/members/events/', withPermissionsCheck(ALLOWED_ROLES, function ({memberActivityEvents}, {queryParams}) {
|
||||||
let {limit} = queryParams;
|
let {limit} = queryParams;
|
||||||
|
|
||||||
limit = +limit || 15;
|
limit = +limit || 15;
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
import {paginatedResponse} from '../utils';
|
import {paginatedResponse, withPermissionsCheck} from '../utils';
|
||||||
|
|
||||||
|
const ALLOWED_WRITE_ROLES = [
|
||||||
|
'Owner',
|
||||||
|
'Administrator'
|
||||||
|
];
|
||||||
|
const ALLOWED_READ_ROLES = [
|
||||||
|
'Owner',
|
||||||
|
'Administrator',
|
||||||
|
'Editor',
|
||||||
|
'Author'
|
||||||
|
];
|
||||||
|
|
||||||
export default function mockTiers(server) {
|
export default function mockTiers(server) {
|
||||||
server.post('/tiers/');
|
// CREATE
|
||||||
|
server.post('/tiers/', withPermissionsCheck(ALLOWED_WRITE_ROLES, function ({tiers}) {
|
||||||
|
const attrs = this.normalizedRequestAttrs();
|
||||||
|
return tiers.create(attrs);
|
||||||
|
}));
|
||||||
|
|
||||||
server.get('/tiers/', paginatedResponse('tiers'));
|
// READ
|
||||||
|
server.get('/tiers/', withPermissionsCheck(ALLOWED_READ_ROLES, paginatedResponse('tiers')));
|
||||||
|
|
||||||
server.get('/tiers/:id/', function ({tiers}, {params}) {
|
server.get('/tiers/:id/', withPermissionsCheck(ALLOWED_READ_ROLES, function ({tiers}, {params}) {
|
||||||
let {id} = params;
|
let {id} = params;
|
||||||
let tier = tiers.find(id);
|
let tier = tiers.find(id);
|
||||||
|
|
||||||
|
@ -15,16 +31,21 @@ export default function mockTiers(server) {
|
||||||
message: 'Tier not found.'
|
message: 'Tier not found.'
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
server.put('/tiers/:id/', function ({tiers}, {params}) {
|
// UPDATE
|
||||||
|
server.put('/tiers/:id/', withPermissionsCheck(ALLOWED_WRITE_ROLES, function ({tiers}, {params}) {
|
||||||
const attrs = this.normalizedRequestAttrs();
|
const attrs = this.normalizedRequestAttrs();
|
||||||
const tier = tiers.find(params.id);
|
const tier = tiers.find(params.id);
|
||||||
|
|
||||||
tier.update(attrs);
|
tier.update(attrs);
|
||||||
|
|
||||||
return tier.save();
|
return tier.save();
|
||||||
});
|
}));
|
||||||
|
|
||||||
server.del('/tiers/:id/');
|
// DELETE
|
||||||
|
server.del('/tiers/:id/', withPermissionsCheck(ALLOWED_WRITE_ROLES, function (schema, request) {
|
||||||
|
const id = request.params.id;
|
||||||
|
schema.tiers.find(id).destroy();
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,3 +122,35 @@ export function extractFilterParam(param, filter = '') {
|
||||||
|
|
||||||
return normalizeBooleanParams(normalizeStringParams(match));
|
return normalizeBooleanParams(normalizeStringParams(match));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasInvalidPermissions(allowedRoles) {
|
||||||
|
const {schema, request} = this;
|
||||||
|
|
||||||
|
// always allow dev requests through - the logged in user will be real so
|
||||||
|
// we can't check against it in the mocked db
|
||||||
|
if (!request.requestHeaders['X-Test-User']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidPermsResponse = new Response(403, {}, {
|
||||||
|
errors: [{
|
||||||
|
type: 'NoPermissionError',
|
||||||
|
message: 'You do not have permission to perform this action'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = schema.users.find(request.requestHeaders['X-Test-User']);
|
||||||
|
const adminRoles = user.roles.filter(role => allowedRoles.includes(role.name));
|
||||||
|
|
||||||
|
if (adminRoles.length === 0) {
|
||||||
|
return invalidPermsResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withPermissionsCheck(allowedRoles, fn) {
|
||||||
|
return function () {
|
||||||
|
const boundPermsCheck = hasInvalidPermissions.bind(this);
|
||||||
|
const boundFn = fn.bind(this);
|
||||||
|
return boundPermsCheck(allowedRoles) || boundFn(...arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ghost-admin",
|
"name": "ghost-admin",
|
||||||
"version": "5.22.0",
|
"version": "5.22.1",
|
||||||
"description": "Ember.js admin client for Ghost",
|
"description": "Ember.js admin client for Ghost",
|
||||||
"author": "Ghost Foundation",
|
"author": "Ghost Foundation",
|
||||||
"homepage": "http://ghost.org",
|
"homepage": "http://ghost.org",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||||
import {beforeEach, describe, it} from 'mocha';
|
import {beforeEach, describe, it} from 'mocha';
|
||||||
import {click, currentURL, fillIn, find, findAll, settled, visit} from '@ember/test-helpers';
|
import {blur, click, currentURL, fillIn, find, findAll, settled, visit} from '@ember/test-helpers';
|
||||||
import {clickTrigger, selectChoose} from 'ember-power-select/test-support/helpers';
|
import {clickTrigger, selectChoose} from 'ember-power-select/test-support/helpers';
|
||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import {setupApplicationTest} from 'ember-mocha';
|
import {setupApplicationTest} from 'ember-mocha';
|
||||||
|
@ -247,13 +247,33 @@ describe('Acceptance: Content', function () {
|
||||||
|
|
||||||
describe('as contributor', function () {
|
describe('as contributor', function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
let adminRole = this.server.create('role', {name: 'Administrator'});
|
let contributorRole = this.server.create('role', {name: 'Contributor'});
|
||||||
let admin = this.server.create('user', {roles: [adminRole]});
|
this.server.create('user', {roles: [contributorRole]});
|
||||||
|
|
||||||
// Create posts
|
|
||||||
this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
|
|
||||||
|
|
||||||
return await authenticateSession();
|
return await authenticateSession();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows posts list and allows post creation', async function () {
|
||||||
|
await visit('/posts');
|
||||||
|
|
||||||
|
// has an empty state
|
||||||
|
expect(findAll('[data-test-post-id]')).to.have.length(0);
|
||||||
|
expect(find('[data-test-no-posts-box]')).to.exist;
|
||||||
|
expect(find('[data-test-link="write-a-new-post"]')).to.exist;
|
||||||
|
|
||||||
|
await click('[data-test-link="write-a-new-post"]');
|
||||||
|
|
||||||
|
expect(currentURL()).to.equal('/editor/post');
|
||||||
|
|
||||||
|
await fillIn('[data-test-editor-title-input]', 'First contributor post');
|
||||||
|
await blur('[data-test-editor-title-input]');
|
||||||
|
|
||||||
|
expect(currentURL()).to.equal('/editor/post/1');
|
||||||
|
|
||||||
|
await click('[data-test-link="posts"]');
|
||||||
|
|
||||||
|
expect(findAll('[data-test-post-id]')).to.have.length(1);
|
||||||
|
expect(find('[data-test-no-posts-box]')).to.not.exist;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ghost",
|
"name": "ghost",
|
||||||
"version": "5.22.0",
|
"version": "5.22.1",
|
||||||
"description": "The professional publishing platform",
|
"description": "The professional publishing platform",
|
||||||
"author": "Ghost Foundation",
|
"author": "Ghost Foundation",
|
||||||
"homepage": "https://ghost.org",
|
"homepage": "https://ghost.org",
|
||||||
|
|
Loading…
Reference in a new issue