0

I have two servers running concurrently on localhost ports 3300 and 4400. The first one is hosting my SSR Vite Vue application and the other one is hosting db.json for a mocked Web API using 'json-server'.

Here is my json-server:

import bodyParser from 'body-parser';
import cors from 'cors';
import dotenv from 'dotenv';
import express from 'express';
import jsonServer from 'json-server';

import mockLogin from './mocked-auth/login.handler.js';
import mockLogout from './mocked-auth/logout.handler.js';
import me from './mocked-auth/me.handler.js';
import { expressCookieSession } from './mocked-auth/session.middleware.js';

dotenv.config();

const server = jsonServer.create();
const router = jsonServer.router('./json-server/db.json');
const middlewares = jsonServer.defaults();

// Enable All CORS Requests
server.use(cors());

// #region mocked auth API endpoints
const app = express();
app.use(expressCookieSession);
app.use(bodyParser.json());
mockLogin(app, router);// POST /login
mockLogout(app);
me(app);
server.use(app); 
// #endregion mocked auth API endpoints

server.use(middlewares);
server.use(router);

const port = process.env.JSON_SERVER_PORT;

server.listen(port, () => {
  console.log('\x1b[32m%s\x1b[0m', `JSON Server is running at http://localhost:${port}/`);
});

The important thing here is how I declared my express-session and the /login & /me endpoints.

mocked-auth/session.middleware.js

import session from 'express-session';

const sessionConfig = {
  secret: 'your_secret_key',
  resave: false,
  saveUninitialized: true,
  cookie: {
    // secure: true, // Set 'secure' to true if using HTTPS
    httpOnly: true, // Prevent client-side JavaScript from accessing the cookie
    sameSite: false,
  },
};

// Create the session middleware
export const expressCookieSession = session(sessionConfig);

./mocked-auth/login.handler.js

/**
 * Returns an `Express.Application` with a `RequestHandler` for a mocked `POST /login`.
 *
 * Traditionally, session management in `Express.js` involves storing a session ID in a cookie
 * and maintaining session data typically using a session store (e.g. in-memory store, db, or
 * external session store like Redis). For this project we are using express-session which does
 * the in-memory session management for us.
 */
export default function mockLogin(app, router) {
  app.post('/login', (req, res) => {
    const { email, password } = req.body;
    const users = router.db.get('users');
    const user = users.find({ email, password }).value();
    console.log(`SELECT * from 'users' WHERE email = '${email}' AND password = '${password}' :\n`, user);

    if (user) {
      req.session.user = user;
      req.session.save();
      res.jsonp({ user });
    } else {
      console.log('USER NOTE FOUND! Returning 401, { error: "Invalid credentials" } ');
      // Clear the session cookie on the client side
      // "connect.sid" is the default cookie name set by express-session.
      res.clearCookie('connect.sid', { httpOnly: true });
      res.status(401).jsonp({ error: 'Invalid credentials' });
    }
  });
  return app;
}

./mocked-auth/me.handler.js

export default function me(app) {
  app.get('/me', (req, res) => {
    console.log('API GET /me req.headers.cookie:\n', req.headers.cookie);
    console.log('API GET /me req.session:\n', req.session);
    const user = req.session?.user;
    if (user) res.jsonp(user);
    else res.status(401).jsonp({ error: 'Unauthorized Request.' });
  });

  return app;
}

I am aware that express-session builtin memory storage instance is only alive on the json-server port, so I cannot simply add a expressCookieSession middleware on my SSR server app and expect to magically get the express.session from there. So I have to request a GET /me from my SSR entrypoint by forwarding the cookie from the browser (which is automatically written by /login and carried over on the succeeding API requests).

Here is the SSR server app:

// Note that this file isn't processed by Vite, see https://github.com/brillout/vite-plugin-ssr/issues/562
import dotenv from 'dotenv';
dotenv.config();

import express from 'express';
import compression from 'compression';
import { root } from './root.js';

import auth from './middlewares/auth.middleware.js';
import ssr from './middlewares/ssr.middleware.js';

const isProduction = process.env.NODE_ENV === 'production';

startServer();

async function startServer() {
  const app = express();

  // Refer to https://expressjs.com/en/guide/using-middleware.html#middleware.application

  app.use(compression());

  if (isProduction) {
    const sirv = (await import('sirv')).default;
    app.use(sirv(`${root}/dist/client`));
  } else {
    const vite = await import('vite');
    const viteDevMiddleware = (
      await vite.createServer({
        root,
        server: { middlewareMode: true },
      })
    ).middlewares;
    app.use(viteDevMiddleware);
  }

  app.get('*', auth, ssr);

  const port = process.env.VITE_SSR_PORT || 3200;
  app.listen(port);
  console.log(`Server running at http://localhost:${port}`);
}

And the auth middleware:

import { RequestHandler } from 'express';
import { ofetch } from 'ofetch';

const auth: RequestHandler = async (req, res, next) => {
  console.log('SSR Entrypoing, AUTH', req.headers.cookie);

  const user = await ofetch('http://localhost:4400/me', {
    method: 'get',
    headers: {
      cookie: req.headers.cookie as any,
    },
    credentials: 'include',
  }).catch(err => {
    console.log(err, '\njson-server has not found user from req.session');
  });
  if (!user) return next();
  req.user = user;

  next();
};

export default auth;

As you can see I just forwarded the req.headers.cookie on the headers (and also tried adding credentias: 'include' after reading from somewhere mentioning it). I can guarantee that the req.headers.cookie contain the default session cookie and that it is passed successfully to the other server's /me endpoint. Looking at the logs I have something like this for the req.session (no .user):

 Session {
    cookie: {
    path: '/',
    _expires: null,
    originalMaxAge: null,
    httpOnly: true,
    sameSite: false
   }
 }

and the req.headers.cookie actually has the session cookie (example notice the connect.sid is there):

__stripe_mid=2e077326-a501-4f3f-bd3b-df8e8c9b2924a96499; _ga=GA1.1.1007045372.1684917775; _ga_8F4BEDXWDH=GS1.1.1684917775.1.1.1684918949.60.0.0; connect.sid=s%3AzZwnxnP5dOjNyEjoY4gdfBUN0bHVsxyT.%2FjYg918nKih9R7U4WLoNNwDh9%2B8qhetK0gVcnM6g9o4

I have made sure I hit req.session.save() after setting req.session.user on the login endpoint. I don't know what I am missing here. Is there something lacking on the ofetch from the SSR server app side? Or is there something I'm missing in how I should set the req.session or the express-session storage on the other server? Or am I missing something else?

Note that I have confirmed that doing /login and /me to the json-server directly (I was using postman) is working without issues (i.e. req.session.user is available after successful login). (Postman automatically passes the cookie written on it by the /login response)

UPDATE

I have updated the cors config to:

const corsOptions = {
  origin: `http://localhost:${process.env.VITE_SSR_PORT}`,
  credentials: true,
};
server.use(cors(corsOptions));

after reading this StackOverflow post, still didn't work for me.

Alex Pappas
  • 2,377
  • 3
  • 24
  • 48

0 Answers0