mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 02:41:50 +03:00
🔥 Removed versioned APIs
refs: https://github.com/TryGhost/Toolbox/issues/229 - we are getting rid of the concept of having multiple api versions in a single ghost install - removed all the code for multiple api versions & left canary wired up, but without the version in the URL - TODO: reorganise the folders so there's no canary folder when we're closer to shipping we need to minimise the pain of merging changes across from main for now
This commit is contained in:
parent
d7ff49dc88
commit
a4a9ba7940
@ -26,8 +26,6 @@
|
|||||||
"!core/server/web/parent/middleware/**",
|
"!core/server/web/parent/middleware/**",
|
||||||
"core/server/web/shared/**",
|
"core/server/web/shared/**",
|
||||||
"!core/server/web/shared/middleware/**",
|
"!core/server/web/shared/middleware/**",
|
||||||
"core/server/api/v2/**",
|
|
||||||
"core/server/api/v3/**",
|
|
||||||
"core/server/api/canary/**",
|
"core/server/api/canary/**",
|
||||||
"!core/server/api/canary/utils"
|
"!core/server/api/canary/utils"
|
||||||
]
|
]
|
||||||
|
@ -39,7 +39,7 @@ module.exports = {
|
|||||||
settingsCache: settingsCache,
|
settingsCache: settingsCache,
|
||||||
|
|
||||||
// TODO: Expose less of the API to make this safe
|
// TODO: Expose less of the API to make this safe
|
||||||
api: require('../../server/api').canary,
|
api: require('../../server/api').endpoints,
|
||||||
|
|
||||||
// Labs utils for enabling/disabling helpers
|
// Labs utils for enabling/disabling helpers
|
||||||
labs: require('../../shared/labs'),
|
labs: require('../../shared/labs'),
|
||||||
|
@ -9,7 +9,7 @@ const getSchedulerIntegration = require('./scheduler-intergation');
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
const loadScheduledResources = async function () {
|
const loadScheduledResources = async function () {
|
||||||
const api = require('../../../api');
|
const api = require('../../../api').endpoints;
|
||||||
const SCHEDULED_RESOURCES = ['post', 'page'];
|
const SCHEDULED_RESOURCES = ['post', 'page'];
|
||||||
|
|
||||||
// Fetches all scheduled resources(posts/pages) with default API
|
// Fetches all scheduled resources(posts/pages) with default API
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
# API Versioning
|
# API
|
||||||
|
|
||||||
Ghost supports multiple API versions.
|
|
||||||
Each version lives in a separate folder e.g. api/v2, api/v3, api/canary etc.
|
|
||||||
Next to the API folders there is a shared folder, which contains shared code, which all API versions use.
|
|
||||||
|
|
||||||
## Stages
|
## Stages
|
||||||
|
|
||||||
@ -19,7 +15,7 @@ The framework we are building pipes a request through these stages in respect of
|
|||||||
|
|
||||||
## Frame
|
## Frame
|
||||||
|
|
||||||
Is a class, which holds all the information for request processing. We pass this instance by reference.
|
Is a class, which holds all the information for request processing. We pass this instance by reference.
|
||||||
Each function can modify the original instance. No need to return the class instance.
|
Each function can modify the original instance. No need to return the class instance.
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
@ -8,10 +8,6 @@ const localUtils = require('./utils');
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
get http() {
|
|
||||||
return shared.http;
|
|
||||||
},
|
|
||||||
|
|
||||||
get authentication() {
|
get authentication() {
|
||||||
return shared.pipeline(require('./authentication'), localUtils);
|
return shared.pipeline(require('./authentication'), localUtils);
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
const defaultAPI = require('./canary');
|
module.exports.endpoints = require('./canary');
|
||||||
|
|
||||||
module.exports = defaultAPI;
|
|
||||||
module.exports.canary = defaultAPI;
|
|
||||||
module.exports.v4 = defaultAPI;
|
|
||||||
module.exports.v3 = require('./v3');
|
|
||||||
module.exports.v2 = require('./v2');
|
|
||||||
|
|
||||||
module.exports.shared = require('./shared');
|
module.exports.shared = require('./shared');
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
const models = require('../../models');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'actions',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'page',
|
|
||||||
'limit',
|
|
||||||
'fields'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'id',
|
|
||||||
'type'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
required: true,
|
|
||||||
values: ['resource', 'actor']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
if (frame.data.type === 'resource') {
|
|
||||||
frame.options.withRelated = ['actor'];
|
|
||||||
frame.options.filter = `resource_id:${frame.data.id}`;
|
|
||||||
} else {
|
|
||||||
frame.options.withRelated = ['resource'];
|
|
||||||
frame.options.filter = `actor_id:${frame.data.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return models.Action.findPage(frame.options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,191 +0,0 @@
|
|||||||
const api = require('./index');
|
|
||||||
const config = require('../../../shared/config');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const web = require('../../web');
|
|
||||||
const models = require('../../models');
|
|
||||||
const auth = require('../../services/auth');
|
|
||||||
const invitations = require('../../services/invitations');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
notTheBlogOwner: 'You are not the site owner.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'authentication',
|
|
||||||
|
|
||||||
setup: {
|
|
||||||
statusCode: 201,
|
|
||||||
permissions: false,
|
|
||||||
validation: {
|
|
||||||
docName: 'setup'
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(false)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
const setupDetails = {
|
|
||||||
name: frame.data.setup[0].name,
|
|
||||||
email: frame.data.setup[0].email,
|
|
||||||
password: frame.data.setup[0].password,
|
|
||||||
blogTitle: frame.data.setup[0].blogTitle,
|
|
||||||
status: 'active'
|
|
||||||
};
|
|
||||||
|
|
||||||
return auth.setup.setupUser(setupDetails);
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
return auth.setup.doSettings(data, api.settings);
|
|
||||||
})
|
|
||||||
.then((user) => {
|
|
||||||
return auth.setup.sendWelcomeEmail(user.get('email'), api.mail)
|
|
||||||
.then(() => user);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSetup: {
|
|
||||||
permissions: (frame) => {
|
|
||||||
return models.User.findOne({role: 'Owner', status: 'all'})
|
|
||||||
.then((owner) => {
|
|
||||||
if (owner.id !== frame.options.context.user) {
|
|
||||||
throw new errors.NoPermissionError({
|
|
||||||
message: tpl(messages.notTheBlogOwner)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
validation: {
|
|
||||||
docName: 'setup'
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(true)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
const setupDetails = {
|
|
||||||
name: frame.data.setup[0].name,
|
|
||||||
email: frame.data.setup[0].email,
|
|
||||||
password: frame.data.setup[0].password,
|
|
||||||
blogTitle: frame.data.setup[0].blogTitle,
|
|
||||||
status: 'active'
|
|
||||||
};
|
|
||||||
|
|
||||||
return auth.setup.setupUser(setupDetails);
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
return auth.setup.doSettings(data, api.settings);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isSetup: {
|
|
||||||
permissions: false,
|
|
||||||
async query() {
|
|
||||||
const isSetup = await auth.setup.checkIsSetup();
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: isSetup,
|
|
||||||
title: config.title,
|
|
||||||
name: config.user_name,
|
|
||||||
email: config.user_email
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
generateResetToken: {
|
|
||||||
validation: {
|
|
||||||
docName: 'passwordreset'
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
options: [
|
|
||||||
'email'
|
|
||||||
],
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(true)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return auth.passwordreset.generateToken(frame.data.passwordreset[0].email, api.settings);
|
|
||||||
})
|
|
||||||
.then((token) => {
|
|
||||||
return auth.passwordreset.sendResetNotification(token, api.mail);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
resetPassword: {
|
|
||||||
validation: {
|
|
||||||
docName: 'passwordreset',
|
|
||||||
data: {
|
|
||||||
newPassword: {required: true},
|
|
||||||
ne2Password: {required: true}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: false,
|
|
||||||
options: [
|
|
||||||
'ip'
|
|
||||||
],
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(true)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return auth.passwordreset.extractTokenParts(frame);
|
|
||||||
})
|
|
||||||
.then((params) => {
|
|
||||||
return auth.passwordreset.protectBruteForce(params);
|
|
||||||
})
|
|
||||||
.then(({options, tokenParts}) => {
|
|
||||||
options = Object.assign(options, {context: {internal: true}});
|
|
||||||
return auth.passwordreset.doReset(options, tokenParts, api.settings)
|
|
||||||
.then((params) => {
|
|
||||||
web.shared.middleware.api.spamPrevention.userLogin().reset(frame.options.ip, `${tokenParts.email}login`);
|
|
||||||
return params;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
acceptInvitation: {
|
|
||||||
validation: {
|
|
||||||
docName: 'invitations'
|
|
||||||
},
|
|
||||||
permissions: false,
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(true)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return invitations.accept(frame.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isInvitation: {
|
|
||||||
data: [
|
|
||||||
'email'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
docName: 'invitations'
|
|
||||||
},
|
|
||||||
permissions: false,
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(true)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
const email = frame.data.email;
|
|
||||||
|
|
||||||
return models.Invite.findOne({email: email, status: 'sent'}, frame.options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,69 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const models = require('../../models');
|
|
||||||
const ALLOWED_INCLUDES = ['count.posts'];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
authorsNotFound: 'Author not found.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'authors',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'limit',
|
|
||||||
'order',
|
|
||||||
'page'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Author.findPage(frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'id',
|
|
||||||
'slug',
|
|
||||||
'email',
|
|
||||||
'role'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Author.findOne(frame.data, frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.authorsNotFound)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,12 +0,0 @@
|
|||||||
const publicConfig = require('../../services/public-config');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'config',
|
|
||||||
|
|
||||||
read: {
|
|
||||||
permissions: false,
|
|
||||||
query() {
|
|
||||||
return publicConfig.config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,120 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const dbBackup = require('../../data/db/backup');
|
|
||||||
const exporter = require('../../data/exporter');
|
|
||||||
const importer = require('../../data/importer');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const models = require('../../models');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'db',
|
|
||||||
|
|
||||||
backupContent: {
|
|
||||||
permissions: true,
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filename'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: exporter.BACKUP_TABLES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
// NOTE: we need to have `include` property available as backupDatabase uses it internally
|
|
||||||
Object.assign(frame.options, {include: frame.options.withRelated});
|
|
||||||
|
|
||||||
return dbBackup.backup(frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
exportContent: {
|
|
||||||
options: [
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: exporter.BACKUP_TABLES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
disposition: {
|
|
||||||
type: 'file',
|
|
||||||
value: () => (exporter.fileName())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => exporter.doExport({include: frame.options.withRelated}))
|
|
||||||
.catch((err) => {
|
|
||||||
return Promise.reject(new errors.InternalServerError({err: err}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
importContent: {
|
|
||||||
options: [
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: exporter.BACKUP_TABLES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return importer.importFromFile(frame.file, {include: frame.options.withRelated});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteAllContent: {
|
|
||||||
statusCode: 204,
|
|
||||||
permissions: true,
|
|
||||||
query() {
|
|
||||||
/**
|
|
||||||
* @NOTE:
|
|
||||||
* We fetch all posts with `columns:id` to increase the speed of this endpoint.
|
|
||||||
* And if you trigger `post.destroy(..)`, this will trigger bookshelf and model events.
|
|
||||||
* But we only have to `id` available in the model. This won't work, because:
|
|
||||||
* - model layer can't trigger event e.g. `post.page` to trigger `post|page.unpublished`.
|
|
||||||
* - `onDestroyed` or `onDestroying` can contain custom logic
|
|
||||||
*/
|
|
||||||
function deleteContent() {
|
|
||||||
return models.Base.transaction((transacting) => {
|
|
||||||
const queryOpts = {
|
|
||||||
columns: 'id',
|
|
||||||
context: {internal: true},
|
|
||||||
destroyAll: true,
|
|
||||||
transacting: transacting
|
|
||||||
};
|
|
||||||
|
|
||||||
return models.Post.findAll(queryOpts)
|
|
||||||
.then((response) => {
|
|
||||||
return Promise.map(response.models, (post) => {
|
|
||||||
return models.Post.destroy(Object.assign({id: post.id}, queryOpts));
|
|
||||||
}, {concurrency: 100});
|
|
||||||
})
|
|
||||||
.then(() => models.Tag.findAll(queryOpts))
|
|
||||||
.then((response) => {
|
|
||||||
return Promise.map(response.models, (tag) => {
|
|
||||||
return models.Tag.destroy(Object.assign({id: tag.id}, queryOpts));
|
|
||||||
}, {concurrency: 100});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
throw new errors.InternalServerError({
|
|
||||||
err: err
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbBackup.backup().then(deleteContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,20 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const storage = require('../../adapters/storage');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'images',
|
|
||||||
upload: {
|
|
||||||
statusCode: 201,
|
|
||||||
permissions: false,
|
|
||||||
query(frame) {
|
|
||||||
const store = storage.getStorage('images');
|
|
||||||
|
|
||||||
if (frame.files) {
|
|
||||||
return Promise
|
|
||||||
.map(frame.files, file => store.save(file))
|
|
||||||
.then(paths => paths[0]);
|
|
||||||
}
|
|
||||||
return store.save(frame.file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,147 +0,0 @@
|
|||||||
const shared = require('../shared');
|
|
||||||
const localUtils = require('./utils');
|
|
||||||
|
|
||||||
// ESLint Override Notice
|
|
||||||
// This is a valid index.js file - it just exports a lot of stuff!
|
|
||||||
// Long term we would like to change the API architecture to reduce this file,
|
|
||||||
// but that's not the problem the index.js max - line eslint "proxy" rule is there to solve.
|
|
||||||
/* eslint-disable max-lines */
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
get http() {
|
|
||||||
return shared.http;
|
|
||||||
},
|
|
||||||
|
|
||||||
get authentication() {
|
|
||||||
return shared.pipeline(require('./authentication'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get db() {
|
|
||||||
return shared.pipeline(require('./db'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get integrations() {
|
|
||||||
return shared.pipeline(require('./integrations'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
// @TODO: transform
|
|
||||||
get session() {
|
|
||||||
return require('./session');
|
|
||||||
},
|
|
||||||
|
|
||||||
get schedules() {
|
|
||||||
return shared.pipeline(require('./schedules'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get pages() {
|
|
||||||
return shared.pipeline(require('./pages'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get redirects() {
|
|
||||||
return shared.pipeline(require('./redirects'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get roles() {
|
|
||||||
return shared.pipeline(require('./roles'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get slugs() {
|
|
||||||
return shared.pipeline(require('./slugs'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get webhooks() {
|
|
||||||
return shared.pipeline(require('./webhooks'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get posts() {
|
|
||||||
return shared.pipeline(require('./posts'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get invites() {
|
|
||||||
return shared.pipeline(require('./invites'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get mail() {
|
|
||||||
return shared.pipeline(require('./mail'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get notifications() {
|
|
||||||
return shared.pipeline(require('./notifications'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get settings() {
|
|
||||||
return shared.pipeline(require('./settings'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get images() {
|
|
||||||
return shared.pipeline(require('./images'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get tags() {
|
|
||||||
return shared.pipeline(require('./tags'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get users() {
|
|
||||||
return shared.pipeline(require('./users'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get preview() {
|
|
||||||
return shared.pipeline(require('./preview'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get oembed() {
|
|
||||||
return shared.pipeline(require('./oembed'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get slack() {
|
|
||||||
return shared.pipeline(require('./slack'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get config() {
|
|
||||||
return shared.pipeline(require('./config'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get themes() {
|
|
||||||
return shared.pipeline(require('./themes'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get actions() {
|
|
||||||
return shared.pipeline(require('./actions'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get site() {
|
|
||||||
return shared.pipeline(require('./site'), localUtils);
|
|
||||||
},
|
|
||||||
|
|
||||||
get serializers() {
|
|
||||||
return require('./utils/serializers');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content API Controllers
|
|
||||||
*
|
|
||||||
* @NOTE:
|
|
||||||
*
|
|
||||||
* Please create separate controllers for Content & Admin API. The goal is to expose `api.v2.content` and
|
|
||||||
* `api.v2.admin` soon. Need to figure out how serializers & validation works then.
|
|
||||||
*/
|
|
||||||
get pagesPublic() {
|
|
||||||
return shared.pipeline(require('./pages-public'), localUtils, 'content');
|
|
||||||
},
|
|
||||||
|
|
||||||
get tagsPublic() {
|
|
||||||
return shared.pipeline(require('./tags-public'), localUtils, 'content');
|
|
||||||
},
|
|
||||||
|
|
||||||
get publicSettings() {
|
|
||||||
return shared.pipeline(require('./settings-public'), localUtils, 'content');
|
|
||||||
},
|
|
||||||
|
|
||||||
get postsPublic() {
|
|
||||||
return shared.pipeline(require('./posts-public'), localUtils, 'content');
|
|
||||||
},
|
|
||||||
|
|
||||||
get authorsPublic() {
|
|
||||||
return shared.pipeline(require('./authors-public'), localUtils, 'content');
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,144 +0,0 @@
|
|||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const models = require('../../models');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
resourceNotFound: '{resource} not found.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'integrations',
|
|
||||||
browse: {
|
|
||||||
permissions: true,
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'limit'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ['api_keys', 'webhooks']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query({options}) {
|
|
||||||
return models.Integration.findPage(options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
read: {
|
|
||||||
permissions: true,
|
|
||||||
data: [
|
|
||||||
'id'
|
|
||||||
],
|
|
||||||
options: [
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
data: {
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ['api_keys', 'webhooks']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query({data, options}) {
|
|
||||||
return models.Integration.findOne(data, Object.assign(options, {require: true}))
|
|
||||||
.catch(models.Integration.NotFoundError, () => {
|
|
||||||
throw new errors.NotFoundError({
|
|
||||||
message: tpl(messages.resourceNotFound, {resource: 'Integration'})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
edit: {
|
|
||||||
permissions: true,
|
|
||||||
data: [
|
|
||||||
'name',
|
|
||||||
'icon_image',
|
|
||||||
'description',
|
|
||||||
'webhooks'
|
|
||||||
],
|
|
||||||
options: [
|
|
||||||
'id',
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
values: ['api_keys', 'webhooks']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query({data, options}) {
|
|
||||||
return models.Integration.edit(data, Object.assign(options, {require: true}))
|
|
||||||
.catch(models.Integration.NotFoundError, () => {
|
|
||||||
throw new errors.NotFoundError({
|
|
||||||
message: tpl(messages.resourceNotFound, {resource: 'Integration'})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
add: {
|
|
||||||
statusCode: 201,
|
|
||||||
permissions: true,
|
|
||||||
data: [
|
|
||||||
'name',
|
|
||||||
'icon_image',
|
|
||||||
'description',
|
|
||||||
'webhooks'
|
|
||||||
],
|
|
||||||
options: [
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
data: {
|
|
||||||
name: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ['api_keys', 'webhooks']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query({data, options}) {
|
|
||||||
const dataWithApiKeys = Object.assign({
|
|
||||||
api_keys: [
|
|
||||||
{type: 'content'},
|
|
||||||
{type: 'admin'}
|
|
||||||
]
|
|
||||||
}, data);
|
|
||||||
return models.Integration.add(dataWithApiKeys, options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
destroy: {
|
|
||||||
statusCode: 204,
|
|
||||||
permissions: true,
|
|
||||||
options: [
|
|
||||||
'id'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query({options}) {
|
|
||||||
return models.Integration.destroy(Object.assign(options, {require: true}))
|
|
||||||
.catch(models.Integration.NotFoundError, () => {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.resourceNotFound, {resource: 'Integration'})
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,126 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const invites = require('../../services/invites');
|
|
||||||
const models = require('../../models');
|
|
||||||
const api = require('./index');
|
|
||||||
const ALLOWED_INCLUDES = [];
|
|
||||||
const UNSAFE_ATTRS = ['role_id'];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
inviteNotFound: 'Invite not found.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'invites',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'page',
|
|
||||||
'limit',
|
|
||||||
'fields',
|
|
||||||
'filter',
|
|
||||||
'order',
|
|
||||||
'debug'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Invite.findPage(frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read: {
|
|
||||||
options: [
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'id',
|
|
||||||
'email'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Invite.findOne(frame.data, frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.inviteNotFound)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: {
|
|
||||||
statusCode: 204,
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'id'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
frame.options.require = true;
|
|
||||||
|
|
||||||
return models.Invite.destroy(frame.options)
|
|
||||||
.then(() => null)
|
|
||||||
.catch(models.Invite.NotFoundError, () => {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.inviteNotFound)
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
add: {
|
|
||||||
statusCode: 201,
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'email'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
role_id: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
unsafeAttrs: UNSAFE_ATTRS
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return invites.add({
|
|
||||||
api,
|
|
||||||
InviteModel: models.Invite,
|
|
||||||
invites: frame.data.invites,
|
|
||||||
options: frame.options,
|
|
||||||
user: {
|
|
||||||
name: frame.user.get('name'),
|
|
||||||
email: frame.user.get('email')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,66 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const mailService = require('../../services/mail');
|
|
||||||
const api = require('./');
|
|
||||||
let mailer;
|
|
||||||
let _private = {};
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
unableToSendEmail: 'Ghost is currently unable to send email.',
|
|
||||||
seeLinkForInstructions: 'See {link} for instructions.',
|
|
||||||
testGhostEmail: 'Test Ghost Email'
|
|
||||||
};
|
|
||||||
|
|
||||||
_private.sendMail = (object) => {
|
|
||||||
if (!(mailer instanceof mailService.GhostMailer)) {
|
|
||||||
mailer = new mailService.GhostMailer();
|
|
||||||
}
|
|
||||||
|
|
||||||
return mailer.send(object.mail[0].message).catch((err) => {
|
|
||||||
if (mailer.state.usingDirect) {
|
|
||||||
api.notifications.add(
|
|
||||||
{
|
|
||||||
notifications: [{
|
|
||||||
type: 'warn',
|
|
||||||
message: [
|
|
||||||
tpl(messages.unableToSendEmail),
|
|
||||||
tpl(messages.seeLinkForInstructions, {link: 'https://ghost.org/docs/concepts/config/#mail'})
|
|
||||||
].join(' ')
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{context: {internal: true}}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'mail',
|
|
||||||
|
|
||||||
send: {
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return _private.sendMail(frame.data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
sendTest(frame) {
|
|
||||||
return mailService.utils.generateContent({template: 'test'})
|
|
||||||
.then((content) => {
|
|
||||||
const payload = {
|
|
||||||
mail: [{
|
|
||||||
message: {
|
|
||||||
to: frame.user.get('email'),
|
|
||||||
subject: tpl(messages.testGhostEmail),
|
|
||||||
html: content.html,
|
|
||||||
text: content.text
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
return _private.sendMail(payload);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,96 +0,0 @@
|
|||||||
const {notifications} = require('../../services/notifications');
|
|
||||||
const api = require('./index');
|
|
||||||
const internalContext = {context: {internal: true}};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'notifications',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return notifications.browse({
|
|
||||||
user: {
|
|
||||||
id: frame.user && frame.user.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
add: {
|
|
||||||
statusCode(result) {
|
|
||||||
if (result.notifications.length) {
|
|
||||||
return 201;
|
|
||||||
} else {
|
|
||||||
return 200;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
const {allNotifications, notificationsToAdd} = notifications.add({
|
|
||||||
notifications: frame.data.notifications
|
|
||||||
});
|
|
||||||
|
|
||||||
if (notificationsToAdd.length){
|
|
||||||
return api.settings.edit({
|
|
||||||
settings: [{
|
|
||||||
key: 'notifications',
|
|
||||||
// @NOTE: We always need to store all notifications!
|
|
||||||
value: allNotifications.concat(notificationsToAdd)
|
|
||||||
}]
|
|
||||||
}, internalContext).then(() => {
|
|
||||||
return notificationsToAdd;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: {
|
|
||||||
statusCode: 204,
|
|
||||||
options: ['notification_id'],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
notification_id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
async query(frame) {
|
|
||||||
const allNotifications = await notifications.destroy({
|
|
||||||
notificationId: frame.options.notification_id,
|
|
||||||
user: {
|
|
||||||
id: frame.user && frame.user.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return api.settings.edit({
|
|
||||||
settings: [{
|
|
||||||
key: 'notifications',
|
|
||||||
value: allNotifications
|
|
||||||
}]
|
|
||||||
}, internalContext).return();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all notifications. Method used in tests only
|
|
||||||
*
|
|
||||||
* @private Not exposed over HTTP
|
|
||||||
*/
|
|
||||||
destroyAll: {
|
|
||||||
statusCode: 204,
|
|
||||||
permissions: {
|
|
||||||
method: 'destroy'
|
|
||||||
},
|
|
||||||
query() {
|
|
||||||
const allNotifications = notifications.destroyAll();
|
|
||||||
|
|
||||||
return api.settings.edit({
|
|
||||||
settings: [{
|
|
||||||
key: 'notifications',
|
|
||||||
value: allNotifications
|
|
||||||
}]
|
|
||||||
}, internalContext).return();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,186 +0,0 @@
|
|||||||
const errors = require('@tryghost/errors');
|
|
||||||
const {extract, hasProvider} = require('oembed-parser');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const cheerio = require('cheerio');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const config = require('../../../shared/config');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const externalRequest = require('../../lib/request-external');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
unknownProvider: 'No provider found for supplied URL.'
|
|
||||||
};
|
|
||||||
|
|
||||||
const findUrlWithProvider = (url) => {
|
|
||||||
let provider;
|
|
||||||
|
|
||||||
// build up a list of URL variations to test against because the oembed
|
|
||||||
// providers list is not always up to date with scheme or www vs non-www
|
|
||||||
let baseUrl = url.replace(/^\/\/|^https?:\/\/(?:www\.)?/, '');
|
|
||||||
let testUrls = [
|
|
||||||
`http://${baseUrl}`,
|
|
||||||
`https://${baseUrl}`,
|
|
||||||
`http://www.${baseUrl}`,
|
|
||||||
`https://www.${baseUrl}`
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let testUrl of testUrls) {
|
|
||||||
provider = hasProvider(testUrl);
|
|
||||||
if (provider) {
|
|
||||||
url = testUrl;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {url, provider};
|
|
||||||
};
|
|
||||||
|
|
||||||
function unknownProvider(url) {
|
|
||||||
return Promise.reject(new errors.ValidationError({
|
|
||||||
message: tpl(messages.unknownProvider),
|
|
||||||
context: url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function knownProvider(url) {
|
|
||||||
return extract(url, {maxwidth: 1280}).catch((err) => {
|
|
||||||
return Promise.reject(new errors.InternalServerError({
|
|
||||||
message: err.message
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIpOrLocalhost(url) {
|
|
||||||
try {
|
|
||||||
const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
||||||
const IPV6_REGEX = /:/; // fqdns will not have colons
|
|
||||||
const HTTP_REGEX = /^https?:/i;
|
|
||||||
|
|
||||||
const siteUrl = new URL(config.get('url'));
|
|
||||||
const {protocol, hostname, host} = new URL(url);
|
|
||||||
|
|
||||||
// allow requests to Ghost's own url through
|
|
||||||
if (siteUrl.host === host) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!HTTP_REGEX.test(protocol) || hostname === 'localhost' || IPV4_REGEX.test(hostname) || IPV6_REGEX.test(hostname)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchOembedData(_url) {
|
|
||||||
// parse the url then validate the protocol and host to make sure it's
|
|
||||||
// http(s) and not an IP address or localhost to avoid potential access to
|
|
||||||
// internal network endpoints
|
|
||||||
if (isIpOrLocalhost(_url)) {
|
|
||||||
return unknownProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
// check against known oembed list
|
|
||||||
let {url, provider} = findUrlWithProvider(_url);
|
|
||||||
if (provider) {
|
|
||||||
return knownProvider(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// url not in oembed list so fetch it in case it's a redirect or has a
|
|
||||||
// <link rel="alternate" type="application/json+oembed"> element
|
|
||||||
return externalRequest(url, {
|
|
||||||
method: 'GET',
|
|
||||||
timeout: 2 * 1000,
|
|
||||||
followRedirect: true
|
|
||||||
}).then((pageResponse) => {
|
|
||||||
// url changed after fetch, see if we were redirected to a known oembed
|
|
||||||
if (pageResponse.url !== url) {
|
|
||||||
({url, provider} = findUrlWithProvider(pageResponse.url));
|
|
||||||
if (provider) {
|
|
||||||
return knownProvider(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for <link rel="alternate" type="application/json+oembed"> element
|
|
||||||
let oembedUrl;
|
|
||||||
try {
|
|
||||||
oembedUrl = cheerio('link[type="application/json+oembed"]', pageResponse.body).attr('href');
|
|
||||||
} catch (e) {
|
|
||||||
return unknownProvider(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oembedUrl) {
|
|
||||||
// make sure the linked url is not an ip address or localhost
|
|
||||||
if (isIpOrLocalhost(oembedUrl)) {
|
|
||||||
return unknownProvider(oembedUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch oembed response from embedded rel="alternate" url
|
|
||||||
return externalRequest(oembedUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
json: true,
|
|
||||||
timeout: 2 * 1000,
|
|
||||||
followRedirect: true
|
|
||||||
}).then((oembedResponse) => {
|
|
||||||
// validate the fetched json against the oembed spec to avoid
|
|
||||||
// leaking non-oembed responses
|
|
||||||
const body = oembedResponse.body;
|
|
||||||
const hasRequiredFields = body.type && body.version;
|
|
||||||
const hasValidType = ['photo', 'video', 'link', 'rich'].includes(body.type);
|
|
||||||
|
|
||||||
if (hasRequiredFields && hasValidType) {
|
|
||||||
// extract known oembed fields from the response to limit leaking of unrecognised data
|
|
||||||
const knownFields = [
|
|
||||||
'type',
|
|
||||||
'version',
|
|
||||||
'html',
|
|
||||||
'url',
|
|
||||||
'title',
|
|
||||||
'width',
|
|
||||||
'height',
|
|
||||||
'author_name',
|
|
||||||
'author_url',
|
|
||||||
'provider_name',
|
|
||||||
'provider_url',
|
|
||||||
'thumbnail_url',
|
|
||||||
'thumbnail_width',
|
|
||||||
'thumbnail_height'
|
|
||||||
];
|
|
||||||
const oembed = _.pick(body, knownFields);
|
|
||||||
|
|
||||||
// ensure we have required data for certain types
|
|
||||||
if (oembed.type === 'photo' && !oembed.url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((oembed.type === 'video' || oembed.type === 'rich') && (!oembed.html || !oembed.width || !oembed.height)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the extracted object, don't pass through the response body
|
|
||||||
return oembed;
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'oembed',
|
|
||||||
|
|
||||||
read: {
|
|
||||||
permissions: false,
|
|
||||||
data: [
|
|
||||||
'url'
|
|
||||||
],
|
|
||||||
options: [],
|
|
||||||
query({data: {url}}) {
|
|
||||||
return fetchOembedData(url).then((response) => {
|
|
||||||
return response || unknownProvider(url);
|
|
||||||
}).catch(() => {
|
|
||||||
return unknownProvider(url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,78 +0,0 @@
|
|||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const models = require('../../models');
|
|
||||||
const ALLOWED_INCLUDES = ['tags', 'authors'];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
pageNotFound: 'Page not found'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'pages',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'formats',
|
|
||||||
'absolute_urls',
|
|
||||||
'page',
|
|
||||||
'limit',
|
|
||||||
'order',
|
|
||||||
'debug'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
formats: {
|
|
||||||
values: models.Post.allowedFormats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.findPage(frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'fields',
|
|
||||||
'formats',
|
|
||||||
'debug',
|
|
||||||
'absolute_urls'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'id',
|
|
||||||
'slug',
|
|
||||||
'uuid'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
formats: {
|
|
||||||
values: models.Post.allowedFormats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.findOne(frame.data, frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
throw new errors.NotFoundError({
|
|
||||||
message: tpl(messages.pageNotFound)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,197 +0,0 @@
|
|||||||
const models = require('../../models');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const getPostServiceInstance = require('../../services/posts/posts-service');
|
|
||||||
const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles'];
|
|
||||||
const UNSAFE_ATTRS = ['status', 'authors', 'visibility'];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
pageNotFound: 'Page not found.'
|
|
||||||
};
|
|
||||||
|
|
||||||
const postsService = getPostServiceInstance();
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'pages',
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'formats',
|
|
||||||
'limit',
|
|
||||||
'order',
|
|
||||||
'page',
|
|
||||||
'debug',
|
|
||||||
'absolute_urls'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
formats: {
|
|
||||||
values: models.Post.allowedFormats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
docName: 'posts',
|
|
||||||
unsafeAttrs: UNSAFE_ATTRS
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.findPage(frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'fields',
|
|
||||||
'formats',
|
|
||||||
'debug',
|
|
||||||
'absolute_urls',
|
|
||||||
// NOTE: only for internal context
|
|
||||||
'forUpdate',
|
|
||||||
'transacting'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'id',
|
|
||||||
'slug',
|
|
||||||
'uuid'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
formats: {
|
|
||||||
values: models.Post.allowedFormats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
docName: 'posts',
|
|
||||||
unsafeAttrs: UNSAFE_ATTRS
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.findOne(frame.data, frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
throw new errors.NotFoundError({
|
|
||||||
message: tpl(messages.pageNotFound)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
add: {
|
|
||||||
statusCode: 201,
|
|
||||||
headers: {},
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'source'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
source: {
|
|
||||||
values: ['html']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
docName: 'posts',
|
|
||||||
unsafeAttrs: UNSAFE_ATTRS
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.add(frame.data.pages[0], frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (model.get('status') !== 'published') {
|
|
||||||
this.headers.cacheInvalidate = false;
|
|
||||||
} else {
|
|
||||||
this.headers.cacheInvalidate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
edit: {
|
|
||||||
headers: {},
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'id',
|
|
||||||
'source',
|
|
||||||
// NOTE: only for internal context
|
|
||||||
'forUpdate',
|
|
||||||
'transacting'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
source: {
|
|
||||||
values: ['html']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
docName: 'posts',
|
|
||||||
unsafeAttrs: UNSAFE_ATTRS
|
|
||||||
},
|
|
||||||
async query(frame) {
|
|
||||||
const model = await models.Post.edit(frame.data.pages[0], frame.options);
|
|
||||||
|
|
||||||
this.headers.cacheInvalidate = postsService.handleCacheInvalidation(model);
|
|
||||||
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: {
|
|
||||||
statusCode: 204,
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'id'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
docName: 'posts',
|
|
||||||
unsafeAttrs: UNSAFE_ATTRS
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
frame.options.require = true;
|
|
||||||
|
|
||||||
return models.Post.destroy(frame.options)
|
|
||||||
.then(() => null)
|
|
||||||
.catch(models.Post.NotFoundError, () => {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.pageNotFound)
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,78 +0,0 @@
|
|||||||
const models = require('../../models');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const allowedIncludes = ['tags', 'authors'];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
postNotFound: 'Post not found.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'posts',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'formats',
|
|
||||||
'limit',
|
|
||||||
'order',
|
|
||||||
'page',
|
|
||||||
'debug',
|
|
||||||
'absolute_urls'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: allowedIncludes
|
|
||||||
},
|
|
||||||
formats: {
|
|
||||||
values: models.Post.allowedFormats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.findPage(frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'fields',
|
|
||||||
'formats',
|
|
||||||
'debug',
|
|
||||||
'absolute_urls'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'id',
|
|
||||||
'slug',
|
|
||||||
'uuid'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: allowedIncludes
|
|
||||||
},
|
|
||||||
formats: {
|
|
||||||
values: models.Post.allowedFormats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.findOne(frame.data, frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
throw new errors.NotFoundError({
|
|
||||||
message: tpl(messages.postNotFound)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,192 +0,0 @@
|
|||||||
const models = require('../../models');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const getPostServiceInstance = require('../../services/posts/posts-service');
|
|
||||||
const allowedIncludes = ['tags', 'authors', 'authors.roles'];
|
|
||||||
const unsafeAttrs = ['status', 'authors', 'visibility'];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
postNotFound: 'Post not found.'
|
|
||||||
};
|
|
||||||
|
|
||||||
const postsService = getPostServiceInstance();
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'posts',
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'formats',
|
|
||||||
'limit',
|
|
||||||
'order',
|
|
||||||
'page',
|
|
||||||
'debug',
|
|
||||||
'absolute_urls'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: allowedIncludes
|
|
||||||
},
|
|
||||||
formats: {
|
|
||||||
values: models.Post.allowedFormats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
unsafeAttrs: unsafeAttrs
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.findPage(frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'fields',
|
|
||||||
'formats',
|
|
||||||
'debug',
|
|
||||||
'absolute_urls',
|
|
||||||
// NOTE: only for internal context
|
|
||||||
'forUpdate',
|
|
||||||
'transacting'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'id',
|
|
||||||
'slug',
|
|
||||||
'uuid'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: allowedIncludes
|
|
||||||
},
|
|
||||||
formats: {
|
|
||||||
values: models.Post.allowedFormats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
unsafeAttrs: unsafeAttrs
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.findOne(frame.data, frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
throw new errors.NotFoundError({
|
|
||||||
message: tpl(messages.postNotFound)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
add: {
|
|
||||||
statusCode: 201,
|
|
||||||
headers: {},
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'source'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: allowedIncludes
|
|
||||||
},
|
|
||||||
source: {
|
|
||||||
values: ['html']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
unsafeAttrs: unsafeAttrs
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.add(frame.data.posts[0], frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (model.get('status') !== 'published') {
|
|
||||||
this.headers.cacheInvalidate = false;
|
|
||||||
} else {
|
|
||||||
this.headers.cacheInvalidate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
edit: {
|
|
||||||
headers: {},
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'id',
|
|
||||||
'source',
|
|
||||||
// NOTE: only for internal context
|
|
||||||
'forUpdate',
|
|
||||||
'transacting'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: allowedIncludes
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
source: {
|
|
||||||
values: ['html']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
unsafeAttrs: unsafeAttrs
|
|
||||||
},
|
|
||||||
async query(frame) {
|
|
||||||
const model = await models.Post.edit(frame.data.posts[0], frame.options);
|
|
||||||
|
|
||||||
this.headers.cacheInvalidate = postsService.handleCacheInvalidation(model);
|
|
||||||
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: {
|
|
||||||
statusCode: 204,
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'id'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: allowedIncludes
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
unsafeAttrs: unsafeAttrs
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
frame.options.require = true;
|
|
||||||
|
|
||||||
return models.Post.destroy(frame.options)
|
|
||||||
.then(() => null)
|
|
||||||
.catch(models.Post.NotFoundError, () => {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.postNotFound)
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,46 +0,0 @@
|
|||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const models = require('../../models');
|
|
||||||
const ALLOWED_INCLUDES = ['authors', 'tags'];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
postNotFound: 'Post not found.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'preview',
|
|
||||||
|
|
||||||
read: {
|
|
||||||
permissions: true,
|
|
||||||
options: [
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'uuid'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
uuid: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return models.Post.findOne(Object.assign({status: 'all'}, frame.data), frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
throw new errors.NotFoundError({
|
|
||||||
message: tpl(messages.postNotFound)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,28 +0,0 @@
|
|||||||
const redirects = require('../../services/redirects');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'redirects',
|
|
||||||
|
|
||||||
download: {
|
|
||||||
headers: {
|
|
||||||
disposition: {
|
|
||||||
type: 'file',
|
|
||||||
value: 'redirects.json'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query() {
|
|
||||||
return redirects.api.get();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
upload: {
|
|
||||||
permissions: true,
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return redirects.api.setFromFilePath(frame.file.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
const models = require('../../models');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'roles',
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'permissions'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
permissions: ['assign']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Role.findAll(frame.options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,77 +0,0 @@
|
|||||||
const models = require('../../models');
|
|
||||||
|
|
||||||
const postSchedulingService = require('../../services/posts/post-scheduling-service')('canary');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'schedules',
|
|
||||||
publish: {
|
|
||||||
headers: {},
|
|
||||||
options: [
|
|
||||||
'id',
|
|
||||||
'resource'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'force'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
resource: {
|
|
||||||
required: true,
|
|
||||||
values: ['posts', 'pages']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
docName: 'posts'
|
|
||||||
},
|
|
||||||
async query(frame) {
|
|
||||||
const resourceType = frame.options.resource;
|
|
||||||
const options = {
|
|
||||||
status: 'scheduled',
|
|
||||||
id: frame.options.id,
|
|
||||||
context: {
|
|
||||||
internal: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const {scheduledResource, preScheduledResource} = await postSchedulingService.publish(resourceType, frame.options.id, frame.data.force, options);
|
|
||||||
const cacheInvalidate = postSchedulingService.handleCacheInvalidation(scheduledResource, preScheduledResource);
|
|
||||||
this.headers.cacheInvalidate = cacheInvalidate;
|
|
||||||
|
|
||||||
const response = {};
|
|
||||||
response[resourceType] = [scheduledResource];
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getScheduled: {
|
|
||||||
// NOTE: this method is for internal use only by DefaultScheduler
|
|
||||||
// it is not exposed anywhere!
|
|
||||||
permissions: false,
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
resource: {
|
|
||||||
required: true,
|
|
||||||
values: ['posts', 'pages']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
const resourceModel = 'Post';
|
|
||||||
const resourceType = (frame.options.resource === 'post') ? 'post' : 'page';
|
|
||||||
const cleanOptions = {};
|
|
||||||
cleanOptions.filter = `status:scheduled+type:${resourceType}`;
|
|
||||||
cleanOptions.columns = ['id', 'published_at', 'created_at', 'type'];
|
|
||||||
|
|
||||||
return models[resourceModel].findAll(cleanOptions)
|
|
||||||
.then((result) => {
|
|
||||||
let response = {};
|
|
||||||
response[resourceType] = result;
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,70 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const models = require('../../models');
|
|
||||||
const auth = require('../../services/auth');
|
|
||||||
const api = require('./index');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
authAccessDenied: 'Access denied.'
|
|
||||||
};
|
|
||||||
|
|
||||||
const session = {
|
|
||||||
read(frame) {
|
|
||||||
/*
|
|
||||||
* TODO
|
|
||||||
* Don't query db for user, when new api http wrapper is in we can
|
|
||||||
* have direct access to req.user, we can also get access to some session
|
|
||||||
* inofrmation too and send it back
|
|
||||||
*/
|
|
||||||
return models.User.findOne({id: frame.options.context.user});
|
|
||||||
},
|
|
||||||
add(frame) {
|
|
||||||
const object = frame.data;
|
|
||||||
|
|
||||||
if (!object || !object.username || !object.password) {
|
|
||||||
return Promise.reject(new errors.UnauthorizedError({
|
|
||||||
message: tpl(messages.authAccessDenied)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return models.User.check({
|
|
||||||
email: object.username,
|
|
||||||
password: object.password
|
|
||||||
}).then((user) => {
|
|
||||||
return Promise.resolve((req, res, next) => {
|
|
||||||
req.brute.reset(function (err) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
req.user = user;
|
|
||||||
auth.session.createSession(req, res, next);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}).catch(async (err) => {
|
|
||||||
if (!errors.utils.isGhostError(err)) {
|
|
||||||
throw new errors.UnauthorizedError({
|
|
||||||
message: tpl(messages.authAccessDenied),
|
|
||||||
err
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.errorType === 'PasswordResetRequiredError') {
|
|
||||||
await api.authentication.generateResetToken({
|
|
||||||
passwordreset: [{
|
|
||||||
email: object.username
|
|
||||||
}]
|
|
||||||
}, frame.options.context);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
delete() {
|
|
||||||
return Promise.resolve((req, res, next) => {
|
|
||||||
auth.session.destroySession(req, res, next);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = session;
|
|
@ -1,17 +0,0 @@
|
|||||||
const settingsCache = require('../../../shared/settings-cache');
|
|
||||||
const urlUtils = require('../../../shared/url-utils');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'settings',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
permissions: true,
|
|
||||||
query() {
|
|
||||||
// @TODO: decouple settings cache from API knowledge
|
|
||||||
// The controller fetches models (or cached models) and the API frame for the target API version formats the response.
|
|
||||||
return Object.assign({}, settingsCache.getPublic(), {
|
|
||||||
url: urlUtils.urlFor('home', true)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,195 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const models = require('../../models');
|
|
||||||
const routeSettings = require('../../services/route-settings');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const {NoPermissionError, NotFoundError} = require('@tryghost/errors');
|
|
||||||
const settingsService = require('../../services/settings');
|
|
||||||
const settingsCache = require('../../../shared/settings-cache');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
problemFindingSetting: 'Problem finding setting: {key}',
|
|
||||||
accessCoreSettingFromExtReq: 'Attempted to access core setting from external request'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'settings',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
options: ['type'],
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
let settings = settingsCache.getAll();
|
|
||||||
|
|
||||||
// CASE: no context passed (functional call)
|
|
||||||
if (!frame.options.context) {
|
|
||||||
return Promise.resolve(settings.filter((setting) => {
|
|
||||||
return setting.group === 'site';
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!frame.options.context.internal) {
|
|
||||||
// CASE: omit core settings unless internal request
|
|
||||||
settings = _.filter(settings, (setting) => {
|
|
||||||
const isCore = setting.group === 'core';
|
|
||||||
return !isCore;
|
|
||||||
});
|
|
||||||
// CASE: omit secret settings unless internal request
|
|
||||||
settings = settings.map(settingsService.hideValueIfSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read: {
|
|
||||||
options: ['key'],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
key: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
identifier(frame) {
|
|
||||||
return frame.options.key;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
let setting;
|
|
||||||
if (frame.options.key === 'slack') {
|
|
||||||
const slackURL = settingsCache.get('slack_url', {resolve: false});
|
|
||||||
const slackUsername = settingsCache.get('slack_username', {resolve: false});
|
|
||||||
|
|
||||||
setting = slackURL || slackUsername;
|
|
||||||
setting.key = 'slack';
|
|
||||||
setting.value = [{
|
|
||||||
url: slackURL && slackURL.value,
|
|
||||||
username: slackUsername && slackUsername.value
|
|
||||||
}];
|
|
||||||
} else if (frame.options.key === 'slack_url' || frame.options.key === 'slack_username') {
|
|
||||||
// leave the value empty returning 404 for unknown in current API keys
|
|
||||||
} else {
|
|
||||||
setting = settingsCache.get(frame.options.key, {resolve: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!setting) {
|
|
||||||
return Promise.reject(new NotFoundError({
|
|
||||||
message: tpl(messages.problemFindingSetting, {key: frame.options.key})
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO: handle in settings model permissible fn
|
|
||||||
if (setting.group === 'core' && !(frame.options.context && frame.options.context.internal)) {
|
|
||||||
return Promise.reject(new NoPermissionError({
|
|
||||||
message: tpl(messages.accessCoreSettingFromExtReq)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
setting = settingsService.hideValueIfSecret(setting);
|
|
||||||
|
|
||||||
return {
|
|
||||||
[frame.options.key]: setting
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
edit: {
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
unsafeAttrsObject(frame) {
|
|
||||||
return _.find(frame.data.settings, {key: 'labs'});
|
|
||||||
},
|
|
||||||
before(frame) {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Using eslint disable line here as we are about to drop v2 - no point in fixing
|
|
||||||
frame.data.settings.map((setting) => { /* eslint-disable-line array-callback-return */
|
|
||||||
if (setting.group === 'core' && !(frame.options.context && frame.options.context.internal)) {
|
|
||||||
errors.push(new NoPermissionError({
|
|
||||||
message: tpl(messages.accessCoreSettingFromExtReq)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
return Promise.reject(errors[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
let type = frame.data.settings.find((setting) => {
|
|
||||||
return setting.key === 'type';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (_.isObject(type)) {
|
|
||||||
type = type.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.data.settings = _.reject(frame.data.settings, (setting) => {
|
|
||||||
return setting.key === 'type'
|
|
||||||
// Remove obfuscated settings
|
|
||||||
|| (setting.value === settingsService.obfuscatedSetting
|
|
||||||
&& settingsService.isSecretSetting(setting));
|
|
||||||
});
|
|
||||||
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
_.each(frame.data.settings, (setting) => {
|
|
||||||
const settingFromCache = settingsCache.get(setting.key, {resolve: false});
|
|
||||||
|
|
||||||
if (!settingFromCache) {
|
|
||||||
errors.push(new NotFoundError({
|
|
||||||
message: tpl(messages.problemFindingSetting, {key: setting.key})
|
|
||||||
}));
|
|
||||||
} else if (settingFromCache.core === 'core' && !(frame.options.context && frame.options.context.internal)) {
|
|
||||||
// @TODO: handle in settings model permissible fn
|
|
||||||
errors.push(new NoPermissionError({
|
|
||||||
message: tpl(messages.accessCoreSettingFromExtReq)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
return Promise.reject(errors[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return models.Settings.edit(frame.data.settings, frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
upload: {
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
method: 'edit'
|
|
||||||
},
|
|
||||||
async query(frame) {
|
|
||||||
await routeSettings.api.setFromFilePath(frame.file.path);
|
|
||||||
const getRoutesHash = () => routeSettings.api.getCurrentHash();
|
|
||||||
await settingsService.syncRoutesHash(getRoutesHash);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
download: {
|
|
||||||
headers: {
|
|
||||||
disposition: {
|
|
||||||
type: 'yaml',
|
|
||||||
value: 'routes.yaml'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
response: {
|
|
||||||
format: 'plain'
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
method: 'browse'
|
|
||||||
},
|
|
||||||
query() {
|
|
||||||
return routeSettings.api.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,14 +0,0 @@
|
|||||||
const publicConfig = require('../../services/public-config');
|
|
||||||
|
|
||||||
const site = {
|
|
||||||
docName: 'site',
|
|
||||||
|
|
||||||
read: {
|
|
||||||
permissions: false,
|
|
||||||
query() {
|
|
||||||
return publicConfig.site;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = site;
|
|
@ -1,12 +0,0 @@
|
|||||||
// Used to call the slack ping service, iirc this was done to avoid circular deps a long time ago
|
|
||||||
const events = require('../../lib/common/events');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'slack',
|
|
||||||
sendTest: {
|
|
||||||
permissions: false,
|
|
||||||
query() {
|
|
||||||
events.emit('slack.test');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,51 +0,0 @@
|
|||||||
const models = require('../../models');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
couldNotGenerateSlug: 'Could not generate slug.'
|
|
||||||
};
|
|
||||||
|
|
||||||
const allowedTypes = {
|
|
||||||
post: models.Post,
|
|
||||||
tag: models.Tag,
|
|
||||||
user: models.User
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'slugs',
|
|
||||||
generate: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'type'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'name'
|
|
||||||
],
|
|
||||||
permissions: true,
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
type: {
|
|
||||||
required: true,
|
|
||||||
values: Object.keys(allowedTypes)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return models.Base.Model.generateSlug(allowedTypes[frame.options.type], frame.data.name, {status: 'all'})
|
|
||||||
.then((slug) => {
|
|
||||||
if (!slug) {
|
|
||||||
return Promise.reject(new errors.InternalServerError({
|
|
||||||
message: tpl(messages.couldNotGenerateSlug)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return slug;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,71 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const models = require('../../models');
|
|
||||||
|
|
||||||
const ALLOWED_INCLUDES = ['count.posts'];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
tagNotFound: 'Tag not found.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'tags',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'limit',
|
|
||||||
'order',
|
|
||||||
'page',
|
|
||||||
'debug'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.TagPublic.findPage(frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'debug'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'id',
|
|
||||||
'slug',
|
|
||||||
'visibility'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.TagPublic.findOne(frame.data, frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.tagNotFound)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,159 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const models = require('../../models');
|
|
||||||
|
|
||||||
const ALLOWED_INCLUDES = ['count.posts'];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
tagNotFound: 'Tag not found.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'tags',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'limit',
|
|
||||||
'order',
|
|
||||||
'page',
|
|
||||||
'debug'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Tag.findPage(frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'debug'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'id',
|
|
||||||
'slug',
|
|
||||||
'visibility'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Tag.findOne(frame.data, frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.tagNotFound)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
add: {
|
|
||||||
statusCode: 201,
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Tag.add(frame.data.tags[0], frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
edit: {
|
|
||||||
headers: {},
|
|
||||||
options: [
|
|
||||||
'id',
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Tag.edit(frame.data.tags[0], frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.tagNotFound)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.wasChanged()) {
|
|
||||||
this.headers.cacheInvalidate = true;
|
|
||||||
} else {
|
|
||||||
this.headers.cacheInvalidate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: {
|
|
||||||
statusCode: 204,
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
'id'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Tag.destroy(frame.options)
|
|
||||||
.then(() => null)
|
|
||||||
.catch(models.Tag.NotFoundError, () => {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.tagNotFound)
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,133 +0,0 @@
|
|||||||
const themeService = require('../../services/themes');
|
|
||||||
const limitService = require('../../services/limits');
|
|
||||||
const models = require('../../models');
|
|
||||||
|
|
||||||
// Used to emit theme.uploaded which is used in core/server/analytics-events
|
|
||||||
const events = require('../../lib/common/events');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'themes',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
permissions: true,
|
|
||||||
query() {
|
|
||||||
return themeService.api.getJSON();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
activate: {
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
'name'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
name: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
async query(frame) {
|
|
||||||
let themeName = frame.options.name;
|
|
||||||
|
|
||||||
if (limitService.isLimited('customThemes')) {
|
|
||||||
await limitService.errorIfWouldGoOverLimit('customThemes', {value: themeName});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSettings = [{
|
|
||||||
key: 'active_theme',
|
|
||||||
value: themeName
|
|
||||||
}];
|
|
||||||
|
|
||||||
return themeService.api.activate(themeName)
|
|
||||||
.then((checkedTheme) => {
|
|
||||||
// @NOTE: we use the model, not the API here, as we don't want to trigger permissions
|
|
||||||
return models.Settings.edit(newSettings, frame.options)
|
|
||||||
.then(() => checkedTheme);
|
|
||||||
})
|
|
||||||
.then((checkedTheme) => {
|
|
||||||
return themeService.api.getJSON(themeName, checkedTheme);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
upload: {
|
|
||||||
headers: {},
|
|
||||||
permissions: {
|
|
||||||
method: 'add'
|
|
||||||
},
|
|
||||||
async query(frame) {
|
|
||||||
if (limitService.isLimited('customThemes')) {
|
|
||||||
// Sending a bad string to make sure it fails (empty string isn't valid)
|
|
||||||
await limitService.errorIfWouldGoOverLimit('customThemes', {value: '.'});
|
|
||||||
}
|
|
||||||
|
|
||||||
// @NOTE: consistent filename uploads
|
|
||||||
{
|
|
||||||
frame.options.originalname = frame.file.originalname.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
let zip = {
|
|
||||||
path: frame.file.path,
|
|
||||||
name: frame.file.originalname
|
|
||||||
};
|
|
||||||
|
|
||||||
return themeService.api.setFromZip(zip)
|
|
||||||
.then(({theme, themeOverridden}) => {
|
|
||||||
if (themeOverridden) {
|
|
||||||
// CASE: clear cache
|
|
||||||
this.headers.cacheInvalidate = true;
|
|
||||||
}
|
|
||||||
events.emit('theme.uploaded');
|
|
||||||
return theme;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
download: {
|
|
||||||
options: [
|
|
||||||
'name'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
name: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
method: 'read'
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
let themeName = frame.options.name;
|
|
||||||
|
|
||||||
return themeService.api.getZip(themeName);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: {
|
|
||||||
statusCode: 204,
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
'name'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
name: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
let themeName = frame.options.name;
|
|
||||||
|
|
||||||
return themeService.api.destroy(themeName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,179 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const models = require('../../models');
|
|
||||||
const permissionsService = require('../../services/permissions');
|
|
||||||
const ALLOWED_INCLUDES = ['count.posts', 'permissions', 'roles', 'roles.permissions'];
|
|
||||||
const UNSAFE_ATTRS = ['status', 'roles'];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
userNotFound: 'User not found.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'users',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'limit',
|
|
||||||
'order',
|
|
||||||
'page',
|
|
||||||
'debug'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.User.findPage(frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read: {
|
|
||||||
options: [
|
|
||||||
'include',
|
|
||||||
'filter',
|
|
||||||
'fields',
|
|
||||||
'debug'
|
|
||||||
],
|
|
||||||
data: [
|
|
||||||
'id',
|
|
||||||
'slug',
|
|
||||||
'email',
|
|
||||||
'role'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.User.findOne(frame.data, frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.userNotFound)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
edit: {
|
|
||||||
headers: {},
|
|
||||||
options: [
|
|
||||||
'id',
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
include: {
|
|
||||||
values: ALLOWED_INCLUDES
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
unsafeAttrs: UNSAFE_ATTRS
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return models.User.edit(frame.data.users[0], frame.options)
|
|
||||||
.then((model) => {
|
|
||||||
if (!model) {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.userNotFound)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.wasChanged()) {
|
|
||||||
this.headers.cacheInvalidate = true;
|
|
||||||
} else {
|
|
||||||
this.headers.cacheInvalidate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: {
|
|
||||||
statusCode: 204,
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
'id'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Base.transaction((t) => {
|
|
||||||
frame.options.transacting = t;
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
models.Post.destroyByAuthor(frame.options)
|
|
||||||
]).then(() => {
|
|
||||||
return models.User.destroy(Object.assign({status: 'all'}, frame.options));
|
|
||||||
}).then(() => null);
|
|
||||||
}).catch((err) => {
|
|
||||||
return Promise.reject(new errors.NoPermissionError({
|
|
||||||
err: err
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
changePassword: {
|
|
||||||
validation: {
|
|
||||||
docName: 'password',
|
|
||||||
data: {
|
|
||||||
newPassword: {required: true},
|
|
||||||
ne2Password: {required: true},
|
|
||||||
user_id: {required: true}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
docName: 'user',
|
|
||||||
method: 'edit',
|
|
||||||
identifier(frame) {
|
|
||||||
return frame.data.password[0].user_id;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
frame.options.skipSessionID = frame.original.session.id;
|
|
||||||
return models.User.changePassword(frame.data.password[0], frame.options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
transferOwnership: {
|
|
||||||
permissions(frame) {
|
|
||||||
return models.Role.findOne({name: 'Owner'})
|
|
||||||
.then((ownerRole) => {
|
|
||||||
return permissionsService.canThis(frame.options.context).assign.role(ownerRole);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return models.User.transferOwnership(frame.data.owner[0], frame.options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,34 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
get permissions() {
|
|
||||||
return require('./permissions');
|
|
||||||
},
|
|
||||||
|
|
||||||
get serializers() {
|
|
||||||
return require('./serializers');
|
|
||||||
},
|
|
||||||
|
|
||||||
get validators() {
|
|
||||||
return require('./validators');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Does the request access the Content API?
|
|
||||||
*
|
|
||||||
* Each controller is either for the Content or for the Admin API.
|
|
||||||
* When Ghost registers each controller, it currently passes a String "content" if the controller
|
|
||||||
* is a Content API implementation - see index.js file.
|
|
||||||
*
|
|
||||||
* @TODO: Move this helper function into a utils.js file.
|
|
||||||
* @param {Object} frame
|
|
||||||
* @return {boolean}
|
|
||||||
*/
|
|
||||||
isContentAPI: (frame) => {
|
|
||||||
return frame.apiType === 'content';
|
|
||||||
},
|
|
||||||
|
|
||||||
// @TODO: Remove, not used.
|
|
||||||
isAdminAPIKey: (frame) => {
|
|
||||||
return frame.options.context && Object.keys(frame.options.context).length !== 0 && frame.options.context.api_key &&
|
|
||||||
frame.options.context.api_key.type === 'admin';
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,112 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:permissions');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const permissions = require('../../../services/permissions');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
noPermissionToCall: 'You do not have permission to {method} {docName}'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Handle requests, which need authentication.
|
|
||||||
*
|
|
||||||
* @param {Object} apiConfig - Docname & method of API ctrl
|
|
||||||
* @param {Object} frame
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
const nonePublicAuth = (apiConfig, frame) => {
|
|
||||||
debug('check admin permissions');
|
|
||||||
|
|
||||||
const singular = apiConfig.docName.replace(/s$/, '');
|
|
||||||
|
|
||||||
let permissionIdentifier = frame.options.id;
|
|
||||||
|
|
||||||
// CASE: Target ctrl can override the identifier. The identifier is the unique identifier of the target resource
|
|
||||||
// e.g. edit a setting -> the key of the setting
|
|
||||||
// e.g. edit a post -> post id from url param
|
|
||||||
// e.g. change user password -> user id inside of the body structure
|
|
||||||
if (apiConfig.identifier) {
|
|
||||||
permissionIdentifier = apiConfig.identifier(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
let unsafeAttrObject = apiConfig.unsafeAttrs && _.has(frame, `data.[${apiConfig.docName}][0]`) ? _.pick(frame.data[apiConfig.docName][0], apiConfig.unsafeAttrs) : {};
|
|
||||||
|
|
||||||
if (apiConfig.unsafeAttrsObject) {
|
|
||||||
unsafeAttrObject = apiConfig.unsafeAttrsObject(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
const permsPromise = permissions.canThis(frame.options.context)[apiConfig.method][singular](permissionIdentifier, unsafeAttrObject);
|
|
||||||
|
|
||||||
return permsPromise.then((result) => {
|
|
||||||
/*
|
|
||||||
* Allow the permissions function to return a list of excluded attributes.
|
|
||||||
* If it does, omit those attrs from the data passed through
|
|
||||||
*
|
|
||||||
* NOTE: excludedAttrs differ from unsafeAttrs in that they're determined by the model's permissible function,
|
|
||||||
* and the attributes are simply excluded rather than throwing a NoPermission exception
|
|
||||||
*
|
|
||||||
* TODO: This is currently only needed because of the posts model and the contributor role. Once we extend the
|
|
||||||
* contributor role to be able to edit existing tags, this concept can be removed.
|
|
||||||
*/
|
|
||||||
if (result && result.excludedAttrs && _.has(frame, `data.[${apiConfig.docName}][0]`)) {
|
|
||||||
frame.data[apiConfig.docName][0] = _.omit(frame.data[apiConfig.docName][0], result.excludedAttrs);
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
if (err instanceof errors.NoPermissionError) {
|
|
||||||
err.message = tpl(messages.noPermissionToCall, {
|
|
||||||
method: apiConfig.method,
|
|
||||||
docName: apiConfig.docName
|
|
||||||
});
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.utils.isGhostError(err)) {
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(new errors.InternalServerError({
|
|
||||||
err: err
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10735
|
|
||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* @description Handle permission stage for API version v2.
|
|
||||||
*
|
|
||||||
* @param {Object} apiConfig - Docname & method of target ctrl.
|
|
||||||
* @param {Object} frame
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
handle(apiConfig, frame) {
|
|
||||||
debug('handle');
|
|
||||||
|
|
||||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10099
|
|
||||||
frame.options.context = permissions.parseContext(frame.options.context);
|
|
||||||
|
|
||||||
// CASE: Content API access
|
|
||||||
if (frame.options.context.public) {
|
|
||||||
debug('check content permissions');
|
|
||||||
|
|
||||||
// @TODO: Remove when we drop v0.1
|
|
||||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10733
|
|
||||||
return permissions.applyPublicRules(apiConfig.docName, apiConfig.method, {
|
|
||||||
status: frame.options.status,
|
|
||||||
id: frame.options.id,
|
|
||||||
uuid: frame.options.uuid,
|
|
||||||
slug: frame.options.slug,
|
|
||||||
data: {
|
|
||||||
status: frame.data.status,
|
|
||||||
id: frame.data.id,
|
|
||||||
uuid: frame.data.uuid,
|
|
||||||
slug: frame.data.slug
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return nonePublicAuth(apiConfig, frame);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
get input() {
|
|
||||||
return require('./input');
|
|
||||||
},
|
|
||||||
|
|
||||||
get output() {
|
|
||||||
return require('./output');
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,20 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:input:db');
|
|
||||||
const optionsUtil = require('../../../../shared/utils/options');
|
|
||||||
|
|
||||||
const INTERNAL_OPTIONS = ['transacting', 'forUpdate'];
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(apiConfig, frame) {
|
|
||||||
debug('serialize all');
|
|
||||||
|
|
||||||
if (frame.options.include) {
|
|
||||||
frame.options.include = optionsUtil.trimAndLowerCase(frame.options.include);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!frame.options.context.internal) {
|
|
||||||
debug('omit internal options');
|
|
||||||
frame.options = _.omit(frame.options, INTERNAL_OPTIONS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,33 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
get db() {
|
|
||||||
return require('./db');
|
|
||||||
},
|
|
||||||
|
|
||||||
get integrations() {
|
|
||||||
return require('./integrations');
|
|
||||||
},
|
|
||||||
|
|
||||||
get pages() {
|
|
||||||
return require('./pages');
|
|
||||||
},
|
|
||||||
|
|
||||||
get posts() {
|
|
||||||
return require('./posts');
|
|
||||||
},
|
|
||||||
|
|
||||||
get settings() {
|
|
||||||
return require('./settings');
|
|
||||||
},
|
|
||||||
|
|
||||||
get users() {
|
|
||||||
return require('./users');
|
|
||||||
},
|
|
||||||
|
|
||||||
get tags() {
|
|
||||||
return require('./tags');
|
|
||||||
},
|
|
||||||
|
|
||||||
get webhooks() {
|
|
||||||
return require('./webhooks');
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,33 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:input:integrations');
|
|
||||||
|
|
||||||
function setDefaultFilter(frame) {
|
|
||||||
if (frame.options.filter) {
|
|
||||||
frame.options.filter = `(${frame.options.filter})+type:[custom,builtin]`;
|
|
||||||
} else {
|
|
||||||
frame.options.filter = 'type:[custom,builtin]';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse(apiConfig, frame) {
|
|
||||||
debug('browse');
|
|
||||||
|
|
||||||
setDefaultFilter(frame);
|
|
||||||
},
|
|
||||||
read(apiConfig, frame) {
|
|
||||||
debug('read');
|
|
||||||
|
|
||||||
setDefaultFilter(frame);
|
|
||||||
},
|
|
||||||
add(apiConfig, frame) {
|
|
||||||
debug('add');
|
|
||||||
|
|
||||||
frame.data = _.pick(frame.data.integrations[0], apiConfig.data);
|
|
||||||
},
|
|
||||||
edit(apiConfig, frame) {
|
|
||||||
debug('edit');
|
|
||||||
|
|
||||||
frame.data = _.pick(frame.data.integrations[0], apiConfig.data);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,203 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const mapNQLKeyValues = require('@tryghost/nql').utils.mapKeyValues;
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:input:pages');
|
|
||||||
const mobiledoc = require('../../../../../lib/mobiledoc');
|
|
||||||
const url = require('./utils/url');
|
|
||||||
const localUtils = require('../../index');
|
|
||||||
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
|
|
||||||
|
|
||||||
const replacePageWithType = mapNQLKeyValues({
|
|
||||||
key: {
|
|
||||||
from: 'page',
|
|
||||||
to: 'type'
|
|
||||||
},
|
|
||||||
values: [{
|
|
||||||
from: false,
|
|
||||||
to: 'post'
|
|
||||||
}, {
|
|
||||||
from: true,
|
|
||||||
to: 'page'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
function removeMobiledocFormat(frame) {
|
|
||||||
if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
|
|
||||||
frame.options.formats = frame.options.formats.filter((format) => {
|
|
||||||
return (format !== 'mobiledoc');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultRelations(frame) {
|
|
||||||
if (frame.options.withRelated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.options.columns && !frame.options.withRelated) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.options.withRelated = ['tags', 'authors', 'authors.roles'];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDefaultOrder(frame) {
|
|
||||||
let includesOrderedRelations = false;
|
|
||||||
|
|
||||||
if (frame.options.withRelated) {
|
|
||||||
const orderedRelations = ['author', 'authors', 'tag', 'tags'];
|
|
||||||
includesOrderedRelations = _.intersection(orderedRelations, frame.options.withRelated).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!frame.options.order && !includesOrderedRelations) {
|
|
||||||
frame.options.order = 'title asc';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function forceVisibilityColumn(frame) {
|
|
||||||
if (frame.options.columns && !frame.options.columns.includes('visibility')) {
|
|
||||||
frame.options.columns.push('visibility');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultFormat(frame) {
|
|
||||||
if (frame.options.formats) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.options.formats = 'mobiledoc';
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePostsMeta(frame) {
|
|
||||||
let metaAttrs = _.keys(_.omit(postsMetaSchema, ['id', 'post_id']));
|
|
||||||
let meta = _.pick(frame.data.pages[0], metaAttrs);
|
|
||||||
frame.data.pages[0].posts_meta = meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CASE:
|
|
||||||
*
|
|
||||||
* - the content api endpoints for pages forces the model layer to return static pages only
|
|
||||||
* - we have to enforce the filter
|
|
||||||
*
|
|
||||||
* @TODO: https://github.com/TryGhost/Ghost/issues/10268
|
|
||||||
*/
|
|
||||||
const forcePageFilter = (frame) => {
|
|
||||||
if (frame.options.filter) {
|
|
||||||
frame.options.filter = `(${frame.options.filter})+type:page`;
|
|
||||||
} else {
|
|
||||||
frame.options.filter = 'type:page';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const forceStatusFilter = (frame) => {
|
|
||||||
if (!frame.options.filter) {
|
|
||||||
frame.options.filter = 'status:[draft,published,scheduled]';
|
|
||||||
} else if (!frame.options.filter.match(/status:/)) {
|
|
||||||
frame.options.filter = `(${frame.options.filter})+status:[draft,published,scheduled]`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse(apiConfig, frame) {
|
|
||||||
debug('browse');
|
|
||||||
|
|
||||||
forcePageFilter(frame);
|
|
||||||
|
|
||||||
if (localUtils.isContentAPI(frame)) {
|
|
||||||
removeMobiledocFormat(frame);
|
|
||||||
setDefaultOrder(frame);
|
|
||||||
forceVisibilityColumn(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!localUtils.isContentAPI(frame)) {
|
|
||||||
forceStatusFilter(frame);
|
|
||||||
defaultFormat(frame);
|
|
||||||
defaultRelations(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.options.mongoTransformer = replacePageWithType;
|
|
||||||
},
|
|
||||||
|
|
||||||
read(apiConfig, frame) {
|
|
||||||
debug('read');
|
|
||||||
|
|
||||||
forcePageFilter(frame);
|
|
||||||
|
|
||||||
if (localUtils.isContentAPI(frame)) {
|
|
||||||
removeMobiledocFormat(frame);
|
|
||||||
setDefaultOrder(frame);
|
|
||||||
forceVisibilityColumn(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!localUtils.isContentAPI(frame)) {
|
|
||||||
forceStatusFilter(frame);
|
|
||||||
defaultFormat(frame);
|
|
||||||
defaultRelations(frame);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
add(apiConfig, frame, options = {add: true}) {
|
|
||||||
debug('add');
|
|
||||||
|
|
||||||
if (_.get(frame,'options.source')) {
|
|
||||||
const html = frame.data.pages[0].html;
|
|
||||||
|
|
||||||
if (frame.options.source === 'html' && !_.isEmpty(html)) {
|
|
||||||
frame.data.pages[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.data.pages[0] = url.forPost(Object.assign({}, frame.data.pages[0]), frame.options);
|
|
||||||
|
|
||||||
// @NOTE: force storing page
|
|
||||||
if (options.add) {
|
|
||||||
frame.data.pages[0].type = 'page';
|
|
||||||
}
|
|
||||||
|
|
||||||
// CASE: Transform short to long format
|
|
||||||
if (frame.data.pages[0].authors) {
|
|
||||||
frame.data.pages[0].authors.forEach((author, index) => {
|
|
||||||
if (_.isString(author)) {
|
|
||||||
frame.data.pages[0].authors[index] = {
|
|
||||||
email: author
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.data.pages[0].tags) {
|
|
||||||
frame.data.pages[0].tags.forEach((tag, index) => {
|
|
||||||
if (_.isString(tag)) {
|
|
||||||
frame.data.pages[0].tags[index] = {
|
|
||||||
name: tag
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePostsMeta(frame);
|
|
||||||
defaultFormat(frame);
|
|
||||||
defaultRelations(frame);
|
|
||||||
},
|
|
||||||
|
|
||||||
edit(apiConfig, frame) {
|
|
||||||
debug('edit');
|
|
||||||
|
|
||||||
this.add(...arguments, {add: false});
|
|
||||||
|
|
||||||
handlePostsMeta(frame);
|
|
||||||
forceStatusFilter(frame);
|
|
||||||
forcePageFilter(frame);
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy(apiConfig, frame) {
|
|
||||||
debug('destroy');
|
|
||||||
frame.options.destroyBy = {
|
|
||||||
id: frame.options.id,
|
|
||||||
type: 'page'
|
|
||||||
};
|
|
||||||
|
|
||||||
defaultFormat(frame);
|
|
||||||
defaultRelations(frame);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,218 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const mapNQLKeyValues = require('@tryghost/nql').utils.mapKeyValues;
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:input:posts');
|
|
||||||
const url = require('./utils/url');
|
|
||||||
const localUtils = require('../../index');
|
|
||||||
const mobiledoc = require('../../../../../lib/mobiledoc');
|
|
||||||
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
|
|
||||||
|
|
||||||
const replacePageWithType = mapNQLKeyValues({
|
|
||||||
key: {
|
|
||||||
from: 'page',
|
|
||||||
to: 'type'
|
|
||||||
},
|
|
||||||
values: [{
|
|
||||||
from: false,
|
|
||||||
to: 'post'
|
|
||||||
}, {
|
|
||||||
from: true,
|
|
||||||
to: 'page'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
function removeMobiledocFormat(frame) {
|
|
||||||
if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
|
|
||||||
frame.options.formats = frame.options.formats.filter((format) => {
|
|
||||||
return (format !== 'mobiledoc');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultRelations(frame) {
|
|
||||||
if (frame.options.withRelated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.options.columns && !frame.options.withRelated) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.options.withRelated = ['tags', 'authors', 'authors.roles'];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDefaultOrder(frame) {
|
|
||||||
let includesOrderedRelations = false;
|
|
||||||
|
|
||||||
if (frame.options.withRelated) {
|
|
||||||
const orderedRelations = ['author', 'authors', 'tag', 'tags'];
|
|
||||||
includesOrderedRelations = _.intersection(orderedRelations, frame.options.withRelated).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!frame.options.order && !includesOrderedRelations) {
|
|
||||||
frame.options.order = 'published_at desc';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function forceVisibilityColumn(frame) {
|
|
||||||
if (frame.options.columns && !frame.options.columns.includes('visibility')) {
|
|
||||||
frame.options.columns.push('visibility');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultFormat(frame) {
|
|
||||||
if (frame.options.formats) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.options.formats = 'mobiledoc';
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePostsMeta(frame) {
|
|
||||||
let metaAttrs = _.keys(_.omit(postsMetaSchema, ['id', 'post_id']));
|
|
||||||
let meta = _.pick(frame.data.posts[0], metaAttrs);
|
|
||||||
frame.data.posts[0].posts_meta = meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CASE:
|
|
||||||
*
|
|
||||||
* - posts endpoint only returns posts, not pages
|
|
||||||
* - we have to enforce the filter
|
|
||||||
*
|
|
||||||
* @TODO: https://github.com/TryGhost/Ghost/issues/10268
|
|
||||||
*/
|
|
||||||
const forcePageFilter = (frame) => {
|
|
||||||
if (frame.options.filter) {
|
|
||||||
frame.options.filter = `(${frame.options.filter})+type:post`;
|
|
||||||
} else {
|
|
||||||
frame.options.filter = 'type:post';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const forceStatusFilter = (frame) => {
|
|
||||||
if (!frame.options.filter) {
|
|
||||||
frame.options.filter = 'status:[draft,published,scheduled]';
|
|
||||||
} else if (!frame.options.filter.match(/status:/)) {
|
|
||||||
frame.options.filter = `(${frame.options.filter})+status:[draft,published,scheduled]`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse(apiConfig, frame) {
|
|
||||||
debug('browse');
|
|
||||||
|
|
||||||
forcePageFilter(frame);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ## current cases:
|
|
||||||
* - context object is empty (functional call, content api access)
|
|
||||||
* - api_key.type == 'content' ? content api access
|
|
||||||
* - user exists? admin api access
|
|
||||||
*/
|
|
||||||
if (localUtils.isContentAPI(frame)) {
|
|
||||||
// CASE: the content api endpoint for posts should not return mobiledoc
|
|
||||||
removeMobiledocFormat(frame);
|
|
||||||
|
|
||||||
setDefaultOrder(frame);
|
|
||||||
forceVisibilityColumn(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!localUtils.isContentAPI(frame)) {
|
|
||||||
forceStatusFilter(frame);
|
|
||||||
defaultFormat(frame);
|
|
||||||
defaultRelations(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.options.mongoTransformer = replacePageWithType;
|
|
||||||
},
|
|
||||||
|
|
||||||
read(apiConfig, frame) {
|
|
||||||
debug('read');
|
|
||||||
|
|
||||||
forcePageFilter(frame);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ## current cases:
|
|
||||||
* - context object is empty (functional call, content api access)
|
|
||||||
* - api_key.type == 'content' ? content api access
|
|
||||||
* - user exists? admin api access
|
|
||||||
*/
|
|
||||||
if (localUtils.isContentAPI(frame)) {
|
|
||||||
// CASE: the content api endpoint for posts should not return mobiledoc
|
|
||||||
removeMobiledocFormat(frame);
|
|
||||||
|
|
||||||
setDefaultOrder(frame);
|
|
||||||
forceVisibilityColumn(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!localUtils.isContentAPI(frame)) {
|
|
||||||
forceStatusFilter(frame);
|
|
||||||
defaultFormat(frame);
|
|
||||||
defaultRelations(frame);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
add(apiConfig, frame, options = {add: true}) {
|
|
||||||
debug('add');
|
|
||||||
|
|
||||||
if (_.get(frame,'options.source')) {
|
|
||||||
const html = frame.data.posts[0].html;
|
|
||||||
|
|
||||||
if (frame.options.source === 'html' && !_.isEmpty(html)) {
|
|
||||||
frame.data.posts[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.data.posts[0] = url.forPost(Object.assign({}, frame.data.posts[0]), frame.options);
|
|
||||||
|
|
||||||
// @NOTE: force adding post
|
|
||||||
if (options.add) {
|
|
||||||
frame.data.posts[0].type = 'post';
|
|
||||||
}
|
|
||||||
|
|
||||||
// CASE: Transform short to long format
|
|
||||||
if (frame.data.posts[0].authors) {
|
|
||||||
frame.data.posts[0].authors.forEach((author, index) => {
|
|
||||||
if (_.isString(author)) {
|
|
||||||
frame.data.posts[0].authors[index] = {
|
|
||||||
email: author
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.data.posts[0].tags) {
|
|
||||||
frame.data.posts[0].tags.forEach((tag, index) => {
|
|
||||||
if (_.isString(tag)) {
|
|
||||||
frame.data.posts[0].tags[index] = {
|
|
||||||
name: tag
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePostsMeta(frame);
|
|
||||||
defaultFormat(frame);
|
|
||||||
defaultRelations(frame);
|
|
||||||
},
|
|
||||||
|
|
||||||
edit(apiConfig, frame) {
|
|
||||||
debug('edit');
|
|
||||||
this.add(apiConfig, frame, {add: false});
|
|
||||||
|
|
||||||
handlePostsMeta(frame);
|
|
||||||
forceStatusFilter(frame);
|
|
||||||
forcePageFilter(frame);
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy(apiConfig, frame) {
|
|
||||||
debug('destroy');
|
|
||||||
frame.options.destroyBy = {
|
|
||||||
id: frame.options.id,
|
|
||||||
type: 'post'
|
|
||||||
};
|
|
||||||
|
|
||||||
defaultFormat(frame);
|
|
||||||
defaultRelations(frame);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,152 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const url = require('./utils/url');
|
|
||||||
const typeGroupMapper = require('../../../../shared/serializers/input/utils/settings-filter-type-group-mapper');
|
|
||||||
const settingsCache = require('../../../../../../shared/settings-cache');
|
|
||||||
|
|
||||||
const DEPRECATED_SETTINGS = [
|
|
||||||
'bulk_email_settings',
|
|
||||||
'slack'
|
|
||||||
];
|
|
||||||
|
|
||||||
const deprecatedSupportedSettingsOneToManyMap = {
|
|
||||||
slack: [{
|
|
||||||
from: '[0].url',
|
|
||||||
to: {
|
|
||||||
key: 'slack_url',
|
|
||||||
group: 'slack',
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
from: '[0].username',
|
|
||||||
to: {
|
|
||||||
key: 'slack_username',
|
|
||||||
group: 'slack',
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMappedDeprecatedSettings = (settings) => {
|
|
||||||
const mappedSettings = [];
|
|
||||||
|
|
||||||
for (const key in deprecatedSupportedSettingsOneToManyMap) {
|
|
||||||
const deprecatedSetting = settings.find(setting => setting.key === key);
|
|
||||||
|
|
||||||
if (deprecatedSetting) {
|
|
||||||
let deprecatedSettingValue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
deprecatedSettingValue = JSON.parse(deprecatedSetting.value);
|
|
||||||
} catch (err) {
|
|
||||||
// ignore the value if it's invalid
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deprecatedSettingValue) {
|
|
||||||
deprecatedSupportedSettingsOneToManyMap[key].forEach(({from, to}) => {
|
|
||||||
const value = _.get(deprecatedSettingValue, from);
|
|
||||||
mappedSettings.push({
|
|
||||||
key: to.key,
|
|
||||||
value: value
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappedSettings;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse(apiConfig, frame) {
|
|
||||||
if (frame.options.type) {
|
|
||||||
let mappedGroupOptions = typeGroupMapper(frame.options.type);
|
|
||||||
|
|
||||||
frame.options.group = mappedGroupOptions;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read(apiConfig, frame) {
|
|
||||||
if (frame.options.key === 'ghost_head') {
|
|
||||||
frame.options.key = 'codeinjection_head';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.options.key === 'ghost_foot') {
|
|
||||||
frame.options.key = 'codeinjection_foot';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.options.key === 'default_locale') {
|
|
||||||
frame.options.key = 'lang';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.options.key === 'active_timezone') {
|
|
||||||
frame.options.key = 'timezone';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
edit(apiConfig, frame) {
|
|
||||||
// CASE: allow shorthand syntax where a single key and value are passed to edit instead of object and options
|
|
||||||
if (_.isString(frame.data)) {
|
|
||||||
frame.data = {settings: [{key: frame.data, value: frame.options}]};
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = settingsCache.getAll();
|
|
||||||
|
|
||||||
frame.data.settings = frame.data.settings.filter((setting) => {
|
|
||||||
const settingFlagsStr = settings[setting.key] ? settings[setting.key].flags : '';
|
|
||||||
const settingFlagsArr = settingFlagsStr ? settingFlagsStr.split(',') : [];
|
|
||||||
|
|
||||||
// Ignore and drop all values with Read-only flag AND 'labs' setting
|
|
||||||
return !settingFlagsArr.includes('RO') && (setting.key !== 'labs');
|
|
||||||
});
|
|
||||||
|
|
||||||
frame.data.settings.push(...getMappedDeprecatedSettings(frame.data.settings));
|
|
||||||
|
|
||||||
frame.data.settings.forEach((setting) => {
|
|
||||||
const settingType = settings[setting.key] ? settings[setting.key].type : '';
|
|
||||||
|
|
||||||
// TODO: Needs to be removed once we get rid of all `object` type settings
|
|
||||||
// NOTE: this transformation is more related to the fact that internal API calls call
|
|
||||||
// settings API with plain objects instead of stringified ones
|
|
||||||
if (_.isObject(setting.value)) {
|
|
||||||
setting.value = JSON.stringify(setting.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CASE: Ensure we won't forward strings for booleans, otherwise model events or model interactions can fail
|
|
||||||
if (settingType === 'boolean' && (setting.value === '0' || setting.value === '1')) {
|
|
||||||
setting.value = !!+setting.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CASE: Ensure we won't forward strings for booleans, otherwise model events or model interactions can fail
|
|
||||||
if (settingType === 'boolean' && (setting.value === 'false' || setting.value === 'true')) {
|
|
||||||
setting.value = setting.value === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.key === 'ghost_head') {
|
|
||||||
setting.key = 'codeinjection_head';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.key === 'ghost_foot') {
|
|
||||||
setting.key = 'codeinjection_foot';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.key === 'default_locale') {
|
|
||||||
setting.key = 'lang';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.key === 'active_timezone') {
|
|
||||||
setting.key = 'timezone';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.key === 'unsplash') {
|
|
||||||
setting.value = JSON.parse(setting.value).isActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
setting = url.forSetting(setting);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ignore all deprecated settings
|
|
||||||
frame.data.settings = frame.data.settings.filter((setting) => {
|
|
||||||
return DEPRECATED_SETTINGS.includes(setting.key) === false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,35 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:input:tags');
|
|
||||||
const url = require('./utils/url');
|
|
||||||
const utils = require('../../index');
|
|
||||||
|
|
||||||
function setDefaultOrder(frame) {
|
|
||||||
if (!frame.options.order) {
|
|
||||||
frame.options.order = 'name asc';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse(apiConfig, frame) {
|
|
||||||
debug('browse');
|
|
||||||
|
|
||||||
if (utils.isContentAPI(frame)) {
|
|
||||||
setDefaultOrder(frame);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read() {
|
|
||||||
debug('read');
|
|
||||||
|
|
||||||
this.browse(...arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
add(apiConfig, frame) {
|
|
||||||
debug('add');
|
|
||||||
frame.data.tags[0] = url.forTag(Object.assign({}, frame.data.tags[0]));
|
|
||||||
},
|
|
||||||
|
|
||||||
edit(apiConfig, frame) {
|
|
||||||
debug('edit');
|
|
||||||
this.add(apiConfig, frame);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,26 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:input:users');
|
|
||||||
const url = require('./utils/url');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
read(apiConfig, frame) {
|
|
||||||
debug('read');
|
|
||||||
|
|
||||||
if (frame.data.id === 'me' && frame.options.context && frame.options.context.user) {
|
|
||||||
frame.data.id = frame.options.context.user;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
edit(apiConfig, frame) {
|
|
||||||
debug('edit');
|
|
||||||
|
|
||||||
if (frame.options.id === 'me' && frame.options.context && frame.options.context.user) {
|
|
||||||
frame.options.id = frame.options.context.user;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.data.users[0].password) {
|
|
||||||
delete frame.data.users[0].password;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.data.users[0] = url.forUser(Object.assign({}, frame.data.users[0]));
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,71 +0,0 @@
|
|||||||
const urlUtils = require('../../../../../../../shared/url-utils');
|
|
||||||
|
|
||||||
const handleImageUrl = (imageUrl) => {
|
|
||||||
try {
|
|
||||||
const imageURL = new URL(imageUrl, urlUtils.getSiteUrl());
|
|
||||||
const siteURL = new URL(urlUtils.getSiteUrl());
|
|
||||||
const subdir = siteURL.pathname.replace(/\/$/, '');
|
|
||||||
const imagePathRe = new RegExp(`${subdir}/${urlUtils.STATIC_IMAGE_URL_PREFIX}`);
|
|
||||||
|
|
||||||
if (imagePathRe.test(imageURL.pathname)) {
|
|
||||||
return urlUtils.relativeToAbsolute(imageUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageUrl;
|
|
||||||
} catch (e) {
|
|
||||||
return imageUrl;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const forPost = (attrs, options) => {
|
|
||||||
if (options && options.withRelated) {
|
|
||||||
options.withRelated.forEach((relation) => {
|
|
||||||
if (relation === 'tags' && attrs.tags) {
|
|
||||||
attrs.tags = attrs.tags.map(tag => forTag(tag));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relation === 'author' && attrs.author) {
|
|
||||||
attrs.author = forUser(attrs.author, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relation === 'authors' && attrs.authors) {
|
|
||||||
attrs.authors = attrs.authors.map(author => forUser(author, options));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forUser = (attrs) => {
|
|
||||||
if (attrs.profile_image) {
|
|
||||||
attrs.profile_image = handleImageUrl(attrs.profile_image);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attrs.cover_image) {
|
|
||||||
attrs.cover_image = handleImageUrl(attrs.cover_image);
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forTag = (attrs) => {
|
|
||||||
if (attrs.feature_image) {
|
|
||||||
attrs.feature_image = handleImageUrl(attrs.feature_image);
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forSetting = (attrs) => {
|
|
||||||
if (attrs.value && ['cover_image', 'logo', 'icon', 'portal_button_icon', 'og_image', 'twitter_image'].includes(attrs.key)) {
|
|
||||||
attrs.value = urlUtils.relativeToAbsolute(attrs.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.forPost = forPost;
|
|
||||||
module.exports.forUser = forUser;
|
|
||||||
module.exports.forTag = forTag;
|
|
||||||
module.exports.forSetting = forSetting;
|
|
@ -1,12 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const debug = require('@tryghost/debug')('api:canary:utils:serializers:input:webhooks');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
add(apiConfig, frame) {
|
|
||||||
debug('add');
|
|
||||||
|
|
||||||
if (_.get(frame, 'options.context.integration.id')) {
|
|
||||||
frame.data.webhooks[0].integration_id = frame.options.context.integration.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,13 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:actions');
|
|
||||||
const mapper = require('./utils/mapper');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse(models, apiConfig, frame) {
|
|
||||||
debug('browse');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
actions: models.data.map(model => mapper.mapAction(model, frame)),
|
|
||||||
meta: models.meta
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,25 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:all');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const removeXBY = (object) => {
|
|
||||||
_.each(object, (value, key) => {
|
|
||||||
// CASE: go deeper
|
|
||||||
if (_.isObject(value) || _.isArray(value)) {
|
|
||||||
removeXBY(value);
|
|
||||||
} else if (['updated_by', 'created_by', 'published_by'].includes(key)) {
|
|
||||||
delete object[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return object;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
after(apiConfig, frame) {
|
|
||||||
debug('all after');
|
|
||||||
|
|
||||||
if (frame.response) {
|
|
||||||
frame.response = removeXBY(frame.response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,68 +0,0 @@
|
|||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const mapper = require('./utils/mapper');
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:authentication');
|
|
||||||
const messages = {
|
|
||||||
checkEmailForInstructions: 'Check your email for further instructions.',
|
|
||||||
passwordChanged: 'Password changed successfully.',
|
|
||||||
invitationAccepted: 'Invitation accepted.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
setup(user, apiConfig, frame) {
|
|
||||||
frame.response = {
|
|
||||||
users: [
|
|
||||||
mapper.mapUser(user, {options: {context: {internal: true}}})
|
|
||||||
]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSetup(user, apiConfig, frame) {
|
|
||||||
frame.response = {
|
|
||||||
users: [
|
|
||||||
mapper.mapUser(user, {options: {context: {internal: true}}})
|
|
||||||
]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
isSetup(data, apiConfig, frame) {
|
|
||||||
frame.response = {
|
|
||||||
setup: [data]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
generateResetToken(data, apiConfig, frame) {
|
|
||||||
frame.response = {
|
|
||||||
passwordreset: [{
|
|
||||||
message: tpl(messages.checkEmailForInstructions)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
resetPassword(data, apiConfig, frame) {
|
|
||||||
frame.response = {
|
|
||||||
passwordreset: [{
|
|
||||||
message: tpl(messages.passwordChanged)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
acceptInvitation(data, apiConfig, frame) {
|
|
||||||
debug('acceptInvitation');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
invitation: [
|
|
||||||
{message: tpl(messages.invitationAccepted)}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
isInvitation(data, apiConfig, frame) {
|
|
||||||
debug('acceptInvitation');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
invitation: [{
|
|
||||||
valid: !!data
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,21 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:authors');
|
|
||||||
const mapper = require('./utils/mapper');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse(models, apiConfig, frame) {
|
|
||||||
debug('browse');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
authors: models.data.map(model => mapper.mapUser(model, frame)),
|
|
||||||
meta: models.meta
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
read(model, apiConfig, frame) {
|
|
||||||
debug('read');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
authors: [mapper.mapUser(model, frame)]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,21 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:config');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(data, apiConfig, frame) {
|
|
||||||
debug('all');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
config: _.pick(data, [
|
|
||||||
'version',
|
|
||||||
'environment',
|
|
||||||
'database',
|
|
||||||
'mail',
|
|
||||||
'useGravatar',
|
|
||||||
'labs',
|
|
||||||
'clientExtensions',
|
|
||||||
'enableDeveloperExperiments'
|
|
||||||
])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,40 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:db');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
backupContent(filename, apiConfig, frame) {
|
|
||||||
debug('backupContent');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
db: [{filename: filename}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
exportContent(exportedData, apiConfig, frame) {
|
|
||||||
debug('exportContent');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
db: [exportedData]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
importContent(response, apiConfig, frame) {
|
|
||||||
debug('importContent');
|
|
||||||
|
|
||||||
// NOTE: response can contain 2 objects if images are imported
|
|
||||||
const problems = (response.length === 2)
|
|
||||||
? response[1].problems
|
|
||||||
: response[0].problems;
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
db: [],
|
|
||||||
problems: problems
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteAllContent(response, apiConfig, frame) {
|
|
||||||
frame.response = {
|
|
||||||
db: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:images');
|
|
||||||
const mapper = require('./utils/mapper');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
upload(path, apiConfig, frame) {
|
|
||||||
debug('upload');
|
|
||||||
|
|
||||||
return frame.response = {
|
|
||||||
images: [{
|
|
||||||
url: mapper.mapImage(path),
|
|
||||||
// NOTE: ref field is here to have reference point on the client
|
|
||||||
// for example when substituting existing images in the mobiledoc
|
|
||||||
// this field would serve as an identifier to find images to replace
|
|
||||||
// once the response is back. Think of it as ID on the client's side.
|
|
||||||
ref: frame.data.ref || null
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,107 +0,0 @@
|
|||||||
// ESLint Override Notice
|
|
||||||
// This is a valid index.js file - it just exports a lot of stuff!
|
|
||||||
// Long term we would like to change the API architecture to reduce this file,
|
|
||||||
// but that's not the problem the index.js max - line eslint "proxy" rule is there to solve.
|
|
||||||
/* eslint-disable max-lines */
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
get all() {
|
|
||||||
return require('./all');
|
|
||||||
},
|
|
||||||
|
|
||||||
get authentication() {
|
|
||||||
return require('./authentication');
|
|
||||||
},
|
|
||||||
|
|
||||||
get db() {
|
|
||||||
return require('./db');
|
|
||||||
},
|
|
||||||
|
|
||||||
get integrations() {
|
|
||||||
return require('./integrations');
|
|
||||||
},
|
|
||||||
|
|
||||||
get pages() {
|
|
||||||
return require('./pages');
|
|
||||||
},
|
|
||||||
|
|
||||||
get redirects() {
|
|
||||||
return require('./redirects');
|
|
||||||
},
|
|
||||||
|
|
||||||
get roles() {
|
|
||||||
return require('./roles');
|
|
||||||
},
|
|
||||||
|
|
||||||
get slugs() {
|
|
||||||
return require('./slugs');
|
|
||||||
},
|
|
||||||
|
|
||||||
get schedules() {
|
|
||||||
return require('./schedules');
|
|
||||||
},
|
|
||||||
|
|
||||||
get webhooks() {
|
|
||||||
return require('./webhooks');
|
|
||||||
},
|
|
||||||
|
|
||||||
get posts() {
|
|
||||||
return require('./posts');
|
|
||||||
},
|
|
||||||
|
|
||||||
get invites() {
|
|
||||||
return require('./invites');
|
|
||||||
},
|
|
||||||
|
|
||||||
get settings() {
|
|
||||||
return require('./settings');
|
|
||||||
},
|
|
||||||
|
|
||||||
get notifications() {
|
|
||||||
return require('./notifications');
|
|
||||||
},
|
|
||||||
|
|
||||||
get mail() {
|
|
||||||
return require('./mail');
|
|
||||||
},
|
|
||||||
|
|
||||||
get images() {
|
|
||||||
return require('./images');
|
|
||||||
},
|
|
||||||
|
|
||||||
get tags() {
|
|
||||||
return require('./tags');
|
|
||||||
},
|
|
||||||
|
|
||||||
get users() {
|
|
||||||
return require('./users');
|
|
||||||
},
|
|
||||||
|
|
||||||
get preview() {
|
|
||||||
return require('./preview');
|
|
||||||
},
|
|
||||||
|
|
||||||
get oembed() {
|
|
||||||
return require('./oembed');
|
|
||||||
},
|
|
||||||
|
|
||||||
get authors() {
|
|
||||||
return require('./authors');
|
|
||||||
},
|
|
||||||
|
|
||||||
get config() {
|
|
||||||
return require('./config');
|
|
||||||
},
|
|
||||||
|
|
||||||
get themes() {
|
|
||||||
return require('./themes');
|
|
||||||
},
|
|
||||||
|
|
||||||
get actions() {
|
|
||||||
return require('./actions');
|
|
||||||
},
|
|
||||||
|
|
||||||
get site() {
|
|
||||||
return require('./site');
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,35 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:integrations');
|
|
||||||
const mapper = require('./utils/mapper');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse({data, meta}, apiConfig, frame) {
|
|
||||||
debug('browse');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
integrations: data.map(model => mapper.mapIntegration(model, frame)),
|
|
||||||
meta
|
|
||||||
};
|
|
||||||
},
|
|
||||||
read(model, apiConfig, frame) {
|
|
||||||
debug('read');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
integrations: [mapper.mapIntegration(model, frame)]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
add(model, apiConfig, frame) {
|
|
||||||
debug('add');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
integrations: [mapper.mapIntegration(model, frame)]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
edit(model, apiConfig, frame) {
|
|
||||||
debug('edit');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
integrations: [mapper.mapIntegration(model, frame)]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:invites');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(models, apiConfig, frame) {
|
|
||||||
debug('all');
|
|
||||||
|
|
||||||
if (!models) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (models.meta) {
|
|
||||||
frame.response = {
|
|
||||||
invites: models.data.map(model => model.toJSON(frame.options)),
|
|
||||||
meta: models.meta
|
|
||||||
};
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
invites: [models.toJSON(frame.options)]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:mail');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(response, apiConfig, frame) {
|
|
||||||
debug('all');
|
|
||||||
const toReturn = _.cloneDeep(frame.data);
|
|
||||||
|
|
||||||
delete toReturn.mail[0].options;
|
|
||||||
// Sendmail returns extra details we don't need and that don't convert to JSON
|
|
||||||
delete toReturn.mail[0].message.transport;
|
|
||||||
|
|
||||||
toReturn.mail[0].status = {
|
|
||||||
message: response.message
|
|
||||||
};
|
|
||||||
|
|
||||||
frame.response = toReturn;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:notifications');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(response, apiConfig, frame) {
|
|
||||||
debug('all');
|
|
||||||
if (!response) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response || !response.length) {
|
|
||||||
frame.response = {
|
|
||||||
notifications: []
|
|
||||||
};
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.forEach((notification) => {
|
|
||||||
delete notification.seen;
|
|
||||||
delete notification.seenBy;
|
|
||||||
delete notification.addedAt;
|
|
||||||
delete notification.createdAtVersion;
|
|
||||||
});
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
notifications: response
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,8 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:oembed');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(res, apiConfig, frame) {
|
|
||||||
debug('all');
|
|
||||||
frame.response = res;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,26 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:pages');
|
|
||||||
const mapper = require('./utils/mapper');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(models, apiConfig, frame) {
|
|
||||||
debug('all');
|
|
||||||
|
|
||||||
// CASE: e.g. destroy returns null
|
|
||||||
if (!models) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (models.meta) {
|
|
||||||
frame.response = {
|
|
||||||
pages: models.data.map(model => mapper.mapPost(model, frame)),
|
|
||||||
meta: models.meta
|
|
||||||
};
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
pages: [mapper.mapPost(models, frame)]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,26 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:posts');
|
|
||||||
const mapper = require('./utils/mapper');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(models, apiConfig, frame) {
|
|
||||||
debug('all');
|
|
||||||
|
|
||||||
// CASE: e.g. destroy returns null
|
|
||||||
if (!models) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (models.meta) {
|
|
||||||
frame.response = {
|
|
||||||
posts: models.data.map(model => mapper.mapPost(model, frame)),
|
|
||||||
meta: models.meta
|
|
||||||
};
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
posts: [mapper.mapPost(models, frame)]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
const mapper = require('./utils/mapper');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(model, apiConfig, frame) {
|
|
||||||
frame.response = {
|
|
||||||
preview: [mapper.mapPost(model, frame)]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
download(response, apiConfig, frame) {
|
|
||||||
frame.response = response;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:roles');
|
|
||||||
const canThis = require('../../../../../services/permissions').canThis;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse(models, apiConfig, frame) {
|
|
||||||
debug('browse');
|
|
||||||
|
|
||||||
const roles = models.toJSON(frame.options);
|
|
||||||
|
|
||||||
if (frame.options.permissions !== 'assign') {
|
|
||||||
return frame.response = {
|
|
||||||
roles: roles
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return Promise.filter(roles.map((role) => {
|
|
||||||
return canThis(frame.options.context).assign.role(role)
|
|
||||||
.return(role)
|
|
||||||
.catch(() => {});
|
|
||||||
}), (value) => {
|
|
||||||
return value && (value.name !== 'Owner');
|
|
||||||
}).then((filteredRoles) => {
|
|
||||||
return frame.response = {
|
|
||||||
roles: filteredRoles
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
all(model, apiConfig, frame) {
|
|
||||||
frame.response = model;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,61 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const utils = require('../../index');
|
|
||||||
const mapper = require('./utils/mapper');
|
|
||||||
const _private = {};
|
|
||||||
const deprecatedSettings = ['secondary_nav'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ### Settings Filter
|
|
||||||
* Filters an object based on a given filter object
|
|
||||||
* @private
|
|
||||||
* @param {Object} settings
|
|
||||||
* @param {String} filter
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
_private.settingsFilter = (settings, filter) => {
|
|
||||||
let filteredGroups = filter ? filter.split(',') : false;
|
|
||||||
return _.filter(settings, (setting) => {
|
|
||||||
if (filteredGroups) {
|
|
||||||
return _.includes(filteredGroups, setting.group) && !_.includes(deprecatedSettings, setting.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !_.includes(deprecatedSettings, setting.key);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse(models, apiConfig, frame) {
|
|
||||||
let filteredSettings;
|
|
||||||
|
|
||||||
// If this is public, we already have the right data, we just need to add an Array wrapper
|
|
||||||
if (utils.isContentAPI(frame)) {
|
|
||||||
filteredSettings = models;
|
|
||||||
} else {
|
|
||||||
filteredSettings = _.values(_private.settingsFilter(models, frame.options.type));
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
settings: mapper.mapSettings(filteredSettings, frame),
|
|
||||||
meta: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (frame.options.type) {
|
|
||||||
frame.response.meta.filters = {
|
|
||||||
type: frame.options.type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
read() {
|
|
||||||
this.browse(...arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
edit(models, apiConfig, frame) {
|
|
||||||
const settingsKeyedJSON = _.keyBy(_.invokeMap(models, 'toJSON'), 'key');
|
|
||||||
this.browse(settingsKeyedJSON, apiConfig, frame);
|
|
||||||
},
|
|
||||||
|
|
||||||
download(bytes, apiConfig, frame) {
|
|
||||||
frame.response = bytes;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,16 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:site');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
read(data, apiConfig, frame) {
|
|
||||||
debug('read');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
site: _.pick(data, [
|
|
||||||
'title',
|
|
||||||
'url',
|
|
||||||
'version'
|
|
||||||
])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,11 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:slugs');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(slug, apiConfig, frame) {
|
|
||||||
debug('all');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
slugs: [{slug}]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,25 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:tags');
|
|
||||||
const mapper = require('./utils/mapper');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(models, apiConfig, frame) {
|
|
||||||
debug('all');
|
|
||||||
|
|
||||||
if (!models) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (models.meta) {
|
|
||||||
frame.response = {
|
|
||||||
tags: models.data.map(model => mapper.mapTag(model, frame)),
|
|
||||||
meta: models.meta
|
|
||||||
};
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
tags: [mapper.mapTag(models, frame)]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,25 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:themes');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
browse(themes, apiConfig, frame) {
|
|
||||||
debug('browse');
|
|
||||||
|
|
||||||
frame.response = themes;
|
|
||||||
},
|
|
||||||
|
|
||||||
upload() {
|
|
||||||
debug('upload');
|
|
||||||
this.browse(...arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
activate() {
|
|
||||||
debug('activate');
|
|
||||||
this.browse(...arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
download(fn, apiConfig, frame) {
|
|
||||||
debug('download');
|
|
||||||
|
|
||||||
frame.response = fn;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,45 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:users');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const mapper = require('./utils/mapper');
|
|
||||||
const messages = {
|
|
||||||
pwdChangedSuccessfully: 'Password changed successfully.'
|
|
||||||
};
|
|
||||||
module.exports = {
|
|
||||||
browse(models, apiConfig, frame) {
|
|
||||||
debug('browse');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
users: models.data.map(model => mapper.mapUser(model, frame)),
|
|
||||||
meta: models.meta
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
read(model, apiConfig, frame) {
|
|
||||||
debug('read');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
users: [mapper.mapUser(model, frame)]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
edit() {
|
|
||||||
debug('edit');
|
|
||||||
this.read(...arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
changePassword(models, apiConfig, frame) {
|
|
||||||
debug('changePassword');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
password: [{message: tpl(messages.pwdChangedSuccessfully)}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
transferOwnership(models, apiConfig, frame) {
|
|
||||||
debug('transferOwnership');
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
users: models.map(model => model.toJSON(frame.options))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,174 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const localUtils = require('../../../index');
|
|
||||||
|
|
||||||
const tag = (attrs, frame) => {
|
|
||||||
if (localUtils.isContentAPI(frame)) {
|
|
||||||
const contentAttrs = _.pick(attrs, [
|
|
||||||
'description',
|
|
||||||
'feature_image',
|
|
||||||
'id',
|
|
||||||
'meta_description',
|
|
||||||
'meta_title',
|
|
||||||
'name',
|
|
||||||
'slug',
|
|
||||||
'url',
|
|
||||||
'visibility',
|
|
||||||
'count'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// We are standardising on returning null from the Content API for any empty values
|
|
||||||
if (contentAttrs.meta_title === '') {
|
|
||||||
contentAttrs.meta_title = null;
|
|
||||||
}
|
|
||||||
if (contentAttrs.meta_description === '') {
|
|
||||||
contentAttrs.meta_description = null;
|
|
||||||
}
|
|
||||||
if (contentAttrs.description === '') {
|
|
||||||
contentAttrs.description = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentAttrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.pick(attrs, [
|
|
||||||
'created_at',
|
|
||||||
'description',
|
|
||||||
'feature_image',
|
|
||||||
'id',
|
|
||||||
'meta_description',
|
|
||||||
'meta_title',
|
|
||||||
'name',
|
|
||||||
'slug',
|
|
||||||
'updated_at',
|
|
||||||
'url',
|
|
||||||
'visibility',
|
|
||||||
'count'
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const author = (attrs, frame) => {
|
|
||||||
if (localUtils.isContentAPI(frame)) {
|
|
||||||
delete attrs.created_at;
|
|
||||||
delete attrs.updated_at;
|
|
||||||
delete attrs.last_seen;
|
|
||||||
delete attrs.status;
|
|
||||||
delete attrs.email;
|
|
||||||
|
|
||||||
// @NOTE: used for night shift
|
|
||||||
delete attrs.accessibility;
|
|
||||||
|
|
||||||
// Extra properties removed from v2
|
|
||||||
delete attrs.tour;
|
|
||||||
|
|
||||||
// We are standardising on returning null from the Content API for any empty values
|
|
||||||
if (attrs.twitter === '') {
|
|
||||||
attrs.twitter = null;
|
|
||||||
}
|
|
||||||
if (attrs.bio === '') {
|
|
||||||
attrs.bio = null;
|
|
||||||
}
|
|
||||||
if (attrs.website === '') {
|
|
||||||
attrs.website = null;
|
|
||||||
}
|
|
||||||
if (attrs.facebook === '') {
|
|
||||||
attrs.facebook = null;
|
|
||||||
}
|
|
||||||
if (attrs.meta_title === '') {
|
|
||||||
attrs.meta_title = null;
|
|
||||||
}
|
|
||||||
if (attrs.meta_description === '') {
|
|
||||||
attrs.meta_description = null;
|
|
||||||
}
|
|
||||||
if (attrs.location === '') {
|
|
||||||
attrs.location = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @NOTE: unused fields
|
|
||||||
delete attrs.visibility;
|
|
||||||
delete attrs.locale;
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const post = (attrs, frame) => {
|
|
||||||
if (localUtils.isContentAPI(frame)) {
|
|
||||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10335
|
|
||||||
// delete attrs.page;
|
|
||||||
delete attrs.status;
|
|
||||||
delete attrs.email_only;
|
|
||||||
|
|
||||||
// We are standardising on returning null from the Content API for any empty values
|
|
||||||
if (attrs.twitter_title === '') {
|
|
||||||
attrs.twitter_title = null;
|
|
||||||
}
|
|
||||||
if (attrs.twitter_description === '') {
|
|
||||||
attrs.twitter_description = null;
|
|
||||||
}
|
|
||||||
if (attrs.meta_title === '') {
|
|
||||||
attrs.meta_title = null;
|
|
||||||
}
|
|
||||||
if (attrs.meta_description === '') {
|
|
||||||
attrs.meta_description = null;
|
|
||||||
}
|
|
||||||
if (attrs.og_title === '') {
|
|
||||||
attrs.og_title = null;
|
|
||||||
}
|
|
||||||
if (attrs.og_description === '') {
|
|
||||||
attrs.og_description = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete attrs.visibility;
|
|
||||||
} else {
|
|
||||||
delete attrs.page;
|
|
||||||
|
|
||||||
if (!attrs.tags) {
|
|
||||||
delete attrs.primary_tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!attrs.authors) {
|
|
||||||
delete attrs.primary_author;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delete attrs.locale;
|
|
||||||
delete attrs.author;
|
|
||||||
delete attrs.type;
|
|
||||||
delete attrs.send_email_when_published;
|
|
||||||
delete attrs.email_recipient_filter;
|
|
||||||
delete attrs.email_subject;
|
|
||||||
delete attrs.feature_image_alt;
|
|
||||||
delete attrs.feature_image_caption;
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const action = (attrs) => {
|
|
||||||
if (attrs.actor) {
|
|
||||||
delete attrs.actor_id;
|
|
||||||
delete attrs.resource_id;
|
|
||||||
|
|
||||||
if (attrs.actor_type === 'user') {
|
|
||||||
attrs.actor = _.pick(attrs.actor, ['id', 'name', 'slug', 'profile_image']);
|
|
||||||
attrs.actor.image = attrs.actor.profile_image;
|
|
||||||
delete attrs.actor.profile_image;
|
|
||||||
} else {
|
|
||||||
attrs.actor = _.pick(attrs.actor, ['id', 'name', 'slug', 'icon_image']);
|
|
||||||
attrs.actor.image = attrs.actor.icon_image;
|
|
||||||
delete attrs.actor.icon_image;
|
|
||||||
}
|
|
||||||
} else if (attrs.resource) {
|
|
||||||
delete attrs.actor_id;
|
|
||||||
delete attrs.resource_id;
|
|
||||||
|
|
||||||
// @NOTE: we only support posts right now
|
|
||||||
attrs.resource = _.pick(attrs.resource, ['id', 'title', 'slug', 'feature_image']);
|
|
||||||
attrs.resource.image = attrs.resource.feature_image;
|
|
||||||
delete attrs.resource.feature_image;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.post = post;
|
|
||||||
module.exports.tag = tag;
|
|
||||||
module.exports.author = author;
|
|
||||||
module.exports.action = action;
|
|
@ -1,21 +0,0 @@
|
|||||||
const moment = require('moment-timezone');
|
|
||||||
const settingsCache = require('../../../../../../../shared/settings-cache');
|
|
||||||
|
|
||||||
const format = (date) => {
|
|
||||||
return moment(date)
|
|
||||||
.tz(settingsCache.get('timezone'))
|
|
||||||
.toISOString(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const forPost = (attrs) => {
|
|
||||||
['created_at', 'updated_at', 'published_at'].forEach((field) => {
|
|
||||||
if (attrs[field]) {
|
|
||||||
attrs[field] = format(attrs[field]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.format = format;
|
|
||||||
module.exports.forPost = forPost;
|
|
@ -1,161 +0,0 @@
|
|||||||
module.exports.forPost = (frame, model, attrs) => {
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(frame.options, 'columns') ||
|
|
||||||
(frame.options.columns.includes('excerpt') && frame.options.formats && frame.options.formats.includes('plaintext'))) {
|
|
||||||
if (_.isEmpty(attrs.custom_excerpt)) {
|
|
||||||
let plaintext = model.get('plaintext');
|
|
||||||
|
|
||||||
if (plaintext) {
|
|
||||||
attrs.excerpt = plaintext.substring(0, 500);
|
|
||||||
} else {
|
|
||||||
attrs.excerpt = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
attrs.excerpt = attrs.custom_excerpt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.forSettings = (attrs, frame) => {
|
|
||||||
const _ = require('lodash');
|
|
||||||
const mapGroupToType = require('./settings-type-group-mapper');
|
|
||||||
|
|
||||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10106
|
|
||||||
// @NOTE: Admin & Content API return a different format, needs two mappers
|
|
||||||
if (_.isArray(attrs)) {
|
|
||||||
attrs.forEach((attr) => {
|
|
||||||
attr.type = mapGroupToType(attr.group);
|
|
||||||
delete attr.group;
|
|
||||||
});
|
|
||||||
|
|
||||||
// CASE: read single setting
|
|
||||||
if (frame.original.params && frame.original.params.key) {
|
|
||||||
if (frame.original.params.key === 'ghost_head') {
|
|
||||||
attrs[0].key = 'ghost_head';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.original.params.key === 'ghost_foot') {
|
|
||||||
attrs[0].key = 'ghost_foot';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.original.params.key === 'codeinjection_head') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.original.params.key === 'codeinjection_foot') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.original.params.key === 'active_timezone') {
|
|
||||||
attrs[0].key = 'active_timezone';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.original.params.key === 'default_locale') {
|
|
||||||
attrs[0].key = 'default_locale';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.original.params.key === 'unsplash') {
|
|
||||||
attrs[0].value = JSON.stringify({
|
|
||||||
isActive: attrs[0].value
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CASE: edit
|
|
||||||
if (frame.original.body && frame.original.body.settings) {
|
|
||||||
frame.original.body.settings.forEach((setting) => {
|
|
||||||
if (setting.key === 'ghost_head') {
|
|
||||||
const target = _.find(attrs, {key: 'codeinjection_head'});
|
|
||||||
target.key = 'ghost_head';
|
|
||||||
} else if (setting.key === 'ghost_foot') {
|
|
||||||
const target = _.find(attrs, {key: 'codeinjection_foot'});
|
|
||||||
target.key = 'ghost_foot';
|
|
||||||
} else if (setting.key === 'active_timezone') {
|
|
||||||
const target = _.find(attrs, {key: 'timezone'});
|
|
||||||
target.key = 'active_timezone';
|
|
||||||
} else if (setting.key === 'default_locale') {
|
|
||||||
const target = _.find(attrs, {key: 'timezone'});
|
|
||||||
target.key = 'lang';
|
|
||||||
} else if (setting.key === 'slack') {
|
|
||||||
const slackURL = _.cloneDeep(_.find(attrs, {key: 'slack_url'}));
|
|
||||||
const slackUsername = _.cloneDeep(_.find(attrs, {key: 'slack_username'}));
|
|
||||||
|
|
||||||
if (slackURL || slackUsername) {
|
|
||||||
const slack = slackURL || slackUsername;
|
|
||||||
slack.key = 'slack';
|
|
||||||
slack.value = JSON.stringify([{
|
|
||||||
url: slackURL && slackURL.value,
|
|
||||||
username: slackUsername && slackUsername.value
|
|
||||||
}]);
|
|
||||||
|
|
||||||
attrs.push(slack);
|
|
||||||
}
|
|
||||||
} else if (setting.key === 'unsplash') {
|
|
||||||
const target = _.find(attrs, {key: 'unsplash'});
|
|
||||||
target.value = JSON.stringify({
|
|
||||||
isActive: target.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CASE: browse all settings, add extra keys and keep deprecated
|
|
||||||
const ghostHead = _.cloneDeep(_.find(attrs, {key: 'codeinjection_head'}));
|
|
||||||
const ghostFoot = _.cloneDeep(_.find(attrs, {key: 'codeinjection_foot'}));
|
|
||||||
const timezone = _.cloneDeep(_.find(attrs, {key: 'timezone'}));
|
|
||||||
const lang = _.cloneDeep(_.find(attrs, {key: 'lang'}));
|
|
||||||
const slackURL = _.cloneDeep(_.find(attrs, {key: 'slack_url'}));
|
|
||||||
const slackUsername = _.cloneDeep(_.find(attrs, {key: 'slack_username'}));
|
|
||||||
const unsplash = _.find(attrs, {key: 'unsplash'});
|
|
||||||
|
|
||||||
if (ghostHead) {
|
|
||||||
ghostHead.key = 'ghost_head';
|
|
||||||
attrs.push(ghostHead);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ghostFoot) {
|
|
||||||
ghostFoot.key = 'ghost_foot';
|
|
||||||
attrs.push(ghostFoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timezone) {
|
|
||||||
timezone.key = 'active_timezone';
|
|
||||||
attrs.push(timezone);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lang) {
|
|
||||||
lang.key = 'default_locale';
|
|
||||||
attrs.push(lang);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slackURL || slackUsername) {
|
|
||||||
const slack = slackURL || slackUsername;
|
|
||||||
slack.key = 'slack';
|
|
||||||
slack.value = JSON.stringify([{
|
|
||||||
url: slackURL && slackURL.value,
|
|
||||||
username: slackUsername && slackUsername.value
|
|
||||||
}]);
|
|
||||||
|
|
||||||
attrs.push(slack);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unsplash) {
|
|
||||||
unsplash.value = JSON.stringify({
|
|
||||||
isActive: unsplash.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
attrs.ghost_head = attrs.codeinjection_head;
|
|
||||||
attrs.ghost_foot = attrs.codeinjection_foot;
|
|
||||||
attrs.active_timezone = attrs.timezone;
|
|
||||||
attrs.default_locale = attrs.lang;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,136 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const utils = require('../../../index');
|
|
||||||
const url = require('./url');
|
|
||||||
const date = require('./date');
|
|
||||||
const gating = require('./post-gating');
|
|
||||||
const clean = require('./clean');
|
|
||||||
const extraAttrs = require('./extra-attrs');
|
|
||||||
const postsMetaSchema = require('../../../../../../data/schema').tables.posts_meta;
|
|
||||||
|
|
||||||
const mapUser = (model, frame) => {
|
|
||||||
const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
|
|
||||||
|
|
||||||
url.forUser(model.id, jsonModel, frame.options);
|
|
||||||
|
|
||||||
clean.author(jsonModel, frame);
|
|
||||||
|
|
||||||
return jsonModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapTag = (model, frame) => {
|
|
||||||
const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
|
|
||||||
|
|
||||||
url.forTag(model.id, jsonModel, frame.options);
|
|
||||||
const cleanedAttrs = clean.tag(jsonModel, frame);
|
|
||||||
|
|
||||||
return cleanedAttrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapPost = (model, frame) => {
|
|
||||||
const extendedOptions = Object.assign(_.cloneDeep(frame.options), {
|
|
||||||
extraProperties: ['canonical_url']
|
|
||||||
});
|
|
||||||
|
|
||||||
const jsonModel = model.toJSON(extendedOptions);
|
|
||||||
|
|
||||||
url.forPost(model.id, jsonModel, frame);
|
|
||||||
|
|
||||||
extraAttrs.forPost(frame, model, jsonModel);
|
|
||||||
|
|
||||||
if (utils.isContentAPI(frame)) {
|
|
||||||
// Content api v2 still expects page prop
|
|
||||||
if (!frame.options.columns || frame.options.columns.includes('page')) {
|
|
||||||
if (jsonModel.type === 'page') {
|
|
||||||
jsonModel.page = true;
|
|
||||||
} else {
|
|
||||||
jsonModel.page = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
date.forPost(jsonModel);
|
|
||||||
gating.forPost(jsonModel, frame);
|
|
||||||
|
|
||||||
delete jsonModel.newsletter_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transforms post/page metadata to flat structure
|
|
||||||
let metaAttrs = _.keys(_.omit(postsMetaSchema, ['id', 'post_id']));
|
|
||||||
_(metaAttrs).filter((k) => {
|
|
||||||
return (!frame.options.columns || (frame.options.columns && frame.options.columns.includes(k)));
|
|
||||||
}).each((attr) => {
|
|
||||||
if (!(attr === 'email_only')) {
|
|
||||||
jsonModel[attr] = _.get(jsonModel.posts_meta, attr) || null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
delete jsonModel.posts_meta;
|
|
||||||
|
|
||||||
clean.post(jsonModel, frame);
|
|
||||||
|
|
||||||
if (frame.options && frame.options.withRelated) {
|
|
||||||
frame.options.withRelated.forEach((relation) => {
|
|
||||||
// @NOTE: this block also decorates primary_tag/primary_author objects as they
|
|
||||||
// are being passed by reference in tags/authors. Might be refactored into more explicit call
|
|
||||||
// in the future, but is good enough for current use-case
|
|
||||||
if (relation === 'tags' && jsonModel.tags) {
|
|
||||||
jsonModel.tags = jsonModel.tags.map(tag => mapTag(tag, frame));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relation === 'authors' && jsonModel.authors) {
|
|
||||||
jsonModel.authors = jsonModel.authors.map(author => mapUser(author, frame));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapSettings = (attrs, frame) => {
|
|
||||||
url.forSettings(attrs);
|
|
||||||
extraAttrs.forSettings(attrs, frame);
|
|
||||||
|
|
||||||
if (_.isArray(attrs)) {
|
|
||||||
const DEPRECATED_KEYS = ['lang', 'timezone', 'accent_color', 'slack_url', 'slack_username'];
|
|
||||||
attrs = _.filter(attrs, (o) => {
|
|
||||||
return !DEPRECATED_KEYS.includes(o.key);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
delete attrs.lang;
|
|
||||||
delete attrs.timezone;
|
|
||||||
delete attrs.codeinjection_head;
|
|
||||||
delete attrs.codeinjection_foot;
|
|
||||||
delete attrs.accent_color;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapIntegration = (model, frame) => {
|
|
||||||
const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
|
|
||||||
|
|
||||||
if (jsonModel.api_keys) {
|
|
||||||
jsonModel.api_keys.forEach((key) => {
|
|
||||||
if (key.type === 'admin') {
|
|
||||||
key.secret = `${key.id}:${key.secret}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapImage = (path) => {
|
|
||||||
return url.forImage(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapAction = (model, frame) => {
|
|
||||||
const attrs = model.toJSON(frame.options);
|
|
||||||
clean.action(attrs);
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.mapPost = mapPost;
|
|
||||||
module.exports.mapUser = mapUser;
|
|
||||||
module.exports.mapTag = mapTag;
|
|
||||||
module.exports.mapIntegration = mapIntegration;
|
|
||||||
module.exports.mapSettings = mapSettings;
|
|
||||||
module.exports.mapImage = mapImage;
|
|
||||||
module.exports.mapAction = mapAction;
|
|
@ -1,29 +0,0 @@
|
|||||||
const membersService = require('../../../../../../services/members');
|
|
||||||
const htmlToPlaintext = require('../../../../../../../shared/html-to-plaintext');
|
|
||||||
|
|
||||||
const forPost = (attrs, frame) => {
|
|
||||||
const memberHasAccess = membersService.contentGating.checkPostAccess(attrs, frame.original.context.member);
|
|
||||||
|
|
||||||
if (!memberHasAccess) {
|
|
||||||
const paywallIndex = (attrs.html || '').indexOf('<!--members-only-->');
|
|
||||||
|
|
||||||
if (paywallIndex !== -1) {
|
|
||||||
attrs.html = attrs.html.slice(0, paywallIndex);
|
|
||||||
attrs.plaintext = htmlToPlaintext(attrs.html);
|
|
||||||
|
|
||||||
if (!attrs.custom_excerpt && attrs.excerpt) {
|
|
||||||
attrs.excerpt = attrs.plaintext.substring(0, 500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
['plaintext', 'html', 'excerpt'].forEach((field) => {
|
|
||||||
if (attrs[field] !== undefined) {
|
|
||||||
attrs[field] = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.forPost = forPost;
|
|
@ -1,24 +0,0 @@
|
|||||||
const groupTypeMapping = {
|
|
||||||
core: 'core',
|
|
||||||
amp: 'blog',
|
|
||||||
labs: 'blog',
|
|
||||||
slack: 'blog',
|
|
||||||
site: 'blog',
|
|
||||||
unsplash: 'blog',
|
|
||||||
views: 'blog',
|
|
||||||
theme: 'theme',
|
|
||||||
members: 'members',
|
|
||||||
private: 'private',
|
|
||||||
portal: 'portal',
|
|
||||||
email: 'bulk_email',
|
|
||||||
newsletter: 'newsletter',
|
|
||||||
firstpromoter: 'firstpromoter',
|
|
||||||
oauth: 'oauth',
|
|
||||||
editor: 'editor'
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapGroupToType = (group) => {
|
|
||||||
return groupTypeMapping[group];
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = mapGroupToType;
|
|
@ -1,67 +0,0 @@
|
|||||||
const urlService = require('../../../../../../services/url');
|
|
||||||
const urlUtils = require('../../../../../../../shared/url-utils');
|
|
||||||
const localUtils = require('../../../index');
|
|
||||||
|
|
||||||
const forPost = (id, attrs, frame) => {
|
|
||||||
attrs.url = urlService.getUrlByResourceId(id, {absolute: true});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CASE: admin api should serve preview urls
|
|
||||||
*
|
|
||||||
* @NOTE
|
|
||||||
* The url service has no clue of the draft/scheduled concept. It only generates urls for published resources.
|
|
||||||
* Adding a hardcoded fallback into the url service feels wrong IMO.
|
|
||||||
*
|
|
||||||
* Imagine the site won't be part of core and core does not serve urls anymore.
|
|
||||||
* Core needs to offer a preview API, which returns draft posts.
|
|
||||||
* That means the url is no longer /p/:uuid, it's e.g. GET /api/v2/content/preview/:uuid/.
|
|
||||||
* /p/ is a concept of the site, not of core.
|
|
||||||
*
|
|
||||||
* The site is not aware of existing drafts. It won't be able to get the uuid.
|
|
||||||
*
|
|
||||||
* Needs further discussion.
|
|
||||||
*/
|
|
||||||
if (!localUtils.isContentAPI(frame)) {
|
|
||||||
if (attrs.status !== 'published' && attrs.url.match(/\/404\//)) {
|
|
||||||
attrs.url = urlUtils.urlFor({
|
|
||||||
relativeUrl: urlUtils.urlJoin('/p', attrs.uuid, '/')
|
|
||||||
}, null, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.options.columns && !frame.options.columns.includes('url')) {
|
|
||||||
delete attrs.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forUser = (id, attrs, options) => {
|
|
||||||
if (!options.columns || (options.columns && options.columns.includes('url'))) {
|
|
||||||
attrs.url = urlService.getUrlByResourceId(id, {absolute: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forTag = (id, attrs, options) => {
|
|
||||||
if (!options.columns || (options.columns && options.columns.includes('url'))) {
|
|
||||||
attrs.url = urlService.getUrlByResourceId(id, {absolute: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forSettings = (attrs) => {
|
|
||||||
return attrs;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forImage = (path) => {
|
|
||||||
return urlUtils.urlFor('image', {image: path}, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.forPost = forPost;
|
|
||||||
module.exports.forUser = forUser;
|
|
||||||
module.exports.forTag = forTag;
|
|
||||||
module.exports.forSettings = forSettings;
|
|
||||||
module.exports.forImage = forImage;
|
|
@ -1,15 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:serializers:output:webhooks');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
all(models, apiConfig, frame) {
|
|
||||||
debug('all');
|
|
||||||
// CASE: e.g. destroy returns null
|
|
||||||
if (!models) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.response = {
|
|
||||||
webhooks: [models.toJSON(frame.options)]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
get input() {
|
|
||||||
return require('./input');
|
|
||||||
},
|
|
||||||
|
|
||||||
get output() {
|
|
||||||
return require('./output');
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,85 +0,0 @@
|
|||||||
const jsonSchema = require('../utils/json-schema');
|
|
||||||
const config = require('../../../../../../shared/config');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const {imageSize, blogIcon} = require('../../../../../lib/image');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
isNotSquare: 'Please select a valid image file with square dimensions.',
|
|
||||||
invalidFile: 'Please select a valid image.'
|
|
||||||
};
|
|
||||||
|
|
||||||
const profileImage = (frame) => {
|
|
||||||
return imageSize.getImageSizeFromPath(frame.file.path).then((response) => {
|
|
||||||
// save the image dimensions in new property for file
|
|
||||||
frame.file.dimensions = response;
|
|
||||||
|
|
||||||
// CASE: file needs to be a square
|
|
||||||
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
|
|
||||||
return Promise.reject(new errors.ValidationError({
|
|
||||||
message: tpl(messages.isNotSquare)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const icon = (frame) => {
|
|
||||||
const iconExtensions = (config.get('uploads').icons && config.get('uploads').icons.extensions) || [];
|
|
||||||
|
|
||||||
const validIconFileSize = (size) => {
|
|
||||||
return (size / 1024) <= 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
// CASE: file should not be larger than 100kb
|
|
||||||
if (!validIconFileSize(frame.file.size)) {
|
|
||||||
return Promise.reject(new errors.ValidationError({
|
|
||||||
message: tpl(messages.invalidFile, {extensions: iconExtensions})
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return blogIcon.getIconDimensions(frame.file.path).then((response) => {
|
|
||||||
// save the image dimensions in new property for file
|
|
||||||
frame.file.dimensions = response;
|
|
||||||
|
|
||||||
// CASE: file needs to be a square
|
|
||||||
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
|
|
||||||
return Promise.reject(new errors.ValidationError({
|
|
||||||
message: tpl(messages.invalidFile, {extensions: iconExtensions})
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// CASE: icon needs to be bigger than or equal to 60px
|
|
||||||
// .ico files can contain multiple sizes, we need at least a minimum of 60px (16px is ok, as long as 60px are present as well)
|
|
||||||
if (frame.file.dimensions.width < 60) {
|
|
||||||
return Promise.reject(new errors.ValidationError({
|
|
||||||
message: tpl(messages.invalidFile, {extensions: iconExtensions})
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// CASE: icon needs to be smaller than or equal to 1000px
|
|
||||||
if (frame.file.dimensions.width > 1000) {
|
|
||||||
return Promise.reject(new errors.ValidationError({
|
|
||||||
message: tpl(messages.invalidFile, {extensions: iconExtensions})
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
upload(apiConfig, frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return jsonSchema.validate(apiConfig, frame);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
if (frame.data.purpose === 'profile_image') {
|
|
||||||
return profileImage(frame);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
if (frame.data.purpose === 'icon') {
|
|
||||||
return icon(frame);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,45 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
get passwordreset() {
|
|
||||||
return require('./passwordreset');
|
|
||||||
},
|
|
||||||
|
|
||||||
get setup() {
|
|
||||||
return require('./setup');
|
|
||||||
},
|
|
||||||
|
|
||||||
get posts() {
|
|
||||||
return require('./posts');
|
|
||||||
},
|
|
||||||
|
|
||||||
get pages() {
|
|
||||||
return require('./pages');
|
|
||||||
},
|
|
||||||
|
|
||||||
get invites() {
|
|
||||||
return require('./invites');
|
|
||||||
},
|
|
||||||
|
|
||||||
get invitations() {
|
|
||||||
return require('./invitations');
|
|
||||||
},
|
|
||||||
|
|
||||||
get settings() {
|
|
||||||
return require('./settings');
|
|
||||||
},
|
|
||||||
|
|
||||||
get tags() {
|
|
||||||
return require('./tags');
|
|
||||||
},
|
|
||||||
|
|
||||||
get users() {
|
|
||||||
return require('./users');
|
|
||||||
},
|
|
||||||
|
|
||||||
get images() {
|
|
||||||
return require('./images');
|
|
||||||
},
|
|
||||||
|
|
||||||
get oembed() {
|
|
||||||
return require('./oembed');
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,49 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const validator = require('@tryghost/validator');
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:validators:input:invitation');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
noTokenProvided: 'No token provided.',
|
|
||||||
noEmailProvided: 'No email provided.',
|
|
||||||
noPasswordProvided: 'No password provided.',
|
|
||||||
noNameProvided: 'No name provided.',
|
|
||||||
invalidEmailReceived: 'The server did not receive a valid email'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
acceptInvitation(apiConfig, frame) {
|
|
||||||
debug('acceptInvitation');
|
|
||||||
|
|
||||||
const data = frame.data.invitation[0];
|
|
||||||
|
|
||||||
if (!data.token) {
|
|
||||||
return Promise.reject(new errors.ValidationError({message: tpl(messages.noTokenProvided)}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.email) {
|
|
||||||
return Promise.reject(new errors.ValidationError({message: tpl(messages.noEmailProvided)}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.password) {
|
|
||||||
return Promise.reject(new errors.ValidationError({message: tpl(messages.noPasswordProvided)}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.name) {
|
|
||||||
return Promise.reject(new errors.ValidationError({message: tpl(messages.noNameProvided)}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isInvitation(apiConfig, frame) {
|
|
||||||
debug('isInvitation');
|
|
||||||
|
|
||||||
const email = frame.data.email;
|
|
||||||
|
|
||||||
if (typeof email !== 'string' || !validator.isEmail(email)) {
|
|
||||||
throw new errors.BadRequestError({
|
|
||||||
message: tpl(messages.invalidEmailReceived)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,21 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const models = require('../../../../../models');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
userAlreadyRegistered: 'User is already registered.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
add(apiConfig, frame) {
|
|
||||||
return models.User.findOne({email: frame.data.invites[0].email}, frame.options)
|
|
||||||
.then((user) => {
|
|
||||||
if (user) {
|
|
||||||
return Promise.reject(new errors.ValidationError({
|
|
||||||
message: tpl(messages.userAlreadyRegistered)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,17 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
noUrlProvided: 'No url provided.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
read(apiConfig, frame) {
|
|
||||||
if (!frame.data.url || !frame.data.url.trim()) {
|
|
||||||
return Promise.reject(new errors.BadRequestError({
|
|
||||||
message: tpl(messages.noUrlProvided)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,46 +0,0 @@
|
|||||||
const jsonSchema = require('../utils/json-schema');
|
|
||||||
const models = require('../../../../../models');
|
|
||||||
const {ValidationError} = require('@tryghost/errors');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
invalidVisibilityFilter: 'Invalid filter in visibility property'
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateVisibility = async function (frame) {
|
|
||||||
if (!frame.data.pages || !frame.data.pages[0]) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate visibility - not done at schema level because this can be an NQL query so needs model access
|
|
||||||
const visibility = frame.data.pages[0].visibility;
|
|
||||||
if (visibility) {
|
|
||||||
if (!['public', 'members', 'paid'].includes(visibility)) {
|
|
||||||
// check filter is valid
|
|
||||||
try {
|
|
||||||
await models.Member.findPage({filter: visibility, limit: 1});
|
|
||||||
return Promise.resolve();
|
|
||||||
} catch (err) {
|
|
||||||
return Promise.reject(new ValidationError({
|
|
||||||
message: tpl(messages.invalidVisibilityFilter),
|
|
||||||
property: 'visibility'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
add(apiConfig, frame) {
|
|
||||||
return jsonSchema.validate(...arguments).then(() => {
|
|
||||||
return validateVisibility(frame);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
edit(apiConfig, frame) {
|
|
||||||
return jsonSchema.validate(...arguments).then(() => {
|
|
||||||
return validateVisibility(frame);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,36 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const validator = require('@tryghost/validator');
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:validators:input:passwordreset');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
newPasswordsDoNotMatch: 'Your new passwords do not match',
|
|
||||||
invalidEmailReceived: 'The server did not receive a valid email'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
resetPassword(apiConfig, frame) {
|
|
||||||
debug('resetPassword');
|
|
||||||
|
|
||||||
const data = frame.data.passwordreset[0];
|
|
||||||
|
|
||||||
if (data.newPassword !== data.ne2Password) {
|
|
||||||
return Promise.reject(new errors.ValidationError({
|
|
||||||
message: tpl(messages.newPasswordsDoNotMatch)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
generateResetToken(apiConfig, frame) {
|
|
||||||
debug('generateResetToken');
|
|
||||||
|
|
||||||
const email = frame.data.passwordreset[0].email;
|
|
||||||
|
|
||||||
if (typeof email !== 'string' || !validator.isEmail(email)) {
|
|
||||||
throw new errors.BadRequestError({
|
|
||||||
message: tpl(messages.invalidEmailReceived)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,46 +0,0 @@
|
|||||||
const jsonSchema = require('../utils/json-schema');
|
|
||||||
const models = require('../../../../../models');
|
|
||||||
const {ValidationError} = require('@tryghost/errors');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
invalidVisibilityFilter: 'Invalid filter in visibility property'
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateVisibility = async function (frame) {
|
|
||||||
if (!frame.data.posts || !frame.data.posts[0]) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate visibility - not done at schema level because this can be an NQL query so needs model access
|
|
||||||
const visibility = frame.data.posts[0].visibility;
|
|
||||||
if (visibility) {
|
|
||||||
if (!['public', 'members', 'paid'].includes(visibility)) {
|
|
||||||
// check filter is valid
|
|
||||||
try {
|
|
||||||
await models.Member.findPage({filter: visibility, limit: 1});
|
|
||||||
return Promise.resolve();
|
|
||||||
} catch (err) {
|
|
||||||
return Promise.reject(new ValidationError({
|
|
||||||
message: tpl(messages.invalidVisibilityFilter),
|
|
||||||
property: 'visibility'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
add(apiConfig, frame) {
|
|
||||||
return jsonSchema.validate(...arguments).then(() => {
|
|
||||||
return validateVisibility(frame);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
edit(apiConfig, frame) {
|
|
||||||
return jsonSchema.validate(...arguments).then(() => {
|
|
||||||
return validateVisibility(frame);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,79 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const {BadRequestError, ValidationError} = require('@tryghost/errors');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
error: 'Attempted to change active_theme via settings API',
|
|
||||||
help: 'Please activate theme via the themes API endpoints instead',
|
|
||||||
schemaValidationFailed: 'Validation failed for \'{key}\'.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
edit(apiConfig, frame) {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
_.each(frame.data.settings, (setting) => {
|
|
||||||
if (setting.key === 'active_theme') {
|
|
||||||
// @NOTE: active theme has to be changed via theme endpoints
|
|
||||||
errors.push(
|
|
||||||
new BadRequestError({
|
|
||||||
message: tpl(messages.error),
|
|
||||||
help: tpl(messages.help)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.key === 'unsplash') {
|
|
||||||
// NOTE: unsplash is expected to have object format in v2 API to keep back compatibility
|
|
||||||
try {
|
|
||||||
JSON.parse(setting.value);
|
|
||||||
} catch (e) {
|
|
||||||
errors.push(
|
|
||||||
new ValidationError({
|
|
||||||
message: tpl(messages.schemaValidationFailed, {
|
|
||||||
key: 'unsplash'
|
|
||||||
}),
|
|
||||||
property: 'unsplash'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: the below array is INCOMPLETE
|
|
||||||
// it should include all setting values that have array as a type
|
|
||||||
const arrayTypeSettings = [
|
|
||||||
'notifications',
|
|
||||||
'navigation',
|
|
||||||
'secondary_navigation'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (arrayTypeSettings.includes(setting.key)) {
|
|
||||||
const typeError = new ValidationError({
|
|
||||||
message: `Value in ${setting.key} should be an array.`,
|
|
||||||
property: 'value'
|
|
||||||
});
|
|
||||||
|
|
||||||
// NOTE: The additional check on raw value is here because internal calls to
|
|
||||||
// settings API use raw unstringified objects (e.g. when adding notifications)
|
|
||||||
// The conditional can be removed once internals are changed to do the calls properly
|
|
||||||
// and the JSON.parse should be left as the only valid way to check the value.
|
|
||||||
if (!_.isArray(setting.value)) {
|
|
||||||
try {
|
|
||||||
const parsedSettingValue = JSON.parse(setting.value);
|
|
||||||
|
|
||||||
if (!_.isArray(parsedSettingValue)) {
|
|
||||||
errors.push(typeError);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
errors.push(typeError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
return Promise.reject(errors[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,17 +0,0 @@
|
|||||||
const debug = require('@tryghost/debug')('api:v2:utils:validators:input:updateSetup');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
notTheBlogOwner: 'You are not the site owner.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
updateSetup(apiConfig, frame) {
|
|
||||||
debug('resetPassword');
|
|
||||||
|
|
||||||
if (!frame.options.context || !frame.options.context.user) {
|
|
||||||
throw new errors.NoPermissionError({message: tpl(messages.notTheBlogOwner)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
const jsonSchema = require('../utils/json-schema');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
add: jsonSchema.validate,
|
|
||||||
edit: jsonSchema.validate
|
|
||||||
};
|
|
@ -1,21 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const debug = require('@tryghost/debug')('api:v2:utils:validators:input:users');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const messages = {
|
|
||||||
newPasswordsDoNotMatch: 'Your new passwords do not match'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
changePassword(apiConfig, frame) {
|
|
||||||
debug('changePassword');
|
|
||||||
|
|
||||||
const data = frame.data.password[0];
|
|
||||||
|
|
||||||
if (data.newPassword !== data.ne2Password) {
|
|
||||||
return Promise.reject(new errors.ValidationError({
|
|
||||||
message: tpl(messages.newPasswordsDoNotMatch)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
module.exports = {};
|
|
@ -1,17 +0,0 @@
|
|||||||
const jsonSchema = require('@tryghost/admin-api-schema');
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Object} apiConfig "frame" api configuration object
|
|
||||||
* @param {string} apiConfig.docName the name of the resource
|
|
||||||
* @param {string} apiConfig.method API's method name
|
|
||||||
* @param {Object} frame "frame" object with data attached to it
|
|
||||||
* @param {Object} frame.data request data to validate
|
|
||||||
*/
|
|
||||||
const validate = async (apiConfig, frame) => await jsonSchema.validate({
|
|
||||||
data: frame.data,
|
|
||||||
schema: `${apiConfig.docName}-${apiConfig.method}`,
|
|
||||||
version: 'v2'
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports.validate = validate;
|
|
@ -1,163 +0,0 @@
|
|||||||
const models = require('../../models');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
resourceNotFound: '{resource} not found.',
|
|
||||||
noPermissionToEdit: {
|
|
||||||
message: 'You do not have permission to {method} this webhook.',
|
|
||||||
context: 'You may only {method} webhooks that belong to the authenticated integration. Check the supplied Admin API Key.'
|
|
||||||
},
|
|
||||||
webhookAlreadyExists: 'Target URL has already been used for this event.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'webhooks',
|
|
||||||
|
|
||||||
add: {
|
|
||||||
statusCode: 201,
|
|
||||||
headers: {
|
|
||||||
// NOTE: remove if there is ever a 'read' method
|
|
||||||
location: false
|
|
||||||
},
|
|
||||||
options: [],
|
|
||||||
data: [],
|
|
||||||
validation: {
|
|
||||||
data: {
|
|
||||||
event: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
target_url: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Webhook.getByEventAndTarget(
|
|
||||||
frame.data.webhooks[0].event,
|
|
||||||
frame.data.webhooks[0].target_url,
|
|
||||||
frame.options
|
|
||||||
).then((webhook) => {
|
|
||||||
if (webhook) {
|
|
||||||
return Promise.reject(
|
|
||||||
new errors.ValidationError({message: tpl(messages.webhookAlreadyExists)})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return models.Webhook.add(frame.data.webhooks[0], frame.options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
edit: {
|
|
||||||
permissions: {
|
|
||||||
before: (frame) => {
|
|
||||||
if (frame.options.context && frame.options.context.integration && frame.options.context.integration.id) {
|
|
||||||
return models.Webhook.findOne({id: frame.options.id})
|
|
||||||
.then((webhook) => {
|
|
||||||
if (!webhook) {
|
|
||||||
throw new errors.NotFoundError({
|
|
||||||
message: tpl(messages.resourceNotFound, {
|
|
||||||
resource: 'Webhook'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webhook.get('integration_id') !== frame.options.context.integration.id) {
|
|
||||||
throw new errors.NoPermissionError({
|
|
||||||
message: tpl(messages.noPermissionToEdit.message, {
|
|
||||||
method: 'edit'
|
|
||||||
}),
|
|
||||||
context: tpl(messages.noPermissionToEdit.context, {
|
|
||||||
method: 'edit'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
'name',
|
|
||||||
'event',
|
|
||||||
'target_url',
|
|
||||||
'secret',
|
|
||||||
'api_version'
|
|
||||||
],
|
|
||||||
options: [
|
|
||||||
'id'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query({data, options}) {
|
|
||||||
return models.Webhook.edit(data.webhooks[0], Object.assign(options, {require: true}))
|
|
||||||
.catch(models.Webhook.NotFoundError, () => {
|
|
||||||
throw new errors.NotFoundError({
|
|
||||||
message: tpl(messages.resourceNotFound, {
|
|
||||||
resource: 'Webhook'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: {
|
|
||||||
statusCode: 204,
|
|
||||||
headers: {},
|
|
||||||
options: [
|
|
||||||
'id'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
options: {
|
|
||||||
id: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
before: (frame) => {
|
|
||||||
if (frame.options.context && frame.options.context.integration && frame.options.context.integration.id) {
|
|
||||||
return models.Webhook.findOne({id: frame.options.id})
|
|
||||||
.then((webhook) => {
|
|
||||||
if (!webhook) {
|
|
||||||
throw new errors.NotFoundError({
|
|
||||||
message: tpl(messages.resourceNotFound, {
|
|
||||||
resource: 'Webhook'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webhook.get('integration_id') !== frame.options.context.integration.id) {
|
|
||||||
throw new errors.NoPermissionError({
|
|
||||||
message: tpl(messages.noPermissionToEdit.message, {
|
|
||||||
method: 'destroy'
|
|
||||||
}),
|
|
||||||
context: tpl(messages.noPermissionToEdit.context, {
|
|
||||||
method: 'destroy'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
frame.options.require = true;
|
|
||||||
return models.Webhook.destroy(frame.options)
|
|
||||||
.then(() => null)
|
|
||||||
.catch(models.Webhook.NotFoundError, () => {
|
|
||||||
return Promise.reject(new errors.NotFoundError({
|
|
||||||
message: tpl(messages.resourceNotFound, {
|
|
||||||
resource: 'Webhook'
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
const models = require('../../models');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'actions',
|
|
||||||
|
|
||||||
browse: {
|
|
||||||
options: [
|
|
||||||
'page',
|
|
||||||
'limit',
|
|
||||||
'fields',
|
|
||||||
'include',
|
|
||||||
'filter'
|
|
||||||
],
|
|
||||||
permissions: true,
|
|
||||||
query(frame) {
|
|
||||||
return models.Action.findPage(frame.options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,192 +0,0 @@
|
|||||||
const api = require('./index');
|
|
||||||
const config = require('../../../shared/config');
|
|
||||||
const errors = require('@tryghost/errors');
|
|
||||||
const web = require('../../web');
|
|
||||||
const models = require('../../models');
|
|
||||||
const auth = require('../../services/auth');
|
|
||||||
const invitations = require('../../services/invitations');
|
|
||||||
const tpl = require('@tryghost/tpl');
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
notTheBlogOwner: 'You are not the site owner.'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
docName: 'authentication',
|
|
||||||
|
|
||||||
setup: {
|
|
||||||
statusCode: 201,
|
|
||||||
permissions: false,
|
|
||||||
headers: {
|
|
||||||
cacheInvalidate: true
|
|
||||||
},
|
|
||||||
validation: {
|
|
||||||
docName: 'setup'
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(false)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
const setupDetails = {
|
|
||||||
name: frame.data.setup[0].name,
|
|
||||||
email: frame.data.setup[0].email,
|
|
||||||
password: frame.data.setup[0].password,
|
|
||||||
blogTitle: frame.data.setup[0].blogTitle,
|
|
||||||
status: 'active'
|
|
||||||
};
|
|
||||||
|
|
||||||
return auth.setup.setupUser(setupDetails);
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
return auth.setup.doSettings(data, api.settings);
|
|
||||||
})
|
|
||||||
.then((user) => {
|
|
||||||
return auth.setup.sendWelcomeEmail(user.get('email'), api.mail)
|
|
||||||
.then(() => user);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSetup: {
|
|
||||||
permissions: (frame) => {
|
|
||||||
return models.User.findOne({role: 'Owner', status: 'all'})
|
|
||||||
.then((owner) => {
|
|
||||||
if (owner.id !== frame.options.context.user) {
|
|
||||||
throw new errors.NoPermissionError({message: tpl(messages.notTheBlogOwner)});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
validation: {
|
|
||||||
docName: 'setup'
|
|
||||||
},
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(true)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
const setupDetails = {
|
|
||||||
name: frame.data.setup[0].name,
|
|
||||||
email: frame.data.setup[0].email,
|
|
||||||
password: frame.data.setup[0].password,
|
|
||||||
blogTitle: frame.data.setup[0].blogTitle,
|
|
||||||
status: 'active'
|
|
||||||
};
|
|
||||||
|
|
||||||
return auth.setup.setupUser(setupDetails);
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
return auth.setup.doSettings(data, api.settings);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isSetup: {
|
|
||||||
permissions: false,
|
|
||||||
async query() {
|
|
||||||
const isSetup = await auth.setup.checkIsSetup();
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: isSetup,
|
|
||||||
title: config.title,
|
|
||||||
name: config.user_name,
|
|
||||||
email: config.user_email
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
generateResetToken: {
|
|
||||||
validation: {
|
|
||||||
docName: 'passwordreset'
|
|
||||||
},
|
|
||||||
permissions: true,
|
|
||||||
options: [
|
|
||||||
'email'
|
|
||||||
],
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(true)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return auth.passwordreset.generateToken(frame.data.passwordreset[0].email, api.settings);
|
|
||||||
})
|
|
||||||
.then((token) => {
|
|
||||||
return auth.passwordreset.sendResetNotification(token, api.mail);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
resetPassword: {
|
|
||||||
validation: {
|
|
||||||
docName: 'passwordreset',
|
|
||||||
data: {
|
|
||||||
newPassword: {required: true},
|
|
||||||
ne2Password: {required: true}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
permissions: false,
|
|
||||||
options: [
|
|
||||||
'ip'
|
|
||||||
],
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(true)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return auth.passwordreset.extractTokenParts(frame);
|
|
||||||
})
|
|
||||||
.then((params) => {
|
|
||||||
return auth.passwordreset.protectBruteForce(params);
|
|
||||||
})
|
|
||||||
.then(({options, tokenParts}) => {
|
|
||||||
options = Object.assign(options, {context: {internal: true}});
|
|
||||||
return auth.passwordreset.doReset(options, tokenParts, api.settings)
|
|
||||||
.then((params) => {
|
|
||||||
web.shared.middleware.api.spamPrevention.userLogin().reset(frame.options.ip, `${tokenParts.email}login`);
|
|
||||||
return params;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
acceptInvitation: {
|
|
||||||
validation: {
|
|
||||||
docName: 'invitations'
|
|
||||||
},
|
|
||||||
permissions: false,
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(true)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return invitations.accept(frame.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isInvitation: {
|
|
||||||
data: [
|
|
||||||
'email'
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
docName: 'invitations'
|
|
||||||
},
|
|
||||||
permissions: false,
|
|
||||||
query(frame) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
return auth.setup.assertSetupCompleted(true)();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
const email = frame.data.email;
|
|
||||||
|
|
||||||
return models.Invite.findOne({email: email, status: 'sent'}, frame.options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user