fix: hide chat delete actions when the chat delete permission is disabled (#27714)

The chat delete endpoints gate on chat.delete for non-admins, but none of
the UI affordances that reach them were conditioned on it, so a user
without the permission is offered controls that always fail with
"Access prohibited".

Gates every entry point on the same condition the backend enforces,
matching the existing chat.share / chat.export / chat.import gates in the
same components:

- Sidebar chat menu (ChatMenu) Delete item — also covers the Search
  modal's menu, which reuses ChatMenu
- Sidebar Shift+hover inline Delete button (ChatItem)
- Sidebar hidden #delete-chat-button keyboard-shortcut target (ChatItem)
- Chat navbar menu Delete item (Navbar/Menu)
- Search modal Shift-held inline Delete button (SearchModal)
- Settings -> Data Controls -> Delete All Chats
- Settings -> Archived Chats per-row Delete
This commit is contained in:
G30 authored and GitHub committed 2026-08-10 23:24:02 -06:00
1 parent fe62934be7
commit a3d33b4cf3
6 files changed
+83 -71

No files matched your search

@@ -19,7 +19,7 @@
getArchivedChatList, getArchivedChatList,
unarchiveAllChats unarchiveAllChats
} from '$lib/apis/chats'; } from '$lib/apis/chats';
import { chatId, showSettings } from '$lib/stores'; import { chatId, showSettings, user } from '$lib/stores';
import { refreshChatList } from '$lib/stores/chatList'; import { refreshChatList } from '$lib/stores/chatList';
import { formatNumber } from '$lib/utils'; import { formatNumber } from '$lib/utils';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
@@ -348,19 +348,21 @@
<UndoAction className="size-3.5" strokeWidth="1.5" /> <UndoAction className="size-3.5" strokeWidth="1.5" />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content={$i18n.t('Delete Chat')}> {#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
<button <Tooltip content={$i18n.t('Delete Chat')}>
class="rounded-lg p-1 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300" <button
type="button" class="rounded-lg p-1 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
aria-label={$i18n.t('Delete Chat')} type="button"
on:click={() => { aria-label={$i18n.t('Delete Chat')}
selectedChatId = chat.id; on:click={() => {
showDeleteConfirmDialog = true; selectedChatId = chat.id;
}} showDeleteConfirmDialog = true;
> }}
<Trash className="size-3.5" strokeWidth="1.5" /> >
</button> <Trash className="size-3.5" strokeWidth="1.5" />
</Tooltip> </button>
</Tooltip>
{/if}
</div> </div>
</div> </div>
{/each} {/each}
@@ -216,20 +216,22 @@
</button> </button>
</UserSettingRow> </UserSettingRow>
<UserSettingRow {#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
label={$i18n.t('Delete All Chats')} <UserSettingRow
description={$i18n.t('Permanently delete every chat after confirmation.')} label={$i18n.t('Delete All Chats')}
> description={$i18n.t('Permanently delete every chat after confirmation.')}
<button
class={actionButtonClass}
on:click={() => {
showDeleteConfirmDialog = true;
}}
type="button"
> >
{$i18n.t('Delete All')} <button
</button> class={actionButtonClass}
</UserSettingRow> on:click={() => {
showDeleteConfirmDialog = true;
}}
type="button"
>
{$i18n.t('Delete All')}
</button>
</UserSettingRow>
{/if}
</UserSettingSection> </UserSettingSection>
<UserSettingSection title={$i18n.t('Files')}> <UserSettingSection title={$i18n.t('Files')}>
+12 -10
View File
@@ -487,16 +487,18 @@
<div class="flex items-center">{$i18n.t('Archive')}</div> <div class="flex items-center">{$i18n.t('Archive')}</div>
</button> </button>
<button {#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
draggable="false" <button
class="flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] cursor-pointer select-none hover:bg-gray-50/40 dark:hover:bg-gray-800/40" draggable="false"
on:click={() => { class="flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[0.8125rem] cursor-pointer select-none hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
deleteChatHandler(); on:click={() => {
}} deleteChatHandler();
> }}
<GarbageBin className="size-3.5" strokeWidth="1.5" /> >
<div class="flex items-center">{$i18n.t('Delete')}</div> <GarbageBin className="size-3.5" strokeWidth="1.5" />
</button> <div class="flex items-center">{$i18n.t('Delete')}</div>
</button>
{/if}
<hr class="border-gray-50/30 dark:border-gray-800/30 mx-1 my-0.5" /> <hr class="border-gray-50/30 dark:border-gray-800/30 mx-1 my-0.5" />
+13 -11
View File
@@ -786,17 +786,19 @@
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content={$i18n.t('Delete')}> {#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
<button <Tooltip content={$i18n.t('Delete')}>
class="self-center dark:hover:text-white transition" <button
on:click|stopPropagation={() => { class="self-center dark:hover:text-white transition"
deleteChatHandler(chat.id); on:click|stopPropagation={() => {
}} deleteChatHandler(chat.id);
type="button" }}
> type="button"
<GarbageBin strokeWidth="2" /> >
</button> <GarbageBin strokeWidth="2" />
</Tooltip> </button>
</Tooltip>
{/if}
</div> </div>
{:else} {:else}
<div class="flex items-center"> <div class="flex items-center">
@@ -705,18 +705,20 @@
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content={$i18n.t('Delete')}> {#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
<button <Tooltip content={$i18n.t('Delete')}>
class=" self-center dark:hover:text-white transition disabled:cursor-not-allowed" <button
disabled={deleting} class=" self-center dark:hover:text-white transition disabled:cursor-not-allowed"
on:click={() => { disabled={deleting}
deleteChatHandler(id); on:click={() => {
}} deleteChatHandler(id);
type="button" }}
> type="button"
<GarbageBinIcon className="size-3.5" strokeWidth="1.7" /> >
</button> <GarbageBinIcon className="size-3.5" strokeWidth="1.7" />
</Tooltip> </button>
</Tooltip>
{/if}
</div> </div>
{:else} {:else}
<div class="flex self-center z-10 items-end"> <div class="flex self-center z-10 items-end">
@@ -756,7 +758,7 @@
</button> </button>
</ChatMenu> </ChatMenu>
{#if id === $chatId} {#if id === $chatId && ($user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true))}
<!-- Shortcut support using "delete-chat-button" id --> <!-- Shortcut support using "delete-chat-button" id -->
<button <button
id="delete-chat-button" id="delete-chat-button"
@@ -456,16 +456,18 @@
<div class="flex items-center">{$i18n.t('Archive')}</div> <div class="flex items-center">{$i18n.t('Archive')}</div>
</button> </button>
<button {#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
draggable="false" <button
class="flex h-[1.6875rem] gap-2 items-center rounded-xl px-2 text-[0.8125rem] cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 w-full" draggable="false"
on:click={() => { class="flex h-[1.6875rem] gap-2 items-center rounded-xl px-2 text-[0.8125rem] cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 w-full"
deleteHandler(); on:click={() => {
}} deleteHandler();
> }}
<TrashIcon className="size-3.5" strokeWidth="1.5" /> >
<div class="flex items-center">{$i18n.t('Delete')}</div> <TrashIcon className="size-3.5" strokeWidth="1.5" />
</button> <div class="flex items-center">{$i18n.t('Delete')}</div>
</button>
{/if}
</DropdownMenu> </DropdownMenu>
</div> </div>
</Dropdown> </Dropdown>