Dynamic Organization Routing with React
If you look at a website like Twitter, each user has their own unique URL with their username in it, like:
https://twitter.com/USERNAME
In a B2B product, like Amplitude, you’ll see the same pattern:
https://analytics.amplitude.com/COMPANY_NAME/activity
Instead of an individual user’s username, the path includes the name of the company and every employee of that company has access to it. We’ll look at how to do this in React using react-router and PropelAuth.
Initial setup
To get started, we’ll create a React application:
$ npx create-react-app example-app
And we’ll install both react-router and PropelAuth’s React library
$ npm install react-router-dom@6
$ npm install @propelauth/react
Both libraries require a top level provider, so let’s add those to our index.js
import {RequiredAuthProvider} from "@propelauth/react";
import {BrowserRouter} from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RequiredAuthProvider authUrl={process.env.REACT_APP_AUTH_URL}>
<BrowserRouter>
<App/>
</BrowserRouter>
</RequiredAuthProvider>
</React.StrictMode>
);
BrowserRouter manages our user’s URL history and RequiredAuthProvider manages our user’s authentication information. We’re using RequiredAuthProvider instead of AuthProvider which means if the user isn’t logged in, they are redirected to our login page. This means we never need to worry about the case where a user is not logged in.
Creating our application’s routes
We’ll have two routes. One base route which shows the user all the organizations they are a member of and one route which will display information specific to one organization.
import {Routes, Route} from "react-router-dom";
import Home from "./Home";
import ViewOrg from "./ViewOrg";
function App() {
return <Routes>
<Route exact path="/" element={<Home/>}/>
<Route path="/org/:orgName" element={<ViewOrg/>}/>
</Routes>
}
export default App
:orgName is a path parameter, and we’ll see how we can access it’s value when we implement the ViewOrg component.
Next, we’ll build out our Home component:
import {useRedirectFunctions, withRequiredAuthInfo} from "@propelauth/react";
import {Link} from "react-router-dom";
function Home(props) {
const orgs = props.orgHelper.getOrgs()
if (orgs.length === 0) {
return <NoOrganizations />
} else {
return <ListOrganizations orgs={orgs}/>
}
}
// By default, if the user is not logged in they are redirected to the login page
export default withRequiredAuthInfo(Home);
orgHelper allows us to easily get all the organizations that the user is a member of.
If the user isn’t a member of any organizations, we can prompt them to create one:
function NoOrganizations() {
const {redirectToCreateOrgPage} = useRedirectFunctions()
return <div>
You aren't a member of any organizations.<br/>
You can either create one below, or ask for an invitation.<br/>
<button onClick={redirectToCreateOrgPage}>
Create an organization
</button>
</div>
}
Alternatively, we could always redirect them if we want to make sure all users have at least one organization.
If they are a member of some organizations, we can display them:
function ListOrganizations({orgs}) {
return <>
<h3>Your organizations</h3>
<ul>
{orgs.map(org => {
return <li key={org.orgId}>
<Link to={`/org/${org.urlSafeOrgName}`}>
{org.orgName}
</Link>
</li>
})}
</ul>
</>
}
With PropelAuth’s hosted pages, our users can create organizations and manage their membership already, so all we have to do is read and display the information in our React app.
Checking organization membership
The only remaining component is ViewOrg. For a first version, what if we wrote it like this:
import {useParams} from "react-router-dom";
function ViewOrg(props) {
const {orgName} = useParams();
return <div>Welcome to {orgName}</div>
}
export default ViewOrg
The good news is that if our users click on one of the links, we’ll see the right message:
The bad news is that if our users manually type in any organization name, they’ll get the same message, even if they aren’t a member of that organization:
We need to check to make sure the user is actually a member of that organization, or display an error. Luckily, this is pretty easy to do with our orgHelper from before:
import {useParams} from "react-router-dom";
import {withRequiredAuthInfo} from "@propelauth/react";
function ViewOrg(props) {
const {orgName} = useParams();
const org = props.orgHelper.getOrgByName(orgName)
if (org) {
return <div>Welcome to {org.orgName}</div>
} else {
return <div>Not found</div>
}
}
export default withRequiredAuthInfo(ViewOrg)
A note on checking client-side vs server-side
Any check that runs in the browser should not be considered secure. In the above case, for example, a user could modify the orgHelper to always return an org and make it seem like they are a member of whichever org they like.
We prefer to check things like organization membership on the server, because the user doesn’t have control of the server.
If we were using Express, as an example, we can verify the user is a member of the organization like this:
app.get("/org/:orgId/fetchSensitiveInformation",
requireOrgMember(), // PropelAuth's express middleware
// which verifies that the user is in orgId by default
(req, res) => {
res.json(fetchSensitiveInfoFor(req.org.orgId))
}
)
But if we’re verifying access on the server, why did we also check in our React application? It’s because it provides a better user experience. It’s much better to tell the user they don’t have access immediately instead of waiting for a fetch to fail.
Summary
If you are building a B2B/multi-tenant application, you’ll want to have some dynamic way to know which organization your users are accessing at any point in time. With PropelAuth and react-router, you can set this up in minutes and go back to building the rest of your product.