1import { Inject, Injectable } from "@nestjs/common";
2import { PrismaService } from "../../common/prisma.service.js";
3import { AuditService } from "../../common/audit.service.js";
4import { ApiError } from "../../common/errors.js";
5import { parsePagination } from "../../lib/pagination.js";
6
7@Injectable()
8export class SessionsService {
9 constructor(
10 @Inject(PrismaService) private readonly prisma: PrismaService,
11 @Inject(AuditService) private readonly audit: AuditService
12 ) {}
13
14
15 async list(userId: string, query: { cursor?: string; limit?: string }) {
16 const { cursor, limit } = parsePagination(query);
17 const sessions = await this.prisma.session.findMany({
18 where: { userId, status: "ACTIVE", expiresAt: { gt: new Date() } },
19 ...(cursor ? { cursor: { id: cursor.id }, skip: 1 } : {}),
20 orderBy: { createdAt: "desc" },
21 take: limit + 1
22 });
23 const page = sessions.slice(0, limit);
24 const next = sessions.length > limit ? page[page.length - 1] : undefined;
25 return {
26 data: page.map((s) => ({
27 id: s.id,
28 userAgent: s.userAgent,
29 ipAddress: s.ipAddress,
30 status: s.status,
31 isCurrent: false,
32 createdAt: s.createdAt,
33 expiresAt: s.expiresAt
34 })),
35 nextCursor: next ? Buffer.from(JSON.stringify({ id: next.id, position: next.id })).toString("base64url") : null
36 };
37 }
38
39
40 async revoke(userId: string, sessionId: string, actor: { req?: any }) {
41 const session = await this.prisma.session.findFirst({ where: { id: sessionId, userId } });
42 if (!session) throw ApiError.notFound("Session not found.");
43 if (session.status === "ACTIVE") {
44 await this.prisma.$transaction([
45 this.prisma.session.update({ where: { id: session.id }, data: { status: "REVOKED", revokedAt: new Date() } }),
46 this.prisma.refreshToken.updateMany({
47 where: { userId, sessionAgent: session.id, revokedAt: null },
48 data: { revokedAt: new Date() }
49 })
50 ]);
51 }
52 if (actor?.req) {
53 await this.audit.audit(actor.req, actor.req.auth, {
54 action: "session.revoked",
55 resourceType: "session",
56 resourceId: session.id,
57 metadata: { targetUserId: userId }
58 });
59 }
60 }
61
62
63 async revokeAll(userId: string) {
64 const result = await this.prisma.$transaction([
65 this.prisma.session.updateMany({
66 where: { userId, status: "ACTIVE" },
67 data: { status: "REVOKED", revokedAt: new Date() }
68 }),
69 this.prisma.refreshToken.updateMany({
70 where: { userId, revokedAt: null },
71 data: { revokedAt: new Date() }
72 })
73 ]);
74 return { revoked: result[0].count, refreshTokens: result[1].count };
75 }
76}
77