diff --git a/docs/api.md b/docs/api.md index a93a01d..4ea613d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -241,6 +241,12 @@ Specify `hideSocial` to hide the ability to sign in with a social provider. ``` +Specify `endpoint` to send the form data to a custom endpoint for logging in. + +```html + +``` + Customize the form by providing your own markup. ```html @@ -349,7 +355,13 @@ Specify `hideSocial` to hide the ability to register with a social provider. ``` -Customize the form by providing your own markup. +Specify `endpoint` to send the form data to a custom endpoint for registration. + +```html + +``` + +Customize the form by providing your own markup. By default, the registration form will render these four fields, and they will be required by the user: `givenName`, `surname`, `email`, and `password`. Express.js users who want to make `givenName` and/or `surname` optional, or to add new required fields (like `username`), can refer to [Stormpath Express Library Guide](https://docs.stormpath.com/nodejs/express/latest/registration.html). @@ -473,6 +485,13 @@ Renders a password reset form. ``` +Specify `endpoint` to send the form data to a custom endpoint for initiating the reset +password process. + +```html + +``` + Customize the form by providing your own markup. ```html @@ -534,6 +553,12 @@ Renders a change password form. The parameter `spToken` is required in order for ``` +Specify `endpoint` to send the form data to a custom endpoint for changing the password. + +```html + +``` + Customize the form by providing your own markup. ```html @@ -598,6 +623,12 @@ Renders a form that allows you to update the user profile. ``` +Specify `endpoint` to send the form data to a custom endpoint for updating the user profile. + +```html + +``` + **Important:** In order to update user data, you need to provide your own POST API for the `me` endpoint. Using the `express-stormpath` library, simply expose a new endpoint as shown below. diff --git a/src/actions/UserActions.js b/src/actions/UserActions.js index 6bcbf07..57d3201 100644 --- a/src/actions/UserActions.js +++ b/src/actions/UserActions.js @@ -7,55 +7,85 @@ function dispatch(event) { }, 0); } +function resolveActionParams(options, settings, callback) { + if (typeof options === 'function') { + return [{}, {}, callback]; + } + + if (typeof settings === 'function' && typeof callback === 'undefined') { + return [options, {}, callback]; + } + + return [options, settings, callback]; +} + class UserActions { - login(options, callback) { + login(options, settings, callback) { + [options, settings, callback] = resolveActionParams(options, settings, callback); + dispatch({ type: UserConstants.USER_LOGIN, options: options, + settings: settings, callback: callback }); } - register(options, callback) { + register(options, settings, callback) { + [options, settings, callback] = resolveActionParams(options, settings, callback); + dispatch({ type: UserConstants.USER_REGISTER, options: options, + settings: settings, callback: callback }); } - forgotPassword(options, callback) { + forgotPassword(options, settings, callback) { + [options, settings, callback] = resolveActionParams(options, settings, callback); + dispatch({ type: UserConstants.USER_FORGOT_PASSWORD, options: options, + settings: settings, callback: callback }); } - verifyEmail(spToken, callback) { + verifyEmail(spToken, settings, callback) { + [options, settings, callback] = resolveActionParams(options, settings, callback); + dispatch({ type: UserConstants.USER_VERIFY_EMAIL, options: { spToken: spToken }, + settings: settings, callback: callback }); } - changePassword(options, callback) { + changePassword(options, settings, callback) { + [options, settings, callback] = resolveActionParams(options, settings, callback); + dispatch({ type: UserConstants.USER_CHANGE_PASSWORD, options: options, + settings: settings, callback: callback }); } - updateProfile(data, callback) { + updateProfile(data, settings, callback) { + [options, settings, callback] = resolveActionParams(options, settings, callback); + dispatch({ type: UserConstants.USER_UPDATE_PROFILE, options: { data: data }, + settings: settings, callback: callback }); } @@ -69,10 +99,16 @@ class UserActions { }); } - logout(callback) { + logout(settings, callback) { + if (typeof settings === 'function' && typeof callback === 'undefined') { + callback = settings; + settings = {}; + } + dispatch({ type: UserConstants.USER_LOGOUT, - callback: callback + callback: callback, + settings: settings }); } } diff --git a/src/app.js b/src/app.js index f3cf024..98e5054 100644 --- a/src/app.js +++ b/src/app.js @@ -79,25 +79,25 @@ class App extends EventEmitter { let appReducer = (payload) => { switch(payload.type) { case UserConstants.USER_LOGIN: - userStore.login(payload.options, payload.callback); + userStore.login(payload.options, payload.settings, payload.callback); break; case UserConstants.USER_LOGOUT: - userStore.logout(payload.callback); + userStore.logout(payload.settings, payload.callback); break; case UserConstants.USER_REGISTER: - userStore.register(payload.options, payload.callback); + userStore.register(payload.options, payload.settings, payload.callback); break; case UserConstants.USER_FORGOT_PASSWORD: - userStore.forgotPassword(payload.options, payload.callback); + userStore.forgotPassword(payload.options, payload.settings, payload.callback); break; case UserConstants.USER_CHANGE_PASSWORD: - userStore.changePassword(payload.options, payload.callback); + userStore.changePassword(payload.options, payload.settings, payload.callback); break; case UserConstants.USER_UPDATE_PROFILE: - userStore.updateProfile(payload.options.data, payload.callback); + userStore.updateProfile(payload.options.data, payload.settings, payload.callback); break; case UserConstants.USER_VERIFY_EMAIL: - userStore.verifyEmail(payload.options.spToken, payload.callback); + userStore.verifyEmail(payload.options.spToken, payload.settings, payload.callback); break; case TokenConstants.TOKEN_SET: userService.setToken(payload.options.type, payload.options.token); diff --git a/src/components/ChangePasswordForm.js b/src/components/ChangePasswordForm.js index 8cbd301..9f4068d 100644 --- a/src/components/ChangePasswordForm.js +++ b/src/components/ChangePasswordForm.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import { Link } from 'react-router'; import utils from '../utils'; @@ -48,6 +48,10 @@ class DefaultChangePasswordForm extends React.Component { } export default class ChangePasswordForm extends React.Component { + static propTypes = { + endpoint: PropTypes.string + }; + state = { spToken: null, fields: { @@ -72,7 +76,7 @@ export default class ChangePasswordForm extends React.Component { e.preventDefault(); e.persist(); - var next = (err, data) => { + var next = (err, data, headers = {}) => { if (err) { return this.setState({ isFormProcessing: false, @@ -91,7 +95,12 @@ export default class ChangePasswordForm extends React.Component { }); } - UserActions.changePassword(data, (err) => { + const settings = { + endpoint: this.props.endpoint, + headers: headers + }; + + UserActions.changePassword(data, settings, (err) => { if (err) { if (err.status === 404) { err.message = 'The reset password token is not valid. Please try resetting your password again.'; @@ -186,7 +195,7 @@ export default class ChangePasswordForm extends React.Component { render() { if (this.props.children) { - let selectedProps = utils.excludeProps(['onSubmit', 'children', 'spToken'], this.props); + let selectedProps = utils.excludeProps(['onSubmit', 'children', 'spToken', 'endpoint'], this.props); return (
diff --git a/src/components/LoginForm.js b/src/components/LoginForm.js index 1627865..f57e370 100644 --- a/src/components/LoginForm.js +++ b/src/components/LoginForm.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import { Link } from 'react-router'; import utils from '../utils'; @@ -143,7 +143,11 @@ class DefaultLoginForm extends React.Component { export default class LoginForm extends React.Component { static contextTypes = { - router: React.PropTypes.object.isRequired + router: PropTypes.object.isRequired + }; + + static propTypes = { + endpoint: PropTypes.string }; state = { @@ -170,7 +174,7 @@ export default class LoginForm extends React.Component { }); }; - var next = (err, data) => { + var next = (err, data, headers = {}) => { if (err) { if (onSubmitError) { return onSubmitError({ @@ -185,11 +189,15 @@ export default class LoginForm extends React.Component { // If the user didn't specify any data, // then simply default to what we have in state. data = data || this.state.fields; + const settings = { + endpoint: this.props.endpoint, + headers: headers + } UserActions.login({ login: data.username, password: data.password - }, (err, result) => { + }, settings, (err, result) => { if (err) { if (onSubmitError) { return onSubmitError({ @@ -289,7 +297,7 @@ export default class LoginForm extends React.Component { render() { if (this.props.children) { - let selectedProps = utils.excludeProps(['redirectTo', 'hideSocial', 'onSubmit', 'onSubmitError', 'onSubmitSuccess', 'children'], this.props); + let selectedProps = utils.excludeProps(['redirectTo', 'hideSocial', 'onSubmit', 'onSubmitError', 'onSubmitSuccess', 'children', 'endpoint'], this.props); return ( diff --git a/src/components/LogoutLink.js b/src/components/LogoutLink.js index c60a7e3..b086168 100644 --- a/src/components/LogoutLink.js +++ b/src/components/LogoutLink.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import utils from './../utils'; import context from './../context'; @@ -6,7 +6,11 @@ import UserActions from './../actions/UserActions'; export default class LogoutLink extends React.Component { static contextTypes = { - router: React.PropTypes.object.isRequired + router: PropTypes.object.isRequired + }; + + static propTypes = { + endpoint: PropTypes.string }; state = { @@ -30,14 +34,14 @@ export default class LogoutLink extends React.Component { if (!this.state.disabled) { this.setState({ disabled: true }); - UserActions.logout(() => { + UserActions.logout({ endpoint: this.props.endpoint }, () => { this._performRedirect(primaryRedirectTo); }); } } render() { - var selectedProps = utils.excludeProps(['redirectTo', 'href', 'onClick', 'disabled', 'children'], this.props); + var selectedProps = utils.excludeProps(['redirectTo', 'href', 'onClick', 'disabled', 'children', 'endpoint'], this.props); return ( diff --git a/src/components/LogoutRoute.js b/src/components/LogoutRoute.js index 6aaa5e2..880711d 100644 --- a/src/components/LogoutRoute.js +++ b/src/components/LogoutRoute.js @@ -7,7 +7,7 @@ import UserActions from './../actions/UserActions'; export default class LogoutRoute extends Route { static defaultProps = { onEnter(nextState, replace, callback) { - UserActions.logout(() => { + UserActions.logout({ endpoint: this.props.endpoint }, () => { var router = context.getRouter(); var homeRoute = router.getHomeRoute(); var loginRoute = router.getLoginRoute(); diff --git a/src/components/RegistrationForm.js b/src/components/RegistrationForm.js index 524831b..38a5487 100644 --- a/src/components/RegistrationForm.js +++ b/src/components/RegistrationForm.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import { Link } from 'react-router'; import utils from '../utils'; @@ -171,7 +171,11 @@ class DefaultRegistrationForm extends React.Component { export default class RegistrationForm extends React.Component { static contextTypes = { - router: React.PropTypes.object.isRequired + router: PropTypes.object.isRequired + }; + + static propTypes = { + endpoint: PropTypes.string }; state = { @@ -203,7 +207,7 @@ export default class RegistrationForm extends React.Component { }); }; - let next = (err, data) => { + let next = (err, data, headers = {}) => { if (err) { if (onSubmitError) { return onSubmitError({ @@ -218,8 +222,12 @@ export default class RegistrationForm extends React.Component { // If the user didn't specify any data, // then simply default to what we have in state. data = data || this.state.fields; + const settings = { + endpoint: this.props.endpoint, + headers: headers + }; - UserActions.register(data, (err, result) => { + UserActions.register(data, settings, (err, result) => { if (err) { if (onSubmitError) { return onSubmitError({ @@ -234,7 +242,7 @@ export default class RegistrationForm extends React.Component { UserActions.login({ login: data.email || data.username, password: data.password - }, (err) => { + }, settings, (err) => { if (err) { if (onSubmitError) { return onSubmitError({ @@ -376,7 +384,7 @@ export default class RegistrationForm extends React.Component { render() { if (this.props.children) { - var selectedProps = utils.excludeProps(['redirectTo', 'hideSocial', 'onSubmit', 'onSubmitError', 'onSubmitSuccess', 'children'], this.props); + var selectedProps = utils.excludeProps(['redirectTo', 'hideSocial', 'onSubmit', 'onSubmitError', 'onSubmitSuccess', 'children', 'endpoint'], this.props); return ( diff --git a/src/components/ResetPasswordForm.js b/src/components/ResetPasswordForm.js index dc98c6d..90db759 100644 --- a/src/components/ResetPasswordForm.js +++ b/src/components/ResetPasswordForm.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import { Link } from 'react-router'; import utils from '../utils'; @@ -44,6 +44,10 @@ class DefaultResetPasswordForm extends React.Component { } export default class ResetPasswordForm extends React.Component { + static propTypes = { + endpoint: PropTypes.string + }; + state = { fields: { email: '' @@ -57,7 +61,7 @@ export default class ResetPasswordForm extends React.Component { e.preventDefault(); e.persist(); - var next = (err, data) => { + var next = (err, data, headers = {}) => { if (err) { return this.setState({ isFormProcessing: false, @@ -68,8 +72,12 @@ export default class ResetPasswordForm extends React.Component { // If the user didn't specify any data, // then simply default to what we have in state. data = data || this.state.fields; + const settings = { + endpoint: this.props.endpoint, + headers: headers + }; - UserActions.forgotPassword(data, (err) => { + UserActions.forgotPassword(data, settings, (err) => { if (err) { this.setState({ isFormProcessing: false, @@ -150,7 +158,7 @@ export default class ResetPasswordForm extends React.Component { render() { if (this.props.children) { - var selectedProps = utils.excludeProps(['onSubmit', 'children'], this.props); + var selectedProps = utils.excludeProps(['onSubmit', 'children', 'endpoint'], this.props); return ( diff --git a/src/components/UserProfileForm.js b/src/components/UserProfileForm.js index 4cb2822..560a438 100644 --- a/src/components/UserProfileForm.js +++ b/src/components/UserProfileForm.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import { Link } from 'react-router'; import utils from '../utils'; @@ -80,7 +80,11 @@ class DefaultUserProfileForm extends React.Component { export default class UserProfileForm extends React.Component { static contextTypes = { - user: React.PropTypes.object + user: PropTypes.object + }; + + static propTypes = { + endpoint: PropTypes.string }; state = { @@ -119,7 +123,7 @@ export default class UserProfileForm extends React.Component { e.preventDefault(); e.persist(); - var next = (err, data) => { + var next = (err, data, headers = {}) => { if (err) { return this.setState({ isFormProcessing: false, @@ -131,8 +135,12 @@ export default class UserProfileForm extends React.Component { // If the user didn't specify any data, // then simply default to what we have in state. data = data || this.state.fields; + const settings = { + endpoint: this.props.endpoint, + headers: headers + }; - UserActions.updateProfile(data, (err) => { + UserActions.updateProfile(data, settings, (err) => { if (err) { return this.setState({ isFormProcessing: false, @@ -214,7 +222,7 @@ export default class UserProfileForm extends React.Component { render() { if (this.props.children) { - let selectedProps = utils.excludeProps(['onSubmit', 'children'], this.props); + let selectedProps = utils.excludeProps(['onSubmit', 'children', 'endpoint'], this.props); return ( diff --git a/src/components/VerifyEmailView.js b/src/components/VerifyEmailView.js index 4be2a0a..135f99c 100644 --- a/src/components/VerifyEmailView.js +++ b/src/components/VerifyEmailView.js @@ -1,10 +1,14 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import utils from '../utils'; import LoginLink from '../components/LoginLink'; import UserActions from '../actions/UserActions'; export default class VerifyEmailView extends React.Component { + static propTypes = { + endpoint: PropTypes.string + }; + state = { status: 'VERIFYING' }; @@ -12,7 +16,7 @@ export default class VerifyEmailView extends React.Component { componentDidMount() { var spToken = this.props.spToken; - UserActions.verifyEmail(spToken, (err) => { + UserActions.verifyEmail(spToken, { endpoint: this.props.endpoint }, (err) => { if (err) { this.setState({ status: 'ERROR' diff --git a/src/dispatchers/FluxDispatcher.js b/src/dispatchers/FluxDispatcher.js index f185ccc..c914cce 100644 --- a/src/dispatchers/FluxDispatcher.js +++ b/src/dispatchers/FluxDispatcher.js @@ -21,6 +21,7 @@ export default class FluxDispatcher { this.dispatcher.dispatch({ actionType: event.type, options: event.options, + settings: event.settings, callback: event.callback }); } diff --git a/src/services/ClientApiUserService.js b/src/services/ClientApiUserService.js index 0342124..1a3c929 100644 --- a/src/services/ClientApiUserService.js +++ b/src/services/ClientApiUserService.js @@ -98,8 +98,9 @@ export default class ClientApiUserService extends UserService { }; } - authenticate(oauthGrantBody, callback) { - this._makeFormRequest('post', this.endpoints.oauthToken, oauthGrantBody, null, (err, result) => { + authenticate(oauthGrantBody, settings, callback) { + const endpoint = settings.endpoint || this.endpoints.oauthToken; + this._makeFormRequest('post', endpoint, oauthGrantBody, settings.headers, (err, result) => { if (err) { return callback(err); } @@ -112,14 +113,14 @@ export default class ClientApiUserService extends UserService { }) } - login(options, callback) { + login(options, settings, callback) { let oauthGrantBody = { grant_type: 'password', username: options.login, password: options.password }; - this.authenticate(oauthGrantBody, callback); + this.authenticate(oauthGrantBody, settings, callback); } refreshToken(token, callback) { @@ -137,14 +138,15 @@ export default class ClientApiUserService extends UserService { }); } - logout(callback) { + logout(settings, callback) { this.getToken('refresh_token').then((token) => { let options = { token: token, token_type_hint: 'refresh_token' }; - this._makeFormRequest('post', this.endpoints.oauthRevoke, options, null, (err, result) => { + const endpoint = settings.endpoint || this.endpoints.oauthRevoke; + this._makeFormRequest('post', endpoint, options, settings.headers, (err, result) => { if (err) { return callback(err); } diff --git a/src/services/UserService.js b/src/services/UserService.js index 9c5b59f..236f277 100644 --- a/src/services/UserService.js +++ b/src/services/UserService.js @@ -35,39 +35,53 @@ export default class UserService extends BaseService { }, callback); } - updateProfile(data, callback) { - this._makeRequest('post', this.endpoints.me, data, null, callback); + updateProfile(data, settings, callback) { + const endpoint = settings.endpoint || this.endpoints.me; + this._makeRequest('post', endpoint, data, settings.headers, callback); } getLoginViewData(callback) { this._makeRequest('get', this.endpoints.login, null, null, callback); } - login(options, callback) { - this._makeRequest('post', this.endpoints.login, options, null, this._unwrapAccountResult(callback)); + login(options, settings, callback) { + const endpoint = settings.endpoint || this.endpoints.login; + + this._makeRequest('post', endpoint, options, settings.headers, this._unwrapAccountResult(callback)); } - register(options, callback) { - this._makeRequest('post', this.endpoints.register, options, null, this._unwrapAccountResult(callback)); + register(options, settings, callback) { + const endpoint = settings.endpoint || this.endpoints.register; + + this._makeRequest('post', endpoint, options, settings.headers, this._unwrapAccountResult(callback)); } getRegisterViewData(callback) { this._makeRequest('get', this.endpoints.register, null, null, callback); } - verifyEmail(spToken, callback) { - this._makeRequest('get', this.endpoints.verifyEmail + '?sptoken=' + encodeURIComponent(spToken), null, null, callback); + verifyEmail(spToken, settings, callback) { + const endpoint = (settings.endpoint || this.endpoints.verifyEmail) + + '?sptoken=' + encodeURIComponent(spToken); + + this._makeRequest('get', endpoint, null, settings.headers, callback); } - forgotPassword(options, callback) { - this._makeRequest('post', this.endpoints.forgotPassword, options, null, callback); + forgotPassword(options, settings, callback) { + const endpoint = settings.endpoint || this.endpoints.forgotPassword; + + this._makeRequest('post', endpoint, options, settings.headers, callback); } - changePassword(options, callback) { - this._makeRequest('post', this.endpoints.changePassword, options, null, callback); + changePassword(options, settings, callback) { + const endpoint = settings.endpoint || this.endpoints.forgotPassword; + + this._makeRequest('post', endpoint, options, settings.headers, callback); } - logout(callback) { - this._makeRequest('post', this.endpoints.logout, null, null, callback); + logout(settings, callback) { + const endpoint = settings.endpoint || this.endpoints.logout; + + this._makeRequest('post', endpoint, null, settings.headers, callback); } } diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index c461ef6..6eb8bcc 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js @@ -50,10 +50,10 @@ export default class UserStore extends BaseStore { this.service.getLoginViewData(callback); } - login(options, callback) { + login(options, settings, callback) { this.reset(); - this.service.login(options, (err) => { + this.service.login(options, settings, (err) => { if (err) { return callback(err); } @@ -62,32 +62,32 @@ export default class UserStore extends BaseStore { }); } - register(options, callback) { - this.service.register(options, callback); + register(options, settings, callback) { + this.service.register(options, settings, callback); } getRegisterViewData(callback) { this.service.getRegisterViewData(callback); } - forgotPassword(options, callback) { - this.service.forgotPassword(options, callback); + forgotPassword(options, settings, callback) { + this.service.forgotPassword(options, settings, callback); } - changePassword(options, callback) { - this.service.changePassword(options, callback); + changePassword(options, settings, callback) { + this.service.changePassword(options, settings, callback); } - updateProfile(data, callback) { - this.service.updateProfile(data, callback); + updateProfile(data, settings, callback) { + this.service.updateProfile(data, settings, callback); } - verifyEmail(spToken, callback) { - this.service.verifyEmail(spToken, callback); + verifyEmail(spToken, settings, callback) { + this.service.verifyEmail(spToken, settings, callback); } - logout(callback) { - this.service.logout((err) => { + logout(settings, callback) { + this.service.logout(settings, (err) => { if (err) { return callback(err); }