import { Inject, Injectable } from "@nestjs/common"; import { PrismaService } from "../../common/prisma.service.js"; import { AuditService } from "../../common/audit.service.js"; import { ApiError } from "../../common/errors.js"; import { parsePagination } from "../../lib/pagination.js"; @Injectable() export class SessionsService { constructor( @Inject(PrismaService) private readonly prisma: PrismaService, @Inject(AuditService) private readonly audit: AuditService ) {} /** Active sessions for the current user (most recently active first). */ async list(userId: string, query: { cursor?: string; limit?: string }) { const { cursor, limit } = parsePagination(query); const sessions = await this.prisma.session.findMany({ where: { userId, status: "ACTIVE", expiresAt: { gt: new Date() } }, ...(cursor ? { cursor: { id: cursor.id }, skip: 1 } : {}), orderBy: { createdAt: "desc" }, take: limit + 1 }); const page = sessions.slice(0, limit); const next = sessions.length > limit ? page[page.length - 1] : undefined; return { data: page.map((s) => ({ id: s.id, userAgent: s.userAgent, ipAddress: s.ipAddress, status: s.status, isCurrent: false, createdAt: s.createdAt, expiresAt: s.expiresAt })), nextCursor: next ? Buffer.from(JSON.stringify({ id: next.id, position: next.id })).toString("base64url") : null }; } /** Revoke one session (idempotent; 404 only when the id is unknown). */ async revoke(userId: string, sessionId: string, actor: { req?: any }) { const session = await this.prisma.session.findFirst({ where: { id: sessionId, userId } }); if (!session) throw ApiError.notFound("Session not found."); if (session.status === "ACTIVE") { await this.prisma.$transaction([ this.prisma.session.update({ where: { id: session.id }, data: { status: "REVOKED", revokedAt: new Date() } }), this.prisma.refreshToken.updateMany({ where: { userId, sessionAgent: session.id, revokedAt: null }, data: { revokedAt: new Date() } }) ]); } if (actor?.req) { await this.audit.audit(actor.req, actor.req.auth, { action: "session.revoked", resourceType: "session", resourceId: session.id, metadata: { targetUserId: userId } }); } } /** Revoke all sessions for the user. */ async revokeAll(userId: string) { const result = await this.prisma.$transaction([ this.prisma.session.updateMany({ where: { userId, status: "ACTIVE" }, data: { status: "REVOKED", revokedAt: new Date() } }), this.prisma.refreshToken.updateMany({ where: { userId, revokedAt: null }, data: { revokedAt: new Date() } }) ]); return { revoked: result[0].count, refreshTokens: result[1].count }; } }