Skip to content

Recipes ​

GitHub-Style Device Page ​

List every signed-in device with browser, IP, and activity — mark the current one:

typescript
import { Controller, Get, Inject, Req } from '@nestjs/common';
import { SESSION_SERVICE } from '@nestjs-redisx/session';
import type { ISessionService } from '@nestjs-redisx/session';

import { AppSession } from './types';

interface SessionRequest {
  sessionID: string;
  session: { passport?: { user?: string } };
}

@Controller('account')
export class DevicePageController {
  constructor(@Inject(SESSION_SERVICE) private readonly sessions: ISessionService<AppSession>) {}

  // GitHub-style "Sessions" page: every device with IP, browser, and activity.
  @Get('sessions')
  async devicePage(@Req() req: SessionRequest) {
    const userId = req.session.passport?.user;
    const devices = await this.sessions.getSessionsByUser(userId!);

    return devices.map((device) => ({
      id: device.id,
      current: device.id === req.sessionID,
      ip: device.metadata?.ip,
      userAgent: device.metadata?.userAgent,
      signedInAt: device.metadata?.createdAt,
      lastActiveAt: device.metadata?.lastSeenAt,
    }));
  }
}

Pair it with activity stamping so the IP / user-agent columns are populated:

typescript
import { NestFactory } from '@nestjs/core';
import { SESSION_SERVICE } from '@nestjs-redisx/session';
import type { ISessionService } from '@nestjs-redisx/session';

import { AppModule } from './types';

interface SessionRequest {
  sessionID: string;
  session?: { passport?: { user?: string } };
  ip: string;
  get(header: string): string | undefined;
}

async function bootstrap(): Promise<void> {
  const app = await NestFactory.create(AppModule);
  const sessions = app.get<ISessionService>(SESSION_SERVICE);

  // Opt-in metadata stamping AFTER the session middleware: gives the device
  // page its IP and user-agent columns. Fire-and-forget — never blocks.
  app.use((req: SessionRequest, _res: unknown, next: () => void) => {
    if (req.session?.passport?.user) {
      void sessions.recordActivity(req.sessionID, { ip: req.ip, userAgent: req.get('user-agent') }).catch(() => undefined);
    }
    next();
  });

  await app.listen(3000);
}

void bootstrap();

Log Out Everywhere (Else) ​

The security page button, the password-change hook, and the support tool:

typescript
import { Injectable, Inject } from '@nestjs/common';
import { SESSION_SERVICE } from '@nestjs-redisx/session';
import type { ISessionService } from '@nestjs-redisx/session';

@Injectable()
export class SessionSecurityService {
  constructor(@Inject(SESSION_SERVICE) private readonly sessions: ISessionService) {}

  // The "log out everywhere else" button: keeps the clicking device signed in.
  logoutOtherDevices(userId: string, currentSessionId: string): Promise<number> {
    return this.sessions.revokeAllExcept(userId, currentSessionId);
  }

  // Password change / account compromise: terminate everything.
  async onPasswordChanged(userId: string): Promise<number> {
    return this.sessions.revokeAll(userId);
  }

  // Support/admin: terminate one specific session by ID.
  async revokeSingle(sessionId: string): Promise<boolean> {
    return this.sessions.revoke(sessionId);
  }

  // Live counters for dashboards.
  async stats(userId: string): Promise<{ total: number; forUser: number }> {
    return {
      total: await this.sessions.count(),
      forUser: await this.sessions.countByUser(userId),
    };
  }
}

Seat Limits + Compliance Cap ​

Banking-grade policies in two options:

typescript
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { SessionPlugin } from '@nestjs-redisx/session';

@Module({
  imports: [
    RedisModule.forRoot({
      clients: { host: 'localhost', port: 6379 },
      plugins: [
        new SessionPlugin({
          // Seat limit: at most 3 concurrent sessions per user.
          // 'evict-oldest' silently signs out the oldest device;
          // 'reject' throws SessionLimitExceededError at login instead.
          maxSessionsPerUser: 3,
          maxSessionsPolicy: 'evict-oldest',

          // Compliance cap (PCI DSS / OWASP): force re-login every 12 hours
          // regardless of activity. Idle timeout stays the middleware's job;
          // this cap is what express-session alone cannot enforce.
          absoluteLifetimeMs: 12 * 3600 * 1000,
        }),
      ],
    }),
  ],
})
export class AppModule {}

Audit Trail ​

typescript
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { SessionPlugin } from '@nestjs-redisx/session';

@Module({
  imports: [
    RedisModule.forRoot({
      clients: { host: 'localhost', port: 6379 },
      plugins: [
        new SessionPlugin({
          // Audit hooks: fire-and-forget, failures are logged and never break
          // the request. Ship them to your audit log / SIEM.
          events: {
            onCreated: ({ sessionId, userId }) => console.log('session created', sessionId, userId),
            onDestroyed: ({ sessionId, userId }) => console.log('logout', sessionId, userId),
            onRevoked: ({ sessionId, userId }) => console.log('revoked/evicted', sessionId, userId),
            onExpiredByCap: ({ sessionId, userId }) => console.log('lifetime cap hit', sessionId, userId),
          },
        }),
      ],
    }),
  ],
})
export class AppModule {}

Typed Sessions ​

typescript
import { Injectable, Inject } from '@nestjs/common';
import { SESSION_SERVICE } from '@nestjs-redisx/session';
import type { ISessionService } from '@nestjs-redisx/session';

import { AppSession } from './types';

// req.session typing stays middleware-owned — extend it via declaration
// merging (compile-time only; session contents are not validated at runtime):
//
// declare module 'express-session' {
//   interface SessionData extends AppSession {}
// }

@Injectable()
export class TypedSessionService {
  constructor(
    // Our API is genuinely typed: pass your payload shape as the generic.
    @Inject(SESSION_SERVICE) private readonly sessions: ISessionService<AppSession>,
  ) {}

  async cartOf(sessionId: string): Promise<string[]> {
    const info = await this.sessions.getSession(sessionId);
    return info?.data.cart ?? []; // data is AppSession, not unknown
  }
}

OIDC Back-Channel Logout ​

The plugin makes back-channel logout implementable in a few lines: the IdP's backchannel_logout request carries the user (sub) — resolve it and call revokeAll(sub). No session enumeration, no custom index.

typescript
@Post('backchannel-logout')
async backchannelLogout(@Body() body: { logout_token: string }) {
  const { sub } = await this.oidc.verifyLogoutToken(body.logout_token);
  await this.sessions.revokeAll(sub);
  return {};
}

Released under the MIT License.