From 01293f640b7f537b4530ce809844ba991d6b0567 Mon Sep 17 00:00:00 2001 From: pablodanswer Date: Mon, 16 Dec 2024 12:58:18 -0800 Subject: [PATCH] Add delete all chats option --- backend/danswer/auth/noauth_user.py | 2 +- backend/danswer/server/manage/models.py | 3 + .../server/query_and_chat/token_limit.py | 4 +- .../common_utils/managers/settings.py | 73 +++++++++++++++++++ .../integration/common_utils/test_models.py | 16 ++++ .../anonymous_user/test_anonymous_user.py | 14 ++++ web/src/app/auth/login/page.tsx | 13 +++- web/src/app/auth/signup/page.tsx | 6 +- web/src/app/auth/verify-email/page.tsx | 2 +- .../app/auth/waiting-on-verification/page.tsx | 4 +- web/src/app/chat/ChatPage.tsx | 13 +--- web/src/app/chat/input/ChatInputBar.tsx | 20 +---- web/src/app/not-found.tsx | 2 +- web/src/app/page.tsx | 2 +- web/src/components/UserDropdown.tsx | 10 +++ web/src/components/admin/Layout.tsx | 2 +- web/src/components/chat_search/Header.tsx | 16 ++-- web/src/components/chat_search/hooks.ts | 5 ++ .../components/context/AssistantsContext.tsx | 6 -- web/src/components/icons/icons.tsx | 10 +-- web/src/lib/chat/fetchAssistantdata.ts | 2 - web/src/lib/chat/fetchChatData.ts | 4 - 22 files changed, 157 insertions(+), 72 deletions(-) create mode 100644 backend/tests/integration/common_utils/managers/settings.py create mode 100644 backend/tests/integration/tests/anonymous_user/test_anonymous_user.py diff --git a/backend/danswer/auth/noauth_user.py b/backend/danswer/auth/noauth_user.py index fdb48f34854..ea1ce812ea6 100644 --- a/backend/danswer/auth/noauth_user.py +++ b/backend/danswer/auth/noauth_user.py @@ -37,7 +37,7 @@ def fetch_no_auth_user( is_active=True, is_superuser=False, is_verified=True, - role=UserRole.ADMIN if anonymous_user_enabled else UserRole.BASIC, + role=UserRole.BASIC if anonymous_user_enabled else UserRole.ADMIN, preferences=load_no_auth_user_preferences(store), is_anonymous_user=anonymous_user_enabled, ) diff --git a/backend/danswer/server/manage/models.py b/backend/danswer/server/manage/models.py index af8e3568b76..8f84ef5fa21 100644 --- a/backend/danswer/server/manage/models.py +++ b/backend/danswer/server/manage/models.py @@ -62,6 +62,7 @@ class UserInfo(BaseModel): current_token_expiry_length: int | None = None is_cloud_superuser: bool = False organization_name: str | None = None + is_anonymous_user: bool | None = None @classmethod def from_model( @@ -71,6 +72,7 @@ def from_model( expiry_length: int | None = None, is_cloud_superuser: bool = False, organization_name: str | None = None, + is_anonymous_user: bool | None = None, ) -> "UserInfo": return cls( id=str(user.id), @@ -97,6 +99,7 @@ def from_model( current_token_created_at=current_token_created_at, current_token_expiry_length=expiry_length, is_cloud_superuser=is_cloud_superuser, + is_anonymous_user=is_anonymous_user, ) diff --git a/backend/danswer/server/query_and_chat/token_limit.py b/backend/danswer/server/query_and_chat/token_limit.py index 0f47ef7266f..4936644879c 100644 --- a/backend/danswer/server/query_and_chat/token_limit.py +++ b/backend/danswer/server/query_and_chat/token_limit.py @@ -11,7 +11,7 @@ from sqlalchemy import select from sqlalchemy.orm import Session -from danswer.auth.users import current_user +from danswer.auth.users import current_second_level_limited_user from danswer.db.engine import get_session_context_manager from danswer.db.engine import get_session_with_tenant from danswer.db.models import ChatMessage @@ -31,7 +31,7 @@ def check_token_rate_limits( - user: User | None = Depends(current_user), + user: User | None = Depends(current_second_level_limited_user), ) -> None: # short circuit if no rate limits are set up # NOTE: result of `any_rate_limit_exists` is cached, so this call is fast 99% of the time diff --git a/backend/tests/integration/common_utils/managers/settings.py b/backend/tests/integration/common_utils/managers/settings.py new file mode 100644 index 00000000000..241d85bedb3 --- /dev/null +++ b/backend/tests/integration/common_utils/managers/settings.py @@ -0,0 +1,73 @@ +from typing import Any +from typing import Dict +from typing import Optional + +import requests + +from tests.integration.common_utils.constants import API_SERVER_URL +from tests.integration.common_utils.constants import GENERAL_HEADERS +from tests.integration.common_utils.test_models import DATestSettings +from tests.integration.common_utils.test_models import DATestUser + + +class SettingsManager: + @staticmethod + def get_settings( + user_performing_action: DATestUser | None = None, + ) -> Dict[str, Any]: + headers = ( + user_performing_action.headers + if user_performing_action + else GENERAL_HEADERS + ) + headers.pop("Content-Type", None) + + response = requests.get( + f"{API_SERVER_URL}/api/manage/admin/settings", + headers=headers, + ) + + if not response.ok: + return ( + {}, + f"Failed to get settings - {response.json().get('detail', 'Unknown error')}", + ) + + return response.json(), "" + + @staticmethod + def update_settings( + settings: DATestSettings, + user_performing_action: DATestUser | None = None, + ) -> Dict[str, Any]: + headers = ( + user_performing_action.headers + if user_performing_action + else GENERAL_HEADERS + ) + headers.pop("Content-Type", None) + + payload = settings.model_dump() + response = requests.patch( + f"{API_SERVER_URL}/api/manage/admin/settings", + json=payload, + headers=headers, + ) + + if not response.ok: + return ( + {}, + f"Failed to update settings - {response.json().get('detail', 'Unknown error')}", + ) + + return response.json(), "" + + @staticmethod + def get_setting( + key: str, + user_performing_action: DATestUser | None = None, + ) -> Optional[Any]: + settings, error = SettingsManager.get_settings(user_performing_action) + if error: + return None + return settings.get(key) diff --git a/backend/tests/integration/common_utils/test_models.py b/backend/tests/integration/common_utils/test_models.py index 65a90259d8b..4b195444a28 100644 --- a/backend/tests/integration/common_utils/test_models.py +++ b/backend/tests/integration/common_utils/test_models.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Any from uuid import UUID @@ -150,3 +151,18 @@ class StreamedResponse(BaseModel): relevance_summaries: list[dict[str, Any]] | None = None tool_result: Any | None = None user: str | None = None + + +class DATestGatingType(str, Enum): + FULL = "full" + PARTIAL = "partial" + NONE = "none" + + +class DATestSettings(BaseModel): + """General settings""" + + maximum_chat_retention_days: int | None = None + gpu_enabled: bool | None = None + product_gating: DATestGatingType = DATestGatingType.NONE + anonymous_user_enabled: bool | None = None diff --git a/backend/tests/integration/tests/anonymous_user/test_anonymous_user.py b/backend/tests/integration/tests/anonymous_user/test_anonymous_user.py new file mode 100644 index 00000000000..101524a0fc1 --- /dev/null +++ b/backend/tests/integration/tests/anonymous_user/test_anonymous_user.py @@ -0,0 +1,14 @@ +from tests.integration.common_utils.managers.settings import SettingsManager +from tests.integration.common_utils.managers.user import UserManager +from tests.integration.common_utils.test_models import DATestSettings +from tests.integration.common_utils.test_models import DATestUser + + +def test_limited(reset: None) -> None: + """Verify that with a limited role key, limited endpoints are accessible and + others are not.""" + + # Creating an admin user (first user created is automatically an admin) + admin_user: DATestUser = UserManager.create(name="admin_user") + SettingsManager.update_settings(DATestSettings(anonymous_user_enabled=True)) + print(admin_user.headers) diff --git a/web/src/app/auth/login/page.tsx b/web/src/app/auth/login/page.tsx index 7a6459655ca..61aad0b0505 100644 --- a/web/src/app/auth/login/page.tsx +++ b/web/src/app/auth/login/page.tsx @@ -42,7 +42,7 @@ const Page = async (props: { // simply take the user to the home page if Auth is disabled if (authTypeMetadata?.authType === "disabled") { - return redirect("/"); + return redirect("/chat"); } // if user is already logged in, take them to the main app page @@ -50,12 +50,13 @@ const Page = async (props: { if ( currentUser && currentUser.is_active && + !currentUser.is_anonymous_user && (secondsTillExpiration === null || secondsTillExpiration > 0) ) { if (authTypeMetadata?.requiresVerification && !currentUser.is_verified) { return redirect("/auth/waiting-on-verification"); } - return redirect("/"); + return redirect("/chat"); } // get where to send the user to authenticate @@ -105,7 +106,9 @@ const Page = async (props: { Don't have an account?{" "} Create an account @@ -127,7 +130,9 @@ const Page = async (props: { Don't have an account?{" "} Create an account diff --git a/web/src/app/auth/signup/page.tsx b/web/src/app/auth/signup/page.tsx index 94a7d1967bb..921af4d2c47 100644 --- a/web/src/app/auth/signup/page.tsx +++ b/web/src/app/auth/signup/page.tsx @@ -39,13 +39,13 @@ const Page = async (props: { // simply take the user to the home page if Auth is disabled if (authTypeMetadata?.authType === "disabled") { - return redirect("/"); + return redirect("/chat"); } // if user is already logged in, take them to the main app page if (currentUser && currentUser.is_active) { if (!authTypeMetadata?.requiresVerification || currentUser.is_verified) { - return redirect("/"); + return redirect("/chat"); } return redirect("/auth/waiting-on-verification"); } @@ -53,7 +53,7 @@ const Page = async (props: { // only enable this page if basic login is enabled if (authTypeMetadata?.authType !== "basic" && !cloud) { - return redirect("/"); + return redirect("/chat"); } let authUrl: string | null = null; diff --git a/web/src/app/auth/verify-email/page.tsx b/web/src/app/auth/verify-email/page.tsx index f6faa075b9f..2c70c98d7ab 100644 --- a/web/src/app/auth/verify-email/page.tsx +++ b/web/src/app/auth/verify-email/page.tsx @@ -23,7 +23,7 @@ export default async function Page() { } if (!authTypeMetadata?.requiresVerification || currentUser?.is_verified) { - return redirect("/"); + return redirect("/chat"); } return ; diff --git a/web/src/app/auth/waiting-on-verification/page.tsx b/web/src/app/auth/waiting-on-verification/page.tsx index 3cd8035ae17..cadc4431438 100644 --- a/web/src/app/auth/waiting-on-verification/page.tsx +++ b/web/src/app/auth/waiting-on-verification/page.tsx @@ -27,13 +27,13 @@ export default async function Page() { if (!currentUser) { if (authTypeMetadata?.authType === "disabled") { - return redirect("/"); + return redirect("/chat"); } return redirect("/auth/login"); } if (!authTypeMetadata?.requiresVerification || currentUser.is_verified) { - return redirect("/"); + return redirect("/chat"); } return ( diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index de5b8cea481..149970787ba 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -184,8 +184,6 @@ export function ChatPage({ const [userSettingsToggled, setUserSettingsToggled] = useState(false); const { assistants: availableAssistants, finalAssistants } = useAssistants(); - console.log(availableAssistants); - console.log(finalAssistants); const [showApiKeyModal, setShowApiKeyModal] = useState( !shouldShowWelcomeModal @@ -1652,6 +1650,7 @@ export function ChatPage({ setShowDocSidebar: setShowHistorySidebar, setToggled: removeToggle, mobile: settings?.isMobile, + isAnonymousUser: user?.is_anonymous_user, }); const autoScrollEnabled = @@ -2735,24 +2734,16 @@ export function ChatPage({ removeDocs={() => { clearSelectedDocuments(); }} - removeFilters={() => { - filterManager.setSelectedSources([]); - filterManager.setSelectedTags([]); - filterManager.setSelectedDocumentSets([]); - setDocumentSidebarToggled(false); - }} showConfigureAPIKey={() => setShowApiKeyModal(true) } chatState={currentSessionChatState} stopGenerating={stopGenerating} - openModelSettings={() => setSettingsToggled(true)} inputPrompts={userInputPrompts} showDocs={() => setDocumentSelection(true)} selectedDocuments={selectedDocuments} // assistant stuff selectedAssistant={liveAssistant} - setSelectedAssistant={onAssistantChange} setAlternativeAssistant={setAlternativeAssistant} alternativeAssistant={alternativeAssistant} // end assistant stuff @@ -2760,7 +2751,6 @@ export function ChatPage({ setMessage={setMessage} onSubmit={onSubmit} filterManager={filterManager} - llmOverrideManager={llmOverrideManager} files={currentMessageFiles} setFiles={setCurrentMessageFiles} toggleFilters={ @@ -2768,7 +2758,6 @@ export function ChatPage({ } handleFileUpload={handleImageUpload} textAreaRef={textAreaRef} - chatSessionId={chatSessionIdRef.current!} /> {enterpriseSettings && enterpriseSettings.custom_lower_disclaimer_content && ( diff --git a/web/src/app/chat/input/ChatInputBar.tsx b/web/src/app/chat/input/ChatInputBar.tsx index 3e41f4f3203..46284f945ac 100644 --- a/web/src/app/chat/input/ChatInputBar.tsx +++ b/web/src/app/chat/input/ChatInputBar.tsx @@ -3,8 +3,7 @@ import { FiPlusCircle, FiPlus, FiInfo, FiX, FiSearch } from "react-icons/fi"; import { ChatInputOption } from "./ChatInputOption"; import { Persona } from "@/app/admin/assistants/interfaces"; import { InputPrompt } from "@/app/admin/prompt-library/interfaces"; -import { FilterManager, LlmOverrideManager } from "@/lib/hooks"; -import { SelectedFilterDisplay } from "./SelectedFilterDisplay"; +import { FilterManager } from "@/lib/hooks"; import { useChatContext } from "@/components/context/ChatContext"; import { getFinalLLM } from "@/lib/llm/utils"; import { ChatFileType, FileDescriptor } from "../interfaces"; @@ -31,22 +30,13 @@ import { SettingsContext } from "@/components/settings/SettingsProvider"; import { ChatState } from "../types"; import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText"; import { useAssistants } from "@/components/context/AssistantsContext"; -import AnimatedToggle from "@/components/search/SearchBar"; -import { Popup } from "@/components/admin/connectors/Popup"; -import { AssistantsTab } from "../modal/configuration/AssistantsTab"; -import { IconType } from "react-icons"; -import { LlmTab } from "../modal/configuration/LlmTab"; import { XIcon } from "lucide-react"; -import { FilterPills } from "./FilterPills"; -import { Tag } from "@/lib/types"; import FiltersDisplay from "./FilterDisplay"; const MAX_INPUT_HEIGHT = 200; interface ChatInputBarProps { - removeFilters: () => void; removeDocs: () => void; - openModelSettings: () => void; showDocs: () => void; showConfigureAPIKey: () => void; selectedDocuments: DanswerDocument[]; @@ -55,27 +45,22 @@ interface ChatInputBarProps { stopGenerating: () => void; onSubmit: () => void; filterManager: FilterManager; - llmOverrideManager: LlmOverrideManager; chatState: ChatState; alternativeAssistant: Persona | null; inputPrompts: InputPrompt[]; // assistants selectedAssistant: Persona; - setSelectedAssistant: (assistant: Persona) => void; setAlternativeAssistant: (alternativeAssistant: Persona | null) => void; files: FileDescriptor[]; setFiles: (files: FileDescriptor[]) => void; handleFileUpload: (files: File[]) => void; textAreaRef: React.RefObject; - chatSessionId?: string; toggleFilters?: () => void; } export function ChatInputBar({ - removeFilters, removeDocs, - openModelSettings, showDocs, showConfigureAPIKey, selectedDocuments, @@ -84,12 +69,10 @@ export function ChatInputBar({ stopGenerating, onSubmit, filterManager, - llmOverrideManager, chatState, // assistants selectedAssistant, - setSelectedAssistant, setAlternativeAssistant, files, @@ -97,7 +80,6 @@ export function ChatInputBar({ handleFileUpload, textAreaRef, alternativeAssistant, - chatSessionId, inputPrompts, toggleFilters, }: ChatInputBarProps) { diff --git a/web/src/app/not-found.tsx b/web/src/app/not-found.tsx index 6866db7cbbb..3eb4f1b948f 100644 --- a/web/src/app/not-found.tsx +++ b/web/src/app/not-found.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function NotFound() { - redirect("/chat"); + redirect("/auth/login"); } diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 557deca870a..78b4bf9e612 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default async function Page() { - redirect("/chat"); + redirect("/auth/login"); } diff --git a/web/src/components/UserDropdown.tsx b/web/src/components/UserDropdown.tsx index cc43980b8c2..33af868fc40 100644 --- a/web/src/components/UserDropdown.tsx +++ b/web/src/components/UserDropdown.tsx @@ -59,9 +59,11 @@ const DropdownOption: React.FC = ({ export function UserDropdown({ page, toggleUserSettings, + hideUserDropdown, }: { page?: pageType; toggleUserSettings?: () => void; + hideUserDropdown?: boolean; }) { const { user, isCurator } = useUser(); const [userInfoVisible, setUserInfoVisible] = useState(false); @@ -114,6 +116,7 @@ export function UserDropdown({ }; const showAdminPanel = !user || user.role === UserRole.ADMIN; + const showCuratorPanel = user && isCurator; const showLogout = user && !checkUserIsNoAuthUser(user.id) && !LOGOUT_DISABLED; @@ -183,6 +186,12 @@ export function UserDropdown({ notifications={notifications || []} refreshNotifications={refreshNotifications} /> + ) : hideUserDropdown ? ( + router.push("/auth/login")} + icon={} + label="Log In" + /> ) : ( <> {customNavItems.map((item, i) => ( @@ -251,6 +260,7 @@ export function UserDropdown({ label="User Settings" /> )} + { setUserInfoVisible(true); diff --git a/web/src/components/admin/Layout.tsx b/web/src/components/admin/Layout.tsx index 2b1cef8cc2c..8928fabceec 100644 --- a/web/src/components/admin/Layout.tsx +++ b/web/src/components/admin/Layout.tsx @@ -35,7 +35,7 @@ export async function Layout({ children }: { children: React.ReactNode }) { return redirect("/auth/login"); } if (user.role === UserRole.BASIC) { - return redirect("/"); + return redirect("/chat"); } if (!user.is_verified && requiresVerification) { return redirect("/auth/waiting-on-verification"); diff --git a/web/src/components/chat_search/Header.tsx b/web/src/components/chat_search/Header.tsx index ec4f04d447d..03caaa81517 100644 --- a/web/src/components/chat_search/Header.tsx +++ b/web/src/components/chat_search/Header.tsx @@ -121,14 +121,14 @@ export default function FunctionalHeader({ )} - {!hideUserDropdown && ( -
- -
- )} + +
+ +
>; mobile?: boolean; setToggled?: () => void; + isAnonymousUser?: boolean; } export const useSidebarVisibility = ({ @@ -16,11 +17,15 @@ export const useSidebarVisibility = ({ setToggled, showDocSidebar, mobile, + isAnonymousUser, }: UseSidebarVisibilityProps) => { const xPosition = useRef(0); useEffect(() => { const handleEvent = (event: MouseEvent) => { + if (isAnonymousUser) { + return; + } const currentXPosition = event.clientX; xPosition.current = currentXPosition; diff --git a/web/src/components/context/AssistantsContext.tsx b/web/src/components/context/AssistantsContext.tsx index dd8bc0a9cac..5321affb140 100644 --- a/web/src/components/context/AssistantsContext.tsx +++ b/web/src/components/context/AssistantsContext.tsx @@ -45,7 +45,6 @@ export const AssistantsProvider: React.FC<{ hasAnyConnectors, hasImageCompatibleModel, }) => { - console.log("initialAssistants", initialAssistants); const [assistants, setAssistants] = useState( initialAssistants || [] ); @@ -175,15 +174,10 @@ export const AssistantsProvider: React.FC<{ finalAssistants, ownedButHiddenAssistants, } = useMemo(() => { - console.log("claaassifying assistants", assistants); - // console.log(assistants); - const { visibleAssistants, hiddenAssistants } = classifyAssistants( user, assistants ); - console.log("visibleAssistants", visibleAssistants); - console.log("hiddenAssistants", hiddenAssistants); const finalAssistants = user ? orderAssistantsForUser(visibleAssistants, user) diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 2ae66809136..60ba0b68ff7 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -2621,7 +2621,7 @@ export const OpenIcon = ({ @@ -2645,7 +2645,7 @@ export const DexpandTwoIcon = ({ @@ -2669,7 +2669,7 @@ export const ExpandTwoIcon = ({ @@ -2693,7 +2693,7 @@ export const DownloadCSVIcon = ({ @@ -2717,7 +2717,7 @@ export const UserIcon = ({ { return defaultState; } - console.log("returning hte basic list assistants", assistants); // Parallel fetch of additional data const [ccPairsResponse, llmProviders] = await Promise.all([ fetchSS("/manage/connector-status").catch((error) => { @@ -56,7 +55,6 @@ export async function fetchAssistantData(): Promise { hasAnyConnectors, hasImageCompatibleModel ); - console.log("filteredAssistants", filteredAssistants); return { assistants: filteredAssistants, diff --git a/web/src/lib/chat/fetchChatData.ts b/web/src/lib/chat/fetchChatData.ts index 02adf4e7713..a43ca0ee92b 100644 --- a/web/src/lib/chat/fetchChatData.ts +++ b/web/src/lib/chat/fetchChatData.ts @@ -91,7 +91,6 @@ export async function fetchChatData(searchParams: { const authDisabled = authTypeMetadata?.authType === "disabled"; - console.log(authTypeMetadata); // TODO Validate need if (!authDisabled && !user && !authTypeMetadata?.anonymousUserEnabled) { const headersList = await headers(); @@ -103,13 +102,10 @@ export async function fetchChatData(searchParams: { ? `${fullUrl}?${searchParamsString}` : fullUrl; - console.log("HISSE 1S"); - return redirect(`/auth/login?next=${encodeURIComponent(redirectUrl)}`); } if (user && !user.is_verified && authTypeMetadata?.requiresVerification) { - console.log("HISSES 2"); return { redirect: "/auth/waiting-on-verification" }; }