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.
This commit is contained in:
Nick Szaunig committed 2026-08-23 11:44:52 +02:00
1 parent 6346bfab1d
commit 57979f0371
4 files changed
+68

No files matched your search

@@ -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);
}
}
@@ -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' }),
);
});
});
@@ -116,4 +116,24 @@ export class UsersService {
});
return this.toSummary(user);
}
async activate(id: string, actingUserId?: string): Promise<UserSummary> {
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);
}
}