Repository: auth0/node-auth0 (ts)

Issue:
How to delete guardian multifactor?
I see that there is a method to delete a user's multifactor provider, but it seems that this only accepts "google-authenticator" or "duo" as an option (https://auth0.com/docs/api/management/v2#!/Users/delete_multifactor_by_provider). How can I clear a user's sms verification setup?


**None of the endpoints dealing with Guardian (get Guardian enrollments, delete enrollments) are implemented here.**

Pull request description:
Add Guardian Enrollments endpoints
This PR adds two new methods on `management`:

- `getGuardianEnrollment` for getting a single guardian enrollment
- `deleteGuardianEnrollment` for deleting a guardian enrollment

Note that this PR doesn't include other methods for all the endpoints described on https://auth0.com/docs/api/management/v2#!/Guardian that might be included in future PRs in order to _close_ #249.

Closes #168 (already closed)

### Update (July 3, 2018)

Added a new endpoint for regenerating the recovery code (ref: https://auth0.com/docs/api/management/v2#!/Users/post_recovery_code_regeneration)

Patch:
diff --git a/src/management/GuardianManager.js b/src/management/GuardianManager.js
new file mode 100644
index 000000000..eede511d2
--- /dev/null
+++ b/src/management/GuardianManager.js
@@ -0,0 +1,96 @@
+var ArgumentError = require('rest-facade').ArgumentError;
+var Auth0RestClient = require('../Auth0RestClient');
+var RetryRestClient = require('../RetryRestClient');
+
+/**
+ * Simple facade for consuming a REST API endpoint.
+ * @external RestClient
+ * @see https://github.com/ngonzalvez/rest-facade
+ */
+
+/**
+ * @class
+ * Abstracts interaction with the Guardian endpoint.
+ * @constructor
+ * @memberOf module:management
+ *
+ * @param {Object} options            The client options.
+ * @param {String} options.baseUrl    The URL of the API.
+ * @param {Object} [options.headers]  Headers to be included in all requests.
+ * @param {Object} [options.retry]    Retry Policy Config
+ */
+var GuardianManager = function(options) {
+  if (options === null || typeof options !== 'object') {
+    throw new ArgumentError('Must provide manager options');
+  }
+
+  if (options.baseUrl === null || options.baseUrl === undefined) {
+    throw new ArgumentError('Must provide a base URL for the API');
+  }
+
+  if ('string' !== typeof options.baseUrl || options.baseUrl.length === 0) {
+    throw new ArgumentError('The provided base URL is invalid');
+  }
+
+  var clientOptions = {
+    errorFormatter: { message: 'message', name: 'error' },
+    headers: options.headers,
+    query: { repeatParams: false }
+  };
+
+  /**
+   * Provides an abstraction layer for retrieving Guardian enrollments.
+   *
+   * @type {external:RestClient}
+   */
+  var guardianEnrollmentsAuth0RestClient = new Auth0RestClient(
+    options.baseUrl + '/guardian/enrollments/:id',
+    clientOptions,
+    options.tokenProvider
+  );
+  this.enrollments = new RetryRestClient(guardianEnrollmentsAuth0RestClient, options.retry);
+};
+
+/**
+ * Get a single Guardian enrollment.
+ *
+ * @method    getGuardianEnrollment
+ * @memberOf  module:management.GuardianManager.prototype
+ *
+ * @example
+ * management.users.getGuardianEnrollment({ id: ENROLLMENT_ID }, function (err, enrollment) {
+ *   console.log(enrollment);
+ * });
+ *
+ * @param   {Object}    data      The user data object.
+ * @param   {String}    data.id   The user id.
+ * @param   {Function}  [cb]      Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+GuardianManager.prototype.getGuardianEnrollment = function(params, cb) {
+  return this.enrollments.get(params, cb);
+};
+
+/**
+ * Delete a Guardian enrollment.
+ *
+ * @method    deleteGuardianEnrollment
+ * @memberOf  module:management.GuardianManager.prototype
+ *
+ * @example
+ * management.users.deleteGuardianEnrollment({ id: ENROLLMENT_ID }, function (err, enrollments) {
+ *   console.log(enrollments);
+ * });
+ *
+ * @param   {Object}    data      The user data object.
+ * @param   {String}    data.id   The user id.
+ * @param   {Function}  [cb]      Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+GuardianManager.prototype.deleteGuardianEnrollment = function(params, cb) {
+  return this.enrollments.delete(params, cb);
+};
+
+module.exports = GuardianManager;
diff --git a/src/management/UsersManager.js b/src/management/UsersManager.js
index 6cf05f7c4..534a52409 100644
--- a/src/management/UsersManager.js
+++ b/src/management/UsersManager.js
@@ -106,6 +106,21 @@ var UsersManager = function(options) {
     options.tokenProvider
   );
   this.usersByEmail = new RetryRestClient(usersByEmailClient, options.retry);
+
+  /**
+   * Provides an abstraction layer for regenerating Guardian recovery codes.
+   *
+   * @type {external:RestClient}
+   */
+  var recoveryCodeRegenerationAuth0RestClients = new Auth0RestClient(
+    options.baseUrl + '/users/:id/recovery-code-regeneration',
+    clientOptions,
+    options.tokenProvider
+  );
+  this.recoveryCodeRegenerations = new RetryRestClient(
+    recoveryCodeRegenerationAuth0RestClients,
+    options.retry
+  );
 };
 
 /**
@@ -577,4 +592,33 @@ UsersManager.prototype.getGuardianEnrollments = function() {
   return this.enrollments.get.apply(this.enrollments, arguments);
 };
 
+/**
+ * Generate new Guardian recovery code.
+ *
+ * @method    regenerateRecoveryCode
+ * @memberOf  module:management.UsersManager.prototype
+ *
+ * @example
+ * management.users.regenerateRecoveryCode("USER_ID", function (err, result) {
+ *   console.log(result.recovery_code);
+ * });
+ *
+ * @param   {Object}    params                Get logs data.
+ * @param   {String}    params.id             User id.
+ * @param   {Function}  [cb]                  Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+UsersManager.prototype.regenerateRecoveryCode = function(params, cb) {
+  if (!params || !params.id) {
+    throw new ArgumentError('The userId cannot be null or undefined');
+  }
+
+  if (cb && cb instanceof Function) {
+    return this.recoveryCodeRegenerations.create(params, {}, cb);
+  }
+
+  return this.recoveryCodeRegenerations.create(params, {});
+};
+
 module.exports = UsersManager;
diff --git a/src/management/index.js b/src/management/index.js
index ec0691f1d..2f60ed207 100644
--- a/src/management/index.js
+++ b/src/management/index.js
@@ -26,6 +26,7 @@ var ResourceServersManager = require('./ResourceServersManager');
 var ManagementTokenProvider = require('./ManagementTokenProvider');
 var RulesConfigsManager = require('./RulesConfigsManager');
 var EmailTemplatesManager = require('./EmailTemplatesManager');
+var GuardianManager = require('./GuardianManager');
 
 var BASE_URL_FORMAT = 'https://%s/api/v2';
 var MANAGEMENT_API_AUD_FORMAT = 'https://%s/api/v2/';
@@ -155,6 +156,14 @@ var ManagementClient = function(options) {
    */
   this.users = new UsersManager(managerOptions);
 
+  /**
+   * Simple abstraction for performing CRUD operations on the
+   * guardian endpoint.
+   *
+   * @type {GuardianManager}
+   */
+  this.guardian = new GuardianManager(managerOptions);
+
   /**
    * Simple abstraction for performing CRUD operations on the
    * connections endpoint.
@@ -1236,6 +1245,79 @@ utils.wrapPropertyMethod(
   'users.getGuardianEnrollments'
 );
 
+/**
+ * Generate new Guardian recovery code.
+ *
+ * @method    regenerateRecoveryCode
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * management.regenerateRecoveryCode({ id: USER_ID }, function (err, newRecoveryCode) {
+ *   console.log(newRecoveryCode);
+ * });
+ *
+ * @param   {Object}    data      The user data object.
+ * @param   {String}    data.id   The user id.
+ * @param   {Function}  [cb]      Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(
+  ManagementClient,
+  'regenerateRecoveryCode',
+  'users.regenerateRecoveryCode'
+);
+
+/**
+ * Get a single Guardian enrollment.
+ *
+ * @method    getGuardianEnrollment
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * management.getGuardianEnrollment({ id: ENROLLMENT_ID }, function (err, enrollment) {
+ *   console.log(enrollment);
+ * });
+ *
+ * @param   {Object}    data      The Guardian enrollment data object.
+ * @param   {String}    data.id   The Guardian enrollment id.
+ * @param   {Function}  [cb]      Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(
+  ManagementClient,
+  'getGuardianEnrollment',
+  'guardian.getGuardianEnrollment'
+);
+
+/**
+ * Delete a user's Guardian enrollment.
+ *
+ * @method    deleteGuardianEnrollment
+ * @memberOf  module:management.ManagementClient.prototype
+ *
+ * @example
+ * management.deleteGuardianEnrollment({ id: ENROLLMENT_ID }, function (err) {
+ *   if (err) {
+ *     // Handle error.
+ *   }
+ *
+ *   // Email provider deleted.
+ * });
+ *
+ * @param   {Object}    data      The Guardian enrollment data object.
+ * @param   {String}    data.id   The Guardian enrollment id.
+ * @param   {Function}  [cb]      Callback function.
+ *
+ * @return  {Promise|undefined}
+ */
+utils.wrapPropertyMethod(
+  ManagementClient,
+  'deleteGuardianEnrollment',
+  'guardian.deleteGuardianEnrollment'
+);
+
 /**
  * Get all blacklisted tokens.
  *


Test patch:
diff --git a/test/management/guardian.tests.js b/test/management/guardian.tests.js
new file mode 100644
index 000000000..75f7e2bac
--- /dev/null
+++ b/test/management/guardian.tests.js
@@ -0,0 +1,189 @@
+var expect = require('chai').expect;
+var nock = require('nock');
+
+var SRC_DIR = '../../src';
+var API_URL = 'https://tenants.auth0.com';
+
+var GuardianManager = require(SRC_DIR + '/management/GuardianManager');
+var ArgumentError = require('rest-facade').ArgumentError;
+
+describe('GuardianManager', function() {
+  before(function() {
+    this.token = 'TOKEN';
+    this.guardian = new GuardianManager({
+      headers: { authorization: 'Bearer ' + this.token },
+      baseUrl: API_URL
+    });
+  });
+
+  describe('instance', function() {
+    var methods = ['getGuardianEnrollment', 'deleteGuardianEnrollment'];
+
+    methods.forEach(function(method) {
+      it('should have a ' + method + ' method', function() {
+        expect(this.guardian[method]).to.exist.to.be.an.instanceOf(Function);
+      });
+    });
+  });
+
+  describe('#constructor', function() {
+    it('should error when no options are provided', function() {
+      expect(GuardianManager).to.throw(ArgumentError, 'Must provide manager options');
+    });
+
+    it('should throw an error when no base URL is provided', function() {
+      var manager = GuardianManager.bind(null, {});
+
+      expect(manager).to.throw(ArgumentError, 'Must provide a base URL for the API');
+    });
+
+    it('should throw an error when the base URL is invalid', function() {
+      var manager = GuardianManager.bind(null, { baseUrl: '' });
+
+      expect(manager).to.throw(ArgumentError, 'The provided base URL is invalid');
+    });
+  });
+
+  describe('#getGuardianEnrollment', function() {
+    beforeEach(function() {
+      this.data = {
+        id: 'dev_0000000000000001'
+      };
+      this.params = { id: this.data.id };
+
+      this.request = nock(API_URL)
+        .get('/guardian/enrollments/' + this.data.id)
+        .reply(200);
+    });
+
+    it('should accept a callback', function(done) {
+      this.guardian.getGuardianEnrollment(this.params, done.bind(null, null));
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.guardian
+        .getGuardianEnrollment(this.params)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+    it('should pass any errors to the promise catch handler', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/guardian/enrollment')
+        .reply(500);
+
+      this.guardian.getGuardianEnrollment(this.params).catch(function(err) {
+        expect(err).to.exist;
+        done();
+      });
+    });
+
+    it('should pass the body of the response to the "then" handler', function(done) {
+      nock.cleanAll();
+
+      var data = {
+        id: 'dev_0000000000000001',
+        status: 'pending',
+        name: 'iPhone 7',
+        identifier: '76dc-a90c-a88c-a90c-a88c-a88c-a90c',
+        phone_number: '+1 999999999999',
+        enrolled_at: '2016-07-12T17:56:26.804Z',
+        last_auth: '2016-07-12T17:56:26.804Z'
+      };
+      var request = nock(API_URL)
+        .get('/guardian/enrollments/' + data.id)
+        .reply(200, data);
+
+      this.guardian.getGuardianEnrollment(this.params).then(function(enrollment) {
+        expect(enrollment).to.deep.equal(data);
+
+        done();
+      });
+    });
+
+    it('should perform a GET request to /api/v2/guardian/enrollments', function(done) {
+      var request = this.request;
+
+      var params = { id: this.data.id };
+      this.guardian.getGuardianEnrollment(this.params).then(function() {
+        expect(request.isDone()).to.be.true;
+        done();
+      });
+    });
+
+    it('should include the token in the Authorization header', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .get('/guardian/enrollments/' + this.data.id)
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.guardian.getGuardianEnrollment(this.params).then(function() {
+        expect(request.isDone()).to.be.true;
+        done();
+      });
+    });
+  });
+
+  describe('#deleteGuardianEnrollment', function() {
+    beforeEach(function() {
+      this.data = {
+        id: 'dev_0000000000000001'
+      };
+
+      this.request = nock(API_URL)
+        .delete('/guardian/enrollments/' + this.data.id)
+        .reply(200);
+    });
+
+    it('should accept a callback', function(done) {
+      this.guardian.deleteGuardianEnrollment(this.data, done.bind(null, null));
+    });
+
+    it('should return a promise when no callback is given', function(done) {
+      this.guardian.deleteGuardianEnrollment(this.data).then(done.bind(null, null));
+    });
+
+    it('should perform a DELETE request to /guardian/enrollments/:id', function(done) {
+      var request = this.request;
+
+      this.guardian.deleteGuardianEnrollment(this.data).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+
+    it('should pass any errors to the promise catch handler', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/guardian/enrollments/' + this.data.id)
+        .reply(500);
+
+      this.guardian.deleteGuardianEnrollment(this.data).catch(function(err) {
+        expect(err).to.exist;
+
+        done();
+      });
+    });
+
+    it('should include the token in the authorization header', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .delete('/guardian/enrollments/' + this.data.id)
+        .matchHeader('authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.guardian.deleteGuardianEnrollment(this.data).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+  });
+});
diff --git a/test/management/users.tests.js b/test/management/users.tests.js
index 33ede4485..7d55bb7b1 100644
--- a/test/management/users.tests.js
+++ b/test/management/users.tests.js
@@ -30,7 +30,8 @@ describe('UsersManager', function() {
       'deleteMultifactorProvider',
       'updateUserMetadata',
       'updateAppMetadata',
-      'getGuardianEnrollments'
+      'getGuardianEnrollments',
+      'regenerateRecoveryCode'
     ];
 
     methods.forEach(function(method) {
@@ -963,4 +964,75 @@ describe('UsersManager', function() {
       });
     });
   });
+
+  describe('#regenerateRecoveryCode', function() {
+    var data = {
+      id: 'USER_ID'
+    };
+
+    beforeEach(function() {
+      this.request = nock(API_URL)
+        .post('/users/' + data.id + '/recovery-code-regeneration')
+        .reply(200);
+    });
+
+    it('should validate empty userId', function() {
+      var _this = this;
+      expect(function() {
+        _this.users.regenerateRecoveryCode(null, function() {});
+      }).to.throw('The userId cannot be null or undefined');
+    });
+
+    it('should accept a callback', function(done) {
+      this.users.regenerateRecoveryCode(data, function() {
+        done();
+      });
+    });
+
+    it('should return a promise if no callback is given', function(done) {
+      this.users
+        .regenerateRecoveryCode(data)
+        .then(done.bind(null, null))
+        .catch(done.bind(null, null));
+    });
+
+    it('should pass any errors to the promise catch handler', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/users/' + data.id + '/recovery-code-regeneration')
+        .reply(500);
+
+      this.users.regenerateRecoveryCode(data).catch(function(err) {
+        expect(err).to.exist;
+
+        done();
+      });
+    });
+
+    it('should perform a POST request to /api/v2/users/:id/recovery-code-regeneration', function(done) {
+      var request = this.request;
+
+      this.users.regenerateRecoveryCode(data).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+
+    it('should include the token in the Authorization header', function(done) {
+      nock.cleanAll();
+
+      var request = nock(API_URL)
+        .post('/users/' + data.id + '/recovery-code-regeneration')
+        .matchHeader('Authorization', 'Bearer ' + this.token)
+        .reply(200);
+
+      this.users.regenerateRecoveryCode(data).then(function() {
+        expect(request.isDone()).to.be.true;
+
+        done();
+      });
+    });
+  });
 });