diff --git a/core/server/web/api/canary/admin/app.js b/core/server/web/api/canary/admin/app.js new file mode 100644 index 0000000000..82d1cf99b7 --- /dev/null +++ b/core/server/web/api/canary/admin/app.js @@ -0,0 +1,41 @@ +const debug = require('ghost-ignition').debug('web:canary:admin:app'); +const boolParser = require('express-query-boolean'); +const express = require('express'); +const bodyParser = require('body-parser'); +const shared = require('../../../shared'); +const routes = require('./routes'); + +module.exports = function setupApiApp() { + debug('Admin API canary setup start'); + const apiApp = express(); + + // 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(shared.middlewares.maintenance); + + // Check version matches for API requests, depends on res.locals.safeVersion being set + // Therefore must come after themeHandler.ghostLocals, for now + apiApp.use(shared.middlewares.api.versionMatch); + + // Admin API shouldn't be cached + apiApp.use(shared.middlewares.cacheControl('private')); + + // Routing + apiApp.use(routes()); + + // API error handling + apiApp.use(shared.middlewares.errorHandler.resourceNotFound); + apiApp.use(shared.middlewares.errorHandler.handleJSONResponseV2); + + debug('Admin API canary setup end'); + + return apiApp; +}; diff --git a/core/server/web/api/canary/admin/middleware.js b/core/server/web/api/canary/admin/middleware.js new file mode 100644 index 0000000000..0fd49f12fb --- /dev/null +++ b/core/server/web/api/canary/admin/middleware.js @@ -0,0 +1,57 @@ +const common = require('../../../../lib/common'); +const auth = require('../../../../services/auth'); +const shared = require('../../../shared'); + +const notImplemented = function (req, res, next) { + // CASE: user is logged in, allow + if (!req.api_key) { + return next(); + } + + // @NOTE: integrations have limited access for now + const whitelisted = { + // @NOTE: stable + site: ['GET'], + posts: ['GET', 'PUT', 'DELETE', 'POST'], + pages: ['GET', 'PUT', 'DELETE', 'POST'], + images: ['POST'], + // @NOTE: experimental + tags: ['GET', 'PUT', 'DELETE', 'POST'], + users: ['GET'], + themes: ['POST', 'PUT'], + subscribers: ['GET', 'PUT', 'DELETE', 'POST'], + config: ['GET'], + webhooks: ['POST', 'DELETE'], + schedules: ['PUT'], + db: ['POST'] + }; + + const match = req.url.match(/^\/(\w+)\/?/); + + if (match) { + const entity = match[1]; + + if (whitelisted[entity] && whitelisted[entity].includes(req.method)) { + return next(); + } + } + + next(new common.errors.GhostError({ + errorType: 'NotImplementedError', + message: common.i18n.t('errors.api.common.notImplemented'), + statusCode: '501' + })); +}; + +/** + * Authentication for private endpoints + */ +module.exports.authAdminApi = [ + auth.authenticate.authenticateAdminApi, + auth.authorize.authorizeAdminApi, + shared.middlewares.updateUserLastSeen, + shared.middlewares.api.cors, + shared.middlewares.urlRedirects.adminRedirect, + shared.middlewares.prettyUrls, + notImplemented +]; diff --git a/core/server/web/api/canary/admin/routes.js b/core/server/web/api/canary/admin/routes.js new file mode 100644 index 0000000000..642b9f13c3 --- /dev/null +++ b/core/server/web/api/canary/admin/routes.js @@ -0,0 +1,228 @@ +const express = require('express'); +const api = require('../../../../api'); +const apiCanary = require('../../../../api/canary'); +const mw = require('./middleware'); + +const shared = require('../../../shared'); + +// Handling uploads & imports +const upload = shared.middlewares.upload; + +module.exports = function apiRoutes() { + const router = express.Router(); + + // alias delete with del + router.del = router.delete; + + router.use(shared.middlewares.api.cors); + + const http = apiCanary.http; + + // ## Public + router.get('/site', http(apiCanary.site.read)); + + // ## Configuration + router.get('/config', mw.authAdminApi, http(apiCanary.config.read)); + + // ## Posts + router.get('/posts', mw.authAdminApi, http(apiCanary.posts.browse)); + router.post('/posts', mw.authAdminApi, http(apiCanary.posts.add)); + router.get('/posts/:id', mw.authAdminApi, http(apiCanary.posts.read)); + router.get('/posts/slug/:slug', mw.authAdminApi, http(apiCanary.posts.read)); + router.put('/posts/:id', mw.authAdminApi, http(apiCanary.posts.edit)); + router.del('/posts/:id', mw.authAdminApi, http(apiCanary.posts.destroy)); + + // ## Pages + router.get('/pages', mw.authAdminApi, http(apiCanary.pages.browse)); + router.post('/pages', mw.authAdminApi, http(apiCanary.pages.add)); + router.get('/pages/:id', mw.authAdminApi, http(apiCanary.pages.read)); + router.get('/pages/slug/:slug', mw.authAdminApi, http(apiCanary.pages.read)); + router.put('/pages/:id', mw.authAdminApi, http(apiCanary.pages.edit)); + router.del('/pages/:id', mw.authAdminApi, http(apiCanary.pages.destroy)); + + // # Integrations + + router.get('/integrations', mw.authAdminApi, http(apiCanary.integrations.browse)); + router.get('/integrations/:id', mw.authAdminApi, http(apiCanary.integrations.read)); + router.post('/integrations', mw.authAdminApi, http(apiCanary.integrations.add)); + router.put('/integrations/:id', mw.authAdminApi, http(apiCanary.integrations.edit)); + router.del('/integrations/:id', mw.authAdminApi, http(apiCanary.integrations.destroy)); + + // ## Schedules + router.put('/schedules/:resource/:id', mw.authAdminApi, http(apiCanary.schedules.publish)); + + // ## Settings + router.get('/settings/routes/yaml', mw.authAdminApi, http(apiCanary.settings.download)); + router.post('/settings/routes/yaml', + mw.authAdminApi, + upload.single('routes'), + shared.middlewares.validation.upload({type: 'routes'}), + http(apiCanary.settings.upload) + ); + + router.get('/settings', mw.authAdminApi, http(apiCanary.settings.browse)); + router.get('/settings/:key', mw.authAdminApi, http(apiCanary.settings.read)); + router.put('/settings', mw.authAdminApi, http(apiCanary.settings.edit)); + + // ## Users + router.get('/users', mw.authAdminApi, http(apiCanary.users.browse)); + router.get('/users/:id', mw.authAdminApi, http(apiCanary.users.read)); + router.get('/users/slug/:slug', mw.authAdminApi, http(apiCanary.users.read)); + // NOTE: We don't expose any email addresses via the public api. + router.get('/users/email/:email', mw.authAdminApi, http(apiCanary.users.read)); + + router.put('/users/password', mw.authAdminApi, http(apiCanary.users.changePassword)); + router.put('/users/owner', mw.authAdminApi, http(apiCanary.users.transferOwnership)); + router.put('/users/:id', mw.authAdminApi, http(apiCanary.users.edit)); + router.del('/users/:id', mw.authAdminApi, http(apiCanary.users.destroy)); + + // ## Tags + router.get('/tags', mw.authAdminApi, http(apiCanary.tags.browse)); + router.get('/tags/:id', mw.authAdminApi, http(apiCanary.tags.read)); + router.get('/tags/slug/:slug', mw.authAdminApi, http(apiCanary.tags.read)); + router.post('/tags', mw.authAdminApi, http(apiCanary.tags.add)); + router.put('/tags/:id', mw.authAdminApi, http(apiCanary.tags.edit)); + router.del('/tags/:id', mw.authAdminApi, http(apiCanary.tags.destroy)); + + // ## Subscribers + router.get('/subscribers', shared.middlewares.labs.subscribers, mw.authAdminApi, http(apiCanary.subscribers.browse)); + router.get('/subscribers/csv', shared.middlewares.labs.subscribers, mw.authAdminApi, http(apiCanary.subscribers.exportCSV)); + router.post('/subscribers/csv', + shared.middlewares.labs.subscribers, + mw.authAdminApi, + upload.single('subscribersfile'), + shared.middlewares.validation.upload({type: 'subscribers'}), + http(apiCanary.subscribers.importCSV) + ); + router.get('/subscribers/:id', shared.middlewares.labs.subscribers, mw.authAdminApi, http(apiCanary.subscribers.read)); + router.get('/subscribers/email/:email', shared.middlewares.labs.subscribers, mw.authAdminApi, http(apiCanary.subscribers.read)); + router.post('/subscribers', shared.middlewares.labs.subscribers, mw.authAdminApi, http(apiCanary.subscribers.add)); + router.put('/subscribers/:id', shared.middlewares.labs.subscribers, mw.authAdminApi, http(apiCanary.subscribers.edit)); + router.del('/subscribers/:id', shared.middlewares.labs.subscribers, mw.authAdminApi, http(apiCanary.subscribers.destroy)); + router.del('/subscribers/email/:email', shared.middlewares.labs.subscribers, mw.authAdminApi, http(apiCanary.subscribers.destroy)); + + // ## Members + router.get('/members', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.browse)); + router.get('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.read)); + router.del('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.destroy)); + + // ## Roles + router.get('/roles/', mw.authAdminApi, http(apiCanary.roles.browse)); + + // ## Clients + router.get('/clients/slug/:slug', api.http(api.clients.read)); + + // ## Slugs + router.get('/slugs/:type/:name', mw.authAdminApi, http(apiCanary.slugs.generate)); + + // ## Themes + router.get('/themes/', mw.authAdminApi, http(apiCanary.themes.browse)); + + router.get('/themes/:name/download', + mw.authAdminApi, + http(apiCanary.themes.download) + ); + + router.post('/themes/upload', + mw.authAdminApi, + upload.single('file'), + shared.middlewares.validation.upload({type: 'themes'}), + http(apiCanary.themes.upload) + ); + + router.put('/themes/:name/activate', + mw.authAdminApi, + http(apiCanary.themes.activate) + ); + + router.del('/themes/:name', + mw.authAdminApi, + http(apiCanary.themes.destroy) + ); + + // ## Notifications + router.get('/notifications', mw.authAdminApi, http(apiCanary.notifications.browse)); + router.post('/notifications', mw.authAdminApi, http(apiCanary.notifications.add)); + router.del('/notifications/:notification_id', mw.authAdminApi, http(apiCanary.notifications.destroy)); + + // ## DB + router.get('/db', mw.authAdminApi, http(apiCanary.db.exportContent)); + router.post('/db', + mw.authAdminApi, + upload.single('importfile'), + shared.middlewares.validation.upload({type: 'db'}), + http(apiCanary.db.importContent) + ); + router.del('/db', mw.authAdminApi, http(apiCanary.db.deleteAllContent)); + router.post('/db/backup', + mw.authAdminApi, + http(apiCanary.db.backupContent) + ); + + // ## Mail + router.post('/mail', mw.authAdminApi, http(apiCanary.mail.send)); + router.post('/mail/test', mw.authAdminApi, http(apiCanary.mail.sendTest)); + + // ## Slack + router.post('/slack/test', mw.authAdminApi, http(apiCanary.slack.sendTest)); + + // ## Sessions + router.get('/session', mw.authAdminApi, api.http(apiCanary.session.read)); + // We don't need auth when creating a new session (logging in) + router.post('/session', + shared.middlewares.brute.globalBlock, + shared.middlewares.brute.userLogin, + api.http(apiCanary.session.add) + ); + router.del('/session', mw.authAdminApi, api.http(apiCanary.session.delete)); + + // ## Authentication + router.post('/authentication/passwordreset', + shared.middlewares.brute.globalReset, + shared.middlewares.brute.userReset, + http(apiCanary.authentication.generateResetToken) + ); + router.put('/authentication/passwordreset', shared.middlewares.brute.globalBlock, http(apiCanary.authentication.resetPassword)); + router.post('/authentication/invitation', http(apiCanary.authentication.acceptInvitation)); + router.get('/authentication/invitation', http(apiCanary.authentication.isInvitation)); + router.post('/authentication/setup', http(apiCanary.authentication.setup)); + router.put('/authentication/setup', mw.authAdminApi, http(apiCanary.authentication.updateSetup)); + router.get('/authentication/setup', http(apiCanary.authentication.isSetup)); + + // ## Images + router.post('/images/upload', + mw.authAdminApi, + upload.single('file'), + shared.middlewares.validation.upload({type: 'images'}), + shared.middlewares.image.normalize, + http(apiCanary.images.upload) + ); + + // ## Invites + router.get('/invites', mw.authAdminApi, http(apiCanary.invites.browse)); + router.get('/invites/:id', mw.authAdminApi, http(apiCanary.invites.read)); + router.post('/invites', mw.authAdminApi, http(apiCanary.invites.add)); + router.del('/invites/:id', mw.authAdminApi, http(apiCanary.invites.destroy)); + + // ## Redirects (JSON based) + router.get('/redirects/json', mw.authAdminApi, http(apiCanary.redirects.download)); + router.post('/redirects/json', + mw.authAdminApi, + upload.single('redirects'), + shared.middlewares.validation.upload({type: 'redirects'}), + http(apiCanary.redirects.upload) + ); + + // ## Webhooks (RESTHooks) + router.post('/webhooks', mw.authAdminApi, http(apiCanary.webhooks.add)); + router.put('/webhooks/:id', mw.authAdminApi, http(apiCanary.webhooks.edit)); + router.del('/webhooks/:id', mw.authAdminApi, http(apiCanary.webhooks.destroy)); + + // ## Oembed (fetch response from oembed provider) + router.get('/oembed', mw.authAdminApi, http(apiCanary.oembed.read)); + + // ## Actions + router.get('/actions/:type/:id', mw.authAdminApi, http(apiCanary.actions.browse)); + + return router; +}; diff --git a/core/server/web/api/canary/content/app.js b/core/server/web/api/canary/content/app.js new file mode 100644 index 0000000000..0a1cc727a5 --- /dev/null +++ b/core/server/web/api/canary/content/app.js @@ -0,0 +1,36 @@ +const debug = require('ghost-ignition').debug('web:api:canary:content:app'); +const boolParser = require('express-query-boolean'); +const bodyParser = require('body-parser'); +const express = require('express'); +const shared = require('../../../shared'); +const routes = require('./routes'); + +module.exports = function setupApiApp() { + debug('Content API canary setup start'); + const apiApp = express(); + + // API middleware + + // @NOTE: req.body is undefined if we don't use this parser, this can trouble if components rely on req.body being present + apiApp.use(bodyParser.json({limit: '1mb'})); + + // Query parsing + apiApp.use(boolParser()); + + // send 503 json response in case of maintenance + apiApp.use(shared.middlewares.maintenance); + + // API shouldn't be cached + apiApp.use(shared.middlewares.cacheControl('private')); + + // Routing + apiApp.use(routes()); + + // API error handling + apiApp.use(shared.middlewares.errorHandler.resourceNotFound); + apiApp.use(shared.middlewares.errorHandler.handleJSONResponse); + + debug('Content API canary setup end'); + + return apiApp; +}; diff --git a/core/server/web/api/canary/content/middleware.js b/core/server/web/api/canary/content/middleware.js new file mode 100644 index 0000000000..434fa1aa86 --- /dev/null +++ b/core/server/web/api/canary/content/middleware.js @@ -0,0 +1,23 @@ +const cors = require('cors'); +const auth = require('../../../../services/auth'); +const shared = require('../../../shared'); + +/** + * Auth Middleware Packages + * + * IMPORTANT + * - cors middleware MUST happen before pretty urls, because otherwise cors header can get lost on redirect + * - url redirects MUST happen after cors, otherwise cors header can get lost on redirect + */ + +/** + * Authentication for public endpoints + */ +module.exports.authenticatePublic = [ + shared.middlewares.brute.contentApiKey, + auth.authenticate.authenticateContentApi, + auth.authorize.authorizeContentApi, + cors(), + shared.middlewares.urlRedirects.adminRedirect, + shared.middlewares.prettyUrls +]; diff --git a/core/server/web/api/canary/content/routes.js b/core/server/web/api/canary/content/routes.js new file mode 100644 index 0000000000..63d9910b8a --- /dev/null +++ b/core/server/web/api/canary/content/routes.js @@ -0,0 +1,37 @@ +const express = require('express'); +const cors = require('cors'); +const apiCanary = require('../../../../api/canary'); +const mw = require('./middleware'); + +module.exports = function apiRoutes() { + const router = express.Router(); + + router.use(cors()); + + const http = apiCanary.http; + + // ## Posts + router.get('/posts', mw.authenticatePublic, http(apiCanary.postsPublic.browse)); + router.get('/posts/:id', mw.authenticatePublic, http(apiCanary.postsPublic.read)); + router.get('/posts/slug/:slug', mw.authenticatePublic, http(apiCanary.postsPublic.read)); + + // ## Pages + router.get('/pages', mw.authenticatePublic, http(apiCanary.pagesPublic.browse)); + router.get('/pages/:id', mw.authenticatePublic, http(apiCanary.pagesPublic.read)); + router.get('/pages/slug/:slug', mw.authenticatePublic, http(apiCanary.pagesPublic.read)); + + // ## Users + router.get('/authors', mw.authenticatePublic, http(apiCanary.authorsPublic.browse)); + router.get('/authors/:id', mw.authenticatePublic, http(apiCanary.authorsPublic.read)); + router.get('/authors/slug/:slug', mw.authenticatePublic, http(apiCanary.authorsPublic.read)); + + // ## Tags + router.get('/tags', mw.authenticatePublic, http(apiCanary.tagsPublic.browse)); + router.get('/tags/:id', mw.authenticatePublic, http(apiCanary.tagsPublic.read)); + router.get('/tags/slug/:slug', mw.authenticatePublic, http(apiCanary.tagsPublic.read)); + + // ## Settings + router.get('/settings', mw.authenticatePublic, http(apiCanary.publicSettings.browse)); + + return router; +}; diff --git a/core/server/web/api/canary/members/app.js b/core/server/web/api/canary/members/app.js new file mode 100644 index 0000000000..84d18d9293 --- /dev/null +++ b/core/server/web/api/canary/members/app.js @@ -0,0 +1,28 @@ +const debug = require('ghost-ignition').debug('web:canary:members:app'); +const express = require('express'); +const membersService = require('../../../../services/members'); +const labs = require('../../../shared/middlewares/labs'); +const shared = require('../../../shared'); + +module.exports = function setupMembersApiApp() { + debug('Members API canary setup start'); + const apiApp = express(); + + // Entire app is behind labs flag + apiApp.use(labs.members); + + // Set up the auth pages + apiApp.use('/static/auth', membersService.authPages); + + // Set up the api endpoints and the gateway + // NOTE: this is wrapped in a function to ensure we always go via the getter + apiApp.use((req, res, next) => membersService.api(req, res, next)); + + // API error handling + apiApp.use(shared.middlewares.errorHandler.resourceNotFound); + apiApp.use(shared.middlewares.errorHandler.handleJSONResponseV2); + + debug('Members API canary setup end'); + + return apiApp; +}; diff --git a/core/server/web/api/index.js b/core/server/web/api/index.js index 34d9912108..de69e99995 100644 --- a/core/server/web/api/index.js +++ b/core/server/web/api/index.js @@ -12,6 +12,9 @@ module.exports = function setupApiApp() { apiApp.use(urlUtils.getVersionPath({version: 'v2', type: 'content'}), require('./v2/content/app')()); apiApp.use(urlUtils.getVersionPath({version: 'v2', type: 'admin'}), require('./v2/admin/app')()); apiApp.use(urlUtils.getVersionPath({version: 'v2', type: 'members'}), require('./v2/members/app')()); + apiApp.use(urlUtils.getVersionPath({version: 'canary', type: 'content'}), require('./canary/content/app')()); + apiApp.use(urlUtils.getVersionPath({version: 'canary', type: 'admin'}), require('./canary/admin/app')()); + apiApp.use(urlUtils.getVersionPath({version: 'canary', type: 'members'}), require('./canary/members/app')()); // Error handling for requests to non-existent API versions apiApp.use(errorHandler.resourceNotFound);