Parcourir la source

fix(web): disable metadata edit if user is not owner (#5415)

* fix(web): disable metadata edit if user is not owner

* pr feedback

* pr feedback

* get data from page data

* fix: better representation

* feat: warn user if there's issues with the selected assets

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
martin il y a 1 an
Parent
commit
ec92608024

+ 1 - 1
web/src/lib/components/album-page/album-viewer.svelte

@@ -144,7 +144,7 @@
 <main
   class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
 >
-  <AssetGrid {album} {user} {assetStore} {assetInteractionStore}>
+  <AssetGrid {album} {assetStore} {assetInteractionStore}>
     <section class="pt-24">
       <!-- ALBUM TITLE -->
       <p

+ 5 - 6
web/src/lib/components/asset-viewer/asset-viewer.svelte

@@ -9,7 +9,6 @@
     AssetTypeEnum,
     ReactionType,
     SharedLinkResponseDto,
-    UserResponseDto,
   } from '@api';
   import { createEventDispatcher, onDestroy, onMount } from 'svelte';
   import { fly } from 'svelte/transition';
@@ -42,6 +41,7 @@
   import { updateNumberOfComments } from '$lib/stores/activity.store';
   import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
   import SlideshowBar from './slideshow-bar.svelte';
+  import { user } from '$lib/stores/user.store';
 
   export let assetStore: AssetStore | null = null;
   export let asset: AssetResponseDto;
@@ -51,7 +51,6 @@
   export let force = false;
   export let withStacked = false;
   export let isShared = false;
-  export let user: UserResponseDto | null = null;
   export let album: AlbumResponseDto | null = null;
 
   let reactions: ActivityResponseDto[] = [];
@@ -143,10 +142,10 @@
   };
 
   const getFavorite = async () => {
-    if (album && user) {
+    if (album && $user) {
       try {
         const { data } = await api.activityApi.getActivities({
-          userId: user.id,
+          userId: $user.id,
           assetId: asset.id,
           albumId: album.id,
           type: ReactionType.Like,
@@ -743,7 +742,7 @@
     </div>
   {/if}
 
-  {#if isShared && album && isShowActivity && user}
+  {#if isShared && album && isShowActivity && $user}
     <div
       transition:fly={{ duration: 150 }}
       id="activity-panel"
@@ -751,7 +750,7 @@
       translate="yes"
     >
       <ActivityViewer
-        {user}
+        user={$user}
         disabled={!album.isActivityEnabled}
         assetType={asset.type}
         albumOwnerId={album.ownerId}

+ 26 - 16
web/src/lib/components/asset-viewer/detail-panel.svelte

@@ -26,6 +26,7 @@
   import { AppRoute } from '$lib/constants';
   import ChangeLocation from '../shared-components/change-location.svelte';
   import { handleError } from '../../utils/handle-error';
+  import { user } from '$lib/stores/user.store';
 
   export let asset: AssetResponseDto;
   export let albums: AlbumResponseDto[] = [];
@@ -238,12 +239,14 @@
         zone: asset.exifInfo.timeZone ?? undefined,
       })}
       <div
-        class="flex justify-between place-items-start gap-4 py-4 hover:dark:text-immich-dark-primary hover:text-immich-primary cursor-pointer"
-        on:click={() => (isShowChangeDate = true)}
-        on:keydown={(event) => event.key === 'Enter' && (isShowChangeDate = true)}
+        class="flex justify-between place-items-start gap-4 py-4"
         tabindex="0"
         role="button"
-        title="Edit date"
+        on:click={() => (isOwner ? (isShowChangeDate = true) : null)}
+        on:keydown={(event) => (isOwner ? event.key === 'Enter' && (isShowChangeDate = true) : null)}
+        title={isOwner ? 'Edit date' : ''}
+        class:hover:dark:text-immich-dark-primary={isOwner}
+        class:hover:text-immich-primary={isOwner}
       >
         <div class="flex gap-4">
           <div>
@@ -276,11 +279,14 @@
             </div>
           </div>
         </div>
-        <button class="focus:outline-none">
-          <Icon path={mdiPencil} size="20" />
-        </button>
+
+        {#if isOwner}
+          <button class="focus:outline-none">
+            <Icon path={mdiPencil} size="20" />
+          </button>
+        {/if}
       </div>
-    {:else if !asset.exifInfo?.dateTimeOriginal && !asset.isReadOnly}
+    {:else if !asset.exifInfo?.dateTimeOriginal && !asset.isReadOnly && $user && asset.ownerId === $user.id}
       <div class="flex justify-between place-items-start gap-4 py-4">
         <div class="flex gap-4">
           <div>
@@ -410,12 +416,14 @@
 
     {#if asset.exifInfo?.city && !asset.isReadOnly}
       <div
-        class="flex justify-between place-items-start gap-4 py-4 hover:dark:text-immich-dark-primary hover:text-immich-primary cursor-pointer"
-        on:click={() => (isShowChangeLocation = true)}
-        on:keydown={(event) => event.key === 'Enter' && (isShowChangeLocation = true)}
+        class="flex justify-between place-items-start gap-4 py-4"
+        on:click={() => (isOwner ? (isShowChangeLocation = true) : null)}
+        on:keydown={(event) => (isOwner ? event.key === 'Enter' && (isShowChangeLocation = true) : null)}
         tabindex="0"
+        title={isOwner ? 'Edit location' : ''}
         role="button"
-        title="Edit location"
+        class:hover:dark:text-immich-dark-primary={isOwner}
+        class:hover:text-immich-primary={isOwner}
       >
         <div class="flex gap-4">
           <div><Icon path={mdiMapMarkerOutline} size="24" /></div>
@@ -435,11 +443,13 @@
           </div>
         </div>
 
-        <div>
-          <Icon path={mdiPencil} size="20" />
-        </div>
+        {#if isOwner}
+          <div>
+            <Icon path={mdiPencil} size="20" />
+          </div>
+        {/if}
       </div>
-    {:else if !asset.exifInfo?.city && !asset.isReadOnly}
+    {:else if !asset.exifInfo?.city && !asset.isReadOnly && $user && asset.ownerId === $user.id}
       <div
         class="flex justify-between place-items-start gap-4 py-4 rounded-lg pr-2 hover:dark:text-immich-dark-primary hover:text-immich-primary"
         on:click={() => (isShowChangeLocation = true)}

+ 3 - 3
web/src/lib/components/photos-page/actions/change-date-action.svelte

@@ -5,6 +5,8 @@
   import { DateTime } from 'luxon';
   import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
+  import { user } from '$lib/stores/user.store';
+  import { getSelectedAssets } from '$lib/utils/asset-utils';
   export let menuItem = false;
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
@@ -12,9 +14,7 @@
 
   const handleConfirm = async (dateTimeOriginal: string) => {
     isShowChangeDate = false;
-    const ids = Array.from(getOwnedAssets())
-      .filter((a) => !a.isExternal)
-      .map((a) => a.id);
+    const ids = getSelectedAssets(getOwnedAssets(), $user);
 
     try {
       await api.assetApi.updateAssets({

+ 3 - 3
web/src/lib/components/photos-page/actions/change-location-action.svelte

@@ -4,6 +4,8 @@
   import { getAssetControlContext } from '../asset-select-control-bar.svelte';
   import ChangeLocation from '$lib/components/shared-components/change-location.svelte';
   import { handleError } from '../../../utils/handle-error';
+  import { user } from '$lib/stores/user.store';
+  import { getSelectedAssets } from '$lib/utils/asset-utils';
 
   export let menuItem = false;
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
@@ -12,9 +14,7 @@
 
   async function handleConfirm(point: { lng: number; lat: number }) {
     isShowChangeLocation = false;
-    const ids = Array.from(getOwnedAssets())
-      .filter((a) => !a.isExternal)
-      .map((a) => a.id);
+    const ids = getSelectedAssets(getOwnedAssets(), $user);
 
     try {
       await api.assetApi.updateAssets({

+ 1 - 3
web/src/lib/components/photos-page/asset-grid.svelte

@@ -8,7 +8,7 @@
   import { locale } from '$lib/stores/preferences.store';
   import { isSearchEnabled } from '$lib/stores/search.store';
   import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
-  import type { AlbumResponseDto, AssetResponseDto, UserResponseDto } from '@api';
+  import type { AlbumResponseDto, AssetResponseDto } from '@api';
   import { DateTime } from 'luxon';
   import { createEventDispatcher, onDestroy, onMount } from 'svelte';
   import AssetViewer from '../asset-viewer/asset-viewer.svelte';
@@ -27,7 +27,6 @@
   export let removeAction: AssetAction | null = null;
   export let withStacked = false;
   export let isShared = false;
-  export let user: UserResponseDto | null = null;
   export let album: AlbumResponseDto | null = null;
 
   $: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
@@ -394,7 +393,6 @@
 <Portal target="body">
   {#if $showAssetViewer}
     <AssetViewer
-      {user}
       {withStacked}
       {assetStore}
       asset={$viewingAsset}

+ 2 - 3
web/src/lib/components/share-page/individual-shared-viewer.svelte

@@ -2,7 +2,7 @@
   import { goto } from '$app/navigation';
   import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
   import { downloadArchive } from '$lib/utils/asset-utils';
-  import { api, AssetResponseDto, SharedLinkResponseDto, UserResponseDto } from '@api';
+  import { api, AssetResponseDto, SharedLinkResponseDto } from '@api';
   import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import DownloadAction from '../photos-page/actions/download-action.svelte';
@@ -17,7 +17,6 @@
 
   export let sharedLink: SharedLinkResponseDto;
   export let isOwned: boolean;
-  export let user: UserResponseDto | undefined = undefined;
 
   let selectedAssets: Set<AssetResponseDto> = new Set();
 
@@ -103,6 +102,6 @@
     </ControlAppBar>
   {/if}
   <section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40">
-    <GalleryViewer {user} {assets} bind:selectedAssets />
+    <GalleryViewer {assets} bind:selectedAssets />
   </section>
 </section>

+ 1 - 3
web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte

@@ -2,7 +2,7 @@
   import { page } from '$app/stores';
   import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
   import { handleError } from '$lib/utils/handle-error';
-  import { AssetResponseDto, ThumbnailFormat, UserResponseDto } from '@api';
+  import { AssetResponseDto, ThumbnailFormat } from '@api';
   import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
   import { flip } from 'svelte/animate';
   import { getThumbnailSize } from '$lib/utils/thumbnail-util';
@@ -13,7 +13,6 @@
   export let selectedAssets: Set<AssetResponseDto> = new Set();
   export let disableAssetSelect = false;
   export let showArchiveIcon = false;
-  export let user: UserResponseDto | undefined = undefined;
 
   let { isViewing: showAssetViewer } = assetViewingStore;
 
@@ -109,7 +108,6 @@
 <!-- Overlay Asset Viewer -->
 {#if $showAssetViewer}
   <AssetViewer
-    {user}
     asset={selectedAsset}
     on:previous={navigateAssetBackward}
     on:next={navigateAssetForward}

+ 4 - 0
web/src/lib/stores/user.store.ts

@@ -0,0 +1,4 @@
+import { writable } from 'svelte/store';
+import type { UserResponseDto } from '@api';
+
+export const user = writable<UserResponseDto | null>(null);

+ 23 - 1
web/src/lib/utils/asset-utils.ts

@@ -1,6 +1,14 @@
 import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
 import { downloadManager } from '$lib/stores/download';
-import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto, AssetTypeEnum } from '@api';
+import {
+  api,
+  BulkIdResponseDto,
+  AssetResponseDto,
+  DownloadResponseDto,
+  DownloadInfoDto,
+  AssetTypeEnum,
+  UserResponseDto,
+} from '@api';
 import { handleError } from './handle-error';
 
 export const addAssetsToAlbum = async (albumId: string, assetIds: Array<string>): Promise<BulkIdResponseDto[]> =>
@@ -203,3 +211,17 @@ export const getAssetType = (type: AssetTypeEnum) => {
       return 'Asset';
   }
 };
+
+export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserResponseDto | null): string[] => {
+  const ids = Array.from(assets)
+    .filter((a) => !a.isExternal && user && a.ownerId !== user.id)
+    .map((a) => a.id);
+  const numberOfIssues = Array.from(assets).filter((a) => a.isExternal || (user && a.ownerId === user.id)).length;
+  if (numberOfIssues > 0) {
+    notificationController.show({
+      message: `Can't change metadata of ${numberOfIssues} asset${numberOfIssues > 1 ? 's' : ''}`,
+      type: NotificationType.Warning,
+    });
+  }
+  return ids;
+};

+ 11 - 14
web/src/routes/(user)/albums/[albumId]/+page.svelte

@@ -59,6 +59,7 @@
   import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
   import AlbumOptions from '$lib/components/album-page/album-options.svelte';
   import UpdatePanel from '$lib/components/shared-components/update-panel.svelte';
+  import { user } from '$lib/stores/user.store';
 
   export let data: PageData;
 
@@ -66,6 +67,9 @@
   let { slideshowState, slideshowShuffle } = slideshowStore;
 
   let album = data.album;
+
+  $user = data.user;
+
   $: album = data.album;
 
   $: {
@@ -96,7 +100,6 @@
   let isShowActivity = false;
   let isLiked: ActivityResponseDto | null = null;
   let reactions: ActivityResponseDto[] = [];
-  let user = data.user;
   let globalWidth: number;
   let assetGridWidth: number;
 
@@ -179,10 +182,10 @@
   };
 
   const getFavorite = async () => {
-    if (user) {
+    if ($user) {
       try {
         const { data } = await api.activityApi.getActivities({
-          userId: user.id,
+          userId: $user.id,
           albumId: album.id,
           type: ReactionType.Like,
           level: ReactionLevel.Album,
@@ -549,16 +552,10 @@
       style={`width:${assetGridWidth}px`}
     >
       {#if viewMode === ViewMode.SELECT_ASSETS}
-        <AssetGrid
-          user={data.user}
-          assetStore={timelineStore}
-          assetInteractionStore={timelineInteractionStore}
-          isSelectionMode={true}
-        />
+        <AssetGrid assetStore={timelineStore} assetInteractionStore={timelineInteractionStore} isSelectionMode={true} />
       {:else}
         <AssetGrid
           {album}
-          user={data.user}
           {assetStore}
           {assetInteractionStore}
           isShared={album.sharedUsers.length > 0}
@@ -679,7 +676,7 @@
       {/if}
     </main>
   </div>
-  {#if album.sharedUsers.length > 0 && album && isShowActivity && user && !$showAssetViewer}
+  {#if album.sharedUsers.length > 0 && album && isShowActivity && $user && !$showAssetViewer}
     <div class="flex">
       <div
         transition:fly={{ duration: 150 }}
@@ -688,7 +685,7 @@
         translate="yes"
       >
         <ActivityViewer
-          {user}
+          user={$user}
           disabled={!album.isActivityEnabled}
           albumOwnerId={album.ownerId}
           albumId={album.id}
@@ -738,10 +735,10 @@
   </ConfirmDialogue>
 {/if}
 
-{#if viewMode === ViewMode.OPTIONS}
+{#if viewMode === ViewMode.OPTIONS && $user}
   <AlbumOptions
     {album}
-    {user}
+    user={$user}
     on:close={() => (viewMode = ViewMode.VIEW)}
     on:toggleEnableActivity={handleToggleEnableActivity}
     on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)}

+ 3 - 0
web/src/routes/(user)/photos/+page.svelte

@@ -24,6 +24,7 @@
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { mdiDotsVertical, mdiPlus } from '@mdi/js';
   import UpdatePanel from '$lib/components/shared-components/update-panel.svelte';
+  import { user } from '$lib/stores/user.store';
 
   export let data: PageData;
 
@@ -33,6 +34,8 @@
   const assetInteractionStore = createAssetInteractionStore();
   const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 
+  $user = data.user;
+
   $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
 
   const handleEscape = () => {