mweibel / connect-session-sequelize

Sequelize SessionStore for Express/Connect
212 stars 72 forks source link

When using with connect-flash, it didn't see just set message after redirect. #20

Closed axln closed 9 years ago

axln commented 9 years ago

In my code I use connect-flash which utilizes sessions:

req.flash('message', 'Please check your email to confirm it.');
res.redirect('/register');

Then

app.get('/register', function(req, res) {
  res.render('auth/register', {
    message: req.flash('message')
  });
});

The problem is that connect-flash didn't see the message that just has been set. If I hit F5, the message appeared. Of course this breaks the main idea behind session flash messages.

Tried the same example with MemoryStore and it works as expected. So probably the problem is in the async nature of connect-session-sequelize.

axln commented 9 years ago

Just tested with express-mysql-session, and it also works as exptected.

jakeorr commented 9 years ago

I'm seeing the same issue, axin. The behaviour occurs because express/session does not currently make the response wait for the async session save to complete. I've seen a number of issues dealing with similar problems. A workaround is to use req.session.save(cb) and call your redirect in the callback. For example, with your above code:

req.flash('message', 'Please check your email to confirm it.');
req.session.save(function () {
  res.redirect('/register');
});

I wish I could give you a better answer, but this issue is at a higher level than this repo.

This issue talks about the behaviour and work on a solution is mentioned, but I haven't heard anything concrete yet.

If you're curious, here's a brief explanation of why express-session functions the way it does.

jontelm commented 9 years ago

@jakeorr And when this problem not showing up in https://github.com/chill117/express-mysql-session/blob/master/lib/index.js#L163 is just because it's faster?

reymillenium commented 1 year ago

I'm having the same issue with a Node.js + Express.js + EJS + Sequelize app.

I spent three whole days trying to figure out what was going on, until I found it's because of the sessions takes longer to be saved with the package connect-session-sequelize (it's async), so the redirect happens way before the session is saved. And I need the sessions to use flash & toastr. None of them were working obviously.

I have another project (Node.js + Express.js + PUG + Mongoose) that was working perfectly (uses connect-mongodb-session) and for many hours I thought it was because of many different things: EJS, the bootstrap version, another js file colliding, something missing in the html meta info, etc, etc, etc. It was horrible.

Until I found this link I thought I was the ojnly one experiencing this. I searched on the Internet EVERYWHERE, and nothing.

I even tried in my controller with:

        setTimeout(() => {
            response.redirect(`/#contact`);
        }, 400);

... and nothing. The only way to see the toastr, the flash message and the retrieved component's info from the controller using flash on the form was reloading the page.

But now it works this way (workaround):

        request.flash('flashMessage', [{focusOn: errors.array()[0].param, formValues: newContactLetterInfo}]);
        request.toastr.error(errors.array()[0].msg, 'Validation Error!');
        request.session.save(() => {
            response.redirect(`/#contact`);
        });

Is there any other way to achieve this? Because it really sucks having to use this workaround in every action response, on every action method of every controller.

I will try with another package to store the sessions on the MySQL DB, because connect-session-sequelize is not working as expected

This is my app.js file

if (process.env.NODE_ENV !== 'production') require('dotenv').config();

const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const {sequelize} = require('./models');
const session = require('express-session') // https://www.npmjs.com/package/express-session
const cookieParser = require('cookie-parser'); // https://www.npmjs.com/package/cookie-parser
const flash = require('connect-flash'); // https://www.npmjs.com/package/connect-flash
const toastr = require('express-toastr'); // https://www.npmjs.com/package/express-toastr
const SequelizeStore = require('connect-session-sequelize')(session.Store);
const back = require('express-back'); // https://www.npmjs.com/package/express-back
const helmet = require('helmet'); // https://www.npmjs.com/package/helmet
const compression = require('compression'); // https://www.npmjs.com/package/compression
const morgan = require('morgan'); // https://www.npmjs.com/package/morgan
const rfs = require('rotating-file-stream'); // version 3.x

// Creation of the Express.js app:
const app = express();

const store = new SequelizeStore({
    db: sequelize,
    tableName: 'sessions',
    checkExpirationInterval: 15 * 60 * 1000, // The interval at which to clean up expired sessions in milliseconds (15 min)
    expiration: 60 * 60 * 1000, // The maximum age (in milliseconds) of a valid session. (1 hour)
})

// View Templating Engine Setup:
app.set('view engine', 'pug');
app.set('views', 'views');

// Using Helmet as a Middleware to protect our responses:
const safeWebsites = [
    "https://use.fontawesome.com",
    "https://cdnjs.cloudflare.com",
    "https://maxcdn.bootstrapcdn.com",
    "https://cdn.jsdelivr.net",
    'https://www.google.com/',
    'https://unpkg.com',
    'https://fonts.googleapis.com',
];
app.use(
    helmet.contentSecurityPolicy({
        useDefaults: true,
        directives: {
            "img-src": ["'self'", "https: data:"],
            "script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'", ...safeWebsites],
            "frame-src": '*.google.com'
        }
    })
);

// Using Morgan as a Middleware to add logging as a single file always and on the terminal just in development:
// Creates a single file write stream
// const accessLogStream = fs.createWriteStream(path.join(__dirname, 'logs', 'access.log'), {flags: 'a'}); // New data will be appended at the end of the file
// Creates a rotating write stream
const accessLogStream = rfs.createStream('access.log', {
    size: "10M", // rotate every 10 MegaBytes written
    interval: '1d', // rotates daily
    path: path.join(__dirname, 'logs'),
    compress: "gzip" // compress rotated files
});
app.use(morgan("combined", {stream: accessLogStream}));
if (app.get("env") !== "production") {
    app.use(morgan("dev")); //log to console only on development
}

// Using Compression as a Middleware to compress part of the html, css & js code:
app.use(compression());

// Routes Declaration:
const {routes: contactLettersRoutes} = require('./routes/contactLetters.route');
const {routes: homeRoutes} = require('./routes/home.route');

// Serving static files
app.use(express.static(path.join(__dirname, 'public'), {redirect: false}));

// BodyParser Parser Middleware (For text data only): x-www-form-urlencoded
app.use(bodyParser.urlencoded({extended: false}));

// Using a cool js editor alike:
app.use('/tinymce', express.static(path.join(__dirname, 'node_modules', 'tinymce')));

// Extra Middlewares:
app.use(cookieParser('keyboard cat'));
app.use(session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: false,
    cookie: {
        // secure: true,
        maxAge: 60 * 60 * 1000 //  1 hour
    },
    store: store
}));
app.use(flash());
// Multer-S3 Parser Middlewares Setup (For hybrid data, a single file named 'image'):
// app.use(imageS3UploadV3.single('image'));
// // Load express-toastr. We can pass an object of default options to toaster(), see example/index.coffee
app.use(toastr());
app.use(back());

// Setting the local variables for each view
app.use((request, response, next) => {
    const flashMessage = request.flash('flashMessage')[0];
    response.locals.session = request.session;
    response.locals.flashMessage = flashMessage;
    // response.locals.toasts = request.toastr.render();
    response.locals.toastr = request.toastr;
    response.locals.path = path;
    next();
});

// Routes Usage:
app.use('/contactLetters', contactLettersRoutes);
app.use(homeRoutes);

// Error Handling Middlewares: If we have more than one, they'll execute from top to bottom, just like the "normal" middleware
app.use((error, request, response, next) => {
    request.flash('flashMessage', [{class: 'alert-danger', message: error.toString()}]);
    return response.redirect('/500');
});

const PORT = process.env.PORT || 3000
const withForcedSync = false;
sequelize.sync({force: withForcedSync}).then(result => {
    app.listen(PORT, () => {
        console.log(`The server is running on the port ${PORT}.`, '\n');
    });
}).catch(error => {
    console.log('app.js -> sequelize.sync() -> catch -> error = ', error, '\n');
});

Update: I just tried with express-session-sequelize and it has the same problem. So the issue must be with the express-session package. I wish there was something I could do on the middleware which saves the locals in order to receive the flash info before the redirect is completed.