import { Injectable, NestInterceptor, ExecutionContext, CallHandler, UnauthorizedException } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { PrismaService } from '../prisma.service'; import { ConfigService } from '@nestjs/config'; @Injectable() export class SessionActivityInterceptor implements NestInterceptor { private readonly inactivityMinutes: number; constructor( private readonly prisma: PrismaService, private readonly config: ConfigService, ) { this.inactivityMinutes = parseInt(this.config.get('SESSION_INACTIVITY_MINUTES') || '30', 10); } async intercept(context: ExecutionContext, next: CallHandler): Promise> { const request = context.switchToHttp().getRequest(); const response = context.switchToHttp().getResponse(); const user = request.user; if (user?.userId) { const session = await this.prisma.session.findFirst({ where: { userId: user.userId }, orderBy: { lastActivityAt: 'desc' }, }); if (session) { const inactiveMinutes = (Date.now() - session.lastActivityAt.getTime()) / 60000; if (inactiveMinutes > this.inactivityMinutes) { await this.prisma.session.delete({ where: { id: session.id } }); throw new UnauthorizedException('Session expired due to inactivity'); } const expiryWarningMinutes = Math.max(0, this.inactivityMinutes - inactiveMinutes); response.setHeader('X-Session-Expiry-Warning', Math.floor(expiryWarningMinutes).toString()); await this.prisma.session.update({ where: { id: session.id }, data: { lastActivityAt: new Date() }, }); } } return next.handle().pipe(tap(() => {})); } }