diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 77e632ccc..b589e9490 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -356,15 +356,16 @@ WEBUI_SECRET_KEY = os.environ.get( ), # DEPRECATED: remove at next major version ) -WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get( - "WEBUI_SESSION_COOKIE_SAME_SITE", - os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"), -) +WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax") -WEBUI_SESSION_COOKIE_SECURE = os.environ.get( - "WEBUI_SESSION_COOKIE_SECURE", - os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true", -) +WEBUI_SESSION_COOKIE_SECURE = os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true" + +WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get("WEBUI_AUTH_COOKIE_SAME_SITE", WEBUI_SESSION_COOKIE_SAME_SITE) + +WEBUI_AUTH_COOKIE_SECURE = os.environ.get( + "WEBUI_AUTH_COOKIE_SECURE", + os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false") +).lower() == "true" if WEBUI_AUTH and WEBUI_SECRET_KEY == "": raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND) diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 47baeb0ac..d7c4fa013 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -25,8 +25,8 @@ from open_webui.env import ( WEBUI_AUTH, WEBUI_AUTH_TRUSTED_EMAIL_HEADER, WEBUI_AUTH_TRUSTED_NAME_HEADER, - WEBUI_SESSION_COOKIE_SAME_SITE, - WEBUI_SESSION_COOKIE_SECURE, + WEBUI_AUTH_COOKIE_SAME_SITE, + WEBUI_AUTH_COOKIE_SECURE, SRC_LOG_LEVELS, ) from fastapi import APIRouter, Depends, HTTPException, Request, status @@ -95,8 +95,8 @@ async def get_session_user( value=token, expires=datetime_expires_at, httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_SESSION_COOKIE_SAME_SITE, - secure=WEBUI_SESSION_COOKIE_SECURE, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, ) user_permissions = get_permissions( @@ -378,8 +378,8 @@ async def signin(request: Request, response: Response, form_data: SigninForm): value=token, expires=datetime_expires_at, httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_SESSION_COOKIE_SAME_SITE, - secure=WEBUI_SESSION_COOKIE_SECURE, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, ) user_permissions = get_permissions( @@ -473,8 +473,8 @@ async def signup(request: Request, response: Response, form_data: SignupForm): value=token, expires=datetime_expires_at, httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_SESSION_COOKIE_SAME_SITE, - secure=WEBUI_SESSION_COOKIE_SECURE, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, ) if request.app.state.config.WEBHOOK_URL: diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index cce3d6311..a85ccd05e 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -264,7 +264,10 @@ def add_file_to_knowledge_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if (knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -342,7 +345,12 @@ def update_file_from_knowledge_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): + raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -406,7 +414,11 @@ def remove_file_from_knowledge_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -484,7 +496,11 @@ async def delete_knowledge_by_id(id: str, user=Depends(get_verified_user)): detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -543,7 +559,11 @@ async def reset_knowledge_by_id(id: str, user=Depends(get_verified_user)): detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -582,7 +602,11 @@ def add_files_to_knowledge_batch( detail=ERROR_MESSAGES.NOT_FOUND, ) - if knowledge.user_id != user.id and user.role != "admin": + if ( + knowledge.user_id != user.id + and not has_access(user.id, "write", knowledge.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 6c8519b2c..a45814d32 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -183,7 +183,11 @@ async def delete_model_by_id(id: str, user=Depends(get_verified_user)): detail=ERROR_MESSAGES.NOT_FOUND, ) - if model.user_id != user.id and user.role != "admin": + if ( + user.role == "admin" + or model.user_id == user.id + or has_access(user.id, "write", model.access_control) + ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 014e5652e..9fb946c6e 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -147,7 +147,11 @@ async def delete_prompt_by_command(command: str, user=Depends(get_verified_user) detail=ERROR_MESSAGES.NOT_FOUND, ) - if prompt.user_id != user.id and user.role != "admin": + if ( + prompt.user_id != user.id + and not has_access(user.id, "write", prompt.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 7b9144b4c..d6a5c5532 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -227,7 +227,11 @@ async def delete_tools_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if tools.user_id != user.id and user.role != "admin": + if ( + tools.user_id != user.id + and not has_access(user.id, "write", tools.access_control) + and user.role != "admin" + ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index a83733d63..8792b1cfc 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -149,6 +149,7 @@ def openai_chat_chunk_message_template( template["choices"][0]["delta"] = {"content": message} else: template["choices"][0]["finish_reason"] = "stop" + template["choices"][0]["delta"] = {} if usage: template["usage"] = usage diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 1ae6d4aa7..b336f0631 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -35,7 +35,7 @@ from open_webui.config import ( AppConfig, ) from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES -from open_webui.env import WEBUI_SESSION_COOKIE_SAME_SITE, WEBUI_SESSION_COOKIE_SECURE +from open_webui.env import WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE from open_webui.utils.misc import parse_duration from open_webui.utils.auth import get_password_hash, create_token from open_webui.utils.webhook import post_webhook @@ -323,8 +323,8 @@ class OAuthManager: key="token", value=jwt_token, httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_SESSION_COOKIE_SAME_SITE, - secure=WEBUI_SESSION_COOKIE_SECURE, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, ) if ENABLE_OAUTH_SIGNUP.value: @@ -333,8 +333,8 @@ class OAuthManager: key="oauth_id_token", value=oauth_id_token, httponly=True, - samesite=WEBUI_SESSION_COOKIE_SAME_SITE, - secure=WEBUI_SESSION_COOKIE_SECURE, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, ) # Redirect back to the frontend with the JWT token redirect_url = f"{request.base_url}auth#token={jwt_token}" diff --git a/src/lib/components/admin/Users/UserList.svelte b/src/lib/components/admin/Users/UserList.svelte index 2ee4297a4..ce730571f 100644 --- a/src/lib/components/admin/Users/UserList.svelte +++ b/src/lib/components/admin/Users/UserList.svelte @@ -6,7 +6,9 @@ import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; + import localizedFormat from 'dayjs/plugin/localizedFormat'; dayjs.extend(relativeTime); + dayjs.extend(localizedFormat); import { toast } from 'svelte-sonner'; @@ -364,7 +366,7 @@ - {dayjs(user.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))} + {dayjs(user.created_at * 1000).format('LL')} {user.oauth_sub ?? ''} diff --git a/src/lib/components/admin/Users/UserList/EditUserModal.svelte b/src/lib/components/admin/Users/UserList/EditUserModal.svelte index 868951d57..9b2edb407 100644 --- a/src/lib/components/admin/Users/UserList/EditUserModal.svelte +++ b/src/lib/components/admin/Users/UserList/EditUserModal.svelte @@ -7,9 +7,11 @@ import { updateUserById } from '$lib/apis/users'; import Modal from '$lib/components/common/Modal.svelte'; + import localizedFormat from 'dayjs/plugin/localizedFormat'; const i18n = getContext('i18n'); const dispatch = createEventDispatcher(); + dayjs.extend(localizedFormat); export let show = false; export let selectedUser; @@ -87,7 +89,7 @@
{$i18n.t('Created at')} - {dayjs(selectedUser.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))} + {dayjs(selectedUser.created_at * 1000).format('LL')}
diff --git a/src/lib/components/admin/Users/UserList/UserChatsModal.svelte b/src/lib/components/admin/Users/UserList/UserChatsModal.svelte index 92970b658..a95df8291 100644 --- a/src/lib/components/admin/Users/UserList/UserChatsModal.svelte +++ b/src/lib/components/admin/Users/UserList/UserChatsModal.svelte @@ -2,8 +2,10 @@ import { toast } from 'svelte-sonner'; import dayjs from 'dayjs'; import { getContext, createEventDispatcher } from 'svelte'; + import localizedFormat from 'dayjs/plugin/localizedFormat'; const dispatch = createEventDispatcher(); + dayjs.extend(localizedFormat); import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats'; @@ -130,7 +132,7 @@
- {dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))} + {dayjs(chat.updated_at * 1000).format('LLL')}
diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte index ef17feb7f..a84077823 100644 --- a/src/lib/components/channel/Messages/Message.svelte +++ b/src/lib/components/channel/Messages/Message.svelte @@ -3,10 +3,12 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import isToday from 'dayjs/plugin/isToday'; import isYesterday from 'dayjs/plugin/isYesterday'; + import localizedFormat from 'dayjs/plugin/localizedFormat'; dayjs.extend(relativeTime); dayjs.extend(isToday); dayjs.extend(isYesterday); + dayjs.extend(localizedFormat); import { getContext, onMount } from 'svelte'; const i18n = getContext>('i18n'); @@ -154,9 +156,9 @@ class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize" > - {dayjs(message.created_at / 1000000).format('HH:mm')} + {dayjs(message.created_at / 1000000).format('LT')} {/if} @@ -175,7 +177,7 @@ class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]" > {formatDate(message.created_at / 1000000)} diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index bfd643c09..9c2a55b4f 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -82,10 +82,12 @@ import EventConfirmDialog from '../common/ConfirmDialog.svelte'; import Placeholder from './Placeholder.svelte'; import NotificationToast from '../NotificationToast.svelte'; + import Spinner from '../common/Spinner.svelte'; export let chatIdProp = ''; - let loaded = false; + let loading = false; + const eventTarget = new EventTarget(); let controlPane; let controlPaneComponent; @@ -133,6 +135,7 @@ $: if (chatIdProp) { (async () => { + loading = true; console.log(chatIdProp); prompt = ''; @@ -141,11 +144,9 @@ webSearchEnabled = false; imageGenerationEnabled = false; - loaded = false; - if (chatIdProp && (await loadChat())) { await tick(); - loaded = true; + loading = false; if (localStorage.getItem(`chat-input-${chatIdProp}`)) { try { @@ -1866,7 +1867,7 @@ : ' '} w-full max-w-full flex flex-col" id="chat-container" > - {#if !chatIdProp || (loaded && chatIdProp)} + {#if chatIdProp === '' || (!loading && chatIdProp)} {#if $settings?.backgroundImageUrl ?? null}
+
+ +
+
{/if} diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte index 20b297eb3..3c8eaf008 100644 --- a/src/lib/components/chat/MessageInput/InputMenu.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu.svelte @@ -48,6 +48,9 @@ init(); } + let fileUploadEnabled = true; + $: fileUploadEnabled = $user.role === 'admin' || $user?.permissions?.chat?.file_upload; + const init = async () => { if ($_tools === null) { await _tools.set(await getTools(localStorage.token)); @@ -166,26 +169,44 @@ {/if} {#if !$mobile} - { - screenCaptureHandler(); - }} + - -
{$i18n.t('Capture')}
-
+ { + if (fileUploadEnabled) { + screenCaptureHandler(); + } + }} + > + +
{$i18n.t('Capture')}
+
+ {/if} - { - uploadFilesHandler(); - }} + - -
{$i18n.t('Upload Files')}
-
+ { + if (fileUploadEnabled) { + uploadFilesHandler(); + } + }} + > + +
{$i18n.t('Upload Files')}
+
+ {#if $config?.features?.enable_google_drive_integration}