Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a custom connection resolver option #1052

Merged
merged 1 commit into from
Jul 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,23 @@ var options = {
- **configurationBaseUrl {String}**: Overrides client settings base url. By default it uses Auth0's CDN url when `domain` has the format `*.auth0.com`. Otherwise, it uses the provided `domain`.
- **languageBaseUrl {String}**: Overrides the language source url for Auth0's provided translations. By default it uses to Auth0's CDN url `https://cdn.auth0.com`.
- **hashCleanup {Boolean}**: When enabled, it will remove the hash part of the callback url after the user authentication. Defaults to `true`.
- **connectionResolver {Function}**: When in use, provides an extensibility point to make it possible to choose which connection to use based on the username information. Has `username`, `context` and `callback` as parameters. The callback expects an object like: `{type: 'database', name: 'connection name'}`. **This only works for database connections.** Keep in mind that this resolver will run in the username/email input's `onBlur` event, so keep it simple and fast.

```js
var options = {
connectionResolver: function (username, context, cb) {
var domain = username.includes('@') && username.split('@')[1];
if (domain) {
// If the username is test@auth0.com, the connection used will be the `auth0.com` connection.
// Make sure you have a database connection with the name `auth0.com`.
cb({ type: 'database', name: domain });
} else {
// Use the default approach to figure it out the connection
cb(null);
}
}
}
```

#### Language Dictionary Specification

Expand Down
31 changes: 31 additions & 0 deletions src/__tests__/field/__snapshots__/email_pane.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ exports[`EmailPane renders correctly 1`] = `
data-autoComplete={false}
data-invalidHint="invalidErrorHint"
data-isValid={false}
data-onBlur={[Function]}
data-onChange={[Function]}
data-placeholder="placeholder"
data-value="user@example.com"
Expand All @@ -28,6 +29,7 @@ exports[`EmailPane sets \`blankErrorHint\` when username is empty 1`] = `
data-autoComplete={false}
data-invalidHint="blankErrorHint"
data-isValid={false}
data-onBlur={[Function]}
data-onChange={[Function]}
data-placeholder="placeholder"
data-value={undefined}
Expand All @@ -40,6 +42,7 @@ exports[`EmailPane sets autoComplete to true when \`allowAutocomplete\` is true
data-autoComplete={true}
data-invalidHint="invalidErrorHint"
data-isValid={false}
data-onBlur={[Function]}
data-onChange={[Function]}
data-placeholder="placeholder"
data-value="user@example.com"
Expand All @@ -52,8 +55,36 @@ exports[`EmailPane sets isValid as true when \`isFieldVisiblyInvalid\` is false
data-autoComplete={false}
data-invalidHint="invalidErrorHint"
data-isValid={true}
data-onBlur={[Function]}
data-onChange={[Function]}
data-placeholder="placeholder"
data-value="user@example.com"
/>
`;

exports[`EmailPane with a custom \`connectionResolver\` \`swap\` calls \`setResolvedConnection\` 1`] = `
Array [
"model",
"resolvedConnection",
]
`;

exports[`EmailPane with a custom \`connectionResolver\` calls \`connectionResolver\` onBlur 1`] = `
Array [
"user@example.com",
Object {
"connections": "connections",
"id": "id",
},
[Function],
]
`;

exports[`EmailPane with a custom \`connectionResolver\` calls \`swap\` in the \`connectionResolver\` callback 1`] = `
Array [
"updateEntity",
"lock",
1,
[Function],
]
`;
32 changes: 32 additions & 0 deletions src/__tests__/field/__snapshots__/username_pane.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ exports[`UsernamePane renders correctly 1`] = `
data-autoComplete={false}
data-invalidHint="invalidErrorHint"
data-isValid={false}
data-onBlur={[Function]}
data-onChange={[Function]}
data-placeholder="placeholder"
data-value="username"
Expand All @@ -30,6 +31,7 @@ exports[`UsernamePane sets \`blankErrorHint\` when username is empty 1`] = `
data-autoComplete={false}
data-invalidHint="blankErrorHint"
data-isValid={false}
data-onBlur={[Function]}
data-onChange={[Function]}
data-placeholder="placeholder"
data-value={undefined}
Expand All @@ -42,6 +44,7 @@ exports[`UsernamePane sets \`usernameFormatErrorHint\` when usernameLooksLikeEma
data-autoComplete={false}
data-invalidHint="usernameFormatErrorHint,min,max"
data-isValid={false}
data-onBlur={[Function]}
data-onChange={[Function]}
data-placeholder="placeholder"
data-value="username"
Expand All @@ -54,6 +57,7 @@ exports[`UsernamePane sets autoComplete to true when \`allowAutocomplete\` is tr
data-autoComplete={true}
data-invalidHint="invalidErrorHint"
data-isValid={false}
data-onBlur={[Function]}
data-onChange={[Function]}
data-placeholder="placeholder"
data-value="username"
Expand All @@ -66,8 +70,36 @@ exports[`UsernamePane sets isValid as true when \`isFieldVisiblyInvalid\` is fal
data-autoComplete={false}
data-invalidHint="invalidErrorHint"
data-isValid={true}
data-onBlur={[Function]}
data-onChange={[Function]}
data-placeholder="placeholder"
data-value="username"
/>
`;

exports[`UsernamePane with a custom \`connectionResolver\` \`swap\` calls \`setResolvedConnection\` 1`] = `
Array [
"model",
"resolvedConnection",
]
`;

exports[`UsernamePane with a custom \`connectionResolver\` calls \`connectionResolver\` onBlur 1`] = `
Array [
"username",
Object {
"connections": "connections",
"id": "id",
},
[Function],
]
`;

exports[`UsernamePane with a custom \`connectionResolver\` calls \`swap\` in the \`connectionResolver\` callback 1`] = `
Array [
"updateEntity",
"lock",
1,
[Function],
]
`;
65 changes: 63 additions & 2 deletions src/__tests__/field/email_pane.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ describe('EmailPane', () => {
i18n: {
str: (...keys) => keys.join(',')
},
lock: {},
lock: Immutable.fromJS({
client: {
connections: 'connections',
id: 'id'
}
}),
placeholder: 'placeholder'
};

Expand All @@ -40,7 +45,8 @@ describe('EmailPane', () => {
ui: {
avatar: () => false,
allowAutocomplete: () => false
}
},
connectionResolver: () => undefined
}));

jest.mock('avatar', () => ({
Expand Down Expand Up @@ -113,4 +119,59 @@ describe('EmailPane', () => {
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toMatchSnapshot();
});
it('does nothing on blur when there is no custom `connectionResolver`', () => {
let EmailPane = getComponent();

const wrapper = mount(<EmailPane {...defaultProps} />);
const props = extractPropsFromWrapper(wrapper);
props.onBlur({ target: { value: 'newUser@example.com' } });

const { mock } = require('store/index').swap;
expect(mock.calls.length).toBe(0);
});
describe('with a custom `connectionResolver`', () => {
let connectionResolverMock;
let setResolvedConnectionMock;
beforeEach(() => {
connectionResolverMock = jest.fn();
setResolvedConnectionMock = jest.fn();
require('core/index').connectionResolver = () => connectionResolverMock;
require('core/index').setResolvedConnection = setResolvedConnectionMock;
});
it('calls `connectionResolver` onBlur', () => {
let EmailPane = getComponent();

const wrapper = mount(<EmailPane {...defaultProps} />);
const props = extractPropsFromWrapper(wrapper);
props.onBlur({ target: { value: 'newUser@example.com' } });

const { mock } = connectionResolverMock;
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toMatchSnapshot();
});
it('calls `swap` in the `connectionResolver` callback', () => {
let EmailPane = getComponent();

const wrapper = mount(<EmailPane {...defaultProps} />);
const props = extractPropsFromWrapper(wrapper);
props.onBlur({ target: { value: 'newUser@example.com' } });
connectionResolverMock.mock.calls[0][2]('resolvedConnection');
const { mock } = require('store/index').swap;
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toMatchSnapshot();
});
it.only('`swap` calls `setResolvedConnection`', () => {
let EmailPane = getComponent();

const wrapper = mount(<EmailPane {...defaultProps} />);
const props = extractPropsFromWrapper(wrapper);
props.onBlur({ target: { value: 'newUser@example.com' } });
connectionResolverMock.mock.calls[0][2]('resolvedConnection');
require('store/index').swap.mock.calls[0][3]('model');

const { mock } = setResolvedConnectionMock;
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toMatchSnapshot();
});
});
});
66 changes: 64 additions & 2 deletions src/__tests__/field/username_pane.test.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import Immutable from 'immutable';
import { mount } from 'enzyme';

import { expectComponent, extractPropsFromWrapper, mockComponent } from 'testUtils';
Expand All @@ -12,7 +13,12 @@ describe('UsernamePane', () => {
i18n: {
str: (...keys) => keys.join(',')
},
lock: {},
lock: Immutable.fromJS({
client: {
connections: 'connections',
id: 'id'
}
}),
placeholder: 'placeholder',
validateFormat: false,
usernameStyle: 'any',
Expand Down Expand Up @@ -42,7 +48,8 @@ describe('UsernamePane', () => {
ui: {
avatar: () => false,
allowAutocomplete: () => false
}
},
connectionResolver: () => undefined
}));

jest.mock('avatar', () => ({
Expand Down Expand Up @@ -119,4 +126,59 @@ describe('UsernamePane', () => {
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toMatchSnapshot();
});
it('does nothing on blur when there is no custom `connectionResolver`', () => {
let EmailPane = getComponent();

const wrapper = mount(<EmailPane {...defaultProps} />);
const props = extractPropsFromWrapper(wrapper);
props.onBlur({ target: { value: 'newUser@example.com' } });

const { mock } = require('store/index').swap;
expect(mock.calls.length).toBe(0);
});
describe('with a custom `connectionResolver`', () => {
let connectionResolverMock;
let setResolvedConnectionMock;
beforeEach(() => {
connectionResolverMock = jest.fn();
setResolvedConnectionMock = jest.fn();
require('core/index').connectionResolver = () => connectionResolverMock;
require('core/index').setResolvedConnection = setResolvedConnectionMock;
});
it('calls `connectionResolver` onBlur', () => {
let UsernamePane = getComponent();

const wrapper = mount(<UsernamePane {...defaultProps} />);
const props = extractPropsFromWrapper(wrapper);
props.onBlur({ target: { value: 'newUser@example.com' } });

const { mock } = connectionResolverMock;
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toMatchSnapshot();
});
it('calls `swap` in the `connectionResolver` callback', () => {
let UsernamePane = getComponent();

const wrapper = mount(<UsernamePane {...defaultProps} />);
const props = extractPropsFromWrapper(wrapper);
props.onBlur({ target: { value: 'newUser@example.com' } });
connectionResolverMock.mock.calls[0][2]('resolvedConnection');
const { mock } = require('store/index').swap;
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toMatchSnapshot();
});
it('`swap` calls `setResolvedConnection`', () => {
let UsernamePane = getComponent();

const wrapper = mount(<UsernamePane {...defaultProps} />);
const props = extractPropsFromWrapper(wrapper);
props.onBlur({ target: { value: 'newUser@example.com' } });
connectionResolverMock.mock.calls[0][2]('resolvedConnection');
require('store/index').swap.mock.calls[0][3]('model');

const { mock } = setResolvedConnectionMock;
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toMatchSnapshot();
});
});
});
15 changes: 11 additions & 4 deletions src/connection/database/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ export function logIn(id, needsMFA = false) {
const m = read(getEntity, 'lock', id);
const usernameField = databaseLogInWithEmail(m) ? 'email' : 'username';
const username = c.getFieldValue(m, usernameField);

const customResolvedConnection = l.resolvedConnection(m);
let connectionName = databaseConnectionName(m);
if (customResolvedConnection) {
connectionName = customResolvedConnection.name;
}
const params = {
connection: databaseConnectionName(m),
connection: connectionName,
username: username,
password: c.getFieldValue(m, 'password')
};
Expand Down Expand Up @@ -167,8 +171,11 @@ export function resetPassword(id) {
function resetPasswordSuccess(id) {
const m = read(getEntity, 'lock', id);
if (hasScreen(m, 'login')) {
swap(updateEntity, 'lock', id, m =>
setScreen(l.setSubmitting(m, false), 'login', ['']) // array with one empty string tells the function to not clear any field
swap(
updateEntity,
'lock',
id,
m => setScreen(l.setSubmitting(m, false), 'login', ['']) // array with one empty string tells the function to not clear any field
);

// TODO: should be handled by box
Expand Down
28 changes: 27 additions & 1 deletion src/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export function setup(id, clientID, domain, options, hookRunner, emitEventFn) {
defaultADUsernameFromEmailPrefix: options.defaultADUsernameFromEmailPrefix === false
? false
: true,
prefill: options.prefill || {}
prefill: options.prefill || {},
connectionResolver: options.connectionResolver
})
);

Expand Down Expand Up @@ -71,6 +72,31 @@ export function oidcConformant(m) {
return get(m, 'oidcConformant');
}

export function connectionResolver(m) {
return get(m, 'connectionResolver');
}

export function setResolvedConnection(m, resolvedConnection) {
if (!resolvedConnection) {
return set(m, 'resolvedConnection', undefined);
}
if (!resolvedConnection.type || !resolvedConnection.name) {
throw new Error(
'Invalid connection object. The resolved connection must look like: `{ type: "database", name: "connection name" }`.'
);
}
if (resolvedConnection.type !== 'database') {
throw new Error(
'Invalid connection type. Only database connections can be resolved with a custom resolver.'
);
}
return set(m, 'resolvedConnection', resolvedConnection);
}

export function resolvedConnection(m) {
return get(m, 'resolvedConnection');
}

export function languageBaseUrl(m) {
return get(m, 'languageBaseUrl');
}
Expand Down
Loading