Sesiones y Persistencia

Servir páginas es lo más fácil en web apps, simplemente creas el archivo HTML y lo presentas desde el servidor cuando el cliente solicita una ruta en particular. Pero la naturaleza asíncrona del servidor hace que luego de enviar una respuesta simplemente se desconecta y se olvida de todo, incluso de los datos que recibió y envió. Entonces como podemos persistir dichos datos cada vez que queramos accesar otra página sin tener que volver a identificarnos y enviar información de nuevo al servidor?

Cookies

Cookies son pedacitos de datos que guardamos en nuestro navegador por cada dominio que visitamos y que enviamos de nuevo al servidor por cada página que solicitamos. De esa forma el servidor puede saber quienes somos sin tener que preguntarnos de nuevo. Así las usaremos en nuestra app:


// First include the module in the app
const cookies = require('cookie-parser')
:
// Then tell Express to use cookies
app.use(cookies())
:
// Then set the user for the first time
res.cookie('user', 'George') // response to the client, browser stores cookie
:
// Then use it on every request
const user = req.cookies.user // request from client, browser sends cookie
console.log('Hello', user)
        

Pero primero debemos instalar el módulo de cookies:


$ npm install cookie-parser
      

Una vez obtengamos la cookie sabremos si el usuario nos ha visitado anteriormente y podremos identificarlo adecuando nuestra app a sus gustos, idioma, colores, etc. Si acomodamos nuestra app para manejar cookies quearía así:


const fs      = require('node:fs')
const path    = require('node:path')
const cookies = require('cookie-parser') // Cookies
const express = require('express')       // Framework
const ejs     = require('ejs')           // Templates

async function main(){
  console.log('App is running...')
  const app = express()
  app.use(express.static(path.join(__dirname, 'public'))) // static files here
  app.set('views', path.join(__dirname, 'pages'))         // use pages folder
  app.set('view engine', 'html')                          // use HTML templates
  app.engine('html', ejs.renderFile)                      // use EJS engine
  app.use(cookies())                                      // use cookies
  app.use(express.json())                                 // process form data as JSON
  app.use(express.urlencoded({ extended: true }))         // auto parse post form data

  // ROUTER

  app.get('/', (req, res)=>{
    res.render('index')
  })
  
  app.get('/about', (req, res)=>{
    res.render('about')
  })
  
  app.get('/contact', (req, res)=>{
    const user = req.cookies.user // request from client, browser sends cookie
    if(user){
      res.redirect('/welcome')
      return
    }
    res.render('contact')
  })
  
  app.post('/contact', (req, res)=>{
    const name  = req.body.name || 'Anonymous'
    const phone = req.body.phone || 'Unknown'
    console.log('Data:', name, phone)
    res.cookie('user', name) // response to the client, browser stores cookie
    // const ok = database.saveContact(name, phone) // NOT READY
    res.redirect('/welcome')
  })

  app.get('/welcome', (req, res)=>{
    const user = req.cookies.user // request from client, browser sends cookie
    res.render('welcome', {user})
  })
  
  app.get('/logout', (req, res)=>{
    const user = res.clearCookie('user') // remove cookie from browser
    res.redirect('/')
  })
  
  // Catch all
  app.get(/(.*)/, (req, res)=>{
    res.render('notfound')
  })

  app.listen(3000)
}

main()
        

Tokens

Como las cookies pueden ser fácilmente modificadas en el browser suplantando identidades, es recomendable usar tokens criptográficos más difíciles de alterar. Muchas veces un UUID puede ser suficiente, pero para casos mas exigentes es recomendable usar algún otro método como SHA256, JWT, OAUTH2. Por ahora, reemplazemos el nombre del usuario con el token UUID como cookie de identificación y reduciremos la posibilidad de suplantar identidades:


const UUID = require('uuid')
:
const token = UUID.v4()  // ie. FA15769C-5242-4F25-888D-69AF63AA5055
res.cookie('token', token) // response to the client, browser stores token
        

Session

Muchas veces no basta con el nombre o token y también hace falta persistir otros datos como idioma, email, estilo (light or dark), etc y entonces es mejor usar sesiones. Las sesiones permiten guardar mas datos a la vez pero son volátiles en el sentido que tienen tiempo de caducidad y son eliminadas del servidor al transcurrir cierto tiempo. Por eso para la permanencia de datos siempre es recomendado el uso de bases de datos que veremos en el próximo capítulo. Por ahora aprendamos el uso de sesiones así:


const sessions = require('express-session') // install with npm
const session = sessions({
  secret: "mysecretkey",
  cookie: { maxAge: 1000 * 60 * 60 * 24 }, // milliseconds
  saveUninitialized:true,
  resave: false
})
app.use(session)

// When saving contact data, add fields to session instead of cookie
//res.cookie('user', name)         // no
res.session.user = { name, phone } // yes
        

So the final code would lookk like this:


const fs       = require('node:fs')
const path     = require('node:path')
const cookies  = require('cookie-parser')   // Cookies
const express  = require('express')         // Framework
const ejs      = require('ejs')             // Templates
const sessions = require('express-session') // Session

async function main(){
  console.log('App is running...')
  const session = sessions({
    secret: "mysecretkey",  // TODO: use env vars
    cookie: { maxAge: 24 * 60 * 60 * 1000 }, // one day in milliseconds
    saveUninitialized: false,
    resave: false
  })
  const app = express()
  app.use(express.static(path.join(__dirname, 'public'))) // static files here
  app.set('views', path.join(__dirname, 'pages'))         // use pages folder
  app.set('view engine', 'html')                          // use HTML templates
  app.engine('html', ejs.renderFile)                      // use EJS engine
  app.use(cookies())                                      // use cookies
  app.use(express.json())                                 // process form data as JSON
  app.use(express.urlencoded({ extended: true }))         // auto parse post form data
  app.use(session)                                        // persist user data

  // ROUTER

  app.get('/', (req, res)=>{
    res.render('index')
  })
  
  app.get('/about', (req, res)=>{
    res.render('about')
  })
  
  app.get('/contact', (req, res)=>{
    //const user = req.cookies.user // request from client, browser sends cookie
    const user = req.session.user
    if(user){
      res.redirect('/welcome')
      return
    }
    res.render('contact')
  })
  
  app.post('/contact', (req, res)=>{
    const name  = req.body.name || 'Anonymous'
    const phone = req.body.phone || 'Unknown'
    console.log('Data:', name, phone)
    req.session.user = { name, phone } // session stays on the server
    // res.cookie('user', name)        // response to the client, browser stores cookie
    // const ok = database.saveContact(name, phone) // NOT READY
    res.redirect('/welcome')
  })

  app.get('/welcome', (req, res)=>{
    //const user = req.cookies.user // request from client, browser sends cookie
    const user = req.session.user?.name   // stored on server
    if(!user){
      res.redirect('/')
      return
    }
    res.render('welcome', {user})
  })
  
  app.get('/logout', (req, res)=>{
    //const user = res.clearCookie('user') // remove cookie from browser
    req.session.destroy()                  // remove session from server
    res.redirect('/')
  })
  
  // Catch all
  app.get(/(.*)/, (req, res)=>{
    res.render('notfound')
  })

  app.listen(3000)
}

main()
        

Y no olvidemos instalar el módulo de sesiones antes de usarlo:


$ npm install express-session