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 {};
}