'use strict'; exports = module.exports = { get: get, getByUsername: getByUsername, getByEmail: getByEmail, getByAccessToken: getByAccessToken, getByResetToken: getByResetToken, getAllWithGroupIds: getAllWithGroupIds, getAllWithGroupIdsPaged: getAllWithGroupIdsPaged, getByRole: getByRole, add: add, del: del, update: update, count: count, addAppPassword: addAppPassword, getAppPasswords: getAppPasswords, getAppPassword: getAppPassword, delAppPassword: delAppPassword, _clear: clear }; var assert = require('assert'), BoxError = require('./boxerror.js'), database = require('./database.js'), debug = require('debug')('box:userdb'), mysql = require('mysql'); var USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'createdAt', 'resetToken', 'displayName', 'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime' ].join(','); var APP_PASSWORD_FIELDS = [ 'id', 'name', 'userId', 'identifier', 'hashedPassword', 'creationTime' ].join(','); function postProcess(result) { assert.strictEqual(typeof result, 'object'); result.twoFactorAuthenticationEnabled = !!result.twoFactorAuthenticationEnabled; result.active = !!result.active; return result; } function get(userId, callback) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof callback, 'function'); database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE id = ?', [ userId ], function (error, result) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); callback(null, postProcess(result[0])); }); } function getByUsername(username, callback) { assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof callback, 'function'); database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE username = ?', [ username ], function (error, result) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); callback(null, postProcess(result[0])); }); } function getByEmail(email, callback) { assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof callback, 'function'); database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE email = ?', [ email ], function (error, result) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); callback(null, postProcess(result[0])); }); } function getByResetToken(resetToken, callback) { assert.strictEqual(typeof resetToken, 'string'); assert.strictEqual(typeof callback, 'function'); // empty reset tokens means it does not exist if (!resetToken) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE resetToken=?', [ resetToken ], function (error, result) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); callback(null, postProcess(result[0])); }); } function getAllWithGroupIds(callback) { assert.strictEqual(typeof callback, 'function'); database.query('SELECT ' + USERS_FIELDS + ',GROUP_CONCAT(groupMembers.groupId) AS groupIds ' + ' FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId ' + ' GROUP BY users.id ORDER BY users.username', function (error, results) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); results.forEach(function (result) { result.groupIds = result.groupIds ? result.groupIds.split(',') : [ ]; }); results.forEach(postProcess); callback(null, results); }); } function getAllWithGroupIdsPaged(search, page, perPage, callback) { assert(typeof search === 'string' || search === null); assert.strictEqual(typeof page, 'number'); assert.strictEqual(typeof perPage, 'number'); assert.strictEqual(typeof callback, 'function'); var query = `SELECT ${USERS_FIELDS},GROUP_CONCAT(groupMembers.groupId) AS groupIds FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId `; if (search) { query += ' WHERE '; query += '(LOWER(users.username) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')'; query += ' OR '; query += '(LOWER(users.email) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')'; query += ' OR '; query += '(LOWER(users.displayName) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')'; } query += ` GROUP BY users.id ORDER BY users.username ASC LIMIT ${(page-1)*perPage},${perPage} `; database.query(query, function (error, results) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); results.forEach(function (result) { result.groupIds = result.groupIds ? result.groupIds.split(',') : [ ]; }); results.forEach(postProcess); callback(null, results); }); } function getByRole(role, callback) { assert.strictEqual(typeof role, 'string'); assert.strictEqual(typeof callback, 'function'); // the mailer code relies on the first object being the 'owner' (thus the ORDER) database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE role=? ORDER BY createdAt', [ role ], function (error, results) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); results.forEach(postProcess); callback(null, results); }); } function add(userId, user, callback) { assert.strictEqual(typeof userId, 'string'); assert(user.username === null || typeof user.username === 'string'); assert.strictEqual(typeof user.password, 'string'); assert.strictEqual(typeof user.email, 'string'); assert.strictEqual(typeof user.fallbackEmail, 'string'); assert.strictEqual(typeof user.salt, 'string'); assert.strictEqual(typeof user.createdAt, 'string'); assert.strictEqual(typeof user.resetToken, 'string'); assert.strictEqual(typeof user.displayName, 'string'); assert.strictEqual(typeof user.source, 'string'); assert.strictEqual(typeof user.role, 'string'); assert.strictEqual(typeof callback, 'function'); const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, createdAt, resetToken, displayName, source, role) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; const args = [ userId, user.username, user.password, user.email, user.fallbackEmail, user.salt, user.createdAt, user.resetToken, user.displayName, user.source, user.role ]; database.query(query, args, function (error) { if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_email') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'email already exists')); if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_username') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'username already exists')); if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('PRIMARY') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'id already exists')); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); callback(null); }); } function del(userId, callback) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof callback, 'function'); // also cleanup the groupMembers table var queries = []; queries.push({ query: 'DELETE FROM groupMembers WHERE userId = ?', args: [ userId ] }); queries.push({ query: 'DELETE FROM tokens WHERE identifier = ?', args: [ userId ] }); queries.push({ query: 'DELETE FROM appPasswords WHERE userId = ?', args: [ userId ] }); queries.push({ query: 'DELETE FROM users WHERE id = ?', args: [ userId ] }); database.transaction(queries, function (error, result) { if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, error)); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (result[3].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); callback(error); }); } function getByAccessToken(accessToken, callback) { assert.strictEqual(typeof accessToken, 'string'); assert.strictEqual(typeof callback, 'function'); debug('getByAccessToken: ' + accessToken); database.query('SELECT ' + USERS_FIELDS + ' FROM users, tokens WHERE tokens.accessToken = ?', [ accessToken ], function (error, result) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); callback(null, postProcess(result[0])); }); } function clear(callback) { database.query('DELETE FROM users', function (error) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); callback(error); }); } function update(userId, user, callback) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof callback, 'function'); assert(!('username' in user) || (user.username === null || typeof user.username === 'string')); assert(!('email' in user) || (typeof user.email === 'string')); assert(!('fallbackEmail' in user) || (typeof user.fallbackEmail === 'string')); assert(!('twoFactorAuthenticationEnabled' in user) || (typeof user.twoFactorAuthenticationEnabled === 'boolean')); assert(!('role' in user) || (typeof user.role === 'string')); assert(!('active' in user) || (typeof user.active === 'boolean')); var args = [ ]; var fields = [ ]; for (var k in user) { if (k === 'twoFactorAuthenticationEnabled' || k === 'active') { fields.push(k + ' = ?'); args.push(user[k] ? 1 : 0); } else { fields.push(k + ' = ?'); args.push(user[k]); } } args.push(userId); database.query('UPDATE users SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) { if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_email') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'email already exists')); if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_username') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'username already exists')); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); return callback(null); }); } function count(callback) { assert.strictEqual(typeof callback, 'function'); database.query('SELECT COUNT(*) AS total FROM users', function (error, result) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); return callback(null, result[0].total); }); } function getAppPasswords(userId, callback) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof callback, 'function'); database.query('SELECT ' + APP_PASSWORD_FIELDS + ' FROM appPasswords WHERE userId = ?', [ userId ], function (error, results) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); callback(null, results); }); } function getAppPassword(id, callback) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof callback, 'function'); database.query('SELECT ' + APP_PASSWORD_FIELDS + ' FROM appPasswords WHERE id = ?', [ id ], function (error, results) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); callback(null, results[0]); }); } function addAppPassword(id, appPassword, callback) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof appPassword, 'object'); assert.strictEqual(typeof callback, 'function'); const query = 'INSERT INTO appPasswords (id, userId, identifier, name, hashedPassword) VALUES (?, ?, ?, ?, ?)'; const args = [ id, appPassword.userId, appPassword.identifier, appPassword.name, appPassword.hashedPassword ]; database.query(query, args, function (error) { if (error && error.code === 'ER_DUP_ENTRY' && error.message.indexOf('appPasswords_name_userId_identifier') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'name/app combination already exists')); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); callback(null); }); } function delAppPassword(id, callback) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof callback, 'function'); database.query('DELETE FROM appPasswords 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, 'password not found')); return callback(null); }); }