diff --git a/src/accesscontrol.js b/src/accesscontrol.js index 3703efc93..baae3d254 100644 --- a/src/accesscontrol.js +++ b/src/accesscontrol.js @@ -4,31 +4,28 @@ exports = module.exports = { verifyToken }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('./boxerror.js'), - debug = require('debug')('box:accesscontrol'), + safe = require('safetydance'), tokens = require('./tokens.js'), - users = require('./users.js'); + users = require('./users.js'), + util = require('util'); -const NOOP_CALLBACK = function (error) { if (error) debug(error); }; +const userGet = util.promisify(users.get); -function verifyToken(accessToken, callback) { +async function verifyToken(accessToken) { assert.strictEqual(typeof accessToken, 'string'); - assert.strictEqual(typeof callback, 'function'); - tokens.getByAccessToken(accessToken, function (error, token) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (error) return callback(error); + const token = await tokens.getByAccessToken(accessToken); + if (!token) throw new BoxError(BoxError.INVALID_CREDENTIALS); - users.get(token.identifier, function (error, user) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (error) return callback(error); + const [error, user] = await safe(userGet(token.identifier)); + if (error && error.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (error) throw error; - if (!user.active) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); + if (!user.active) throw new BoxError(BoxError.INVALID_CREDENTIALS); - tokens.update(token.id, { lastUsedTime: new Date() }, NOOP_CALLBACK); + await safe(tokens.update(token.id, { lastUsedTime: new Date() })); // ignore any error - callback(null, user); - }); - }); + return user; } diff --git a/src/janitor.js b/src/janitor.js index 00e815792..6729e8dff 100644 --- a/src/janitor.js +++ b/src/janitor.js @@ -1,11 +1,12 @@ 'use strict'; -var assert = require('assert'), +const assert = require('assert'), async = require('async'), BoxError = require('./boxerror.js'), debug = require('debug')('box:janitor'), Docker = require('dockerode'), - tokendb = require('./tokendb.js'); + safe = require('safetydance'), + tokens = require('./tokens.js'); exports = module.exports = { cleanupTokens, @@ -16,20 +17,13 @@ const NOOP_CALLBACK = function () { }; const gConnection = new Docker({ socketPath: '/var/run/docker.sock' }); -function cleanupTokens(callback) { - assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob - - callback = callback || NOOP_CALLBACK; - +async function cleanupTokens() { debug('Cleaning up expired tokens'); - tokendb.delExpired(function (error, result) { - if (error) return debug('cleanupTokens: error removing expired tokens', error); + const [error, result] = await safe(tokens.delExpired()); + if (error) return debug('cleanupTokens: error removing expired tokens', error); - debug('Cleaned up %s expired tokens.', result); - - callback(null); - }); + debug(`Cleaned up ${result} expired tokens`,); } function cleanupTmpVolume(containerInfo, callback) { diff --git a/src/provision.js b/src/provision.js index 185ca7eaf..d57e2688c 100644 --- a/src/provision.js +++ b/src/provision.js @@ -137,23 +137,24 @@ function activate(username, password, email, displayName, ip, auditSource, callb debug('activating user:%s email:%s', username, email); - users.createOwner(username, password, email, displayName, auditSource, function (error, userObject) { + users.createOwner(username, password, email, displayName, auditSource, async function (error, userObject) { if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Already activated')); if (error) return callback(error); - tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS, {}, function (error, result) { - if (error) return callback(error); + const token = { clientId: tokens.ID_WEBADMIN, identifier: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; + let result; + [error, result] = await safe(tokens.add(token)); + if (error) return callback(error); - eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { }); + eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { }); - callback(null, { - userId: userObject.id, - token: result.accessToken, - expires: result.expires - }); - - setImmediate(cloudron.onActivated.bind(null, {}, NOOP_CALLBACK)); // hack for now to not block the above http response + callback(null, { + userId: userObject.id, + token: result.accessToken, + expires: result.expires }); + + setImmediate(cloudron.onActivated.bind(null, {}, NOOP_CALLBACK)); // hack for now to not block the above http response }); } diff --git a/src/routes/accesscontrol.js b/src/routes/accesscontrol.js index b38168344..5e1f545fb 100644 --- a/src/routes/accesscontrol.js +++ b/src/routes/accesscontrol.js @@ -8,13 +8,14 @@ exports = module.exports = { websocketAuth }; -var accesscontrol = require('../accesscontrol.js'), +const accesscontrol = require('../accesscontrol.js'), assert = require('assert'), BoxError = require('../boxerror.js'), externalLdap = require('../externalldap.js'), HttpError = require('connect-lastmile').HttpError, - users = require('../users.js'), - speakeasy = require('speakeasy'); + safe = require('safetydance'), + speakeasy = require('speakeasy'), + users = require('../users.js'); function passwordAuth(req, res, next) { assert.strictEqual(typeof req.body, 'object'); @@ -77,17 +78,16 @@ function passwordAuth(req, res, next) { } } -function tokenAuth(req, res, next) { - var token; +async function tokenAuth(req, res, next) { + let token; // this determines the priority if (req.body && req.body.access_token) token = req.body.access_token; if (req.query && req.query.access_token) token = req.query.access_token; if (req.headers && req.headers.authorization) { - var parts = req.headers.authorization.split(' '); + const parts = req.headers.authorization.split(' '); if (parts.length == 2) { - var scheme = parts[0]; - var credentials = parts[1]; + const [scheme, credentials] = parts; if (/^Bearer$/i.test(scheme)) token = credentials; } @@ -95,15 +95,14 @@ function tokenAuth(req, res, next) { if (!token) return next(new HttpError(401, 'Unauthorized')); - accesscontrol.verifyToken(token, function (error, user) { - if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); - if (error) return next(new HttpError(500, error.message)); + const [error, user] = await safe(accesscontrol.verifyToken(token)); + if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); + if (error) return next(new HttpError(500, error.message)); - req.access_token = token; // used in logout route - req.user = user; + req.access_token = token; // used in logout route + req.user = user; - next(); - }); + next(); } function authorize(requiredRole) { @@ -118,19 +117,18 @@ function authorize(requiredRole) { }; } -function websocketAuth(requiredRole, req, res, next) { +async function websocketAuth(requiredRole, req, res, next) { assert.strictEqual(typeof requiredRole, 'string'); if (typeof req.query.access_token !== 'string') return next(new HttpError(401, 'Unauthorized')); - accesscontrol.verifyToken(req.query.access_token, function (error, user) { - if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); - if (error) return next(new HttpError(500, error.message)); + const [error, user] = await safe(accesscontrol.verifyToken(req.query.access_token)); + if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); + if (error) return next(new HttpError(500, error.message)); - req.user = user; + req.user = user; - if (users.compareRoles(req.user.role, requiredRole) < 0) return next(new HttpError(403, `role '${requiredRole}' is required but user has only '${user.role}'`)); + if (users.compareRoles(req.user.role, requiredRole) < 0) return next(new HttpError(403, `role '${requiredRole}' is required but user has only '${user.role}'`)); - next(); - }); + next(); } diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index f67334084..4aeee47c8 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -25,7 +25,7 @@ exports = module.exports = { syncDnsRecords }; -let assert = require('assert'), +const assert = require('assert'), auditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), cloudron = require('../cloudron.js'), @@ -34,16 +34,16 @@ let assert = require('assert'), externalLdap = require('../externalldap.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'), sysinfo = require('../sysinfo.js'), system = require('../system.js'), - tokendb = require('../tokendb.js'), tokens = require('../tokens.js'), translation = require('../translation.js'), updater = require('../updater.js'), users = require('../users.js'), updateChecker = require('../updatechecker.js'); -function login(req, res, next) { +async function login(req, res, next) { assert.strictEqual(typeof req.user, 'object'); if ('type' in req.body && typeof req.body.type !== 'string') return next(new HttpError(400, 'type must be a string')); @@ -53,26 +53,27 @@ function login(req, res, next) { const userAgent = req.headers['user-agent'] || ''; const auditSource = { authType: 'basic', ip: ip }; - const error = tokens.validateTokenType(type); + let error = tokens.validateTokenType(type); if (error) return next(new HttpError(400, error.message)); - tokens.add(type, req.user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS, {}, function (error, token) { - if (error) return next(new HttpError(500, error)); + let token; + [error, token] = await safe(tokens.add({ clientId: type, identifier: req.user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); + if (error) return next(new HttpError(500, error)); - eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) }); + eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) }); - users.checkLoginLocation(req.user, ip, userAgent); + users.checkLoginLocation(req.user, ip, userAgent); - next(new HttpSuccess(200, token)); - }); + next(new HttpSuccess(200, token)); } -function logout(req, res) { +async function logout(req, res) { assert.strictEqual(typeof req.access_token, 'string'); eventlog.add(eventlog.ACTION_USER_LOGOUT, auditSource.fromRequest(req), { userId: req.user.id, user: users.removePrivateFields(req.user) }); - tokendb.delByAccessToken(req.access_token, function () { res.redirect('/login.html'); }); + await safe(tokens.delByAccessToken(req.access_token)); + res.redirect('/login.html'); } function passwordResetRequest(req, res, next) { @@ -99,15 +100,15 @@ function passwordReset(req, res, next) { if (!userObject.username) return next(new HttpError(409, 'No username set')); // setPassword clears the resetToken - users.setPassword(userObject, req.body.password, function (error) { + users.setPassword(userObject, req.body.password, async function (error) { if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message)); if (error) return next(BoxError.toHttpError(error)); - tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS, {}, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + let result; + [error, result] = await safe(tokens.add({ clientId: tokens.ID_WEBADMIN, identifer: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { accessToken: result.accessToken })); - }); + next(new HttpSuccess(202, { accessToken: result.accessToken })); }); }); } diff --git a/src/routes/test/common.js b/src/routes/test/common.js index e15ef9053..97ef11014 100644 --- a/src/routes/test/common.js +++ b/src/routes/test/common.js @@ -7,7 +7,7 @@ const async = require('async'), server = require('../../server.js'), settings = require('../../settings.js'), superagent = require('superagent'), - tokendb = require('../../tokendb.js'); + tokens = require('../../tokens.js'); exports = module.exports = { setup, @@ -66,7 +66,7 @@ function setup(done) { superagent.post(`${serverUrl}/api/v1/users`) .query({ access_token: owner.token }) .send({ username: user.username, email: user.email }) - .end(function (error, result) { + .end(async function (error, result) { expect(error).to.not.be.ok(); expect(result.statusCode).to.equal(201); @@ -74,7 +74,9 @@ function setup(done) { user.token = 'usertoken'; // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add({ id: 'tid-3', accessToken: user.token, identifier: user.id, clientId: 'test-client-id', expires: Date.now() + 10000, scope: 'unused', name: 'fromtest' }, callback); + const token = await tokens.add({ identifier: user.id, clientId: 'test-client-id', expires: Date.now() + 10000, name: 'fromtest' }); + user.token = token.accessToken; + callback(); }); } diff --git a/src/routes/test/profile-test.js b/src/routes/test/profile-test.js index d11c88c6b..12d2bca6c 100644 --- a/src/routes/test/profile-test.js +++ b/src/routes/test/profile-test.js @@ -6,267 +6,187 @@ 'use strict'; -var constants = require('../../constants.js'), - database = require('../../database.js'), +const common = require('./common.js'), expect = require('expect.js'), - hat = require('../../hat.js'), - mailer = require('../../mailer.js'), superagent = require('superagent'), - server = require('../../server.js'), - tokendb = require('../../tokendb.js'); - -const SERVER_URL = 'http://localhost:' + constants.PORT; - -const USERNAME_0 = 'superaDmIn'; -const PASSWORD = 'Foobar?1337'; -const EMAIL_0 = 'silLY@me.com'; -const EMAIL_0_NEW = 'stupID@me.com'; -const EMAIL_0_NEW_FALLBACK = 'stupIDfallback@me.com'; -const DISPLAY_NAME_0_NEW = 'New Name'; + tokens = require('../../tokens.js'); describe('Profile API', function () { - var user_0 = null; - var token_0; + const { setup, cleanup, serverUrl, owner } = common; - function setup(done) { - server.start(function (error) { - expect(!error).to.be.ok(); - - database._clear(function (error) { - expect(error).to.eql(null); - - superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: USERNAME_0, password: PASSWORD, email: EMAIL_0 }) - .end(function (err, res) { - expect(err).to.eql(null); - expect(res.statusCode).to.equal(201); - - // stash for later use - token_0 = res.body.token; - - done(); - }); - }); - }); - } - - function cleanup(done) { - database._clear(function (error) { - expect(!error).to.be.ok(); - - mailer._mailQueue = []; - - server.stop(done); - }); - } + before(setup); + after(cleanup); describe('get profile', function () { - before(setup); - after(cleanup); + it('fails without token', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/profile`) + .ok(() => true); - it('fails without token', function (done) { - superagent.get(SERVER_URL + '/api/v1/profile/').end(function (error, result) { - expect(result.statusCode).to.equal(401); - - done(); - }); + expect(response.statusCode).to.equal(401); }); - it('fails with empty token', function (done) { - superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: '' }).end(function (error, result) { - expect(result.statusCode).to.equal(401); + it('fails with empty token', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/profile`) + .query({ access_token: '' }) + .ok(() => true); - done(); - }); + expect(response.statusCode).to.equal(401); }); - it('fails with invalid token', function (done) { - superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: 'some token' }).end(function (error, result) { - expect(result.statusCode).to.equal(401); + it('fails with invalid token', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/profile`) + .query({ access_token: 'some token' }) + .ok(() => true); - done(); - }); + expect(response.statusCode).to.equal(401); }); - it('succeeds', function (done) { - superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: token_0 }).end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.username).to.equal(USERNAME_0.toLowerCase()); - expect(result.body.email).to.equal(EMAIL_0.toLowerCase()); - expect(result.body.fallbackEmail).to.equal(EMAIL_0.toLowerCase()); - expect(result.body.displayName).to.be.a('string'); - expect(result.body.password).to.not.be.ok(); - expect(result.body.salt).to.not.be.ok(); + it('succeeds', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/profile`) + .query({ access_token: owner.token }); - user_0 = result.body; - - done(); - }); + expect(response.statusCode).to.equal(200); + expect(response.body.username).to.equal(owner.username.toLowerCase()); + expect(response.body.email).to.equal(owner.email.toLowerCase()); + expect(response.body.fallbackEmail).to.equal(owner.email.toLowerCase()); + expect(response.body.displayName).to.be.a('string'); + expect(response.body.password).to.not.be.ok(); + expect(response.body.salt).to.not.be.ok(); }); - it('fails with expired token', function (done) { - var token = hat(8 * 32); - var expires = Date.now() - 2000; // 1 sec + it('fails with expired token', async function () { + const token = await tokens.add({ identifier: '0', clientId: 'clientid-0', expires: Date.now() - 2000 }); + expect(token.accessToken).to.be.a('string'); - tokendb.add({ id: 'tid-3', accessToken: token, identifier: user_0.id, clientId: null, expires: expires, scope: 'unused', name: 'fromtest' }, function (error) { - expect(error).to.not.be.ok(); + const response = await superagent.get(`${serverUrl}/api/v1/profile`) + .query({ access_token: token.accessToken }) + .ok(() => true); - superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) { - expect(result.statusCode).to.equal(401); - - done(); - }); - }); + expect(response.statusCode).to.equal(401); }); - it('fails with invalid token in auth header', function (done) { - superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + 'x' + token_0).end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); + it('fails with invalid token in auth header', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/profile`) + .set('Authorization', 'Bearer ' + 'x' + owner.token) + .ok(() => true); + + expect(response.statusCode).to.equal(401); }); - it('succeeds with token in auth header', function (done) { - superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + token_0).end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.username).to.equal(USERNAME_0.toLowerCase()); - expect(result.body.email).to.equal(EMAIL_0.toLowerCase()); - expect(result.body.displayName).to.be.a('string'); - expect(result.body.password).to.not.be.ok(); - expect(result.body.salt).to.not.be.ok(); - done(); - }); + it('succeeds with token in auth header', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/profile`).set('Authorization', 'Bearer ' + owner.token); + + expect(response.statusCode).to.equal(200); + expect(response.body.username).to.equal(owner.username.toLowerCase()); + expect(response.body.email).to.equal(owner.email.toLowerCase()); + expect(response.body.displayName).to.be.a('string'); + expect(response.body.password).to.not.be.ok(); + expect(response.body.salt).to.not.be.ok(); }); }); describe('update', function () { - before(setup); - after(cleanup); + it('change email fails due to missing token', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile`) + .send({ email: 'newemail@example.com' }) + .ok(() => true); - it('change email fails due to missing token', function (done) { - superagent.post(SERVER_URL + '/api/v1/profile') - .send({ email: EMAIL_0_NEW }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); + expect(response.statusCode).to.equal(401); }); - it('change email fails due to invalid email', function (done) { - superagent.post(SERVER_URL + '/api/v1/profile') - .query({ access_token: token_0 }) + it('change email fails due to invalid email', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile`) + .query({ access_token: owner.token }) .send({ email: 'foo@bar' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .ok(() => true); + + expect(response.statusCode).to.equal(400); }); - it('change user succeeds without email nor displayName', function (done) { - superagent.post(SERVER_URL + '/api/v1/profile') - .query({ access_token: token_0 }) - .send({}) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - done(); - }); + it('change user succeeds without email nor displayName', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile`) + .query({ access_token: owner.token }) + .send({}); + + expect(response.statusCode).to.equal(204); }); - it('change email succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/profile') - .query({ access_token: token_0 }) - .send({ email: EMAIL_0_NEW, fallbackEmail: EMAIL_0_NEW_FALLBACK }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); + it('change email succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile`) + .query({ access_token: owner.token }) + .send({ email: 'newemail@example.Com', fallbackEmail: 'NewFallbackemail@example.com' }); - superagent.get(SERVER_URL + '/api/v1/profile') - .query({ access_token: token_0 }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.username).to.equal(USERNAME_0.toLowerCase()); - expect(res.body.email).to.equal(EMAIL_0_NEW.toLowerCase()); - expect(res.body.fallbackEmail).to.equal(EMAIL_0_NEW_FALLBACK.toLowerCase()); - expect(res.body.displayName).to.equal(''); + expect(response.statusCode).to.equal(204); - done(); - }); - }); + const response2 = await superagent.get(`${serverUrl}/api/v1/profile`) + .query({ access_token: owner.token }); + + expect(response2.statusCode).to.equal(200); + expect(response2.body.username).to.equal(owner.username); + expect(response2.body.email).to.equal('newemail@example.com'); // lower cased + expect(response2.body.fallbackEmail).to.equal('newfallbackemail@example.com'); + expect(response2.body.displayName).to.equal(''); }); - it('change displayName succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/profile') - .query({ access_token: token_0 }) - .send({ displayName: DISPLAY_NAME_0_NEW }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); + it('change displayName succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile`) + .query({ access_token: owner.token }) + .send({ displayName: 'Agent Smith' }); - superagent.get(SERVER_URL + '/api/v1/profile') - .query({ access_token: token_0 }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.username).to.equal(USERNAME_0.toLowerCase()); - expect(res.body.email).to.equal(EMAIL_0_NEW.toLowerCase()); - expect(res.body.displayName).to.equal(DISPLAY_NAME_0_NEW); + expect(response.statusCode).to.equal(204); - done(); - }); - }); + const response2 = await superagent.get(`${serverUrl}/api/v1/profile`) + .query({ access_token: owner.token }); + expect(response2.statusCode).to.equal(200); + expect(response2.body.username).to.equal(owner.username); + expect(response2.body.email).to.equal('newemail@example.com'); // lower cased + expect(response2.body.displayName).to.equal('Agent Smith'); }); }); describe('password change', function () { - before(setup); - after(cleanup); - - it('fails due to missing current password', function (done) { - superagent.post(SERVER_URL + '/api/v1/profile/password') - .query({ access_token: token_0 }) + it('fails due to missing current password', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile/password`) + .query({ access_token: owner.token }) .send({ newPassword: 'some wrong password' }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); + .ok(() => true); + + expect(response.statusCode).to.equal(400); }); - it('fails due to missing new password', function (done) { - superagent.post(SERVER_URL + '/api/v1/profile/password') - .query({ access_token: token_0 }) - .send({ password: PASSWORD }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); + it('fails due to missing new password', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile/password`) + .query({ access_token: owner.token }) + .send({ password: owner.password.password }) + .ok(() => true); + + expect(response.statusCode).to.equal(400); }); - it('fails due to wrong password', function (done) { - superagent.post(SERVER_URL + '/api/v1/profile/password') - .query({ access_token: token_0 }) + it('fails due to wrong password', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile/password`) + .query({ access_token: owner.token }) .send({ password: 'some wrong password', newPassword: 'MOre#$%34' }) - .end(function (err, res) { - expect(res.statusCode).to.equal(412); - done(); - }); + .ok(() => true); + + expect(response.statusCode).to.equal(412); }); - it('fails due to invalid password', function (done) { - superagent.post(SERVER_URL + '/api/v1/profile/password') - .query({ access_token: token_0 }) - .send({ password: PASSWORD, newPassword: 'five' }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); + it('fails due to invalid password', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile/password`) + .query({ access_token: owner.token }) + .send({ password: owner.password, newPassword: 'five' }) + .ok(() => true); + + expect(response.statusCode).to.equal(400); }); - it('succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/profile/password') - .query({ access_token: token_0 }) - .send({ password: PASSWORD, newPassword: 'MOre#$%34' }) - .end(function (err, res) { - expect(res.statusCode).to.equal(204); - done(); - }); + it('succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile/password`) + .query({ access_token: owner.token }) + .send({ password: owner.password, newPassword: 'MOre#$%34' }); + + expect(response.statusCode).to.equal(204); }); }); }); diff --git a/src/routes/test/tokens-test.js b/src/routes/test/tokens-test.js new file mode 100644 index 000000000..76e28c7b8 --- /dev/null +++ b/src/routes/test/tokens-test.js @@ -0,0 +1,66 @@ +'use strict'; + +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +const common = require('./common.js'), + expect = require('expect.js'), + superagent = require('superagent'); + +describe('Tokens API', function () { + const { setup, cleanup, serverUrl, owner } = common; + + before(setup); + after(cleanup); + + let token; + + it('cannot create token with bad name', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/tokens`) + .query({ access_token: owner.token }) + .send({ name: new Array(128).fill('s').join('') }) + .ok(() => true); + expect(response.statusCode).to.equal(400); + }); + + it('can create token', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/tokens`) + .query({ access_token: owner.token }) + .send({ name: 'mytoken1' }); + + expect(response.status).to.equal(201); + expect(response.body).to.be.a('object'); + token = response.body; + }); + + it('can list tokens', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/tokens`) + .query({ access_token: owner.token }); + expect(response.statusCode).to.equal(200); + expect(response.body.tokens.length).to.be(2); // one is owner token on activation + const tokenIds = response.body.tokens.map(t => t.id); + expect(tokenIds).to.contain(token.id); + }); + + it('cannot get non-existent token', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/tokens/foobar`) + .query({ access_token: owner.token }) + .ok(() => true); + expect(response.statusCode).to.equal(404); + }); + + it('can get token', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/tokens/${token.id}`) + .query({ access_token: owner.token }); + expect(response.statusCode).to.equal(200); + expect(response.body.id).to.be(token.id); + }); + + it('can delete token', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/tokens/${token.id}`) + .query({ access_token: owner.token }); + expect(response.statusCode).to.equal(204); + }); +}); diff --git a/src/routes/tokens.js b/src/routes/tokens.js index eb7b917ea..2d91a8836 100644 --- a/src/routes/tokens.js +++ b/src/routes/tokens.js @@ -2,7 +2,7 @@ exports = module.exports = { verifyOwnership, - getAll, + list, get, add, del @@ -12,41 +12,39 @@ const assert = require('assert'), BoxError = require('../boxerror.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'), tokens = require('../tokens.js'); -function verifyOwnership(req, res, next) { +async function verifyOwnership(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.params.id, 'string'); - tokens.get(req.params.id, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(tokens.get(req.params.id)); + if (error) return next(BoxError.toHttpError(error)); + if (!result) return next(new HttpError(404, 'Token not found')); + if (result.identifier !== req.user.id) return next(new HttpError(403, 'User is not owner')); - if (result.identifier !== req.user.id) return next(new HttpError(403, 'User is not owner')); - - req.token = result; - - next(); - }); + req.token = result; + next(); } -function getAll(req, res, next) { +async function list(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - tokens.getAllByUserId(req.user.id, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(tokens.listByUserId(req.user.id)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { tokens: result })); - }); + next(new HttpSuccess(200, { tokens: result })); } function get(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.token, 'object'); - next(new HttpSuccess(200, { token: req.token })); + next(new HttpSuccess(200, req.token)); } -function add(req, res, next) { +async function add(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.body, 'object'); @@ -55,20 +53,18 @@ function add(req, res, next) { const expiresAt = req.body.expiresAt || (Date.now() + (100 * 365 * 24 * 60 * 60 * 1000)); // forever - 100 years TODO maybe we should allow 0 or -1 to make that explicit - tokens.add(tokens.ID_SDK, req.user.id, expiresAt, { name: req.body.name }, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(tokens.add({ clientId: tokens.ID_SDK, identifier: req.user.id, expires: expiresAt, name: req.body.name })); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, { token: result })); - }); + next(new HttpSuccess(201, result)); } -function del(req, res, next) { +async function del(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.token, 'object'); - tokens.del(req.token.id, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(tokens.del(req.token.id)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204, {})); - }); + next(new HttpSuccess(204, {})); } diff --git a/src/server.js b/src/server.js index 8aa779798..13bd58ef1 100644 --- a/src/server.js +++ b/src/server.js @@ -163,7 +163,7 @@ function initializeExpressSync() { router.del ('/api/v1/app_passwords/:id', token, routes.appPasswords.del); // access tokens - router.get ('/api/v1/tokens', token, routes.tokens.getAll); + router.get ('/api/v1/tokens', token, routes.tokens.list); router.post('/api/v1/tokens', json, token, routes.tokens.add); router.get ('/api/v1/tokens/:id', token, routes.tokens.verifyOwnership, routes.tokens.get); router.del ('/api/v1/tokens/:id', token, routes.tokens.verifyOwnership, routes.tokens.del); diff --git a/src/test/database-test.js b/src/test/database-test.js index a2968385e..7190bb36a 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -22,7 +22,6 @@ const appdb = require('../appdb.js'), reverseProxy = require('../reverseproxy.js'), settingsdb = require('../settingsdb.js'), taskdb = require('../taskdb.js'), - tokendb = require('../tokendb.js'), userdb = require('../userdb.js'), _ = require('underscore'); @@ -551,142 +550,6 @@ describe('database', function () { }); }); - describe('token', function () { - var TOKEN_0 = { - id: 'tid-0', - name: 'token0', - accessToken: hat(8 * 32), - identifier: '0', - clientId: 'clientid-0', - expires: Date.now() + 60 * 60000, - lastUsedTime: null, - scope: '' - }; - var TOKEN_1 = { - id: 'tid-1', - name: 'token1', - accessToken: hat(8 * 32), - identifier: '1', - clientId: 'clientid-1', - expires: Number.MAX_SAFE_INTEGER, - lastUsedTime: null, - scope: '' - }; - var TOKEN_2 = { - id: 'tid-2', - name: 'token2', - accessToken: hat(8 * 32), - identifier: '2', - clientId: 'clientid-2', - expires: Date.now(), - lastUsedTime: null, - scope: '' - }; - - it('add succeeds', function (done) { - tokendb.add(TOKEN_0, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('add of same token fails', function (done) { - tokendb.add(TOKEN_0, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.ALREADY_EXISTS); - done(); - }); - }); - - it('get succeeds', function (done) { - tokendb.get(TOKEN_0.id, function (error, result) { - expect(error).to.be(null); - expect(result).to.be.an('object'); - expect(result).to.be.eql(TOKEN_0); - done(); - }); - }); - - it('getByAccessToken succeeds', function (done) { - tokendb.getByAccessToken(TOKEN_0.accessToken, function (error, result) { - expect(error).to.be(null); - expect(result).to.be.an('object'); - expect(result).to.be.eql(TOKEN_0); - done(); - }); - }); - - it('get of nonexisting token fails', function (done) { - tokendb.getByAccessToken(TOKEN_1.accessToken, function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - done(); - }); - }); - - it('getByIdentifier succeeds', function (done) { - tokendb.getByIdentifier(TOKEN_0.identifier, function (error, result) { - expect(error).to.be(null); - expect(result).to.be.an(Array); - expect(result.length).to.equal(1); - expect(result[0]).to.be.an('object'); - expect(result[0]).to.be.eql(TOKEN_0); - done(); - }); - }); - - it('delete fails', function (done) { - tokendb.del(TOKEN_0.id + 'x', function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - done(); - }); - }); - - it('delete succeeds', function (done) { - tokendb.del(TOKEN_0.id, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('getByIdentifier succeeds after token deletion', function (done) { - tokendb.getByIdentifier(TOKEN_0.identifier, function (error, result) { - expect(error).to.be(null); - expect(result).to.be.an(Array); - expect(result.length).to.equal(0); - done(); - }); - }); - - it('cannot delete previously delete record', function (done) { - tokendb.del(TOKEN_0.id, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - done(); - }); - }); - - it('delExpired succeeds', function (done) { - tokendb.add(TOKEN_2, function (error) { - expect(error).to.be(null); - - tokendb.delExpired(function (error, result) { - expect(error).to.not.be.ok(); - expect(result).to.eql(1); - - tokendb.getByAccessToken(TOKEN_2.accessToken, function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - done(); - }); - }); - }); - }); - }); - describe('apps', function () { var APP_0 = { id: 'appid-0', diff --git a/src/test/janitor-test.js b/src/test/janitor-test.js index a34dca980..8b1b602cc 100644 --- a/src/test/janitor-test.js +++ b/src/test/janitor-test.js @@ -6,71 +6,51 @@ 'use strict'; -var async = require('async'), - BoxError = require('../boxerror.js'), - database = require('../database'), +const common = require('./common.js'), expect = require('expect.js'), - hat = require('../hat.js'), janitor = require('../janitor.js'), - tokendb = require('../tokendb.js'); + tokens = require('../tokens.js'); describe('janitor', function () { - var TOKEN_0 = { - id: 'tid-0', - accessToken: hat(8 * 32), - identifier: '0', - clientId: 'clientid-0', - expires: Date.now() + 60 * 60 * 1000, - scope: 'settings', - name: 'clientid0', - lastUsedTime: null - }; - var TOKEN_1 = { - id: 'tid-1', - accessToken: hat(8 * 32), + before(common.setup); + after(common.cleanup); + + const token1 = { + name: 'token1', identifier: '1', clientId: 'clientid-1', - expires: Date.now() - 1000, - scope: 'apps', - name: 'clientid1', + expires: Number.MAX_SAFE_INTEGER, + lastUsedTime: null, + scope: 'unused' + }; + const token2 = { + name: 'token2', + identifier: '2', + clientId: 'clientid-2', + expires: Date.now(), lastUsedTime: null }; - before(function (done) { - async.series([ - database.initialize, - database._clear, - tokendb.add.bind(null, TOKEN_0), - tokendb.add.bind(null, TOKEN_1) - ], done); + it('can cleanupTokens', async function () { + let result = await tokens.add(token1); + token1.id = result.id; + token1.accessToken = result.accessToken; + + result = await tokens.add(token2); + token2.id = result.id; + token2.accessToken = result.accessToken; + + await janitor.cleanupTokens(); }); - after(function (done) { - async.series([ - database._clear, - database.uninitialize - ], done); + it('did not remove the non-expired token', async function () { + const result = await tokens.getByAccessToken(token1.accessToken); + expect(result).to.be.eql(token1); }); - it('can cleanupTokens', function (done) { - janitor.cleanupTokens(done); - }); - - it('did not remove the non-expired token', function (done) { - tokendb.getByAccessToken(TOKEN_0.accessToken, function (error, result) { - expect(error).to.be(null); - expect(result).to.be.eql(TOKEN_0); - done(); - }); - }); - - it('did remove the non-expired token', function (done) { - tokendb.getByAccessToken(TOKEN_1.accessToken, function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - done(); - }); + it('did remove the non-expired token', async function () { + const result = await tokens.getByAccessToken(token2.accessToken); + expect(result).to.be(null); }); }); diff --git a/src/test/tokens-test.js b/src/test/tokens-test.js new file mode 100644 index 000000000..d89228333 --- /dev/null +++ b/src/test/tokens-test.js @@ -0,0 +1,120 @@ +/* jslint node:true */ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +const BoxError = require('../boxerror.js'), + common = require('./common.js'), + expect = require('expect.js'), + safe = require('safetydance'), + tokens = require('../tokens.js'); + +describe('Tokens', function () { + before(common.setup); + after(common.cleanup); + + const TOKEN_0 = { + id: null, + name: 'token0', + accessToken: null, + identifier: '0', + clientId: 'clientid-0', + expires: Date.now() + 60 * 60000, + lastUsedTime: null, + scope: 'unused' + }; + + it('add succeeds', async function () { + const { id, accessToken } = await tokens.add(TOKEN_0); + TOKEN_0.id = id; + TOKEN_0.accessToken = accessToken; + }); + + it('add fails with bad name', async function () { + const badToken = Object.assign({}, TOKEN_0); + badToken.name = new Array(100).fill('x').join(''); + const [error] = await safe(tokens.add(badToken)); + expect(error.reason).to.be(BoxError.BAD_FIELD); + }); + + it('get succeeds', async function () { + const result = await tokens.get(TOKEN_0.id); + expect(result).to.be.eql(TOKEN_0); + }); + + it('getByAccessToken succeeds', async function () { + const result = await tokens.getByAccessToken(TOKEN_0.accessToken); + expect(result).to.be.eql(TOKEN_0); + }); + + it('get of nonexisting token fails', async function () { + const result = await tokens.getByAccessToken('somerandomaccesstoken'); + expect(result).to.be(null); + }); + + it('getAllByUserId succeeds', async function () { + const result = await tokens.getAllByUserId(TOKEN_0.identifier); + expect(result).to.be.an(Array); + expect(result.length).to.equal(1); + expect(result[0]).to.be.an('object'); + expect(result[0]).to.be.eql(TOKEN_0); + }); + + it('delete fails', async function () { + const [error] = await safe(tokens.del(TOKEN_0.id + 'x')); + expect(error).to.be.a(BoxError); + expect(error.reason).to.be(BoxError.NOT_FOUND); + }); + + it('delete succeeds', async function () { + await tokens.del(TOKEN_0.id); + }); + + it('get returns null after token deletion', async function () { + const result = await tokens.get(TOKEN_0.id); + expect(result).to.be(null); + }); + + it('cannot delete previously delete record', async function () { + const [error] = await safe(tokens.del(TOKEN_0.id)); + expect(error).to.be.a(BoxError); + expect(error.reason).to.be(BoxError.NOT_FOUND); + }); + + it('delExpired succeeds', async function () { + const token1 = { + name: 'token1', + identifier: '1', + clientId: 'clientid-1', + expires: Number.MAX_SAFE_INTEGER, + lastUsedTime: null, + scope: 'unused' + }; + const token2 = { + name: 'token2', + identifier: '2', + clientId: 'clientid-2', + expires: Date.now(), + lastUsedTime: null + }; + + let result = await tokens.add(token1); + token1.id = result.id; + token1.accessToken = result.accessToken; + + result = await tokens.add(token2); + token2.id = result.id; + token2.accessToken = result.accessToken; + + await tokens.delExpired(); + + result = await tokens.getByAccessToken(token2.accessToken); + expect(result).to.be(null); + + result = await tokens.getByAccessToken(token1.accessToken); + expect(result).to.eql(token1); + }); +}); diff --git a/src/tokendb.js b/src/tokendb.js deleted file mode 100644 index 314e0c4f2..000000000 --- a/src/tokendb.js +++ /dev/null @@ -1,144 +0,0 @@ -/* jslint node: true */ - -'use strict'; - -exports = module.exports = { - get, - getByAccessToken, - delByAccessToken, - add, - del, - getByIdentifier, - delExpired, - update, - - _clear: clear -}; - -var assert = require('assert'), - BoxError = require('./boxerror.js'), - database = require('./database.js'); - -var TOKENS_FIELDS = [ 'id', 'accessToken', 'identifier', 'clientId', 'scope', 'expires', 'name', 'lastUsedTime' ].join(','); - -function getByAccessToken(accessToken, callback) { - assert.strictEqual(typeof accessToken, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + TOKENS_FIELDS + ' FROM tokens WHERE accessToken = ? AND expires > ?', [ accessToken, Date.now() ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Token not found')); - - callback(null, result[0]); - }); -} - -function delByAccessToken(accessToken, callback) { - assert.strictEqual(typeof accessToken, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM tokens WHERE accessToken = ?', [ accessToken ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Token not found')); - - return callback(null); - }); -} - -function get(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + TOKENS_FIELDS + ' FROM tokens WHERE id = ?', [ id ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Token not found')); - - callback(null, result[0]); - }); -} - -function add(token, callback) { - assert.strictEqual(typeof token, 'object'); - assert.strictEqual(typeof callback, 'function'); - - let { id, accessToken, identifier, clientId, expires, scope, name } = token; - - assert.strictEqual(typeof accessToken, 'string'); - assert.strictEqual(typeof identifier, 'string'); - assert(typeof clientId === 'string' || clientId === null); - assert.strictEqual(typeof expires, 'number'); - assert.strictEqual(typeof scope, 'string'); - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('INSERT INTO tokens (id, accessToken, identifier, clientId, expires, scope, name) VALUES (?, ?, ?, ?, ?, ?, ?)', - [ id, accessToken, identifier, clientId, expires, scope, name ], function (error, result) { - if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS)); - if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function update(id, values, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof values, 'object'); - assert.strictEqual(typeof callback, 'function'); - - let args = [ ]; - let fields = [ ]; - for (let k in values) { - fields.push(k + ' = ?'); - args.push(values[k]); - } - args.push(id); - - database.query('UPDATE tokens SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Token not found')); - - return callback(null); - }); -} - -function del(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM tokens WHERE id = ?', [ id ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Token not found')); - - callback(error); - }); -} - -function getByIdentifier(identifier, callback) { - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + TOKENS_FIELDS + ' FROM tokens WHERE identifier = ?', [ identifier ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, results); - }); -} - -function delExpired(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM tokens WHERE expires <= ?', [ Date.now() ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - return callback(null, result.affectedRows); - }); -} - -function clear(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM tokens', function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - return callback(null); - }); -} diff --git a/src/tokens.js b/src/tokens.js index 7baffd784..b69e5b489 100644 --- a/src/tokens.js +++ b/src/tokens.js @@ -5,7 +5,11 @@ exports = module.exports = { get, update, del, - getAllByUserId, + + delByAccessToken, + delExpired, + + listByUserId, getByAccessToken, validateTokenType, @@ -16,11 +20,13 @@ exports = module.exports = { ID_CLI: 'cid-cli' // created via cli tool }; -let assert = require('assert'), +const TOKENS_FIELDS = [ 'id', 'accessToken', 'identifier', 'clientId', 'scope', 'expires', 'name', 'lastUsedTime' ].join(','); + +const assert = require('assert'), BoxError = require('./boxerror.js'), + database = require('./database.js'), hat = require('./hat.js'), - uuid = require('uuid'), - tokendb = require('./tokendb.js'); + uuid = require('uuid'); function validateTokenName(name) { assert.strictEqual(typeof name, 'string'); @@ -39,88 +45,77 @@ function validateTokenType(type) { return null; } -function add(clientId, userId, expiresAt, options, callback) { - assert.strictEqual(typeof clientId, 'string'); +async function add(token) { + assert.strictEqual(typeof token, 'object'); + + const { clientId, identifier, expires } = token; + const name = token.name || ''; + const error = validateTokenName(name); + if (error) throw error; + + const id = 'tid-' + uuid.v4(); + const accessToken = hat(8 * 32); + const scope = 'unused'; + + await database.query('INSERT INTO tokens (id, accessToken, identifier, clientId, expires, scope, name) VALUES (?, ?, ?, ?, ?, ?, ?)', [ id, accessToken, identifier, clientId, expires, scope, name ]); + + return { id, accessToken, scope, clientId, identifier, expires, name }; +} + +async function get(id) { + assert.strictEqual(typeof id, 'string'); + + const result = await database.query(`SELECT ${TOKENS_FIELDS} FROM tokens WHERE id = ?`, [ id ]); + if (result.length === 0) return null; + + return result[0]; +} + +async function del(id) { + assert.strictEqual(typeof id, 'string'); + + const result = await database.query('DELETE FROM tokens WHERE id = ?', [ id ]); + if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Token not found'); +} + +async function listByUserId(userId) { assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof expiresAt, 'number'); - assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); - const name = options.name || ''; - let error = validateTokenName(name); - if (error) return callback(error); - - const token = { - id: 'tid-' + uuid.v4(), - accessToken: hat(8 * 32), - identifier: userId, - clientId: clientId, - expires: expiresAt, - scope: 'unused', - name: name - }; - - tokendb.add(token, function (error) { - if (error) return callback(error); - - callback(null, { - accessToken: token.accessToken, - tokenScopes: 'unused', - identifier: userId, - clientId: clientId, - expires: expiresAt - }); - }); + return await database.query(`SELECT ${TOKENS_FIELDS} FROM tokens WHERE identifier = ?`, [ userId ]); } -function get(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); +async function getByAccessToken(accessToken) { + assert.strictEqual(typeof accessToken, 'string'); - tokendb.get(id, function (error, result) { - if (error) return callback(error); - - callback(null, result); - }); + const result = await database.query('SELECT ' + TOKENS_FIELDS + ' FROM tokens WHERE accessToken = ? AND expires > ?', [ accessToken, Date.now() ]); + if (result.length === 0) return null; + return result[0]; } -function del(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); +async function delByAccessToken(accessToken) { + assert.strictEqual(typeof accessToken, 'string'); - tokendb.del(id, function (error, result) { - if (error) return callback(error); - - callback(null, result); - }); + const result = await database.query('DELETE FROM tokens WHERE accessToken = ?', [ accessToken ]); + if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Token not found'); } -function getAllByUserId(userId, callback) { - assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - tokendb.getByIdentifier(userId, function (error, result) { - if (error) return callback(error); - - callback(null, result); - }); +async function delExpired() { + const result = await database.query('DELETE FROM tokens WHERE expires <= ?', [ Date.now() ]); + return result.affectedRows; } -function getByAccessToken(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - tokendb.getByAccessToken(id, function (error, result) { - if (error) return callback(error); - - callback(null, result); - }); -} - -function update(id, values, callback) { +async function update(id, values) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof values, 'object'); - assert.strictEqual(typeof callback, 'function'); - tokendb.update(id, values, callback); -} \ No newline at end of file + let args = [ ]; + let fields = [ ]; + for (let k in values) { + fields.push(k + ' = ?'); + args.push(values[k]); + } + args.push(id); + + const result = await database.query('UPDATE tokens SET ' + fields.join(', ') + ' WHERE id = ?', args); + if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Token not found'); +} diff --git a/src/users.js b/src/users.js index 2d32f2805..ca0a114bd 100644 --- a/src/users.js +++ b/src/users.js @@ -695,14 +695,15 @@ function setupAccount(user, data, auditSource, callback) { updateFunc(function (error) { if (error) return callback(error); - setPassword(user, data.password, function (error) { // setPassword clears the resetToken + setPassword(user, data.password, async function (error) { // setPassword clears the resetToken if (error) return callback(error); - tokens.add(tokens.ID_WEBADMIN, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS, {}, function (error, result) { - if (error) return callback(error); + const token = { clientId: tokens.ID_WEBADMIN, identifier: user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; + let result; + [error, result] = await safe(tokens.add(token)); + if (error) return callback(error); - callback(null, result.accessToken); - }); + callback(null, result.accessToken); }); }); });