mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	fix(server,web): correctly remove metadata from shared links (#4464)
* wip: strip metadata * fix: authenticate time buckets * hide detail panel * fix tests * fix lint * add e2e tests * chore: open api * fix web compilation error * feat: test with asset with gps position * fix: only import fs.promises.cp * fix: cleanup mapasset * fix: format --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							4a9f58bf9b
						
					
				
				
					commit
					dadcf49eca
				
			| @@ -5770,6 +5770,9 @@ | ||||
|             "format": "date-time", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "hasMetadata": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "id": { | ||||
|             "type": "string" | ||||
|           }, | ||||
| @@ -5833,7 +5836,6 @@ | ||||
|             "type": "array" | ||||
|           }, | ||||
|           "thumbhash": { | ||||
|             "description": "base64 encoded thumbhash", | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
|           }, | ||||
| @@ -5847,7 +5849,6 @@ | ||||
|         }, | ||||
|         "required": [ | ||||
|           "type", | ||||
|           "id", | ||||
|           "deviceAssetId", | ||||
|           "deviceId", | ||||
|           "ownerId", | ||||
| @@ -5855,19 +5856,21 @@ | ||||
|           "originalPath", | ||||
|           "originalFileName", | ||||
|           "resized", | ||||
|           "thumbhash", | ||||
|           "fileCreatedAt", | ||||
|           "fileModifiedAt", | ||||
|           "updatedAt", | ||||
|           "isFavorite", | ||||
|           "isArchived", | ||||
|           "isTrashed", | ||||
|           "localDateTime", | ||||
|           "isOffline", | ||||
|           "isExternal", | ||||
|           "isReadOnly", | ||||
|           "checksum", | ||||
|           "id", | ||||
|           "thumbhash", | ||||
|           "localDateTime", | ||||
|           "duration", | ||||
|           "checksum" | ||||
|           "hasMetadata" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
| @@ -7599,7 +7602,7 @@ | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "showExif": { | ||||
|           "showMetadata": { | ||||
|             "default": true, | ||||
|             "type": "boolean" | ||||
|           }, | ||||
| @@ -7628,7 +7631,7 @@ | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "showExif": { | ||||
|           "showMetadata": { | ||||
|             "type": "boolean" | ||||
|           } | ||||
|         }, | ||||
| @@ -7670,7 +7673,7 @@ | ||||
|           "key": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "showExif": { | ||||
|           "showMetadata": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "type": { | ||||
| @@ -7691,7 +7694,7 @@ | ||||
|           "assets", | ||||
|           "allowUpload", | ||||
|           "allowDownload", | ||||
|           "showExif" | ||||
|           "showMetadata" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|   | ||||
| @@ -47,6 +47,7 @@ import { | ||||
|   BulkIdsDto, | ||||
|   MapMarkerResponseDto, | ||||
|   MemoryLaneResponseDto, | ||||
|   SanitizedAssetResponseDto, | ||||
|   TimeBucketResponseDto, | ||||
|   mapAsset, | ||||
| } from './response-dto'; | ||||
| @@ -198,10 +199,17 @@ export class AssetService { | ||||
|     return this.assetRepository.getTimeBuckets(dto); | ||||
|   } | ||||
|  | ||||
|   async getByTimeBucket(authUser: AuthUserDto, dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> { | ||||
|   async getByTimeBucket( | ||||
|     authUser: AuthUserDto, | ||||
|     dto: TimeBucketAssetDto, | ||||
|   ): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> { | ||||
|     await this.timeBucketChecks(authUser, dto); | ||||
|     const assets = await this.assetRepository.getByTimeBucket(dto.timeBucket, dto); | ||||
|     return assets.map(mapAsset); | ||||
|     if (authUser.isShowMetadata) { | ||||
|       return assets.map((asset) => mapAsset(asset)); | ||||
|     } else { | ||||
|       return assets.map((asset) => mapAsset(asset, true)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> { | ||||
|   | ||||
| @@ -6,43 +6,62 @@ import { UserResponseDto, mapUser } from '../../user/response-dto/user-response. | ||||
| import { ExifResponseDto, mapExif } from './exif-response.dto'; | ||||
| import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto'; | ||||
|  | ||||
| export class AssetResponseDto { | ||||
| export class SanitizedAssetResponseDto { | ||||
|   id!: string; | ||||
|   @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) | ||||
|   type!: AssetType; | ||||
|   thumbhash!: string | null; | ||||
|   resized!: boolean; | ||||
|   localDateTime!: Date; | ||||
|   duration!: string; | ||||
|   livePhotoVideoId?: string | null; | ||||
|   hasMetadata!: boolean; | ||||
| } | ||||
|  | ||||
| export class AssetResponseDto extends SanitizedAssetResponseDto { | ||||
|   deviceAssetId!: string; | ||||
|   deviceId!: string; | ||||
|   ownerId!: string; | ||||
|   owner?: UserResponseDto; | ||||
|   libraryId!: string; | ||||
|  | ||||
|   @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) | ||||
|   type!: AssetType; | ||||
|   originalPath!: string; | ||||
|   originalFileName!: string; | ||||
|   resized!: boolean; | ||||
|   /**base64 encoded thumbhash */ | ||||
|   thumbhash!: string | null; | ||||
|   fileCreatedAt!: Date; | ||||
|   fileModifiedAt!: Date; | ||||
|   updatedAt!: Date; | ||||
|   isFavorite!: boolean; | ||||
|   isArchived!: boolean; | ||||
|   isTrashed!: boolean; | ||||
|   localDateTime!: Date; | ||||
|   isOffline!: boolean; | ||||
|   isExternal!: boolean; | ||||
|   isReadOnly!: boolean; | ||||
|   duration!: string; | ||||
|   exifInfo?: ExifResponseDto; | ||||
|   smartInfo?: SmartInfoResponseDto; | ||||
|   livePhotoVideoId?: string | null; | ||||
|   tags?: TagResponseDto[]; | ||||
|   people?: PersonResponseDto[]; | ||||
|   /**base64 encoded sha1 hash */ | ||||
|   checksum!: string; | ||||
| } | ||||
|  | ||||
| function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto { | ||||
| export function mapAsset(entity: AssetEntity, stripMetadata = false): AssetResponseDto { | ||||
|   const sanitizedAssetResponse: SanitizedAssetResponseDto = { | ||||
|     id: entity.id, | ||||
|     type: entity.type, | ||||
|     thumbhash: entity.thumbhash?.toString('base64') ?? null, | ||||
|     localDateTime: entity.localDateTime, | ||||
|     resized: !!entity.resizePath, | ||||
|     duration: entity.duration ?? '0:00:00.00000', | ||||
|     livePhotoVideoId: entity.livePhotoVideoId, | ||||
|     hasMetadata: false, | ||||
|   }; | ||||
|  | ||||
|   if (stripMetadata) { | ||||
|     return sanitizedAssetResponse as AssetResponseDto; | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     ...sanitizedAssetResponse, | ||||
|     id: entity.id, | ||||
|     deviceAssetId: entity.deviceAssetId, | ||||
|     ownerId: entity.ownerId, | ||||
| @@ -62,7 +81,7 @@ function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto { | ||||
|     isArchived: entity.isArchived, | ||||
|     isTrashed: !!entity.deletedAt, | ||||
|     duration: entity.duration ?? '0:00:00.00000', | ||||
|     exifInfo: withExif ? (entity.exifInfo ? mapExif(entity.exifInfo) : undefined) : undefined, | ||||
|     exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, | ||||
|     smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, | ||||
|     livePhotoVideoId: entity.livePhotoVideoId, | ||||
|     tags: entity.tags?.map(mapTag), | ||||
| @@ -71,17 +90,10 @@ function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto { | ||||
|     isExternal: entity.isExternal, | ||||
|     isOffline: entity.isOffline, | ||||
|     isReadOnly: entity.isReadOnly, | ||||
|     hasMetadata: true, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function mapAsset(entity: AssetEntity): AssetResponseDto { | ||||
|   return _map(entity, true); | ||||
| } | ||||
|  | ||||
| export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto { | ||||
|   return _map(entity, false); | ||||
| } | ||||
|  | ||||
| export class MemoryLaneResponseDto { | ||||
|   title!: string; | ||||
|   assets!: AssetResponseDto[]; | ||||
|   | ||||
| @@ -52,3 +52,15 @@ export function mapExif(entity: ExifEntity): ExifResponseDto { | ||||
|     projectionType: entity.projectionType, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto { | ||||
|   return { | ||||
|     fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null, | ||||
|     orientation: entity.orientation, | ||||
|     dateTimeOriginal: entity.dateTimeOriginal, | ||||
|     timeZone: entity.timeZone, | ||||
|     projectionType: entity.projectionType, | ||||
|     exifImageWidth: entity.exifImageWidth, | ||||
|     exifImageHeight: entity.exifImageHeight, | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -380,7 +380,7 @@ export class AuthService { | ||||
|             sharedLinkId: link.id, | ||||
|             isAllowUpload: link.allowUpload, | ||||
|             isAllowDownload: link.allowDownload, | ||||
|             isShowExif: link.showExif, | ||||
|             isShowMetadata: link.showExif, | ||||
|           }; | ||||
|         } | ||||
|       } | ||||
| @@ -431,7 +431,7 @@ export class AuthService { | ||||
|         isPublicUser: false, | ||||
|         isAllowUpload: true, | ||||
|         isAllowDownload: true, | ||||
|         isShowExif: true, | ||||
|         isShowMetadata: true, | ||||
|         accessTokenId: token.id, | ||||
|       }; | ||||
|     } | ||||
|   | ||||
| @@ -6,7 +6,7 @@ export class AuthUserDto { | ||||
|   sharedLinkId?: string; | ||||
|   isAllowUpload?: boolean; | ||||
|   isAllowDownload?: boolean; | ||||
|   isShowExif?: boolean; | ||||
|   isShowMetadata?: boolean; | ||||
|   accessTokenId?: string; | ||||
|   externalPath?: string | null; | ||||
| } | ||||
|   | ||||
| @@ -97,7 +97,7 @@ export class PersonService { | ||||
|   async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> { | ||||
|     await this.access.requirePermission(authUser, Permission.PERSON_READ, id); | ||||
|     const assets = await this.repository.getAssets(id); | ||||
|     return assets.map(mapAsset); | ||||
|     return assets.map((asset) => mapAsset(asset)); | ||||
|   } | ||||
|  | ||||
|   async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> { | ||||
|   | ||||
| @@ -154,7 +154,7 @@ export class SearchService { | ||||
|         items: assets.items | ||||
|           .map((item) => lookup[item.id]) | ||||
|           .filter((item) => !!item) | ||||
|           .map(mapAsset), | ||||
|           .map((asset) => mapAsset(asset)), | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { SharedLinkEntity, SharedLinkType } from '@app/infra/entities'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import _ from 'lodash'; | ||||
| import { AlbumResponseDto, mapAlbumWithoutAssets } from '../album'; | ||||
| import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../asset'; | ||||
| import { AssetResponseDto, mapAsset } from '../asset'; | ||||
|  | ||||
| export class SharedLinkResponseDto { | ||||
|   id!: string; | ||||
| @@ -17,8 +17,9 @@ export class SharedLinkResponseDto { | ||||
|   assets!: AssetResponseDto[]; | ||||
|   album?: AlbumResponseDto; | ||||
|   allowUpload!: boolean; | ||||
|  | ||||
|   allowDownload!: boolean; | ||||
|   showExif!: boolean; | ||||
|   showMetadata!: boolean; | ||||
| } | ||||
|  | ||||
| export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto { | ||||
| @@ -35,15 +36,15 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD | ||||
|     type: sharedLink.type, | ||||
|     createdAt: sharedLink.createdAt, | ||||
|     expiresAt: sharedLink.expiresAt, | ||||
|     assets: assets.map(mapAsset), | ||||
|     assets: assets.map((asset) => mapAsset(asset)), | ||||
|     album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, | ||||
|     allowUpload: sharedLink.allowUpload, | ||||
|     allowDownload: sharedLink.allowDownload, | ||||
|     showExif: sharedLink.showExif, | ||||
|     showMetadata: sharedLink.showExif, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLinkResponseDto { | ||||
| export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): SharedLinkResponseDto { | ||||
|   const linkAssets = sharedLink.assets || []; | ||||
|   const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset); | ||||
|  | ||||
| @@ -57,10 +58,10 @@ export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLin | ||||
|     type: sharedLink.type, | ||||
|     createdAt: sharedLink.createdAt, | ||||
|     expiresAt: sharedLink.expiresAt, | ||||
|     assets: assets.map(mapAssetWithoutExif), | ||||
|     assets: assets.map((asset) => mapAsset(asset, true)) as AssetResponseDto[], | ||||
|     album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, | ||||
|     allowUpload: sharedLink.allowUpload, | ||||
|     allowDownload: sharedLink.allowDownload, | ||||
|     showExif: sharedLink.showExif, | ||||
|     showMetadata: sharedLink.showExif, | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -34,7 +34,7 @@ export class SharedLinkCreateDto { | ||||
|  | ||||
|   @Optional() | ||||
|   @IsBoolean() | ||||
|   showExif?: boolean = true; | ||||
|   showMetadata?: boolean = true; | ||||
| } | ||||
|  | ||||
| export class SharedLinkEditDto { | ||||
| @@ -51,5 +51,5 @@ export class SharedLinkEditDto { | ||||
|   allowDownload?: boolean; | ||||
|  | ||||
|   @Optional() | ||||
|   showExif?: boolean; | ||||
|   showMetadata?: boolean; | ||||
| } | ||||
|   | ||||
| @@ -59,10 +59,10 @@ describe(SharedLinkService.name, () => { | ||||
|       expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); | ||||
|     }); | ||||
|  | ||||
|     it('should return not return exif', async () => { | ||||
|     it('should not return metadata', async () => { | ||||
|       const authDto = authStub.adminSharedLinkNoExif; | ||||
|       shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif); | ||||
|       await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.readonlyNoExif); | ||||
|       await expect(sut.getMine(authDto)).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata); | ||||
|       expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); | ||||
|     }); | ||||
|   }); | ||||
| @@ -137,7 +137,7 @@ describe(SharedLinkService.name, () => { | ||||
|       await sut.create(authStub.admin, { | ||||
|         type: SharedLinkType.INDIVIDUAL, | ||||
|         assetIds: [assetStub.image.id], | ||||
|         showExif: true, | ||||
|         showMetadata: true, | ||||
|         allowDownload: true, | ||||
|         allowUpload: true, | ||||
|       }); | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { AccessCore, Permission } from '../access'; | ||||
| import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset'; | ||||
| import { AuthUserDto } from '../auth'; | ||||
| import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories'; | ||||
| import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithNoExif } from './shared-link-response.dto'; | ||||
| import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithoutMetadata } from './shared-link-response.dto'; | ||||
| import { SharedLinkCreateDto, SharedLinkEditDto } from './shared-link.dto'; | ||||
|  | ||||
| @Injectable() | ||||
| @@ -24,7 +24,7 @@ export class SharedLinkService { | ||||
|   } | ||||
|  | ||||
|   async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> { | ||||
|     const { sharedLinkId: id, isPublicUser, isShowExif } = authUser; | ||||
|     const { sharedLinkId: id, isPublicUser, isShowMetadata: isShowExif } = authUser; | ||||
|  | ||||
|     if (!isPublicUser || !id) { | ||||
|       throw new ForbiddenException(); | ||||
| @@ -69,7 +69,7 @@ export class SharedLinkService { | ||||
|       expiresAt: dto.expiresAt || null, | ||||
|       allowUpload: dto.allowUpload ?? true, | ||||
|       allowDownload: dto.allowDownload ?? true, | ||||
|       showExif: dto.showExif ?? true, | ||||
|       showExif: dto.showMetadata ?? true, | ||||
|     }); | ||||
|  | ||||
|     return this.map(sharedLink, { withExif: true }); | ||||
| @@ -84,7 +84,7 @@ export class SharedLinkService { | ||||
|       expiresAt: dto.expiresAt, | ||||
|       allowUpload: dto.allowUpload, | ||||
|       allowDownload: dto.allowDownload, | ||||
|       showExif: dto.showExif, | ||||
|       showExif: dto.showMetadata, | ||||
|     }); | ||||
|     return this.map(sharedLink, { withExif: true }); | ||||
|   } | ||||
| @@ -157,6 +157,6 @@ export class SharedLinkService { | ||||
|   } | ||||
|  | ||||
|   private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) { | ||||
|     return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithNoExif(sharedLink); | ||||
|     return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -47,7 +47,7 @@ export class TagService { | ||||
|   async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> { | ||||
|     await this.findOrFail(authUser, id); | ||||
|     const assets = await this.repository.getAssets(authUser.id, id); | ||||
|     return assets.map(mapAsset); | ||||
|     return assets.map((asset) => mapAsset(asset)); | ||||
|   } | ||||
|  | ||||
|   async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> { | ||||
|   | ||||
| @@ -186,7 +186,7 @@ export class AssetController { | ||||
|   @SharedLinkRoute() | ||||
|   @Get('/assetById/:id') | ||||
|   getAssetById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> { | ||||
|     return this.assetService.getAssetById(authUser, id); | ||||
|     return this.assetService.getAssetById(authUser, id) as Promise<AssetResponseDto>; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   | ||||
| @@ -10,9 +10,9 @@ import { | ||||
|   IStorageRepository, | ||||
|   JobName, | ||||
|   mapAsset, | ||||
|   mapAssetWithoutExif, | ||||
|   mimeTypes, | ||||
|   Permission, | ||||
|   SanitizedAssetResponseDto, | ||||
|   UploadFile, | ||||
| } from '@app/domain'; | ||||
| import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities'; | ||||
| @@ -187,22 +187,29 @@ export class AssetService { | ||||
|     return assets.map((asset) => mapAsset(asset)); | ||||
|   } | ||||
|  | ||||
|   public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> { | ||||
|   public async getAssetById( | ||||
|     authUser: AuthUserDto, | ||||
|     assetId: string, | ||||
|   ): Promise<AssetResponseDto | SanitizedAssetResponseDto> { | ||||
|     await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId); | ||||
|  | ||||
|     const allowExif = this.getExifPermission(authUser); | ||||
|     const includeMetadata = this.getExifPermission(authUser); | ||||
|     const asset = await this._assetRepository.getById(assetId); | ||||
|     const data = allowExif ? mapAsset(asset) : mapAssetWithoutExif(asset); | ||||
|     if (includeMetadata) { | ||||
|       const data = mapAsset(asset); | ||||
|  | ||||
|     if (data.ownerId !== authUser.id) { | ||||
|       data.people = []; | ||||
|       if (data.ownerId !== authUser.id) { | ||||
|         data.people = []; | ||||
|       } | ||||
|  | ||||
|       if (authUser.isPublicUser) { | ||||
|         delete data.owner; | ||||
|       } | ||||
|  | ||||
|       return data; | ||||
|     } else { | ||||
|       return mapAsset(asset, true); | ||||
|     } | ||||
|  | ||||
|     if (authUser.isPublicUser) { | ||||
|       delete data.owner; | ||||
|     } | ||||
|  | ||||
|     return data; | ||||
|   } | ||||
|  | ||||
|   async serveThumbnail(authUser: AuthUserDto, assetId: string, query: GetAssetThumbnailDto, res: Res) { | ||||
| @@ -374,7 +381,7 @@ export class AssetService { | ||||
|   } | ||||
|  | ||||
|   getExifPermission(authUser: AuthUserDto) { | ||||
|     return !authUser.isPublicUser || authUser.isShowExif; | ||||
|     return !authUser.isPublicUser || authUser.isShowMetadata; | ||||
|   } | ||||
|  | ||||
|   private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { | ||||
|   | ||||
| @@ -98,7 +98,7 @@ export class AssetController { | ||||
|   @Authenticated({ isShared: true }) | ||||
|   @Get('time-bucket') | ||||
|   getByTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> { | ||||
|     return this.service.getByTimeBucket(authUser, dto); | ||||
|     return this.service.getByTimeBucket(authUser, dto) as Promise<AssetResponseDto[]>; | ||||
|   } | ||||
|  | ||||
|   @Post('jobs') | ||||
|   | ||||
| @@ -10,4 +10,11 @@ export const sharedLinkApi = { | ||||
|     expect(status).toBe(201); | ||||
|     return body as SharedLinkResponseDto; | ||||
|   }, | ||||
|  | ||||
|   getMySharedLink: async (server: any, key: string) => { | ||||
|     const { status, body } = await request(server).get('/shared-link/me').query({ key }); | ||||
|  | ||||
|     expect(status).toBe(200); | ||||
|     return body as SharedLinkResponseDto; | ||||
|   }, | ||||
| }; | ||||
|   | ||||
 Submodule server/test/assets updated: 9e6e1bcc24...948f353e3c
									
								
							| @@ -1,11 +1,17 @@ | ||||
| import { AlbumResponseDto, LoginResponseDto, SharedLinkResponseDto } from '@app/domain'; | ||||
| import { PartnerController } from '@app/immich'; | ||||
| import { SharedLinkType } from '@app/infra/entities'; | ||||
| import { LibraryType, SharedLinkType } from '@app/infra/entities'; | ||||
| import { INestApplication } from '@nestjs/common'; | ||||
| import { api } from '@test/api'; | ||||
| import { db } from '@test/db'; | ||||
| import { errorStub, uuidStub } from '@test/fixtures'; | ||||
| import { createTestApp } from '@test/test-utils'; | ||||
| import { | ||||
|   IMMICH_TEST_ASSET_PATH, | ||||
|   IMMICH_TEST_ASSET_TEMP_PATH, | ||||
|   createTestApp, | ||||
|   restoreTempFolder, | ||||
| } from '@test/test-utils'; | ||||
| import { cp } from 'fs/promises'; | ||||
| import request from 'supertest'; | ||||
|  | ||||
| const user1Dto = { | ||||
| @@ -18,24 +24,22 @@ const user1Dto = { | ||||
| describe(`${PartnerController.name} (e2e)`, () => { | ||||
|   let app: INestApplication; | ||||
|   let server: any; | ||||
|   let loginResponse: LoginResponseDto; | ||||
|   let accessToken: string; | ||||
|   let admin: LoginResponseDto; | ||||
|   let user1: LoginResponseDto; | ||||
|   let album: AlbumResponseDto; | ||||
|   let sharedLink: SharedLinkResponseDto; | ||||
|  | ||||
|   beforeAll(async () => { | ||||
|     app = await createTestApp(); | ||||
|     app = await createTestApp(true); | ||||
|     server = app.getHttpServer(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await db.reset(); | ||||
|     await api.authApi.adminSignUp(server); | ||||
|     loginResponse = await api.authApi.adminLogin(server); | ||||
|     accessToken = loginResponse.accessToken; | ||||
|     admin = await api.authApi.adminLogin(server); | ||||
|  | ||||
|     await api.userApi.create(server, accessToken, user1Dto); | ||||
|     await api.userApi.create(server, admin.accessToken, user1Dto); | ||||
|     user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }); | ||||
|  | ||||
|     album = await api.albumApi.create(server, user1.accessToken, { albumName: 'shared with link' }); | ||||
| @@ -48,6 +52,7 @@ describe(`${PartnerController.name} (e2e)`, () => { | ||||
|   afterAll(async () => { | ||||
|     await db.disconnect(); | ||||
|     await app.close(); | ||||
|     await restoreTempFolder(); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /shared-link', () => { | ||||
| @@ -68,7 +73,9 @@ describe(`${PartnerController.name} (e2e)`, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should not get shared links created by other users', async () => { | ||||
|       const { status, body } = await request(server).get('/shared-link').set('Authorization', `Bearer ${accessToken}`); | ||||
|       const { status, body } = await request(server) | ||||
|         .get('/shared-link') | ||||
|         .set('Authorization', `Bearer ${admin.accessToken}`); | ||||
|  | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual([]); | ||||
| @@ -77,7 +84,9 @@ describe(`${PartnerController.name} (e2e)`, () => { | ||||
|  | ||||
|   describe('GET /shared-link/me', () => { | ||||
|     it('should not require admin authentication', async () => { | ||||
|       const { status } = await request(server).get('/shared-link/me').set('Authorization', `Bearer ${accessToken}`); | ||||
|       const { status } = await request(server) | ||||
|         .get('/shared-link/me') | ||||
|         .set('Authorization', `Bearer ${admin.accessToken}`); | ||||
|  | ||||
|       expect(status).toBe(403); | ||||
|     }); | ||||
| @@ -104,7 +113,7 @@ describe(`${PartnerController.name} (e2e)`, () => { | ||||
|         type: SharedLinkType.ALBUM, | ||||
|         albumId: softDeletedAlbum.id, | ||||
|       }); | ||||
|       await api.userApi.delete(server, accessToken, user1.userId); | ||||
|       await api.userApi.delete(server, admin.accessToken, user1.userId); | ||||
|  | ||||
|       const { status, body } = await request(server).get('/shared-link/me').query({ key: softDeletedAlbumLink.key }); | ||||
|  | ||||
| @@ -133,7 +142,7 @@ describe(`${PartnerController.name} (e2e)`, () => { | ||||
|     it('should not get shared link by id if user has not created the link or it does not exist', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .get(`/shared-link/${sharedLink.id}`) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|         .set('Authorization', `Bearer ${admin.accessToken}`); | ||||
|  | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' })); | ||||
| @@ -248,4 +257,81 @@ describe(`${PartnerController.name} (e2e)`, () => { | ||||
|       expect(status).toBe(200); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('Shared link metadata', () => { | ||||
|     beforeEach(async () => { | ||||
|       await restoreTempFolder(); | ||||
|  | ||||
|       await cp( | ||||
|         `${IMMICH_TEST_ASSET_PATH}/metadata/gps-position/thompson-springs.jpg`, | ||||
|         `${IMMICH_TEST_ASSET_TEMP_PATH}/thompson-springs.jpg`, | ||||
|       ); | ||||
|  | ||||
|       await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); | ||||
|  | ||||
|       const library = await api.libraryApi.create(server, admin.accessToken, { | ||||
|         type: LibraryType.EXTERNAL, | ||||
|         importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], | ||||
|       }); | ||||
|  | ||||
|       await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); | ||||
|  | ||||
|       const assets = await api.assetApi.getAllAssets(server, admin.accessToken); | ||||
|  | ||||
|       expect(assets).toHaveLength(1); | ||||
|  | ||||
|       album = await api.albumApi.create(server, admin.accessToken, { albumName: 'New album' }); | ||||
|       await api.albumApi.addAssets(server, admin.accessToken, album.id, { ids: [assets[0].id] }); | ||||
|     }); | ||||
|  | ||||
|     it('should return metadata for album shared link', async () => { | ||||
|       const sharedLink = await api.sharedLinkApi.create(server, admin.accessToken, { | ||||
|         type: SharedLinkType.ALBUM, | ||||
|         albumId: album.id, | ||||
|       }); | ||||
|  | ||||
|       const returnedLink = await api.sharedLinkApi.getMySharedLink(server, sharedLink.key); | ||||
|  | ||||
|       expect(returnedLink.assets).toHaveLength(1); | ||||
|       expect(returnedLink.album).toBeDefined(); | ||||
|  | ||||
|       const returnedAsset = returnedLink.assets[0]; | ||||
|       expect(returnedAsset).toEqual( | ||||
|         expect.objectContaining({ | ||||
|           originalFileName: 'thompson-springs', | ||||
|           resized: true, | ||||
|           localDateTime: '2022-01-10T15:15:44.310Z', | ||||
|           fileCreatedAt: '2022-01-10T19:15:44.310Z', | ||||
|           exifInfo: expect.objectContaining({ | ||||
|             longitude: -108.400968333333, | ||||
|             latitude: 39.115, | ||||
|             orientation: '1', | ||||
|             dateTimeOriginal: '2022-01-10T19:15:44.310Z', | ||||
|             timeZone: 'UTC-4', | ||||
|             state: 'Mesa County, Colorado', | ||||
|             country: 'United States of America', | ||||
|           }), | ||||
|         }), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should not return metadata for album shared link without metadata', async () => { | ||||
|       const sharedLink = await api.sharedLinkApi.create(server, admin.accessToken, { | ||||
|         type: SharedLinkType.ALBUM, | ||||
|         albumId: album.id, | ||||
|         showMetadata: false, | ||||
|       }); | ||||
|  | ||||
|       const returnedLink = await api.sharedLinkApi.getMySharedLink(server, sharedLink.key); | ||||
|  | ||||
|       expect(returnedLink.assets).toHaveLength(1); | ||||
|       expect(returnedLink.album).toBeDefined(); | ||||
|  | ||||
|       const returnedAsset = returnedLink.assets[0]; | ||||
|       expect(returnedAsset).not.toHaveProperty('exifInfo'); | ||||
|       expect(returnedAsset).not.toHaveProperty('fileCreatedAt'); | ||||
|       expect(returnedAsset).not.toHaveProperty('originalFilename'); | ||||
|       expect(returnedAsset).not.toHaveProperty('originalPath'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										12
									
								
								server/test/fixtures/auth.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								server/test/fixtures/auth.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -48,7 +48,7 @@ export const authStub = { | ||||
|     isPublicUser: false, | ||||
|     isAllowUpload: true, | ||||
|     isAllowDownload: true, | ||||
|     isShowExif: true, | ||||
|     isShowMetadata: true, | ||||
|     accessTokenId: 'token-id', | ||||
|     externalPath: null, | ||||
|   }), | ||||
| @@ -59,7 +59,7 @@ export const authStub = { | ||||
|     isPublicUser: false, | ||||
|     isAllowUpload: true, | ||||
|     isAllowDownload: true, | ||||
|     isShowExif: true, | ||||
|     isShowMetadata: true, | ||||
|     accessTokenId: 'token-id', | ||||
|     externalPath: null, | ||||
|   }), | ||||
| @@ -70,7 +70,7 @@ export const authStub = { | ||||
|     isPublicUser: false, | ||||
|     isAllowUpload: true, | ||||
|     isAllowDownload: true, | ||||
|     isShowExif: true, | ||||
|     isShowMetadata: true, | ||||
|     accessTokenId: 'token-id', | ||||
|     externalPath: '/data/user1', | ||||
|   }), | ||||
| @@ -81,7 +81,7 @@ export const authStub = { | ||||
|     isAllowUpload: true, | ||||
|     isAllowDownload: true, | ||||
|     isPublicUser: true, | ||||
|     isShowExif: true, | ||||
|     isShowMetadata: true, | ||||
|     sharedLinkId: '123', | ||||
|   }), | ||||
|   adminSharedLinkNoExif: Object.freeze<AuthUserDto>({ | ||||
| @@ -91,7 +91,7 @@ export const authStub = { | ||||
|     isAllowUpload: true, | ||||
|     isAllowDownload: true, | ||||
|     isPublicUser: true, | ||||
|     isShowExif: false, | ||||
|     isShowMetadata: false, | ||||
|     sharedLinkId: '123', | ||||
|   }), | ||||
|   readonlySharedLink: Object.freeze<AuthUserDto>({ | ||||
| @@ -101,7 +101,7 @@ export const authStub = { | ||||
|     isAllowUpload: false, | ||||
|     isAllowDownload: false, | ||||
|     isPublicUser: true, | ||||
|     isShowExif: true, | ||||
|     isShowMetadata: true, | ||||
|     sharedLinkId: '123', | ||||
|     accessTokenId: 'token-id', | ||||
|   }), | ||||
|   | ||||
							
								
								
									
										24
									
								
								server/test/fixtures/shared-link.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								server/test/fixtures/shared-link.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -71,8 +71,20 @@ const assetResponse: AssetResponseDto = { | ||||
|   checksum: 'ZmlsZSBoYXNo', | ||||
|   isTrashed: false, | ||||
|   libraryId: 'library-id', | ||||
|   hasMetadata: true, | ||||
| }; | ||||
|  | ||||
| const assetResponseWithoutMetadata = { | ||||
|   id: 'id_1', | ||||
|   type: AssetType.VIDEO, | ||||
|   resized: false, | ||||
|   thumbhash: null, | ||||
|   localDateTime: today, | ||||
|   duration: '0:00:00.00000', | ||||
|   livePhotoVideoId: null, | ||||
|   hasMetadata: false, | ||||
| } as AssetResponseDto; | ||||
|  | ||||
| const albumResponse: AlbumResponseDto = { | ||||
|   albumName: 'Test Album', | ||||
|   description: '', | ||||
| @@ -253,7 +265,7 @@ export const sharedLinkResponseStub = { | ||||
|     expiresAt: tomorrow, | ||||
|     id: '123', | ||||
|     key: sharedLinkBytes.toString('base64url'), | ||||
|     showExif: true, | ||||
|     showMetadata: true, | ||||
|     type: SharedLinkType.ALBUM, | ||||
|     userId: 'admin_id', | ||||
|   }), | ||||
| @@ -267,7 +279,7 @@ export const sharedLinkResponseStub = { | ||||
|     expiresAt: yesterday, | ||||
|     id: '123', | ||||
|     key: sharedLinkBytes.toString('base64url'), | ||||
|     showExif: true, | ||||
|     showMetadata: true, | ||||
|     type: SharedLinkType.ALBUM, | ||||
|     userId: 'admin_id', | ||||
|   }), | ||||
| @@ -281,11 +293,11 @@ export const sharedLinkResponseStub = { | ||||
|     description: null, | ||||
|     allowUpload: false, | ||||
|     allowDownload: false, | ||||
|     showExif: true, | ||||
|     showMetadata: true, | ||||
|     album: albumResponse, | ||||
|     assets: [assetResponse], | ||||
|   }), | ||||
|   readonlyNoExif: Object.freeze<SharedLinkResponseDto>({ | ||||
|   readonlyNoMetadata: Object.freeze<SharedLinkResponseDto>({ | ||||
|     id: '123', | ||||
|     userId: 'admin_id', | ||||
|     key: sharedLinkBytes.toString('base64url'), | ||||
| @@ -295,8 +307,8 @@ export const sharedLinkResponseStub = { | ||||
|     description: null, | ||||
|     allowUpload: false, | ||||
|     allowDownload: false, | ||||
|     showExif: false, | ||||
|     showMetadata: false, | ||||
|     album: { ...albumResponse, startDate: assetResponse.fileCreatedAt, endDate: assetResponse.fileCreatedAt }, | ||||
|     assets: [{ ...assetResponse, exifInfo: undefined }], | ||||
|     assets: [{ ...assetResponseWithoutMetadata, exifInfo: undefined }], | ||||
|   }), | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user