1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

refactor(server): shared links (#2632)

* refactor: rename share => shared-link

* refactor: shared link crud methods

* chore: open api
This commit is contained in:
Jason Rasmussen 2023-06-01 22:09:57 -04:00 committed by GitHub
parent 038e064e60
commit 3ea2fe1c48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 391 additions and 420 deletions

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -10,13 +10,19 @@ import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
import { AddAssetsDto } from './dto/add-assets.dto';
import { DownloadService } from '../../modules/download/download.service';
import { DownloadDto } from '../asset/dto/download-library.dto';
import { ShareCore, ISharedLinkRepository, mapSharedLink, SharedLinkResponseDto, ICryptoRepository } from '@app/domain';
import {
SharedLinkCore,
ISharedLinkRepository,
mapSharedLink,
SharedLinkResponseDto,
ICryptoRepository,
} from '@app/domain';
import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto';
@Injectable()
export class AlbumService {
readonly logger = new Logger(AlbumService.name);
private shareCore: ShareCore;
private shareCore: SharedLinkCore;
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@ -25,7 +31,7 @@ export class AlbumService {
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
) {
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
this.shareCore = new SharedLinkCore(sharedLinkRepository, cryptoRepository);
}
private async _getAlbum({

View File

@ -229,7 +229,7 @@ describe('AssetService', () => {
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled();
expect(sharedLinkRepositoryMock.update).not.toHaveBeenCalled();
});
it('should add assets to a shared link', async () => {
@ -241,13 +241,13 @@ describe('AssetService', () => {
assetRepositoryMock.getById.mockResolvedValue(asset1);
sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
sharedLinkRepositoryMock.update.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.addAssetsToSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
expect(sharedLinkRepositoryMock.update).toHaveBeenCalled();
});
it('should remove assets from a shared link', async () => {
@ -259,13 +259,13 @@ describe('AssetService', () => {
assetRepositoryMock.getById.mockResolvedValue(asset1);
sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
sharedLinkRepositoryMock.update.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.removeAssetsFromSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
expect(sharedLinkRepositoryMock.update).toHaveBeenCalled();
});
});

View File

@ -54,7 +54,7 @@ import { ICryptoRepository, IJobRepository } from '@app/domain';
import { DownloadService } from '../../modules/download/download.service';
import { DownloadDto } from './dto/download-library.dto';
import { IAlbumRepository } from '../album/album-repository';
import { ShareCore } from '@app/domain';
import { SharedLinkCore } from '@app/domain';
import { IPartnerRepository } from '@app/domain';
import { ISharedLinkRepository } from '@app/domain';
import { DownloadFilesDto } from './dto/download-files.dto';
@ -80,7 +80,7 @@ interface ServableFile {
@Injectable()
export class AssetService {
readonly logger = new Logger(AssetService.name);
private shareCore: ShareCore;
private shareCore: SharedLinkCore;
private assetCore: AssetCore;
private partnerCore: PartnerCore;
@ -97,7 +97,7 @@ export class AssetService {
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
) {
this.assetCore = new AssetCore(_assetRepository, jobRepository);
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
this.shareCore = new SharedLinkCore(sharedLinkRepository, cryptoRepository);
this.partnerCore = new PartnerCore(partnerRepository);
}

View File

@ -1,4 +1,4 @@
import { AuthUserDto, EditSharedLinkDto, SharedLinkResponseDto, ShareService } from '@app/domain';
import { AuthUserDto, EditSharedLinkDto, SharedLinkResponseDto, SharedLinkService } from '@app/domain';
import { Body, Controller, Delete, Get, Param, Patch } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { GetAuthUser } from '../decorators/auth-user.decorator';
@ -11,7 +11,7 @@ import { UUIDParamDto } from './dto/uuid-param.dto';
@Authenticated()
@UseValidation()
export class SharedLinkController {
constructor(private readonly service: ShareService) {}
constructor(private readonly service: SharedLinkService) {}
@Get()
getAllSharedLinks(@GetAuthUser() authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
@ -29,20 +29,20 @@ export class SharedLinkController {
@GetAuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
): Promise<SharedLinkResponseDto> {
return this.service.getById(authUser, id, true);
return this.service.get(authUser, id);
}
@Patch(':id')
updateSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: EditSharedLinkDto,
): Promise<SharedLinkResponseDto> {
return this.service.update(authUser, id, dto);
}
@Delete(':id')
removeSharedLink(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id);
}
@Patch(':id')
editSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: EditSharedLinkDto,
): Promise<SharedLinkResponseDto> {
return this.service.edit(authUser, id, dto);
}
}

View File

@ -1565,41 +1565,8 @@
}
]
},
"delete": {
"operationId": "removeSharedLink",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"share"
],
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
]
},
"patch": {
"operationId": "editSharedLink",
"operationId": "updateSharedLink",
"parameters": [
{
"name": "id",
@ -1647,6 +1614,39 @@
"api_key": []
}
]
},
"delete": {
"operationId": "removeSharedLink",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"share"
],
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
]
}
},
"/system-config": {

View File

@ -20,7 +20,7 @@ import {
} from '../../test';
import { IKeyRepository } from '../api-key';
import { ICryptoRepository } from '../crypto/crypto.repository';
import { ISharedLinkRepository } from '../share';
import { ISharedLinkRepository } from '../shared-link';
import { ISystemConfigRepository } from '../system-config';
import { IUserRepository } from '../user';
import { IUserTokenRepository } from '../user-token';

View File

@ -18,7 +18,7 @@ import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, SignUpDto } from '.
import { AdminSignupResponseDto, LoginResponseDto, LogoutResponseDto, mapAdminSignupResponse } from './response-dto';
import { IUserTokenRepository, UserTokenCore } from '../user-token';
import cookieParser from 'cookie';
import { ISharedLinkRepository, ShareCore } from '../share';
import { ISharedLinkRepository, SharedLinkCore } from '../shared-link';
import { APIKeyCore } from '../api-key/api-key.core';
import { IKeyRepository } from '../api-key';
import { AuthDeviceResponseDto, mapUserToken } from './response-dto';
@ -29,7 +29,7 @@ export class AuthService {
private authCore: AuthCore;
private oauthCore: OAuthCore;
private userCore: UserCore;
private shareCore: ShareCore;
private shareCore: SharedLinkCore;
private keyCore: APIKeyCore;
private logger = new Logger(AuthService.name);
@ -48,7 +48,7 @@ export class AuthService {
this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
this.oauthCore = new OAuthCore(configRepository, initialConfig);
this.userCore = new UserCore(userRepository, cryptoRepository);
this.shareCore = new ShareCore(shareRepository, cryptoRepository);
this.shareCore = new SharedLinkCore(shareRepository, cryptoRepository);
this.keyCore = new APIKeyCore(cryptoRepository, keyRepository);
}

View File

@ -12,7 +12,7 @@ import { PartnerService } from './partner';
import { PersonService } from './person';
import { SearchService } from './search';
import { ServerInfoService } from './server-info';
import { ShareService } from './share';
import { SharedLinkService } from './shared-link';
import { SmartInfoService } from './smart-info';
import { StorageService } from './storage';
import { StorageTemplateService } from './storage-template';
@ -34,7 +34,7 @@ const providers: Provider[] = [
PartnerService,
SearchService,
ServerInfoService,
ShareService,
SharedLinkService,
SmartInfoService,
StorageService,
StorageTemplateService,

View File

@ -17,7 +17,7 @@ export * from './person';
export * from './search';
export * from './server-info';
export * from './partner';
export * from './share';
export * from './shared-link';
export * from './smart-info';
export * from './storage';
export * from './storage-template';

View File

@ -1,127 +0,0 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import {
authStub,
newCryptoRepositoryMock,
newSharedLinkRepositoryMock,
sharedLinkResponseStub,
sharedLinkStub,
} from '../../test';
import { ICryptoRepository } from '../crypto';
import { ShareService } from './share.service';
import { ISharedLinkRepository } from './shared-link.repository';
describe(ShareService.name, () => {
let sut: ShareService;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let shareMock: jest.Mocked<ISharedLinkRepository>;
beforeEach(async () => {
cryptoMock = newCryptoRepositoryMock();
shareMock = newSharedLinkRepositoryMock();
sut = new ShareService(cryptoMock, shareMock);
});
it('should work', () => {
expect(sut).toBeDefined();
});
describe('getAll', () => {
it('should return all keys for a user', async () => {
shareMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
await expect(sut.getAll(authStub.user1)).resolves.toEqual([
sharedLinkResponseStub.expired,
sharedLinkResponseStub.valid,
]);
expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
});
});
describe('getMine', () => {
it('should only work for a public user', async () => {
await expect(sut.getMine(authStub.admin)).rejects.toBeInstanceOf(ForbiddenException);
expect(shareMock.get).not.toHaveBeenCalled();
});
it('should return the key for the public user (auth dto)', async () => {
const authDto = authStub.adminSharedLink;
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
});
});
describe('get', () => {
it('should not work on a missing key', async () => {
shareMock.get.mockResolvedValue(null);
await expect(sut.getById(authStub.user1, sharedLinkStub.valid.id, true)).rejects.toBeInstanceOf(
BadRequestException,
);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
expect(shareMock.remove).not.toHaveBeenCalled();
});
it('should get a key by id', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.getById(authStub.user1, sharedLinkStub.valid.id, false)).resolves.toEqual(
sharedLinkResponseStub.valid,
);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
});
it('should include exif', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.readonly);
await expect(sut.getById(authStub.user1, sharedLinkStub.readonly.id, true)).resolves.toEqual(
sharedLinkResponseStub.readonly,
);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.readonly.id);
});
it('should exclude exif', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.readonly);
await expect(sut.getById(authStub.user1, sharedLinkStub.readonly.id, false)).resolves.toEqual(
sharedLinkResponseStub.readonlyNoExif,
);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.readonly.id);
});
});
describe('remove', () => {
it('should not work on a missing key', async () => {
shareMock.get.mockResolvedValue(null);
await expect(sut.remove(authStub.user1, sharedLinkStub.valid.id)).rejects.toBeInstanceOf(BadRequestException);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
expect(shareMock.remove).not.toHaveBeenCalled();
});
it('should remove a key', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
await sut.remove(authStub.user1, sharedLinkStub.valid.id);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
});
});
describe('edit', () => {
it('should not work on a missing key', async () => {
shareMock.get.mockResolvedValue(null);
await expect(sut.edit(authStub.user1, sharedLinkStub.valid.id, {})).rejects.toBeInstanceOf(BadRequestException);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
expect(shareMock.save).not.toHaveBeenCalled();
});
it('should edit a key', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
shareMock.save.mockResolvedValue(sharedLinkStub.valid);
const dto = { allowDownload: false };
await sut.edit(authStub.user1, sharedLinkStub.valid.id, dto);
// await expect(sut.edit(authStub.user1, sharedLinkStub.valid.id, dto)).rejects.toBeInstanceOf(BadRequestException);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
expect(shareMock.save).toHaveBeenCalledWith({
id: sharedLinkStub.valid.id,
userId: authStub.user1.id,
allowDownload: false,
});
});
});
});

View File

@ -1,60 +0,0 @@
import { BadRequestException, ForbiddenException, Inject, Injectable, Logger } from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { ICryptoRepository } from '../crypto';
import { EditSharedLinkDto } from './dto';
import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto';
import { ShareCore } from './share.core';
import { ISharedLinkRepository } from './shared-link.repository';
@Injectable()
export class ShareService {
readonly logger = new Logger(ShareService.name);
private shareCore: ShareCore;
constructor(
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
) {
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
}
async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
const links = await this.shareCore.getAll(authUser.id);
return links.map(mapSharedLink);
}
async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
if (!authUser.isPublicUser || !authUser.sharedLinkId) {
throw new ForbiddenException();
}
let allowExif = true;
if (authUser.isShowExif != undefined) {
allowExif = authUser.isShowExif;
}
return this.getById(authUser, authUser.sharedLinkId, allowExif);
}
async getById(authUser: AuthUserDto, id: string, allowExif: boolean): Promise<SharedLinkResponseDto> {
const link = await this.shareCore.get(authUser.id, id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
if (allowExif) {
return mapSharedLink(link);
} else {
return mapSharedLinkWithNoExif(link);
}
}
async remove(authUser: AuthUserDto, id: string): Promise<void> {
await this.shareCore.remove(authUser.id, id);
}
async edit(authUser: AuthUserDto, id: string, dto: EditSharedLinkDto) {
const link = await this.shareCore.save(authUser.id, id, dto);
return mapSharedLink(link);
}
}

View File

@ -1,5 +1,5 @@
export * from './dto';
export * from './response-dto';
export * from './share.core';
export * from './share.service';
export * from './shared-link.core';
export * from './shared-link.service';
export * from './shared-link.repository';

View File

@ -5,19 +5,12 @@ import { ICryptoRepository } from '../crypto';
import { CreateSharedLinkDto } from './dto';
import { ISharedLinkRepository } from './shared-link.repository';
export class ShareCore {
readonly logger = new Logger(ShareCore.name);
export class SharedLinkCore {
readonly logger = new Logger(SharedLinkCore.name);
constructor(private repository: ISharedLinkRepository, private cryptoRepository: ICryptoRepository) {}
getAll(userId: string): Promise<SharedLinkEntity[]> {
return this.repository.getAll(userId);
}
get(userId: string, id: string): Promise<SharedLinkEntity | null> {
return this.repository.get(userId, id);
}
// TODO: move to SharedLinkController/SharedLinkService
create(userId: string, dto: CreateSharedLinkDto): Promise<SharedLinkEntity> {
return this.repository.create({
key: Buffer.from(this.cryptoRepository.randomBytes(50)),
@ -34,42 +27,24 @@ export class ShareCore {
});
}
async save(userId: string, id: string, entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
const link = await this.get(userId, id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
return this.repository.save({ ...entity, userId, id });
}
async remove(userId: string, id: string): Promise<void> {
const link = await this.get(userId, id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
await this.repository.remove(link);
}
async addAssets(userId: string, id: string, assets: AssetEntity[]) {
const link = await this.get(userId, id);
const link = await this.repository.get(userId, id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
return this.repository.save({ ...link, assets: [...link.assets, ...assets] });
return this.repository.update({ ...link, assets: [...link.assets, ...assets] });
}
async removeAssets(userId: string, id: string, assets: AssetEntity[]) {
const link = await this.get(userId, id);
const link = await this.repository.get(userId, id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
const newAssets = link.assets.filter((asset) => assets.find((a) => a.id === asset.id));
return this.repository.save({ ...link, assets: newAssets });
return this.repository.update({ ...link, assets: newAssets });
}
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {

View File

@ -7,7 +7,7 @@ export interface ISharedLinkRepository {
get(userId: string, id: string): Promise<SharedLinkEntity | null>;
getByKey(key: Buffer): Promise<SharedLinkEntity | null>;
create(entity: Omit<SharedLinkEntity, 'id' | 'user'>): Promise<SharedLinkEntity>;
update(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
remove(entity: SharedLinkEntity): Promise<void>;
save(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
hasAssetAccess(id: string, assetId: string): Promise<boolean>;
}

View File

@ -0,0 +1,103 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { authStub, newSharedLinkRepositoryMock, sharedLinkResponseStub, sharedLinkStub } from '../../test';
import { SharedLinkService } from './shared-link.service';
import { ISharedLinkRepository } from './shared-link.repository';
describe(SharedLinkService.name, () => {
let sut: SharedLinkService;
let shareMock: jest.Mocked<ISharedLinkRepository>;
beforeEach(async () => {
shareMock = newSharedLinkRepositoryMock();
sut = new SharedLinkService(shareMock);
});
it('should work', () => {
expect(sut).toBeDefined();
});
describe('getAll', () => {
it('should return all shared links for a user', async () => {
shareMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
await expect(sut.getAll(authStub.user1)).resolves.toEqual([
sharedLinkResponseStub.expired,
sharedLinkResponseStub.valid,
]);
expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
});
});
describe('getMine', () => {
it('should only work for a public user', async () => {
await expect(sut.getMine(authStub.admin)).rejects.toBeInstanceOf(ForbiddenException);
expect(shareMock.get).not.toHaveBeenCalled();
});
it('should return the shared link for the public user', async () => {
const authDto = authStub.adminSharedLink;
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
});
it('should return not return exif', async () => {
const authDto = authStub.adminSharedLinkNoExif;
shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.readonlyNoExif);
expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
});
});
describe('get', () => {
it('should throw an error for an invalid shared link', async () => {
shareMock.get.mockResolvedValue(null);
await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id');
expect(shareMock.update).not.toHaveBeenCalled();
});
it('should get a shared link by id', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
});
});
describe('update', () => {
it('should throw an error for an invalid shared link', async () => {
shareMock.get.mockResolvedValue(null);
await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id');
expect(shareMock.update).not.toHaveBeenCalled();
});
it('should update a shared link', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
shareMock.update.mockResolvedValue(sharedLinkStub.valid);
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
expect(shareMock.update).toHaveBeenCalledWith({
id: sharedLinkStub.valid.id,
userId: authStub.user1.id,
allowDownload: false,
});
});
});
describe('remove', () => {
it('should throw an error for an invalid shared link', async () => {
shareMock.get.mockResolvedValue(null);
await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id');
expect(shareMock.update).not.toHaveBeenCalled();
});
it('should remove a key', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid);
await sut.remove(authStub.user1, sharedLinkStub.valid.id);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id);
expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
});
});
});

View File

@ -0,0 +1,63 @@
import { SharedLinkEntity } from '@app/infra/entities';
import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { EditSharedLinkDto } from './dto';
import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto';
import { ISharedLinkRepository } from './shared-link.repository';
@Injectable()
export class SharedLinkService {
constructor(@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository) {}
async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink));
}
async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
const { sharedLinkId: id, isPublicUser, isShowExif } = authUser;
if (!isPublicUser || !id) {
throw new ForbiddenException();
}
const sharedLink = await this.findOrFail(authUser, id);
return this.map(sharedLink, { withExif: isShowExif ?? true });
}
async get(authUser: AuthUserDto, id: string): Promise<SharedLinkResponseDto> {
const sharedLink = await this.findOrFail(authUser, id);
return this.map(sharedLink, { withExif: true });
}
async update(authUser: AuthUserDto, id: string, dto: EditSharedLinkDto) {
await this.findOrFail(authUser, id);
const sharedLink = await this.repository.update({
id,
userId: authUser.id,
description: dto.description,
expiresAt: dto.expiresAt,
allowUpload: dto.allowUpload,
allowDownload: dto.allowDownload,
showExif: dto.showExif,
});
return this.map(sharedLink, { withExif: true });
}
async remove(authUser: AuthUserDto, id: string): Promise<void> {
const sharedLink = await this.findOrFail(authUser, id);
await this.repository.remove(sharedLink);
}
private async findOrFail(authUser: AuthUserDto, id: string) {
const sharedLink = await this.repository.get(authUser.id, id);
if (!sharedLink) {
throw new BadRequestException('Shared link not found');
}
return sharedLink;
}
private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithNoExif(sharedLink);
}
}

View File

@ -71,6 +71,16 @@ export const authStub = {
isShowExif: true,
sharedLinkId: '123',
}),
adminSharedLinkNoExif: Object.freeze<AuthUserDto>({
id: 'admin_id',
email: 'admin@test.com',
isAdmin: true,
isAllowUpload: true,
isAllowDownload: true,
isPublicUser: true,
isShowExif: false,
sharedLinkId: '123',
}),
readonlySharedLink: Object.freeze<AuthUserDto>({
id: 'admin_id',
email: 'admin@test.com',
@ -690,7 +700,7 @@ export const sharedLinkStub = {
showExif: true,
assets: [],
} as SharedLinkEntity),
readonly: Object.freeze<SharedLinkEntity>({
readonlyNoExif: Object.freeze<SharedLinkEntity>({
id: '123',
userId: authStub.admin.id,
user: userEntityStub.admin,
@ -700,7 +710,7 @@ export const sharedLinkStub = {
expiresAt: tomorrow,
allowUpload: false,
allowDownload: false,
showExif: true,
showExif: false,
assets: [],
album: {
id: 'album-123',
@ -834,7 +844,7 @@ export const sharedLinkResponseStub = {
description: undefined,
allowUpload: false,
allowDownload: false,
showExif: true,
showExif: false,
album: albumResponse,
assets: [{ ...assetResponse, exifInfo: undefined }],
}),

View File

@ -7,7 +7,7 @@ export const newSharedLinkRepositoryMock = (): jest.Mocked<ISharedLinkRepository
getByKey: jest.fn(),
create: jest.fn(),
remove: jest.fn(),
save: jest.fn(),
update: jest.fn(),
hasAssetAccess: jest.fn(),
};
};

View File

@ -1,16 +1,12 @@
import { ISharedLinkRepository } from '@app/domain';
import { Injectable, Logger } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { SharedLinkEntity } from '../entities';
@Injectable()
export class SharedLinkRepository implements ISharedLinkRepository {
readonly logger = new Logger(SharedLinkRepository.name);
constructor(
@InjectRepository(SharedLinkEntity)
private readonly repository: Repository<SharedLinkEntity>,
) {}
constructor(@InjectRepository(SharedLinkEntity) private repository: Repository<SharedLinkEntity>) {}
get(userId: string, id: string): Promise<SharedLinkEntity | null> {
return this.repository.findOne({
@ -78,30 +74,22 @@ export class SharedLinkRepository implements ISharedLinkRepository {
});
}
create(entity: Omit<SharedLinkEntity, 'id'>): Promise<SharedLinkEntity> {
return this.repository.save(entity);
create(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
return this.save(entity);
}
update(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
return this.save(entity);
}
async remove(entity: SharedLinkEntity): Promise<void> {
await this.repository.remove(entity);
}
async save(entity: SharedLinkEntity): Promise<SharedLinkEntity> {
await this.repository.save(entity);
return this.repository.findOneOrFail({ where: { id: entity.id } });
}
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {
const count1 = await this.repository.count({
where: {
id,
assets: {
id: assetId,
},
},
});
const count2 = await this.repository.count({
return (
// album asset
(await this.repository.exist({
where: {
id,
album: {
@ -110,8 +98,21 @@ export class SharedLinkRepository implements ISharedLinkRepository {
},
},
},
});
})) ||
// individual asset
(await this.repository.exist({
where: {
id,
assets: {
id: assetId,
},
},
}))
);
}
return Boolean(count1 + count2);
private async save(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
await this.repository.save(entity);
return this.repository.findOneOrFail({ where: { id: entity.id } });
}
}

View File

@ -9984,54 +9984,6 @@ export class ServerInfoApi extends BaseAPI {
*/
export const ShareApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {string} id
* @param {EditSharedLinkDto} editSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
editSharedLink: async (id: string, editSharedLinkDto: EditSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('editSharedLink', 'id', id)
// verify required parameter 'editSharedLinkDto' is not null or undefined
assertParamExists('editSharedLink', 'editSharedLinkDto', editSharedLinkDto)
const localVarPath = `/share/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(editSharedLinkDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
@ -10192,6 +10144,54 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {EditSharedLinkDto} editSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateSharedLink: async (id: string, editSharedLinkDto: EditSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updateSharedLink', 'id', id)
// verify required parameter 'editSharedLinkDto' is not null or undefined
assertParamExists('updateSharedLink', 'editSharedLinkDto', editSharedLinkDto)
const localVarPath = `/share/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(editSharedLinkDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@ -10207,17 +10207,6 @@ export const ShareApiAxiosParamCreator = function (configuration?: Configuration
export const ShareApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = ShareApiAxiosParamCreator(configuration)
return {
/**
*
* @param {string} id
* @param {EditSharedLinkDto} editSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async editSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.editSharedLink(id, editSharedLinkDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
@ -10257,6 +10246,17 @@ export const ShareApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.removeSharedLink(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {EditSharedLinkDto} editSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateSharedLink(id, editSharedLinkDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
@ -10267,16 +10267,6 @@ export const ShareApiFp = function(configuration?: Configuration) {
export const ShareApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = ShareApiFp(configuration)
return {
/**
*
* @param {string} id
* @param {EditSharedLinkDto} editSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
editSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.editSharedLink(id, editSharedLinkDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
@ -10312,30 +10302,19 @@ export const ShareApiFactory = function (configuration?: Configuration, basePath
removeSharedLink(id: string, options?: any): AxiosPromise<void> {
return localVarFp.removeSharedLink(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id
* @param {EditSharedLinkDto} editSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.updateSharedLink(id, editSharedLinkDto, options).then((request) => request(axios, basePath));
},
};
};
/**
* Request parameters for editSharedLink operation in ShareApi.
* @export
* @interface ShareApiEditSharedLinkRequest
*/
export interface ShareApiEditSharedLinkRequest {
/**
*
* @type {string}
* @memberof ShareApiEditSharedLink
*/
readonly id: string
/**
*
* @type {EditSharedLinkDto}
* @memberof ShareApiEditSharedLink
*/
readonly editSharedLinkDto: EditSharedLinkDto
}
/**
* Request parameters for getMySharedLink operation in ShareApi.
* @export
@ -10378,6 +10357,27 @@ export interface ShareApiRemoveSharedLinkRequest {
readonly id: string
}
/**
* Request parameters for updateSharedLink operation in ShareApi.
* @export
* @interface ShareApiUpdateSharedLinkRequest
*/
export interface ShareApiUpdateSharedLinkRequest {
/**
*
* @type {string}
* @memberof ShareApiUpdateSharedLink
*/
readonly id: string
/**
*
* @type {EditSharedLinkDto}
* @memberof ShareApiUpdateSharedLink
*/
readonly editSharedLinkDto: EditSharedLinkDto
}
/**
* ShareApi - object-oriented interface
* @export
@ -10385,17 +10385,6 @@ export interface ShareApiRemoveSharedLinkRequest {
* @extends {BaseAPI}
*/
export class ShareApi extends BaseAPI {
/**
*
* @param {ShareApiEditSharedLinkRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ShareApi
*/
public editSharedLink(requestParameters: ShareApiEditSharedLinkRequest, options?: AxiosRequestConfig) {
return ShareApiFp(this.configuration).editSharedLink(requestParameters.id, requestParameters.editSharedLinkDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
@ -10438,6 +10427,17 @@ export class ShareApi extends BaseAPI {
public removeSharedLink(requestParameters: ShareApiRemoveSharedLinkRequest, options?: AxiosRequestConfig) {
return ShareApiFp(this.configuration).removeSharedLink(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {ShareApiUpdateSharedLinkRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ShareApi
*/
public updateSharedLink(requestParameters: ShareApiUpdateSharedLinkRequest, options?: AxiosRequestConfig) {
return ShareApiFp(this.configuration).updateSharedLink(requestParameters.id, requestParameters.editSharedLinkDto, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -137,7 +137,7 @@
? new Date(currentTime + expirationTime).toISOString()
: null;
await api.shareApi.editSharedLink({
await api.shareApi.updateSharedLink({
id: editingLink.id,
editSharedLinkDto: {
description,