Files

140 lines
3.9 KiB
JavaScript

'use strict'
module.exports = init
const pouchdbErrors = require('pouchdb-errors')
const ms = require('milliseconds')
const uuid = require('uuid').v4
/**
* Setup the Grid session methods.
* @param {import('express').Application} app
*/
function init (app) {
// Stores the active session ids and their state.
// The state can be:
// - {number} Date.now() of when the last activity. But the check returns 'inactive'
// - 'active' when there is an open WebSocket connection.
//
// When the user logs in the session will be generated and the date will be stored.
// If the user then connects, before the timeout, then the state will be switched to 'active'.
// When a WebSocket disconnects, then the time of disconnect will be stored.
// And after a timeout the session will be deleted.
const sessions = new Map()
const onTimeoutSessionIds = new Set()
app.set('gridSessions', sessions)
app.set('gridSessionsIdsOnTimeout', onTimeoutSessionIds)
app.set('generateSession', generate.bind(null, app))
app.set('checkSession', check.bind(null, app))
app.set('changeSessionState', change.bind(null, app))
}
/**
* Generates a new session UUID.
* @param {import('express').Application} app Express Server App
* @returns {string} A new session UUID.
*/
function generate (app) {
const id = uuid()
const sessions = app.get('gridSessions')
const timeoutIds = app.get('gridSessionsIdsOnTimeout')
sessions.set(id, Date.now())
timeoutIds.add(id)
return id
}
/**
* Check if the session is active.
* @param {import('express').Application} app Express Server App
* @param {string} id UUID of the session.
* @returns {'active'|'inactive|} State of the session. Number = timeout & no active connection.
*/
function check (app, id) {
const sessions = app.get('gridSessions')
const timeoutIds = app.get('gridSessionsIdsOnTimeout')
if (!sessions.has(id)) {
throw pouchdbErrors.createError(
pouchdbErrors.UNAUTHORIZED,
'"x-andromeda-session-id" is wrong'
)
}
// Check if sessions did timeout and remove some of them.
process.nextTick(() => {
const timeout = Date.now() - ms.minutes(10)
const toRemove = []
for (const sessionId of timeoutIds) {
if (sessions.get(sessionId) < timeout) {
toRemove.push(sessionId)
} else {
break
}
}
if (toRemove.length > 0) {
for (const sessionId of toRemove) {
sessions.delete(sessionId)
timeoutIds.delete(sessionId)
}
}
})
const state = sessions.get(id)
if (typeof state === 'number' && timeoutIds.has(id) && state < (Date.now() - ms.minutes(10))) {
sessions.delete(id)
timeoutIds.delete(id)
throw pouchdbErrors.createError(
pouchdbErrors.UNAUTHORIZED,
'"x-andromeda-session-id" is wrong'
)
}
return typeof state === 'number' ? 'inactive' : state
}
/**
* Change the session state
* @param {import('express').Application} app Express Server App
* @param {string} id UUID of the session
* @param {'active'|'inactive'|'end'} state Session is active (has a connection), inactive or did end
* @returns {'active'|'end'|number} The next state.
*/
function change (app, id, state) {
const sessions = app.get('gridSessions')
const timeoutIds = app.get('gridSessionsIdsOnTimeout')
if (!sessions.has(id)) {
throw pouchdbErrors.createError(
pouchdbErrors.UNAUTHORIZED,
'"x-andromeda-session-id" is wrong'
)
}
switch (state) {
case 'end':
sessions.delete(id)
if (timeoutIds.has(id)) {
timeoutIds.delete(id)
}
return 'end'
case 'active':
sessions.set(id, 'active')
if (timeoutIds.has(id)) {
timeoutIds.delete(id)
}
return 'active'
case 'inactive':
{
const now = Date.now()
sessions.set(id, now)
timeoutIds.add(id)
return 'inactive'
}
default:
throw pouchdbErrors.createError(pouchdbErrors.INVALID_REQUEST, `bad state "${state}"`)
}
}