enh: chat share & export permissions

This commit is contained in:
Timothy Jaeryang Baek 2025-04-23 14:43:33 +09:00
parent 7937ed3ee1
commit 2f7b5acdf8
8 changed files with 103 additions and 51 deletions

View File

@ -1068,6 +1068,14 @@ USER_PERMISSIONS_CHAT_EDIT = (
os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true" os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true"
) )
USER_PERMISSIONS_CHAT_SHARE = (
os.environ.get("USER_PERMISSIONS_CHAT_SHARE", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_EXPORT = (
os.environ.get("USER_PERMISSIONS_CHAT_EXPORT", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_STT = ( USER_PERMISSIONS_CHAT_STT = (
os.environ.get("USER_PERMISSIONS_CHAT_STT", "True").lower() == "true" os.environ.get("USER_PERMISSIONS_CHAT_STT", "True").lower() == "true"
) )
@ -1132,6 +1140,8 @@ DEFAULT_USER_PERMISSIONS = {
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD, "file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
"delete": USER_PERMISSIONS_CHAT_DELETE, "delete": USER_PERMISSIONS_CHAT_DELETE,
"edit": USER_PERMISSIONS_CHAT_EDIT, "edit": USER_PERMISSIONS_CHAT_EDIT,
"share": USER_PERMISSIONS_CHAT_SHARE,
"export": USER_PERMISSIONS_CHAT_EXPORT,
"stt": USER_PERMISSIONS_CHAT_STT, "stt": USER_PERMISSIONS_CHAT_STT,
"tts": USER_PERMISSIONS_CHAT_TTS, "tts": USER_PERMISSIONS_CHAT_TTS,
"call": USER_PERMISSIONS_CHAT_CALL, "call": USER_PERMISSIONS_CHAT_CALL,

View File

@ -638,8 +638,17 @@ async def archive_chat_by_id(id: str, user=Depends(get_verified_user)):
@router.post("/{id}/share", response_model=Optional[ChatResponse]) @router.post("/{id}/share", response_model=Optional[ChatResponse])
async def share_chat_by_id(id: str, user=Depends(get_verified_user)): async def share_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if not has_permission(
user.id, "chat.share", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
chat = Chats.get_chat_by_id_and_user_id(id, user.id) chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat: if chat:
if chat.share_id: if chat.share_id:
shared_chat = Chats.update_shared_chat_by_chat_id(chat.id) shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)

View File

@ -88,6 +88,8 @@ class ChatPermissions(BaseModel):
file_upload: bool = True file_upload: bool = True
delete: bool = True delete: bool = True
edit: bool = True edit: bool = True
share: bool = True
export: bool = True
stt: bool = True stt: bool = True
tts: bool = True tts: bool = True
call: bool = True call: bool = True

View File

@ -63,6 +63,8 @@
file_upload: true, file_upload: true,
delete: true, delete: true,
edit: true, edit: true,
share: true,
export: true,
stt: true, stt: true,
tts: true, tts: true,
call: true, call: true,

View File

@ -24,6 +24,8 @@
file_upload: true, file_upload: true,
delete: true, delete: true,
edit: true, edit: true,
share: true,
export: true,
stt: true, stt: true,
tts: true, tts: true,
call: true, call: true,
@ -276,6 +278,22 @@
<Switch bind:state={permissions.chat.edit} /> <Switch bind:state={permissions.chat.edit} />
</div> </div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Allow Chat Share')}
</div>
<Switch bind:state={permissions.chat.share} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Allow Chat Export')}
</div>
<Switch bind:state={permissions.chat.export} />
</div>
<div class=" flex w-full justify-between my-2 pr-2"> <div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Allow Speech to Text')} {$i18n.t('Allow Speech to Text')}

View File

@ -140,28 +140,31 @@
</div> </div>
<div class=" self-center text-sm font-medium">{$i18n.t('Import Chats')}</div> <div class=" self-center text-sm font-medium">{$i18n.t('Import Chats')}</div>
</button> </button>
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" {#if $user?.role === 'admin' || ($user.permissions?.chat?.export ?? true)}
on:click={() => { <button
exportChats(); class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
}} on:click={() => {
> exportChats();
<div class=" self-center mr-3"> }}
<svg >
xmlns="http://www.w3.org/2000/svg" <div class=" self-center mr-3">
viewBox="0 0 16 16" <svg
fill="currentColor" xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4" viewBox="0 0 16 16"
> fill="currentColor"
<path class="w-4 h-4"
fill-rule="evenodd" >
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z" <path
clip-rule="evenodd" fill-rule="evenodd"
/> d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
</svg> clip-rule="evenodd"
</div> />
<div class=" self-center text-sm font-medium">{$i18n.t('Export Chats')}</div> </svg>
</button> </div>
<div class=" self-center text-sm font-medium">{$i18n.t('Export Chats')}</div>
</button>
{/if}
</div> </div>
<hr class=" border-gray-100 dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />

View File

@ -18,7 +18,8 @@
showArtifacts, showArtifacts,
mobile, mobile,
temporaryChatEnabled, temporaryChatEnabled,
theme theme,
user
} from '$lib/stores'; } from '$lib/stores';
import { flyAndScale } from '$lib/utils/transitions'; import { flyAndScale } from '$lib/utils/transitions';
@ -212,7 +213,7 @@
</DropdownMenu.Item> </DropdownMenu.Item>
{/if} {/if}
{#if !$temporaryChatEnabled} {#if !$temporaryChatEnabled && ($user?.role === 'admin' || ($user.permissions?.chat?.share ?? true))}
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
id="chat-share-button" id="chat-share-button"
@ -288,14 +289,16 @@
transition={flyAndScale} transition={flyAndScale}
sideOffset={8} sideOffset={8}
> >
<DropdownMenu.Item {#if $user?.role === 'admin' || ($user.permissions?.chat?.export ?? true)}
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" <DropdownMenu.Item
on:click={() => { class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
downloadJSONExport(); on:click={() => {
}} downloadJSONExport();
> }}
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div> >
</DropdownMenu.Item> <div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => { on:click={() => {

View File

@ -26,7 +26,7 @@
getChatPinnedStatusById, getChatPinnedStatusById,
toggleChatPinnedStatusById toggleChatPinnedStatusById
} from '$lib/apis/chats'; } from '$lib/apis/chats';
import { chats, theme } from '$lib/stores'; import { chats, theme, user } from '$lib/stores';
import { createMessagesList } from '$lib/utils'; import { createMessagesList } from '$lib/utils';
import { downloadChatAsPDF } from '$lib/apis/utils'; import { downloadChatAsPDF } from '$lib/apis/utils';
import Download from '$lib/components/icons/Download.svelte'; import Download from '$lib/components/icons/Download.svelte';
@ -233,15 +233,17 @@
<div class="flex items-center">{$i18n.t('Archive')}</div> <div class="flex items-center">{$i18n.t('Archive')}</div>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item {#if $user?.role === 'admin' || ($user.permissions?.chat?.share ?? true)}
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" <DropdownMenu.Item
on:click={() => { class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
shareHandler(); on:click={() => {
}} shareHandler();
> }}
<Share /> >
<div class="flex items-center">{$i18n.t('Share')}</div> <Share />
</DropdownMenu.Item> <div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Sub> <DropdownMenu.Sub>
<DropdownMenu.SubTrigger <DropdownMenu.SubTrigger
@ -256,14 +258,17 @@
transition={flyAndScale} transition={flyAndScale}
sideOffset={8} sideOffset={8}
> >
<DropdownMenu.Item {#if $user?.role === 'admin' || ($user.permissions?.chat?.export ?? true)}
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" <DropdownMenu.Item
on:click={() => { class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
downloadJSONExport(); on:click={() => {
}} downloadJSONExport();
> }}
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div> >
</DropdownMenu.Item> <div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => { on:click={() => {