diff --git a/History.md b/History.md index ee57ac10a1..ea2ebe65c3 100644 --- a/History.md +++ b/History.md @@ -13,6 +13,7 @@ * Insert a `Date` header into emails by default: https://github.com/meteor/meteor/pull/6916/files * `meteor test` now supports setting the bind address using `--port IP:PORT` the same as `meteor run` [PR #6964](https://github.com/meteor/meteor/pull/6964) [Issue #6961](https://github.com/meteor/meteor/issues/6961) * `Meteor.apply` now takes a `noRetry` option to opt-out of automatically retrying non-idempotent methods on connection blips: [PR #6180](https://github.com/meteor/meteor/pull/6180) +* Adds `Accounts.onLogout()` a hook directly analogous to `Accounts.onLogin()`. [PR #6889](https://github.com/meteor/meteor/pull/6889) ## v1.3.2.3 diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index e089b45eb1..a7e83902f5 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -319,6 +319,13 @@ Ap.callLoginMethod = function (options) { }; Ap.makeClientLoggedOut = function () { + // Ensure client was successfully logged in before running logout hooks. + if (this.connection._userId) { + this._onLogoutHook.each(function (callback) { + callback(); + return true; + }); + } this._unstoreLoginToken(); this.connection.setUserId(null); this.connection.onReconnect = null; diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index a8f49b00f8..8282789cfa 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -35,6 +35,11 @@ export class AccountsCommon { bindEnvironment: false, debugPrintExceptions: "onLoginFailure callback" }); + + this._onLogoutHook = new Hook({ + bindEnvironment: false, + debugPrintExceptions: "onLogout callback" + }); } /** @@ -156,6 +161,15 @@ export class AccountsCommon { return this._onLoginFailureHook.register(func); } + /** + * @summary Register a callback to be called after a logout attempt succeeds. + * @locus Anywhere + * @param {Function} func The callback to be called when logout is successful. + */ + onLogout(func) { + return this._onLogoutHook.register(func); + } + _initConnection(options) { if (! Meteor.isClient) { return; diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 6783d1ff8b..dbffd61e64 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -176,6 +176,12 @@ Ap._failedLogin = function (connection, attempt) { }); }; +Ap._successfulLogout = function () { + this._onLogoutHook.each(function (callback) { + callback(); + return true; + }); +}; /// /// LOGIN METHODS @@ -532,6 +538,7 @@ Ap._initServerMethods = function () { if (token && this.userId) accounts.destroyToken(this.userId, token); this.setUserId(null); + accounts._successfulLogout(); }; // Delete all the current user's tokens and close all open connections logged diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 3a3628ee01..b6f831d6f6 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -996,6 +996,54 @@ if (Meteor.isClient) (function () { } ]); + testAsyncMulti("passwords - server onLogout hook", [ + function (test, expect) { + Meteor.call("testCaptureLogouts", expect(function (error) { + test.isFalse(error); + })); + }, + function (test, expect) { + this.username = Random.id(); + this.password = "password"; + + Accounts.createUser( + {username: this.username, password: this.password}, + loggedInAs(this.username, test, expect)); + }, + logoutStep, + function (test, expect) { + var self = this; + Meteor.call("testFetchCapturedLogouts", expect(function (error, logouts) { + test.isFalse(error); + test.equal(logouts.length, 1); + var logout = logouts[0]; + test.isTrue(logout.successful); + })); + } + ]); + + testAsyncMulti("passwords - client onLogout hook", [ + function (test, expect) { + var self = this; + this.username = Random.id(); + this.password = "password"; + this.attempt = false; + + this.onLogout = Accounts.onLogout(function () { + self.logoutSuccess = true; + }); + + Accounts.createUser( + {username: this.username, password: this.password}, + loggedInAs(this.username, test, expect)); + }, + logoutStep, + function (test, expect) { + test.isTrue(this.logoutSuccess); + expect(function() {})(); + } + ]); + testAsyncMulti("passwords - server onLoginFailure hook", [ function (test, expect) { this.username = Random.id(); diff --git a/packages/accounts-password/password_tests_setup.js b/packages/accounts-password/password_tests_setup.js index 9f46f7f79a..07db15556b 100644 --- a/packages/accounts-password/password_tests_setup.js +++ b/packages/accounts-password/password_tests_setup.js @@ -54,6 +54,10 @@ Meteor.methods({ capturedLogins[this.connection.id] = []; }, + testCaptureLogouts: function() { + capturedLogouts = []; + }, + testFetchCapturedLogins: function () { if (capturedLogins[this.connection.id]) { var logins = capturedLogins[this.connection.id]; @@ -62,6 +66,10 @@ Meteor.methods({ } else return []; + }, + + testFetchCapturedLogouts: function() { + return capturedLogouts; } }); @@ -88,6 +96,14 @@ Accounts.onLoginFailure(function (attempt) { } }); +var capturedLogouts = []; + +Accounts.onLogout(function() { + capturedLogouts.push({ + successful: true + }); +}); + // Because this is global state that affects every client, we can't turn // it on and off during the tests. Doing so would mean two simultaneous // test runs could collide with each other.