From a0aea021a1041efe438e877963baac07183b7a34 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 29 Jan 2025 11:49:08 -0500 Subject: [PATCH] fix(server): restore user (#15763) --- e2e/src/api/specs/user-admin.e2e-spec.ts | 19 +++++++++++++++++++ open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- .../src/controllers/user-admin.controller.ts | 3 ++- server/src/interfaces/user.interface.ts | 1 + server/src/repositories/user.repository.ts | 11 +++++++++++ .../src/services/user-admin.service.spec.ts | 4 ++-- server/src/services/user-admin.service.ts | 2 +- .../test/repositories/user.repository.mock.ts | 1 + 9 files changed, 39 insertions(+), 6 deletions(-) diff --git a/e2e/src/api/specs/user-admin.e2e-spec.ts b/e2e/src/api/specs/user-admin.e2e-spec.ts index 8a417387e7d..9299e62b79a 100644 --- a/e2e/src/api/specs/user-admin.e2e-spec.ts +++ b/e2e/src/api/specs/user-admin.e2e-spec.ts @@ -356,5 +356,24 @@ describe('/admin/users', () => { expect(status).toBe(403); expect(body).toEqual(errorDto.forbidden); }); + + it('should restore a user', async () => { + const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore')); + + await deleteUserAdmin({ id: user.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }); + + const { status, body } = await request(app) + .post(`/admin/users/${user.userId}/restore`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual( + expect.objectContaining({ + id: user.userId, + email: user.userEmail, + status: 'active', + deletedAt: null, + }), + ); + }); }); }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 3067b254494..351d865608c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -539,7 +539,7 @@ } ], "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 088e30f9d8b..d42725a2356 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1475,7 +1475,7 @@ export function restoreUserAdmin({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ - status: 201; + status: 200; data: UserAdminResponseDto; }>(`/admin/users/${encodeURIComponent(id)}/restore`, { ...opts, diff --git a/server/src/controllers/user-admin.controller.ts b/server/src/controllers/user-admin.controller.ts index d44115be2fb..4dfeae949a8 100644 --- a/server/src/controllers/user-admin.controller.ts +++ b/server/src/controllers/user-admin.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; @@ -75,6 +75,7 @@ export class UserAdminController { @Post(':id/restore') @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) + @HttpCode(HttpStatus.OK) restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.restore(auth, id); } diff --git a/server/src/interfaces/user.interface.ts b/server/src/interfaces/user.interface.ts index 6ff3fc824aa..f126b6eb34a 100644 --- a/server/src/interfaces/user.interface.ts +++ b/server/src/interfaces/user.interface.ts @@ -36,6 +36,7 @@ export interface IUserRepository { getUserStats(): Promise; create(user: Insertable): Promise; update(id: string, user: Updateable): Promise; + restore(id: string): Promise; upsertMetadata(id: string, item: { key: T; value: UserMetadata[T] }): Promise; deleteMetadata(id: string, key: T): Promise; delete(user: UserEntity, hard?: boolean): Promise; diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index e7c65b3f017..417ee141f4d 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -5,6 +5,7 @@ import { DB, UserMetadata as DbUserMetadata, Users } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { UserMetadata } from 'src/entities/user-metadata.entity'; import { UserEntity, withMetadata } from 'src/entities/user.entity'; +import { UserStatus } from 'src/enum'; import { IUserRepository, UserFindOptions, @@ -140,6 +141,16 @@ export class UserRepository implements IUserRepository { .executeTakeFirst() as unknown as Promise; } + restore(id: string): Promise { + return this.db + .updateTable('users') + .set({ status: UserStatus.ACTIVE, deletedAt: null }) + .where('users.id', '=', asUuid(id)) + .returning(columns) + .returning(withMetadata) + .executeTakeFirst() as unknown as Promise; + } + async upsertMetadata(id: string, { key, value }: { key: T; value: UserMetadata[T] }) { await this.db .insertInto('user_metadata') diff --git a/server/src/services/user-admin.service.spec.ts b/server/src/services/user-admin.service.spec.ts index 6d2bc31cb70..b14f1c86559 100644 --- a/server/src/services/user-admin.service.spec.ts +++ b/server/src/services/user-admin.service.spec.ts @@ -173,9 +173,9 @@ describe(UserAdminService.name, () => { it('should restore an user', async () => { userMock.get.mockResolvedValue(userStub.user1); - userMock.update.mockResolvedValue(userStub.user1); + userMock.restore.mockResolvedValue(userStub.user1); await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUserAdmin(userStub.user1)); - expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { status: UserStatus.ACTIVE, deletedAt: null }); + expect(userMock.restore).toHaveBeenCalledWith(userStub.user1.id); }); }); }); diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index a4be671c223..784c95954ea 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -102,7 +102,7 @@ export class UserAdminService extends BaseService { async restore(auth: AuthDto, id: string): Promise { await this.findOrFail(id, { withDeleted: true }); await this.albumRepository.restoreAll(id); - const user = await this.userRepository.update(id, { deletedAt: null, status: UserStatus.ACTIVE }); + const user = await this.userRepository.restore(id); return mapUserAdmin(user); } diff --git a/server/test/repositories/user.repository.mock.ts b/server/test/repositories/user.repository.mock.ts index 6362ab6a999..e6e8c38184a 100644 --- a/server/test/repositories/user.repository.mock.ts +++ b/server/test/repositories/user.repository.mock.ts @@ -13,6 +13,7 @@ export const newUserRepositoryMock = (): Mocked => { create: vitest.fn(), update: vitest.fn(), delete: vitest.fn(), + restore: vitest.fn(), getDeletedUsers: vitest.fn(), hasAdmin: vitest.fn(), updateUsage: vitest.fn(),