When google-cloud/connect-datastore is used Next.js App (SSR) doesn't get a user after first login

Viewed 154

When I add @google-cloud/connect-datastore as my session storage for Express.js Next.js App (with SSR) doesn't get a user after first login. If I refresh page after that user comes in ctx.req.session.passport.user (_app.js file).

How can I fix this?

That's how I've set up Next.js App (which includes Auth0 and Passport):

https://auth0.com/blog/next-js-authentication-tutorial/

And here is my server.js file:

// Next line fixes this issue: https://stackoverflow.com/questions/36628420/nodejs-request-hpe-invalid-header-token
process.binding('http_parser').HTTPParser = require('http-parser-js').HTTPParser;
const http = require('http');
const express = require('express');
const path = require('path');
const next = require('next');
const session = require('express-session');
const bodyParser = require('body-parser');
// Gcloud
const gcloudDebugAgent = require('@google-cloud/debug-agent');
const { Datastore } = require('@google-cloud/datastore');
const DatastoreStore = require('@google-cloud/connect-datastore')(session);
// Auth0
const uid = require('uid-safe');
const passport = require('passport');
// i18n
const nextI18NextMiddleware = require('next-i18next/middleware').default;
const nextI18next = require('./src/i18n');
// Routes
const auth0Routes = require('./src/routes/auth0-routes');
const mainRoutes = require('./src/routes/main-routes');
const twoCheckoutRoutes = require('./src/routes/2checkout-routes');

console.log('process.env.NODE_ENV', process.env.NODE_ENV);
const isProd = process.env.NODE_ENV === 'production';
if (isProd) gcloudDebugAgent.start({ allowExpressions: true });

const app = next({
 dev: !isProd,
 dir: './src',
});

const handle = app.getRequestHandler();

const server = express();
const serverInstance = http.createServer(server);

app.prepare().then(async () => {
 const { error } = await initAPIs();
 if (error) {
  console.log('---> ---> Error: Init Utils', error);
  return;
 }

 // enable the use of request body parsing middleware
 server.use(bodyParser.json());
 server.use(bodyParser.urlencoded({
  extended: true
 }));

 // Need to use explicit define of static folder
 // because of [dir: './src',] change
 server.use('/static', express.static(path.join(__dirname, './static')));

 // translations
 await nextI18next.initPromise;
 server.use(nextI18NextMiddleware(nextI18next));

 // Express session management
 const sessionConfig = {
  store: new DatastoreStore({
   kind: 'express-sessions',
   expirationMs: 0,

   dataset: new Datastore({
    projectId: process.env.GCLOUD_PROJECT,
    keyFilename: path.join(__dirname, 'serviceaccount.json'),
   }),
  }),
  secret: uid.sync(18),
  cookie: {
   // 24 hours in milliseconds
   maxAge: 86400 * 1000,
  },
  resave: false,
  saveUninitialized: true,
 };
 server.use(session(sessionConfig));

 // Auth0 - Passport configuration`
        const auth0Strategy = new Auth0Strategy(
          {
            domain: process.env.AUTH0_DOMAIN,
            clientID: process.env.AUTH0_CLIENT_ID,
            clientSecret: process.env.AUTH0_CLIENT_SECRET,
            callbackURL: process.env.AUTH0_CALLBACK_URL,
          },
          async (accessToken, refreshToken, extraParams, profile, done) => {
            // Get and save in session standartly formatted user
            const userFormatted = await getUserById({ id: profile.id });
            return done(null, userFormatted);
          }
        );
 passport.use(auth0Strategy);
 passport.serializeUser((user, done) => done(null, user));
 passport.deserializeUser((user, done) => done(null, user));

 // Auth0 - adding Passport and authentication routes
 server.use(passport.initialize());
 server.use(passport.session());

 // Routes
 server.use(mainRoutes);
 server.use(auth0Routes);
 server.use(twoCheckoutRoutes);

 // handling everything else with Next.js
 server.get('*', handle);

 // app server
 serverInstance.listen(process.env.PORT, (err) => {
  console.log(err || `Listening on port ${process.env.PORT}`);
 });
});

Also I use _document.js and _app.js for Next.js.

_document.js

import React from 'react';
import { observer } from 'mobx-react';
import { ServerStyleSheets } from '@material-ui/styles';
import Document, {
 Head,
 Html,
 Main,
 NextScript,
} from 'next/document';
import theme from 'src/theme';

@observer
class MyDocument extends Document {
 render() {
  return (
   <Html lang="en">
    <Head>
     <meta charSet="utf-8" />
     {/* Use minimum-scale=1 to enable GPU rasterization */}
     <meta
      name="viewport"
      content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
     />
     {/* PWA primary color */}
     <meta name="theme-color" content={theme.get().palette.primary.main} />
     <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
     />
    </Head>
    <body>
     <Main />
     <NextScript />
    </body>
   </Html>
  );
 }
}

MyDocument.getInitialProps = async (ctx) => {
 // Resolution order
 //
 // On the server:
 // 1. app.getInitialProps
 // 2. page.getInitialProps
 // 3. document.getInitialProps
 // 4. app.render
 // 5. page.render
 // 6. document.render
 //
 // On the server with error:
 // 1. document.getInitialProps
 // 2. app.render
 // 3. page.render
 // 4. document.render
 //
 // On the client
 // 1. app.getInitialProps
 // 2. page.getInitialProps
 // 3. app.render
 // 4. page.render

 // Render app and page and get the context of the page with collected side effects.
 const sheets = new ServerStyleSheets();
 const originalRenderPage = ctx.renderPage;

 ctx.renderPage = () =>
  originalRenderPage({
   enhanceApp: App => props => sheets.collect(<App {...props} />),
  });

 const initialProps = await Document.getInitialProps(ctx);

 return {
  ...initialProps,
  // Styles fragment is rendered after the app and page rendering finish.
  styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
 };
};

export default MyDocument;

_app.js:

import React from 'react';
import { observer } from 'mobx-react';
import Head from 'next/head';
import App from 'next/app';
import Router, { withRouter } from 'next/router';
// Mobx
import { applySnapshot, getSnapshot } from 'mobx-state-tree';
// MaterialUI
import CssBaseline from '@material-ui/core/CssBaseline';
import { ThemeProvider } from '@material-ui/styles';
import { appWithTranslation } from 'src/i18n';
// Styles
import theme from 'src/theme';
// Store
import store from 'src/store';
// Components
import MainLayout from 'src/components/layout/MainLayout';
import Alerts from 'src/components/Alerts';

@observer
class MyApp extends App {
 /**
  * For the initial page load, getInitialProps will execute on the server only. 
  * getInitialProps will only be executed on the client when navigating
  * to a different route via the next/link component or by using next/router.
  */
 static async getInitialProps({ Component, ctx }) {
  const isServer = typeof window === 'undefined';
  // Set user
  const user = ctx.req && ctx.req.session.passport && ctx.req.session.passport.user;
  if (user) {
   await store.setUser({ user });
  } else if (isServer) {
   store.removeUser();
  }

  let pageProps = {};
  if (Component.getInitialProps) {
   pageProps = await Component.getInitialProps(ctx);
  }
  
  return {
   snapshot: getSnapshot(store),
   pageProps
  };
 }

 constructor(props) {
  super(props);
  const isServer = typeof window === 'undefined';
  // Server: does nothing, because the store has been already initialized
  // Client: apply store recived from server-side 
  if (!isServer) {
   applySnapshot(store, props.snapshot);
  }
 }

 componentDidMount() {
  // Remove the server-side injected CSS.
  const jssStyles = document.querySelector('#jss-чserver-side');
  if (jssStyles) {
   jssStyles.parentNode.removeChild(jssStyles);
  }
  store.orgs.initOrgFromURl();
 }

 componentDidUpdate() {
  if (!this.props.router.query.org && store.orgs.selectedOrg) {
   this.props.router.replace({
    pathname: this.props.router.pathname,
    query: { ...this.props.router.query, org: store.orgs.selectedOrg.name }
   });
  }
 }

 render() {
  const { Component, pageProps } = this.props;

  return (
   <>
    <Head>
     <title>Axonops-SAAS</title>
     <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
     <link rel="shortcut icon" href="/static/images/favicon.ico" />
    </Head>
    <ThemeProvider theme={theme.get()}>
     {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
     <CssBaseline />

     <MainLayout>
      <Component {...pageProps} />
     </MainLayout>

     <Alerts />
    </ThemeProvider>
   </>
  );
 }
}

export default withRouter(appWithTranslation(MyApp));

0 Answers
Related