Pup

v1.x

The BasicsRoutes

In your application, routes define what will be rendered or displayed on screen when a user visits a certain URL. For example, if you visit http://myapp.com/login, you're visiting the login route which renders the login page. In Pup, routing is handled using a third-party package called React Router.

React Router is used in Pup because all of its user interface components are written using React. As the name suggests, React Router is designed specifically for defining routes in applications where the user interface is built using React.

All of the routes in Pup are defined in the application's main layout file at /imports/ui/layouts/App/App.js. This component is rendered on client-side startup in /imports/startup/client/index.js.

By default, Pup ships with a few different routes:

Miscellaneous routes

Nondescript routes in Pup. These are the routes that don't fit into a specific group/purpose.

  • / Renders the <Index /> component or "home" page of your application.
  • /terms Renders the <Terms /> component for your application. This is where you can display the "Terms of Service" for your application. Relies on the static pages pattern.
  • /privacy Renders the <Privacy /> component for your application. This is where you can display the "Privacy Policy" for your application. Relies on the static pages pattern.
  • /example-page Renders the <ExamplePage /> component for your application. This is simply an additional placeholder demonstrating how to define a static page. Relies on the static pages pattern.
  • * (no path) Renders the <NotFound /> component for your application. This is displayed whenever a user attempts to access a URL that does not have a matching route defined (e.g., http://localhost:3000/fhqwhgads).

Accounts-related routes

These routes make up the accounts workflows in Pup. Together, these routes allow users to create and manage their accounts.

  • /signup Renders the <Signup /> component for your application. This is where users can create a new account.
  • /login Renders the <Login /> component for your application. This is where existing users can login to their account.
  • /profile Renders the <Profile /> component for your application. This is where logged-in users can change their first name, last name, email address, and password for their account.
  • /recover-password Renders the <RecoverPassword /> component for your application. This is where users can request a password reset if they've forgotten their password.
  • /reset-password/:token Renders the <ResetPassword /> component for your application. This is where users can reset their password, using a unique token that's emailed to them when they request the reset.
  • /logout Renders the <Logout /> component for your application. This is displayed after a user logs out and is useful for promoting your product on social media or new features you've just released.

Example Routes

The following routes are added to Pup to showcase creating a "CRUD" (create, read, update, delete) feature in your application. Feel free to use these as a template for your own features.

  • /documents Renders the <Documents /> component for your application. For logged-in users, displays that user's list of documents that they've created.
  • /documents/new Renders the <NewDocument /> component for your application. For logged-in users, displays a form for creating a new document.
  • /documents/:_id Renders the <ViewDocument /> component for your application. For logged-in users, displays a single document corresponding to the :_id parameter in the route. :_id refers to the literal _id value of the document in the Documents collection.
  • /documents/:_id/edit Renders the <EditDocument /> component for your application. For logged-in users, displays a form for making changes to an existing document, corresponding to the :_id parameter in the route. :_id refers to the literal _id value of the document in the Documents collection.
/imports/startup/client/index.js
import React from 'react';
import { render } from 'react-dom';
import { Meteor } from 'meteor/meteor';
import App from '../../ui/layouts/App/App';

import '../../ui/stylesheets/app.scss';

Meteor.startup(() => render(<App />, document.getElementById('react-root')));
/imports/ui/layouts/App/App.js
import React from 'react';
import PropTypes from 'prop-types';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { Grid } from 'react-bootstrap';
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import { Roles } from 'meteor/alanning:roles';
import Navigation from '../../components/Navigation/Navigation';
import Authenticated from '../../components/Authenticated/Authenticated';
import Public from '../../components/Public/Public';
import Index from '../../pages/Index/Index';
import Documents from '../../pages/Documents/Documents';
import NewDocument from '../../pages/NewDocument/NewDocument';
import ViewDocument from '../../pages/ViewDocument/ViewDocument';
import EditDocument from '../../pages/EditDocument/EditDocument';
import Signup from '../../pages/Signup/Signup';
import Login from '../../pages/Login/Login';
import Logout from '../../pages/Logout/Logout';
import RecoverPassword from '../../pages/RecoverPassword/RecoverPassword';
import ResetPassword from '../../pages/ResetPassword/ResetPassword';
import Profile from '../../pages/Profile/Profile';
import NotFound from '../../pages/NotFound/NotFound';
import Footer from '../../components/Footer/Footer';
import Terms from '../../pages/Terms/Terms';
import Privacy from '../../pages/Privacy/Privacy';

import './App.scss';

const App = props => (
  <Router>
    {!props.loading ? <div className="App">
      <Navigation {...props} />
      <Grid>
        <Switch>
          <Route exact name="index" path="/" component={Index} />
          <Authenticated exact path="/documents" component={Documents} {...props} />
          <Authenticated exact path="/documents/new" component={NewDocument} {...props} />
          <Authenticated exact path="/documents/:_id" component={ViewDocument} {...props} />
          <Authenticated exact path="/documents/:_id/edit" component={EditDocument} {...props} />
          <Authenticated exact path="/profile" component={Profile} {...props} />
          <Public path="/signup" component={Signup} {...props} />
          <Public path="/login" component={Login} {...props} />
          <Public path="/logout" component={Logout} {...props} />
          <Route name="recover-password" path="/recover-password" component={RecoverPassword} />
          <Route name="reset-password" path="/reset-password/:token" component={ResetPassword} />
          <Route name="terms" path="/terms" component={Terms} />
          <Route name="privacy" path="/privacy" component={Privacy} />
          <Route component={NotFound} />
        </Switch>
      </Grid>
      <Footer />
    </div> : ''}
  </Router>
);

App.propTypes = {
  loading: PropTypes.bool.isRequired,
};

const getUserName = name => ({
  string: name,
  object: `${name.first} ${name.last}`,
}[typeof name]);

export default createContainer(() => {
  const loggingIn = Meteor.loggingIn();
  const user = Meteor.user();
  const userId = Meteor.userId();
  const loading = !Roles.subscription.ready();
  const name = user && user.profile && user.profile.name && getUserName(user.profile.name);
  const emailAddress = user && user.emails && user.emails[0].address;

  return {
    loading,
    loggingIn,
    authenticated: !loggingIn && !!userId,
    name: name || emailAddress,
    roles: !loading && Roles.getRolesForUser(userId),
  };
}, App);

Authenticated, Authorized, and Public Routes

You may have noticed that in the list of routes above, a few different components are in use for rendering routes: <Route />, <Authenticated />, and <Public />. The first, <Route />, is a component imported directly from the react-router-dom package. This simply creates a route in the application at the URL specified in the path prop and renders the component specified in the component prop.

The other two components, <Authenticated /> and <Public />, are Pup-specific components used to create special types of routes that render the specified component prop depending on the user's authentication (logged-in/logged-out) status.

Authenticated Routes

As the name implies, routes created using the <Authenticated /> component are only intended to be accessed by authenticated or logged-in users. To handle the authentication of a user, the component takes in two special props loggingIn and authenticated which are passed to the component via Pup's <App /> component.

The <App /> component is wrapped by a data container which passes down the value of Meteor.loggingIn() independently, and authenticated as a combination of checking if Meteor is logging in and if a userId exists !loggingIn && !!userId.

Initial page is tracked

Good news! If a user attempts to access an authenticated page while they're logged out, Pup will keep track of this and redirect the user back to that page after they've logged in.

Inside of the <Authenticated /> component, if the passed loggingIn prop is true, a blank <div></div> element is rendered on screen until loggingIn is false, meaning, we have a user that we can authenticate with. If we do have a user, we go ahead and render the passed component prop using the React.createElement() method.

If we do not have a user and we're not logging in, we consider this user to be "unauthenticated" and redirect them away from the page they're accessing. By default this redirect is to the /login route in the application but can be customized to any page you'd like.

Notice that what we're returning from our <Authenticated /> component is just the <Route /> component from the react-router-dom package. What's ultimately happening here is that the <Route /> component is returned and then rendered within our routes list, at which point React Router takes over.

Authorized Routes

As the name implies, routes created using the <Authorized /> component are only intended to be accessed by authorized or users in a specific role (like admin). To handle the authorization of a user, the component takes in a special prop allowedRoles which expects an array of roles to be passed (e.g., ['admin', 'manager']) specifying the roles the current user must have in order to access that route.

Inside of the <Authorized /> component, a function is defined checkIfAuthorized which attempts to verify whether or not the current user is allowed to access the route defined using the component. Due to Meteor's reactivity, the checkIfAuthorized method is called twice: once on componentDidMount(), meaning, when the <Authorized /> component is mounted and on componentDidUpdate(), whenever the props passed to the <Authorized /> component are updated (in this case, when the user's logged in state or roles change).

If the user's roles match those passed in the allowedRoles array, the page is rendered for the user as expected. If the user is not in one of the necessary roles, they're redirected to the path specified by the pathAfterFailure prop on the component. If this prop isn't provided, the user is redirected to the / or index page of the app.

Public Routes

The <Public /> component is designed as a wrapper component around pages that are intended for the public only. By wrapping a component in the <Public > component, the intent is to redirect an authenticated or logged-in user away from this page (e.g., a logged in user has no purpose accessing the /signup page). If we look at the code for the <Public /> component, it follows the exact same pattern as the <Authenticated /> component described above, however, running its authentication check in reverse.

/imports/ui/components/Authenticated/Authenticated.js
import React, { PropTypes } from 'react';
import { Route, Redirect } from 'react-router-dom';

const Authenticated = ({ loggingIn, authenticated, component, ...rest }) => (
  <Route
    {...rest}
    render={(props) => {
      if (loggingIn) return <div />;
      return authenticated ?
      (React.createElement(component, { ...props, loggingIn, authenticated })) :
      (<Redirect to="/logout" />);
    }}
  />
);

Authenticated.propTypes = {
  loggingIn: PropTypes.bool.isRequired,
  authenticated: PropTypes.bool.isRequired,
  component: PropTypes.func.isRequired,
};

export default Authenticated;
/imports/ui/components/Authorized/Authorized.js
import React from 'react';
import PropTypes from 'prop-types';
import autoBind from 'react-autobind';
import { Route } from 'react-router-dom';
import { withRouter } from 'react-router';
import { Roles } from 'meteor/alanning:roles';
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';

class Authorized extends React.Component {
  constructor(props) {
    super(props);
    this.state = { authorized: false };
    autoBind(this);
  }

  componentDidMount() {
    this.checkIfAuthorized();
  }

  componentDidUpdate() {
    this.checkIfAuthorized();
  }

  checkIfAuthorized() {
    const {
      loading, userId, userRoles, userIsInRoles, pathAfterFailure,
    } = this.props;

    if (!userId) this.props.history.push(pathAfterFailure || '/');

    if (!loading && userRoles.length > 0) {
      if (!userIsInRoles) {
        this.props.history.push(pathAfterFailure || '/');
      } else {
        // Check to see if authorized is still false before setting. This prevents an infinite loop
        // when this is used within componentDidUpdate.
        if (!this.state.authorized) this.setState({ authorized: true }); // eslint-disable-line
      }
    }
  }

  render() {
    const {
      component, path, exact, ...rest
    } = this.props;

    return (this.state.authorized ? (
      <Route
        path={path}
        exact={exact}
        render={props => (React.createElement(component, { ...rest, ...props }))}
      />
    ) : <div />);
  }
}

Authorized.defaultProps = {
  allowedGroup: null,
  userId: null,
  exact: false,
  userRoles: [],
  userIsInRoles: false,
  pathAfterFailure: '/login',
};

Authorized.propTypes = {
  loading: PropTypes.bool.isRequired,
  allowedRoles: PropTypes.array.isRequired,
  allowedGroup: PropTypes.string,
  userId: PropTypes.string,
  component: PropTypes.func.isRequired,
  path: PropTypes.string.isRequired,
  exact: PropTypes.bool,
  history: PropTypes.object.isRequired,
  userRoles: PropTypes.array,
  userIsInRoles: PropTypes.bool,
  pathAfterFailure: PropTypes.string,
};

export default withRouter(withTracker(({ allowedRoles, allowedGroup }) => { // eslint-disable-line
  return Meteor.isClient ? {
    loading: Meteor.isClient ? !Roles.subscription.ready() : true,
    userId: Meteor.userId(),
    userRoles: Roles.getRolesForUser(Meteor.userId()),
    userIsInRoles: Roles.userIsInRole(Meteor.userId(), allowedRoles, allowedGroup),
  } : {};
})(Authorized));
/imports/ui/components/Public/Public.js
import React, { PropTypes } from 'react';
import { Route, Redirect } from 'react-router-dom';

const Public = ({ loggingIn, authenticated, component, ...rest }) => (
  <Route
    {...rest}
    render={(props) => {
      if (loggingIn) return <div />;
      return !authenticated ?
      (React.createElement(component, { ...props, loggingIn, authenticated })) :
      (<Redirect to="/documents" />);
    }}
  />
);

Public.propTypes = {
  loggingIn: PropTypes.bool.isRequired,
  authenticated: PropTypes.bool.isRequired,
  component: PropTypes.func.isRequired,
};

export default Public;