Skip to content

Commit

Permalink
add support for specifying groups for the AuthenticatedRoute, Authent…
Browse files Browse the repository at this point in the history
…icated and NotAuthenticated components
  • Loading branch information
typerandom committed Mar 12, 2016
1 parent 48d18f7 commit 0f747af
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 33 deletions.
49 changes: 43 additions & 6 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,18 @@ Wrap the `HomeRoute` in an `AuthenticatedRoute` to specify the route you want to
Route that when used, requires that a session is established before continuing. Else redirects the user to the `LoginRoute` path.

```html
<AuthenticatedRoute path='/home/protected' component={RegisterPage} />
<AuthenticatedRoute path='/profile' component={ProfilePage} />
```

Specify the option `inGroup` to only allow users in a certain group to access the path.

```html
<AuthenticatedRoute path='/admin' inGroup="administrator" component={AdminPage} />
<AuthenticatedRoute path='/admin/support' inGroup={["administrator", "support"]} component={SupportAdminPage} />
```

**Important:** In order to use the `inGroup` option, you must expand the `groups` resource for the `/me` endpoint.

#### LoginRoute

Route that marks a specific route as the place to go in order to login.
Expand Down Expand Up @@ -148,6 +157,24 @@ Renders any child components if there is an established user session.
</Authenticated>
```

Specify the option `inGroup` to only show child components when a user is in a certain group.

```html
<Authenticated inGroup="administrator">
You are authenticated as an administrator!
</Authenticated>
```

Specify an array of groups in order to only show the child components for a user in all of the specified groups.

```html
<Authenticated inGroup={["administrator", "support"]}>
You are authenticated as a support administrator!
</Authenticated>
```

**Important:** In order to use the `inGroup` option, you must expand the `groups` resource for the `/me` endpoint.

#### NotAuthenticated

Renders any child components if there isn't an established user session.
Expand All @@ -158,6 +185,16 @@ Renders any child components if there isn't an established user session.
</NotAuthenticated>
```

Specify the option `inGroup` to only show child components when a user isn't in a certain group.

```html
<NotAuthenticated inGroup="administrator">
You are authenticated as an administrator!
</NotAuthenticated>
```

**Important:** In order to use the `inGroup` option, you must expand the `groups` resource for the `/me` endpoint.

#### LoginForm

Renders a username and password login form.
Expand Down Expand Up @@ -369,12 +406,12 @@ app.post('/me', bodyParser.json(), stormpath.loginRequired, function (req, res)
res.json({ message: message, status: 400 });
res.end();
}

function saveAccount() {
req.user.givenName = req.body.givenName;
req.user.surname = req.body.surname;
req.user.email = req.body.email;

req.user.save(function (err) {
if (err) {
return writeError(err.userMessage || err.message);
Expand All @@ -385,17 +422,17 @@ app.post('/me', bodyParser.json(), stormpath.loginRequired, function (req, res)

if (req.body.password) {
var application = req.app.get('stormpathApplication');

application.authenticateAccount({
username: req.user.username,
password: req.body.existingPassword
}, function (err) {
if (err) {
return writeError('The existing password that you entered was incorrect.');
}

req.user.password = req.body.password();

saveAccount();
});
} else {
Expand Down
17 changes: 15 additions & 2 deletions src/components/Authenticated.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import React from 'react';

import utils from '../utils';

export default class Authenticated extends React.Component {
static contextTypes = {
user: React.PropTypes.object
};

render() {
return this.context.user !== undefined ?
this.props.children : null;
var user = this.context.user;
var authenticated = user !== undefined;

if (authenticated && this.props.inGroup) {
if (user.groups) {
authenticated = utils.isInGroup(user.groups, this.props.inGroup);
} else {
console.log(authenticated, user, user.groups);
utils.logWarning('<Authenticated> In order to use the inGroup option, you must expand the groups resource for the /me endpoint.');
}
}

return authenticated ? utils.enforceRootElement(this.props.children) : null;
}
}
4 changes: 3 additions & 1 deletion src/components/AuthenticatedRoute.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import UserStore from './../stores/UserStore';
export default class AuthenticatedRoute extends Route {
static defaultProps = {
onEnter(nextState, replaceState, callback) {
UserStore.isAuthenticated((err, authenticated) => {
UserStore.isAuthenticated({
inGroup: this.inGroup
}, (err, authenticated) => {
if (!authenticated) {
var router = context.getRouter();
var homeRoute = router.getHomeRoute();
Expand Down
20 changes: 18 additions & 2 deletions src/components/NotAuthenticated.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import React from 'react';

import utils from '../utils';

export default class NotAuthenticated extends React.Component {
static contextTypes = {
user: React.PropTypes.object
};

render() {
return this.context.user === undefined ?
this.props.children : null;
var user = this.context.user;
var authenticated = user !== undefined;

if (this.props.inGroup) {
if (authenticated) {
if (user.groups) {
authenticated = utils.isInGroup(user.groups, this.props.inGroup);
} else {
utils.logWarning('<NotAuthenticated> In order to use the inGroup option, you must expand the groups resource for the /me endpoint.');
}
} else {
return null;
}
}

return !authenticated ? utils.enforceRootElement(this.props.children) : null;
}
}
4 changes: 3 additions & 1 deletion src/components/UserComponent.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React from 'react';

import utils from '../utils';
import UserStore from '../stores/UserStore';

export default class UserComponent extends React.Component {
onChangeListener = null;

constructor() {
super(...arguments);
console.error('Stormpath SDK: Warning! The UserComponent class has been deprecated. Please use the user context instead. See: https://github.com/stormpath/stormpath-sdk-react/blob/master/docs/api.md#contexts');
utils.logWarning('The UserComponent class has been deprecated. Please use the user context instead. See: https://github.com/stormpath/stormpath-sdk-react/blob/master/docs/api.md#contexts');
}

state = {
Expand Down
4 changes: 3 additions & 1 deletion src/components/UserField.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react';

import utils from '../utils';
import UserComponent from './UserComponent';

export default class UserField extends UserComponent {
constructor() {
super(...arguments);
console.error('Stormpath SDK: Warning! The UserField component has been deprecated. Please use the user context instead. See: https://github.com/stormpath/stormpath-sdk-react/blob/master/docs/api.md#contexts');
utils.logWarning('The UserField component has been deprecated. Please use the user context instead. See: https://github.com/stormpath/stormpath-sdk-react/blob/master/docs/api.md#contexts');
}

_resolveFieldValue(name) {
Expand Down
20 changes: 11 additions & 9 deletions src/components/UserProfileForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default class UserProfileForm extends React.Component {
isFormSuccessful: false
};

_updateSessionData = (data) => {
_updateSessionData = (data, callback) => {
var sessionStore = context.sessionStore;

if (!sessionStore.empty()) {
Expand All @@ -111,7 +111,9 @@ export default class UserProfileForm extends React.Component {
}

if (hasChanged) {
sessionStore.set(updatedSession);
UserStore.resolveSession(callback, true);
} else {
callback();
}
}
};
Expand All @@ -132,7 +134,7 @@ export default class UserProfileForm extends React.Component {
// then simply default to what we have in state.
data = data || this.state.fields;

UserActions.updateProfile(data, (err, result) => {
UserActions.updateProfile(data, (err) => {
if (err) {
return this.setState({
isFormProcessing: false,
Expand All @@ -141,12 +143,12 @@ export default class UserProfileForm extends React.Component {
});
}

this._updateSessionData(data);

this.setState({
isFormProcessing: false,
isFormSuccessful: true,
errorMessage: null
this._updateSessionData(data, () => {
this.setState({
isFormProcessing: false,
isFormSuccessful: true,
errorMessage: null
});
});
});
};
Expand Down
6 changes: 6 additions & 0 deletions src/stores/SessionStore.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import BaseStore from './BaseStore';

import utils from '../utils';

export default class SessionStore extends BaseStore {
session = undefined;

Expand All @@ -8,6 +10,10 @@ export default class SessionStore extends BaseStore {
}

set(session) {
if (session && session.groups) {
session.groups = utils.getEnabledGroups(session.groups);
}

if (JSON.stringify(this.session) !== JSON.stringify(session)) {
this.session = session;
this.emitChange(session);
Expand Down
34 changes: 23 additions & 11 deletions src/stores/UserStore.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import app from '../app';
import context from '../context';

import utils from '../utils';
import BaseStore from './BaseStore';
import SessionStore from './SessionStore';
import UserService from '../services/UserService';
Expand All @@ -19,9 +20,24 @@ class UserStore extends BaseStore {
this.resolveSession();
}

isAuthenticated(callback) {
this.resolveSession((err, result) => {
callback(err, !err && !this.sessionStore.empty());
isAuthenticated(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}

this.resolveSession((err, user) => {
var authenticated = !err && !this.sessionStore.empty();

if (authenticated && options.inGroup) {
if (user.groups) {
authenticated = utils.isInGroup(user.groups, options.inGroup);
} else {
utils.logWarning('<AuthenticatedRoute> In order to use the inGroup option, you must expand the groups resource for the /me endpoint.');
}
}

callback(err, authenticated);
});
}

Expand All @@ -37,11 +53,7 @@ class UserStore extends BaseStore {
return callback(err);
}

this.sessionError = null;
this.sessionStore.set(result);
this.emitChange();

callback(null, result);
this.resolveSession(callback);
});
}

Expand Down Expand Up @@ -82,8 +94,8 @@ class UserStore extends BaseStore {
});
}

resolveSession(callback) {
if (this.sessionError || !this.sessionStore.empty()) {
resolveSession(callback, force) {
if (!force && (this.sessionError || !this.sessionStore.empty())) {
return callback && callback(this.sessionError, this.sessionStore.get());
}

Expand All @@ -97,7 +109,7 @@ class UserStore extends BaseStore {
}

if (callback) {
callback(this.sessionError, this.session);
callback(this.sessionError, this.sessionStore.get());
}

this.emitChange();
Expand Down
51 changes: 51 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,57 @@ class Utils {

return urlA.host === urlB.host;
}

logWarning(message) {
console.error('[WARNING] Stormpath SDK: ' + message);
}

getEnabledGroups(groups) {
var enabledGroups = {};

if (groups && groups.items) {
groups.items.forEach((item) => {
if (item.status === 'ENABLED') {
enabledGroups[item.name] = undefined;
}
});
}

return enabledGroups;
}

isInGroup(groups, assertGroups) {
if (!groups) {
return false;
}

if (typeof assertGroups !== 'array') {
assertGroups = assertGroups ? [assertGroups] : [];
}

var authenticated = true;

assertGroups.forEach((group) => {
if (!(group in groups)) {
authenticated = false;
}
});

return authenticated;
}

isArray(object) {
var nativeIsArray = Array.isArray;
var toString = Object.prototype.toString;
return nativeIsArray(object) || toString.call(object) === '[object Array]';
}

enforceRootElement(object) {
if (typeof object === 'string' || this.isArray(object)) {
object = <span>{ object }</span>;
}
return object;
}
}

export default new Utils()
Empty file added test/ContextTest.js
Empty file.

0 comments on commit 0f747af

Please sign in to comment.