1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-04-21 15:36:26 +02:00

fix(server): restore user ()

This commit is contained in:
Jason Rasmussen 2025-01-29 11:49:08 -05:00 committed by GitHub
parent 9033a99587
commit a0aea021a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 39 additions and 6 deletions

View file

@ -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,
}),
);
});
});
});

View file

@ -539,7 +539,7 @@
}
],
"responses": {
"201": {
"200": {
"content": {
"application/json": {
"schema": {

View file

@ -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,

View file

@ -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<UserAdminResponseDto> {
return this.service.restore(auth, id);
}

View file

@ -36,6 +36,7 @@ export interface IUserRepository {
getUserStats(): Promise<UserStatsQueryResponse[]>;
create(user: Insertable<Users>): Promise<UserEntity>;
update(id: string, user: Updateable<Users>): Promise<UserEntity>;
restore(id: string): Promise<UserEntity>;
upsertMetadata<T extends keyof UserMetadata>(id: string, item: { key: T; value: UserMetadata[T] }): Promise<void>;
deleteMetadata<T extends keyof UserMetadata>(id: string, key: T): Promise<void>;
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;

View file

@ -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<UserEntity>;
}
restore(id: string): Promise<UserEntity> {
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<UserEntity>;
}
async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) {
await this.db
.insertInto('user_metadata')

View file

@ -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);
});
});
});

View file

@ -102,7 +102,7 @@ export class UserAdminService extends BaseService {
async restore(auth: AuthDto, id: string): Promise<UserAdminResponseDto> {
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);
}

View file

@ -13,6 +13,7 @@ export const newUserRepositoryMock = (): Mocked<IUserRepository> => {
create: vitest.fn(),
update: vitest.fn(),
delete: vitest.fn(),
restore: vitest.fn(),
getDeletedUsers: vitest.fn(),
hasAdmin: vitest.fn(),
updateUsage: vitest.fn(),