Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Private pages in Next, even when you don't have a server #49

Open
1 of 6 tasks
eric-burel opened this issue Jun 24, 2020 · 7 comments
Open
1 of 6 tasks

Private pages in Next, even when you don't have a server #49

eric-burel opened this issue Jun 24, 2020 · 7 comments

Comments

@eric-burel
Copy link
Collaborator

eric-burel commented Jun 24, 2020

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's useQuery)
  • 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

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 for res.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 with currentUser, computed during this call. So the Apollo queries were not collected as expected during getDataFromTree, 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:

image

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.
@eric-burel eric-burel changed the title Private pages in Next, but for real Private pages in Next / SPA mode Jun 24, 2020
@eric-burel eric-burel changed the title Private pages in Next / SPA mode Private pages in Next, even when you don't have a server Jun 24, 2020
@jasonraimondi
Copy link

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!

@eric-burel
Copy link
Collaborator Author

eric-burel commented Jun 24, 2020

@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.

@eric-burel
Copy link
Collaborator Author

I'll make this a reusable page wrapper, I think that's doable to abstract everything into an HOC + a getInitialProps enhancer.

@jasonraimondi
Copy link

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

@eric-burel
Copy link
Collaborator Author

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.

@eric-burel
Copy link
Collaborator Author

See #71

@eric-burel
Copy link
Collaborator Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants