UNPKG

apollo-passport-mongodb-driver

Version:
581 lines (490 loc) 19.5 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); require('regenerator-runtime/runtime'); var _mongodb = require('mongodb'); var _mongodb2 = _interopRequireDefault(_mongodb); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var ObjectId = _mongodb2.default.ObjectId; /** Class implementing the Apollo Passport DBDriver interface */ var MongoDbDriver = function () { /** * Returns a DBDriver instance (for use by Apollo Passport). Parameters are * driver-specific and should be clearly specificied in the README. * This documents the RethinkDBDash DBDriver specifically, although some * *options* are relevant for all drivers. * * @param {db} mongo instance, e.g. MongoClient.connect(url, function(err, db) { ... db }); * * @param {string} options.userTableName default: 'users' * @param {string} options.configTableName default: 'apolloPassportConfig' * @param {string} options.dbName default: current database */ function MongoDbDriver(db) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, MongoDbDriver); this.db = db; this.userTableName = options.userTableName || 'users'; this.configTableName = options.configTableName || 'apolloPassportConfig'; this.dbName = options.dbName; this.readySubs = []; // don't await the init, run async if (options.init !== false) this._init(); } /** * Internal method, documented for benefit of driver authors. Most important * is to call fetchConfig() (XXX unfinished), but may also assert that all * tables exist, and run ready callbacks. */ _createClass(MongoDbDriver, [{ key: '_init', value: function _init() { return regeneratorRuntime.async(function _init$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: this.users = this.db.collection(this.userTableName); this.config = this.db.collection(this.configTableName); this.initted = true; while (this.readySubs.length) { this.readySubs.shift().call(); } case 4: case 'end': return _context.stop(); } } }, null, this); } /** * Internal method, documented for benefit of driver authors. An awaitable * promise that returns if the driver is ready (or when it becomes ready). */ }, { key: '_ready', value: function _ready() { var _this = this; return new Promise(function (resolve) { if (_this.initted) resolve();else _this.readySubs.push(resolve); }); } ////////////////// // CONFIG TABLE // ////////////////// /** * Retrieves _all_ configuration from the database. * @return {object} A nested dictionary arranged by type, i.e. * * ```js * { * service: { // type * facebook: { // id * ...data // value (de-JSONified if from non-document DB) * } * } * } * ``` */ }, { key: 'fetchConfig', value: function fetchConfig() { var results, out; return regeneratorRuntime.async(function fetchConfig$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: _context2.next = 2; return regeneratorRuntime.awrap(this._ready()); case 2: _context2.next = 4; return regeneratorRuntime.awrap(this.config.find().toArray()); case 4: results = _context2.sent; out = {}; results.forEach(function (row) { if (!out[row.type]) out[row.type] = {}; out[row.type][row._id] = row; }); return _context2.abrupt('return', out); case 8: case 'end': return _context2.stop(); } } }, null, this); } /** * Creates or updates the key with the given value. * NoSQL databases can store the destructured value as part of the record. * Fixed-schema databases should JSON-encode the 'value' column. * * @param {string} type - e.g. "service" * @param {string} id - e.g. "facebook" * @param {object} value - e.g. { id: 1, ...profile } */ }, { key: 'setConfigKey', value: function setConfigKey(type, _id, value) { return regeneratorRuntime.async(function setConfigKey$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: _context3.next = 2; return regeneratorRuntime.awrap(this._ready()); case 2: _context3.next = 4; return regeneratorRuntime.awrap(this.config.insertOne(_extends({ type: type, _id: _id }, value))); case 4: case 'end': return _context3.stop(); } } }, null, this); } /////////// // USERS // /////////// /** * Given a user record, save it to the database, and return its given id. * NoSQL databases should store the entire object, schema-based databases * should honor the 'emails' and 'services' keys and store as necessary * in another table. * * @param {object} user * * Emails format follow this standard: * http://passportjs.org/docs/profile * { * emails: [ { value: "me@me.com" } ], * services: [ { facebook: { id: 1, ...profile } } ] * ...anyOtherDataForUserRecordAtCreationTimeFromAppHooks * } * * @return {string} the id of the inserted user record */ }, { key: 'createUser', value: function createUser(user) { var id; return regeneratorRuntime.async(function createUser$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: _context4.next = 2; return regeneratorRuntime.awrap(this._ready()); case 2: if (!user._id) { user._id = new ObjectId().toString(); } user.dateUpdated = new Date(); user.dateAdded = new Date(); user.dateRegistered = new Date(); id = user._id; _context4.next = 9; return regeneratorRuntime.awrap(this.users.insertOne(user)); case 9: return _context4.abrupt('return', id); case 10: case 'end': return _context4.stop(); } } }, null, this); } }, { key: 'updateUser', value: function updateUser(userId, updatedUser) { var result; return regeneratorRuntime.async(function updateUser$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: _context5.next = 2; return regeneratorRuntime.awrap(this._ready()); case 2: _context5.next = 4; return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, { $set: updatedUser, $currentDate: { dateUpdated: true } })); case 4: result = _context5.sent; return _context5.abrupt('return', result.result.ok); case 6: case 'end': return _context5.stop(); } } }, null, this); } /** * Fetches a user record by id. Schema-based databases should merge * appropriate user-data from e.g. `user_emails` and `user_services`. * * @param {string} id - the user record's id * * @return {object} user object in the same format expected by * {@link RethinkDBDashDriver#createUser}, or *null* if none found. */ }, { key: 'fetchUserById', value: function fetchUserById(userId) { return regeneratorRuntime.async(function fetchUserById$(_context6) { while (1) { switch (_context6.prev = _context6.next) { case 0: _context6.next = 2; return regeneratorRuntime.awrap(this._ready()); case 2: return _context6.abrupt('return', this.users.findOne({ _id: userId })); case 3: case 'end': return _context6.stop(); } } }, null, this); } /** * Given a single "email" param, returns the matching user record if one * exists, or null, otherwise. * * @param {string} email - the email address to search for, e.g. "me@me.com" * * @return {object} user object in the same format expected by * {@link RethinkDBDashDriver#createUser}, or *null* if none found. */ }, { key: 'fetchUserByEmail', value: function fetchUserByEmail(email) { var results; return regeneratorRuntime.async(function fetchUserByEmail$(_context7) { while (1) { switch (_context7.prev = _context7.next) { case 0: _context7.next = 2; return regeneratorRuntime.awrap(this._ready()); case 2: _context7.next = 4; return regeneratorRuntime.awrap(this.users.findOne({ 'emails.value': email })); case 4: results = _context7.sent; return _context7.abrupt('return', results || null); case 6: case 'end': return _context7.stop(); } } }, null, this); } /** * Returns a user who has *either* a matching email address or matching * service record, or null, otherwise. * * @param {string} service - name of the service, e.g. "facebook" * @param {string} id - id of the service record, e.g. "152356242" * @param {string} email - the email address to search for, e.g. "me@me.com" * * @return {object} user object in the same format expected by * {@link RethinkDBDashDriver#createUser}, or *null* if none found */ }, { key: 'fetchUserByServiceIdOrEmail', value: function fetchUserByServiceIdOrEmail(service, id, email) { var results; return regeneratorRuntime.async(function fetchUserByServiceIdOrEmail$(_context8) { while (1) { switch (_context8.prev = _context8.next) { case 0: _context8.next = 2; return regeneratorRuntime.awrap(this._ready()); case 2: _context8.next = 4; return regeneratorRuntime.awrap(this.users.findOne({ $or: [_defineProperty({}, 'services.' + service + '.id', id), { 'emails.value': email }] })); case 4: results = _context8.sent; return _context8.abrupt('return', results || null); case 6: case 'end': return _context8.stop(); } } }, null, this); } /** * Given a userId, set the verified field as true, and delete the verification tokens * * @param {string} userId - the id of the user to assert * @param {string} verifiedField - name of the verify field to change to true * @param {string} tokenField - name of the token field to delete * @param {string} tokenExpirationField - name of the token expiration field to delete * */ }, { key: 'verifyUserAccount', value: function verifyUserAccount(userId) { var verifiedField = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'verified'; var _$unset; var tokenField = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'verificationToken'; var tokenExpirationField = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'verificationTokenExpiration'; var user; return regeneratorRuntime.async(function verifyUserAccount$(_context9) { while (1) { switch (_context9.prev = _context9.next) { case 0: _context9.next = 2; return regeneratorRuntime.awrap(this._ready()); case 2: _context9.next = 4; return regeneratorRuntime.awrap(this.users.findOne({ _id: userId })); case 4: user = _context9.sent; _context9.next = 7; return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, { $set: _defineProperty({}, verifiedField, true), $unset: (_$unset = {}, _defineProperty(_$unset, tokenField, ''), _defineProperty(_$unset, tokenExpirationField, ''), _$unset), $currentDate: { dateUpdated: true } })); case 7: case 'end': return _context9.stop(); } } }, null, this); } }, { key: 'addResetPasswordToken', value: function addResetPasswordToken(userId, token, tokenExpiration) { var _$set2; var tokenField = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'resetPassToken'; var tokenExpirationField = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 'resetPassTokenExpiration'; return regeneratorRuntime.async(function addResetPasswordToken$(_context10) { while (1) { switch (_context10.prev = _context10.next) { case 0: _context10.next = 2; return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, { $set: (_$set2 = {}, _defineProperty(_$set2, tokenField, token), _defineProperty(_$set2, tokenExpirationField, tokenExpiration), _$set2), $currentDate: { dateUpdated: true }, // Remove the regular verification token, because if the user reset his password // from the email, we are sure that this his email // We don't want both token to live together $unset: { verificationToken: '', tokenExpirationField: '' } })); case 2: case 'end': return _context10.stop(); } } }, null, this); } /** * Given a userId, ensures the user record contains the given email * address, and updates it with optional data. * * @param {string} userId - the id of the user to assert * @param {string} email - the email address to ensure exists * @param {object} data - optional, e.g. { type: 'work', verified: true } */ }, { key: 'assertUserEmailData', value: function assertUserEmailData(userId, email, data) { var user, userEmail, emailData, idx, _emailData; return regeneratorRuntime.async(function assertUserEmailData$(_context11) { while (1) { switch (_context11.prev = _context11.next) { case 0: _context11.next = 2; return regeneratorRuntime.awrap(this._ready()); case 2: _context11.next = 4; return regeneratorRuntime.awrap(this.users.findOne({ _id: userId })); case 4: user = _context11.sent; userEmail = user.emails.find(function (e) { return e.value === email; }); if (userEmail) { _context11.next = 10; break; } emailData = _extends({ value: email }, data); _context11.next = 10; return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, { $push: { 'emails': emailData }, $currentDate: { dateUpdated: true } })); case 10: if (!data) { _context11.next = 15; break; } idx = user.emails.indexOf(userEmail); _emailData = _extends({}, userEmail, data); _context11.next = 15; return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, { $set: _defineProperty({}, 'emails.' + idx, _emailData), $currentDate: { dateUpdated: true } })); case 15: case 'end': return _context11.stop(); } } }, null, this); } /** * Given a userId, ensure the user record contains the given service * record, and updates it with the given data. * * @param {string} userId - the id of the user to assert * @param {string} service - the name of the service, e.g. "facebook" * @param {object} data - e.g. { id: "4321", displayName: "John Sheppard" } */ }, { key: 'assertUserServiceData', value: function assertUserServiceData(userId, service, data) { return regeneratorRuntime.async(function assertUserServiceData$(_context12) { while (1) { switch (_context12.prev = _context12.next) { case 0: _context12.next = 2; return regeneratorRuntime.awrap(this._ready()); case 2: _context12.next = 4; return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, { $set: { services: _defineProperty({}, service, _extends({}, data)) }, $currentDate: { dateUpdated: true } })); case 4: case 'end': return _context12.stop(); } } }, null, this); } // Not sure if we need this anymore, since fetch*() functions return // normalized data. But let's see. }, { key: 'mapUserToServiceData', value: function mapUserToServiceData(user, service) { return user && user.services && user.services[service]; } }]); return MongoDbDriver; }(); exports.default = MongoDbDriver;