mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
refactor(server,web): add/remove album users (#2681)
* refactor(server,web): add/remove album users * fix(web): bug fixes for multiple users * fix: linting
This commit is contained in:
@@ -1,18 +1,15 @@
|
||||
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
|
||||
import { AlbumEntity, AssetEntity } from '@app/infra/entities';
|
||||
import { dataSource } from '@app/infra/database.config';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||
import { AddUsersDto } from './dto/add-users.dto';
|
||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||
|
||||
export interface IAlbumRepository {
|
||||
get(albumId: string): Promise<AlbumEntity | null>;
|
||||
addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
|
||||
removeUser(album: AlbumEntity, userId: string): Promise<void>;
|
||||
removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<number>;
|
||||
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto>;
|
||||
updateThumbnails(): Promise<number | undefined>;
|
||||
@@ -25,11 +22,8 @@ export const IAlbumRepository = 'IAlbumRepository';
|
||||
@Injectable()
|
||||
export class AlbumRepository implements IAlbumRepository {
|
||||
constructor(
|
||||
@InjectRepository(AlbumEntity)
|
||||
private albumRepository: Repository<AlbumEntity>,
|
||||
|
||||
@InjectRepository(AssetEntity)
|
||||
private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
) {}
|
||||
|
||||
async getCountByUserId(userId: string): Promise<AlbumCountResponseDto> {
|
||||
@@ -59,22 +53,6 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity> {
|
||||
album.sharedUsers.push(...addUsersDto.sharedUserIds.map((id) => ({ id } as UserEntity)));
|
||||
album.updatedAt = new Date();
|
||||
|
||||
await this.albumRepository.save(album);
|
||||
|
||||
// need to re-load the shared user relation
|
||||
return this.get(album.id) as Promise<AlbumEntity>;
|
||||
}
|
||||
|
||||
async removeUser(album: AlbumEntity, userId: string): Promise<void> {
|
||||
album.sharedUsers = album.sharedUsers.filter((user) => user.id !== userId);
|
||||
album.updatedAt = new Date();
|
||||
await this.albumRepository.save(album);
|
||||
}
|
||||
|
||||
async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<number> {
|
||||
const assetCount = album.assets.length;
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Controller, Get, Post, Body, Param, Delete, Put, Query, Response } from '@nestjs/common';
|
||||
import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe';
|
||||
import { AlbumService } from './album.service';
|
||||
import { Authenticated, SharedLinkRoute } from '../../decorators/authenticated.decorator';
|
||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||
import { AddUsersDto } from './dto/add-users.dto';
|
||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AlbumResponseDto } from '@app/domain';
|
||||
@@ -29,12 +27,6 @@ export class AlbumController {
|
||||
return this.service.getCountByUserId(authUser);
|
||||
}
|
||||
|
||||
@Put(':id/users')
|
||||
addUsersToAlbum(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: AddUsersDto) {
|
||||
// TODO: Handle nonexistent sharedUserIds.
|
||||
return this.service.addUsers(authUser, id, dto);
|
||||
}
|
||||
|
||||
@SharedLinkRoute()
|
||||
@Put(':id/assets')
|
||||
addAssetsToAlbum(
|
||||
@@ -62,15 +54,6 @@ export class AlbumController {
|
||||
return this.service.removeAssets(authUser, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/user/:userId')
|
||||
removeUserFromAlbum(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string,
|
||||
) {
|
||||
return this.service.removeUser(authUser, id, userId);
|
||||
}
|
||||
|
||||
@SharedLinkRoute()
|
||||
@Get(':id/download')
|
||||
@ApiOkResponse({ content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AlbumService } from './album.service';
|
||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
import { NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
import { AlbumEntity, UserEntity } from '@app/infra/entities';
|
||||
import { AlbumResponseDto, ICryptoRepository, mapUser } from '@app/domain';
|
||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||
@@ -39,7 +39,6 @@ describe('Album service', () => {
|
||||
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
|
||||
const sharedAlbumOwnerId = '2222';
|
||||
const sharedAlbumSharedAlsoWithId = '3333';
|
||||
const ownedAlbumSharedWithId = '4444';
|
||||
|
||||
const _getOwnedAlbum = () => {
|
||||
const albumEntity = new AlbumEntity();
|
||||
@@ -56,25 +55,6 @@ describe('Album service', () => {
|
||||
return albumEntity;
|
||||
};
|
||||
|
||||
const _getOwnedSharedAlbum = () => {
|
||||
const albumEntity = new AlbumEntity();
|
||||
albumEntity.ownerId = albumOwner.id;
|
||||
albumEntity.owner = albumOwner;
|
||||
albumEntity.id = albumId;
|
||||
albumEntity.albumName = 'name';
|
||||
albumEntity.createdAt = new Date('2022-06-19T23:41:36.910Z');
|
||||
albumEntity.assets = [];
|
||||
albumEntity.albumThumbnailAssetId = null;
|
||||
albumEntity.sharedUsers = [
|
||||
{
|
||||
...userEntityStub.user1,
|
||||
id: ownedAlbumSharedWithId,
|
||||
},
|
||||
];
|
||||
|
||||
return albumEntity;
|
||||
};
|
||||
|
||||
const _getSharedWithAuthUserAlbum = () => {
|
||||
const albumEntity = new AlbumEntity();
|
||||
albumEntity.ownerId = sharedAlbumOwnerId;
|
||||
@@ -115,10 +95,8 @@ describe('Album service', () => {
|
||||
beforeAll(() => {
|
||||
albumRepositoryMock = {
|
||||
addAssets: jest.fn(),
|
||||
addSharedUsers: jest.fn(),
|
||||
get: jest.fn(),
|
||||
removeAssets: jest.fn(),
|
||||
removeUser: jest.fn(),
|
||||
updateThumbnails: jest.fn(),
|
||||
getCountByUserId: jest.fn(),
|
||||
getSharedWithUserAlbumCount: jest.fn(),
|
||||
@@ -188,53 +166,6 @@ describe('Album service', () => {
|
||||
await expect(sut.get(authUser, '0002')).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('removes a shared user from an owned album', async () => {
|
||||
const albumEntity = _getOwnedSharedAlbum();
|
||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||
albumRepositoryMock.removeUser.mockImplementation(() => Promise.resolve());
|
||||
await expect(sut.removeUser(authUser, albumEntity.id, ownedAlbumSharedWithId)).resolves.toBeUndefined();
|
||||
expect(albumRepositoryMock.removeUser).toHaveBeenCalledTimes(1);
|
||||
expect(albumRepositoryMock.removeUser).toHaveBeenCalledWith(albumEntity, ownedAlbumSharedWithId);
|
||||
});
|
||||
|
||||
it('prevents removing a shared user from a not owned album (shared with auth user)', async () => {
|
||||
const albumEntity = _getSharedWithAuthUserAlbum();
|
||||
const albumId = albumEntity.id;
|
||||
const userIdToRemove = sharedAlbumSharedAlsoWithId;
|
||||
|
||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||
|
||||
await expect(sut.removeUser(authUser, albumId, userIdToRemove)).rejects.toBeInstanceOf(ForbiddenException);
|
||||
expect(albumRepositoryMock.removeUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('removes itself from a shared album', async () => {
|
||||
const albumEntity = _getSharedWithAuthUserAlbum();
|
||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||
albumRepositoryMock.removeUser.mockImplementation(() => Promise.resolve());
|
||||
|
||||
await sut.removeUser(authUser, albumEntity.id, authUser.id);
|
||||
expect(albumRepositoryMock.removeUser).toHaveReturnedTimes(1);
|
||||
expect(albumRepositoryMock.removeUser).toHaveBeenCalledWith(albumEntity, authUser.id);
|
||||
});
|
||||
|
||||
it('removes itself from a shared album using "me" as id', async () => {
|
||||
const albumEntity = _getSharedWithAuthUserAlbum();
|
||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||
albumRepositoryMock.removeUser.mockImplementation(() => Promise.resolve());
|
||||
|
||||
await sut.removeUser(authUser, albumEntity.id, 'me');
|
||||
expect(albumRepositoryMock.removeUser).toHaveReturnedTimes(1);
|
||||
expect(albumRepositoryMock.removeUser).toHaveBeenCalledWith(albumEntity, authUser.id);
|
||||
});
|
||||
|
||||
it('prevents removing itself from a owned album', async () => {
|
||||
const albumEntity = _getOwnedAlbum();
|
||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
||||
|
||||
await expect(sut.removeUser(authUser, albumEntity.id, authUser.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('adds assets to owned album', async () => {
|
||||
const albumEntity = _getOwnedAlbum();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BadRequestException, Inject, Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
|
||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||
import { AlbumEntity, SharedLinkType } from '@app/infra/entities';
|
||||
import { AddUsersDto } from './dto/add-users.dto';
|
||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||
import { AlbumResponseDto, mapAlbum } from '@app/domain';
|
||||
import { IAlbumRepository } from './album-repository';
|
||||
@@ -63,24 +62,6 @@ export class AlbumService {
|
||||
return mapAlbum(album);
|
||||
}
|
||||
|
||||
async addUsers(authUser: AuthUserDto, albumId: string, dto: AddUsersDto): Promise<AlbumResponseDto> {
|
||||
const album = await this._getAlbum({ authUser, albumId });
|
||||
const updatedAlbum = await this.albumRepository.addSharedUsers(album, dto);
|
||||
return mapAlbum(updatedAlbum);
|
||||
}
|
||||
|
||||
async removeUser(authUser: AuthUserDto, albumId: string, userId: string | 'me'): Promise<void> {
|
||||
const sharedUserId = userId == 'me' ? authUser.id : userId;
|
||||
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
||||
if (album.ownerId != authUser.id && authUser.id != sharedUserId) {
|
||||
throw new ForbiddenException('Cannot remove a user from a album that is not owned');
|
||||
}
|
||||
if (album.ownerId == sharedUserId) {
|
||||
throw new BadRequestException('The owner of the album cannot be removed');
|
||||
}
|
||||
await this.albumRepository.removeUser(album, sharedUserId);
|
||||
}
|
||||
|
||||
async removeAssets(authUser: AuthUserDto, albumId: string, dto: RemoveAssetsDto): Promise<AlbumResponseDto> {
|
||||
const album = await this._getAlbum({ authUser, albumId });
|
||||
const deletedCount = await this.albumRepository.removeAssets(album, dto);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
|
||||
import { ValidateUUID } from '../../../../../../apps/immich/src/decorators/validate-uuid.decorator';
|
||||
|
||||
export class AddUsersDto {
|
||||
@ValidateUUID({ each: true })
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* */ import { AlbumService, AuthUserDto, CreateAlbumDto, UpdateAlbumDto } from '@app/domain';
|
||||
import { AddUsersDto, AlbumService, AuthUserDto, CreateAlbumDto, UpdateAlbumDto } from '@app/domain';
|
||||
import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe';
|
||||
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
||||
@@ -33,4 +34,18 @@ export class AlbumController {
|
||||
deleteAlbum(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
|
||||
return this.service.delete(authUser, id);
|
||||
}
|
||||
|
||||
@Put(':id/users')
|
||||
addUsersToAlbum(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: AddUsersDto) {
|
||||
return this.service.addUsers(authUser, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/user/:userId')
|
||||
removeUserFromAlbum(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string,
|
||||
) {
|
||||
return this.service.removeUser(authUser, id, userId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user