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)); + } +}