import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { ConfigService } from '@nestjs/config'; @Injectable() export class SessionActivityInterceptor implements NestInterceptor { private readonly inactivityMinutes: number; constructor( @InjectDataSource() private readonly dataSource: DataSource, 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 sessionId: string | undefined = request.user?.sessionId; if (sessionId) { const rows = await this.dataSource.query>( `SELECT expiry_time FROM iam.sessions WHERE id = $1 AND status = 'ACTIVE' LIMIT 1`, [sessionId], ); if (rows.length) { const minutesLeft = (rows[0].expiry_time.getTime() - Date.now()) / 60000; if (minutesLeft < this.inactivityMinutes * 0.2) { response.setHeader('X-Session-Expiry-Warning', Math.floor(minutesLeft).toString()); } // Extend session on every authenticated request await this.dataSource.query( `UPDATE iam.sessions SET expiry_time = NOW() + ($1 * INTERVAL '1 minute') WHERE id = $2 AND status = 'ACTIVE'`, [this.inactivityMinutes, sessionId], ); } } return next.handle().pipe(tap(() => {})); } }