Skip to content

Commit

Permalink
Support custom AccountsClient instances (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tomasz Przytuła committed Mar 13, 2018
1 parent b0ff369 commit dcad01b
Show file tree
Hide file tree
Showing 25 changed files with 989 additions and 450 deletions.
37 changes: 37 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"extends": [
"airbnb-base",
"plugin:meteor/recommended"
],
"plugins": [
"meteor"
],
"env": {
"meteor": true
},
"settings": {
"import/resolver": "meteor"
},
"parser": "babel-eslint",
"globals": {
"Meteor": true,
"it": true,
"describe": true
},
"rules": {
"comma-dangle": [
0
],
"indent": [
2,
4,
{
"SwitchCase": 1
}
],
"import/extensions": [
"off",
"never"
]
}
}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea
.npm
.vscode
node_modules
node_modules
package-lock.json
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
# Changelog

## [0.2.0] - 13.03.2018
New feature:
* Add support for custom AccountsClient ([introduction](./CUSTOM_ACCOUNTS.md))

Related improvements:
* Check if onLogin callback from the dependency is already present
* Check if loginAttempt method was already overridden in provided instance

## [0.1.3] - 04.02.2018
* Inform client if the functionality was not activated on the server
* Client side unit tests

## [0.1.2] - 01.02.2018
* Remove 'lodash' dependency, replace usages using ES6
* Remove 'lodash' dependency, replace usages with ES6
* Remove 'crypto-js' dependency, use 'crypto' instead
* Decrease the server bundle size significantly by the above changes

## [0.1.1] - 27.01.2018
* Print correct error in case of already dissallowed attempt
* Print correct error in case of already disallowed attempt
* Add server side tests

## [0.1.0] - 19.01.2018
Expand Down
73 changes: 73 additions & 0 deletions CUSTOM_ACCOUNTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# AccountsClient

## Custom AccountsClient in Meteor Apps

#### Wait why?

Imagine an example situation where you decided to split your Meteor server into two separate ones to reduce the overload. You found it pretty convenient to use one of them only for serving HCP where the separate one will handle all the logic.

Another example could be an android app in the Google Play store where upon start you could decide between connecting to the *NA* or *EU* server.

#### How ?
To achieve the first example you will point all the client apps to the HCP server and connect to the separate one using:

```js
const remoteConnection = DDP.connect('127.0.0.1:4000');
```
By now you can easily start using methods from the new connection the same way as you are doing it on the main:
```js
remoteConnection.call('delayApocalypse', 1000, (error) => {
if (error) {
console.error('Could not delay the apocalypse:', error);
}
})
```

However migrating the accounts system is a bit more tricky. Upon using the default accounts methods the app's clients will try to log in to the main server by the default (which was supposed to be HCP only).

The first step for migration is to create a new instance of AccountsClient

```js
const accountsClient = new AccountsClient({ connection: remoteConnection });
```

Unfortunately if you want to have a method like `loginWithPassword` then you have to implement it yourself the same way as it's done for the main accounts system [Source](https://github.com/meteor/meteor/blob/46257bad264bf089e35e0fe35494b51fe5849c7b/packages/accounts-password/password_client.js#L33)

But don't worry! Using tprzytula:remember-me you don't have to worry about that.

## Switching to the custom AccountsClient in tprzytula:remember-me

Using this dependency the login logic always stays the same no matter of which AccountsClient system you are currently using. You can switch the accounts system at any point during your app lifetime. After you will be done with the AccountsClient configuration the only thing you need to do is to pass the instance to *changeAccountsSystem* method and voila!

### Example:

##### Configuration:

To let the dependency know that you have and want to use a separate custom account system you need to pass the instance to the `changeAccountsSystem` method.

```js
import { AccountsClient } from 'meteor/accounts-base';
import RememberMe from 'meteor/tprzytula:remember-me';

Meteor.remoteConnection = DDP.connect('127.0.0.1:4000'); // Meteor's server for accounts
Meteor.remoteUsers = new AccountsClient({ connection: Meteor.remoteConnection });

RememberMe.changeAccountsSystem(Meteor.remoteUsers);

```

##### Usage:

After the configuration you can use the newly set accounts system in the same way you were doing it previously.

```js
import RememberMe from 'meteor/tprzytula:remember-me';

RememberMe.loginWithPassword('username', 'password', (error) => {
if (error) {
console.error(error);
return;
}
// success!
}, true);
```
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ Wrapper for a Meteor.loginWithPassword with an addition of rememberMe as a last

The default for rememberMe is true to match the behaviour of Meteor.

`changeAccountsSystem(AccountsClient: customAccounts)`

Gives the possibility to set a custom accounts instance to be used for the login system ([more details](./CUSTOM_ACCOUNTS.md))

## Testing

You can test this dependency by running `npm run test`
30 changes: 30 additions & 0 deletions client/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Checks which param is the rememberMe flag
* and returns it. If it's not present then
* returns "true" by default.
* @param {Array} params
* @returns {boolean} flag
*/
export const exportFlagFromParams = (params = []) => {
const [
firstParam = () => {},
secondParam = true,
] = params;
return (typeof firstParam === 'boolean')
? firstParam
: secondParam;
};

/**
* Checks if the first provided param is the callback
* function. If it's not present then returns an
* empty method instead.
* @param {Array} params
* @returns {function} callback
*/
export const exportCallbackFromParams = (params = []) => {
const [firstParam] = params;
return (typeof firstParam === 'function')
? firstParam
: () => {};
};
201 changes: 167 additions & 34 deletions client/index.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,171 @@
import {
Accounts,
AccountsClient
} from 'meteor/accounts-base';

import {
exportFlagFromParams,
exportCallbackFromParams
} from './helpers';

import overrideAccountsLogin from './overrideLogin';
overrideAccountsLogin();

const RememberMe = {};
const updateRememberMe = 'tprzytula:rememberMe-update';

RememberMe.loginWithPassword = (user, password, callback = () => {}, rememberMe = true) => {
const flag = (typeof callback === 'boolean')
? callback
: rememberMe;

const callbackMethod = (typeof callback === 'function')
? callback
: () => {};

Meteor.loginWithPassword(user, password, (error) => {
if (!error) {
Meteor.call(updateRememberMe, flag, (error) => {
if (error && error.error === 404) {
console.warn(
'Dependency meteor/tprzytula:remember-me is not active!\n',
'\nTo activate it make sure to run "RememberMe.activate()" on the server.' +
'It is required to be able to access the functionality on the client.'
)
} else if (error) {
console.error(
'meteor/tprzytula:remember-me' +
'\nCould not update remember me setting.' +
'\nError:', error
);

/**
* RememberMe
*
* @property {Object} remoteConnection - handler to a custom connection.
* @property {string} methodName - unique name for the rememberMe method
*
* @class
*/
class RememberMe {
constructor() {
this.remoteConnection = null;
this.methodName = 'tprzytula:rememberMe-update';
overrideAccountsLogin(Accounts);
}

/**
* Returns login method either from the main
* connection or remote one if set.
* @returns {function} loginWithPassword
* @private
*/
getLoginWithPasswordMethod() {
return this.remoteConnection
? this.remoteConnection.loginWithPassword
: Meteor.loginWithPassword;
}

/**
* Returns call method either from the main
* connection or remote one if set.
* @returns {function} call
* @private
*/
getCallMethod() {
return this.remoteConnection
? this.remoteConnection.call.bind(this.remoteConnection)
: Meteor.call;
}

/**
* Wrapper for the Meteor.loginWithPassword
* Invokes suitable loginMethod and upon results
* passes it to the user's callback and if there
* were no errors then also invokes a method to
* update the rememberMe flag on the server side.
* @public
*/
loginWithPassword(...params) {
const [user, password, ...rest] = params;
const flag = exportFlagFromParams(rest);
const callbackMethod = exportCallbackFromParams(rest);
const loginMethod = this.getLoginWithPasswordMethod();
loginMethod(user, password, (error) => {
if (!error) {
this.updateFlag(flag);
}
callbackMethod(error);
});
}

/**
* Sends request to the server to update
* the remember me setting.
* @param {boolean} flag
* @private
*/
updateFlag(flag) {
const callMethod = this.getCallMethod();
callMethod(this.methodName, flag, (error) => {
if (error && error.error === 404) {
console.warn(
'Dependency meteor/tprzytula:remember-me is not active!\n',
'\nTo activate it make sure to run "RememberMe.activate()" on the server.' +
'It is required to be able to access the functionality on the client.'
);
} else if (error) {
console.error(
'meteor/tprzytula:remember-me' +
'\nCould not update remember me setting.' +
'\nError:',
error
);
}
});
}

/**
* Switches from using the current login system to
* a new custom one. After switching each login attempt
* will be performed to new accounts instance.
* @param {AccountsClient} customAccounts
* @returns {boolean} result
* @public
*/
changeAccountsSystem(customAccounts) {
if (customAccounts instanceof AccountsClient &&
customAccounts.connection) {
this.remoteConnection = customAccounts.connection;
this.setLoginMethod();
overrideAccountsLogin(customAccounts);
return true;
}
console.error('meteor/tprzytula:remember-me' +
'\nProvided parameter is not a valid AccountsClient.');
return false;
}

/**
* Since freshly created AccountsClients are not having
* this method by default it's required to make sure that
* the set accounts system will contain it.
* @private
*/
setLoginMethod() {
if ('loginWithPassword' in this.remoteConnection) {
// Login method is already present
return;
}

/* eslint-disable */
/*
The method is based on the original one in Accounts:
https://github.com/meteor/meteor/blob/46257bad264bf089e35e0fe35494b51fe5849c7b/packages/accounts-password/password_client.js#L33
*/
this.remoteConnection.loginWithPassword = function (selector, password, callback) {
if (typeof selector === 'string') {
selector = selector.indexOf('@') === -1
? { username: selector }
: { email: selector };
}
Meteor.remoteUsers.callLoginMethod({
methodArguments: [{
user: selector,
password: Accounts._hashPassword(password)
}],
userCallback: function (error, result) {
if (error && error.error === 400 &&
error.reason === 'old password format') {
srpUpgradePath({
upgradeError: error,
userSelector: selector,
plaintextPassword: password
}, callback);
} else if (error) {
callback && callback(error);
} else {
callback && callback();
}
}
});
}
callbackMethod(error);
});
};
};
/* eslint-enable */
}
}

export default new RememberMe();

export default RememberMe;
// Export handle to the class only on TEST environment
export const RememberMeClass = process.env.TEST_METADATA ? RememberMe : null;
Loading

0 comments on commit dcad01b

Please sign in to comment.