Authenticated routes in SPA

I wanted to expose this question to more people and thought of this discord server. Original post in GH: https://github.com/solidjs/solid-router/discussions/364 The TL;DR; is that there doesn't seem to be a canonical approach to handling private vs public routes such that repetition is avoided -- I.E some type of middleware or guard. I posted what I'm currently playing around with and will add it here for feedback, though in its current state it isn't working as expected. When using nested routes in the config-based approach this doesn't work.
<Router root={App}>
<For each={routes}>
{(route) => {
// This approach assumes private by default
if (route.info?.public) {
return <Route component={route.component} />;
}

return (
<Route
component={(props) => (
// Where `AuthGuard` contains redirect/navigate logic
<AuthGuard>{route.component?.(props)}</AuthGuard>
)}
/>
);
}}
</For>
</Router>
<Router root={App}>
<For each={routes}>
{(route) => {
// This approach assumes private by default
if (route.info?.public) {
return <Route component={route.component} />;
}

return (
<Route
component={(props) => (
// Where `AuthGuard` contains redirect/navigate logic
<AuthGuard>{route.component?.(props)}</AuthGuard>
)}
/>
);
}}
</For>
</Router>
GitHub
Is there a recommended way to implement Authenticated Routes? · sol...
Assume you have a SPA that includes several public Routes and several private Routes that require an authenticated user. Ideally, I'd like to wrap all of those protected Routes under a single c...
4 Replies
TaQuanMinhLong
TaQuanMinhLong5mo ago
Place your guard from the server side instead, on the client side, you just need to care whether the user is in the right view
flippyflops
flippyflopsOP5mo ago
There are guards on the api for resource requests via Authenticatiion header / bearer token. I'm using a SPA, so the server doesn't receive a request for a new page on navigate, so checking for auth needs to happen at the FE router level and this can be a request to the auth service (or internal API if that is also the auth service) but it can't only happen on the server. I already have a way to refresh a token or redirect to login upon an Unauthenticated request for a resource. I posted another solution in that GH link that I'll likely end up going with. TL;DR; Config-based routes seem like an after thought in @solidjs/router, perhaps by design? 🤷🏼‍♂️ Using the JSX-based approach is significantly easier to handle a groupings of "private" routes. See my comment.
Madaxen86
Madaxen864mo ago
This is a setup that I use. AuthGuard is a layout component which does not affect the url. In getIsAuthenticated will refresh the session / token and returns a boolean respectively, hence the Switch + Match setup and also the refresh interval for the token refresh. Works like charm for me.
export default function AuthGuard(props: ParentProps) {
// will return true if authenticated, false if not, undefined when not fetched yet
const isAuthenticated = createAsync(() => getIsAuthenticated(), {
deferStream: true,
});
const location = useLocation();
onMount(() => {
function refreshWhenVisible() {
if (!document.hidden && isAuthenticated()) {
revalidate(getIsAuthenticated.key);
}
}
//revalidate after focus comes back to app's browser tab
document.addEventListener('visibilitychange', refreshWhenVisible);

//refresh the token before it expires
const interval = setInterval(refreshWhenVisible, 1000 * 60 * 15);

onCleanup(() => {
document.addEventListener('visibilitychange', refreshWhenVisible);
clearInterval(interval);
});
});

return (
<Suspense>
<Switch fallback={'checking auth...'}>
<Match when={isAuthenticated()}>{props.children}</Match>
<Match when={isAuthenticated() === false}>
{/* You could also render your Login component instead of redirecting */}
<Navigate href={`/login?redirectTo=${location.pathname}`} />
</Match>
</Switch>
</Suspense>
);
}
export default function AuthGuard(props: ParentProps) {
// will return true if authenticated, false if not, undefined when not fetched yet
const isAuthenticated = createAsync(() => getIsAuthenticated(), {
deferStream: true,
});
const location = useLocation();
onMount(() => {
function refreshWhenVisible() {
if (!document.hidden && isAuthenticated()) {
revalidate(getIsAuthenticated.key);
}
}
//revalidate after focus comes back to app's browser tab
document.addEventListener('visibilitychange', refreshWhenVisible);

//refresh the token before it expires
const interval = setInterval(refreshWhenVisible, 1000 * 60 * 15);

onCleanup(() => {
document.addEventListener('visibilitychange', refreshWhenVisible);
clearInterval(interval);
});
});

return (
<Suspense>
<Switch fallback={'checking auth...'}>
<Match when={isAuthenticated()}>{props.children}</Match>
<Match when={isAuthenticated() === false}>
{/* You could also render your Login component instead of redirecting */}
<Navigate href={`/login?redirectTo=${location.pathname}`} />
</Match>
</Switch>
</Suspense>
);
}
Stackblitz: https://stackblitz.com/edit/github-p2tzuc?file=src%2Froutes%2F(authLayout).tsx
StackBlitz
Solid-start Basic Example - StackBlitz
Run official live example code for Solid-start Basic, created by Solidjs on StackBlitz
Brendonovich
Brendonovich4mo ago
This approach is good, but there's nothing unique to JSX routes with what you're doing Config routes would also work and in fact using JSX routes is just a fancy way of building a config object
Want results from more Discord servers?
Add your server