feat(api): invalidate sessions on user deactivation (MS21-AUTH-004) #582

Merged
jason.woltje merged 1 commits from feat/ms21-session-invalidation into main 2026-02-28 23:41:11 +00:00
2 changed files with 25 additions and 16 deletions

View File

@@ -24,7 +24,15 @@ describe("AdminService", () => {
workspaceMember: { workspaceMember: {
create: vi.fn(), create: vi.fn(),
}, },
$transaction: vi.fn(), session: {
deleteMany: vi.fn(),
},
$transaction: vi.fn(async (ops) => {
if (typeof ops === "function") {
return ops(mockPrismaService);
}
return Promise.all(ops);
}),
}; };
const mockAdminId = "550e8400-e29b-41d4-a716-446655440001"; const mockAdminId = "550e8400-e29b-41d4-a716-446655440001";
@@ -82,10 +90,6 @@ describe("AdminService", () => {
service = module.get<AdminService>(AdminService); service = module.get<AdminService>(AdminService);
vi.clearAllMocks(); vi.clearAllMocks();
mockPrismaService.$transaction.mockImplementation(async (fn: (tx: unknown) => unknown) => {
return fn(mockPrismaService);
});
}); });
it("should be defined", () => { it("should be defined", () => {
@@ -325,12 +329,13 @@ describe("AdminService", () => {
}); });
describe("deactivateUser", () => { describe("deactivateUser", () => {
it("should set deactivatedAt on the user", async () => { it("should set deactivatedAt and invalidate sessions", async () => {
mockPrismaService.user.findUnique.mockResolvedValue(mockUser); mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
mockPrismaService.user.update.mockResolvedValue({ mockPrismaService.user.update.mockResolvedValue({
...mockUser, ...mockUser,
deactivatedAt: new Date(), deactivatedAt: new Date(),
}); });
mockPrismaService.session.deleteMany.mockResolvedValue({ count: 3 });
const result = await service.deactivateUser(mockUserId); const result = await service.deactivateUser(mockUserId);
@@ -341,6 +346,7 @@ describe("AdminService", () => {
data: { deactivatedAt: expect.any(Date) }, data: { deactivatedAt: expect.any(Date) },
}) })
); );
expect(mockPrismaService.session.deleteMany).toHaveBeenCalledWith({ where: { userId: mockUserId } });
}); });
it("should throw NotFoundException if user does not exist", async () => { it("should throw NotFoundException if user does not exist", async () => {

View File

@@ -192,19 +192,22 @@ export class AdminService {
throw new BadRequestException(`User ${id} is already deactivated`); throw new BadRequestException(`User ${id} is already deactivated`);
} }
const user = await this.prisma.user.update({ const [user] = await this.prisma.$transaction([
where: { id }, this.prisma.user.update({
data: { deactivatedAt: new Date() }, where: { id },
include: { data: { deactivatedAt: new Date() },
workspaceMemberships: { include: {
include: { workspaceMemberships: {
workspace: { select: { id: true, name: true } }, include: {
workspace: { select: { id: true, name: true } },
},
}, },
}, },
}, }),
}); this.prisma.session.deleteMany({ where: { userId: id } }),
]);
this.logger.log(`User deactivated: ${id}`); this.logger.log(`User deactivated and sessions invalidated: ${id}`);
return { return {
id: user.id, id: user.id,