mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 05:50:35 +03:00
353cad7ed2
refs568e4183e3
refs258f56ded9
- when in test environment add a `X-Test-User` header to API requests that allows the mirage endpoints to check the logged in user without having to cross boundaries into the application or test contexts
329 lines
11 KiB
JavaScript
329 lines
11 KiB
JavaScript
import faker from 'faker';
|
|
import moment from 'moment-timezone';
|
|
import nql from '@tryghost/nql';
|
|
import {Response} from 'miragejs';
|
|
import {extractFilterParam, paginateModelCollection} from '../utils';
|
|
import {underscore} from '@ember/string';
|
|
|
|
function hasInvalidPermissions() {
|
|
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 => ['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) {
|
|
server.get('/members/stats/count', withPermissionsCheck(function (db, {queryParams}) {
|
|
let {days} = queryParams;
|
|
|
|
let firstSubscriberDays = faker.datatype.number({min: 30, max: 600});
|
|
|
|
if (days === 'all-time') {
|
|
days = firstSubscriberDays;
|
|
} else {
|
|
days = Number(days);
|
|
}
|
|
|
|
let total = 0;
|
|
if (firstSubscriberDays > days) {
|
|
total += faker.datatype.number({max: 1000});
|
|
}
|
|
|
|
// simulate sql GROUP BY where days with 0 subscribers are missing
|
|
let dateCounts = {};
|
|
let i = 0;
|
|
while (i < days) {
|
|
let date = moment().subtract(i, 'days').format('YYYY-MM-DD');
|
|
let count = faker.datatype.number({min: 0, max: 30});
|
|
|
|
if (count !== 0) {
|
|
dateCounts[date] = count;
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
|
|
// similar to what we'll need to do on the server
|
|
let totalOnDate = {};
|
|
let j = days - 1;
|
|
while (j >= 0) {
|
|
let date = moment().subtract(j, 'days').format('YYYY-MM-DD');
|
|
totalOnDate[date] = total + (dateCounts[date] || 0);
|
|
total += (dateCounts[date] || 0);
|
|
j -= 1;
|
|
}
|
|
|
|
return {
|
|
total,
|
|
resource: 'members',
|
|
data: Object.keys(totalOnDate).map((key, idx, arr) => {
|
|
return {
|
|
date: key,
|
|
free: arr[key],
|
|
paid: 0,
|
|
comped: 0
|
|
};
|
|
})
|
|
};
|
|
}));
|
|
}
|
|
|
|
export default function mockMembers(server) {
|
|
server.post('/members/', withPermissionsCheck(function ({members}) {
|
|
const attrs = this.normalizedRequestAttrs();
|
|
return members.create(attrs);
|
|
}));
|
|
|
|
server.get('/members/', withPermissionsCheck(function ({members}, {queryParams}) {
|
|
let {filter, search, page, limit} = queryParams;
|
|
|
|
page = +page || 1;
|
|
limit = +limit || 15;
|
|
|
|
let collection = members.all();
|
|
|
|
if (filter) {
|
|
try {
|
|
const nqlFilter = nql(filter, {
|
|
expansions: [
|
|
{
|
|
key: 'label',
|
|
replacement: 'labels.slug'
|
|
},
|
|
{
|
|
key: 'tier',
|
|
replacement: 'tiers.slug'
|
|
}
|
|
]
|
|
});
|
|
|
|
collection = collection.filter((member) => {
|
|
const serializedMember = {};
|
|
|
|
// mirage model keys match our main model keys so we need to transform
|
|
// camelCase to underscore to match the filter format
|
|
Object.keys(member.attrs).forEach((key) => {
|
|
serializedMember[underscore(key)] = member.attrs[key];
|
|
});
|
|
|
|
// similar deal for associated models
|
|
['labels', 'tiers', 'subscriptions', 'newsletters'].forEach((association) => {
|
|
serializedMember[association] = [];
|
|
|
|
member[association].models.forEach((associatedModel) => {
|
|
const serializedAssociation = {};
|
|
Object.keys(associatedModel.attrs).forEach((key) => {
|
|
serializedAssociation[underscore(key)] = associatedModel.attrs[key];
|
|
});
|
|
serializedMember[association].push(serializedAssociation);
|
|
});
|
|
});
|
|
|
|
return nqlFilter.queryJSON(serializedMember);
|
|
});
|
|
} catch (err) {
|
|
console.error(err); // eslint-disable-line
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
if (search) {
|
|
const query = search.toLowerCase();
|
|
|
|
collection = collection.filter((member) => {
|
|
return member.name.toLowerCase().indexOf(query) !== -1
|
|
|| member.email.toLowerCase().indexOf(query) !== -1;
|
|
});
|
|
}
|
|
|
|
return paginateModelCollection('members', collection, page, limit);
|
|
}));
|
|
|
|
server.del('/members/', withPermissionsCheck(function ({members}, {queryParams}) {
|
|
if (!queryParams.filter && !queryParams.search && queryParams.all !== 'true') {
|
|
return new Response(422, {}, {errors: [{
|
|
type: 'IncorrectUsageError',
|
|
message: 'DELETE /members/ must be used with a filter, search, or all=true query parameter'
|
|
}]});
|
|
}
|
|
|
|
let membersToDelete = members.all();
|
|
|
|
if (queryParams.filter) {
|
|
let labelFilter = extractFilterParam('label', queryParams.filter);
|
|
|
|
membersToDelete = membersToDelete.filter((member) => {
|
|
let matches = false;
|
|
labelFilter.forEach((slug) => {
|
|
if (member.labels.models.find(l => l.slug === slug)) {
|
|
matches = true;
|
|
}
|
|
});
|
|
return matches;
|
|
});
|
|
}
|
|
|
|
let count = membersToDelete.length;
|
|
membersToDelete.destroy();
|
|
|
|
return {
|
|
meta: {
|
|
stats: {
|
|
successful: count
|
|
}
|
|
}
|
|
};
|
|
}));
|
|
|
|
server.get('/members/:id/', withPermissionsCheck(function ({members}, {params}) {
|
|
let {id} = params;
|
|
let member = members.find(id);
|
|
|
|
return member || new Response(404, {}, {
|
|
errors: [{
|
|
type: 'NotFoundError',
|
|
message: 'Member not found.'
|
|
}]
|
|
});
|
|
}));
|
|
|
|
server.put('/members/:id/', withPermissionsCheck(function ({members, tiers, subscriptions}, {params}) {
|
|
const attrs = this.normalizedRequestAttrs();
|
|
const member = members.find(params.id);
|
|
|
|
// API accepts `tiers: [{id: 'x'}]` which isn't handled natively by mirage
|
|
if (attrs.tiers.length > 0) {
|
|
attrs.tiers.forEach((p) => {
|
|
const tier = tiers.find(p.id);
|
|
|
|
if (!member.tiers.includes(tier)) {
|
|
// TODO: serialize tiers through _active_ subscriptions
|
|
member.tiers.add(tier);
|
|
|
|
subscriptions.create({
|
|
member,
|
|
tier,
|
|
comped: true,
|
|
plan: {
|
|
id: '',
|
|
nickname: 'Complimentary',
|
|
interval: 'year',
|
|
currency: 'USD',
|
|
amount: 0
|
|
},
|
|
status: 'active',
|
|
startDate: moment().toISOString(),
|
|
defaultPaymentCardLast4: '****',
|
|
cancelAtPeriodEnd: false,
|
|
cancellationReason: null,
|
|
currentPeriodEnd: moment().add(1, 'year').toISOString(),
|
|
price: {
|
|
id: '',
|
|
price_id: '',
|
|
nickname: 'Complimentary',
|
|
amount: 0,
|
|
interval: 'year',
|
|
type: 'recurring',
|
|
currency: 'USD',
|
|
tier: {
|
|
id: '',
|
|
tier_id: tier.id
|
|
}
|
|
},
|
|
offer: null
|
|
});
|
|
|
|
member.save();
|
|
}
|
|
});
|
|
}
|
|
|
|
const tierIds = (attrs.tiers || []).map(p => p.id);
|
|
|
|
member.tiers.models.forEach((tier) => {
|
|
if (!tierIds.includes(tier.id)) {
|
|
member.subscriptions.models.filter(sub => sub.tier.id === tier.id).forEach((sub) => {
|
|
member.subscriptions.remove(sub);
|
|
});
|
|
|
|
member.tiers.remove(tier);
|
|
}
|
|
});
|
|
|
|
// these are read-only properties so make sure we don't overwrite data
|
|
delete attrs.tiers;
|
|
delete attrs.subscriptions;
|
|
|
|
return member.update(attrs);
|
|
}));
|
|
|
|
server.del('/members/:id/', withPermissionsCheck(function ({members}, request) {
|
|
const id = request.params.id;
|
|
members.find(id).destroy();
|
|
}));
|
|
|
|
server.get('/members/upload/', withPermissionsCheck(function () {
|
|
return new Response(200, {
|
|
'Content-Disposition': 'attachment',
|
|
filename: `members.${moment().format('YYYY-MM-DD')}.csv`,
|
|
'Content-Type': 'text/csv'
|
|
}, '');
|
|
}));
|
|
|
|
server.post('/members/upload/', withPermissionsCheck(function ({labels}, request) {
|
|
const label = labels.create();
|
|
|
|
// TODO: parse CSV and create member records
|
|
for (const kvPair of request.requestBody.entries()) {
|
|
const [key, value] = kvPair;
|
|
console.log({key, value}); // eslint-disable-line
|
|
}
|
|
|
|
return new Response(201, {}, {
|
|
meta: {
|
|
import_label: label,
|
|
stats: {imported: 1, invalid: []}
|
|
}
|
|
});
|
|
}));
|
|
|
|
server.get('/members/events/', withPermissionsCheck(function ({memberActivityEvents}, {queryParams}) {
|
|
let {limit} = queryParams;
|
|
|
|
limit = +limit || 15;
|
|
|
|
let collection = memberActivityEvents.all().sort((a, b) => {
|
|
return (new Date(a.createdAt)) - (new Date(b.createdAt));
|
|
}).slice(0, limit);
|
|
|
|
return collection;
|
|
}));
|
|
|
|
mockMembersStats(server);
|
|
}
|