diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 3d4b117ca5..009a5b8570 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -11,7 +11,9 @@ const categories = require('../categories'); const plugins = require('../plugins'); const meta = require('../meta'); const middleware = require('../middleware'); +const translator = require('../translator'); +const isLanguageKey = /^\[\[[\w.\-_:]+]]$/; const helpers = module.exports; helpers.noScriptErrors = async function (req, res, error, httpStatus) { @@ -342,4 +344,70 @@ helpers.getHomePageRoutes = async function (uid) { return data.routes; }; +helpers.formatApiResponse = async (statusCode, res, payload) => { + if (statusCode === 200) { + res.status(200).json({ + status: { + code: 'ok', + message: 'OK', + }, + response: payload || {}, + }); + } else if (!payload) { + // Non-2xx statusCode, generate predefined error + res.status(statusCode).json(helpers.generateError(statusCode)); + } else if (payload instanceof Error) { + if (isLanguageKey.test(payload.message)) { + const translated = await translator.translate(payload.message, 'en-GB'); + res.status(statusCode).json(helpers.generateError(statusCode, translated)); + } else { + res.status(statusCode).json(helpers.generateError(statusCode, payload.message)); + } + } +}; + +helpers.generateError = (statusCode, message) => { + var payload = { + status: { + code: 'internal-server-error', + message: 'An unexpected error was encountered while attempting to service your request.', + }, + response: {}, + }; + + // Need to turn all these into translation strings + switch (statusCode) { + case 400: + payload.status.code = 'bad-request'; + payload.status.message = message || 'Something was wrong with the request payload you passed in.'; + break; + + case 401: + payload.status.code = 'not-authorised'; + payload.status.message = 'A valid login session was not found. Please log in and try again.'; + break; + + case 403: + payload.status.code = 'forbidden'; + payload.status.message = 'You are not authorised to make this call'; + break; + + case 404: + payload.status.code = 'not-found'; + payload.status.message = 'Invalid API call'; + break; + + case 426: + payload.status.code = 'upgrade-required'; + payload.status.message = 'HTTPS is required for requests to the write api, please re-send your request via HTTPS'; + break; + + case 500: + payload.status.code = 'internal-server-error'; + payload.status.message = message || payload.status.message; + } + + return payload; +}; + require('../promisify')(helpers); diff --git a/src/middleware/api.js b/src/middleware/api.js new file mode 100644 index 0000000000..8cc1b138ce --- /dev/null +++ b/src/middleware/api.js @@ -0,0 +1,48 @@ +'use strict'; + +const passport = require('passport'); +const winston = require('winston'); + +const helpers = require('../controllers/helpers'); +const middleware = module.exports; + +middleware.authenticate = function (req, res, next) { + if (req.headers.hasOwnProperty('authorization')) { + passport.authenticate('bearer', { session: false }, function (err, user) { + if (err) { return next(err); } + if (!user) { return helpers.formatApiResponse(401, res); } + + // If the token received was a master token, a _uid must also be present for all calls + if (user.hasOwnProperty('uid')) { + req.login(user, function (err) { + if (err) { return helpers.formatApiResponse(500, res, err); } + + req.uid = user.uid; + req.loggedIn = req.uid > 0; + next(); + }); + } else if (user.hasOwnProperty('master') && user.master === true) { + if (req.body.hasOwnProperty('_uid') || req.query.hasOwnProperty('_uid')) { + user.uid = req.body._uid || req.query._uid; + delete user.master; + + req.login(user, function (err) { + if (err) { return helpers.formatApiResponse(500, res, err); } + + req.uid = user.uid; + req.loggedIn = req.uid > 0; + next(); + }); + } else { + return helpers.formatApiResponse(400, res, new Error('A master token was received without a corresponding `_uid` in the request body')); + } + } else { + winston.warn('[api/authenticate] Unable to find user after verifying token'); + helpers.formatApiResponse(500, res); + } + })(req, res, next); + } else { + // No bearer token, reject request + helpers.formatApiResponse(401, res); + } +}; diff --git a/src/routes/authentication.js b/src/routes/authentication.js index c2b238f7d5..34315af324 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -3,8 +3,10 @@ var async = require('async'); var passport = require('passport'); var passportLocal = require('passport-local').Strategy; +const BearerStrategy = require('passport-http-bearer').Strategy; var winston = require('winston'); +const db = require('../database'); var controllers = require('../controllers'); var helpers = require('../controllers/helpers'); var plugins = require('../plugins'); @@ -48,21 +50,45 @@ Auth.getLoginStrategies = function () { return loginStrategies; }; +Auth.verifyToken = async function (token, done) { + const uid = await db.sortedSetScore('apiTokens', token); + + if (uid !== null) { + if (parseInt(uid, 10) > 0) { + done(null, { + uid: uid, + }); + } else { + done(null, { + master: true, + }); + } + } else { + done(false); + } +}; + Auth.reloadRoutes = async function (params) { loginStrategies.length = 0; const router = params.router; + + // Local Logins if (plugins.hasListeners('action:auth.overrideLogin')) { winston.warn('[authentication] Login override detected, skipping local login strategy.'); plugins.fireHook('action:auth.overrideLogin'); } else { passport.use(new passportLocal({ passReqToCallback: true }, controllers.authentication.localLogin)); } + + // HTTP bearer authentication + passport.use(new BearerStrategy({}, Auth.verifyToken)); + + // Additional logins via SSO plugins try { loginStrategies = await plugins.fireHook('filter:auth.init', loginStrategies); } catch (err) { winston.error('[authentication] ' + err.stack); } - loginStrategies = loginStrategies || []; loginStrategies.forEach(function (strategy) { if (strategy.url) { diff --git a/src/routes/index.js b/src/routes/index.js index 7c43db4b43..faff537df5 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -15,6 +15,7 @@ var apiRoutes = require('./api'); var adminRoutes = require('./admin'); var feedRoutes = require('./feeds'); var authRoutes = require('./authentication'); +const writeRoutes = require('./write'); var helpers = require('./helpers'); var setupPageRoute = helpers.setupPageRoute; @@ -111,6 +112,7 @@ module.exports = async function (app, middleware) { await plugins.reloadRoutes({ router: router }); await authRoutes.reloadRoutes({ router: router }); + await writeRoutes.reload({ router: router }); addCoreRoutes(app, router, middleware); winston.info('Routes added'); diff --git a/src/routes/write/index.js b/src/routes/write/index.js new file mode 100644 index 0000000000..a1a12142a0 --- /dev/null +++ b/src/routes/write/index.js @@ -0,0 +1,54 @@ +'use strict'; + +const middleware = require('../../middleware/api'); +const helpers = require('../../controllers/helpers'); + +const Write = module.exports; + +Write.reload = (params) => { + const router = params.router; + + // router.use('/api', function (req, res, next) { + // if (req.protocol !== 'https') { + // res.set('Upgrade', 'TLS/1.0, HTTP/1.1'); + // return helpers.formatApiResponse(426, res); + // } else { + // next(); + // } + // }); + + // router.use('/users', require('./users')(coreMiddleware)); + // router.use('/groups', require('./groups')(coreMiddleware)); + // router.use('/posts', require('./posts')(coreMiddleware)); + // router.use('/topics', require('./topics')(coreMiddleware)); + // router.use('/categories', require('./categories')(coreMiddleware)); + // router.use('/util', require('./util')(coreMiddleware)); + + router.get('/api/ping', function (req, res) { + helpers.formatApiResponse(200, res, { + pong: true, + }); + }); + + router.post('/api/ping', middleware.authenticate, function (req, res) { + helpers.formatApiResponse(200, res, { + uid: req.user.uid, + }); + }); + + // This router is reserved exclusively for plugins to add their own routes into the write api plugin. Confused yet? :trollface: + // var customRouter = require('express').Router(); + // plugins.fireHook('filter:plugin.write-api.routes', { + // router: customRouter, + // apiMiddleware: apiMiddleware, + // middleware: coreMiddleware, + // errorHandler: errorHandler + // }, function (err, payload) { + // router.use('/', payload.router); + + // router.use(function(req, res) { + // // Catch-all + // errorHandler.respond(404, res); + // }); + // }); +};