diff --git a/public/src/client/account/sessions.js b/public/src/client/account/sessions.js index e4b44e98e3..f9dab2419f 100644 --- a/public/src/client/account/sessions.js +++ b/public/src/client/account/sessions.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/account/sessions', ['forum/account/header', 'components'], function (header, components) { +define('forum/account/sessions', ['forum/account/header', 'components', 'api'], function (header, components, api) { var Sessions = {}; Sessions.init = function () { @@ -17,15 +17,9 @@ define('forum/account/sessions', ['forum/account/header', 'components'], functio if (uuid) { // This is done via DELETE because a user shouldn't be able to // revoke his own session! This is what logout is for - $.ajax({ - url: config.relative_path + '/api/user/' + ajaxify.data.userslug + '/session/' + uuid, - method: 'delete', - headers: { - 'x-csrf-token': config.csrf_token, - }, - }).done(function () { + api.del(`/users/${ajaxify.data.uid}/sessions/${uuid}`, {}).then(() => { parentEl.remove(); - }).fail(function (err) { + }).catch((err) => { try { var errorObj = JSON.parse(err.responseText); if (errorObj.loggedIn === false) { diff --git a/src/controllers/accounts/sessions.js b/src/controllers/accounts/sessions.js index fbea034afc..72e1afaaac 100644 --- a/src/controllers/accounts/sessions.js +++ b/src/controllers/accounts/sessions.js @@ -1,8 +1,5 @@ 'use strict'; -const util = require('util'); - -const db = require('../../database'); const user = require('../../user'); const helpers = require('../helpers'); const accountHelpers = require('./helpers'); @@ -21,39 +18,3 @@ sessionController.get = async function (req, res, next) { res.render('account/sessions', userData); }; - -const getSessionAsync = util.promisify(function (sid, callback) { - db.sessionStore.get(sid, (err, sessionObj) => callback(err, sessionObj || null)); -}); - -sessionController.revoke = async function (req, res, next) { - if (!req.params.hasOwnProperty('uuid')) { - return next(); - } - try { - const uid = await user.getUidByUserslug(req.params.userslug); - if (!uid) { - throw new Error('[[error:no-session-found]]'); - } - const sids = await db.getSortedSetRange('uid:' + uid + ':sessions', 0, -1); - let _id; - for (const sid of sids) { - /* eslint-disable no-await-in-loop */ - const sessionObj = await getSessionAsync(sid); - if (sessionObj && sessionObj.meta && sessionObj.meta.uuid === req.params.uuid) { - _id = sid; - break; - } - } - - if (!_id) { - throw new Error('[[error:no-session-found]]'); - } - - await user.auth.revokeSession(_id, uid); - } catch (err) { - return res.status(500).send(err.message); - } - - res.sendStatus(200); -}; diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js index 7978319991..61bfc8827e 100644 --- a/src/controllers/write/users.js +++ b/src/controllers/write/users.js @@ -1,5 +1,8 @@ 'use strict'; +const util = require('util'); + +const db = require('../../database'); const api = require('../../api'); const user = require('../../user'); const meta = require('../../meta'); @@ -120,3 +123,32 @@ Users.deleteToken = async (req, res) => { helpers.formatApiResponse(404, res); } }; + +const getSessionAsync = util.promisify(function (sid, callback) { + db.sessionStore.get(sid, (err, sessionObj) => callback(err, sessionObj || null)); +}); + +Users.revokeSession = async (req, res) => { + // Only admins or global mods (besides the user themselves) can revoke sessions + if (parseInt(req.params.uid, 10) !== req.uid && !await user.isAdminOrGlobalMod(req.uid)) { + return helpers.formatApiResponse(404, res); + } + + const sids = await db.getSortedSetRange('uid:' + req.params.uid + ':sessions', 0, -1); + let _id; + for (const sid of sids) { + /* eslint-disable no-await-in-loop */ + const sessionObj = await getSessionAsync(sid); + if (sessionObj && sessionObj.meta && sessionObj.meta.uuid === req.params.uuid) { + _id = sid; + break; + } + } + + if (!_id) { + throw new Error('[[error:no-session-found]]'); + } + + await user.auth.revokeSession(_id, req.params.uid); + helpers.formatApiResponse(200, res); +}; diff --git a/src/routes/accounts.js b/src/routes/accounts.js index 6051420cdf..1dc77cfd9b 100644 --- a/src/routes/accounts.js +++ b/src/routes/accounts.js @@ -1,5 +1,8 @@ 'use strict'; +const winston = require('winston'); +const nconf = require('nconf'); + var helpers = require('./helpers'); var setupPageRoute = helpers.setupPageRoute; @@ -39,7 +42,14 @@ module.exports = function (app, middleware, controllers) { setupPageRoute(app, '/user/:userslug/consent', middleware, accountMiddlewares, controllers.accounts.consent.get); setupPageRoute(app, '/user/:userslug/blocks', middleware, accountMiddlewares, controllers.accounts.blocks.getBlocks); setupPageRoute(app, '/user/:userslug/sessions', middleware, accountMiddlewares, controllers.accounts.sessions.get); - app.delete('/api/user/:userslug/session/:uuid', [middleware.exposeUid, middleware.ensureSelfOrGlobalPrivilege], controllers.accounts.sessions.revoke); + app.delete('/api/user/:userslug/session/:uuid', [middleware.exposeUid], function (req, res, next) { + // TODO: Remove this entire route in v1.16.0 + winston.warn('[router] `/api/user/:userslug/session/:uuid` has been deprecated, use `DELETE /api/v3/users/:uid/sessions/:uuid` or `DELETE /api/v3/users/bySlug/:userslug/sessions/:uuid` instead'); + if (!res.locals.uid) { + return next(); + } + res.redirect(`${nconf.get('relative_path')}/api/v3/users/${res.locals.uid}/sessions/${req.params.uuid}`); + }); setupPageRoute(app, '/notifications', middleware, [middleware.authenticate], controllers.accounts.notifications.get); setupPageRoute(app, '/user/:userslug/chats/:roomid?', middleware, middlewares, controllers.accounts.chats.get); diff --git a/src/routes/write/users.js b/src/routes/write/users.js index 4538c20213..4edca5d4a6 100644 --- a/src/routes/write/users.js +++ b/src/routes/write/users.js @@ -35,6 +35,8 @@ function authenticatedRoutes() { setupApiRoute(router, 'post', '/:uid/tokens', [...middlewares, middleware.assert.user], controllers.write.users.generateToken); setupApiRoute(router, 'delete', '/:uid/tokens/:token', [...middlewares, middleware.assert.user], controllers.write.users.deleteToken); + setupApiRoute(router, 'delete', '/:uid/sessions/:uuid', [...middlewares, middleware.assert.user], controllers.write.users.revokeSession); + // Shorthand route to access user routes by userslug router.all('/+bySlug/:userslug*?', [], controllers.write.users.redirectBySlug); } diff --git a/test/controllers.js b/test/controllers.js index df75519526..63865727f5 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -813,19 +813,19 @@ describe('Controllers', function () { }); it('should fail if user doesn\'t exist', function (done) { - request.del(nconf.get('url') + '/api/user/doesnotexist/session/1112233', { + request.del(`${nconf.get('url')}/api/v3/users/doesnotexist/sessions/1112233`, { jar: jar, headers: { 'x-csrf-token': csrf_token, }, }, function (err, res, body) { assert.ifError(err); - assert.equal(res.statusCode, 403); + assert.equal(res.statusCode, 404); assert.deepEqual(JSON.parse(body), { response: {}, status: { - code: 'forbidden', - message: 'You are not authorised to make this call', + code: 'not-found', + message: '[[error:no-user]]', }, }); done(); @@ -839,15 +839,21 @@ describe('Controllers', function () { db.sessionStore.get(sid, function (err, sessionObj) { assert.ifError(err); - request.del(nconf.get('url') + '/api/user/revokeme/session/' + sessionObj.meta.uuid, { + request.del(`${nconf.get('url')}/api/v3/users/${uid}/sessions/${sessionObj.meta.uuid}`, { jar: jar, headers: { 'x-csrf-token': csrf_token, }, }, function (err, res, body) { assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body, 'OK'); + assert.strictEqual(res.statusCode, 200); + assert.deepStrictEqual(JSON.parse(body), { + status: { + code: 'ok', + message: 'OK', + }, + response: {}, + }); done(); }); });