Когда я добавляю @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));