Skip to content

Commit

Permalink
Added v2 api endpoints (#9874)
Browse files Browse the repository at this point in the history
refs #9866

- Registered Content API under /ghost/api/v2/content/
- Registered Admin API under /ghost/api/v2/admin/
- Moved API v0.1 implementation to web/api/v0.1
- Created web/api/v2 for the new api endpoints
- Started with reducing the implementation for the new Content API (the Content api does not serve admin api endpoints, that's why it was reducible)
- Covered parent-app module with basic test checking correct applications/routes are being mounted
- Added a readme file, which contains a warning using v2, because it's under active development!
- This PR does only make the new endpoints available, we have not:
  - optimised the web folder (e.g. res.isAdmin)
  - started with different API controllers
  - reason: we want to do more preparation tasks before we copy the api controllers
  • Loading branch information
naz authored and kirrg001 committed Sep 18, 2018
1 parent 6ae5c13 commit 5727112
Show file tree
Hide file tree
Showing 14 changed files with 547 additions and 21 deletions.
7 changes: 7 additions & 0 deletions core/server/web/api/README.md
@@ -0,0 +1,7 @@
# Ghost APIs

Ghost is moving towards providing more robust APIs in the future. A plan and decisions can be found [here](https://github.com/TryGhost/Ghost/issues/9866).

## WARNING!

The v2 API (`/ghost/api/v2/*` endpoints) is to be considered under active development until this message is removed. Please use with caution and don't rely too heavy on it just yet :)
12 changes: 6 additions & 6 deletions core/server/web/api/app.js → core/server/web/api/v0.1/app.js
Expand Up @@ -9,16 +9,16 @@ const debug = require('ghost-ignition').debug('api'),
// Include the middleware

// API specific
versionMatch = require('../middleware/api/version-match'), // global
versionMatch = require('../../middleware/api/version-match'), // global

// Shared
bodyParser = require('body-parser'), // global, shared
cacheControl = require('../middleware/cache-control'), // global, shared
maintenance = require('../middleware/maintenance'), // global, shared
errorHandler = require('../middleware/error-handler'); // global, shared
cacheControl = require('../../middleware/cache-control'), // global, shared
maintenance = require('../../middleware/maintenance'), // global, shared
errorHandler = require('../../middleware/error-handler'); // global, shared

module.exports = function setupApiApp() {
debug('API setup start');
debug('API v0.1 setup start');
const apiApp = express();

// @TODO finish refactoring this away.
Expand Down Expand Up @@ -54,7 +54,7 @@ module.exports = function setupApiApp() {
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(errorHandler.handleJSONResponse);

debug('API setup end');
debug('API v0.1 setup end');

return apiApp;
};
@@ -1,7 +1,7 @@
const prettyURLs = require('../middleware/pretty-urls'),
cors = require('../middleware/api/cors'),
urlRedirects = require('../middleware/url-redirects'),
auth = require('../../services/auth');
const prettyURLs = require('../../middleware/pretty-urls'),
cors = require('../../middleware/api/cors'),
urlRedirects = require('../../middleware/url-redirects'),
auth = require('../../../services/auth');

/**
* Auth Middleware Packages
Expand Down
@@ -1,26 +1,25 @@
const express = require('express'),
// This essentially provides the controllers for the routes
api = require('../../api'),
api = require('../../../api'),

// Middleware
mw = require('./middleware'),

// API specific
auth = require('../../services/auth'),
cors = require('../middleware/api/cors'),
brute = require('../middleware/brute'),
auth = require('../../../services/auth'),
cors = require('../../middleware/api/cors'),
brute = require('../../middleware/brute'),

// Handling uploads & imports
tmpdir = require('os').tmpdir,
upload = require('multer')({dest: tmpdir()}),
validation = require('../middleware/validation'),
image = require('../middleware/image'),
validation = require('../../middleware/validation'),
image = require('../../middleware/image'),

// Temporary
// @TODO find a more appy way to do this!
labs = require('../middleware/labs');
labs = require('../../middleware/labs');

// @TODO refactor/clean this up - how do we want the routing to work long term?
module.exports = function apiRoutes() {
const apiRouter = express.Router();

Expand Down
60 changes: 60 additions & 0 deletions core/server/web/api/v2/admin/app.js
@@ -0,0 +1,60 @@
// # API routes
const debug = require('ghost-ignition').debug('api'),
boolParser = require('express-query-boolean'),
express = require('express'),

// routes
routes = require('./routes'),

// Include the middleware

// API specific
versionMatch = require('../../../middleware/api/version-match'), // global

// Shared
bodyParser = require('body-parser'), // global, shared
cacheControl = require('../../../middleware/cache-control'), // global, shared
maintenance = require('../../../middleware/maintenance'), // global, shared
errorHandler = require('../../../middleware/error-handler'); // global, shared

module.exports = function setupApiApp() {
debug('Admin API v2 setup start');
const apiApp = express();

// @TODO finish refactoring this away.
apiApp.use(function setIsAdmin(req, res, next) {
// api === isAdmin
res.isAdmin = true;
next();
});

// API middleware

// Body parsing
apiApp.use(bodyParser.json({limit: '1mb'}));
apiApp.use(bodyParser.urlencoded({extended: true, limit: '1mb'}));

// Query parsing
apiApp.use(boolParser());

// send 503 json response in case of maintenance
apiApp.use(maintenance);

// Check version matches for API requests, depends on res.locals.safeVersion being set
// Therefore must come after themeHandler.ghostLocals, for now
apiApp.use(versionMatch);

// API shouldn't be cached
apiApp.use(cacheControl('private'));

// Routing
apiApp.use(routes());

// API error handling
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(errorHandler.handleJSONResponse);

debug('Admin API v2 setup end');

return apiApp;
};
30 changes: 30 additions & 0 deletions core/server/web/api/v2/admin/middleware.js
@@ -0,0 +1,30 @@
const prettyURLs = require('../../../middleware/pretty-urls'),
cors = require('../../../middleware/api/cors'),
urlRedirects = require('../../../middleware/url-redirects'),
auth = require('../../../../services/auth');

/**
* Authentication for private endpoints
*/
module.exports.authenticatePrivate = [
auth.authenticate.authenticateClient,
auth.authenticate.authenticateUser,
auth.authorize.requiresAuthorizedUser,
cors,
urlRedirects,
prettyURLs
];

/**
* Authentication for client endpoints
*/
module.exports.authenticateClient = function authenticateClient(client) {
return [
auth.authenticate.authenticateClient,
auth.authenticate.authenticateUser,
auth.authorize.requiresAuthorizedClient(client),
cors,
urlRedirects,
prettyURLs
];
};
222 changes: 222 additions & 0 deletions core/server/web/api/v2/admin/routes.js
@@ -0,0 +1,222 @@
const express = require('express'),
// This essentially provides the controllers for the routes
api = require('../../../../api'),

// Middleware
mw = require('./middleware'),

// API specific
auth = require('../../../../services/auth'),
cors = require('../../../middleware/api/cors'),
brute = require('../../../middleware/brute'),

// Handling uploads & imports
tmpdir = require('os').tmpdir,
upload = require('multer')({dest: tmpdir()}),
validation = require('../../../middleware/validation'),
image = require('../../../middleware/image'),

// Temporary
// @TODO find a more appy way to do this!
labs = require('../../../middleware/labs');

module.exports = function apiRoutes() {
const router = express.Router();

// alias delete with del
router.del = router.delete;

// ## CORS pre-flight check
router.options('*', cors);

// ## Configuration
router.get('/configuration', api.http(api.configuration.read));
router.get('/configuration/:key', mw.authenticatePrivate, api.http(api.configuration.read));

// ## Posts
router.get('/posts', mw.authenticatePrivate, api.http(api.posts.browse));

router.post('/posts', mw.authenticatePrivate, api.http(api.posts.add));
router.get('/posts/:id', mw.authenticatePrivate, api.http(api.posts.read));
router.get('/posts/slug/:slug', mw.authenticatePrivate, api.http(api.posts.read));
router.put('/posts/:id', mw.authenticatePrivate, api.http(api.posts.edit));
router.del('/posts/:id', mw.authenticatePrivate, api.http(api.posts.destroy));

// ## Schedules
router.put('/schedules/posts/:id', [
auth.authenticate.authenticateClient,
auth.authenticate.authenticateUser
], api.http(api.schedules.publishPost));

// ## Settings
router.get('/settings/routes/yaml', mw.authenticatePrivate, api.http(api.settings.download));
router.post('/settings/routes/yaml',
mw.authenticatePrivate,
upload.single('routes'),
validation.upload({type: 'routes'}),
api.http(api.settings.upload)
);

router.get('/settings', mw.authenticatePrivate, api.http(api.settings.browse));
router.get('/settings/:key', mw.authenticatePrivate, api.http(api.settings.read));
router.put('/settings', mw.authenticatePrivate, api.http(api.settings.edit));

// ## Users
router.get('/users', mw.authenticatePrivate, api.http(api.users.browse));
router.get('/users/:id', mw.authenticatePrivate, api.http(api.users.read));
router.get('/users/slug/:slug', mw.authenticatePrivate, api.http(api.users.read));
// NOTE: We don't expose any email addresses via the public api.
router.get('/users/email/:email', mw.authenticatePrivate, api.http(api.users.read));

router.put('/users/password', mw.authenticatePrivate, api.http(api.users.changePassword));
router.put('/users/owner', mw.authenticatePrivate, api.http(api.users.transferOwnership));
router.put('/users/:id', mw.authenticatePrivate, api.http(api.users.edit));
router.del('/users/:id', mw.authenticatePrivate, api.http(api.users.destroy));

// ## Tags
router.get('/tags', mw.authenticatePrivate, api.http(api.tags.browse));
router.get('/tags/:id', mw.authenticatePrivate, api.http(api.tags.read));
router.get('/tags/slug/:slug', mw.authenticatePrivate, api.http(api.tags.read));
router.post('/tags', mw.authenticatePrivate, api.http(api.tags.add));
router.put('/tags/:id', mw.authenticatePrivate, api.http(api.tags.edit));
router.del('/tags/:id', mw.authenticatePrivate, api.http(api.tags.destroy));

// ## Subscribers
router.get('/subscribers', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.browse));
router.get('/subscribers/csv', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.exportCSV));
router.post('/subscribers/csv',
labs.subscribers,
mw.authenticatePrivate,
upload.single('subscribersfile'),
validation.upload({type: 'subscribers'}),
api.http(api.subscribers.importCSV)
);
router.get('/subscribers/:id', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.read));
router.get('/subscribers/email/:email', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.read));
router.post('/subscribers', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.add));
router.put('/subscribers/:id', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.edit));
router.del('/subscribers/:id', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.destroy));
router.del('/subscribers/email/:email', labs.subscribers, mw.authenticatePrivate, api.http(api.subscribers.destroy));

// ## Roles
router.get('/roles/', mw.authenticatePrivate, api.http(api.roles.browse));

// ## Clients
router.get('/clients/slug/:slug', api.http(api.clients.read));

// ## Slugs
router.get('/slugs/:type/:name', mw.authenticatePrivate, api.http(api.slugs.generate));

// ## Themes
router.get('/themes/', mw.authenticatePrivate, api.http(api.themes.browse));

router.get('/themes/:name/download',
mw.authenticatePrivate,
api.http(api.themes.download)
);

router.post('/themes/upload',
mw.authenticatePrivate,
upload.single('theme'),
validation.upload({type: 'themes'}),
api.http(api.themes.upload)
);

router.put('/themes/:name/activate',
mw.authenticatePrivate,
api.http(api.themes.activate)
);

router.del('/themes/:name',
mw.authenticatePrivate,
api.http(api.themes.destroy)
);

// ## Notifications
router.get('/notifications', mw.authenticatePrivate, api.http(api.notifications.browse));
router.post('/notifications', mw.authenticatePrivate, api.http(api.notifications.add));
router.del('/notifications/:id', mw.authenticatePrivate, api.http(api.notifications.destroy));

// ## DB
router.get('/db', mw.authenticatePrivate, api.http(api.db.exportContent));
router.post('/db',
mw.authenticatePrivate,
upload.single('importfile'),
validation.upload({type: 'db'}),
api.http(api.db.importContent)
);
router.del('/db', mw.authenticatePrivate, api.http(api.db.deleteAllContent));

// ## Mail
router.post('/mail', mw.authenticatePrivate, api.http(api.mail.send));
router.post('/mail/test', mw.authenticatePrivate, api.http(api.mail.sendTest));

// ## Slack
router.post('/slack/test', mw.authenticatePrivate, api.http(api.slack.sendTest));

// ## Authentication
router.post('/authentication/passwordreset',
brute.globalReset,
brute.userReset,
api.http(api.authentication.generateResetToken)
);
router.put('/authentication/passwordreset', brute.globalBlock, api.http(api.authentication.resetPassword));
router.post('/authentication/invitation', api.http(api.authentication.acceptInvitation));
router.get('/authentication/invitation', api.http(api.authentication.isInvitation));
router.post('/authentication/setup', api.http(api.authentication.setup));
router.put('/authentication/setup', mw.authenticatePrivate, api.http(api.authentication.updateSetup));
router.get('/authentication/setup', api.http(api.authentication.isSetup));

router.post('/authentication/token',
mw.authenticateClient(),
brute.globalBlock,
brute.userLogin,
auth.oauth.generateAccessToken
);

router.post('/authentication/revoke', mw.authenticatePrivate, api.http(api.authentication.revoke));

// ## Uploads
// @TODO: rename endpoint to /images/upload (or similar)
router.post('/uploads',
mw.authenticatePrivate,
upload.single('uploadimage'),
validation.upload({type: 'images'}),
image.normalize,
api.http(api.uploads.add)
);

router.post('/db/backup', mw.authenticateClient('Ghost Backup'), api.http(api.db.backupContent));

router.post('/uploads/icon',
mw.authenticatePrivate,
upload.single('uploadimage'),
validation.upload({type: 'icons'}),
validation.blogIcon(),
api.http(api.uploads.add)
);

// ## Invites
router.get('/invites', mw.authenticatePrivate, api.http(api.invites.browse));
router.get('/invites/:id', mw.authenticatePrivate, api.http(api.invites.read));
router.post('/invites', mw.authenticatePrivate, api.http(api.invites.add));
router.del('/invites/:id', mw.authenticatePrivate, api.http(api.invites.destroy));

// ## Redirects (JSON based)
router.get('/redirects/json', mw.authenticatePrivate, api.http(api.redirects.download));
router.post('/redirects/json',
mw.authenticatePrivate,
upload.single('redirects'),
validation.upload({type: 'redirects'}),
api.http(api.redirects.upload)
);

// ## Webhooks (RESTHooks)
router.post('/webhooks', mw.authenticatePrivate, api.http(api.webhooks.add));
router.del('/webhooks/:id', mw.authenticatePrivate, api.http(api.webhooks.destroy));

// ## Oembed (fetch response from oembed provider)
router.get('/oembed', mw.authenticatePrivate, api.http(api.oembed.read));

return router;
};

0 comments on commit 5727112

Please sign in to comment.