refactor( iam ): migrate session, logout, and guards to IAM

This commit is contained in:
Abubeker Yasin
2026-06-06 08:58:42 +03:00
parent d06debf1ca
commit 023522c3fb
7 changed files with 53 additions and 190 deletions

View File

@@ -1,7 +1,8 @@
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, UnauthorizedException } from '@nestjs/common';
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PrismaService } from '../prisma.service';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { ConfigService } from '@nestjs/config';
@Injectable()
@@ -9,7 +10,7 @@ export class SessionActivityInterceptor implements NestInterceptor {
private readonly inactivityMinutes: number;
constructor(
private readonly prisma: PrismaService,
@InjectDataSource() private readonly dataSource: DataSource,
private readonly config: ConfigService,
) {
this.inactivityMinutes = parseInt(this.config.get<string>('SESSION_INACTIVITY_MINUTES') || '30', 10);
@@ -18,29 +19,25 @@ export class SessionActivityInterceptor implements NestInterceptor {
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const user = request.user;
const sessionId: string | undefined = request.user?.sessionId;
if (user?.id) {
const session = await this.prisma.session.findFirst({
where: { userId: user.id },
orderBy: { lastActivityAt: 'desc' },
});
if (sessionId) {
const rows = await this.dataSource.query<Array<{ expiry_time: Date }>>(
`SELECT expiry_time FROM iam.sessions WHERE id = $1 AND status = 'ACTIVE' LIMIT 1`,
[sessionId],
);
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');
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());
}
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() },
});
// 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],
);
}
}

View File

@@ -1,6 +1,5 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRole } from '@prisma/client';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
@@ -8,12 +7,15 @@ export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user?.role === role);
// Support IAM roles array [{key, id}][] and legacy role string
return requiredRoles.some(
(role) => user?.roles?.some((r: { key: string }) => r.key === role) || user?.role === role,
);
}
}