{"record":{"id":"c461e7ff7cb43b1b","repo":"immich-app/immich","slug":"live-photo-video-does-not-belong-to-the-user","errorCode":null,"errorMessage":"Live photo video does not belong to the user","messagePattern":"Live photo video does not belong to the user","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/utils/asset.util.ts","lineNumber":157,"sourceCode":"\n  return [...partnerIds];\n};\n\nexport type AssetHookRepositories = { asset: AssetRepository; event: EventRepository };\n\nexport const onBeforeLink = async (\n  { asset: assetRepository, event: eventRepository }: AssetHookRepositories,\n  { userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },\n) => {\n  const motionAsset = await assetRepository.getById(livePhotoVideoId);\n  if (!motionAsset) {\n    throw new BadRequestException('Live photo video not found');\n  }\n  if (motionAsset.type !== AssetType.Video) {\n    throw new BadRequestException('Live photo video must be a video');\n  }\n  if (motionAsset.ownerId !== userId) {\n    throw new BadRequestException('Live photo video does not belong to the user');\n  }\n\n  if (motionAsset && motionAsset.visibility === AssetVisibility.Timeline) {\n    await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.Hidden });\n    await eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId });\n  }\n};\n\nexport const onBeforeUnlink = async (\n  { asset: assetRepository }: AssetHookRepositories,\n  { livePhotoVideoId }: { livePhotoVideoId: string },\n) => {\n  const motion = await assetRepository.getById(livePhotoVideoId);\n  if (!motion) {\n    return null;\n  }\n\n  if (StorageCore.isAndroidMotionPath(motion.originalPath)) {","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/utils/asset.util.ts#L139-L175","documentation":"Thrown by onBeforeLink when the motion video asset exists and is a video, but its ownerId does not match the authenticated userId. This enforces ownership so a user cannot link another user's asset as their Live Photo motion component. HTTP 400 BadRequestException.","triggerScenarios":"Authenticated user A calls AssetService.update with livePhotoVideoId owned by user B; shared-partner asset id mistakenly used as a personal motion asset; cross-account import where ids collide.","commonSituations":"Using an asset id observed in a shared album or partner's library; id copied from another user's instance; multi-tenant confusion where the same numeric/uuid is reused.","solutions":["Use only motion video asset ids owned by the calling user.","If the motion asset legitimately belongs to a partner, have the owner upload/own it or transfer ownership first.","Check ownership via the access control / GET /assets/:id before linking."],"exampleFix":"// before\nawait updateAsset({ id: myAsset.id, livePhotoVideoId: partnerAsset.id });\n\n// after\nconst motion = await getAsset(motionId);\nif (motion.ownerId !== currentUser.id) throw new Error('not owner');\nawait updateAsset({ id: myAsset.id, livePhotoVideoId: motion.id });","handlingStrategy":"validation","validationCode":"const motion = await assetRepository.getById(livePhotoVideoId);\nconst ok = !!motion && motion.ownerId === auth.user.id;","typeGuard":"const isOwnedBy = (a: { ownerId: string } | null, userId: string): a is { ownerId: string } =>\n  !!a && a.ownerId === userId;","tryCatchPattern":"try { await updateAsset({ id, livePhotoVideoId }); }\ncatch (e) { if (/does not belong to the user/.test(e.message)) { notify('Pick one of your own motion assets'); } else throw e; }","preventionTips":["Scope the motion-asset picker to assets owned by the current user only.","Reject cross-user ids at the client boundary."],"tags":["live-photo","asset","ownership","authorization","bad-request"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}