import { Injectable, Inject } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Redis } from "ioredis"; import { Logger } from "nestjs-pino"; @Injectable() export class TokenBlacklistService { constructor( @Inject("REDIS_CLIENT") private readonly redis: Redis, private readonly configService: ConfigService, @Inject(Logger) private readonly logger: Logger ) {} async blacklistToken(token: string, _expiresIn?: number): Promise { // Extract JWT payload to get expiry time try { const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64").toString()) as { exp?: number; }; const expiryTime = (payload.exp ?? 0) * 1000; // Convert to milliseconds const currentTime = Date.now(); const ttl = Math.max(0, Math.floor((expiryTime - currentTime) / 1000)); // Convert to seconds if (ttl > 0) { await this.redis.setex(`blacklist:${token}`, ttl, "1"); } } catch { // If we can't parse the token, blacklist it for the default JWT expiry time try { const defaultTtl = this.parseJwtExpiry(this.configService.get("JWT_EXPIRES_IN", "7d")); await this.redis.setex(`blacklist:${token}`, defaultTtl, "1"); } catch (err) { this.logger.warn( "Failed to write token to Redis blacklist; proceeding without persistence", { error: err instanceof Error ? err.message : String(err), } ); } } } async isTokenBlacklisted(token: string): Promise { try { const result = await this.redis.get(`blacklist:${token}`); return result !== null; } catch (err) { // If Redis is unavailable, treat as not blacklisted to avoid blocking auth this.logger.warn("Redis unavailable during blacklist check; allowing request", { error: err instanceof Error ? err.message : String(err), }); return false; } } private parseJwtExpiry(expiresIn: string): number { // Convert JWT expiry string to seconds const unit = expiresIn.slice(-1); const value = parseInt(expiresIn.slice(0, -1), 10); switch (unit) { case "s": return value; case "m": return value * 60; case "h": return value * 60 * 60; case "d": return value * 24 * 60 * 60; default: return 7 * 24 * 60 * 60; // Default 7 days in seconds } } }