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.