mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
refactor(server): shared links (#1385)
* refactor(server): shared links * chore: tests * fix: bugs and tests * fix: missed one expired at * fix: standardize file upload checks * test: lower flutter version Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
f64db3a2f9
commit
8f304b8157
BIN
mobile/openapi/doc/CreateAlbumShareLinkDto.md
generated
BIN
mobile/openapi/doc/CreateAlbumShareLinkDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/CreateAssetsShareLinkDto.md
generated
BIN
mobile/openapi/doc/CreateAssetsShareLinkDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/EditSharedLinkDto.md
generated
BIN
mobile/openapi/doc/EditSharedLinkDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/ShareApi.md
generated
BIN
mobile/openapi/doc/ShareApi.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/share_api.dart
generated
BIN
mobile/openapi/lib/api/share_api.dart
generated
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/edit_shared_link_dto.dart
generated
BIN
mobile/openapi/lib/model/edit_shared_link_dto.dart
generated
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/test/edit_shared_link_dto_test.dart
generated
BIN
mobile/openapi/test/edit_shared_link_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/share_api_test.dart
generated
BIN
mobile/openapi/test/share_api_test.dart
generated
Binary file not shown.
@ -23,7 +23,7 @@ import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
|||||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
import { UpdateAlbumDto } from './dto/update-album.dto';
|
||||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
import { AlbumResponseDto } from '@app/domain';
|
||||||
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||||
import { Response as Res } from 'express';
|
import { Response as Res } from 'express';
|
||||||
|
@ -6,7 +6,6 @@ import { AlbumEntity, AssetAlbumEntity, UserAlbumEntity } from '@app/infra';
|
|||||||
import { AlbumRepository, IAlbumRepository } from './album-repository';
|
import { AlbumRepository, IAlbumRepository } from './album-repository';
|
||||||
import { DownloadModule } from '../../modules/download/download.module';
|
import { DownloadModule } from '../../modules/download/download.module';
|
||||||
import { AssetModule } from '../asset/asset.module';
|
import { AssetModule } from '../asset/asset.module';
|
||||||
import { ShareModule } from '../share/share.module';
|
|
||||||
|
|
||||||
const ALBUM_REPOSITORY_PROVIDER = {
|
const ALBUM_REPOSITORY_PROVIDER = {
|
||||||
provide: IAlbumRepository,
|
provide: IAlbumRepository,
|
||||||
@ -18,7 +17,6 @@ const ALBUM_REPOSITORY_PROVIDER = {
|
|||||||
TypeOrmModule.forFeature([AlbumEntity, AssetAlbumEntity, UserAlbumEntity]),
|
TypeOrmModule.forFeature([AlbumEntity, AssetAlbumEntity, UserAlbumEntity]),
|
||||||
DownloadModule,
|
DownloadModule,
|
||||||
forwardRef(() => AssetModule),
|
forwardRef(() => AssetModule),
|
||||||
ShareModule,
|
|
||||||
],
|
],
|
||||||
controllers: [AlbumController],
|
controllers: [AlbumController],
|
||||||
providers: [AlbumService, ALBUM_REPOSITORY_PROVIDER],
|
providers: [AlbumService, ALBUM_REPOSITORY_PROVIDER],
|
||||||
|
@ -2,17 +2,19 @@ import { AlbumService } from './album.service';
|
|||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||||
import { AlbumEntity } from '@app/infra';
|
import { AlbumEntity } from '@app/infra';
|
||||||
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
import { AlbumResponseDto, ICryptoRepository } from '@app/domain';
|
||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||||
import { IAlbumRepository } from './album-repository';
|
import { IAlbumRepository } from './album-repository';
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { ISharedLinkRepository } from '../share/shared-link.repository';
|
import { ISharedLinkRepository } from '@app/domain';
|
||||||
|
import { newCryptoRepositoryMock, newSharedLinkRepositoryMock } from '@app/domain/../test';
|
||||||
|
|
||||||
describe('Album service', () => {
|
describe('Album service', () => {
|
||||||
let sut: AlbumService;
|
let sut: AlbumService;
|
||||||
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
||||||
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
||||||
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
|
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
|
||||||
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
||||||
|
|
||||||
const authUser: AuthUserDto = Object.freeze({
|
const authUser: AuthUserDto = Object.freeze({
|
||||||
id: '1111',
|
id: '1111',
|
||||||
@ -129,22 +131,20 @@ describe('Album service', () => {
|
|||||||
getSharedWithUserAlbumCount: jest.fn(),
|
getSharedWithUserAlbumCount: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
sharedLinkRepositoryMock = {
|
sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
|
||||||
create: jest.fn(),
|
|
||||||
remove: jest.fn(),
|
|
||||||
get: jest.fn(),
|
|
||||||
getById: jest.fn(),
|
|
||||||
getByKey: jest.fn(),
|
|
||||||
save: jest.fn(),
|
|
||||||
hasAssetAccess: jest.fn(),
|
|
||||||
getByIdAndUserId: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
downloadServiceMock = {
|
downloadServiceMock = {
|
||||||
downloadArchive: jest.fn(),
|
downloadArchive: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
sut = new AlbumService(albumRepositoryMock, sharedLinkRepositoryMock, downloadServiceMock as DownloadService);
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
|
|
||||||
|
sut = new AlbumService(
|
||||||
|
albumRepositoryMock,
|
||||||
|
sharedLinkRepositoryMock,
|
||||||
|
downloadServiceMock as DownloadService,
|
||||||
|
cryptoMock,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates album', async () => {
|
it('creates album', async () => {
|
||||||
|
@ -6,16 +6,14 @@ import { AddUsersDto } from './dto/add-users.dto';
|
|||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
import { UpdateAlbumDto } from './dto/update-album.dto';
|
||||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||||
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
|
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from '@app/domain';
|
||||||
import { IAlbumRepository } from './album-repository';
|
import { IAlbumRepository } from './album-repository';
|
||||||
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { DownloadDto } from '../asset/dto/download-library.dto';
|
import { DownloadDto } from '../asset/dto/download-library.dto';
|
||||||
import { ShareCore } from '../share/share.core';
|
import { ShareCore, ISharedLinkRepository, mapSharedLink, SharedLinkResponseDto, ICryptoRepository } from '@app/domain';
|
||||||
import { ISharedLinkRepository } from '../share/shared-link.repository';
|
|
||||||
import { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
|
|
||||||
import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto';
|
import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
@ -26,10 +24,11 @@ export class AlbumService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
|
||||||
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
|
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||||
) {
|
) {
|
||||||
this.shareCore = new ShareCore(sharedLinkRepository);
|
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getAlbum({
|
private async _getAlbum({
|
||||||
@ -102,7 +101,7 @@ export class AlbumService {
|
|||||||
const album = await this._getAlbum({ authUser, albumId });
|
const album = await this._getAlbum({ authUser, albumId });
|
||||||
|
|
||||||
for (const sharedLink of album.sharedLinks) {
|
for (const sharedLink of album.sharedLinks) {
|
||||||
await this.shareCore.removeSharedLink(sharedLink.id, authUser.id);
|
await this.shareCore.remove(sharedLink.id, authUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._albumRepository.delete(album);
|
await this._albumRepository.delete(album);
|
||||||
@ -203,11 +202,11 @@ export class AlbumService {
|
|||||||
async createAlbumSharedLink(authUser: AuthUserDto, dto: CreateAlbumShareLinkDto): Promise<SharedLinkResponseDto> {
|
async createAlbumSharedLink(authUser: AuthUserDto, dto: CreateAlbumShareLinkDto): Promise<SharedLinkResponseDto> {
|
||||||
const album = await this._getAlbum({ authUser, albumId: dto.albumId });
|
const album = await this._getAlbum({ authUser, albumId: dto.albumId });
|
||||||
|
|
||||||
const sharedLink = await this.shareCore.createSharedLink(authUser.id, {
|
const sharedLink = await this.shareCore.create(authUser.id, {
|
||||||
sharedType: SharedLinkType.ALBUM,
|
type: SharedLinkType.ALBUM,
|
||||||
expiredAt: dto.expiredAt,
|
expiresAt: dto.expiresAt,
|
||||||
allowUpload: dto.allowUpload,
|
allowUpload: dto.allowUpload,
|
||||||
album: album,
|
album,
|
||||||
assets: [],
|
assets: [],
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
allowDownload: dto.allowDownload,
|
allowDownload: dto.allowDownload,
|
||||||
|
@ -7,7 +7,7 @@ export class CreateAlbumShareLinkDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
expiredAt?: string;
|
expiresAt?: string;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { AlbumResponseDto } from './album-response.dto';
|
import { AlbumResponseDto } from '@app/domain';
|
||||||
|
|
||||||
export class AddAssetsResponseDto {
|
export class AddAssetsResponseDto {
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
|
@ -30,7 +30,7 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
|||||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
|
||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||||
import { AssetResponseDto } from './response-dto/asset-response.dto';
|
import { AssetResponseDto } from '@app/domain';
|
||||||
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
||||||
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
@ -52,7 +52,7 @@ import {
|
|||||||
} from '../../constants/download.constant';
|
} from '../../constants/download.constant';
|
||||||
import { DownloadFilesDto } from './dto/download-files.dto';
|
import { DownloadFilesDto } from './dto/download-files.dto';
|
||||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
||||||
import { SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
|
import { SharedLinkResponseDto } from '@app/domain';
|
||||||
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
||||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ import { DownloadModule } from '../../modules/download/download.module';
|
|||||||
import { TagModule } from '../tag/tag.module';
|
import { TagModule } from '../tag/tag.module';
|
||||||
import { AlbumModule } from '../album/album.module';
|
import { AlbumModule } from '../album/album.module';
|
||||||
import { StorageModule } from '@app/storage';
|
import { StorageModule } from '@app/storage';
|
||||||
import { ShareModule } from '../share/share.module';
|
|
||||||
|
|
||||||
const ASSET_REPOSITORY_PROVIDER = {
|
const ASSET_REPOSITORY_PROVIDER = {
|
||||||
provide: IAssetRepository,
|
provide: IAssetRepository,
|
||||||
@ -27,7 +26,6 @@ const ASSET_REPOSITORY_PROVIDER = {
|
|||||||
TagModule,
|
TagModule,
|
||||||
StorageModule,
|
StorageModule,
|
||||||
forwardRef(() => AlbumModule),
|
forwardRef(() => AlbumModule),
|
||||||
ShareModule,
|
|
||||||
],
|
],
|
||||||
controllers: [AssetController],
|
controllers: [AssetController],
|
||||||
providers: [AssetService, BackgroundTaskService, ASSET_REPOSITORY_PROVIDER],
|
providers: [AssetService, BackgroundTaskService, ASSET_REPOSITORY_PROVIDER],
|
||||||
|
@ -9,11 +9,19 @@ import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
|||||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||||
import { IAlbumRepository } from '../album/album-repository';
|
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
|
||||||
import { StorageService } from '@app/storage';
|
import { StorageService } from '@app/storage';
|
||||||
import { ISharedLinkRepository } from '../share/shared-link.repository';
|
import { ICryptoRepository, IJobRepository, ISharedLinkRepository } from '@app/domain';
|
||||||
import { IJobRepository } from '@app/domain';
|
import {
|
||||||
import { newJobRepositoryMock } from '@app/domain/../test';
|
authStub,
|
||||||
|
newCryptoRepositoryMock,
|
||||||
|
newJobRepositoryMock,
|
||||||
|
newSharedLinkRepositoryMock,
|
||||||
|
sharedLinkResponseStub,
|
||||||
|
sharedLinkStub,
|
||||||
|
} from '@app/domain/../test';
|
||||||
|
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
||||||
|
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||||
|
|
||||||
describe('AssetService', () => {
|
describe('AssetService', () => {
|
||||||
let sui: AssetService;
|
let sui: AssetService;
|
||||||
@ -24,6 +32,7 @@ describe('AssetService', () => {
|
|||||||
let backgroundTaskServiceMock: jest.Mocked<BackgroundTaskService>;
|
let backgroundTaskServiceMock: jest.Mocked<BackgroundTaskService>;
|
||||||
let storageSeriveMock: jest.Mocked<StorageService>;
|
let storageSeriveMock: jest.Mocked<StorageService>;
|
||||||
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
||||||
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
||||||
let jobMock: jest.Mocked<IJobRepository>;
|
let jobMock: jest.Mocked<IJobRepository>;
|
||||||
const authUser: AuthUserDto = Object.freeze({
|
const authUser: AuthUserDto = Object.freeze({
|
||||||
id: 'user_id_1',
|
id: 'user_id_1',
|
||||||
@ -132,22 +141,18 @@ describe('AssetService', () => {
|
|||||||
countByIdAndUser: jest.fn(),
|
countByIdAndUser: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
albumRepositoryMock = {
|
||||||
|
getSharedWithUserAlbumCount: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<AlbumRepository>;
|
||||||
|
|
||||||
downloadServiceMock = {
|
downloadServiceMock = {
|
||||||
downloadArchive: jest.fn(),
|
downloadArchive: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
sharedLinkRepositoryMock = {
|
sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
|
||||||
create: jest.fn(),
|
|
||||||
get: jest.fn(),
|
|
||||||
getById: jest.fn(),
|
|
||||||
getByKey: jest.fn(),
|
|
||||||
remove: jest.fn(),
|
|
||||||
save: jest.fn(),
|
|
||||||
hasAssetAccess: jest.fn(),
|
|
||||||
getByIdAndUserId: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
|
|
||||||
sui = new AssetService(
|
sui = new AssetService(
|
||||||
assetRepositoryMock,
|
assetRepositoryMock,
|
||||||
@ -158,9 +163,64 @@ describe('AssetService', () => {
|
|||||||
storageSeriveMock,
|
storageSeriveMock,
|
||||||
sharedLinkRepositoryMock,
|
sharedLinkRepositoryMock,
|
||||||
jobMock,
|
jobMock,
|
||||||
|
cryptoMock,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createAssetsSharedLink', () => {
|
||||||
|
it('should create an individual share link', async () => {
|
||||||
|
const asset1 = _getAsset_1();
|
||||||
|
const dto: CreateAssetsShareLinkDto = { assetIds: [asset1.id] };
|
||||||
|
|
||||||
|
assetRepositoryMock.getById.mockResolvedValue(asset1);
|
||||||
|
assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
|
||||||
|
sharedLinkRepositoryMock.create.mockResolvedValue(sharedLinkStub.valid);
|
||||||
|
|
||||||
|
await expect(sui.createAssetsSharedLink(authStub.user1, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||||
|
|
||||||
|
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
||||||
|
expect(assetRepositoryMock.countByIdAndUser).toHaveBeenCalledWith(asset1.id, authStub.user1.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateAssetsInSharedLink', () => {
|
||||||
|
it('should require a valid shared link', async () => {
|
||||||
|
const asset1 = _getAsset_1();
|
||||||
|
|
||||||
|
const authDto = authStub.adminSharedLink;
|
||||||
|
const dto = { assetIds: [asset1.id] };
|
||||||
|
|
||||||
|
assetRepositoryMock.getById.mockResolvedValue(asset1);
|
||||||
|
sharedLinkRepositoryMock.get.mockResolvedValue(null);
|
||||||
|
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await expect(sui.updateAssetsInSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
|
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
||||||
|
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||||
|
expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
|
||||||
|
expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove assets from a shared link', async () => {
|
||||||
|
const asset1 = _getAsset_1();
|
||||||
|
|
||||||
|
const authDto = authStub.adminSharedLink;
|
||||||
|
const dto = { assetIds: [asset1.id] };
|
||||||
|
|
||||||
|
assetRepositoryMock.getById.mockResolvedValue(asset1);
|
||||||
|
sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||||
|
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
||||||
|
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
|
||||||
|
|
||||||
|
await expect(sui.updateAssetsInSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||||
|
|
||||||
|
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
||||||
|
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||||
|
expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Currently failing due to calculate checksum from a file
|
// Currently failing due to calculate checksum from a file
|
||||||
it('create an asset', async () => {
|
it('create an asset', async () => {
|
||||||
const assetEntity = _getAsset_1();
|
const assetEntity = _getAsset_1();
|
||||||
@ -224,4 +284,14 @@ describe('AssetService', () => {
|
|||||||
|
|
||||||
expect(result).toEqual(assetCount);
|
expect(result).toEqual(assetCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('checkDownloadAccess', () => {
|
||||||
|
it('should validate download access', async () => {
|
||||||
|
await sui.checkDownloadAccess(authStub.adminSharedLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow when user is not allowed to download', async () => {
|
||||||
|
expect(() => sui.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -23,7 +23,7 @@ import { SearchAssetDto } from './dto/search-asset.dto';
|
|||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||||
import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from './response-dto/asset-response.dto';
|
import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '@app/domain';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
||||||
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
|
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
|
||||||
@ -43,16 +43,16 @@ import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-as
|
|||||||
import { UpdateAssetDto } from './dto/update-asset.dto';
|
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||||
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||||
import { IJobRepository, JobName } from '@app/domain';
|
import { ICryptoRepository, IJobRepository, JobName } from '@app/domain';
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { DownloadDto } from './dto/download-library.dto';
|
import { DownloadDto } from './dto/download-library.dto';
|
||||||
import { IAlbumRepository } from '../album/album-repository';
|
import { IAlbumRepository } from '../album/album-repository';
|
||||||
import { StorageService } from '@app/storage';
|
import { StorageService } from '@app/storage';
|
||||||
import { ShareCore } from '../share/share.core';
|
import { ShareCore } from '@app/domain';
|
||||||
import { ISharedLinkRepository } from '../share/shared-link.repository';
|
import { ISharedLinkRepository } from '@app/domain';
|
||||||
import { DownloadFilesDto } from './dto/download-files.dto';
|
import { DownloadFilesDto } from './dto/download-files.dto';
|
||||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
||||||
import { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
|
import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
|
||||||
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
||||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
|
|
||||||
@ -73,8 +73,9 @@ export class AssetService {
|
|||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
|
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||||
) {
|
) {
|
||||||
this.shareCore = new ShareCore(sharedLinkRepository);
|
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleUploadedAsset(
|
public async handleUploadedAsset(
|
||||||
@ -669,23 +670,24 @@ export class AssetService {
|
|||||||
// Step 1: Check if asset is part of a public shared
|
// Step 1: Check if asset is part of a public shared
|
||||||
if (authUser.sharedLinkId) {
|
if (authUser.sharedLinkId) {
|
||||||
const canAccess = await this.shareCore.hasAssetAccess(authUser.sharedLinkId, assetId);
|
const canAccess = await this.shareCore.hasAssetAccess(authUser.sharedLinkId, assetId);
|
||||||
if (!canAccess) {
|
if (canAccess) {
|
||||||
throw new ForbiddenException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Check if user owns asset
|
|
||||||
if ((await this._assetRepository.countByIdAndUser(assetId, authUser.id)) == 1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid additional checks if ownership is required
|
|
||||||
if (!mustBeOwner) {
|
|
||||||
// Step 2: Check if asset is part of an album shared with me
|
|
||||||
if ((await this._albumRepository.getSharedWithUserAlbumCount(authUser.id, assetId)) > 0) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Step 2: Check if user owns asset
|
||||||
|
if ((await this._assetRepository.countByIdAndUser(assetId, authUser.id)) == 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid additional checks if ownership is required
|
||||||
|
if (!mustBeOwner) {
|
||||||
|
// Step 2: Check if asset is part of an album shared with me
|
||||||
|
if ((await this._albumRepository.getSharedWithUserAlbumCount(authUser.id, assetId)) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -703,11 +705,11 @@ export class AssetService {
|
|||||||
assets.push(asset);
|
assets.push(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharedLink = await this.shareCore.createSharedLink(authUser.id, {
|
const sharedLink = await this.shareCore.create(authUser.id, {
|
||||||
sharedType: SharedLinkType.INDIVIDUAL,
|
type: SharedLinkType.INDIVIDUAL,
|
||||||
expiredAt: dto.expiredAt,
|
expiresAt: dto.expiresAt,
|
||||||
allowUpload: dto.allowUpload,
|
allowUpload: dto.allowUpload,
|
||||||
assets: assets,
|
assets,
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
allowDownload: dto.allowDownload,
|
allowDownload: dto.allowDownload,
|
||||||
showExif: dto.showExif,
|
showExif: dto.showExif,
|
||||||
@ -720,15 +722,19 @@ export class AssetService {
|
|||||||
authUser: AuthUserDto,
|
authUser: AuthUserDto,
|
||||||
dto: UpdateAssetsToSharedLinkDto,
|
dto: UpdateAssetsToSharedLinkDto,
|
||||||
): Promise<SharedLinkResponseDto> {
|
): Promise<SharedLinkResponseDto> {
|
||||||
if (!authUser.sharedLinkId) throw new ForbiddenException();
|
if (!authUser.sharedLinkId) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
const assets = [];
|
const assets = [];
|
||||||
|
|
||||||
|
await this.checkAssetsAccess(authUser, dto.assetIds);
|
||||||
for (const assetId of dto.assetIds) {
|
for (const assetId of dto.assetIds) {
|
||||||
const asset = await this._assetRepository.getById(assetId);
|
const asset = await this._assetRepository.getById(assetId);
|
||||||
assets.push(asset);
|
assets.push(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedLink = await this.shareCore.updateAssetsInSharedLink(authUser.sharedLinkId, assets);
|
const updatedLink = await this.shareCore.updateAssets(authUser.id, authUser.sharedLinkId, assets);
|
||||||
return mapSharedLink(updatedLink);
|
return mapSharedLink(updatedLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export class CreateAssetsShareLinkDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
expiredAt?: string;
|
expiresAt?: string;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -1,101 +0,0 @@
|
|||||||
import { SharedLinkEntity } from '@app/infra';
|
|
||||||
import { CreateSharedLinkDto } from './dto/create-shared-link.dto';
|
|
||||||
import { ISharedLinkRepository } from './shared-link.repository';
|
|
||||||
import crypto from 'node:crypto';
|
|
||||||
import { BadRequestException, ForbiddenException, InternalServerErrorException, Logger } from '@nestjs/common';
|
|
||||||
import { AssetEntity } from '@app/infra';
|
|
||||||
import { EditSharedLinkDto } from './dto/edit-shared-link.dto';
|
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
|
||||||
|
|
||||||
export class ShareCore {
|
|
||||||
readonly logger = new Logger(ShareCore.name);
|
|
||||||
|
|
||||||
constructor(private sharedLinkRepository: ISharedLinkRepository) {}
|
|
||||||
|
|
||||||
async createSharedLink(userId: string, dto: CreateSharedLinkDto): Promise<SharedLinkEntity> {
|
|
||||||
try {
|
|
||||||
const sharedLink = new SharedLinkEntity();
|
|
||||||
|
|
||||||
sharedLink.key = Buffer.from(crypto.randomBytes(50));
|
|
||||||
sharedLink.description = dto.description;
|
|
||||||
sharedLink.userId = userId;
|
|
||||||
sharedLink.createdAt = new Date().toISOString();
|
|
||||||
sharedLink.expiresAt = dto.expiredAt ?? null;
|
|
||||||
sharedLink.type = dto.sharedType;
|
|
||||||
sharedLink.assets = dto.assets;
|
|
||||||
sharedLink.album = dto.album;
|
|
||||||
sharedLink.allowUpload = dto.allowUpload ?? false;
|
|
||||||
sharedLink.allowDownload = dto.allowDownload ?? true;
|
|
||||||
sharedLink.showExif = dto.showExif ?? true;
|
|
||||||
|
|
||||||
return this.sharedLinkRepository.create(sharedLink);
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.error(error, error.stack);
|
|
||||||
throw new InternalServerErrorException('failed to create shared link');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSharedLinks(userId: string): Promise<SharedLinkEntity[]> {
|
|
||||||
return this.sharedLinkRepository.get(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeSharedLink(id: string, userId: string): Promise<SharedLinkEntity> {
|
|
||||||
const link = await this.sharedLinkRepository.getByIdAndUserId(id, userId);
|
|
||||||
|
|
||||||
if (!link) {
|
|
||||||
throw new BadRequestException('Shared link not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.sharedLinkRepository.remove(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSharedLinkById(id: string): Promise<SharedLinkEntity | null> {
|
|
||||||
return this.sharedLinkRepository.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSharedLinkByKey(key: string): Promise<SharedLinkEntity | null> {
|
|
||||||
return this.sharedLinkRepository.getByKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateAssetsInSharedLink(sharedLinkId: string, assets: AssetEntity[]) {
|
|
||||||
const link = await this.getSharedLinkById(sharedLinkId);
|
|
||||||
if (!link) {
|
|
||||||
throw new BadRequestException('Shared link not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
link.assets = assets;
|
|
||||||
|
|
||||||
return await this.sharedLinkRepository.save(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateSharedLink(id: string, userId: string, dto: EditSharedLinkDto): Promise<SharedLinkEntity> {
|
|
||||||
const link = await this.sharedLinkRepository.getByIdAndUserId(id, userId);
|
|
||||||
|
|
||||||
if (!link) {
|
|
||||||
throw new BadRequestException('Shared link not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
link.description = dto.description ?? link.description;
|
|
||||||
link.allowUpload = dto.allowUpload ?? link.allowUpload;
|
|
||||||
link.allowDownload = dto.allowDownload ?? link.allowDownload;
|
|
||||||
link.showExif = dto.showExif ?? link.showExif;
|
|
||||||
|
|
||||||
if (dto.isEditExpireTime && dto.expiredAt) {
|
|
||||||
link.expiresAt = dto.expiredAt;
|
|
||||||
} else if (dto.isEditExpireTime && !dto.expiredAt) {
|
|
||||||
link.expiresAt = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.sharedLinkRepository.save(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {
|
|
||||||
return this.sharedLinkRepository.hasAssetAccess(id, assetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkDownloadAccess(user: AuthUserDto) {
|
|
||||||
if (user.isPublicUser && !user.isAllowDownload) {
|
|
||||||
throw new ForbiddenException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ShareService } from './share.service';
|
|
||||||
import { ShareController } from './share.controller';
|
|
||||||
import { SharedLinkEntity } from '@app/infra';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { SharedLinkRepository, ISharedLinkRepository } from './shared-link.repository';
|
|
||||||
|
|
||||||
const SHARED_LINK_REPOSITORY_PROVIDER = {
|
|
||||||
provide: ISharedLinkRepository,
|
|
||||||
useClass: SharedLinkRepository,
|
|
||||||
};
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [TypeOrmModule.forFeature([SharedLinkEntity])],
|
|
||||||
controllers: [ShareController],
|
|
||||||
providers: [ShareService, SHARED_LINK_REPOSITORY_PROVIDER],
|
|
||||||
exports: [SHARED_LINK_REPOSITORY_PROVIDER, ShareService],
|
|
||||||
})
|
|
||||||
export class ShareModule {}
|
|
@ -1,137 +0,0 @@
|
|||||||
import { SharedLinkEntity } from '@app/infra';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
export interface ISharedLinkRepository {
|
|
||||||
get(userId: string): Promise<SharedLinkEntity[]>;
|
|
||||||
getById(id: string): Promise<SharedLinkEntity | null>;
|
|
||||||
getByIdAndUserId(id: string, userId: string): Promise<SharedLinkEntity | null>;
|
|
||||||
getByKey(key: string): Promise<SharedLinkEntity | null>;
|
|
||||||
create(payload: SharedLinkEntity): Promise<SharedLinkEntity>;
|
|
||||||
remove(entity: SharedLinkEntity): Promise<SharedLinkEntity>;
|
|
||||||
save(entity: SharedLinkEntity): Promise<SharedLinkEntity>;
|
|
||||||
hasAssetAccess(id: string, assetId: string): Promise<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ISharedLinkRepository = 'ISharedLinkRepository';
|
|
||||||
|
|
||||||
export class SharedLinkRepository implements ISharedLinkRepository {
|
|
||||||
readonly logger = new Logger(SharedLinkRepository.name);
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(SharedLinkEntity)
|
|
||||||
private readonly sharedLinkRepository: Repository<SharedLinkEntity>,
|
|
||||||
) {}
|
|
||||||
async getByIdAndUserId(id: string, userId: string): Promise<SharedLinkEntity | null> {
|
|
||||||
return await this.sharedLinkRepository.findOne({
|
|
||||||
where: {
|
|
||||||
userId: userId,
|
|
||||||
id: id,
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
createdAt: 'DESC',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(userId: string): Promise<SharedLinkEntity[]> {
|
|
||||||
return await this.sharedLinkRepository.find({
|
|
||||||
where: {
|
|
||||||
userId: userId,
|
|
||||||
},
|
|
||||||
relations: ['assets', 'album'],
|
|
||||||
order: {
|
|
||||||
createdAt: 'DESC',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(payload: SharedLinkEntity): Promise<SharedLinkEntity> {
|
|
||||||
return await this.sharedLinkRepository.save(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getById(id: string): Promise<SharedLinkEntity | null> {
|
|
||||||
return await this.sharedLinkRepository.findOne({
|
|
||||||
where: {
|
|
||||||
id: id,
|
|
||||||
},
|
|
||||||
relations: {
|
|
||||||
assets: {
|
|
||||||
exifInfo: true,
|
|
||||||
},
|
|
||||||
album: {
|
|
||||||
assets: {
|
|
||||||
assetInfo: {
|
|
||||||
exifInfo: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
createdAt: 'DESC',
|
|
||||||
assets: {
|
|
||||||
createdAt: 'ASC',
|
|
||||||
},
|
|
||||||
album: {
|
|
||||||
assets: {
|
|
||||||
assetInfo: {
|
|
||||||
createdAt: 'ASC',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getByKey(key: string): Promise<SharedLinkEntity | null> {
|
|
||||||
return await this.sharedLinkRepository.findOne({
|
|
||||||
where: {
|
|
||||||
key: Buffer.from(key, 'hex'),
|
|
||||||
},
|
|
||||||
relations: {
|
|
||||||
assets: true,
|
|
||||||
album: {
|
|
||||||
assets: {
|
|
||||||
assetInfo: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
createdAt: 'DESC',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(entity: SharedLinkEntity): Promise<SharedLinkEntity> {
|
|
||||||
return await this.sharedLinkRepository.remove(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(entity: SharedLinkEntity): Promise<SharedLinkEntity> {
|
|
||||||
return await this.sharedLinkRepository.save(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {
|
|
||||||
const count1 = await this.sharedLinkRepository.count({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
assets: {
|
|
||||||
id: assetId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const count2 = await this.sharedLinkRepository.count({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
album: {
|
|
||||||
assets: {
|
|
||||||
assetId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return Boolean(count1 + count2);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ import { UpdateTagDto } from './dto/update-tag.dto';
|
|||||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { mapTag, TagResponseDto } from './response-dto/tag-response.dto';
|
import { mapTag, TagResponseDto } from '@app/domain';
|
||||||
|
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
@ApiTags('Tag')
|
@ApiTags('Tag')
|
||||||
|
@ -4,7 +4,7 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
|||||||
import { CreateTagDto } from './dto/create-tag.dto';
|
import { CreateTagDto } from './dto/create-tag.dto';
|
||||||
import { UpdateTagDto } from './dto/update-tag.dto';
|
import { UpdateTagDto } from './dto/update-tag.dto';
|
||||||
import { ITagRepository } from './tag.repository';
|
import { ITagRepository } from './tag.repository';
|
||||||
import { mapTag, TagResponseDto } from './response-dto/tag-response.dto';
|
import { mapTag, TagResponseDto } from '@app/domain';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TagService {
|
export class TagService {
|
||||||
|
@ -13,13 +13,13 @@ import { ScheduleModule } from '@nestjs/schedule';
|
|||||||
import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module';
|
import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module';
|
||||||
import { JobModule } from './api-v1/job/job.module';
|
import { JobModule } from './api-v1/job/job.module';
|
||||||
import { TagModule } from './api-v1/tag/tag.module';
|
import { TagModule } from './api-v1/tag/tag.module';
|
||||||
import { ShareModule } from './api-v1/share/share.module';
|
|
||||||
import { DomainModule } from '@app/domain';
|
import { DomainModule } from '@app/domain';
|
||||||
import { InfraModule } from '@app/infra';
|
import { InfraModule } from '@app/infra';
|
||||||
import {
|
import {
|
||||||
APIKeyController,
|
APIKeyController,
|
||||||
AuthController,
|
AuthController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
|
ShareController,
|
||||||
SystemConfigController,
|
SystemConfigController,
|
||||||
UserController,
|
UserController,
|
||||||
} from './controllers';
|
} from './controllers';
|
||||||
@ -53,8 +53,6 @@ import {
|
|||||||
JobModule,
|
JobModule,
|
||||||
|
|
||||||
TagModule,
|
TagModule,
|
||||||
|
|
||||||
ShareModule,
|
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
//
|
//
|
||||||
@ -62,6 +60,7 @@ import {
|
|||||||
APIKeyController,
|
APIKeyController,
|
||||||
AuthController,
|
AuthController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
|
ShareController,
|
||||||
SystemConfigController,
|
SystemConfigController,
|
||||||
UserController,
|
UserController,
|
||||||
],
|
],
|
||||||
|
@ -23,7 +23,7 @@ export const assetUploadOption: MulterOptions = {
|
|||||||
export const multerUtils = { fileFilter, filename, destination };
|
export const multerUtils = { fileFilter, filename, destination };
|
||||||
|
|
||||||
function fileFilter(req: Request, file: any, cb: any) {
|
function fileFilter(req: Request, file: any, cb: any) {
|
||||||
if (!req.user) {
|
if (!req.user || (req.user.isPublicUser && !req.user.isAllowUpload)) {
|
||||||
return cb(new UnauthorizedException());
|
return cb(new UnauthorizedException());
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@ -39,16 +39,12 @@ function fileFilter(req: Request, file: any, cb: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function destination(req: Request, file: Express.Multer.File, cb: any) {
|
function destination(req: Request, file: Express.Multer.File, cb: any) {
|
||||||
if (!req.user) {
|
if (!req.user || (req.user.isPublicUser && !req.user.isAllowUpload)) {
|
||||||
return cb(new UnauthorizedException());
|
return cb(new UnauthorizedException());
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = req.user as AuthUserDto;
|
const user = req.user as AuthUserDto;
|
||||||
|
|
||||||
if (user.isPublicUser && !user.isAllowUpload) {
|
|
||||||
return cb(new UnauthorizedException());
|
|
||||||
}
|
|
||||||
|
|
||||||
const basePath = APP_UPLOAD_LOCATION;
|
const basePath = APP_UPLOAD_LOCATION;
|
||||||
const sanitizedDeviceId = sanitize(String(req.body['deviceId']));
|
const sanitizedDeviceId = sanitize(String(req.body['deviceId']));
|
||||||
const originalUploadFolder = join(basePath, user.id, 'original', sanitizedDeviceId);
|
const originalUploadFolder = join(basePath, user.id, 'original', sanitizedDeviceId);
|
||||||
@ -62,7 +58,7 @@ function destination(req: Request, file: Express.Multer.File, cb: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function filename(req: Request, file: Express.Multer.File, cb: any) {
|
function filename(req: Request, file: Express.Multer.File, cb: any) {
|
||||||
if (!req.user) {
|
if (!req.user || (req.user.isPublicUser && !req.user.isAllowUpload)) {
|
||||||
return cb(new UnauthorizedException());
|
return cb(new UnauthorizedException());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export * from './api-key.controller';
|
export * from './api-key.controller';
|
||||||
export * from './auth.controller';
|
export * from './auth.controller';
|
||||||
export * from './oauth.controller';
|
export * from './oauth.controller';
|
||||||
|
export * from './share.controller';
|
||||||
export * from './system-config.controller';
|
export * from './system-config.controller';
|
||||||
export * from './user.controller';
|
export * from './user.controller';
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Patch, ValidationPipe } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Patch, ValidationPipe } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
||||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||||
import { EditSharedLinkDto } from './dto/edit-shared-link.dto';
|
import { AuthUserDto, EditSharedLinkDto, SharedLinkResponseDto, ShareService } from '@app/domain';
|
||||||
import { SharedLinkResponseDto } from './response-dto/shared-link-response.dto';
|
|
||||||
import { ShareService } from './share.service';
|
|
||||||
|
|
||||||
@ApiTags('share')
|
@ApiTags('share')
|
||||||
@Controller('share')
|
@Controller('share')
|
||||||
@ -24,23 +22,23 @@ export class ShareController {
|
|||||||
|
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
getSharedLinkById(@Param('id') id: string): Promise<SharedLinkResponseDto> {
|
getSharedLinkById(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise<SharedLinkResponseDto> {
|
||||||
return this.shareService.getById(id, true);
|
return this.shareService.getById(authUser, id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
removeSharedLink(@Param('id') id: string, @GetAuthUser() authUser: AuthUserDto): Promise<string> {
|
removeSharedLink(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise<void> {
|
||||||
return this.shareService.remove(id, authUser.id);
|
return this.shareService.remove(authUser, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
editSharedLink(
|
editSharedLink(
|
||||||
@Param('id') id: string,
|
|
||||||
@GetAuthUser() authUser: AuthUserDto,
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
|
@Param('id') id: string,
|
||||||
@Body(new ValidationPipe()) dto: EditSharedLinkDto,
|
@Body(new ValidationPipe()) dto: EditSharedLinkDto,
|
||||||
): Promise<SharedLinkResponseDto> {
|
): Promise<SharedLinkResponseDto> {
|
||||||
return this.shareService.edit(id, authUser, dto);
|
return this.shareService.edit(authUser, id, dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,11 +1,9 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ShareModule } from '../../api-v1/share/share.module';
|
|
||||||
import { APIKeyStrategy } from './strategies/api-key.strategy';
|
import { APIKeyStrategy } from './strategies/api-key.strategy';
|
||||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
import { PublicShareStrategy } from './strategies/public-share.strategy';
|
import { PublicShareStrategy } from './strategies/public-share.strategy';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ShareModule],
|
|
||||||
providers: [JwtStrategy, APIKeyStrategy, PublicShareStrategy],
|
providers: [JwtStrategy, APIKeyStrategy, PublicShareStrategy],
|
||||||
})
|
})
|
||||||
export class ImmichJwtModule {}
|
export class ImmichJwtModule {}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { IStrategyOptions, Strategy } from 'passport-http-header-strategy';
|
import { IStrategyOptions, Strategy } from 'passport-http-header-strategy';
|
||||||
import { ShareService } from '../../../api-v1/share/share.service';
|
import { AuthUserDto, ShareService } from '@app/domain';
|
||||||
import { AuthUserDto } from '../../../decorators/auth-user.decorator';
|
|
||||||
|
|
||||||
export const PUBLIC_SHARE_STRATEGY = 'public-share';
|
export const PUBLIC_SHARE_STRATEGY = 'public-share';
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { WebpGeneratorProcessor, JpegGeneratorProcessor, QueueName, JobName } fr
|
|||||||
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { mapAsset } from 'apps/immich/src/api-v1/asset/response-dto/asset-response.dto';
|
import { mapAsset } from '@app/domain';
|
||||||
import { Job, Queue } from 'bull';
|
import { Job, Queue } from 'bull';
|
||||||
import ffmpeg from 'fluent-ffmpeg';
|
import ffmpeg from 'fluent-ffmpeg';
|
||||||
import { existsSync, mkdirSync } from 'node:fs';
|
import { existsSync, mkdirSync } from 'node:fs';
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
import { AssetEntity } from '@app/infra';
|
import { AssetEntity } from '@app/infra';
|
||||||
import { AssetResponseDto } from 'apps/immich/src/api-v1/asset/response-dto/asset-response.dto';
|
import { AssetResponseDto } from '@app/domain';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
const deleteFiles = (asset: AssetEntity | AssetResponseDto) => {
|
const deleteFiles = (asset: AssetEntity | AssetResponseDto) => {
|
||||||
|
1
server/libs/domain/src/album/index.ts
Normal file
1
server/libs/domain/src/album/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './response-dto';
|
@ -1,7 +1,7 @@
|
|||||||
import { AlbumEntity } from '@app/infra';
|
import { AlbumEntity } from '@app/infra/db/entities';
|
||||||
import { UserResponseDto, mapUser } from '@app/domain';
|
|
||||||
import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { AssetResponseDto, mapAsset } from '../../asset';
|
||||||
|
import { mapUser, UserResponseDto } from '../../user';
|
||||||
|
|
||||||
export class AlbumResponseDto {
|
export class AlbumResponseDto {
|
||||||
id!: string;
|
id!: string;
|
1
server/libs/domain/src/album/response-dto/index.ts
Normal file
1
server/libs/domain/src/album/response-dto/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './album-response.dto';
|
1
server/libs/domain/src/asset/index.ts
Normal file
1
server/libs/domain/src/asset/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './response-dto';
|
@ -1,6 +1,6 @@
|
|||||||
import { AssetEntity, AssetType } from '@app/infra';
|
import { AssetEntity, AssetType } from '@app/infra/db/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { mapTag, TagResponseDto } from '../../tag/response-dto/tag-response.dto';
|
import { mapTag, TagResponseDto } from '../../tag';
|
||||||
import { ExifResponseDto, mapExif } from './exif-response.dto';
|
import { ExifResponseDto, mapExif } from './exif-response.dto';
|
||||||
import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto';
|
import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto';
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
import { ExifEntity } from '@app/infra';
|
import { ExifEntity } from '@app/infra/db/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class ExifResponseDto {
|
export class ExifResponseDto {
|
||||||
@ -29,7 +29,7 @@ export class ExifResponseDto {
|
|||||||
|
|
||||||
export function mapExif(entity: ExifEntity): ExifResponseDto {
|
export function mapExif(entity: ExifEntity): ExifResponseDto {
|
||||||
return {
|
return {
|
||||||
id: parseInt(entity.id),
|
id: entity.id,
|
||||||
make: entity.make,
|
make: entity.make,
|
||||||
model: entity.model,
|
model: entity.model,
|
||||||
imageName: entity.imageName,
|
imageName: entity.imageName,
|
3
server/libs/domain/src/asset/response-dto/index.ts
Normal file
3
server/libs/domain/src/asset/response-dto/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './asset-response.dto';
|
||||||
|
export * from './exif-response.dto';
|
||||||
|
export * from './smart-info-response.dto';
|
@ -1,4 +1,4 @@
|
|||||||
import { SmartInfoEntity } from '@app/infra';
|
import { SmartInfoEntity } from '@app/infra/db/entities';
|
||||||
|
|
||||||
export class SmartInfoResponseDto {
|
export class SmartInfoResponseDto {
|
||||||
id?: string;
|
id?: string;
|
@ -1,5 +1,6 @@
|
|||||||
import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common';
|
import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common';
|
||||||
import { APIKeyService } from './api-key';
|
import { APIKeyService } from './api-key';
|
||||||
|
import { ShareService } from './share';
|
||||||
import { AuthService } from './auth';
|
import { AuthService } from './auth';
|
||||||
import { OAuthService } from './oauth';
|
import { OAuthService } from './oauth';
|
||||||
import { INITIAL_SYSTEM_CONFIG, SystemConfigService } from './system-config';
|
import { INITIAL_SYSTEM_CONFIG, SystemConfigService } from './system-config';
|
||||||
@ -11,6 +12,7 @@ const providers: Provider[] = [
|
|||||||
OAuthService,
|
OAuthService,
|
||||||
SystemConfigService,
|
SystemConfigService,
|
||||||
UserService,
|
UserService,
|
||||||
|
ShareService,
|
||||||
|
|
||||||
{
|
{
|
||||||
provide: INITIAL_SYSTEM_CONFIG,
|
provide: INITIAL_SYSTEM_CONFIG,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
export * from './album';
|
||||||
export * from './api-key';
|
export * from './api-key';
|
||||||
|
export * from './asset';
|
||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './domain.module';
|
export * from './domain.module';
|
||||||
export * from './job';
|
export * from './job';
|
||||||
export * from './oauth';
|
export * from './oauth';
|
||||||
|
export * from './share';
|
||||||
export * from './system-config';
|
export * from './system-config';
|
||||||
|
export * from './tag';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
@ -25,7 +25,7 @@ export interface IVideoLengthExtractionProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IReverseGeocodingProcessor {
|
export interface IReverseGeocodingProcessor {
|
||||||
exifId: string;
|
exifId: number;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { AlbumEntity, AssetEntity } from '@app/infra';
|
import { AlbumEntity, AssetEntity, SharedLinkType } from '@app/infra/db/entities';
|
||||||
import { SharedLinkType } from '@app/infra';
|
|
||||||
|
|
||||||
export class CreateSharedLinkDto {
|
export class CreateSharedLinkDto {
|
||||||
description?: string;
|
description?: string;
|
||||||
expiredAt?: string;
|
expiresAt?: string;
|
||||||
sharedType!: SharedLinkType;
|
type!: SharedLinkType;
|
||||||
assets!: AssetEntity[];
|
assets!: AssetEntity[];
|
||||||
album?: AlbumEntity;
|
album?: AlbumEntity;
|
||||||
allowUpload?: boolean;
|
allowUpload?: boolean;
|
@ -1,11 +1,11 @@
|
|||||||
import { IsNotEmpty, IsOptional } from 'class-validator';
|
import { IsOptional } from 'class-validator';
|
||||||
|
|
||||||
export class EditSharedLinkDto {
|
export class EditSharedLinkDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
expiredAt?: string;
|
expiresAt?: string | null;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
allowUpload?: boolean;
|
allowUpload?: boolean;
|
||||||
@ -15,7 +15,4 @@ export class EditSharedLinkDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
showExif?: boolean;
|
showExif?: boolean;
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
isEditExpireTime?: boolean;
|
|
||||||
}
|
}
|
2
server/libs/domain/src/share/dto/index.ts
Normal file
2
server/libs/domain/src/share/dto/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './create-shared-link.dto';
|
||||||
|
export * from './edit-shared-link.dto';
|
5
server/libs/domain/src/share/index.ts
Normal file
5
server/libs/domain/src/share/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './dto';
|
||||||
|
export * from './response-dto';
|
||||||
|
export * from './share.core';
|
||||||
|
export * from './share.service';
|
||||||
|
export * from './shared-link.repository';
|
1
server/libs/domain/src/share/response-dto/index.ts
Normal file
1
server/libs/domain/src/share/response-dto/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './shared-link-response.dto';
|
@ -1,8 +1,8 @@
|
|||||||
import { SharedLinkEntity, SharedLinkType } from '@app/infra';
|
import { SharedLinkEntity, SharedLinkType } from '@app/infra/db/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../../album/response-dto/album-response.dto';
|
import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../../album';
|
||||||
import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../../asset/response-dto/asset-response.dto';
|
import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../../asset';
|
||||||
|
|
||||||
export class SharedLinkResponseDto {
|
export class SharedLinkResponseDto {
|
||||||
id!: string;
|
id!: string;
|
81
server/libs/domain/src/share/share.core.ts
Normal file
81
server/libs/domain/src/share/share.core.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { AssetEntity, SharedLinkEntity } from '@app/infra/db/entities';
|
||||||
|
import { BadRequestException, ForbiddenException, InternalServerErrorException, Logger } from '@nestjs/common';
|
||||||
|
import { AuthUserDto, ICryptoRepository } from '../auth';
|
||||||
|
import { CreateSharedLinkDto } from './dto';
|
||||||
|
import { ISharedLinkRepository } from './shared-link.repository';
|
||||||
|
|
||||||
|
export class ShareCore {
|
||||||
|
readonly logger = new Logger(ShareCore.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
getByKey(key: string): Promise<SharedLinkEntity | null> {
|
||||||
|
return this.repository.getByKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(userId: string, dto: CreateSharedLinkDto): Promise<SharedLinkEntity> {
|
||||||
|
try {
|
||||||
|
return this.repository.create({
|
||||||
|
key: Buffer.from(this.cryptoRepository.randomBytes(50)),
|
||||||
|
description: dto.description,
|
||||||
|
userId,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
expiresAt: dto.expiresAt ?? null,
|
||||||
|
type: dto.type,
|
||||||
|
assets: dto.assets,
|
||||||
|
album: dto.album,
|
||||||
|
allowUpload: dto.allowUpload ?? false,
|
||||||
|
allowDownload: dto.allowDownload ?? true,
|
||||||
|
showExif: dto.showExif ?? true,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(error, error.stack);
|
||||||
|
throw new InternalServerErrorException('failed to create shared link');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<SharedLinkEntity> {
|
||||||
|
const link = await this.get(userId, id);
|
||||||
|
if (!link) {
|
||||||
|
throw new BadRequestException('Shared link not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.repository.remove(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAssets(userId: string, id: string, assets: AssetEntity[]) {
|
||||||
|
const link = await this.get(userId, id);
|
||||||
|
if (!link) {
|
||||||
|
throw new BadRequestException('Shared link not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.repository.save({ ...link, assets });
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {
|
||||||
|
return this.repository.hasAssetAccess(id, assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDownloadAccess(user: AuthUserDto) {
|
||||||
|
if (user.isPublicUser && !user.isAllowDownload) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
170
server/libs/domain/src/share/share.service.spec.ts
Normal file
170
server/libs/domain/src/share/share.service.spec.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
authStub,
|
||||||
|
entityStub,
|
||||||
|
newCryptoRepositoryMock,
|
||||||
|
newSharedLinkRepositoryMock,
|
||||||
|
newUserRepositoryMock,
|
||||||
|
sharedLinkResponseStub,
|
||||||
|
sharedLinkStub,
|
||||||
|
} from '../../test';
|
||||||
|
import { ICryptoRepository } from '../auth';
|
||||||
|
import { IUserRepository } from '../user';
|
||||||
|
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>;
|
||||||
|
let userMock: jest.Mocked<IUserRepository>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
|
shareMock = newSharedLinkRepositoryMock();
|
||||||
|
userMock = newUserRepositoryMock();
|
||||||
|
|
||||||
|
sut = new ShareService(cryptoMock, shareMock, userMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work', () => {
|
||||||
|
expect(sut).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validate', () => {
|
||||||
|
it('should not accept a non-existant key', async () => {
|
||||||
|
shareMock.getByKey.mockResolvedValue(null);
|
||||||
|
await expect(sut.validate('key')).rejects.toBeInstanceOf(UnauthorizedException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not accept an expired key', async () => {
|
||||||
|
shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
|
||||||
|
await expect(sut.validate('key')).rejects.toBeInstanceOf(UnauthorizedException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not accept a key without a user', async () => {
|
||||||
|
shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
|
||||||
|
userMock.get.mockResolvedValue(null);
|
||||||
|
await expect(sut.validate('key')).rejects.toBeInstanceOf(UnauthorizedException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept a valid key', async () => {
|
||||||
|
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
||||||
|
userMock.get.mockResolvedValue(entityStub.admin);
|
||||||
|
await expect(sut.validate('key')).resolves.toEqual(authStub.adminSharedLink);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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('getByKey', () => {
|
||||||
|
it('should not work on a missing key', async () => {
|
||||||
|
shareMock.getByKey.mockResolvedValue(null);
|
||||||
|
await expect(sut.getByKey('secret-key')).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
expect(shareMock.getByKey).toHaveBeenCalledWith('secret-key');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a key', async () => {
|
||||||
|
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
||||||
|
await expect(sut.getByKey('secret-key')).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||||
|
expect(shareMock.getByKey).toHaveBeenCalledWith('secret-key');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -6,10 +6,10 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { UserService } from '@app/domain';
|
import { AuthUserDto, ICryptoRepository } from '../auth';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { IUserRepository, UserCore } from '../user';
|
||||||
import { EditSharedLinkDto } from './dto/edit-shared-link.dto';
|
import { EditSharedLinkDto } from './dto';
|
||||||
import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto/shared-link-response.dto';
|
import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto';
|
||||||
import { ShareCore } from './share.core';
|
import { ShareCore } from './share.core';
|
||||||
import { ISharedLinkRepository } from './shared-link.repository';
|
import { ISharedLinkRepository } from './shared-link.repository';
|
||||||
|
|
||||||
@ -17,20 +17,22 @@ import { ISharedLinkRepository } from './shared-link.repository';
|
|||||||
export class ShareService {
|
export class ShareService {
|
||||||
readonly logger = new Logger(ShareService.name);
|
readonly logger = new Logger(ShareService.name);
|
||||||
private shareCore: ShareCore;
|
private shareCore: ShareCore;
|
||||||
|
private userCore: UserCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ISharedLinkRepository)
|
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||||
sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
||||||
private userService: UserService,
|
@Inject(IUserRepository) userRepository: IUserRepository,
|
||||||
) {
|
) {
|
||||||
this.shareCore = new ShareCore(sharedLinkRepository);
|
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
|
||||||
|
this.userCore = new UserCore(userRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(key: string): Promise<AuthUserDto> {
|
async validate(key: string): Promise<AuthUserDto> {
|
||||||
const link = await this.shareCore.getSharedLinkByKey(key);
|
const link = await this.shareCore.getByKey(key);
|
||||||
if (link) {
|
if (link) {
|
||||||
if (!link.expiresAt || new Date(link.expiresAt) > new Date()) {
|
if (!link.expiresAt || new Date(link.expiresAt) > new Date()) {
|
||||||
const user = await this.userService.getUserById(link.userId).catch(() => null);
|
const user = await this.userCore.get(link.userId);
|
||||||
if (user) {
|
if (user) {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@ -49,7 +51,7 @@ export class ShareService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
|
async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
|
||||||
const links = await this.shareCore.getSharedLinks(authUser.id);
|
const links = await this.shareCore.getAll(authUser.id);
|
||||||
return links.map(mapSharedLink);
|
return links.map(mapSharedLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,11 +65,11 @@ export class ShareService {
|
|||||||
allowExif = authUser.isShowExif;
|
allowExif = authUser.isShowExif;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getById(authUser.sharedLinkId, allowExif);
|
return this.getById(authUser, authUser.sharedLinkId, allowExif);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(id: string, allowExif: boolean): Promise<SharedLinkResponseDto> {
|
async getById(authUser: AuthUserDto, id: string, allowExif: boolean): Promise<SharedLinkResponseDto> {
|
||||||
const link = await this.shareCore.getSharedLinkById(id);
|
const link = await this.shareCore.get(authUser.id, id);
|
||||||
if (!link) {
|
if (!link) {
|
||||||
throw new BadRequestException('Shared link not found');
|
throw new BadRequestException('Shared link not found');
|
||||||
}
|
}
|
||||||
@ -79,21 +81,20 @@ export class ShareService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: string, userId: string): Promise<string> {
|
|
||||||
await this.shareCore.removeSharedLink(id, userId);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getByKey(key: string): Promise<SharedLinkResponseDto> {
|
async getByKey(key: string): Promise<SharedLinkResponseDto> {
|
||||||
const link = await this.shareCore.getSharedLinkByKey(key);
|
const link = await this.shareCore.getByKey(key);
|
||||||
if (!link) {
|
if (!link) {
|
||||||
throw new BadRequestException('Shared link not found');
|
throw new BadRequestException('Shared link not found');
|
||||||
}
|
}
|
||||||
return mapSharedLink(link);
|
return mapSharedLink(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
async edit(id: string, authUser: AuthUserDto, dto: EditSharedLinkDto) {
|
async remove(authUser: AuthUserDto, id: string): Promise<void> {
|
||||||
const link = await this.shareCore.updateSharedLink(id, authUser.id, dto);
|
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);
|
return mapSharedLink(link);
|
||||||
}
|
}
|
||||||
}
|
}
|
13
server/libs/domain/src/share/shared-link.repository.ts
Normal file
13
server/libs/domain/src/share/shared-link.repository.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { SharedLinkEntity } from '@app/infra/db/entities';
|
||||||
|
|
||||||
|
export const ISharedLinkRepository = 'ISharedLinkRepository';
|
||||||
|
|
||||||
|
export interface ISharedLinkRepository {
|
||||||
|
getAll(userId: string): Promise<SharedLinkEntity[]>;
|
||||||
|
get(userId: string, id: string): Promise<SharedLinkEntity | null>;
|
||||||
|
getByKey(key: string): Promise<SharedLinkEntity | null>;
|
||||||
|
create(entity: Omit<SharedLinkEntity, 'id'>): Promise<SharedLinkEntity>;
|
||||||
|
remove(entity: SharedLinkEntity): Promise<SharedLinkEntity>;
|
||||||
|
save(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
|
||||||
|
hasAssetAccess(id: string, assetId: string): Promise<boolean>;
|
||||||
|
}
|
1
server/libs/domain/src/tag/index.ts
Normal file
1
server/libs/domain/src/tag/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './response-dto';
|
1
server/libs/domain/src/tag/response-dto/index.ts
Normal file
1
server/libs/domain/src/tag/response-dto/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './tag-response.dto';
|
@ -1,4 +1,4 @@
|
|||||||
import { TagEntity, TagType } from '@app/infra';
|
import { TagEntity, TagType } from '@app/infra/db/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class TagResponseDto {
|
export class TagResponseDto {
|
@ -1,5 +1,71 @@
|
|||||||
import { SystemConfig, UserEntity } from '@app/infra/db/entities';
|
import { AssetType, SharedLinkEntity, SharedLinkType, SystemConfig, UserEntity } from '@app/infra/db/entities';
|
||||||
import { AuthUserDto } from '../src';
|
import { AlbumResponseDto, AssetResponseDto, AuthUserDto, ExifResponseDto, SharedLinkResponseDto } from '../src';
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const tomorrow = new Date();
|
||||||
|
const yesterday = new Date();
|
||||||
|
tomorrow.setDate(today.getDate() + 1);
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
|
||||||
|
const assetInfo: ExifResponseDto = {
|
||||||
|
id: 1,
|
||||||
|
make: 'camera-make',
|
||||||
|
model: 'camera-model',
|
||||||
|
imageName: 'fancy-image',
|
||||||
|
exifImageWidth: 500,
|
||||||
|
exifImageHeight: 500,
|
||||||
|
fileSizeInByte: 100,
|
||||||
|
orientation: 'orientation',
|
||||||
|
dateTimeOriginal: today,
|
||||||
|
modifyDate: today,
|
||||||
|
lensModel: 'fancy',
|
||||||
|
fNumber: 100,
|
||||||
|
focalLength: 100,
|
||||||
|
iso: 100,
|
||||||
|
exposureTime: 100,
|
||||||
|
latitude: 100,
|
||||||
|
longitude: 100,
|
||||||
|
city: 'city',
|
||||||
|
state: 'state',
|
||||||
|
country: 'country',
|
||||||
|
};
|
||||||
|
|
||||||
|
const assetResponse: AssetResponseDto = {
|
||||||
|
id: 'id_1',
|
||||||
|
deviceAssetId: 'device_asset_id_1',
|
||||||
|
ownerId: 'user_id_1',
|
||||||
|
deviceId: 'device_id_1',
|
||||||
|
type: AssetType.VIDEO,
|
||||||
|
originalPath: 'fake_path/jpeg',
|
||||||
|
resizePath: '',
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
modifiedAt: today.toISOString(),
|
||||||
|
isFavorite: false,
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
smartInfo: {
|
||||||
|
id: 'should-be-a-number',
|
||||||
|
tags: [],
|
||||||
|
objects: ['a', 'b', 'c'],
|
||||||
|
},
|
||||||
|
webpPath: '',
|
||||||
|
encodedVideoPath: '',
|
||||||
|
duration: '0:00:00.00000',
|
||||||
|
exifInfo: assetInfo,
|
||||||
|
livePhotoVideoId: null,
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const albumResponse: AlbumResponseDto = {
|
||||||
|
albumName: 'Test Album',
|
||||||
|
albumThumbnailAssetId: null,
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
id: 'album-123',
|
||||||
|
ownerId: 'admin_id',
|
||||||
|
sharedUsers: [],
|
||||||
|
shared: false,
|
||||||
|
assets: [],
|
||||||
|
assetCount: 1,
|
||||||
|
};
|
||||||
|
|
||||||
export const authStub = {
|
export const authStub = {
|
||||||
admin: Object.freeze<AuthUserDto>({
|
admin: Object.freeze<AuthUserDto>({
|
||||||
@ -16,6 +82,26 @@ export const authStub = {
|
|||||||
isPublicUser: false,
|
isPublicUser: false,
|
||||||
isAllowUpload: true,
|
isAllowUpload: true,
|
||||||
}),
|
}),
|
||||||
|
adminSharedLink: Object.freeze<AuthUserDto>({
|
||||||
|
id: 'admin_id',
|
||||||
|
email: 'admin@test.com',
|
||||||
|
isAdmin: true,
|
||||||
|
isAllowUpload: true,
|
||||||
|
isAllowDownload: true,
|
||||||
|
isPublicUser: true,
|
||||||
|
isShowExif: true,
|
||||||
|
sharedLinkId: '123',
|
||||||
|
}),
|
||||||
|
readonlySharedLink: Object.freeze<AuthUserDto>({
|
||||||
|
id: 'admin_id',
|
||||||
|
email: 'admin@test.com',
|
||||||
|
isAdmin: true,
|
||||||
|
isAllowUpload: false,
|
||||||
|
isAllowDownload: false,
|
||||||
|
isPublicUser: true,
|
||||||
|
isShowExif: true,
|
||||||
|
sharedLinkId: '123',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const entityStub = {
|
export const entityStub = {
|
||||||
@ -165,3 +251,175 @@ export const loginResponseStub = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sharedLinkStub = {
|
||||||
|
valid: Object.freeze({
|
||||||
|
id: '123',
|
||||||
|
userId: authStub.admin.id,
|
||||||
|
key: Buffer.from('secret-key', 'utf8'),
|
||||||
|
type: SharedLinkType.ALBUM,
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
expiresAt: tomorrow.toISOString(),
|
||||||
|
allowUpload: true,
|
||||||
|
allowDownload: true,
|
||||||
|
showExif: true,
|
||||||
|
album: undefined,
|
||||||
|
assets: [],
|
||||||
|
} as SharedLinkEntity),
|
||||||
|
expired: Object.freeze({
|
||||||
|
id: '123',
|
||||||
|
userId: authStub.admin.id,
|
||||||
|
key: Buffer.from('secret-key', 'utf8'),
|
||||||
|
type: SharedLinkType.ALBUM,
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
expiresAt: yesterday.toISOString(),
|
||||||
|
allowUpload: true,
|
||||||
|
allowDownload: true,
|
||||||
|
showExif: true,
|
||||||
|
assets: [],
|
||||||
|
} as SharedLinkEntity),
|
||||||
|
readonly: Object.freeze<SharedLinkEntity>({
|
||||||
|
id: '123',
|
||||||
|
userId: authStub.admin.id,
|
||||||
|
key: Buffer.from('secret-key', 'utf8'),
|
||||||
|
type: SharedLinkType.ALBUM,
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
expiresAt: tomorrow.toISOString(),
|
||||||
|
allowUpload: false,
|
||||||
|
allowDownload: false,
|
||||||
|
showExif: true,
|
||||||
|
assets: [],
|
||||||
|
album: {
|
||||||
|
id: 'album-123',
|
||||||
|
ownerId: authStub.admin.id,
|
||||||
|
albumName: 'Test Album',
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
albumThumbnailAssetId: null,
|
||||||
|
sharedUsers: [],
|
||||||
|
sharedLinks: [],
|
||||||
|
assets: [
|
||||||
|
{
|
||||||
|
id: 'album-asset-123',
|
||||||
|
albumId: 'album-123',
|
||||||
|
assetId: 'asset-123',
|
||||||
|
albumInfo: {} as any,
|
||||||
|
assetInfo: {
|
||||||
|
id: 'id_1',
|
||||||
|
userId: 'user_id_1',
|
||||||
|
deviceAssetId: 'device_asset_id_1',
|
||||||
|
deviceId: 'device_id_1',
|
||||||
|
type: AssetType.VIDEO,
|
||||||
|
originalPath: 'fake_path/jpeg',
|
||||||
|
resizePath: '',
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
modifiedAt: today.toISOString(),
|
||||||
|
isFavorite: false,
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
smartInfo: {
|
||||||
|
id: 'should-be-a-number',
|
||||||
|
assetId: 'id_1',
|
||||||
|
tags: [],
|
||||||
|
objects: ['a', 'b', 'c'],
|
||||||
|
asset: null as any,
|
||||||
|
},
|
||||||
|
webpPath: '',
|
||||||
|
encodedVideoPath: '',
|
||||||
|
duration: null,
|
||||||
|
isVisible: true,
|
||||||
|
livePhotoVideoId: null,
|
||||||
|
exifInfo: {
|
||||||
|
id: 1,
|
||||||
|
assetId: 'id_1',
|
||||||
|
description: 'description',
|
||||||
|
exifImageWidth: 500,
|
||||||
|
exifImageHeight: 500,
|
||||||
|
fileSizeInByte: 100,
|
||||||
|
orientation: 'orientation',
|
||||||
|
dateTimeOriginal: today,
|
||||||
|
modifyDate: today,
|
||||||
|
latitude: 100,
|
||||||
|
longitude: 100,
|
||||||
|
city: 'city',
|
||||||
|
state: 'state',
|
||||||
|
country: 'country',
|
||||||
|
make: 'camera-make',
|
||||||
|
model: 'camera-model',
|
||||||
|
imageName: 'fancy-image',
|
||||||
|
lensModel: 'fancy',
|
||||||
|
fNumber: 100,
|
||||||
|
focalLength: 100,
|
||||||
|
iso: 100,
|
||||||
|
exposureTime: 100,
|
||||||
|
fps: 100,
|
||||||
|
asset: null as any,
|
||||||
|
exifTextSearchableColumn: '',
|
||||||
|
},
|
||||||
|
tags: [],
|
||||||
|
sharedLinks: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sharedLinkResponseStub = {
|
||||||
|
valid: Object.freeze<SharedLinkResponseDto>({
|
||||||
|
allowDownload: true,
|
||||||
|
allowUpload: true,
|
||||||
|
assets: [],
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
description: undefined,
|
||||||
|
expiresAt: tomorrow.toISOString(),
|
||||||
|
id: '123',
|
||||||
|
key: '7365637265742d6b6579',
|
||||||
|
showExif: true,
|
||||||
|
type: SharedLinkType.ALBUM,
|
||||||
|
userId: 'admin_id',
|
||||||
|
}),
|
||||||
|
expired: Object.freeze<SharedLinkResponseDto>({
|
||||||
|
album: undefined,
|
||||||
|
allowDownload: true,
|
||||||
|
allowUpload: true,
|
||||||
|
assets: [],
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
description: undefined,
|
||||||
|
expiresAt: yesterday.toISOString(),
|
||||||
|
id: '123',
|
||||||
|
key: '7365637265742d6b6579',
|
||||||
|
showExif: true,
|
||||||
|
type: SharedLinkType.ALBUM,
|
||||||
|
userId: 'admin_id',
|
||||||
|
}),
|
||||||
|
readonly: Object.freeze<SharedLinkResponseDto>({
|
||||||
|
id: '123',
|
||||||
|
userId: 'admin_id',
|
||||||
|
key: '7365637265742d6b6579',
|
||||||
|
type: SharedLinkType.ALBUM,
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
expiresAt: tomorrow.toISOString(),
|
||||||
|
description: undefined,
|
||||||
|
allowUpload: false,
|
||||||
|
allowDownload: false,
|
||||||
|
showExif: true,
|
||||||
|
album: albumResponse,
|
||||||
|
assets: [assetResponse],
|
||||||
|
}),
|
||||||
|
readonlyNoExif: Object.freeze<SharedLinkResponseDto>({
|
||||||
|
id: '123',
|
||||||
|
userId: 'admin_id',
|
||||||
|
key: '7365637265742d6b6579',
|
||||||
|
type: SharedLinkType.ALBUM,
|
||||||
|
createdAt: today.toISOString(),
|
||||||
|
expiresAt: tomorrow.toISOString(),
|
||||||
|
description: undefined,
|
||||||
|
allowUpload: false,
|
||||||
|
allowDownload: false,
|
||||||
|
showExif: true,
|
||||||
|
album: albumResponse,
|
||||||
|
assets: [{ ...assetResponse, exifInfo: undefined }],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO - the constructor isn't used anywhere, so not test coverage
|
||||||
|
new ExifResponseDto();
|
||||||
|
@ -2,5 +2,6 @@ export * from './api-key.repository.mock';
|
|||||||
export * from './crypto.repository.mock';
|
export * from './crypto.repository.mock';
|
||||||
export * from './fixtures';
|
export * from './fixtures';
|
||||||
export * from './job.repository.mock';
|
export * from './job.repository.mock';
|
||||||
|
export * from './shared-link.repository.mock';
|
||||||
export * from './system-config.repository.mock';
|
export * from './system-config.repository.mock';
|
||||||
export * from './user.repository.mock';
|
export * from './user.repository.mock';
|
||||||
|
13
server/libs/domain/test/shared-link.repository.mock.ts
Normal file
13
server/libs/domain/test/shared-link.repository.mock.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ISharedLinkRepository } from '../src';
|
||||||
|
|
||||||
|
export const newSharedLinkRepositoryMock = (): jest.Mocked<ISharedLinkRepository> => {
|
||||||
|
return {
|
||||||
|
getAll: jest.fn(),
|
||||||
|
get: jest.fn(),
|
||||||
|
getByKey: jest.fn(),
|
||||||
|
create: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
save: jest.fn(),
|
||||||
|
hasAssetAccess: jest.fn(),
|
||||||
|
};
|
||||||
|
};
|
@ -7,7 +7,7 @@ import { AssetEntity } from './asset.entity';
|
|||||||
@Entity('exif')
|
@Entity('exif')
|
||||||
export class ExifEntity {
|
export class ExifEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: string;
|
id!: number;
|
||||||
|
|
||||||
@Index({ unique: true })
|
@Index({ unique: true })
|
||||||
@Column({ type: 'uuid' })
|
@Column({ type: 'uuid' })
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './api-key.repository';
|
export * from './api-key.repository';
|
||||||
|
export * from './shared-link.repository';
|
||||||
export * from './user.repository';
|
export * from './user.repository';
|
||||||
|
119
server/libs/infra/src/db/repository/shared-link.repository.ts
Normal file
119
server/libs/infra/src/db/repository/shared-link.repository.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { ISharedLinkRepository } from '@app/domain';
|
||||||
|
import { Injectable, Logger } 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>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get(userId: string, id: string): Promise<SharedLinkEntity | null> {
|
||||||
|
return this.repository.findOne({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
assets: {
|
||||||
|
exifInfo: true,
|
||||||
|
},
|
||||||
|
album: {
|
||||||
|
assets: {
|
||||||
|
assetInfo: {
|
||||||
|
exifInfo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: 'DESC',
|
||||||
|
assets: {
|
||||||
|
createdAt: 'ASC',
|
||||||
|
},
|
||||||
|
album: {
|
||||||
|
assets: {
|
||||||
|
assetInfo: {
|
||||||
|
createdAt: 'ASC',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(userId: string): Promise<SharedLinkEntity[]> {
|
||||||
|
return this.repository.find({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
assets: true,
|
||||||
|
album: true,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: 'DESC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByKey(key: string): Promise<SharedLinkEntity | null> {
|
||||||
|
return await this.repository.findOne({
|
||||||
|
where: {
|
||||||
|
key: Buffer.from(key, 'hex'),
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
assets: true,
|
||||||
|
album: {
|
||||||
|
assets: {
|
||||||
|
assetInfo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: 'DESC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
create(entity: Omit<SharedLinkEntity, 'id'>): Promise<SharedLinkEntity> {
|
||||||
|
return this.repository.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(entity: SharedLinkEntity): Promise<SharedLinkEntity> {
|
||||||
|
return 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({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
album: {
|
||||||
|
assets: {
|
||||||
|
assetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(count1 + count2);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import {
|
|||||||
ICryptoRepository,
|
ICryptoRepository,
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
IKeyRepository,
|
IKeyRepository,
|
||||||
|
ISharedLinkRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
QueueName,
|
QueueName,
|
||||||
@ -11,10 +12,10 @@ import { BullModule } from '@nestjs/bull';
|
|||||||
import { Global, Module, Provider } from '@nestjs/common';
|
import { Global, Module, Provider } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { APIKeyEntity, SharedLinkEntity, SystemConfigEntity, UserRepository } from './db';
|
||||||
|
import { APIKeyRepository, SharedLinkRepository } from './db/repository';
|
||||||
import { jwtConfig } from '@app/domain';
|
import { jwtConfig } from '@app/domain';
|
||||||
import { CryptoRepository } from './auth/crypto.repository';
|
import { CryptoRepository } from './auth/crypto.repository';
|
||||||
import { APIKeyEntity, SystemConfigEntity, UserRepository } from './db';
|
|
||||||
import { APIKeyRepository } from './db/repository';
|
|
||||||
import { SystemConfigRepository } from './db/repository/system-config.repository';
|
import { SystemConfigRepository } from './db/repository/system-config.repository';
|
||||||
import { JobRepository } from './job';
|
import { JobRepository } from './job';
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ const providers: Provider[] = [
|
|||||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||||
{ provide: IKeyRepository, useClass: APIKeyRepository },
|
{ provide: IKeyRepository, useClass: APIKeyRepository },
|
||||||
{ provide: IJobRepository, useClass: JobRepository },
|
{ provide: IJobRepository, useClass: JobRepository },
|
||||||
|
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||||
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
||||||
{ provide: IUserRepository, useClass: UserRepository },
|
{ provide: IUserRepository, useClass: UserRepository },
|
||||||
];
|
];
|
||||||
@ -31,7 +33,7 @@ const providers: Provider[] = [
|
|||||||
imports: [
|
imports: [
|
||||||
JwtModule.register(jwtConfig),
|
JwtModule.register(jwtConfig),
|
||||||
TypeOrmModule.forRoot(databaseConfig),
|
TypeOrmModule.forRoot(databaseConfig),
|
||||||
TypeOrmModule.forFeature([APIKeyEntity, UserEntity, SystemConfigEntity]),
|
TypeOrmModule.forFeature([APIKeyEntity, UserEntity, SharedLinkEntity, SystemConfigEntity]),
|
||||||
BullModule.forRootAsync({
|
BullModule.forRootAsync({
|
||||||
useFactory: async () => ({
|
useFactory: async () => ({
|
||||||
prefix: 'immich_bull',
|
prefix: 'immich_bull',
|
||||||
|
16
web/src/api/open-api/api.ts
generated
16
web/src/api/open-api/api.ts
generated
@ -658,7 +658,7 @@ export interface CreateAlbumShareLinkDto {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof CreateAlbumShareLinkDto
|
* @memberof CreateAlbumShareLinkDto
|
||||||
*/
|
*/
|
||||||
'expiredAt'?: string;
|
'expiresAt'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@ -701,7 +701,7 @@ export interface CreateAssetsShareLinkDto {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof CreateAssetsShareLinkDto
|
* @memberof CreateAssetsShareLinkDto
|
||||||
*/
|
*/
|
||||||
'expiredAt'?: string;
|
'expiresAt'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@ -1004,7 +1004,7 @@ export interface EditSharedLinkDto {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof EditSharedLinkDto
|
* @memberof EditSharedLinkDto
|
||||||
*/
|
*/
|
||||||
'expiredAt'?: string;
|
'expiresAt'?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@ -1023,12 +1023,6 @@ export interface EditSharedLinkDto {
|
|||||||
* @memberof EditSharedLinkDto
|
* @memberof EditSharedLinkDto
|
||||||
*/
|
*/
|
||||||
'showExif'?: boolean;
|
'showExif'?: boolean;
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
* @memberof EditSharedLinkDto
|
|
||||||
*/
|
|
||||||
'isEditExpireTime'?: boolean;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -6745,7 +6739,7 @@ export const ShareApiFp = function(configuration?: Configuration) {
|
|||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async removeSharedLink(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {
|
async removeSharedLink(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.removeSharedLink(id, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.removeSharedLink(id, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
@ -6800,7 +6794,7 @@ export const ShareApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
removeSharedLink(id: string, options?: any): AxiosPromise<string> {
|
removeSharedLink(id: string, options?: any): AxiosPromise<void> {
|
||||||
return localVarFp.removeSharedLink(id, options).then((request) => request(axios, basePath));
|
return localVarFp.removeSharedLink(id, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
if (shareType === SharedLinkType.Album && album) {
|
if (shareType === SharedLinkType.Album && album) {
|
||||||
const { data } = await api.albumApi.createAlbumSharedLink({
|
const { data } = await api.albumApi.createAlbumSharedLink({
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
expiredAt: expirationDate,
|
expiresAt: expirationDate,
|
||||||
allowUpload: isAllowUpload,
|
allowUpload: isAllowUpload,
|
||||||
description: description,
|
description: description,
|
||||||
allowDownload: isAllowDownload,
|
allowDownload: isAllowDownload,
|
||||||
@ -70,7 +70,7 @@
|
|||||||
} else {
|
} else {
|
||||||
const { data } = await api.assetApi.createAssetsSharedLink({
|
const { data } = await api.assetApi.createAssetsSharedLink({
|
||||||
assetIds: sharedAssets.map((a) => a.id),
|
assetIds: sharedAssets.map((a) => a.id),
|
||||||
expiredAt: expirationDate,
|
expiresAt: expirationDate,
|
||||||
allowUpload: isAllowUpload,
|
allowUpload: isAllowUpload,
|
||||||
description: description,
|
description: description,
|
||||||
allowDownload: isAllowDownload,
|
allowDownload: isAllowDownload,
|
||||||
@ -128,19 +128,14 @@
|
|||||||
try {
|
try {
|
||||||
const expirationTime = getExpirationTimeInMillisecond();
|
const expirationTime = getExpirationTimeInMillisecond();
|
||||||
const currentTime = new Date().getTime();
|
const currentTime = new Date().getTime();
|
||||||
let expirationDate = expirationTime
|
const expirationDate: string | null = expirationTime
|
||||||
? new Date(currentTime + expirationTime).toISOString()
|
? new Date(currentTime + expirationTime).toISOString()
|
||||||
: undefined;
|
: null;
|
||||||
|
|
||||||
if (expirationTime === 0) {
|
|
||||||
expirationDate = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.shareApi.editSharedLink(editingLink.id, {
|
await api.shareApi.editSharedLink(editingLink.id, {
|
||||||
description: description,
|
description,
|
||||||
expiredAt: expirationDate,
|
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
||||||
allowUpload: isAllowUpload,
|
allowUpload: isAllowUpload,
|
||||||
isEditExpireTime: shouldChangeExpirationTime,
|
|
||||||
allowDownload: isAllowDownload,
|
allowDownload: isAllowDownload,
|
||||||
showExif: shouldShowExif
|
showExif: shouldShowExif
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user