From a0836c83923bccc72112d3167987717c573abbd4 Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:27:02 +0100 Subject: [PATCH] feat: add SessionsModule with service for session management Implemented SessionsModule and SessionsService to manage user sessions. Includes methods for session creation, refresh token rotation, and session revocation. Integrated with database and CryptoService for secure token handling. --- backend/src/sessions/sessions.module.ts | 11 +++ backend/src/sessions/sessions.service.ts | 88 ++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 backend/src/sessions/sessions.module.ts create mode 100644 backend/src/sessions/sessions.service.ts diff --git a/backend/src/sessions/sessions.module.ts b/backend/src/sessions/sessions.module.ts new file mode 100644 index 0000000..0df6df5 --- /dev/null +++ b/backend/src/sessions/sessions.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { CryptoModule } from "../crypto/crypto.module"; +import { DatabaseModule } from "../database/database.module"; +import { SessionsService } from "./sessions.service"; + +@Module({ + imports: [DatabaseModule, CryptoModule], + providers: [SessionsService], + exports: [SessionsService], +}) +export class SessionsModule {} diff --git a/backend/src/sessions/sessions.service.ts b/backend/src/sessions/sessions.service.ts new file mode 100644 index 0000000..d9546b2 --- /dev/null +++ b/backend/src/sessions/sessions.service.ts @@ -0,0 +1,88 @@ +import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { and, eq } from "drizzle-orm"; +import { CryptoService } from "../crypto/crypto.service"; +import { DatabaseService } from "../database/database.service"; +import { sessions } from "../database/schemas"; + +@Injectable() +export class SessionsService { + constructor( + private readonly databaseService: DatabaseService, + private readonly cryptoService: CryptoService, + ) {} + + async createSession(userId: string, userAgent?: string, ip?: string) { + const refreshToken = await this.cryptoService.generateJwt( + { sub: userId, type: "refresh" }, + "7d", + ); + const ipHash = ip ? await this.cryptoService.hashIp(ip) : null; + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 7); + + const [session] = await this.databaseService.db + .insert(sessions) + .values({ + userId, + refreshToken, + userAgent, + ipHash, + expiresAt, + }) + .returning(); + + return session; + } + + async refreshSession(oldRefreshToken: string) { + const session = await this.databaseService.db + .select() + .from(sessions) + .where( + and(eq(sessions.refreshToken, oldRefreshToken), eq(sessions.isValid, true)), + ) + .limit(1) + .then((res) => res[0]); + + if (!session || session.expiresAt < new Date()) { + if (session) { + await this.revokeSession(session.id); + } + throw new UnauthorizedException("Invalid refresh token"); + } + + // Rotation du refresh token + const newRefreshToken = await this.cryptoService.generateJwt( + { sub: session.userId, type: "refresh" }, + "7d", + ); + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 7); + + const [updatedSession] = await this.databaseService.db + .update(sessions) + .set({ + refreshToken: newRefreshToken, + expiresAt, + updatedAt: new Date(), + }) + .where(eq(sessions.id, session.id)) + .returning(); + + return updatedSession; + } + + async revokeSession(sessionId: string) { + await this.databaseService.db + .update(sessions) + .set({ isValid: false, updatedAt: new Date() }) + .where(eq(sessions.id, sessionId)); + } + + async revokeAllUserSessions(userId: string) { + await this.databaseService.db + .update(sessions) + .set({ isValid: false, updatedAt: new Date() }) + .where(eq(sessions.userId, userId)); + } +}