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 theDocuments
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 theDocuments
collection.
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')));
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.
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;
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));
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;