mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-18 02:21:47 -05:00
💡 Added canary api endpoint
no issue Adds new canary api endpoint, currently replicating v2 endpoint but paving way for future updates to new version
This commit is contained in:
parent
acd1a7fd69
commit
7b761a8751
109 changed files with 6657 additions and 1 deletions
38
core/server/api/canary/actions.js
Normal file
38
core/server/api/canary/actions.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
};
|
186
core/server/api/canary/authentication.js
Normal file
186
core/server/api/canary/authentication.js
Normal file
|
@ -0,0 +1,186 @@
|
|||
const api = require('./index');
|
||||
const config = require('../../config');
|
||||
const common = require('../../lib/common');
|
||||
const web = require('../../web');
|
||||
const models = require('../../models');
|
||||
const auth = require('../../services/auth');
|
||||
const invitations = require('../../services/invitations');
|
||||
|
||||
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 common.errors.NoPermissionError({message: common.i18n.t('errors.api.authentication.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,
|
||||
query() {
|
||||
return auth.setup.checkIsSetup()
|
||||
.then((isSetup) => {
|
||||
return {
|
||||
status: isSetup,
|
||||
// Pre-populate from config if, and only if the values exist in config.
|
||||
title: config.title || undefined,
|
||||
name: config.user_name || undefined,
|
||||
email: config.user_email || undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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.middlewares.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);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
64
core/server/api/canary/authors-public.js
Normal file
64
core/server/api/canary/authors-public.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
const Promise = require('bluebird');
|
||||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
const ALLOWED_INCLUDES = ['count.posts'];
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.authors.notFound')
|
||||
}));
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
24
core/server/api/canary/config.js
Normal file
24
core/server/api/canary/config.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
const {isPlainObject} = require('lodash');
|
||||
const config = require('../../config');
|
||||
const labs = require('../../services/labs');
|
||||
const ghostVersion = require('../../lib/ghost-version');
|
||||
|
||||
module.exports = {
|
||||
docName: 'config',
|
||||
|
||||
read: {
|
||||
permissions: false,
|
||||
query() {
|
||||
return {
|
||||
version: ghostVersion.full,
|
||||
environment: config.get('env'),
|
||||
database: config.get('database').client,
|
||||
mail: isPlainObject(config.get('mail')) ? config.get('mail').transport : '',
|
||||
useGravatar: !config.isPrivacyDisabled('useGravatar'),
|
||||
labs: labs.getAll(),
|
||||
clientExtensions: config.get('clientExtensions') || {},
|
||||
enableDeveloperExperiments: config.get('enableDeveloperExperiments') || false
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
120
core/server/api/canary/db.js
Normal file
120
core/server/api/canary/db.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
const Promise = require('bluebird');
|
||||
const backupDatabase = require('../../data/db/backup');
|
||||
const exporter = require('../../data/exporter');
|
||||
const importer = require('../../data/importer');
|
||||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
|
||||
module.exports = {
|
||||
docName: 'db',
|
||||
|
||||
backupContent: {
|
||||
permissions: true,
|
||||
options: [
|
||||
'include',
|
||||
'filename'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
include: {
|
||||
values: exporter.EXCLUDED_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 backupDatabase(frame.options);
|
||||
}
|
||||
},
|
||||
|
||||
exportContent: {
|
||||
options: [
|
||||
'include'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
include: {
|
||||
values: exporter.EXCLUDED_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 common.errors.GhostError({err: err}));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
importContent: {
|
||||
options: [
|
||||
'include'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
include: {
|
||||
values: exporter.EXCLUDED_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 common.errors.GhostError({
|
||||
err: err
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return backupDatabase().then(deleteContent);
|
||||
}
|
||||
}
|
||||
};
|
19
core/server/api/canary/images.js
Normal file
19
core/server/api/canary/images.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
const storage = require('../../adapters/storage');
|
||||
|
||||
module.exports = {
|
||||
docName: 'images',
|
||||
upload: {
|
||||
statusCode: 201,
|
||||
permissions: false,
|
||||
query(frame) {
|
||||
const store = storage.getStorage();
|
||||
|
||||
if (frame.files) {
|
||||
return Promise
|
||||
.map(frame.files, file => store.save(file))
|
||||
.then(paths => paths[0]);
|
||||
}
|
||||
return store.save(frame.file);
|
||||
}
|
||||
}
|
||||
};
|
149
core/server/api/canary/index.js
Normal file
149
core/server/api/canary/index.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
const shared = require('../shared');
|
||||
const localUtils = require('./utils');
|
||||
|
||||
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 subscribers() {
|
||||
return shared.pipeline(require('./subscribers'), localUtils);
|
||||
},
|
||||
|
||||
get members() {
|
||||
return shared.pipeline(require('./members'), 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.canary.content` and
|
||||
* `api.canary.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');
|
||||
}
|
||||
};
|
145
core/server/api/canary/integrations.js
Normal file
145
core/server/api/canary/integrations.js
Normal file
|
@ -0,0 +1,145 @@
|
|||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.resource.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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.resource.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, () => {
|
||||
throw new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.resource.resourceNotFound', {
|
||||
resource: 'Integration'
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
176
core/server/api/canary/invites.js
Normal file
176
core/server/api/canary/invites.js
Normal file
|
@ -0,0 +1,176 @@
|
|||
const Promise = require('bluebird');
|
||||
const common = require('../../lib/common');
|
||||
const security = require('../../lib/security');
|
||||
const mailService = require('../../services/mail');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
const settingsCache = require('../../services/settings/cache');
|
||||
const models = require('../../models');
|
||||
const api = require('./index');
|
||||
const ALLOWED_INCLUDES = [];
|
||||
const UNSAFE_ATTRS = ['role_id'];
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.invites.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)
|
||||
.return(null);
|
||||
}
|
||||
},
|
||||
|
||||
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) {
|
||||
let invite;
|
||||
let emailData;
|
||||
|
||||
// CASE: ensure we destroy the invite before
|
||||
return models.Invite.findOne({email: frame.data.invites[0].email}, frame.options)
|
||||
.then((invite) => {
|
||||
if (!invite) {
|
||||
return;
|
||||
}
|
||||
|
||||
return invite.destroy(frame.options);
|
||||
})
|
||||
.then(() => {
|
||||
return models.Invite.add(frame.data.invites[0], frame.options);
|
||||
})
|
||||
.then((_invite) => {
|
||||
invite = _invite;
|
||||
|
||||
const adminUrl = urlUtils.urlFor('admin', true);
|
||||
|
||||
emailData = {
|
||||
blogName: settingsCache.get('title'),
|
||||
invitedByName: frame.user.get('name'),
|
||||
invitedByEmail: frame.user.get('email'),
|
||||
resetLink: urlUtils.urlJoin(adminUrl, 'signup', security.url.encodeBase64(invite.get('token')), '/')
|
||||
};
|
||||
|
||||
return mailService.utils.generateContent({data: emailData, template: 'invite-user'});
|
||||
})
|
||||
.then((emailContent) => {
|
||||
const payload = {
|
||||
mail: [{
|
||||
message: {
|
||||
to: invite.get('email'),
|
||||
subject: common.i18n.t('common.api.users.mail.invitedByName', {
|
||||
invitedByName: emailData.invitedByName,
|
||||
blogName: emailData.blogName
|
||||
}),
|
||||
html: emailContent.html,
|
||||
text: emailContent.text
|
||||
},
|
||||
options: {}
|
||||
}]
|
||||
};
|
||||
|
||||
return api.mail.send(payload, {context: {internal: true}});
|
||||
})
|
||||
.then(() => {
|
||||
return models.Invite.edit({
|
||||
status: 'sent'
|
||||
}, Object.assign({id: invite.id}, frame.options));
|
||||
})
|
||||
.then((invite) => {
|
||||
return invite;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err && err.errorType === 'EmailError') {
|
||||
const errorMessage = common.i18n.t('errors.api.invites.errorSendingEmail.error', {
|
||||
message: err.message
|
||||
});
|
||||
const helpText = common.i18n.t('errors.api.invites.errorSendingEmail.help');
|
||||
err.message = `${errorMessage} ${helpText}`;
|
||||
common.logging.warn(err.message);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
60
core/server/api/canary/mail.js
Normal file
60
core/server/api/canary/mail.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
const Promise = require('bluebird');
|
||||
const common = require('../../lib/common');
|
||||
const mailService = require('../../services/mail');
|
||||
const api = require('./');
|
||||
let mailer;
|
||||
let _private = {};
|
||||
|
||||
_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: [
|
||||
common.i18n.t('warnings.index.unableToSendEmail'),
|
||||
common.i18n.t('common.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: common.i18n.t('common.api.mail.testGhostEmail'),
|
||||
html: content.html,
|
||||
text: content.text
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
return _private.sendMail(payload);
|
||||
});
|
||||
}
|
||||
};
|
57
core/server/api/canary/members.js
Normal file
57
core/server/api/canary/members.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
// NOTE: We must not cache references to membersService.api
|
||||
// as it is a getter and may change during runtime.
|
||||
const membersService = require('../../services/members');
|
||||
|
||||
const members = {
|
||||
docName: 'members',
|
||||
browse: {
|
||||
options: [
|
||||
'limit',
|
||||
'fields',
|
||||
'filter',
|
||||
'order',
|
||||
'debug',
|
||||
'page'
|
||||
],
|
||||
permissions: true,
|
||||
validation: {},
|
||||
query(frame) {
|
||||
return membersService.api.members.list(frame.options);
|
||||
}
|
||||
},
|
||||
|
||||
read: {
|
||||
headers: {},
|
||||
data: [
|
||||
'id',
|
||||
'email'
|
||||
],
|
||||
validation: {},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
return membersService.api.members.get(frame.data, frame.options);
|
||||
}
|
||||
},
|
||||
|
||||
destroy: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
options: [
|
||||
'id'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
id: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
frame.options.require = true;
|
||||
return membersService.api.members.destroy(frame.options).return(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = members;
|
231
core/server/api/canary/notifications.js
Normal file
231
core/server/api/canary/notifications.js
Normal file
|
@ -0,0 +1,231 @@
|
|||
const moment = require('moment-timezone');
|
||||
const semver = require('semver');
|
||||
const Promise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const settingsCache = require('../../services/settings/cache');
|
||||
const ghostVersion = require('../../lib/ghost-version');
|
||||
const common = require('../../lib/common');
|
||||
const ObjectId = require('bson-objectid');
|
||||
const api = require('./index');
|
||||
const internalContext = {context: {internal: true}};
|
||||
const _private = {};
|
||||
|
||||
_private.fetchAllNotifications = () => {
|
||||
let allNotifications = settingsCache.get('notifications');
|
||||
|
||||
allNotifications.forEach((notification) => {
|
||||
notification.addedAt = moment(notification.addedAt).toDate();
|
||||
});
|
||||
|
||||
return allNotifications;
|
||||
};
|
||||
|
||||
_private.wasSeen = (notification, user) => {
|
||||
if (notification.seenBy === undefined) {
|
||||
return notification.seen;
|
||||
} else {
|
||||
return notification.seenBy.includes(user.id);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
docName: 'notifications',
|
||||
|
||||
browse: {
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
let allNotifications = _private.fetchAllNotifications();
|
||||
allNotifications = _.orderBy(allNotifications, 'addedAt', 'desc');
|
||||
|
||||
allNotifications = allNotifications.filter((notification) => {
|
||||
// NOTE: Filtering by version below is just a patch for bigger problem - notifications are not removed
|
||||
// after Ghost update. Logic below should be removed when Ghost upgrade detection
|
||||
// is done (https://github.com/TryGhost/Ghost/issues/10236) and notifications are
|
||||
// be removed permanently on upgrade event.
|
||||
const ghost20RegEx = /Ghost 2.0 is now available/gi;
|
||||
|
||||
// CASE: do not return old release notification
|
||||
if (notification.message && (!notification.custom || notification.message.match(ghost20RegEx))) {
|
||||
let notificationVersion = notification.message.match(/(\d+\.)(\d+\.)(\d+)/);
|
||||
|
||||
if (notification.message.match(ghost20RegEx)) {
|
||||
notificationVersion = '2.0.0';
|
||||
} else if (notificationVersion){
|
||||
notificationVersion = notificationVersion[0];
|
||||
}
|
||||
|
||||
const blogVersion = ghostVersion.full.match(/^(\d+\.)(\d+\.)(\d+)/);
|
||||
|
||||
if (notificationVersion && blogVersion && semver.gt(notificationVersion, blogVersion[0])) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return !_private.wasSeen(notification, frame.user);
|
||||
});
|
||||
|
||||
return allNotifications;
|
||||
}
|
||||
},
|
||||
|
||||
add: {
|
||||
statusCode(result) {
|
||||
if (result.notifications.length) {
|
||||
return 201;
|
||||
} else {
|
||||
return 200;
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
const defaults = {
|
||||
dismissible: true,
|
||||
location: 'bottom',
|
||||
status: 'alert',
|
||||
id: ObjectId.generate()
|
||||
};
|
||||
|
||||
const overrides = {
|
||||
seen: false,
|
||||
addedAt: moment().toDate()
|
||||
};
|
||||
|
||||
let notificationsToCheck = frame.data.notifications;
|
||||
let notificationsToAdd = [];
|
||||
|
||||
const allNotifications = _private.fetchAllNotifications();
|
||||
|
||||
notificationsToCheck.forEach((notification) => {
|
||||
const isDuplicate = allNotifications.find((n) => {
|
||||
return n.id === notification.id;
|
||||
});
|
||||
|
||||
if (!isDuplicate) {
|
||||
notificationsToAdd.push(Object.assign({}, defaults, notification, overrides));
|
||||
}
|
||||
});
|
||||
|
||||
const hasReleaseNotification = notificationsToCheck.find((notification) => {
|
||||
return !notification.custom;
|
||||
});
|
||||
|
||||
// CASE: remove any existing release notifications if a new release notification comes in
|
||||
if (hasReleaseNotification) {
|
||||
_.remove(allNotifications, (el) => {
|
||||
return !el.custom;
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: nothing to add, skip
|
||||
if (!notificationsToAdd.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const releaseNotificationsToAdd = notificationsToAdd.filter((notification) => {
|
||||
return !notification.custom;
|
||||
});
|
||||
|
||||
// CASE: reorder notifications before save
|
||||
if (releaseNotificationsToAdd.length > 1) {
|
||||
notificationsToAdd = notificationsToAdd.filter((notification) => {
|
||||
return notification.custom;
|
||||
});
|
||||
notificationsToAdd.push(_.orderBy(releaseNotificationsToAdd, 'created_at', 'desc')[0]);
|
||||
}
|
||||
|
||||
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,
|
||||
query(frame) {
|
||||
const allNotifications = _private.fetchAllNotifications();
|
||||
|
||||
const notificationToMarkAsSeen = allNotifications.find((notification) => {
|
||||
return notification.id === frame.options.notification_id;
|
||||
}),
|
||||
notificationToMarkAsSeenIndex = allNotifications.findIndex((notification) => {
|
||||
return notification.id === frame.options.notification_id;
|
||||
});
|
||||
|
||||
if (notificationToMarkAsSeenIndex > -1 && !notificationToMarkAsSeen.dismissible) {
|
||||
return Promise.reject(new common.errors.NoPermissionError({
|
||||
message: common.i18n.t('errors.api.notifications.noPermissionToDismissNotif')
|
||||
}));
|
||||
}
|
||||
|
||||
if (notificationToMarkAsSeenIndex < 0) {
|
||||
return Promise.reject(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.notifications.notificationDoesNotExist')
|
||||
}));
|
||||
}
|
||||
|
||||
if (_private.wasSeen(notificationToMarkAsSeen, frame.user)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// @NOTE: We don't remove the notifications, because otherwise we will receive them again from the service.
|
||||
allNotifications[notificationToMarkAsSeenIndex].seen = true;
|
||||
|
||||
if (!allNotifications[notificationToMarkAsSeenIndex].seenBy) {
|
||||
allNotifications[notificationToMarkAsSeenIndex].seenBy = [];
|
||||
}
|
||||
|
||||
allNotifications[notificationToMarkAsSeenIndex].seenBy.push(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 = _private.fetchAllNotifications();
|
||||
|
||||
allNotifications.forEach((notification) => {
|
||||
// @NOTE: We don't remove the notifications, because otherwise we will receive them again from the service.
|
||||
notification.seen = true;
|
||||
});
|
||||
|
||||
return api.settings.edit({
|
||||
settings: [{
|
||||
key: 'notifications',
|
||||
value: allNotifications
|
||||
}]
|
||||
}, internalContext).return();
|
||||
}
|
||||
}
|
||||
};
|
97
core/server/api/canary/oembed.js
Normal file
97
core/server/api/canary/oembed.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
const common = require('../../lib/common');
|
||||
const {extract, hasProvider} = require('oembed-parser');
|
||||
const Promise = require('bluebird');
|
||||
const request = require('../../lib/request');
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
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};
|
||||
};
|
||||
|
||||
const getOembedUrlFromHTML = (html) => {
|
||||
return cheerio('link[type="application/json+oembed"]', html).attr('href');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
docName: 'oembed',
|
||||
|
||||
read: {
|
||||
permissions: false,
|
||||
data: [
|
||||
'url'
|
||||
],
|
||||
options: [],
|
||||
query({data}) {
|
||||
let {url} = data;
|
||||
|
||||
function unknownProvider() {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.oembed.unknownProvider'),
|
||||
context: url
|
||||
}));
|
||||
}
|
||||
|
||||
function knownProvider(url) {
|
||||
return extract(url).catch((err) => {
|
||||
return Promise.reject(new common.errors.InternalServerError({
|
||||
message: err.message
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
let provider;
|
||||
({url, provider} = findUrlWithProvider(url));
|
||||
|
||||
if (provider) {
|
||||
return knownProvider(url);
|
||||
}
|
||||
|
||||
// see if the URL is a redirect to cater for shortened urls
|
||||
return request(url, {
|
||||
method: 'GET',
|
||||
timeout: 2 * 1000,
|
||||
followRedirect: true
|
||||
}).then((response) => {
|
||||
if (response.url !== url) {
|
||||
({url, provider} = findUrlWithProvider(response.url));
|
||||
return provider ? knownProvider(url) : unknownProvider();
|
||||
}
|
||||
|
||||
const oembedUrl = getOembedUrlFromHTML(response.body);
|
||||
|
||||
if (!oembedUrl) {
|
||||
return unknownProvider();
|
||||
}
|
||||
|
||||
return request(oembedUrl, {
|
||||
method: 'GET',
|
||||
json: true
|
||||
}).then((response) => {
|
||||
return response.body;
|
||||
});
|
||||
}).catch(() => {
|
||||
return unknownProvider();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
73
core/server/api/canary/pages-public.js
Normal file
73
core/server/api/canary/pages-public.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
const ALLOWED_INCLUDES = ['tags', 'authors'];
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.pages.pageNotFound')
|
||||
});
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
199
core/server/api/canary/pages.js
Normal file
199
core/server/api/canary/pages.js
Normal file
|
@ -0,0 +1,199 @@
|
|||
const models = require('../../models');
|
||||
const common = require('../../lib/common');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles'];
|
||||
const UNSAFE_ATTRS = ['status', 'authors'];
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.pages.pageNotFound')
|
||||
});
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
add: {
|
||||
statusCode: 201,
|
||||
headers: {},
|
||||
options: [
|
||||
'include'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
include: {
|
||||
values: ALLOWED_INCLUDES
|
||||
}
|
||||
}
|
||||
},
|
||||
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',
|
||||
// NOTE: only for internal context
|
||||
'forUpdate',
|
||||
'transacting'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
include: {
|
||||
values: ALLOWED_INCLUDES
|
||||
},
|
||||
id: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: {
|
||||
docName: 'posts',
|
||||
unsafeAttrs: UNSAFE_ATTRS
|
||||
},
|
||||
query(frame) {
|
||||
return models.Post.edit(frame.data.pages[0], frame.options)
|
||||
.then((model) => {
|
||||
if (
|
||||
model.get('status') === 'published' && model.wasChanged() ||
|
||||
model.get('status') === 'draft' && model.previous('status') === 'published'
|
||||
) {
|
||||
this.headers.cacheInvalidate = true;
|
||||
} else if (
|
||||
model.get('status') === 'draft' && model.previous('status') !== 'published' ||
|
||||
model.get('status') === 'scheduled' && model.wasChanged()
|
||||
) {
|
||||
this.headers.cacheInvalidate = {
|
||||
value: urlUtils.urlFor({
|
||||
relativeUrl: urlUtils.urlJoin('/p', model.get('uuid'), '/')
|
||||
})
|
||||
};
|
||||
} else {
|
||||
this.headers.cacheInvalidate = false;
|
||||
}
|
||||
|
||||
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)
|
||||
.return(null)
|
||||
.catch(models.Post.NotFoundError, () => {
|
||||
throw new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.pages.pageNotFound')
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
73
core/server/api/canary/posts-public.js
Normal file
73
core/server/api/canary/posts-public.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
const models = require('../../models');
|
||||
const common = require('../../lib/common');
|
||||
const allowedIncludes = ['tags', 'authors'];
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.posts.postNotFound')
|
||||
});
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
202
core/server/api/canary/posts.js
Normal file
202
core/server/api/canary/posts.js
Normal file
|
@ -0,0 +1,202 @@
|
|||
const models = require('../../models');
|
||||
const common = require('../../lib/common');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
const allowedIncludes = ['tags', 'authors', 'authors.roles'];
|
||||
const unsafeAttrs = ['status', 'authors'];
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.posts.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
|
||||
},
|
||||
query(frame) {
|
||||
return models.Post.edit(frame.data.posts[0], frame.options)
|
||||
.then((model) => {
|
||||
if (
|
||||
model.get('status') === 'published' && model.wasChanged() ||
|
||||
model.get('status') === 'draft' && model.previous('status') === 'published'
|
||||
) {
|
||||
this.headers.cacheInvalidate = true;
|
||||
} else if (
|
||||
model.get('status') === 'draft' && model.previous('status') !== 'published' ||
|
||||
model.get('status') === 'scheduled' && model.wasChanged()
|
||||
) {
|
||||
this.headers.cacheInvalidate = {
|
||||
value: urlUtils.urlFor({
|
||||
relativeUrl: urlUtils.urlJoin('/p', model.get('uuid'), '/')
|
||||
})
|
||||
};
|
||||
} else {
|
||||
this.headers.cacheInvalidate = false;
|
||||
}
|
||||
|
||||
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)
|
||||
.return(null)
|
||||
.catch(models.Post.NotFoundError, () => {
|
||||
throw new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.posts.postNotFound')
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
41
core/server/api/canary/preview.js
Normal file
41
core/server/api/canary/preview.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
const ALLOWED_INCLUDES = ['authors', 'tags'];
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.posts.postNotFound')
|
||||
});
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
33
core/server/api/canary/redirects.js
Normal file
33
core/server/api/canary/redirects.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
const web = require('../../web');
|
||||
const redirects = require('../../../frontend/services/redirects');
|
||||
|
||||
module.exports = {
|
||||
docName: 'redirects',
|
||||
|
||||
download: {
|
||||
headers: {
|
||||
disposition: {
|
||||
type: 'file',
|
||||
value: 'redirects.json'
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query() {
|
||||
return redirects.settings.get();
|
||||
}
|
||||
},
|
||||
|
||||
upload: {
|
||||
permissions: true,
|
||||
headers: {
|
||||
cacheInvalidate: true
|
||||
},
|
||||
query(frame) {
|
||||
return redirects.settings.setFromFilePath(frame.file.path)
|
||||
.then(() => {
|
||||
// CASE: trigger that redirects are getting re-registered
|
||||
web.shared.middlewares.customRedirects.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
19
core/server/api/canary/roles.js
Normal file
19
core/server/api/canary/roles.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
};
|
130
core/server/api/canary/schedules.js
Normal file
130
core/server/api/canary/schedules.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const config = require('../../config');
|
||||
const models = require('../../models');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
const common = require('../../lib/common');
|
||||
const api = require('./index');
|
||||
|
||||
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'
|
||||
},
|
||||
query(frame) {
|
||||
let resource;
|
||||
const resourceType = frame.options.resource;
|
||||
const publishAPostBySchedulerToleranceInMinutes = config.get('times').publishAPostBySchedulerToleranceInMinutes;
|
||||
|
||||
return models.Base.transaction((transacting) => {
|
||||
const options = {
|
||||
transacting: transacting,
|
||||
status: 'scheduled',
|
||||
forUpdate: true,
|
||||
id: frame.options.id,
|
||||
context: {
|
||||
internal: true
|
||||
}
|
||||
};
|
||||
|
||||
return api[resourceType].read({id: frame.options.id}, options)
|
||||
.then((result) => {
|
||||
resource = result[resourceType][0];
|
||||
const publishedAtMoment = moment(resource.published_at);
|
||||
|
||||
if (publishedAtMoment.diff(moment(), 'minutes') > publishAPostBySchedulerToleranceInMinutes) {
|
||||
return Promise.reject(new common.errors.NotFoundError({message: common.i18n.t('errors.api.job.notFound')}));
|
||||
}
|
||||
|
||||
if (publishedAtMoment.diff(moment(), 'minutes') < publishAPostBySchedulerToleranceInMinutes * -1 && frame.data.force !== true) {
|
||||
return Promise.reject(new common.errors.NotFoundError({message: common.i18n.t('errors.api.job.publishInThePast')}));
|
||||
}
|
||||
|
||||
const editedResource = {};
|
||||
editedResource[resourceType] = [{
|
||||
status: 'published',
|
||||
updated_at: moment(resource.updated_at).toISOString(true)
|
||||
}];
|
||||
|
||||
return api[resourceType].edit(
|
||||
editedResource,
|
||||
_.pick(options, ['context', 'id', 'transacting', 'forUpdate'])
|
||||
);
|
||||
})
|
||||
.then((result) => {
|
||||
const scheduledResource = result[resourceType][0];
|
||||
|
||||
if (
|
||||
(scheduledResource.status === 'published' && resource.status !== 'published') ||
|
||||
(scheduledResource.status === 'draft' && resource.status === 'published')
|
||||
) {
|
||||
this.headers.cacheInvalidate = true;
|
||||
} else if (
|
||||
(scheduledResource.status === 'draft' && resource.status !== 'published') ||
|
||||
(scheduledResource.status === 'scheduled' && resource.status !== 'scheduled')
|
||||
) {
|
||||
this.headers.cacheInvalidate = {
|
||||
value: urlUtils.urlFor({
|
||||
relativeUrl: urlUtils.urlJoin('/p', scheduledResource.uuid, '/')
|
||||
})
|
||||
};
|
||||
} else {
|
||||
this.headers.cacheInvalidate = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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 resourceType = frame.options.resource;
|
||||
const resourceModel = (resourceType === 'posts') ? 'Post' : 'Page';
|
||||
|
||||
const cleanOptions = {};
|
||||
cleanOptions.filter = 'status:scheduled';
|
||||
cleanOptions.columns = ['id', 'published_at', 'created_at'];
|
||||
|
||||
return models[resourceModel].findAll(cleanOptions)
|
||||
.then((result) => {
|
||||
let response = {};
|
||||
response[resourceType] = result;
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
50
core/server/api/canary/session.js
Normal file
50
core/server/api/canary/session.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
const Promise = require('bluebird');
|
||||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
const auth = require('../../services/auth');
|
||||
|
||||
const session = {
|
||||
read(options) {
|
||||
/*
|
||||
* 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: options.context.user});
|
||||
},
|
||||
add(object) {
|
||||
if (!object || !object.username || !object.password) {
|
||||
return Promise.reject(new common.errors.UnauthorizedError({
|
||||
message: common.i18n.t('errors.middleware.auth.accessDenied')
|
||||
}));
|
||||
}
|
||||
|
||||
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((err) => {
|
||||
throw new common.errors.UnauthorizedError({
|
||||
message: common.i18n.t('errors.middleware.auth.accessDenied'),
|
||||
err
|
||||
});
|
||||
});
|
||||
},
|
||||
delete() {
|
||||
return Promise.resolve((req, res, next) => {
|
||||
auth.session.destroySession(req, res, next);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = session;
|
17
core/server/api/canary/settings-public.js
Normal file
17
core/server/api/canary/settings-public.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
const settingsCache = require('../../services/settings/cache');
|
||||
const urlUtils = require('../../lib/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)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
171
core/server/api/canary/settings.js
Normal file
171
core/server/api/canary/settings.js
Normal file
|
@ -0,0 +1,171 @@
|
|||
const Promise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const models = require('../../models');
|
||||
const routing = require('../../../frontend/services/routing');
|
||||
const common = require('../../lib/common');
|
||||
const settingsCache = require('../../services/settings/cache');
|
||||
|
||||
const SETTINGS_BLACKLIST = [
|
||||
'members_public_key',
|
||||
'members_private_key',
|
||||
'members_session_secret'
|
||||
];
|
||||
|
||||
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.type === 'blog';
|
||||
}));
|
||||
}
|
||||
|
||||
// CASE: omit core settings unless internal request
|
||||
if (!frame.options.context.internal) {
|
||||
settings = _.filter(settings, (setting) => {
|
||||
const isCore = setting.type === 'core';
|
||||
const isBlacklisted = SETTINGS_BLACKLIST.includes(setting.key);
|
||||
return !isBlacklisted && !isCore;
|
||||
});
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
},
|
||||
|
||||
read: {
|
||||
options: ['key'],
|
||||
validation: {
|
||||
options: {
|
||||
key: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: {
|
||||
identifier(frame) {
|
||||
return frame.options.key;
|
||||
}
|
||||
},
|
||||
query(frame) {
|
||||
let setting = settingsCache.get(frame.options.key, {resolve: false});
|
||||
|
||||
if (!setting) {
|
||||
return Promise.reject(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.settings.problemFindingSetting', {
|
||||
key: frame.options.key
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
// @TODO: handle in settings model permissible fn
|
||||
if (setting.type === 'core' && !(frame.options.context && frame.options.context.internal)) {
|
||||
return Promise.reject(new common.errors.NoPermissionError({
|
||||
message: common.i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
[frame.options.key]: setting
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
edit: {
|
||||
headers: {
|
||||
cacheInvalidate: true
|
||||
},
|
||||
permissions: {
|
||||
before(frame) {
|
||||
const errors = [];
|
||||
|
||||
frame.data.settings.map((setting) => {
|
||||
if (setting.type === 'core' && !(frame.options.context && frame.options.context.internal)) {
|
||||
errors.push(new common.errors.NoPermissionError({
|
||||
message: common.i18n.t('errors.api.settings.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';
|
||||
});
|
||||
|
||||
const errors = [];
|
||||
|
||||
_.each(frame.data.settings, (setting) => {
|
||||
const settingFromCache = settingsCache.get(setting.key, {resolve: false});
|
||||
|
||||
if (!settingFromCache) {
|
||||
errors.push(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.settings.problemFindingSetting', {
|
||||
key: setting.key
|
||||
})
|
||||
}));
|
||||
} else if (settingFromCache.type === 'core' && !(frame.options.context && frame.options.context.internal)) {
|
||||
// @TODO: handle in settings model permissible fn
|
||||
errors.push(new common.errors.NoPermissionError({
|
||||
message: common.i18n.t('errors.api.settings.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'
|
||||
},
|
||||
query(frame) {
|
||||
return routing.settings.setFromFilePath(frame.file.path);
|
||||
}
|
||||
},
|
||||
|
||||
download: {
|
||||
headers: {
|
||||
disposition: {
|
||||
type: 'yaml',
|
||||
value: 'routes.yaml'
|
||||
}
|
||||
},
|
||||
response: {
|
||||
format: 'plain'
|
||||
},
|
||||
permissions: {
|
||||
method: 'browse'
|
||||
},
|
||||
query() {
|
||||
return routing.settings.get();
|
||||
}
|
||||
}
|
||||
};
|
20
core/server/api/canary/site.js
Normal file
20
core/server/api/canary/site.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
const ghostVersion = require('../../lib/ghost-version');
|
||||
const settingsCache = require('../../services/settings/cache');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
|
||||
const site = {
|
||||
docName: 'site',
|
||||
|
||||
read: {
|
||||
permissions: false,
|
||||
query() {
|
||||
return {
|
||||
title: settingsCache.get('title'),
|
||||
url: urlUtils.urlFor('home', true),
|
||||
version: ghostVersion.safe
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = site;
|
11
core/server/api/canary/slack.js
Normal file
11
core/server/api/canary/slack.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const common = require('../../lib/common');
|
||||
|
||||
module.exports = {
|
||||
docName: 'slack',
|
||||
sendTest: {
|
||||
permissions: false,
|
||||
query() {
|
||||
common.events.emit('slack.test');
|
||||
}
|
||||
}
|
||||
};
|
47
core/server/api/canary/slugs.js
Normal file
47
core/server/api/canary/slugs.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
const models = require('../../models');
|
||||
const common = require('../../lib/common');
|
||||
|
||||
const allowedTypes = {
|
||||
post: models.Post,
|
||||
tag: models.Tag,
|
||||
user: models.User,
|
||||
app: models.App
|
||||
};
|
||||
|
||||
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 common.errors.GhostError({
|
||||
message: common.i18n.t('errors.api.slugs.couldNotGenerateSlug')
|
||||
}));
|
||||
}
|
||||
return slug;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
215
core/server/api/canary/subscribers.js
Normal file
215
core/server/api/canary/subscribers.js
Normal file
|
@ -0,0 +1,215 @@
|
|||
const Promise = require('bluebird');
|
||||
const models = require('../../models');
|
||||
const fsLib = require('../../lib/fs');
|
||||
const common = require('../../lib/common');
|
||||
|
||||
const subscribers = {
|
||||
docName: 'subscribers',
|
||||
browse: {
|
||||
options: [
|
||||
'limit',
|
||||
'fields',
|
||||
'filter',
|
||||
'order',
|
||||
'debug',
|
||||
'page'
|
||||
],
|
||||
permissions: true,
|
||||
validation: {},
|
||||
query(frame) {
|
||||
return models.Subscriber.findPage(frame.options);
|
||||
}
|
||||
},
|
||||
|
||||
read: {
|
||||
headers: {},
|
||||
data: [
|
||||
'id',
|
||||
'email'
|
||||
],
|
||||
validation: {},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
return models.Subscriber.findOne(frame.data);
|
||||
}
|
||||
},
|
||||
|
||||
add: {
|
||||
statusCode: 201,
|
||||
headers: {},
|
||||
validation: {
|
||||
data: {
|
||||
email: {required: true}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
return models.Subscriber.getByEmail(frame.data.subscribers[0].email)
|
||||
.then((subscriber) => {
|
||||
if (subscriber && frame.options.context.external) {
|
||||
// we don't expose this information
|
||||
return Promise.resolve(subscriber);
|
||||
} else if (subscriber) {
|
||||
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.subscribers.subscriberAlreadyExists')}));
|
||||
}
|
||||
|
||||
return models.Subscriber
|
||||
.add(frame.data.subscribers[0])
|
||||
.catch((error) => {
|
||||
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
||||
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.subscribers.subscriberAlreadyExists')}));
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
edit: {
|
||||
headers: {},
|
||||
options: [
|
||||
'id'
|
||||
],
|
||||
validation: {
|
||||
id: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
return models.Subscriber.edit(frame.data.subscribers[0], frame.options)
|
||||
.then((model) => {
|
||||
if (!model) {
|
||||
return Promise.reject(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.subscribers.subscriberNotFound')
|
||||
}));
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
destroy: {
|
||||
statusCode: 204,
|
||||
headers: {
|
||||
cacheInvalidate: true
|
||||
},
|
||||
options: [
|
||||
'id',
|
||||
'email'
|
||||
],
|
||||
validation: {},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
/**
|
||||
* ### Delete Subscriber
|
||||
* If we have an email param, check the subscriber exists
|
||||
* @type {[type]}
|
||||
*/
|
||||
function getSubscriberByEmail(options) {
|
||||
if (options.email) {
|
||||
return models.Subscriber.getByEmail(options.email, options)
|
||||
.then((subscriber) => {
|
||||
if (!subscriber) {
|
||||
return Promise.reject(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.subscribers.subscriberNotFound')
|
||||
}));
|
||||
}
|
||||
|
||||
options.id = subscriber.get('id');
|
||||
|
||||
return options;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(options);
|
||||
}
|
||||
|
||||
return getSubscriberByEmail(frame.options)
|
||||
.then((options) => {
|
||||
return models.Subscriber
|
||||
.destroy(options)
|
||||
.return(null);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
exportCSV: {
|
||||
headers: {
|
||||
disposition: {
|
||||
type: 'csv',
|
||||
value() {
|
||||
const datetime = (new Date()).toJSON().substring(0, 10);
|
||||
return `subscribers.${datetime}.csv`;
|
||||
}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
format: 'plain'
|
||||
},
|
||||
permissions: {
|
||||
method: 'browse'
|
||||
},
|
||||
validation: {},
|
||||
query(frame) {
|
||||
return models.Subscriber.findAll(frame.options)
|
||||
.catch((err) => {
|
||||
return Promise.reject(new common.errors.GhostError({err: err}));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
importCSV: {
|
||||
statusCode: 201,
|
||||
permissions: {
|
||||
method: 'add'
|
||||
},
|
||||
validation: {},
|
||||
query(frame) {
|
||||
let filePath = frame.file.path,
|
||||
fulfilled = 0,
|
||||
invalid = 0,
|
||||
duplicates = 0;
|
||||
|
||||
return fsLib.readCSV({
|
||||
path: filePath,
|
||||
columnsToExtract: [{name: 'email', lookup: /email/i}]
|
||||
}).then((result) => {
|
||||
return Promise.all(result.map((entry) => {
|
||||
const apiCanary = require('./index');
|
||||
|
||||
return apiCanary.subscribers.add.query({
|
||||
data: {subscribers: [{email: entry.email}]},
|
||||
options: {
|
||||
context: frame.options.context
|
||||
}
|
||||
}).reflect();
|
||||
})).each((inspection) => {
|
||||
if (inspection.isFulfilled()) {
|
||||
fulfilled = fulfilled + 1;
|
||||
} else {
|
||||
if (inspection.reason() instanceof common.errors.ValidationError) {
|
||||
duplicates = duplicates + 1;
|
||||
} else {
|
||||
invalid = invalid + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
return {
|
||||
meta: {
|
||||
stats: {
|
||||
imported: fulfilled,
|
||||
duplicates: duplicates,
|
||||
invalid: invalid
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = subscribers;
|
66
core/server/api/canary/tags-public.js
Normal file
66
core/server/api/canary/tags-public.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
const Promise = require('bluebird');
|
||||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
|
||||
const ALLOWED_INCLUDES = ['count.posts'];
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.tags.tagNotFound')
|
||||
}));
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
148
core/server/api/canary/tags.js
Normal file
148
core/server/api/canary/tags.js
Normal file
|
@ -0,0 +1,148 @@
|
|||
const Promise = require('bluebird');
|
||||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
|
||||
const ALLOWED_INCLUDES = ['count.posts'];
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.tags.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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.tags.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).return(null);
|
||||
}
|
||||
}
|
||||
};
|
118
core/server/api/canary/themes.js
Normal file
118
core/server/api/canary/themes.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
const common = require('../../lib/common');
|
||||
const themeService = require('../../../frontend/services/themes');
|
||||
const models = require('../../models');
|
||||
|
||||
module.exports = {
|
||||
docName: 'themes',
|
||||
|
||||
browse: {
|
||||
permissions: true,
|
||||
query() {
|
||||
return themeService.getJSON();
|
||||
}
|
||||
},
|
||||
|
||||
activate: {
|
||||
headers: {
|
||||
cacheInvalidate: true
|
||||
},
|
||||
options: [
|
||||
'name'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
name: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
let themeName = frame.options.name;
|
||||
const newSettings = [{
|
||||
key: 'active_theme',
|
||||
value: themeName
|
||||
}];
|
||||
|
||||
return themeService.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.getJSON(themeName, checkedTheme);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
upload: {
|
||||
headers: {},
|
||||
permissions: {
|
||||
method: 'add'
|
||||
},
|
||||
query(frame) {
|
||||
// @NOTE: consistent filename uploads
|
||||
frame.options.originalname = frame.file.originalname.toLowerCase();
|
||||
|
||||
let zip = {
|
||||
path: frame.file.path,
|
||||
name: frame.file.originalname
|
||||
};
|
||||
|
||||
return themeService.storage.setFromZip(zip)
|
||||
.then(({theme, themeOverridden}) => {
|
||||
if (themeOverridden) {
|
||||
// CASE: clear cache
|
||||
this.headers.cacheInvalidate = true;
|
||||
}
|
||||
common.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.storage.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.storage.destroy(themeName);
|
||||
}
|
||||
}
|
||||
};
|
175
core/server/api/canary/users.js
Normal file
175
core/server/api/canary/users.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
const Promise = require('bluebird');
|
||||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
const permissionsService = require('../../services/permissions');
|
||||
const ALLOWED_INCLUDES = ['count.posts', 'permissions', 'roles', 'roles.permissions'];
|
||||
const UNSAFE_ATTRS = ['status', 'roles'];
|
||||
|
||||
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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.users.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 common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.users.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.Accesstoken.destroyByUser(frame.options),
|
||||
models.Refreshtoken.destroyByUser(frame.options),
|
||||
models.Post.destroyByAuthor(frame.options)
|
||||
]).then(() => {
|
||||
return models.User.destroy(Object.assign({status: 'all'}, frame.options));
|
||||
}).return(null);
|
||||
}).catch((err) => {
|
||||
return Promise.reject(new common.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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
34
core/server/api/canary/utils/index.js
Normal file
34
core/server/api/canary/utils/index.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
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';
|
||||
}
|
||||
};
|
102
core/server/api/canary/utils/permissions.js
Normal file
102
core/server/api/canary/utils/permissions.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary:utils:permissions');
|
||||
const Promise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const permissions = require('../../../services/permissions');
|
||||
const common = require('../../../lib/common');
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
const unsafeAttrObject = apiConfig.unsafeAttrs && _.has(frame, `data.[${apiConfig.docName}][0]`) ? _.pick(frame.data[apiConfig.docName][0], apiConfig.unsafeAttrs) : {};
|
||||
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 common.errors.NoPermissionError) {
|
||||
err.message = common.i18n.t('errors.api.utils.noPermissionToCall', {
|
||||
method: apiConfig.method,
|
||||
docName: apiConfig.docName
|
||||
});
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
if (common.errors.utils.isIgnitionError(err)) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
return Promise.reject(new common.errors.GhostError({
|
||||
err: err
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10735
|
||||
module.exports = {
|
||||
/**
|
||||
* @description Handle permission stage for canary API.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
};
|
9
core/server/api/canary/utils/serializers/index.js
Normal file
9
core/server/api/canary/utils/serializers/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
get input() {
|
||||
return require('./input');
|
||||
},
|
||||
|
||||
get output() {
|
||||
return require('./output');
|
||||
}
|
||||
};
|
26
core/server/api/canary/utils/serializers/input/authors.js
Normal file
26
core/server/api/canary/utils/serializers/input/authors.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:input:authors');
|
||||
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(apiConfig, frame) {
|
||||
debug('read');
|
||||
|
||||
if (utils.isContentAPI(frame)) {
|
||||
setDefaultOrder(frame);
|
||||
}
|
||||
}
|
||||
};
|
22
core/server/api/canary/utils/serializers/input/db.js
Normal file
22
core/server/api/canary/utils/serializers/input/db.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
const _ = require('lodash');
|
||||
const debug = require('ghost-ignition').debug('api:canary: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);
|
||||
}
|
||||
|
||||
debug(frame.options);
|
||||
}
|
||||
};
|
29
core/server/api/canary/utils/serializers/input/index.js
Normal file
29
core/server/api/canary/utils/serializers/input/index.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
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');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
const _ = require('lodash');
|
||||
const debug = require('ghost-ignition').debug('api:canary: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);
|
||||
}
|
||||
};
|
172
core/server/api/canary/utils/serializers/input/pages.js
Normal file
172
core/server/api/canary/utils/serializers/input/pages.js
Normal file
|
@ -0,0 +1,172 @@
|
|||
const _ = require('lodash');
|
||||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:input:pages');
|
||||
const converters = require('../../../../../lib/mobiledoc/converters');
|
||||
const url = require('./utils/url');
|
||||
const localUtils = require('../../index');
|
||||
|
||||
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 defaultFormat(frame) {
|
||||
if (frame.options.formats) {
|
||||
return;
|
||||
}
|
||||
|
||||
frame.options.formats = 'mobiledoc';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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})+page:true`;
|
||||
} else {
|
||||
frame.options.filter = 'page:true';
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (!localUtils.isContentAPI(frame)) {
|
||||
forceStatusFilter(frame);
|
||||
defaultFormat(frame);
|
||||
defaultRelations(frame);
|
||||
}
|
||||
|
||||
debug(frame.options);
|
||||
},
|
||||
|
||||
read(apiConfig, frame) {
|
||||
debug('read');
|
||||
|
||||
forcePageFilter(frame);
|
||||
|
||||
if (localUtils.isContentAPI(frame)) {
|
||||
removeMobiledocFormat(frame);
|
||||
setDefaultOrder(frame);
|
||||
}
|
||||
|
||||
if (!localUtils.isContentAPI(frame)) {
|
||||
forceStatusFilter(frame);
|
||||
defaultFormat(frame);
|
||||
defaultRelations(frame);
|
||||
}
|
||||
|
||||
debug(frame.options);
|
||||
},
|
||||
|
||||
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(converters.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].page = true;
|
||||
}
|
||||
|
||||
// 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
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
defaultFormat(frame);
|
||||
defaultRelations(frame);
|
||||
},
|
||||
|
||||
edit(apiConfig, frame) {
|
||||
this.add(...arguments, {add: false});
|
||||
|
||||
debug('edit');
|
||||
|
||||
forceStatusFilter(frame);
|
||||
forcePageFilter(frame);
|
||||
},
|
||||
|
||||
destroy(apiConfig, frame) {
|
||||
frame.options.destroyBy = {
|
||||
id: frame.options.id,
|
||||
page: true
|
||||
};
|
||||
|
||||
defaultFormat(frame);
|
||||
defaultRelations(frame);
|
||||
}
|
||||
};
|
205
core/server/api/canary/utils/serializers/input/posts.js
Normal file
205
core/server/api/canary/utils/serializers/input/posts.js
Normal file
|
@ -0,0 +1,205 @@
|
|||
const _ = require('lodash');
|
||||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:input:posts');
|
||||
const url = require('./utils/url');
|
||||
const localUtils = require('../../index');
|
||||
const labs = require('../../../../../services/labs');
|
||||
const converters = require('../../../../../lib/mobiledoc/converters');
|
||||
|
||||
function removeMobiledocFormat(frame) {
|
||||
if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
|
||||
frame.options.formats = frame.options.formats.filter((format) => {
|
||||
return (format !== 'mobiledoc');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function includeTags(frame) {
|
||||
if (!frame.options.withRelated) {
|
||||
frame.options.withRelated = ['tags'];
|
||||
} else if (!frame.options.withRelated.includes('tags')) {
|
||||
frame.options.withRelated.push('tags');
|
||||
}
|
||||
}
|
||||
|
||||
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 defaultFormat(frame) {
|
||||
if (frame.options.formats) {
|
||||
return;
|
||||
}
|
||||
|
||||
frame.options.formats = 'mobiledoc';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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})+page:false`;
|
||||
} else {
|
||||
frame.options.filter = 'page:false';
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// CASE: Members needs to have the tags to check if its allowed access
|
||||
if (labs.isSet('members')) {
|
||||
includeTags(frame);
|
||||
}
|
||||
|
||||
setDefaultOrder(frame);
|
||||
}
|
||||
|
||||
if (!localUtils.isContentAPI(frame)) {
|
||||
forceStatusFilter(frame);
|
||||
defaultFormat(frame);
|
||||
defaultRelations(frame);
|
||||
}
|
||||
|
||||
debug(frame.options);
|
||||
},
|
||||
|
||||
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);
|
||||
|
||||
if (labs.isSet('members')) {
|
||||
// CASE: Members needs to have the tags to check if its allowed access
|
||||
includeTags(frame);
|
||||
}
|
||||
|
||||
setDefaultOrder(frame);
|
||||
}
|
||||
|
||||
if (!localUtils.isContentAPI(frame)) {
|
||||
forceStatusFilter(frame);
|
||||
defaultFormat(frame);
|
||||
defaultRelations(frame);
|
||||
}
|
||||
|
||||
debug(frame.options);
|
||||
},
|
||||
|
||||
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(converters.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].page = false;
|
||||
}
|
||||
|
||||
// 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
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
defaultFormat(frame);
|
||||
defaultRelations(frame);
|
||||
},
|
||||
|
||||
edit(apiConfig, frame) {
|
||||
this.add(apiConfig, frame, {add: false});
|
||||
|
||||
forceStatusFilter(frame);
|
||||
forcePageFilter(frame);
|
||||
},
|
||||
|
||||
destroy(apiConfig, frame) {
|
||||
frame.options.destroyBy = {
|
||||
id: frame.options.id,
|
||||
page: false
|
||||
};
|
||||
|
||||
defaultFormat(frame);
|
||||
defaultRelations(frame);
|
||||
}
|
||||
};
|
61
core/server/api/canary/utils/serializers/input/settings.js
Normal file
61
core/server/api/canary/utils/serializers/input/settings.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
const _ = require('lodash');
|
||||
const url = require('./utils/url');
|
||||
|
||||
module.exports = {
|
||||
read(apiConfig, frame) {
|
||||
if (frame.options.key === 'codeinjection_head') {
|
||||
frame.options.key = 'ghost_head';
|
||||
}
|
||||
|
||||
if (frame.options.key === 'codeinjection_foot') {
|
||||
frame.options.key = 'ghost_foot';
|
||||
}
|
||||
},
|
||||
|
||||
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}]};
|
||||
}
|
||||
|
||||
frame.data.settings.forEach((setting) => {
|
||||
// CASE: transform objects/arrays into string (we store stringified objects in the db)
|
||||
// @TODO: This belongs into the model layer. We should stringify before saving and parse when fetching from db.
|
||||
// @TODO: Fix when dropping v0.1
|
||||
if (_.isObject(setting.value)) {
|
||||
setting.value = JSON.stringify(setting.value);
|
||||
}
|
||||
|
||||
// @TODO: handle these transformations in a centralised API place (these rules should apply for ALL resources)
|
||||
|
||||
// CASE: Ensure we won't forward strings, otherwise model events or model interactions can fail
|
||||
if (setting.value === '0' || setting.value === '1') {
|
||||
setting.value = !!+setting.value;
|
||||
}
|
||||
|
||||
// CASE: Ensure we won't forward strings, otherwise model events or model interactions can fail
|
||||
if (setting.value === 'false' || setting.value === 'true') {
|
||||
setting.value = setting.value === 'true';
|
||||
}
|
||||
|
||||
if (setting.key === 'codeinjection_head') {
|
||||
setting.key = 'ghost_head';
|
||||
}
|
||||
|
||||
if (setting.key === 'codeinjection_foot') {
|
||||
setting.key = 'ghost_foot';
|
||||
}
|
||||
|
||||
if (['cover_image', 'icon', 'logo'].includes(setting.key)) {
|
||||
setting = url.forSetting(setting);
|
||||
}
|
||||
});
|
||||
|
||||
// CASE: deprecated, won't accept
|
||||
const index = _.findIndex(frame.data.settings, {key: 'force_i18n'});
|
||||
|
||||
if (index !== -1) {
|
||||
frame.data.settings.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
35
core/server/api/canary/utils/serializers/input/tags.js
Normal file
35
core/server/api/canary/utils/serializers/input/tags.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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);
|
||||
}
|
||||
};
|
26
core/server/api/canary/utils/serializers/input/users.js
Normal file
26
core/server/api/canary/utils/serializers/input/users.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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]));
|
||||
}
|
||||
};
|
121
core/server/api/canary/utils/serializers/input/utils/url.js
Normal file
121
core/server/api/canary/utils/serializers/input/utils/url.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
const _ = require('lodash');
|
||||
const url = require('url');
|
||||
const urlUtils = require('../../../../../../lib/url-utils');
|
||||
|
||||
const handleCanonicalUrl = (canonicalUrl) => {
|
||||
const blogURl = urlUtils.getBlogUrl();
|
||||
const isSameProtocol = url.parse(canonicalUrl).protocol === url.parse(blogURl).protocol;
|
||||
const blogDomain = blogURl.replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const absolute = canonicalUrl.replace(/^http(s?):\/\//, '');
|
||||
|
||||
// We only want to transform to a relative URL when the canonical URL matches the current
|
||||
// Blog URL incl. the same protocol. This allows users to keep e.g. Facebook comments after
|
||||
// a http -> https switch
|
||||
if (absolute.startsWith(blogDomain) && isSameProtocol) {
|
||||
return urlUtils.absoluteToRelative(canonicalUrl);
|
||||
}
|
||||
|
||||
return canonicalUrl;
|
||||
};
|
||||
|
||||
const handleImageUrl = (imageUrl) => {
|
||||
const blogDomain = urlUtils.getBlogUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const imageUrlAbsolute = imageUrl.replace(/^http(s?):\/\//, '');
|
||||
const imagePathRe = new RegExp(`^${blogDomain}/${urlUtils.STATIC_IMAGE_URL_PREFIX}`);
|
||||
|
||||
if (imagePathRe.test(imageUrlAbsolute)) {
|
||||
return urlUtils.absoluteToRelative(imageUrl);
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
const handleContentUrls = (content) => {
|
||||
const blogDomain = urlUtils.getBlogUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const imagePathRe = new RegExp(`(http(s?)://)?${blogDomain}/${urlUtils.STATIC_IMAGE_URL_PREFIX}`, 'g');
|
||||
|
||||
const matches = _.uniq(content.match(imagePathRe));
|
||||
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
const relative = urlUtils.absoluteToRelative(match);
|
||||
content = content.replace(new RegExp(match, 'g'), relative);
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const forPost = (attrs, options) => {
|
||||
// make all content image URLs relative, ref: https://github.com/TryGhost/Ghost/issues/10477
|
||||
if (attrs.mobiledoc) {
|
||||
attrs.mobiledoc = handleContentUrls(attrs.mobiledoc);
|
||||
}
|
||||
|
||||
if (attrs.feature_image) {
|
||||
attrs.feature_image = handleImageUrl(attrs.feature_image);
|
||||
}
|
||||
|
||||
if (attrs.og_image) {
|
||||
attrs.og_image = handleImageUrl(attrs.og_image);
|
||||
}
|
||||
|
||||
if (attrs.twitter_image) {
|
||||
attrs.twitter_image = handleImageUrl(attrs.twitter_image);
|
||||
}
|
||||
|
||||
if (attrs.canonical_url) {
|
||||
attrs.canonical_url = handleCanonicalUrl(attrs.canonical_url);
|
||||
}
|
||||
|
||||
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) {
|
||||
attrs.value = handleImageUrl(attrs.value);
|
||||
}
|
||||
|
||||
return attrs;
|
||||
};
|
||||
|
||||
module.exports.forPost = forPost;
|
||||
module.exports.forUser = forUser;
|
||||
module.exports.forTag = forTag;
|
||||
module.exports.forSetting = forSetting;
|
15
core/server/api/canary/utils/serializers/output/actions.js
Normal file
15
core/server/api/canary/utils/serializers/output/actions.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
25
core/server/api/canary/utils/serializers/output/all.js
Normal file
25
core/server/api/canary/utils/serializers/output/all.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
const common = require('../../../../../lib/common');
|
||||
const mapper = require('./utils/mapper');
|
||||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:authentication');
|
||||
|
||||
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: common.i18n.t('common.api.authentication.mail.checkEmailForInstructions')
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
resetPassword(data, apiConfig, frame) {
|
||||
frame.response = {
|
||||
passwordreset: [{
|
||||
message: common.i18n.t('common.api.authentication.mail.passwordChanged')
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
acceptInvitation(data, apiConfig, frame) {
|
||||
debug('acceptInvitation');
|
||||
|
||||
frame.response = {
|
||||
invitation: [
|
||||
{message: common.i18n.t('common.api.authentication.mail.invitationAccepted')}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
isInvitation(data, apiConfig, frame) {
|
||||
debug('acceptInvitation');
|
||||
|
||||
frame.response = {
|
||||
invitation: [{
|
||||
valid: !!data
|
||||
}]
|
||||
};
|
||||
}
|
||||
};
|
25
core/server/api/canary/utils/serializers/output/authors.js
Normal file
25
core/server/api/canary/utils/serializers/output/authors.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
},
|
||||
|
||||
read(model, apiConfig, frame) {
|
||||
debug('read');
|
||||
|
||||
frame.response = {
|
||||
authors: [mapper.mapUser(model, frame)]
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
11
core/server/api/canary/utils/serializers/output/config.js
Normal file
11
core/server/api/canary/utils/serializers/output/config.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:config');
|
||||
|
||||
module.exports = {
|
||||
all(data, apiConfig, frame) {
|
||||
frame.response = {
|
||||
config: data
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
40
core/server/api/canary/utils/serializers/output/db.js
Normal file
40
core/server/api/canary/utils/serializers/output/db.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
15
core/server/api/canary/utils/serializers/output/images.js
Normal file
15
core/server/api/canary/utils/serializers/output/images.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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),
|
||||
ref: frame.data.ref || null
|
||||
}]
|
||||
};
|
||||
}
|
||||
};
|
109
core/server/api/canary/utils/serializers/output/index.js
Normal file
109
core/server/api/canary/utils/serializers/output/index.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
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 subscribers() {
|
||||
return require('./subscribers');
|
||||
},
|
||||
|
||||
get members() {
|
||||
return require('./members');
|
||||
},
|
||||
|
||||
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');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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)]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
26
core/server/api/canary/utils/serializers/output/invites.js
Normal file
26
core/server/api/canary/utils/serializers/output/invites.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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)]
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
20
core/server/api/canary/utils/serializers/output/mail.js
Normal file
20
core/server/api/canary/utils/serializers/output/mail.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
const _ = require('lodash');
|
||||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:mail');
|
||||
|
||||
module.exports = {
|
||||
all(response, apiConfig, frame) {
|
||||
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;
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
24
core/server/api/canary/utils/serializers/output/members.js
Normal file
24
core/server/api/canary/utils/serializers/output/members.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
const common = require('../../../../../lib/common');
|
||||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:members');
|
||||
|
||||
module.exports = {
|
||||
browse(data, apiConfig, frame) {
|
||||
debug('browse');
|
||||
|
||||
frame.response = data;
|
||||
},
|
||||
|
||||
read(data, apiConfig, frame) {
|
||||
debug('read');
|
||||
|
||||
if (!data) {
|
||||
return Promise.reject(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.members.memberNotFound')
|
||||
}));
|
||||
}
|
||||
|
||||
frame.response = {
|
||||
members: [data]
|
||||
};
|
||||
}
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:notifications');
|
||||
|
||||
module.exports = {
|
||||
all(response, apiConfig, frame) {
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response || !response.length) {
|
||||
frame.response = {
|
||||
notifications: []
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
response.forEach((notification) => {
|
||||
delete notification.seen;
|
||||
delete notification.seenBy;
|
||||
delete notification.addedAt;
|
||||
});
|
||||
|
||||
frame.response = {
|
||||
notifications: response
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:oembed');
|
||||
|
||||
module.exports = {
|
||||
all(res, apiConfig, frame) {
|
||||
debug('all');
|
||||
frame.response = res;
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
28
core/server/api/canary/utils/serializers/output/pages.js
Normal file
28
core/server/api/canary/utils/serializers/output/pages.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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)]
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
29
core/server/api/canary/utils/serializers/output/posts.js
Normal file
29
core/server/api/canary/utils/serializers/output/posts.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
return;
|
||||
}
|
||||
|
||||
frame.response = {
|
||||
posts: [mapper.mapPost(models, frame)]
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
all(model, apiConfig, frame) {
|
||||
frame.response = {
|
||||
preview: [model.toJSON(frame.options)]
|
||||
};
|
||||
}
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
download(response, apiConfig, frame) {
|
||||
frame.response = response;
|
||||
}
|
||||
};
|
28
core/server/api/canary/utils/serializers/output/roles.js
Normal file
28
core/server/api/canary/utils/serializers/output/roles.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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((roles) => {
|
||||
return frame.response = {
|
||||
roles: roles
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
all(model, apiConfig, frame) {
|
||||
frame.response = model;
|
||||
}
|
||||
};
|
61
core/server/api/canary/utils/serializers/output/settings.js
Normal file
61
core/server/api/canary/utils/serializers/output/settings.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
const _ = require('lodash');
|
||||
const utils = require('../../index');
|
||||
const mapper = require('./utils/mapper');
|
||||
const _private = {};
|
||||
const deprecatedSettings = ['force_i18n', 'permalinks'];
|
||||
|
||||
/**
|
||||
* ### Settings Filter
|
||||
* Filters an object based on a given filter object
|
||||
* @private
|
||||
* @param {Object} settings
|
||||
* @param {String} filter
|
||||
* @returns {*}
|
||||
*/
|
||||
_private.settingsFilter = (settings, filter) => {
|
||||
let filteredTypes = filter ? filter.split(',') : false;
|
||||
return _.filter(settings, (setting) => {
|
||||
if (filteredTypes) {
|
||||
return _.includes(filteredTypes, setting.type) && !_.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;
|
||||
}
|
||||
};
|
11
core/server/api/canary/utils/serializers/output/site.js
Normal file
11
core/server/api/canary/utils/serializers/output/site.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:site');
|
||||
|
||||
module.exports = {
|
||||
read(data, apiConfig, frame) {
|
||||
debug('read');
|
||||
|
||||
frame.response = {
|
||||
site: data
|
||||
};
|
||||
}
|
||||
};
|
13
core/server/api/canary/utils/serializers/output/slugs.js
Normal file
13
core/server/api/canary/utils/serializers/output/slugs.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:slugs');
|
||||
|
||||
module.exports = {
|
||||
all(slug, apiConfig, frame) {
|
||||
debug('all');
|
||||
|
||||
frame.response = {
|
||||
slugs: [{slug}]
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,83 @@
|
|||
const common = require('../../../../../lib/common');
|
||||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:subscribers');
|
||||
|
||||
module.exports = {
|
||||
browse(models, apiConfig, frame) {
|
||||
debug('browse');
|
||||
|
||||
frame.response = {
|
||||
subscribers: models.data.map(model => model.toJSON(frame.options)),
|
||||
meta: models.meta
|
||||
};
|
||||
},
|
||||
|
||||
read(models, apiConfig, frame) {
|
||||
debug('read');
|
||||
|
||||
if (!models) {
|
||||
return Promise.reject(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.subscribers.subscriberNotFound')
|
||||
}));
|
||||
}
|
||||
|
||||
frame.response = {
|
||||
subscribers: [models.toJSON(frame.options)]
|
||||
};
|
||||
},
|
||||
|
||||
add(models, apiConfig, frame) {
|
||||
debug('add');
|
||||
|
||||
frame.response = {
|
||||
subscribers: [models.toJSON(frame.options)]
|
||||
};
|
||||
},
|
||||
|
||||
edit(models, apiConfig, frame) {
|
||||
debug('edit');
|
||||
|
||||
frame.response = {
|
||||
subscribers: [models.toJSON(frame.options)]
|
||||
};
|
||||
},
|
||||
|
||||
destroy(models, apiConfig, frame) {
|
||||
frame.response = models;
|
||||
},
|
||||
|
||||
exportCSV(models, apiConfig, frame) {
|
||||
debug('exportCSV');
|
||||
|
||||
function formatCSV(data) {
|
||||
let fields = ['id', 'email', 'created_at', 'deleted_at'],
|
||||
csv = `${fields.join(',')}\r\n`,
|
||||
subscriber,
|
||||
field,
|
||||
j,
|
||||
i;
|
||||
|
||||
for (j = 0; j < data.length; j = j + 1) {
|
||||
subscriber = data[j];
|
||||
|
||||
for (i = 0; i < fields.length; i = i + 1) {
|
||||
field = fields[i];
|
||||
csv += subscriber[field] !== null ? subscriber[field] : '';
|
||||
if (i !== fields.length - 1) {
|
||||
csv += ',';
|
||||
}
|
||||
}
|
||||
csv += '\r\n';
|
||||
}
|
||||
|
||||
return csv;
|
||||
}
|
||||
|
||||
frame.response = formatCSV(models.toJSON(frame.options), frame.options);
|
||||
},
|
||||
|
||||
importCSV(models, apiConfig, frame) {
|
||||
debug('importCSV');
|
||||
|
||||
frame.response = models;
|
||||
}
|
||||
};
|
27
core/server/api/canary/utils/serializers/output/tags.js
Normal file
27
core/server/api/canary/utils/serializers/output/tags.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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)]
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
29
core/server/api/canary/utils/serializers/output/themes.js
Normal file
29
core/server/api/canary/utils/serializers/output/themes.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:themes');
|
||||
|
||||
module.exports = {
|
||||
browse(themes, apiConfig, frame) {
|
||||
debug('browse');
|
||||
|
||||
frame.response = themes;
|
||||
|
||||
debug(frame.response);
|
||||
},
|
||||
|
||||
upload() {
|
||||
debug('upload');
|
||||
this.browse(...arguments);
|
||||
},
|
||||
|
||||
activate() {
|
||||
debug('activate');
|
||||
this.browse(...arguments);
|
||||
},
|
||||
|
||||
download(fn, apiConfig, frame) {
|
||||
debug('download');
|
||||
|
||||
frame.response = fn;
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
49
core/server/api/canary/utils/serializers/output/users.js
Normal file
49
core/server/api/canary/utils/serializers/output/users.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:users');
|
||||
const common = require('../../../../../lib/common');
|
||||
const mapper = require('./utils/mapper');
|
||||
|
||||
module.exports = {
|
||||
browse(models, apiConfig, frame) {
|
||||
debug('browse');
|
||||
|
||||
frame.response = {
|
||||
users: models.data.map(model => mapper.mapUser(model, frame)),
|
||||
meta: models.meta
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
},
|
||||
|
||||
read(model, apiConfig, frame) {
|
||||
debug('read');
|
||||
|
||||
frame.response = {
|
||||
users: [mapper.mapUser(model, frame)]
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
},
|
||||
|
||||
edit() {
|
||||
debug('edit');
|
||||
this.read(...arguments);
|
||||
},
|
||||
|
||||
changePassword(models, apiConfig, frame) {
|
||||
debug('changePassword');
|
||||
|
||||
frame.response = {
|
||||
password: [{message: common.i18n.t('notices.api.users.pwdChangedSuccessfully')}]
|
||||
};
|
||||
},
|
||||
|
||||
transferOwnership(models, apiConfig, frame) {
|
||||
debug('transferOwnership');
|
||||
|
||||
frame.response = {
|
||||
users: models.map(model => model.toJSON(frame.options))
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
148
core/server/api/canary/utils/serializers/output/utils/clean.js
Normal file
148
core/server/api/canary/utils/serializers/output/utils/clean.js
Normal file
|
@ -0,0 +1,148 @@
|
|||
const _ = require('lodash');
|
||||
const localUtils = require('../../../index');
|
||||
|
||||
const tag = (attrs, frame) => {
|
||||
if (localUtils.isContentAPI(frame)) {
|
||||
delete attrs.created_at;
|
||||
delete attrs.updated_at;
|
||||
|
||||
// We are standardising on returning null from the Content API for any empty values
|
||||
if (attrs.meta_title === '') {
|
||||
attrs.meta_title = null;
|
||||
}
|
||||
if (attrs.meta_description === '') {
|
||||
attrs.meta_description = null;
|
||||
}
|
||||
if (attrs.description === '') {
|
||||
attrs.description = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Already deleted in model.toJSON, but leaving here so that we can clean that up when we deprecate v0.1
|
||||
delete attrs.parent_id;
|
||||
|
||||
// @NOTE: unused fields
|
||||
delete attrs.parent;
|
||||
|
||||
return attrs;
|
||||
};
|
||||
|
||||
const author = (attrs, frame) => {
|
||||
if (localUtils.isContentAPI(frame)) {
|
||||
// Already deleted in model.toJSON, but leaving here so that we can clean that up when we deprecate v0.1
|
||||
delete attrs.created_at;
|
||||
delete attrs.updated_at;
|
||||
delete attrs.last_seen;
|
||||
delete attrs.status;
|
||||
|
||||
// @NOTE: used for night shift
|
||||
delete attrs.accessibility;
|
||||
|
||||
// Extra properties removed from canary
|
||||
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;
|
||||
delete attrs.ghost_auth_id;
|
||||
|
||||
return attrs;
|
||||
};
|
||||
|
||||
const post = (attrs, frame) => {
|
||||
if (localUtils.isContentAPI(frame)) {
|
||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10335
|
||||
// delete attrs.page;
|
||||
delete attrs.status;
|
||||
|
||||
// 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;
|
||||
}
|
||||
} else {
|
||||
delete attrs.page;
|
||||
|
||||
if (!attrs.tags) {
|
||||
delete attrs.primary_tag;
|
||||
}
|
||||
|
||||
if (!attrs.authors) {
|
||||
delete attrs.primary_author;
|
||||
}
|
||||
}
|
||||
|
||||
delete attrs.locale;
|
||||
delete attrs.visibility;
|
||||
delete attrs.author;
|
||||
|
||||
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;
|
|
@ -0,0 +1,21 @@
|
|||
const moment = require('moment-timezone');
|
||||
const settingsCache = require('../../../../../../services/settings/cache');
|
||||
|
||||
const format = (date) => {
|
||||
return moment(date)
|
||||
.tz(settingsCache.get('active_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;
|
|
@ -0,0 +1,80 @@
|
|||
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)) {
|
||||
const plaintext = model.get('plaintext');
|
||||
|
||||
if (plaintext) {
|
||||
attrs.excerpt = plaintext.substring(0, 500);
|
||||
} else {
|
||||
attrs.excerpt = null;
|
||||
}
|
||||
} else {
|
||||
attrs.excerpt = attrs.custom_excerpt;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// @NOTE: ghost_head & ghost_foot are deprecated, remove in Ghost 3.0
|
||||
module.exports.forSettings = (attrs, frame) => {
|
||||
const _ = require('lodash');
|
||||
|
||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10106
|
||||
// @NOTE: Admin & Content API return a different format, need to mappers
|
||||
if (_.isArray(attrs)) {
|
||||
// CASE: read single setting
|
||||
if (frame.original.params && frame.original.params.key) {
|
||||
if (frame.original.params.key === 'ghost_head') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.original.params.key === 'ghost_foot') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.original.params.key === 'codeinjection_head') {
|
||||
attrs[0].key = 'codeinjection_head';
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.original.params.key === 'codeinjection_foot') {
|
||||
attrs[0].key = 'codeinjection_foot';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: edit
|
||||
if (frame.original.body && frame.original.body.settings) {
|
||||
frame.original.body.settings.forEach((setting) => {
|
||||
if (setting.key === 'codeinjection_head') {
|
||||
const target = _.find(attrs, {key: 'ghost_head'});
|
||||
target.key = 'codeinjection_head';
|
||||
} else if (setting.key === 'codeinjection_foot') {
|
||||
const target = _.find(attrs, {key: 'ghost_foot'});
|
||||
target.key = 'codeinjection_foot';
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// CASE: browse all settings, add extra keys and keep deprecated
|
||||
const ghostHead = _.cloneDeep(_.find(attrs, {key: 'ghost_head'}));
|
||||
const ghostFoot = _.cloneDeep(_.find(attrs, {key: 'ghost_foot'}));
|
||||
|
||||
if (ghostHead) {
|
||||
ghostHead.key = 'codeinjection_head';
|
||||
attrs.push(ghostHead);
|
||||
}
|
||||
|
||||
if (ghostFoot) {
|
||||
ghostFoot.key = 'codeinjection_foot';
|
||||
attrs.push(ghostFoot);
|
||||
}
|
||||
} else {
|
||||
attrs.codeinjection_head = attrs.ghost_head;
|
||||
attrs.codeinjection_foot = attrs.ghost_foot;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
const _ = require('lodash');
|
||||
const utils = require('../../../index');
|
||||
const url = require('./url');
|
||||
const date = require('./date');
|
||||
const members = require('./members');
|
||||
const clean = require('./clean');
|
||||
const extraAttrs = require('./extra-attrs');
|
||||
|
||||
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);
|
||||
clean.tag(jsonModel, frame);
|
||||
|
||||
return jsonModel;
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (utils.isContentAPI(frame)) {
|
||||
date.forPost(jsonModel);
|
||||
members.forPost(jsonModel, frame);
|
||||
}
|
||||
|
||||
extraAttrs.forPost(frame, model, jsonModel);
|
||||
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);
|
||||
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;
|
|
@ -0,0 +1,56 @@
|
|||
const labs = require('../../../../../../services/labs');
|
||||
const membersService = require('../../../../../../services/members');
|
||||
const MEMBER_TAG = '#members';
|
||||
const PERMIT_CONTENT = false;
|
||||
const BLOCK_CONTENT = true;
|
||||
|
||||
// Checks if request should hide memnbers only content
|
||||
function hideMembersOnlyContent(attrs, frame) {
|
||||
const membersEnabled = labs.isSet('members');
|
||||
if (!membersEnabled) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
const postHasMemberTag = attrs.tags && attrs.tags.find((tag) => {
|
||||
return (tag.name === MEMBER_TAG);
|
||||
});
|
||||
const requestFromMember = frame.original.context.member;
|
||||
if (!postHasMemberTag) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
if (!requestFromMember) {
|
||||
return BLOCK_CONTENT;
|
||||
}
|
||||
|
||||
const memberHasPlan = !!(frame.original.context.member.plans || []).length;
|
||||
if (!membersService.isPaymentConfigured()) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
if (memberHasPlan) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
return BLOCK_CONTENT;
|
||||
}
|
||||
|
||||
const forPost = (attrs, frame) => {
|
||||
const hideFormatsData = hideMembersOnlyContent(attrs, frame);
|
||||
if (hideFormatsData) {
|
||||
['plaintext', 'html'].forEach((field) => {
|
||||
attrs[field] = '';
|
||||
});
|
||||
}
|
||||
if (labs.isSet('members')) {
|
||||
// CASE: Members always adds tags, remove if the user didn't originally ask for them
|
||||
const origQueryOrOptions = frame.original.query || frame.original.options || {};
|
||||
const origInclude = origQueryOrOptions.include;
|
||||
|
||||
if (!origInclude || !origInclude.includes('tags')) {
|
||||
delete attrs.tags;
|
||||
attrs.primary_tag = null;
|
||||
}
|
||||
}
|
||||
|
||||
return attrs;
|
||||
};
|
||||
|
||||
module.exports.forPost = forPost;
|
135
core/server/api/canary/utils/serializers/output/utils/url.js
Normal file
135
core/server/api/canary/utils/serializers/output/utils/url.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
const _ = require('lodash');
|
||||
const urlService = require('../../../../../../../frontend/services/url');
|
||||
const urlUtils = require('../../../../../../lib/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/canary/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 (attrs.feature_image) {
|
||||
attrs.feature_image = urlUtils.urlFor('image', {image: attrs.feature_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.og_image) {
|
||||
attrs.og_image = urlUtils.urlFor('image', {image: attrs.og_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.twitter_image) {
|
||||
attrs.twitter_image = urlUtils.urlFor('image', {image: attrs.twitter_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.canonical_url) {
|
||||
attrs.canonical_url = urlUtils.relativeToAbsolute(attrs.canonical_url);
|
||||
}
|
||||
|
||||
if (attrs.html) {
|
||||
const urlOptions = {
|
||||
assetsOnly: true
|
||||
};
|
||||
|
||||
if (frame.options.absolute_urls) {
|
||||
urlOptions.assetsOnly = false;
|
||||
}
|
||||
|
||||
attrs.html = urlUtils.makeAbsoluteUrls(
|
||||
attrs.html,
|
||||
urlUtils.urlFor('home', true),
|
||||
attrs.url,
|
||||
urlOptions
|
||||
).html();
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
if (attrs.profile_image) {
|
||||
attrs.profile_image = urlUtils.urlFor('image', {image: attrs.profile_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.cover_image) {
|
||||
attrs.cover_image = urlUtils.urlFor('image', {image: attrs.cover_image}, true);
|
||||
}
|
||||
|
||||
return attrs;
|
||||
};
|
||||
|
||||
const forTag = (id, attrs, options) => {
|
||||
if (!options.columns || (options.columns && options.columns.includes('url'))) {
|
||||
attrs.url = urlService.getUrlByResourceId(id, {absolute: true});
|
||||
}
|
||||
|
||||
if (attrs.feature_image) {
|
||||
attrs.feature_image = urlUtils.urlFor('image', {image: attrs.feature_image}, true);
|
||||
}
|
||||
|
||||
return attrs;
|
||||
};
|
||||
|
||||
const forSettings = (attrs) => {
|
||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10106
|
||||
// @NOTE: Admin & Content API return a different format, need to mappers
|
||||
if (_.isArray(attrs)) {
|
||||
attrs.forEach((obj) => {
|
||||
if (['cover_image', 'logo', 'icon'].includes(obj.key) && obj.value) {
|
||||
obj.value = urlUtils.urlFor('image', {image: obj.value}, true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (attrs.cover_image) {
|
||||
attrs.cover_image = urlUtils.urlFor('image', {image: attrs.cover_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.logo) {
|
||||
attrs.logo = urlUtils.urlFor('image', {image: attrs.logo}, true);
|
||||
}
|
||||
|
||||
if (attrs.icon) {
|
||||
attrs.icon = urlUtils.urlFor('image', {image: attrs.icon}, true);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
17
core/server/api/canary/utils/serializers/output/webhooks.js
Normal file
17
core/server/api/canary/utils/serializers/output/webhooks.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
const debug = require('ghost-ignition').debug('api:canary: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)]
|
||||
};
|
||||
|
||||
debug(frame.response);
|
||||
}
|
||||
};
|
9
core/server/api/canary/utils/validators/index.js
Normal file
9
core/server/api/canary/utils/validators/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
get input() {
|
||||
return require('./input');
|
||||
},
|
||||
|
||||
get output() {
|
||||
return require('./output');
|
||||
}
|
||||
};
|
81
core/server/api/canary/utils/validators/input/images.js
Normal file
81
core/server/api/canary/utils/validators/input/images.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
const config = require('../../../../../config');
|
||||
const common = require('../../../../../lib/common');
|
||||
const imageLib = require('../../../../../lib/image');
|
||||
|
||||
const profileImage = (frame) => {
|
||||
return imageLib.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 common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.images.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 common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.icons.invalidFile', {extensions: iconExtensions})
|
||||
}));
|
||||
}
|
||||
|
||||
return imageLib.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 common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.icons.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 common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.icons.invalidFile', {extensions: iconExtensions})
|
||||
}));
|
||||
}
|
||||
|
||||
// CASE: icon needs to be smaller than or equal to 1000px
|
||||
if (frame.file.dimensions.width > 1000) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.icons.invalidFile', {extensions: iconExtensions})
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
upload(apiConfig, frame) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const schema = require('./schemas/images-upload');
|
||||
const definitions = require('./schemas/images');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
})
|
||||
.then(() => {
|
||||
if (frame.data.purpose === 'profile_image') {
|
||||
return profileImage(frame);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (frame.data.purpose === 'icon') {
|
||||
return icon(frame);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
45
core/server/api/canary/utils/validators/input/index.js
Normal file
45
core/server/api/canary/utils/validators/input/index.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
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');
|
||||
}
|
||||
};
|
40
core/server/api/canary/utils/validators/input/invitations.js
Normal file
40
core/server/api/canary/utils/validators/input/invitations.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
const Promise = require('bluebird');
|
||||
const validator = require('validator');
|
||||
const debug = require('ghost-ignition').debug('api:canary:utils:validators:input:invitation');
|
||||
const common = require('../../../../../lib/common');
|
||||
|
||||
module.exports = {
|
||||
acceptInvitation(apiConfig, frame) {
|
||||
debug('acceptInvitation');
|
||||
|
||||
const data = frame.data.invitation[0];
|
||||
|
||||
if (!data.token) {
|
||||
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.authentication.noTokenProvided')}));
|
||||
}
|
||||
|
||||
if (!data.email) {
|
||||
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.authentication.noEmailProvided')}));
|
||||
}
|
||||
|
||||
if (!data.password) {
|
||||
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.authentication.noPasswordProvided')}));
|
||||
}
|
||||
|
||||
if (!data.name) {
|
||||
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.authentication.noNameProvided')}));
|
||||
}
|
||||
},
|
||||
|
||||
isInvitation(apiConfig, frame) {
|
||||
debug('isInvitation');
|
||||
|
||||
const email = frame.data.email;
|
||||
|
||||
if (typeof email !== 'string' || !validator.isEmail(email)) {
|
||||
throw new common.errors.BadRequestError({
|
||||
message: common.i18n.t('errors.api.authentication.invalidEmailReceived')
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
16
core/server/api/canary/utils/validators/input/invites.js
Normal file
16
core/server/api/canary/utils/validators/input/invites.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
const Promise = require('bluebird');
|
||||
const common = require('../../../../../lib/common');
|
||||
const models = require('../../../../../models');
|
||||
|
||||
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 common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.users.userAlreadyRegistered')
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
12
core/server/api/canary/utils/validators/input/oembed.js
Normal file
12
core/server/api/canary/utils/validators/input/oembed.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
const Promise = require('bluebird');
|
||||
const common = require('../../../../../lib/common');
|
||||
|
||||
module.exports = {
|
||||
read(apiConfig, frame) {
|
||||
if (!frame.data.url || !frame.data.url.trim()) {
|
||||
return Promise.reject(new common.errors.BadRequestError({
|
||||
message: common.i18n.t('errors.api.oembed.noUrlProvided')
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
15
core/server/api/canary/utils/validators/input/pages.js
Normal file
15
core/server/api/canary/utils/validators/input/pages.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
|
||||
module.exports = {
|
||||
add(apiConfig, frame) {
|
||||
const schema = require(`./schemas/pages-add`);
|
||||
const definitions = require('./schemas/pages');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
},
|
||||
|
||||
edit(apiConfig, frame) {
|
||||
const schema = require(`./schemas/pages-edit`);
|
||||
const definitions = require('./schemas/pages');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
const Promise = require('bluebird');
|
||||
const validator = require('validator');
|
||||
const debug = require('ghost-ignition').debug('api:canary:utils:validators:input:passwordreset');
|
||||
const common = require('../../../../../lib/common');
|
||||
|
||||
module.exports = {
|
||||
resetPassword(apiConfig, frame) {
|
||||
debug('resetPassword');
|
||||
|
||||
const data = frame.data.passwordreset[0];
|
||||
|
||||
if (data.newPassword !== data.ne2Password) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.models.user.newPasswordsDoNotMatch')
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
generateResetToken(apiConfig, frame) {
|
||||
debug('generateResetToken');
|
||||
|
||||
const email = frame.data.passwordreset[0].email;
|
||||
|
||||
if (typeof email !== 'string' || !validator.isEmail(email)) {
|
||||
throw new common.errors.BadRequestError({
|
||||
message: common.i18n.t('errors.api.authentication.invalidEmailReceived')
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
15
core/server/api/canary/utils/validators/input/posts.js
Normal file
15
core/server/api/canary/utils/validators/input/posts.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
|
||||
module.exports = {
|
||||
add(apiConfig, frame) {
|
||||
const schema = require(`./schemas/posts-add`);
|
||||
const definitions = require('./schemas/posts');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
},
|
||||
|
||||
edit(apiConfig, frame) {
|
||||
const schema = require(`./schemas/posts-edit`);
|
||||
const definitions = require('./schemas/posts');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "images.upload",
|
||||
"title": "images.upload",
|
||||
"description": "Schema for images.upload",
|
||||
"$ref": "images#/definitions/image"
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "images",
|
||||
"title": "images",
|
||||
"description": "Base images definitions",
|
||||
"definitions": {
|
||||
"image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"purpose": {
|
||||
"type": "string",
|
||||
"enum": ["image", "profile_image", "icon"],
|
||||
"default": "image"
|
||||
},
|
||||
"ref": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "pages.add",
|
||||
"title": "pages.add",
|
||||
"description": "Schema for pages.add",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"pages": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "pages#/definitions/page"}],
|
||||
"required": ["title"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["pages"]
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "pages.edit",
|
||||
"title": "pages.edit",
|
||||
"description": "Schema for pages.edit",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"pages": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "pages#/definitions/page"}],
|
||||
"required": ["updated_at"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["pages"]
|
||||
}
|
251
core/server/api/canary/utils/validators/input/schemas/pages.json
Normal file
251
core/server/api/canary/utils/validators/input/schemas/pages.json
Normal file
|
@ -0,0 +1,251 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "pages",
|
||||
"title": "pages",
|
||||
"description": "Base pages definitions",
|
||||
"definitions": {
|
||||
"page": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
},
|
||||
"mobiledoc": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 1000000000
|
||||
},
|
||||
"html": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 1000000000
|
||||
},
|
||||
"feature_image": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"featured": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["published", "draft", "scheduled"]
|
||||
},
|
||||
"locale": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 6
|
||||
},
|
||||
"visibility": {
|
||||
"type": ["string", "null"],
|
||||
"enum": ["public"]
|
||||
},
|
||||
"meta_title": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 300
|
||||
},
|
||||
"meta_description": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 500
|
||||
},
|
||||
"updated_at": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time"
|
||||
},
|
||||
"published_at": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time"
|
||||
},
|
||||
"custom_excerpt": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 300
|
||||
},
|
||||
"codeinjection_head": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 65535
|
||||
},
|
||||
"codeinjection_foot": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 65535
|
||||
},
|
||||
"og_image": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"og_title": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 300
|
||||
},
|
||||
"og_description": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 500
|
||||
},
|
||||
"twitter_image": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"twitter_title": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 300
|
||||
},
|
||||
"twitter_description": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 500
|
||||
},
|
||||
"custom_template": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 100
|
||||
},
|
||||
"canonical_url": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"authors": {
|
||||
"$ref": "#/definitions/page-authors"
|
||||
},
|
||||
"tags": {
|
||||
"$ref": "#/definitions/page-tags"
|
||||
},
|
||||
"id": {
|
||||
"strip": true
|
||||
},
|
||||
"page": {
|
||||
"strip": true
|
||||
},
|
||||
"author": {
|
||||
"strip": true
|
||||
},
|
||||
"author_id": {
|
||||
"strip": true
|
||||
},
|
||||
"created_at": {
|
||||
"strip": true
|
||||
},
|
||||
"created_by": {
|
||||
"strip": true
|
||||
},
|
||||
"updated_by": {
|
||||
"strip": true
|
||||
},
|
||||
"published_by": {
|
||||
"strip": true
|
||||
},
|
||||
"url": {
|
||||
"strip": true
|
||||
},
|
||||
"primary_tag": {
|
||||
"strip": true
|
||||
},
|
||||
"primary_author": {
|
||||
"strip": true
|
||||
},
|
||||
"excerpt": {
|
||||
"strip": true
|
||||
},
|
||||
"plaintext": {
|
||||
"strip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"page-authors": {
|
||||
"description": "Authors of the page",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"maxLength": 24
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
},
|
||||
"roles": {
|
||||
"strip": true
|
||||
},
|
||||
"permissions": {
|
||||
"strip": true
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{"required": ["id"]},
|
||||
{"required": ["slug"]},
|
||||
{"required": ["email"]}
|
||||
]
|
||||
}, {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
}]
|
||||
}
|
||||
},
|
||||
"page-tags": {
|
||||
"description": "Tags of the page",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"maxLength": 24
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
},
|
||||
"slug": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"maxLength": 191
|
||||
},
|
||||
"parent": {
|
||||
"strip": true
|
||||
},
|
||||
"parent_id": {
|
||||
"strip": true
|
||||
},
|
||||
"pages": {
|
||||
"strip": true
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
]
|
||||
}, {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "posts.add",
|
||||
"title": "posts.add",
|
||||
"description": "Schema for posts.add",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"posts": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "posts#/definitions/post"}],
|
||||
"required": ["title"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "posts" ]
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "posts.edit",
|
||||
"title": "posts.edit",
|
||||
"description": "Schema for posts.edit",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"posts": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "posts#/definitions/post"}],
|
||||
"required": ["updated_at"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "posts" ]
|
||||
}
|
236
core/server/api/canary/utils/validators/input/schemas/posts.json
Normal file
236
core/server/api/canary/utils/validators/input/schemas/posts.json
Normal file
|
@ -0,0 +1,236 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "posts",
|
||||
"title": "posts",
|
||||
"description": "Base posts definitions",
|
||||
"definitions": {
|
||||
"post": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
},
|
||||
"mobiledoc": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 1000000000
|
||||
},
|
||||
"html": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 1000000000
|
||||
},
|
||||
"feature_image": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"featured": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["published", "draft", "scheduled"]
|
||||
},
|
||||
"locale": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 6
|
||||
},
|
||||
"visibility": {
|
||||
"type": ["string", "null"],
|
||||
"enum": ["public"]
|
||||
},
|
||||
"meta_title": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 300
|
||||
},
|
||||
"meta_description": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 500
|
||||
},
|
||||
"updated_at": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time"
|
||||
},
|
||||
"published_at": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time"
|
||||
},
|
||||
"custom_excerpt": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 300
|
||||
},
|
||||
"codeinjection_head": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 65535
|
||||
},
|
||||
"codeinjection_foot": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 65535
|
||||
},
|
||||
"og_image": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"og_title": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 300
|
||||
},
|
||||
"og_description": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 500
|
||||
},
|
||||
"twitter_image": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"twitter_title": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 300
|
||||
},
|
||||
"twitter_description": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 500
|
||||
},
|
||||
"custom_template": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 100
|
||||
},
|
||||
"canonical_url": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"authors": {
|
||||
"$ref": "#/definitions/post-authors"
|
||||
},
|
||||
"tags": {
|
||||
"$ref": "#/definitions/post-tags"
|
||||
},
|
||||
"id": {
|
||||
"strip": true
|
||||
},
|
||||
"author": {
|
||||
"strip": true
|
||||
},
|
||||
"author_id": {
|
||||
"strip": true
|
||||
},
|
||||
"page": {
|
||||
"strip": true
|
||||
},
|
||||
"created_at": {
|
||||
"strip": true
|
||||
},
|
||||
"created_by": {
|
||||
"strip": true
|
||||
},
|
||||
"updated_by": {
|
||||
"strip": true
|
||||
},
|
||||
"published_by": {
|
||||
"strip": true
|
||||
},
|
||||
"url": {
|
||||
"strip": true
|
||||
},
|
||||
"primary_tag": {
|
||||
"strip": true
|
||||
},
|
||||
"primary_author": {
|
||||
"strip": true
|
||||
},
|
||||
"excerpt": {
|
||||
"strip": true
|
||||
},
|
||||
"plaintext": {
|
||||
"strip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"post-authors": {
|
||||
"description": "Authors of the post",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"maxLength": 24
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
},
|
||||
"roles": {
|
||||
"strip": true
|
||||
},
|
||||
"permissions": {
|
||||
"strip": true
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{"required": ["id"]},
|
||||
{"required": ["slug"]},
|
||||
{"required": ["email"]}
|
||||
]
|
||||
}, {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
}]
|
||||
}
|
||||
},
|
||||
"post-tags": {
|
||||
"description": "Tags of the post",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"maxLength": 24
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
},
|
||||
"slug": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 191
|
||||
},
|
||||
"parent": {
|
||||
"strip": true
|
||||
},
|
||||
"parent_id": {
|
||||
"strip": true
|
||||
},
|
||||
"posts": {
|
||||
"strip": true
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{"required": ["id"]},
|
||||
{"required": ["name"]},
|
||||
{"required": ["slug"]}
|
||||
]
|
||||
}, {
|
||||
"type": "string",
|
||||
"maxLength": 191
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "tags.add",
|
||||
"title": "tags.add",
|
||||
"description": "Schema for tags.add",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"additionalProperties": false,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "tags#/definitions/tag"}],
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "tags" ]
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "tags.edit",
|
||||
"title": "tags.edit",
|
||||
"description": "Schema for tags.edit",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {"$ref": "tags#/definitions/tag"}
|
||||
}
|
||||
},
|
||||
"required": [ "tags" ]
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "tags",
|
||||
"title": "tags",
|
||||
"description": "Base tags definitions",
|
||||
"definitions": {
|
||||
"tag": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 191,
|
||||
"pattern": "^([^,]|$)"
|
||||
},
|
||||
"slug": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 191
|
||||
},
|
||||
"description": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 500
|
||||
},
|
||||
"feature_image": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uri-reference",
|
||||
"maxLength": 2000
|
||||
},
|
||||
"visibility": {
|
||||
"type": "string",
|
||||
"enum": ["public", "internal"]
|
||||
},
|
||||
"meta_title": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 300
|
||||
},
|
||||
"meta_description": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 500
|
||||
},
|
||||
"id": {
|
||||
"strip": true
|
||||
},
|
||||
"parent": {
|
||||
"strip": true
|
||||
},
|
||||
"parent_id": {
|
||||
"strip": true
|
||||
},
|
||||
"created_at": {
|
||||
"strip": true
|
||||
},
|
||||
"created_by": {
|
||||
"strip": true
|
||||
},
|
||||
"updated_at": {
|
||||
"strip": true
|
||||
},
|
||||
"updated_by": {
|
||||
"strip": true
|
||||
},
|
||||
"url": {
|
||||
"strip": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
core/server/api/canary/utils/validators/input/settings.js
Normal file
39
core/server/api/canary/utils/validators/input/settings.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
const Promise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const common = require('../../../../../lib/common');
|
||||
|
||||
module.exports = {
|
||||
read(apiConfig, frame) {
|
||||
// @NOTE: was removed (https://github.com/TryGhost/Ghost/commit/8bb7088ba026efd4a1c9cf7d6f1a5e9b4fa82575)
|
||||
if (frame.options.key === 'permalinks') {
|
||||
return Promise.reject(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.errors.resourceNotFound')
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
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 common.errors.BadRequestError({
|
||||
message: common.i18n.t('errors.api.settings.activeThemeSetViaAPI.error'),
|
||||
help: common.i18n.t('errors.api.settings.activeThemeSetViaAPI.help')
|
||||
})
|
||||
);
|
||||
} else if (setting.key === 'permalinks') {
|
||||
// @NOTE: was removed (https://github.com/TryGhost/Ghost/commit/8bb7088ba026efd4a1c9cf7d6f1a5e9b4fa82575)
|
||||
errors.push(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.settings.problemFindingSetting', {key: setting.key})
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
return Promise.reject(errors[0]);
|
||||
}
|
||||
}
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue