При использовании google-cloud / connect-datastore Next. js Приложение (SSR) не получает пользователя после первого входа - PullRequest
2 голосов
/ 31 марта 2020

Когда я добавляю @google-cloud/connect-datastore в качестве хранилища сеансов для Express.js Next.js Приложение (с SSR) не получает пользователя после первого входа в систему. Если я обновлю sh страницу после того, как этот пользователь войдет в ctx.req.session.passport.user (_app.js файл).

Как я могу это исправить?

Вот как я настроил Next.js Приложение (которое включает в себя Auth0 и Passport):

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

А вот мой server.js файл:

// 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}`);
	});
});

Также я использую _document.js и _app.js для 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));
...