Private pages in Next, even when you don't have a server
Opened this issue ยท 7 comments
Edit: current progress:
- An HOC for pages is available here: withPrivateAccess. You can customize the server side auth check and the client side auth check. It works in all scenarios, from SSR to client-only, and is compatible with
next export
๐. - I lack a pattern to handle hook based auth (eg
useCurrentUser
like we do in Vulcan, based on Apollo'suseQuery
) - I find the client part clumsy/inelegant, but maybe there is no alternatives?
- I lack a way to handle purely synchronous check client-side, like just reading
localStorage
- Improve based on Vulcan's AccessControl
- Check newer examples and technologies: Next-auth, and Next examples related to auth
Goal
Easy, intuitive, way of creating private pages visible only by logged in users.
Should work with and without SSR.
Should also work in client-only context, like any other "JAMstack"-friendly framework.
Previous art
-
Question "How to redirect in getStaticProps" in Next repo. This is the most insightful ressource on the question, as it raises the broader question of redirect in a static page. The page being private is a special case of this, where you not only want the user to be redirected if not logged in, but you also want the static version of the page to never exist in the first place.
-
Example that works in dynamic mode, but does not talk at all about static render
-
My unanswered question in spectrum regarding redirection Dead link...
-
Hosting on github pages => leads to a dead wiki link
-
What about Gatsby? The key difference between Gatsby and Next is the Next's ability to do dynamic server side rendering, which is necessary to create real "private" pages with SSR. In Gatsby, private pages are client-side only. That defeats the purpose of dynamic SSR.
What we need
- During dynamic server render, use an HTTP redirection.
- During client side rendering, use the Router
- During static rendering, disable the route altogether. Private pages should become "client-only" pages, like in a normal SPA. Spoiler alert: this is the use case that bothers me the most here
Private pages only make sense either client-side, or during dynamic server-render, because they depend on whether the user is logged in or not.
Static render happens on your dev machine, once, like the application build itself, so it does not know the end user. Private page of a statically rendered Next app should be rendered client-side only.
First attempt, with getInitialProps
in custom _app
Intuitively, if you are used to react-router or alike, you'll want to setup a global redirection at the top level, so basically adding auth checking and redirection in _app
getInitialProps
TL;DR: this is wrong. This is intuitively what you'll do when used to React router or alike. You'll define an array of public routes, and let the user pass when the route match. That's wrong. You'll redirect to the login page if the user is trying to open a private route while not being connected. Wrong wrong wrong.
In Next, the router is the page. Everything must happen at the page level. Defining the layout, defining redirections, checking auth, etc., etc.
I've never met yet a page with only private pages, if only the login page.
My takeaways:
- getInitialProps is called client-side too when you click a link. Beware of this scenario, in this case, you can call
Router.push
to redirect. appContext.ctx.res
is available both during STATIC server-render and DYNAMIC server-render.You need to check forres.writeHead
to be available if you want to redirect during server render.- Do not change
_app
render based on props status computed during getInitialProps. I've had a weird behaviour breaking apollo SSR of private pages. Basically the app props were not directly enhanced withcurrentUser
, computed during this call. So the Apollo queries were not collected as expected duringgetDataFromTree
, resulting in an app in loading state.
Code looks like this and you should not do that because this is wrong:
// Various helpers
export const redirectServer = (ctx: NextPageContext) => (pathname: string) => {
ctx.res.writeHead(302, { Location: pathname });
ctx.res.end();
};
const isServerSideRenderCtx = (ctx?: NextPageContext) => {
return !!(ctx && ctx.res && ctx.res.writeHead);
};
const isStaticRenderCtx = (ctx?: NextPageContext) => {
return !!(ctx && ctx.res && !ctx.res.writeHead);
};
const isClientRender = () => {
return typeof window !== undefined;
};
// The page
...
// getInitialProps
PrivatePage.getInitialProps = async (ctx?: NextPageContext) => {
// We simulate private connexion
const isAllowed = !!ctx.query.allowed; // just for the demo, replace with a check that the user is logged in
const pageProps = { isAllowed };
if (isAllowed) {
return { ...pageProps, isAllowed };
}
if (isStaticRenderCtx(ctx)) {
debugNext("Detected static render, not doing anything");
// TODO: this is bad...
} else if (isServerSideRenderCtx(ctx)) {
// Scenario 2: we are in a server-side render
debugNext("Detected dynamic server-side rendering");
redirectServer(ctx)("/vns/debug/public"); // redirect server
} else if (isClientRender()) {
// Scenario 3: we are client-side
debugNext("Detected client render");
debugNext("Redirecting (client-side)");
Router.push("/vns/debug/public");
}
return pageProps;
};
Second attempt, with getServerSideProps and getStaticProps, in the page
- the function is not called client side, but is reruns server side
// So beautiful
export const getServerSideProps = async (ctx: NextPageContext) => {
const isAllowed = !!ctx.query.allowed; // demo
if (!isAllowed) {
debugNext("Redirecting (dynamic server render)", ctx.req.url);
redirectServer(ctx)("/vns/debug/public");
}
return { props: { isAllowed } };
};
Issues I struggle to solve
-
I can't tell from the doc and all articles and answer what's the best course of actions to create a private page in Next.
-
As far as I understand,
next export
is the only way to have an application that do not rely on the server. Something I could host on GitHub pages.
From the doc:
Next.js automatically determines that a page is static (can be prerendered) if it has no blocking data requirements. This determination is made by the absence of getServerSideProps and getInitialProps in the page.
Agree with that. However, it's not because a page cannot be prerendered, that you don't want the whole app not to be statically exported.
next build
WON'T create such an app, as far as I understand. Or at least, I found no doc about this.
If you define a page with getServerSideProps
, which you need to in order to have server-side redirect, Next considers that you opt out from static export. This feels wrong, I could still want to have a simple client-side redirect in scenarios where
- More broadly, it seems that Next that your app could both be deployed as a static app, and using SSR. However I often encounter the scenario where the app as a canonical SaaS version, but could also be deployed on-premise. In this case, having a client-only version is nice, even when it includes private pages.
I see you linked my post about private routes in nextjs (with no static mode examples). The static mode examples should be basically the same thing as a traditional ReactJS (all client side) application. If there is anything I can do to help out, or if you want to bounce any ideas around, Iโm here!
@jasonraimondi I think I've figured it out. If you have some spare time, checking this example component and this comment would be awesome. I think it works and covers all uses cases, but I'd love a double check.
It's not really like a normal app, because you have to hack Next a bit. First you can't use getServerSideProps
because currently next export
is too agressive, it fails when it's defined instead of just ignoring it. Then, you can't "disable" the page altogether, you have to render smth. So I've added a check to tell if we are during a static export, and in this case I render nothing.
Edit: ok actually when you open the page in static mode, it still gives you a glimpse of the private page instead of rendering nothing. But we progress.
Edit 2: almost there. For some reason router.query is an empty object when opening vns/debug/private?allowed=true
from the browser directly
Edit 3: I think I got all cases right... client side, client side after ssr, client side after static export, static render, server render.
I'll make this a reusable page wrapper, I think that's doable to abstract everything into an HOC + a getInitialProps enhancer.
From what I can see, it seems like you do have all the cases covered:
- client side
- client side after ssr
- client side after static export
- static render
- server render.
It seems that adding the additional use cases in addition to my "server only" approach does add a lot of complexity. It would definitely be worth abstracting yours into a HOC and just wrapping any private page in that component.
This file would just need a bit of refactoring to be that HOC.
This is my more simple example of the HOC that lacks support for the use cases of client side
, client side after static export
, and static render
I've made it an HOC this morning, see withPrivateAccess here.
It still works for all scenarios.
I think the client-side render is still kinda clumsy and inelegant, I have hard time figuring out right pattern.
You might want to support:
- a synchronous check, but not run it during SSR. Maybe
useLayoutEffect
could be used here, to avoid a flash of blank screen?
Example usage: checking if the user is logged in using only localStorage. - an asynchronous check.
Example usage: checking if the user is logged in using an HTTP query. Seems awfully heavy, is that even a real use case - Apollo
useQuery
with a getCurrentUser query. So basically, it will return very fast if the data is in the cache, with no HTTP call, and otherwise it will run an HTTP call.
However I am not sure how I could use hooks with this HOC. In Apollo, the alternative is to get and call the client-side apollo client manually, without a hook, in the isAllowedClient
method.
See #71
To sum it up, SSR, and thus dynamic server side redirections for private pages, leads to a lot of complexity.
Instead, we should consider pages as always public, and find another pattern to secure them dynamically (eg a reverse proxy or an upfront server). getServerSideProps
/getInitialProps
is the wrong place to do this, if you don't have at least another good reason to already use those.