UNPKG

@accounts/redis

Version:
132 lines (119 loc) 4.57 kB
import { type Redis } from 'ioredis'; import { generate } from 'shortid'; import { type Session, type DatabaseInterfaceSessions, type ConnectionInformations, } from '@accounts/types'; import { type AccountsRedisOptions } from './types'; const defaultOptions = { userCollectionName: 'users', sessionCollectionName: 'sessions', timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt', }, idProvider: () => generate(), dateProvider: (date?: Date) => (date ? date.getTime() : Date.now()), }; export class RedisSessions implements DatabaseInterfaceSessions { private options: AccountsRedisOptions & typeof defaultOptions; private db: Redis; constructor(db: Redis, options: AccountsRedisOptions = {}) { this.options = { ...defaultOptions, ...options, timestamps: { ...defaultOptions.timestamps, ...options.timestamps }, }; if (!db) { throw new Error('A database connection is required'); } this.db = db; } public async createSession( userId: string, token: string, connection: ConnectionInformations = {}, // eslint-disable-next-line @typescript-eslint/no-unused-vars extraData?: object ): Promise<string> { const sessionId = this.options.idProvider(); const pipeline = this.db.pipeline(); pipeline.hmset(`${this.options.sessionCollectionName}:${sessionId}`, { userId, token, userAgent: connection.userAgent, ip: connection.ip, valid: true, [this.options.timestamps.createdAt]: this.options.dateProvider(), [this.options.timestamps.updatedAt]: this.options.dateProvider(), }); // Push the sessionId inside the userId pipeline.sadd( `${this.options.sessionCollectionName}:${this.options.userCollectionName}:${userId}`, sessionId ); // Link the session token to the sessionId pipeline.set(`${this.options.sessionCollectionName}:token:${token}`, sessionId); await pipeline.exec(); return sessionId; } public async updateSession(sessionId: string, connection: ConnectionInformations): Promise<void> { if (await this.db.exists(`${this.options.sessionCollectionName}:${sessionId}`)) { await this.db.hmset(`${this.options.sessionCollectionName}:${sessionId}`, { userAgent: connection.userAgent ?? undefined, ip: connection.ip ?? undefined, [this.options.timestamps.updatedAt]: this.options.dateProvider(), }); } } public async invalidateSession(sessionId: string): Promise<void> { if (await this.db.exists(`${this.options.sessionCollectionName}:${sessionId}`)) { await this.db.hmset(`${this.options.sessionCollectionName}:${sessionId}`, { valid: 'false', [this.options.timestamps.updatedAt]: this.options.dateProvider(), }); } } public async invalidateAllSessions(userId: string, excludedSessionIds?: string[]): Promise<void> { if ( await this.db.exists( `${this.options.sessionCollectionName}:${this.options.userCollectionName}:${userId}` ) ) { let sessionIds: string[] = await this.db.smembers( `${this.options.sessionCollectionName}:${this.options.userCollectionName}:${userId}` ); if (excludedSessionIds && excludedSessionIds.length > 0) { sessionIds = sessionIds.filter((sessionId) => { return !excludedSessionIds.includes(sessionId); }); } await sessionIds.map((sessionId) => this.invalidateSession(sessionId)); } } public async findSessionByToken(token: string): Promise<Session | null> { if (await this.db.exists(`${this.options.sessionCollectionName}:token:${token}`)) { const sessionId = await this.db.get(`${this.options.sessionCollectionName}:token:${token}`); if (sessionId) { return this.findSessionById(sessionId); } } return null; } public async findSessionById(sessionId: string): Promise<Session | null> { if (await this.db.exists(`${this.options.sessionCollectionName}:${sessionId}`)) { const session = await this.db.hgetall(`${this.options.sessionCollectionName}:${sessionId}`); return this.formatSession(sessionId, session); } return null; } /** * We need to format the session to have an object the server can understand. */ private formatSession(sessionId: string, session: any): Session { // Redis doesn't store boolean values, so we need turn this string into a boolean session.valid = session.valid === 'true'; return { id: sessionId, ...session }; } }