I have a few issues.
I'm currently coding a CMS using React for client side with Webpack and Express for server side with Babel.
What I am trying to do is setup a common X-CSRF-Token header on the axios module on my client side that gets a token generated from a shared file that the server side uses to verify the token.
The problem is that, when I use two different compilers, the static class inheritance doesn't work, so instead of verifying the existing generated token inside the class, it instead generates a whole new.
Would it work if I somehow managed to use the same Webpack config for both server and client side?
When I start up my server, I use this npm script:
nodemon --watch server --exec babel-node -- server/index.js
For the client side, I use Webpack HMR with this config:
import path from 'path'
import webpack from 'webpack'
const clientPath = path.join(__dirname, 'client')
//const bundlePath = path.join(__dirname, 'static', 'js')
export default {
devtool: 'eval-source-map',
entry: [
'webpack-hot-middleware/client',
clientPath
],
output: {
filename: 'bundle.js',
path: '/',
publicPath: '/'
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin()
],
module: {
loaders: [{
test: /\.js$/,
include: clientPath,
loaders: ['react-hot-loader', 'babel-loader']
}]
},
resolve: {
extensions: ['.js']
}
}
server/index.js
So in my server index file, I send the index.html file that has the bundle.js required.
import express, {app} from './app'
import config from '../config.json'
import path from 'path'
import './database'
const staticPath = path.join(__dirname, '..', 'static')
app.use((err, req, res, next) => {
if (err) {
res.status(err.statusCode || err.status || 500)
.send(err.data || err.message || {})
} else {
next()
}
})
app.use('/api', require('./api'))
app.use(express.static(staticPath))
app.get('*', (req, res) =>
res.sendFile(path.join(staticPath, 'index.html'))
)
app.listen(config.port, () =>
console.log(`Listening on port: ${config.port}`)
)
server/app.js
And in my server app file I initialize the Webpack and other middleware:
import webpackHotMiddleware from 'webpack-hot-middleware'
import webpackMiddleware from 'webpack-dev-middleware'
import expressSession from 'express-session'
import bodyParser from 'body-parser'
import webpack from 'webpack'
import express from 'express'
import cors from 'cors'
import path from 'path'
import webpackConfig from '../webpack.config'
const compiler = webpack(webpackConfig)
export const router = express.Router()
export const app = express()
.use(cors())
.use(bodyParser.urlencoded({ extended: false }))
.use(bodyParser.json())
.use(expressSession({
secret: 'test',
resave: false,
saveUninitialized: true,
cookie: { secure: true }
}))
.use(webpackMiddleware(compiler, {
hot: true,
publicPath: webpackConfig.output.publicPath
}))
.use(webpackHotMiddleware(compiler))
export default express
utils/csrf.js
This is the CSRF file that's shared between both the client and server side.
import CryptoJS from 'crypto-js'
import randomString from 'crypto-random-string'
import config from '../config.json'
export default class CSRF {
static secret: String
static csrf: String
static tokenCounter: Number
static generate(counter: Number = 10): String {
if (!this.csrf || this.tokenCounter >= (config.csrf.counter || counter)) {
// after 10 or defined times the token has been used, regenerate it
this.secret = config.csrf.secret !== '' ? config.csrf.secret : randomString(8)
let token = randomString(12)
this.csrf = CryptoJS.AES.encrypt(token, this.secret).toString()
this.tokenCounter = 0
}
return this.csrf
}
static verify(req: Object): Boolean {
const csrf = req.headers['x-csrf-token']
if (!csrf) return false
this.tokenCounter++
//const bytes = CryptoJS.AES.decrypt(csrf, this.secret)
//const decrypted = bytes.toString(CryptoJS.enc.Utf8)
return csrf === this.csrf
}
}
So in my client/index.js file, I setup the axios header like so:
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import axios from 'axios'
import Routes from './routes'
import store from './store'
import config from '../config.json'
import CSRF from '../utils/csrf'
if (config.production) process.NODE_ENV = "production"
axios.defaults.headers.common['X-CSRF-Token'] = CSRF.generate()
ReactDOM.render(
<Provider store={store}>
<Routes />
</Provider>,
document.querySelector('#root')
)
Then from a Redux action I send a post request:
import axios from 'axios'
export function userSignUpRequest(data) {
return (dispatch) => {
return new Promise((resolve, reject) => {
axios.post('/api/users/signup', data)
.then(res => res.data)
.then(data => {
if (data.status) {
resolve(data.user)
} else {
reject(data.errors)
}
})
})
}
}
And in server/api/users/signup I verify the CSRF token:
import Users from '../../database/models/users'
import Validate from '../../../utils/validate'
import CSRF from '../../../utils/csrf'
export default async function (req, res) {
if (!CSRF.verify(req)) {
return res.status(405).json({ status: false, reason: 'Invalid CSRF token' })
}
const {validationErrors, isValid} = Validate.registerInput(req.body)
if (!isValid) {
res.json({ status: false, errors: validationErrors })
} else {
const {email, username, password} = req.body
let errors = {}
try {
await Users.query().where('mail', email)
} catch(err) {
errors.email = 'This email is already taken'
}
try {
await Users.query().where('username', username)
} catch(err) {
errors.username = 'This username is already taken'
}
if (Validate.isObjEmpty(errors)) {
Users.query().insert({
mail: email, username, password
})
.then(user => res.status(200).json({ status: true, user }))
.catch(errors => res.json({ status: false, errors }))
} else {
res.json({ status: false, errors })
}
}
}
Whole source code: Github
Related
I have been struggling on this problem for days. I have a NextJS frontend running on localhost:3000 and an ExpressJS backend running on localhost:3001.
I am trying to build a login flow where I send an axios login request to the backend, authenticate with passport, and send the cookie back to the frontend (that can subsequently be used).
When I run through the flow, I can successfully send data to the backend and authenticate with passport (which writes a row to my session table), and redirect on the frontend. However, I do not see the cookie in my frontend browser (Inspect Element > Application > Cookies > localhost:3000). And when I am redirected to my dashboard page, I show as unauthorized from my status endpoint (which I believe means the cookie is not being set correctly). When I hit the backend endpoint with Postman, I can see the cookie is successfully sent and the subsequent /status endpoint call returns as authorized.
Can anyone help me understand why my cookies aren't being set correctly?
Backend - Express Setup:
const app = express ()
// Enable parsing middleware for requests
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
// Enable CORS
const originURL = process.env.RAILWAY_STATIC_FRONTEND_URL ? process.env.RAILWAY_STATIC_FRONTEND_URL : process.env.LOCAL_STATIC_FRONTEND_URL || 'http://localhost:3000'
app.use(cors({
origin: [originURL],
credentials: true
}))
// Session store
const pgSession = require('connect-pg-simple')(session);
const postgreStore = new pgSession({
// check interface PGStoreOptions for more info https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/connect-pg-simple/index.d.ts
// pool: poolInstance,
createTableIfMissing: true, // this will create a `session` table if you do not have it yet
})
// 1000ms * 60seconds * 60min * 24hrs * 7days = 1 week
const maxAge = 1000 * 60 * 60 * 24 * 7
app.use(session({
secret: process.env.EXPRESS_SESSION_SECRET || 'secret',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: maxAge,
sameSite: "none",
secure: false,
httpOnly: false
},
store: postgreStore,
}))
// Enable Passport
app.use(passport.initialize())
app.use(passport.session())
// Prefix all backend routes with '/api'
app.use('/api', routes)
Backend - Login + Status Routes:
import passport from 'passport';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '#prisma/client';
import { Router } from "express";
const router = Router ();
const prisma = new PrismaClient();
router.post('/login', passport.authenticate('local'), (req, res) => {
return res.sendStatus(200)
})
router.get('/status', (req, res) => {
return req.user
? res.send(req.user)
: res.status(401).send({ msg: "Unauthorized" })
})
export default router
Frontend: Login API Call
import type { NextApiRequest, NextApiResponse } from 'next'
import axios from "axios"
type ResponseData = {
message: string
}
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const BACKEND_API_URL = process.env.RAILWAY_STATIC_BACKEND_URL ? process.env.RAILWAY_STATIC_BACKEND_URL : process.env.NEXT_PUBLIC_LOCAL_STATIC_BACKEND_URL
const headers = {
}
const inputs = {
username: req.body.username,
password: req.body.password
}
if (!headers) return res.redirect(302, '/')
const config = {
headers: headers,
withCredentials: true
}
try {
// const { data } = await axios.post(`${BACKEND_API_URL}/api/auth/login`, inputs, { withCredentials: true })
await axios.post(`${BACKEND_API_URL}/api/auth/login`, inputs, { withCredentials: true })
return res.redirect(307, '/dashboard')
} catch (err) {
console.log(err)
return res.redirect(302, '/login')
}
}
Frontend - Dashboard Page
import { GetServerSidePropsContext, NextPage } from 'next'
import axios from 'axios'
type PageProps = {
msg: String,
}
const DashboardPage: NextPage<PageProps> = ({ msg }) => {
console.log(msg)
return (
// <div className={styles.container}>
<div>
<div> Dashboard Page </div>
<div> { msg } </div>
</div>
)
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const BACKEND_API_URL = process.env.RAILWAY_STATIC_BACKEND_URL ? process.env.RAILWAY_STATIC_BACKEND_URL : process.env.NEXT_PUBLIC_LOCAL_STATIC_BACKEND_URL
const headers = {
}
const config = {
headers: headers,
withCredentials: true
}
let msg
try {
const user = await axios.get(`${BACKEND_API_URL}/api/auth/status`, { withCredentials: true })
console.log(user)
msg = user
} catch (err) {
// console.log(err)
console.log(err.response.data)
msg = err.response.data.msg
}
var response = {
props: {
msg
}
}
return response
}
export default DashboardPage
It is basically because the client side is not storing the cookie. It is an issue from backend. There will be different settings while running the project in local and in cloud.
Try tweaking like this in your session settings and check in local environment. These worked for my local environment.
{ httpOnly: true, sameSite: 'None', secure: true, }
you have to tweak this in trial and error method for cloud hosting in railway.
I am trying to build google user authentication using passport strategy and express session. To authorize a user, I am trying to pass his data using the context. Unfortunately, when I want to use a context in resolver, req.session.passport and req.user disappear for unknown reasons. Did I do something wrong?
Apollo Server v4.
server.ts
import { ApolloServerPluginDrainHttpServer } from "#apollo/server/plugin/drainHttpServer";
import { expressMiddleware } from "#apollo/server/express4";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { ApolloServer } from "#apollo/server";
import express from "express";
import http from "http";
import cors from "cors";
import "dotenv/config";
import mongoose from "mongoose";
import { schema, setHttpPlugin } from "./serverSettings/config";
import { json } from "body-parser";
import cookieParser from "cookie-parser";
import passport from "passport";
import authRoutes from "./routes/auth";
import "./services/passport";
import { expressSession } from "./services/session";
//config variables
const port = process.env.PORT;
const host = process.env.HOST;
const dbUri = process.env.DB_URI;
//connect to DB
mongoose
.connect(dbUri)
.then(() => {
console.log("DB connected!");
startApolloServer();
})
.catch((error) => {
console.error(error);
process.exit(1);
});
const startApolloServer = async () => {
const app = express();
const httpServer = http.createServer(app);
const wsServer = new WebSocketServer({
server: httpServer,
path: "/graphql",
});
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
setHttpPlugin,
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
],
});
await server.start();
app.use(expressSession);
app.use(passport.initialize());
app.use(passport.session());
const corsOptions = {
origin: "https://studio.apollographql.com",
credentials: true,
};
app.use(
"/graphql",
// isLoggedIn,
cors<cors.CorsRequest>(corsOptions),
json(),
cookieParser(),
expressMiddleware(server, {
context: async ({ req }: any) => {
// console.log("session --->", req.session);
// console.log("user --->", req.user);
return { req };
},
})
);
//Google Auth
app.get("/", (req, res) => {
res.send('Auth with Google');
});
app.use("/auth", authRoutes);
//check if user is auth
// function isLoggedIn(req: Request, res: Response, next: NextFunction) {
// req.user ? next() : res.sendStatus(401);
// }
await new Promise<void>((resolve) => httpServer.listen({ port }, resolve));
console.log(`🚀 Server ready at ${host}:${port}/graphql`);
};
passport.ts
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import passport from "passport";
import "dotenv/config";
import Settings from "../models/Settings";
const googleClientId = process.env.GOOGLE_CLIENT_ID;
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
const callbackURL = process.env.GOOGLE_OAUTH_REDIRECT_URL;
passport.serializeUser(function (profile: any, done) {
done(null, profile.id);
});
passport.deserializeUser(function (id: string, done) {
done(null, id);
});
passport.use(
new GoogleStrategy(
{
clientID: googleClientId,
clientSecret: googleClientSecret,
callbackURL,
passReqToCallback: true,
},
async (request, accessToken, refreshToken, profile, done) => {
await Settings.collection.drop();
await new Settings({ refreshToken }).save();
done(null, profile);
}
)
);
session.ts
import session from "express-session";
import MongoStore from "connect-mongo";
import "dotenv/config";
const dbUri = process.env.DB_URI;
const sessionSecret = process.env.SESSION_SECRET;
export const expressSession = session({
name: "mySession",
secret: sessionSecret,
store: MongoStore.create({
mongoUrl: dbUri,
}),
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24,
},
});
I will be grateful for your help
For future generations - the problem was with a settings for sending credentials in Apollo's v4 playground. By default, this option is set to "omit" and interestingly it cannot be changed.
The solution is to install the package and import in your server's config file:
import expressPlayground from "graphql-playground-middleware-express";
Put that somewhere at the bottom of the code:
app.get("/playground", expressPlayground({ endpoint: "/graphql" }));
Hit the /playground route and find settings button. Change credentials option:
"request.credentials": "include",
I'm trying to get a test React frontend and Node/Express backend to correctly set session cookies at Heroku.
This frontend/backend work locally.
This frontend/backend work at Heroku in every browser on my development machine.
But on every other machine I have tested (Windows, Ubuntu), I get this Cross-Origin-Request-Blocked error:
Here is the backend code, where I configure CORS origin correctly:
import express from 'express'
import cors from 'cors'
import morgan from 'morgan'
import session from 'express-session';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';
dotenv.config();
const app = express();
app.use(morgan("dev"));
app.set('trust proxy', 1);
app.use(cors({
origin: process.env.FRONTEND_ORIGIN,
credentials: true
}));
app.use(cookieParser());
app.use(session({
name: 'testsession',
secret: 'h$lYS$cr§t!',
resave: true,
saveUninitialized: true,
cookie: {
httpOnly: true,
maxAge: 60 * 60 * 24,
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
secure: process.env.NODE_ENV === "production"
}
}))
app.get('/', (req, res) => {
let user = req.session.user;
if (!user) {
res.json({ message: `${(new Date()).toISOString()}: nobody is logged in` })
} else {
res.json({ message: `${(new Date()).toISOString()}: ${user} is logged in` })
}
});
app.get('/login', (req, res) => {
req.session.user = "user001"
res.json({
message: `${(new Date()).toISOString()}: ${req.session.user} is now logged in`
})
})
app.get('/logout', (req, res) => {
req.session.destroy();
res.json({ message: `${(new Date()).toISOString()}: user logged out` })
});
const PORT = process.env.PORT || 3011
app.listen(PORT, () => {
console.log(`API listening on http://localhost:${PORT}`);
});
Here is my frontend code:
import { useState } from 'react';
import './App.scss';
function App() {
const [message, setMessage] = useState('click a button');
const backendUrl = process.env.REACT_APP_BACKEND_URL;
const handle_checkuser = async () => {
const requestOptions = {
method: 'GET',
credentials: 'include'
};
const response = await fetch(backendUrl, requestOptions);
const data = await response.json();
setMessage(data.message);
}
const handle_login = async () => {
const requestOptions = {
method: 'GET',
credentials: 'include'
};
const response = await fetch(`${backendUrl}/login`, requestOptions);
const data = await response.json();
setMessage(data.message);
}
const handle_logout = async () => {
const requestOptions = {
method: 'GET',
credentials: 'include'
};
const response = await fetch(`${backendUrl}/logout`, requestOptions);
const data = await response.json();
setMessage(data.message);
}
return (
<div className="App">
<div><button onClick={handle_checkuser}>checkuser</button></div>
<div><button onClick={handle_login}>login</button></div>
<div><button onClick={handle_logout}>logout</button></div>
<div>{message}</div>
</div>
);
}
export default App;
Why would it be getting this CORS error with some machines and not others?
Backend:
https://github.com/edwardtanguay/et-cookietest-backend
https://et-cookietest-backend.herokuapp.com
Frontend:
https://github.com/edwardtanguay/et-cookietest-frontend
https://et-cookietest-frontend.herokuapp.com
ADDENDUM
I also noticed that on every other machine except for my development machine, the HTTP connection is not secure. This seems to be the cause of the cookie-setting problem.
But how can that be? Why would one particular computer receive HTTPS connections from a website and others HTTP?
I am currently using Okta with OIDC and Node Express to log into a web app that I made. I want my users to be able to logout of just my app and not okta itself as the okta is for my company and that would log them out of all the other websites that use the company okta. Okta recommends doing this on their website.
app.get('/local-logout', (req, res) => {
req.logout();
res.redirect('/');
});
(https://developer.okta.com/docs/guides/sign-users-out/nodeexpress/sign-out-of-your-app/)
I have tried implementing this but it doesn't work. I have also tried using req.session.destroy, res.clearCookie("connect.sid"), req.session = null and many different combinations of these with or without callbacks. Whenever I redirect, it just goes back to the homepage and the same user is logged in. Whenever I try deleting the connect.sid cookie it deletes and then is reinitialized when the user is redirected. I'm not really sure what to do. My code with irrelevant endpoints removed is below.
require('dotenv').config();
import 'zone.js/dist/zone-node';
import { ngExpressEngine } from '#nguniversal/express-engine';
import { join } from 'path';
import * as express from 'express';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '#angular/common';
import { existsSync, read } from 'fs';
// 30 minutes
const sessionMaxAge = 1800000;
import * as e from 'cors';
import { resolveForwardRef } from '#angular/core';
import { User } from '#okta/okta-sdk-nodejs';
const session = require('express-session');
const { ExpressOIDC } = require('#okta/oidc-middleware');
const cookieParser = require("cookie-parser");
/** Creates the OpenID Connect Middleware and configures it to work with Okta */
const oidc = new ExpressOIDC({
appBaseUrl: process.env.HOST_URL,
issuer: `${process.env.OKTA_ORG_URL}/oauth2/default`,
client_id: process.env.OKTA_CLIENT_ID,
client_secret: process.env.OKTA_CLIENT_SECRET,
redirect_uri: process.env.REDIRECT_URL,
scope: 'openid profile',
routes: {
loginCallback: {
path: '/authorization-code/callback'
},
}
});
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
var root_folder: string = process.env.ROOT_FOLDER || "dist/angular-app/browser"
const server = express();
const distFolder = join(process.cwd(), root_folder);
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found # https://urldefense.proofpoint.com/v2/url?u=https-3A__github.com_angular_universal_tree_master_modules_express-2Dengine&d=DwIGAg&c=-35OiAkTchMrZOngvJPOeA&r=03NdPO1x-l0QAZ_R9TNGwA&m=WF5ia-YADjCituVWMV5vLoZ5wg7d_W1qhCYDTbJNGT0&s=WPOkeRsetPDQ6TrD26RKLo1m9_zxBfQXGhUUSkth0Ew&e= )
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// Configure Session Support
server.use( session({
secret: process.env.APP_SECRET,
resave: false,
saveUninitialized: false,
cookie: {maxAge: sessionMaxAge}
})
);
server.use(cookieParser());
server.use(oidc.router);
// Log the user out of the local session
server.get('/local-logout',(req:any, res:any) => {
req.logout();
res.redirect('/');
});
// All regular routes use the Universal engine
server.get('*', oidc.ensureAuthenticated({ redirectTo: '/login' }), (req: any, res: any) => {
if(req.session && req.session.username != null) {
console.log(process.env);
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
}
else {
console.log("getting user info");
getUserInfo(req)
.then(userInfo => {
req.session.username = userInfo.username;
req.session.group = userInfo.group;
if (userInfo.group=="unauthorized") {
res.sendFile('./403.html', {root: "."})
}
else{
console.log(process.env);
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
}
});
}
});
return server;
}
function run(): void {
const port = process.env.PORT || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on https://urldefense.proofpoint.com/v2/url?u=http-3A__localhost-3A-24-257Bport-257D&d=DwIGAg&c=-35OiAkTchMrZOngvJPOeA&r=03NdPO1x-l0QAZ_R9TNGwA&m=WF5ia-YADjCituVWMV5vLoZ5wg7d_W1qhCYDTbJNGT0&s=PUb7XMS4uP9ICqUY28QgXNRxoWk6sGatPdmZMqmgbJs&e= `);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';
After an exhaustive search, I don't think it is possible. I ended up doing req.logout() and then redirecting to a page that says "you have been logged out of the app but may still be logged into your single sign on provider."
Wepack 2 resolve alias does not work server side
I run wepback middleware with my express server. If the server is started when the import is relative, it'll work fine:
`import from '../../foobar/stuff'`
While the server is already running and the initial server side render is ready in memory; If we change the import in the component, to use the resolve property previously defined through webpack, it'll work:
`import from 'foobar/stuff'`
Meanwhile, if the server is stopped, and re-run with this last change, where the import uses the webpack resolve property defined in the configuration, this will fail (triggers the error not found)
File structure:
[root]
node_modules
webpack.config.js
|____ src
|____ src/js
|____ src/js/modules/
|____ src/js/modules/foobar/containers|components/**/*.js
|____ src/js/modules/lorem/containers|components/**/*.js
It seems that the express server does not know how to resolve the path that it's defined in the webpack 2 resolve property (even though I pass the webpackDevConfig file to the middleware).
Here's the server.dev.js:
import express from 'express'
import path from 'path'
import superagent from 'superagent'
import chalk from 'chalk'
import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router'
import configureStore from './src/js/root/store'
import { Provider } from 'react-redux'
import MyApp from './src/js/modules/app/containers/app'
import Routes from './src/js/root/routes'
const myAppChildRoutes = Routes[0].routes
const app = express()
const port = process.env.PORT ? process.env.PORT : 3000
var serverInstance = null
var dist = path.join(__dirname, 'dist/' + process.env.NODE_ENV)
var config = null
const webpack = require('webpack')
const webpackHotMiddleware = require('webpack-hot-middleware')
const webpackDevConfig = require('./webpack.dev.config')
const compiler = webpack(require('./webpack.dev.config'))
var webpackDevMiddleware = require('webpack-dev-middleware')
const webpackAssets = require('./webpack-assets.json')
config = require('./config')
/**
* Process error handling
*/
process.on('uncaughtException', (err) => {
throw err
})
process.on('SIGINT', () => {
serverInstance.close()
process.exit(0)
})
app.set('views', path.join(__dirname, 'src'))
app.set('view engine', 'ejs')
app.use(webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: webpackDevConfig.output.publicPath,
stats: {
colors: true,
hash: false,
version: true,
timings: false,
assets: false,
chunks: false,
modules: false,
reasons: false,
children: false,
source: false,
errors: true,
errorDetails: true,
warnings: true,
publicPath: false
}
}))
app.use(webpackHotMiddleware(compiler, {
log: console.log
}))
/**
* The Cross origin resource sharing rules
*/
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE')
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type')
res.setHeader('Access-Control-Allow-Credentials', true)
next()
})
/**
* Health check
*/
app.use('/healthcheck', (req, res) => {
res.json({
'env': {
'NODE_ENV': process.env.NODE_ENV
}
})
res.end()
})
app.use('/api/test', (req, res) => {
superagent
.get('https://jsonip.com/')
.end((err, response) => {
if (err) {
console.log('api test err', err)
}
res.send(response.body)
})
})
app.use('/assets', express.static(dist))
app.get('*', (req, res) => {
// (wip) migration to react-router v4 temporary solution
// let matches
// if (typeof routes.props.children !== 'undefined' && Array.isArray(routes.props.children)) {
// matches = routes.props.children.find((v) => {
// return v.props.path === req.url
// })
// } else {
// matches = routes.props.children.props.path === req.url
// }
let matches = true
if (!matches) {
res.status(404).send('Not found')
} else {
const preloadedState = {'foobar': 1}
// Create a new Redux store instance
const store = configureStore(preloadedState)
// Render the component to a string
const myAppHtml = renderToString(<StaticRouter context={{}} location={req.url}>
<Provider store={store}>
<MyApp routes={myAppChildRoutes} />
</Provider>
</StaticRouter>)
// Grab the initial state from our Redux store
const finalState = store.getState()
res.render('index', {
app: myAppHtml,
state: JSON.stringify(finalState).replace(/</g, '\\x3c'),
bundle: webpackAssets.main.js,
build: config.build_name,
css: '/assets/css/main.min.css'
})
}
})
serverInstance = app.listen(port, (error) => {
if (error) {
console.log(error) // eslint-disable-line no-console
}
console.log(chalk.green('[' + config.build_name + '] listening on port ' + port + '!'))
})
Finally, the webpack dev config file is:
var path = require('path')
var webpack = require('webpack')
var AssetsPlugin = require('assets-webpack-plugin')
var assetsPluginInstance = new AssetsPlugin()
var ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
context: path.resolve(__dirname, 'src'),
entry: [
'react-hot-loader/patch',
'webpack/hot/dev-server',
'webpack-hot-middleware/client',
'babel-polyfill',
'./js/index.js'
],
output: {
path: path.join(__dirname, '/dist/development'),
publicPath: '/assets/',
filename: 'js/bundle.js?[hash]'
},
devtool: 'inline-source-map',
devServer: {
hot: true,
// match the output path
contentBase: path.join(__dirname, '/dist/development'),
// match the output `publicPath`
publicPath: '/assets/'
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/,
use: [{
loader: 'babel-loader'
}]
},
{
test: /\.scss$/,
use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader']
}))
},
{
test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
use: [
'file-loader?name=[path][name].[ext]'
]
},
{
test: /\.(jpg|png|gif|svg)$/i,
use: [
'file-loader?name=[path][name].[ext]&emitFile=false'
]
}
]
},
plugins: [
new ExtractTextPlugin('css/[name].min.css?[hash]'),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('development')
}
}),
assetsPluginInstance
],
resolve: {
alias: {
modules: path.resolve(__dirname, 'src/js/modules')
}
}
}
Also, tried to include the path to resolve automaticaly, without success:
module.paths.unshift(path.resolve(__dirname, 'src/js'))
The process.env.NODE_PATH is:
process.env.NODE_PATH Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/johnColtrane/www/coolApp/server.js',
loaded: false,
children: [],
paths:
[ '/Users/johnColtrane/www/coolApp/src/js',
'/Users/johnColtrane/www/coolApp/node_modules',
'/Users/johnColtrane/www/node_modules',
'/Users/johnColtrane/node_modules',
'/Users/node_modules',
'/node_modules' ] }
NOTE: someone suggested babel-plugin-module-alias instead, because of some issues with the approach I want to take. I'll check it and if better then this approach, I'll post here.