Clear all User Redis Sessions - NodeJS

I needed the option to log out all sessions from a user, so I coded an extension. That’s useful for something like a “Logout From All Sessions” button.

It’s configured for a managed Redis database from DigitalOcean and reads the connection parameters from the .env.

You can easily modify the code to use globals or options if that fits better into your stack.

The input is a simple user_Id, and the extension returns the amount of deleted sessions.

CleanShot 2023-01-27 at 03.18.58

clear_sessions.js
const dotenv = require("dotenv");
dotenv.config();

const Redis = require("ioredis");

exports.clearSessions = async function (options) {
  options = this.parse(options);
  let connectPort = process.env.REDIS_PORT;
  let connectHost = process.env.REDIS_HOST;
  let connectPassword = process.env.REDIS_PASSWORD;
  let connectUser = process.env.REDIS_USER;

  if (!options.user_id) {
    console.error("Please provide user ID in options object.");
    return;
  }

  const redis = new Redis({
    port: connectPort,
    host: `${connectHost}`,
    password: `${connectPassword}`,
    user: `${connectUser}`,
    db: 0,
    tls: {},
  });

  redis.on("connect", function () {
    console.log("Connected to Redis...");
  });

  const keys = await redis.keys("sess:*");
  let deletedKeys = 0;
  const deletePromises = keys.map((key) => {
    return redis.get(key).then((value) => {
      try {
        value = JSON.parse(value);
        if (value.securityId === parseInt(options.user_id)) {
          console.log(`Deleting key: ${key}`);
          deletedKeys++;
          return redis.del(key);
        }
      } catch (err) {
        console.log(err);
      }
    });
  });

  await Promise.all(deletePromises);
  console.log(`All keys have been deleted, total: ${deletedKeys}`);
  return deletedKeys;
};
clear_sessions.hjson
[
  {
    "type": "clear_sessions",
    "module": "clear_sessions",
    "action": "clearSessions",
    "groupTitle": "Sessions",
    "groupIcon": "fas fa-lg fa-file comp-data",
    "title": "Clear All User sessions",
    "icon": "fas fa-lg fa-file comp-data",
    "dataPickObject": true,
    "dataScheme": [
      {
        "name": "file_name",
        "type": "text"
      }
    ],
    "properties": [
      {
        "group": "Clear Sessions",
        "variables": [
          {
            "name": "actionName",
            "optionName": "name",
            "title": "Name",
            "type": "text",
            "required": true,
            "defaultValue": ""
          },
          {
            "name": "user_id",
            "optionName": "user_id",
            "title": "User ID",
            "type": "text",
            "required": true,
            "defaultValue": "",
            "serverDataBindings": true
          },
          {
            "name": "output",
            "optionName": "output",
            "title": "Output",
            "type": "boolean",
            "defaultValue": false
          }
        ]
      }
    ]
  }
]
6 Likes

I’ve checked Redis sessions here, and “userID” doesn’t exist, but rather “securityId”:

"{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"securityId\":1}"

Does “userID” exist on yours?

Session names are also prefixed with sess:, so you could use this:

const keys = await redis.keys("sess:*");
2 Likes

userID exists for me.

Yeah good point re filtering by sess*

@Apple that’s what it looks like in my db:

{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userID":24,"securityId":24}

Updated the script to securityId though since that might apply to more community members.

2 Likes

@tbvgl, Nice!!! Thanks for sharing.

We can also use Connections options directly from globals without having to define them:

const redis = new Redis({
    port: global.redisClient.connection_options.port,
    host: global.redisClient.connection_options.host,
    db: 0
  });

and use const redisReady = global.redisClient.ready; to ensure redis is ready before running connect.

Yeah, I’m using this as my default for extensions and custom scripts so that they use ENV variables, if available, and globals as a fallback. I’m doing that because I have some projects with multiple Redis clusters.

const redis = new Redis({
  port: process.env.REDIS_PORT || global.redisClient.options.port,
  host: process.env.REDIS_HOST || global.redisClient.options.host,
  db: process.env.REDIS_DB || 0,
  ...(process.env.REDIS_PASSWORD || global.redisClient.options.password
    ? {
        password:
          process.env.REDIS_PASSWORD || global.redisClient.options.password,
      }
    : {}),
  ...(process.env.REDIS_USER || global.redisClient.options.user
    ? { username: process.env.REDIS_USER || global.redisClient.options.user }
    : {}),
  ...(process.env.REDIS_TLS || global.redisClient.options.tls
    ? { tls: {} }
    : {}),
  ...(process.env.REDIS_PREFIX
    ? { prefix: `{${process.env.REDIS_PREFIX}}` }
    : {}),
});
1 Like