From 57979f03710e2e1728b39f36806d397cdcc13668 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sun, 23 Aug 2026 11:44:52 +0200 Subject: [PATCH] feat(backend): activate endpoint for deactivated users Symmetrisch zu Modul-27/28-Fix fuer Colony/Apiary: kein Weg, einen versehentlich deaktivierten Benutzer wieder zu aktivieren. Neuer POST :id/activate-Endpunkt setzt isActive zurueck auf true und loescht deactivatedAt; ConflictException wenn bereits aktiv. --- ApiCore-Backend/src/users/users.controller.ts | 5 ++++ .../src/users/users.service.spec.ts | 28 +++++++++++++++++++ ApiCore-Backend/src/users/users.service.ts | 20 +++++++++++++ ApiCore-Backend/test/users.e2e-spec.ts | 15 ++++++++++ 4 files changed, 68 insertions(+) diff --git a/ApiCore-Backend/src/users/users.controller.ts b/ApiCore-Backend/src/users/users.controller.ts index 4f70815..eefcc1b 100644 --- a/ApiCore-Backend/src/users/users.controller.ts +++ b/ApiCore-Backend/src/users/users.controller.ts @@ -43,4 +43,9 @@ export class UsersController { deactivate(@Param('id') id: string, @CurrentUser() actor: AuthenticatedUser) { return this.usersService.deactivate(id, actor.id); } + + @Post(':id/activate') + activate(@Param('id') id: string, @CurrentUser() actor: AuthenticatedUser) { + return this.usersService.activate(id, actor.id); + } } diff --git a/ApiCore-Backend/src/users/users.service.spec.ts b/ApiCore-Backend/src/users/users.service.spec.ts index 5b508fc..f6ae9de 100644 --- a/ApiCore-Backend/src/users/users.service.spec.ts +++ b/ApiCore-Backend/src/users/users.service.spec.ts @@ -122,4 +122,32 @@ describe('UsersService', () => { expect.objectContaining({ entityType: 'User', action: 'DEACTIVATE' }), ); }); + + it('activate throws ConflictException when already active', async () => { + prisma.user.findUnique.mockResolvedValue(existingUser); + await expect(usersService.activate(existingUser.id)).rejects.toBeInstanceOf( + ConflictException, + ); + }); + + it('activate sets isActive to true and clears deactivatedAt', async () => { + prisma.user.findUnique.mockResolvedValue({ + ...existingUser, + isActive: false, + deactivatedAt: new Date(), + }); + prisma.user.update.mockResolvedValue({ + ...existingUser, + isActive: true, + deactivatedAt: null, + }); + + const result = await usersService.activate(existingUser.id); + + expect(result.isActive).toBe(true); + expect(result.deactivatedAt).toBeNull(); + expect(auditLog.record).toHaveBeenCalledWith( + expect.objectContaining({ entityType: 'User', action: 'UPDATE' }), + ); + }); }); diff --git a/ApiCore-Backend/src/users/users.service.ts b/ApiCore-Backend/src/users/users.service.ts index 19b2e28..1887ca9 100644 --- a/ApiCore-Backend/src/users/users.service.ts +++ b/ApiCore-Backend/src/users/users.service.ts @@ -116,4 +116,24 @@ export class UsersService { }); return this.toSummary(user); } + + async activate(id: string, actingUserId?: string): Promise { + const before = await this.findById(id); + if (before.isActive) { + throw new ConflictException('User is already active'); + } + const user = await this.prisma.user.update({ + where: { id }, + data: { isActive: true, deactivatedAt: null }, + }); + await this.auditLog.record({ + entityType: 'User', + entityId: id, + action: 'UPDATE', + userId: actingUserId, + beforeData: this.toSummary(before), + afterData: this.toSummary(user), + }); + return this.toSummary(user); + } } diff --git a/ApiCore-Backend/test/users.e2e-spec.ts b/ApiCore-Backend/test/users.e2e-spec.ts index a5e7b3f..fb8c31f 100644 --- a/ApiCore-Backend/test/users.e2e-spec.ts +++ b/ApiCore-Backend/test/users.e2e-spec.ts @@ -99,4 +99,19 @@ describe('Users (e2e)', () => { expect(deactivated.isActive).toBe(false); expect(deactivated.deactivatedAt).not.toBeNull(); }); + + it('lets an admin reactivate a deactivated user, and rejects reactivating an already-active one', async () => { + const activated = await request(app.getHttpServer()) + .post(`/api/v1/users/${createdUserId}/activate`) + .set('Authorization', `Bearer ${adminAccessToken}`) + .expect(201); + const body = activated.body as UserBody; + expect(body.isActive).toBe(true); + expect(body.deactivatedAt).toBeNull(); + + await request(app.getHttpServer()) + .post(`/api/v1/users/${createdUserId}/activate`) + .set('Authorization', `Bearer ${adminAccessToken}`) + .expect(409); + }); });