Add anonymous user to main

Anonymous user
This commit is contained in:
pablonyx 2024-12-31 15:58:52 -05:00 committed by GitHub
commit 1291b3d930
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 583 additions and 330 deletions

View File

@ -30,13 +30,16 @@ def load_no_auth_user_preferences(store: KeyValueStore) -> UserPreferences:
) )
def fetch_no_auth_user(store: KeyValueStore) -> UserInfo: def fetch_no_auth_user(
store: KeyValueStore, *, anonymous_user_enabled: bool | None = None
) -> UserInfo:
return UserInfo( return UserInfo(
id=NO_AUTH_USER_ID, id=NO_AUTH_USER_ID,
email=NO_AUTH_USER_EMAIL, email=NO_AUTH_USER_EMAIL,
is_active=True, is_active=True,
is_superuser=False, is_superuser=False,
is_verified=True, is_verified=True,
role=UserRole.ADMIN, role=UserRole.BASIC if anonymous_user_enabled else UserRole.ADMIN,
preferences=load_no_auth_user_preferences(store), preferences=load_no_auth_user_preferences(store),
is_anonymous_user=anonymous_user_enabled,
) )

View File

@ -69,6 +69,7 @@ from onyx.configs.constants import AuthType
from onyx.configs.constants import DANSWER_API_KEY_DUMMY_EMAIL_DOMAIN from onyx.configs.constants import DANSWER_API_KEY_DUMMY_EMAIL_DOMAIN
from onyx.configs.constants import DANSWER_API_KEY_PREFIX from onyx.configs.constants import DANSWER_API_KEY_PREFIX
from onyx.configs.constants import MilestoneRecordType from onyx.configs.constants import MilestoneRecordType
from onyx.configs.constants import OnyxRedisLocks
from onyx.configs.constants import PASSWORD_SPECIAL_CHARS from onyx.configs.constants import PASSWORD_SPECIAL_CHARS
from onyx.configs.constants import UNNAMED_KEY_PLACEHOLDER from onyx.configs.constants import UNNAMED_KEY_PLACEHOLDER
from onyx.db.api_key import fetch_user_for_api_key from onyx.db.api_key import fetch_user_for_api_key
@ -84,7 +85,7 @@ from onyx.db.models import AccessToken
from onyx.db.models import OAuthAccount from onyx.db.models import OAuthAccount
from onyx.db.models import User from onyx.db.models import User
from onyx.db.users import get_user_by_email from onyx.db.users import get_user_by_email
from onyx.server.utils import BasicAuthenticationError from onyx.redis.redis_pool import get_redis_client
from onyx.utils.logger import setup_logger from onyx.utils.logger import setup_logger
from onyx.utils.telemetry import create_milestone_and_report from onyx.utils.telemetry import create_milestone_and_report
from onyx.utils.telemetry import optional_telemetry from onyx.utils.telemetry import optional_telemetry
@ -98,6 +99,11 @@ from shared_configs.contextvars import CURRENT_TENANT_ID_CONTEXTVAR
logger = setup_logger() logger = setup_logger()
class BasicAuthenticationError(HTTPException):
def __init__(self, detail: str):
super().__init__(status_code=status.HTTP_403_FORBIDDEN, detail=detail)
def is_user_admin(user: User | None) -> bool: def is_user_admin(user: User | None) -> bool:
if AUTH_TYPE == AuthType.DISABLED: if AUTH_TYPE == AuthType.DISABLED:
return True return True
@ -138,6 +144,20 @@ def user_needs_to_be_verified() -> bool:
return False return False
def anonymous_user_enabled() -> bool:
if MULTI_TENANT:
return False
redis_client = get_redis_client(tenant_id=None)
value = redis_client.get(OnyxRedisLocks.ANONYMOUS_USER_ENABLED)
if value is None:
return False
assert isinstance(value, bytes)
return int(value.decode("utf-8")) == 1
def verify_email_is_invited(email: str) -> None: def verify_email_is_invited(email: str) -> None:
whitelist = get_invited_users() whitelist = get_invited_users()
if not whitelist: if not whitelist:
@ -690,30 +710,36 @@ async def double_check_user(
user: User | None, user: User | None,
optional: bool = DISABLE_AUTH, optional: bool = DISABLE_AUTH,
include_expired: bool = False, include_expired: bool = False,
allow_anonymous_access: bool = False,
) -> User | None: ) -> User | None:
if optional: if optional:
return user
if user is not None:
# If user attempted to authenticate, verify them, do not default
# to anonymous access if it fails.
if user_needs_to_be_verified() and not user.is_verified:
raise BasicAuthenticationError(
detail="Access denied. User is not verified.",
)
if (
user.oidc_expiry
and user.oidc_expiry < datetime.now(timezone.utc)
and not include_expired
):
raise BasicAuthenticationError(
detail="Access denied. User's OIDC token has expired.",
)
return user
if allow_anonymous_access:
return None return None
if user is None: raise BasicAuthenticationError(
raise BasicAuthenticationError( detail="Access denied. User is not authenticated.",
detail="Access denied. User is not authenticated.", )
)
if user_needs_to_be_verified() and not user.is_verified:
raise BasicAuthenticationError(
detail="Access denied. User is not verified.",
)
if (
user.oidc_expiry
and user.oidc_expiry < datetime.now(timezone.utc)
and not include_expired
):
raise BasicAuthenticationError(
detail="Access denied. User's OIDC token has expired.",
)
return user
async def current_user_with_expired_token( async def current_user_with_expired_token(
@ -728,6 +754,14 @@ async def current_limited_user(
return await double_check_user(user) return await double_check_user(user)
async def current_chat_accesssible_user(
user: User | None = Depends(optional_user),
) -> User | None:
return await double_check_user(
user, allow_anonymous_access=anonymous_user_enabled()
)
async def current_user( async def current_user(
user: User | None = Depends(optional_user), user: User | None = Depends(optional_user),
) -> User | None: ) -> User | None:

View File

@ -279,6 +279,7 @@ class OnyxRedisLocks:
SLACK_BOT_LOCK = "da_lock:slack_bot" SLACK_BOT_LOCK = "da_lock:slack_bot"
SLACK_BOT_HEARTBEAT_PREFIX = "da_heartbeat:slack_bot" SLACK_BOT_HEARTBEAT_PREFIX = "da_heartbeat:slack_bot"
ANONYMOUS_USER_ENABLED = "anonymous_user_enabled"
class OnyxRedisSignals: class OnyxRedisSignals:

View File

@ -5,6 +5,7 @@ from fastapi.dependencies.models import Dependant
from starlette.routing import BaseRoute from starlette.routing import BaseRoute
from onyx.auth.users import current_admin_user from onyx.auth.users import current_admin_user
from onyx.auth.users import current_chat_accesssible_user
from onyx.auth.users import current_curator_or_admin_user from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_limited_user from onyx.auth.users import current_limited_user
from onyx.auth.users import current_user from onyx.auth.users import current_user
@ -109,6 +110,7 @@ def check_router_auth(
or depends_fn == current_curator_or_admin_user or depends_fn == current_curator_or_admin_user
or depends_fn == api_key_dep or depends_fn == api_key_dep
or depends_fn == current_user_with_expired_token or depends_fn == current_user_with_expired_token
or depends_fn == current_chat_accesssible_user
or depends_fn == control_plane_dep or depends_fn == control_plane_dep
or depends_fn == current_cloud_superuser or depends_fn == current_cloud_superuser
): ):

View File

@ -14,6 +14,7 @@ from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user from onyx.auth.users import current_admin_user
from onyx.auth.users import current_chat_accesssible_user
from onyx.auth.users import current_curator_or_admin_user from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user from onyx.auth.users import current_user
from onyx.background.celery.celery_utils import get_deletion_attempt_snapshot from onyx.background.celery.celery_utils import get_deletion_attempt_snapshot
@ -1055,7 +1056,7 @@ class BasicCCPairInfo(BaseModel):
@router.get("/connector-status") @router.get("/connector-status")
def get_basic_connector_indexing_status( def get_basic_connector_indexing_status(
_: User = Depends(current_user), _: User = Depends(current_chat_accesssible_user),
db_session: Session = Depends(get_session), db_session: Session = Depends(get_session),
) -> list[BasicCCPairInfo]: ) -> list[BasicCCPairInfo]:
cc_pairs = get_connector_credential_pairs(db_session, eager_load_connector=True) cc_pairs = get_connector_credential_pairs(db_session, eager_load_connector=True)

View File

@ -10,6 +10,7 @@ from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user from onyx.auth.users import current_admin_user
from onyx.auth.users import current_chat_accesssible_user
from onyx.auth.users import current_curator_or_admin_user from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_limited_user from onyx.auth.users import current_limited_user
from onyx.auth.users import current_user from onyx.auth.users import current_user
@ -323,7 +324,7 @@ def get_image_generation_tool(
@basic_router.get("") @basic_router.get("")
def list_personas( def list_personas(
user: User | None = Depends(current_user), user: User | None = Depends(current_chat_accesssible_user),
db_session: Session = Depends(get_session), db_session: Session = Depends(get_session),
include_deleted: bool = False, include_deleted: bool = False,
persona_ids: list[int] = Query(None), persona_ids: list[int] = Query(None),

View File

@ -1,6 +1,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from onyx import __version__ from onyx import __version__
from onyx.auth.users import anonymous_user_enabled
from onyx.auth.users import user_needs_to_be_verified from onyx.auth.users import user_needs_to_be_verified
from onyx.configs.app_configs import AUTH_TYPE from onyx.configs.app_configs import AUTH_TYPE
from onyx.server.manage.models import AuthTypeResponse from onyx.server.manage.models import AuthTypeResponse
@ -18,7 +19,9 @@ def healthcheck() -> StatusResponse:
@router.get("/auth/type") @router.get("/auth/type")
def get_auth_type() -> AuthTypeResponse: def get_auth_type() -> AuthTypeResponse:
return AuthTypeResponse( return AuthTypeResponse(
auth_type=AUTH_TYPE, requires_verification=user_needs_to_be_verified() auth_type=AUTH_TYPE,
requires_verification=user_needs_to_be_verified(),
anonymous_user_enabled=anonymous_user_enabled(),
) )

View File

@ -7,7 +7,7 @@ from fastapi import Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user from onyx.auth.users import current_admin_user
from onyx.auth.users import current_user from onyx.auth.users import current_chat_accesssible_user
from onyx.db.engine import get_session from onyx.db.engine import get_session
from onyx.db.llm import fetch_existing_llm_providers from onyx.db.llm import fetch_existing_llm_providers
from onyx.db.llm import fetch_provider from onyx.db.llm import fetch_provider
@ -189,7 +189,7 @@ def set_provider_as_default(
@basic_router.get("/provider") @basic_router.get("/provider")
def list_llm_provider_basics( def list_llm_provider_basics(
user: User | None = Depends(current_user), user: User | None = Depends(current_chat_accesssible_user),
db_session: Session = Depends(get_session), db_session: Session = Depends(get_session),
) -> list[LLMProviderDescriptor]: ) -> list[LLMProviderDescriptor]:
return [ return [

View File

@ -37,6 +37,7 @@ class AuthTypeResponse(BaseModel):
# specifies whether the current auth setup requires # specifies whether the current auth setup requires
# users to have verified emails # users to have verified emails
requires_verification: bool requires_verification: bool
anonymous_user_enabled: bool | None = None
class UserPreferences(BaseModel): class UserPreferences(BaseModel):
@ -61,6 +62,7 @@ class UserInfo(BaseModel):
current_token_expiry_length: int | None = None current_token_expiry_length: int | None = None
is_cloud_superuser: bool = False is_cloud_superuser: bool = False
organization_name: str | None = None organization_name: str | None = None
is_anonymous_user: bool | None = None
@classmethod @classmethod
def from_model( def from_model(
@ -70,6 +72,7 @@ class UserInfo(BaseModel):
expiry_length: int | None = None, expiry_length: int | None = None,
is_cloud_superuser: bool = False, is_cloud_superuser: bool = False,
organization_name: str | None = None, organization_name: str | None = None,
is_anonymous_user: bool | None = None,
) -> "UserInfo": ) -> "UserInfo":
return cls( return cls(
id=str(user.id), id=str(user.id),
@ -96,6 +99,7 @@ class UserInfo(BaseModel):
current_token_created_at=current_token_created_at, current_token_created_at=current_token_created_at,
current_token_expiry_length=expiry_length, current_token_expiry_length=expiry_length,
is_cloud_superuser=is_cloud_superuser, is_cloud_superuser=is_cloud_superuser,
is_anonymous_user=is_anonymous_user,
) )

View File

@ -28,6 +28,7 @@ from onyx.auth.noauth_user import fetch_no_auth_user
from onyx.auth.noauth_user import set_no_auth_user_preferences from onyx.auth.noauth_user import set_no_auth_user_preferences
from onyx.auth.schemas import UserRole from onyx.auth.schemas import UserRole
from onyx.auth.schemas import UserStatus from onyx.auth.schemas import UserStatus
from onyx.auth.users import anonymous_user_enabled
from onyx.auth.users import current_admin_user from onyx.auth.users import current_admin_user
from onyx.auth.users import current_curator_or_admin_user from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user from onyx.auth.users import current_user
@ -484,13 +485,15 @@ def verify_user_logged_in(
# NOTE: this does not use `current_user` / `current_admin_user` because we don't want # NOTE: this does not use `current_user` / `current_admin_user` because we don't want
# to enforce user verification here - the frontend always wants to get the info about # to enforce user verification here - the frontend always wants to get the info about
# the current user regardless of if they are currently verified # the current user regardless of if they are currently verified
if user is None: if user is None:
# if auth type is disabled, return a dummy user with preferences from # if auth type is disabled, return a dummy user with preferences from
# the key-value store # the key-value store
if AUTH_TYPE == AuthType.DISABLED: if AUTH_TYPE == AuthType.DISABLED:
store = get_kv_store() store = get_kv_store()
return fetch_no_auth_user(store) return fetch_no_auth_user(store)
if anonymous_user_enabled():
store = get_kv_store()
return fetch_no_auth_user(store, anonymous_user_enabled=True)
raise BasicAuthenticationError(detail="User Not Authenticated") raise BasicAuthenticationError(detail="User Not Authenticated")
if user.oidc_expiry and user.oidc_expiry < datetime.now(timezone.utc): if user.oidc_expiry and user.oidc_expiry < datetime.now(timezone.utc):

View File

@ -19,6 +19,7 @@ from PIL import Image
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from onyx.auth.users import current_chat_accesssible_user
from onyx.auth.users import current_limited_user from onyx.auth.users import current_limited_user
from onyx.auth.users import current_user from onyx.auth.users import current_user
from onyx.chat.chat_utils import create_chat_chain from onyx.chat.chat_utils import create_chat_chain
@ -145,7 +146,7 @@ def update_chat_session_model(
def get_chat_session( def get_chat_session(
session_id: UUID, session_id: UUID,
is_shared: bool = False, is_shared: bool = False,
user: User | None = Depends(current_user), user: User | None = Depends(current_chat_accesssible_user),
db_session: Session = Depends(get_session), db_session: Session = Depends(get_session),
) -> ChatSessionDetailResponse: ) -> ChatSessionDetailResponse:
user_id = user.id if user is not None else None user_id = user.id if user is not None else None
@ -200,7 +201,7 @@ def get_chat_session(
@router.post("/create-chat-session") @router.post("/create-chat-session")
def create_new_chat_session( def create_new_chat_session(
chat_session_creation_request: ChatSessionCreationRequest, chat_session_creation_request: ChatSessionCreationRequest,
user: User | None = Depends(current_limited_user), user: User | None = Depends(current_chat_accesssible_user),
db_session: Session = Depends(get_session), db_session: Session = Depends(get_session),
) -> CreateChatSessionID: ) -> CreateChatSessionID:
user_id = user.id if user is not None else None user_id = user.id if user is not None else None
@ -333,7 +334,7 @@ async def is_connected(request: Request) -> Callable[[], bool]:
def handle_new_chat_message( def handle_new_chat_message(
chat_message_req: CreateChatMessageRequest, chat_message_req: CreateChatMessageRequest,
request: Request, request: Request,
user: User | None = Depends(current_limited_user), user: User | None = Depends(current_chat_accesssible_user),
_rate_limit_check: None = Depends(check_token_rate_limits), _rate_limit_check: None = Depends(check_token_rate_limits),
is_connected_func: Callable[[], bool] = Depends(is_connected), is_connected_func: Callable[[], bool] = Depends(is_connected),
tenant_id: str = Depends(get_current_tenant_id), tenant_id: str = Depends(get_current_tenant_id),

View File

@ -11,7 +11,7 @@ from sqlalchemy import func
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from onyx.auth.users import current_user from onyx.auth.users import current_chat_accesssible_user
from onyx.db.engine import get_session_context_manager from onyx.db.engine import get_session_context_manager
from onyx.db.engine import get_session_with_tenant from onyx.db.engine import get_session_with_tenant
from onyx.db.models import ChatMessage from onyx.db.models import ChatMessage
@ -31,7 +31,7 @@ TOKEN_BUDGET_UNIT = 1_000
def check_token_rate_limits( def check_token_rate_limits(
user: User | None = Depends(current_user), user: User | None = Depends(current_chat_accesssible_user),
) -> None: ) -> None:
# short circuit if no rate limits are set up # 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 # NOTE: result of `any_rate_limit_exists` is cached, so this call is fast 99% of the time

View File

@ -44,6 +44,7 @@ class Settings(BaseModel):
maximum_chat_retention_days: int | None = None maximum_chat_retention_days: int | None = None
gpu_enabled: bool | None = None gpu_enabled: bool | None = None
product_gating: GatingType = GatingType.NONE product_gating: GatingType = GatingType.NONE
anonymous_user_enabled: bool | None = None
class UserSettings(Settings): class UserSettings(Settings):

View File

@ -1,21 +1,38 @@
from typing import cast
from onyx.configs.constants import KV_SETTINGS_KEY from onyx.configs.constants import KV_SETTINGS_KEY
from onyx.configs.constants import OnyxRedisLocks
from onyx.key_value_store.factory import get_kv_store from onyx.key_value_store.factory import get_kv_store
from onyx.key_value_store.interface import KvKeyNotFoundError from onyx.redis.redis_pool import get_redis_client
from onyx.server.settings.models import Settings from onyx.server.settings.models import Settings
from shared_configs.configs import MULTI_TENANT
def load_settings() -> Settings: def load_settings() -> Settings:
dynamic_config_store = get_kv_store() if MULTI_TENANT:
try: # If multi-tenant, anonymous user is always false
settings = Settings(**cast(dict, dynamic_config_store.load(KV_SETTINGS_KEY))) anonymous_user_enabled = False
except KvKeyNotFoundError: else:
settings = Settings() redis_client = get_redis_client(tenant_id=None)
dynamic_config_store.store(KV_SETTINGS_KEY, settings.model_dump()) value = redis_client.get(OnyxRedisLocks.ANONYMOUS_USER_ENABLED)
if value is not None:
assert isinstance(value, bytes)
anonymous_user_enabled = int(value.decode("utf-8")) == 1
else:
# Default to False
anonymous_user_enabled = False
# Optionally store the default back to Redis
redis_client.set(OnyxRedisLocks.ANONYMOUS_USER_ENABLED, "0")
settings = Settings(anonymous_user_enabled=anonymous_user_enabled)
return settings return settings
def store_settings(settings: Settings) -> None: def store_settings(settings: Settings) -> None:
if not MULTI_TENANT and settings.anonymous_user_enabled is not None:
# Only non-multi-tenant scenario can set the anonymous user enabled flag
redis_client = get_redis_client(tenant_id=None)
redis_client.set(
OnyxRedisLocks.ANONYMOUS_USER_ENABLED,
"1" if settings.anonymous_user_enabled else "0",
)
get_kv_store().store(KV_SETTINGS_KEY, settings.model_dump()) get_kv_store().store(KV_SETTINGS_KEY, settings.model_dump())

View File

@ -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,
) -> tuple[Dict[str, Any], str]:
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,
) -> tuple[Dict[str, Any], str]:
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)

View File

@ -1,3 +1,4 @@
from enum import Enum
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
@ -150,3 +151,18 @@ class StreamedResponse(BaseModel):
relevance_summaries: list[dict[str, Any]] | None = None relevance_summaries: list[dict[str, Any]] | None = None
tool_result: Any | None = None tool_result: Any | None = None
user: str | 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

View File

@ -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)

View File

@ -10,6 +10,7 @@ import { DefaultDropdown, Option } from "@/components/Dropdown";
import React, { useContext, useState, useEffect } from "react"; import React, { useContext, useState, useEffect } from "react";
import { SettingsContext } from "@/components/settings/SettingsProvider"; import { SettingsContext } from "@/components/settings/SettingsProvider";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled"; import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { Modal } from "@/components/Modal";
export function Checkbox({ export function Checkbox({
label, label,
@ -102,6 +103,7 @@ function IntegerInput({
export function SettingsForm() { export function SettingsForm() {
const router = useRouter(); const router = useRouter();
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [settings, setSettings] = useState<Settings | null>(null); const [settings, setSettings] = useState<Settings | null>(null);
const [chatRetention, setChatRetention] = useState(""); const [chatRetention, setChatRetention] = useState("");
const { popup, setPopup } = usePopup(); const { popup, setPopup } = usePopup();
@ -171,11 +173,22 @@ export function SettingsForm() {
fieldName: keyof Settings, fieldName: keyof Settings,
checked: boolean checked: boolean
) { ) {
const updates: { fieldName: keyof Settings; newValue: any }[] = [ if (fieldName === "anonymous_user_enabled" && checked) {
{ fieldName, newValue: checked }, setShowConfirmModal(true);
]; } else {
const updates: { fieldName: keyof Settings; newValue: any }[] = [
{ fieldName, newValue: checked },
];
updateSettingField(updates);
}
}
function handleConfirmAnonymousUsers() {
const updates: { fieldName: keyof Settings; newValue: any }[] = [
{ fieldName: "anonymous_user_enabled", newValue: true },
];
updateSettingField(updates); updateSettingField(updates);
setShowConfirmModal(false);
} }
function handleSetChatRetention() { function handleSetChatRetention() {
@ -205,10 +218,41 @@ export function SettingsForm() {
handleToggleSettingsField("auto_scroll", e.target.checked) handleToggleSettingsField("auto_scroll", e.target.checked)
} }
/> />
<Checkbox
label="Anonymous Users"
sublabel="If set, users will not be required to sign in to use Danswer."
checked={settings.anonymous_user_enabled}
onChange={(e) =>
handleToggleSettingsField("anonymous_user_enabled", e.target.checked)
}
/>
{showConfirmModal && (
<Modal
width="max-w-3xl w-full"
onOutsideClick={() => setShowConfirmModal(false)}
>
<div className="flex flex-col gap-4">
<h2 className="text-xl font-bold">Enable Anonymous Users</h2>
<p>
Are you sure you want to enable anonymous users? This will allow
anyone to use Danswer without signing in.
</p>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setShowConfirmModal(false)}
>
Cancel
</Button>
<Button onClick={handleConfirmAnonymousUsers}>Confirm</Button>
</div>
</div>
</Modal>
)}
{isEnterpriseEnabled && ( {isEnterpriseEnabled && (
<> <>
<Title className="mb-4">Chat Settings</Title> <Title className="mt-8 mb-4">Chat Settings</Title>
<IntegerInput <IntegerInput
label="Chat Retention" label="Chat Retention"
sublabel="Enter the maximum number of days you would like Onyx to retain chat messages. Leaving this field empty will cause Onyx to never delete chat messages." sublabel="Enter the maximum number of days you would like Onyx to retain chat messages. Leaving this field empty will cause Onyx to never delete chat messages."

View File

@ -5,6 +5,7 @@ export enum GatingType {
} }
export interface Settings { export interface Settings {
anonymous_user_enabled: boolean;
maximum_chat_retention_days: number | null; maximum_chat_retention_days: number | null;
notifications: Notification[]; notifications: Notification[];
needs_reindexing: boolean; needs_reindexing: boolean;

View File

@ -26,72 +26,68 @@ const ForgotPasswordPage: React.FC = () => {
return ( return (
<AuthFlowContainer> <AuthFlowContainer>
<div className="flex flex-col w-full justify-center"> <div className="flex flex-col w-full justify-center">
<CardSection className="mt-4 w-full"> <div className="flex">
{" "} <Title className="mb-2 mx-auto font-bold">Forgot Password</Title>
<div className="flex"> </div>
<Title className="mb-2 mx-auto font-bold">Forgot Password</Title> {isWorking && <Spinner />}
</div> {popup}
{isWorking && <Spinner />} <Formik
{popup} initialValues={{
<Formik email: "",
initialValues={{ }}
email: "", validationSchema={Yup.object().shape({
}} email: Yup.string().email().required(),
validationSchema={Yup.object().shape({ })}
email: Yup.string().email().required(), onSubmit={async (values) => {
})} setIsWorking(true);
onSubmit={async (values) => { try {
setIsWorking(true); await forgotPassword(values.email);
try { setPopup({
await forgotPassword(values.email); type: "success",
setPopup({ message: "Password reset email sent. Please check your inbox.",
type: "success", });
message: } catch (error) {
"Password reset email sent. Please check your inbox.", const errorMessage =
}); error instanceof Error
} catch (error) { ? error.message
const errorMessage = : "An error occurred. Please try again.";
error instanceof Error setPopup({
? error.message type: "error",
: "An error occurred. Please try again."; message: errorMessage,
setPopup({ });
type: "error", } finally {
message: errorMessage, setIsWorking(false);
}); }
} finally { }}
setIsWorking(false); >
} {({ isSubmitting }) => (
}} <Form className="w-full flex flex-col items-stretch mt-2">
> <TextFormField
{({ isSubmitting }) => ( name="email"
<Form className="w-full flex flex-col items-stretch mt-2"> label="Email"
<TextFormField type="email"
name="email" placeholder="email@yourcompany.com"
label="Email" />
type="email"
placeholder="email@yourcompany.com"
/>
<div className="flex"> <div className="flex">
<Button <Button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="mx-auto w-full" className="mx-auto w-full"
> >
Reset Password Reset Password
</Button> </Button>
</div> </div>
</Form> </Form>
)} )}
</Formik> </Formik>
<div className="flex"> <div className="flex">
<Text className="mt-4 mx-auto"> <Text className="mt-4 mx-auto">
<Link href="/auth/login" className="text-link font-medium"> <Link href="/auth/login" className="text-link font-medium">
Back to Login Back to Login
</Link> </Link>
</Text> </Text>
</div> </div>
</CardSection>
</div> </div>
</AuthFlowContainer> </AuthFlowContainer>
); );

View File

@ -12,6 +12,7 @@ import { Spinner } from "@/components/Spinner";
import { set } from "lodash"; import { set } from "lodash";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants"; import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
import Link from "next/link"; import Link from "next/link";
import { useUser } from "@/components/user/UserProvider";
export function EmailPasswordForm({ export function EmailPasswordForm({
isSignup = false, isSignup = false,
@ -24,6 +25,7 @@ export function EmailPasswordForm({
referralSource?: string; referralSource?: string;
nextUrl?: string | null; nextUrl?: string | null;
}) { }) {
const { user } = useUser();
const { popup, setPopup } = usePopup(); const { popup, setPopup } = usePopup();
const [isWorking, setIsWorking] = useState(false); const [isWorking, setIsWorking] = useState(false);
return ( return (
@ -116,24 +118,29 @@ export function EmailPasswordForm({
name="password" name="password"
label="Password" label="Password"
type="password" type="password"
includeForgotPassword={
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && !isSignup
}
placeholder="**************" placeholder="**************"
/> />
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && !isSignup && (
<Link
href="/auth/forgot-password"
className="text-sm text-link font-medium whitespace-nowrap"
>
Forgot Password?
</Link>
)}
<Button <Button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="mx-auto w-full" className="mx-auto !py-4 w-full"
> >
{isSignup ? "Sign Up" : "Log In"} {isSignup ? "Sign Up" : "Log In"}
</Button> </Button>
{user?.is_anonymous_user && (
<Link
href="/chat"
className="text-xs text-blue-500 cursor-pointer text-center w-full text-link font-medium mx-auto"
>
<span className="hover:border-b hover:border-dotted hover:border-blue-500">
or continue as guest
</span>
</Link>
)}
</Form> </Form>
)} )}
</Formik> </Formik>

View File

@ -17,6 +17,8 @@ import { getSecondsUntilExpiration } from "@/lib/time";
import AuthFlowContainer from "@/components/auth/AuthFlowContainer"; import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import CardSection from "@/components/admin/CardSection"; import CardSection from "@/components/admin/CardSection";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants"; import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { useContext } from "react";
const Page = async (props: { const Page = async (props: {
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>; searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
@ -43,7 +45,7 @@ const Page = async (props: {
// simply take the user to the home page if Auth is disabled // simply take the user to the home page if Auth is disabled
if (authTypeMetadata?.authType === "disabled") { if (authTypeMetadata?.authType === "disabled") {
return redirect("/"); return redirect("/chat");
} }
// if user is already logged in, take them to the main app page // if user is already logged in, take them to the main app page
@ -51,12 +53,13 @@ const Page = async (props: {
if ( if (
currentUser && currentUser &&
currentUser.is_active && currentUser.is_active &&
!currentUser.is_anonymous_user &&
(secondsTillExpiration === null || secondsTillExpiration > 0) (secondsTillExpiration === null || secondsTillExpiration > 0)
) { ) {
if (authTypeMetadata?.requiresVerification && !currentUser.is_verified) { if (authTypeMetadata?.requiresVerification && !currentUser.is_verified) {
return redirect("/auth/waiting-on-verification"); return redirect("/auth/waiting-on-verification");
} }
return redirect("/"); return redirect("/chat");
} }
// get where to send the user to authenticate // get where to send the user to authenticate
@ -74,67 +77,36 @@ const Page = async (props: {
} }
return ( return (
<AuthFlowContainer> <div className="flex flex-col ">
<div className="absolute top-10x w-full"> <AuthFlowContainer authState="login">
<HealthCheckBanner /> <div className="absolute top-10x w-full">
</div> <HealthCheckBanner />
</div>
<div className="flex flex-col w-full justify-center"> <div className="flex flex-col w-full justify-center">
{authUrl && authTypeMetadata && ( {authUrl && authTypeMetadata && (
<> <>
<h2 className="text-center text-xl text-strong font-bold"> <h2 className="text-center text-xl text-strong font-bold">
<LoginText />
</h2>
<SignInButton
authorizeUrl={authUrl}
authType={authTypeMetadata?.authType}
/>
</>
)}
{authTypeMetadata?.authType === "cloud" && (
<div className="mt-4 w-full justify-center">
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-gray-300"></div>
<span className="px-4 text-gray-500">or</span>
<div className="flex-grow border-t border-gray-300"></div>
</div>
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
<div className="flex mt-4 justify-between">
<Link
href={`/auth/signup${
searchParams?.next ? `?next=${searchParams.next}` : ""
}`}
className="text-link font-medium"
>
Create an account
</Link>
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
<Link
href="/auth/forgot-password"
className="text-link font-medium"
>
Reset Password
</Link>
)}
</div>
</div>
)}
{authTypeMetadata?.authType === "basic" && (
<CardSection className="mt-4 w-96">
<div className="flex">
<Title className="mb-2 mx-auto font-bold">
<LoginText /> <LoginText />
</Title> </h2>
</div>
<EmailPasswordForm nextUrl={nextUrl} /> <SignInButton
<div className="flex flex-col gap-y-2 items-center"> authorizeUrl={authUrl}
<Text className="mt-4 "> authType={authTypeMetadata?.authType}
Don&apos;t have an account?{" "} />
</>
)}
{authTypeMetadata?.authType === "cloud" && (
<div className="mt-4 w-full justify-center">
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-gray-300"></div>
<span className="px-4 text-gray-500">or</span>
<div className="flex-grow border-t border-gray-300"></div>
</div>
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
<div className="flex mt-4 justify-between">
<Link <Link
href={`/auth/signup${ href={`/auth/signup${
searchParams?.next ? `?next=${searchParams.next}` : "" searchParams?.next ? `?next=${searchParams.next}` : ""
@ -143,12 +115,33 @@ const Page = async (props: {
> >
Create an account Create an account
</Link> </Link>
</Text>
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
<Link
href="/auth/forgot-password"
className="text-link font-medium"
>
Reset Password
</Link>
)}
</div>
</div> </div>
</CardSection> )}
)}
</div> {authTypeMetadata?.authType === "basic" && (
</AuthFlowContainer> <>
<div className="flex">
<Title className="mb-2 mx-auto text-xl text-strong font-bold">
<LoginText />
</Title>
</div>
<EmailPasswordForm nextUrl={nextUrl} />
<div className="flex flex-col gap-y-2 items-center"></div>
</>
)}
</div>
</AuthFlowContainer>
</div>
); );
}; };

View File

@ -28,87 +28,84 @@ const ResetPasswordPage: React.FC = () => {
return ( return (
<AuthFlowContainer> <AuthFlowContainer>
<div className="flex flex-col w-full justify-center"> <div className="flex flex-col w-full justify-center">
<CardSection className="mt-4 w-full"> <div className="flex">
<div className="flex"> <Title className="mb-2 mx-auto font-bold">Reset Password</Title>
<Title className="mb-2 mx-auto font-bold">Reset Password</Title> </div>
</div> {isWorking && <Spinner />}
{isWorking && <Spinner />} {popup}
{popup} <Formik
<Formik initialValues={{
initialValues={{ password: "",
password: "", confirmPassword: "",
confirmPassword: "", }}
}} validationSchema={Yup.object().shape({
validationSchema={Yup.object().shape({ password: Yup.string().required("Password is required"),
password: Yup.string().required("Password is required"), confirmPassword: Yup.string()
confirmPassword: Yup.string() .oneOf([Yup.ref("password"), undefined], "Passwords must match")
.oneOf([Yup.ref("password"), undefined], "Passwords must match") .required("Confirm Password is required"),
.required("Confirm Password is required"), })}
})} onSubmit={async (values) => {
onSubmit={async (values) => { if (!token) {
if (!token) { setPopup({
setPopup({ type: "error",
type: "error", message: "Invalid or missing reset token.",
message: "Invalid or missing reset token.", });
}); return;
return; }
} setIsWorking(true);
setIsWorking(true); try {
try { await resetPassword(token, values.password);
await resetPassword(token, values.password); setPopup({
setPopup({ type: "success",
type: "success", message: "Password reset successfully. Redirecting to login...",
message: });
"Password reset successfully. Redirecting to login...", setTimeout(() => {
}); redirect("/auth/login");
setTimeout(() => { }, 1000);
redirect("/auth/login"); } catch (error) {
}, 1000); setPopup({
} catch (error) { type: "error",
setPopup({ message: "An error occurred. Please try again.",
type: "error", });
message: "An error occurred. Please try again.", } finally {
}); setIsWorking(false);
} finally { }
setIsWorking(false); }}
} >
}} {({ isSubmitting }) => (
> <Form className="w-full flex flex-col items-stretch mt-2">
{({ isSubmitting }) => ( <TextFormField
<Form className="w-full flex flex-col items-stretch mt-2"> name="password"
<TextFormField label="New Password"
name="password" type="password"
label="New Password" placeholder="Enter your new password"
type="password" />
placeholder="Enter your new password" <TextFormField
/> name="confirmPassword"
<TextFormField label="Confirm New Password"
name="confirmPassword" type="password"
label="Confirm New Password" placeholder="Confirm your new password"
type="password" />
placeholder="Confirm your new password"
/>
<div className="flex"> <div className="flex">
<Button <Button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="mx-auto w-full" className="mx-auto w-full"
> >
Reset Password Reset Password
</Button> </Button>
</div> </div>
</Form> </Form>
)} )}
</Formik> </Formik>
<div className="flex"> <div className="flex">
<Text className="mt-4 mx-auto"> <Text className="mt-4 mx-auto">
<Link href="/auth/login" className="text-link font-medium"> <Link href="/auth/login" className="text-link font-medium">
Back to Login Back to Login
</Link> </Link>
</Text> </Text>
</div> </div>
</CardSection>
</div> </div>
</AuthFlowContainer> </AuthFlowContainer>
); );

View File

@ -13,7 +13,6 @@ import Link from "next/link";
import { SignInButton } from "../login/SignInButton"; import { SignInButton } from "../login/SignInButton";
import AuthFlowContainer from "@/components/auth/AuthFlowContainer"; import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import ReferralSourceSelector from "./ReferralSourceSelector"; import ReferralSourceSelector from "./ReferralSourceSelector";
import { Separator } from "@/components/ui/separator";
const Page = async (props: { const Page = async (props: {
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>; searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
@ -39,13 +38,13 @@ const Page = async (props: {
// simply take the user to the home page if Auth is disabled // simply take the user to the home page if Auth is disabled
if (authTypeMetadata?.authType === "disabled") { if (authTypeMetadata?.authType === "disabled") {
return redirect("/"); return redirect("/chat");
} }
// if user is already logged in, take them to the main app page // if user is already logged in, take them to the main app page
if (currentUser && currentUser.is_active) { if (currentUser && currentUser.is_active && !currentUser.is_anonymous_user) {
if (!authTypeMetadata?.requiresVerification || currentUser.is_verified) { if (!authTypeMetadata?.requiresVerification || currentUser.is_verified) {
return redirect("/"); return redirect("/chat");
} }
return redirect("/auth/waiting-on-verification"); return redirect("/auth/waiting-on-verification");
} }
@ -53,7 +52,7 @@ const Page = async (props: {
// only enable this page if basic login is enabled // only enable this page if basic login is enabled
if (authTypeMetadata?.authType !== "basic" && !cloud) { if (authTypeMetadata?.authType !== "basic" && !cloud) {
return redirect("/"); return redirect("/chat");
} }
let authUrl: string | null = null; let authUrl: string | null = null;
@ -62,7 +61,7 @@ const Page = async (props: {
} }
return ( return (
<AuthFlowContainer> <AuthFlowContainer authState="signup">
<HealthCheckBanner /> <HealthCheckBanner />
<> <>
@ -95,21 +94,6 @@ const Page = async (props: {
shouldVerify={authTypeMetadata?.requiresVerification} shouldVerify={authTypeMetadata?.requiresVerification}
nextUrl={nextUrl} nextUrl={nextUrl}
/> />
<div className="flex">
<Text className="mt-4 mx-auto">
Already have an account?{" "}
<Link
href={{
pathname: "/auth/login",
query: { ...searchParams },
}}
className="text-link font-medium"
>
Log In
</Link>
</Text>
</div>
</div> </div>
</> </>
</AuthFlowContainer> </AuthFlowContainer>

View File

@ -23,7 +23,7 @@ export default async function Page() {
} }
if (!authTypeMetadata?.requiresVerification || currentUser?.is_verified) { if (!authTypeMetadata?.requiresVerification || currentUser?.is_verified) {
return redirect("/"); return redirect("/chat");
} }
return <Verify user={currentUser} />; return <Verify user={currentUser} />;

View File

@ -27,13 +27,13 @@ export default async function Page() {
if (!currentUser) { if (!currentUser) {
if (authTypeMetadata?.authType === "disabled") { if (authTypeMetadata?.authType === "disabled") {
return redirect("/"); return redirect("/chat");
} }
return redirect("/auth/login"); return redirect("/auth/login");
} }
if (!authTypeMetadata?.requiresVerification || currentUser.is_verified) { if (!authTypeMetadata?.requiresVerification || currentUser.is_verified) {
return redirect("/"); return redirect("/chat");
} }
return ( return (

View File

@ -1661,6 +1661,7 @@ export function ChatPage({
setShowDocSidebar: setShowHistorySidebar, setShowDocSidebar: setShowHistorySidebar,
setToggled: removeToggle, setToggled: removeToggle,
mobile: settings?.isMobile, mobile: settings?.isMobile,
isAnonymousUser: user?.is_anonymous_user,
}); });
const autoScrollEnabled = const autoScrollEnabled =
@ -2229,6 +2230,7 @@ export function ChatPage({
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
currentChatSession={selectedChatSession} currentChatSession={selectedChatSession}
documentSidebarToggled={documentSidebarToggled} documentSidebarToggled={documentSidebarToggled}
hideUserDropdown={user?.is_anonymous_user}
/> />
)} )}
@ -2767,12 +2769,6 @@ export function ChatPage({
setFiltersToggled(false); setFiltersToggled(false);
setDocumentSidebarToggled(true); setDocumentSidebarToggled(true);
}} }}
removeFilters={() => {
filterManager.setSelectedSources([]);
filterManager.setSelectedTags([]);
filterManager.setSelectedDocumentSets([]);
setDocumentSidebarToggled(false);
}}
showConfigureAPIKey={() => showConfigureAPIKey={() =>
setShowApiKeyModal(true) setShowApiKeyModal(true)
} }
@ -2782,7 +2778,6 @@ export function ChatPage({
selectedDocuments={selectedDocuments} selectedDocuments={selectedDocuments}
// assistant stuff // assistant stuff
selectedAssistant={liveAssistant} selectedAssistant={liveAssistant}
setSelectedAssistant={onAssistantChange}
setAlternativeAssistant={setAlternativeAssistant} setAlternativeAssistant={setAlternativeAssistant}
alternativeAssistant={alternativeAssistant} alternativeAssistant={alternativeAssistant}
// end assistant stuff // end assistant stuff
@ -2790,7 +2785,6 @@ export function ChatPage({
setMessage={setMessage} setMessage={setMessage}
onSubmit={onSubmit} onSubmit={onSubmit}
filterManager={filterManager} filterManager={filterManager}
llmOverrideManager={llmOverrideManager}
files={currentMessageFiles} files={currentMessageFiles}
setFiles={setCurrentMessageFiles} setFiles={setCurrentMessageFiles}
toggleFilters={ toggleFilters={
@ -2798,7 +2792,6 @@ export function ChatPage({
} }
handleFileUpload={handleImageUpload} handleFileUpload={handleImageUpload}
textAreaRef={textAreaRef} textAreaRef={textAreaRef}
chatSessionId={chatSessionIdRef.current!}
/> />
{enterpriseSettings && {enterpriseSettings &&
enterpriseSettings.custom_lower_disclaimer_content && ( enterpriseSettings.custom_lower_disclaimer_content && (

View File

@ -3,8 +3,7 @@ import { FiPlusCircle, FiPlus, FiInfo, FiX, FiSearch } from "react-icons/fi";
import { ChatInputOption } from "./ChatInputOption"; import { ChatInputOption } from "./ChatInputOption";
import { Persona } from "@/app/admin/assistants/interfaces"; import { Persona } from "@/app/admin/assistants/interfaces";
import { FilterManager, LlmOverrideManager } from "@/lib/hooks"; import { FilterManager } from "@/lib/hooks";
import { SelectedFilterDisplay } from "./SelectedFilterDisplay";
import { useChatContext } from "@/components/context/ChatContext"; import { useChatContext } from "@/components/context/ChatContext";
import { getFinalLLM } from "@/lib/llm/utils"; import { getFinalLLM } from "@/lib/llm/utils";
import { ChatFileType, FileDescriptor } from "../interfaces"; import { ChatFileType, FileDescriptor } from "../interfaces";
@ -37,9 +36,9 @@ import FiltersDisplay from "./FilterDisplay";
const MAX_INPUT_HEIGHT = 200; const MAX_INPUT_HEIGHT = 200;
interface ChatInputBarProps { interface ChatInputBarProps {
removeFilters: () => void;
removeDocs: () => void; removeDocs: () => void;
openModelSettings: () => void; openModelSettings: () => void;
showDocs: () => void;
showConfigureAPIKey: () => void; showConfigureAPIKey: () => void;
selectedDocuments: OnyxDocument[]; selectedDocuments: OnyxDocument[];
message: string; message: string;
@ -47,41 +46,34 @@ interface ChatInputBarProps {
stopGenerating: () => void; stopGenerating: () => void;
onSubmit: () => void; onSubmit: () => void;
filterManager: FilterManager; filterManager: FilterManager;
llmOverrideManager: LlmOverrideManager;
chatState: ChatState; chatState: ChatState;
showDocs: () => void;
alternativeAssistant: Persona | null; alternativeAssistant: Persona | null;
// assistants // assistants
selectedAssistant: Persona; selectedAssistant: Persona;
setSelectedAssistant: (assistant: Persona) => void;
setAlternativeAssistant: (alternativeAssistant: Persona | null) => void; setAlternativeAssistant: (alternativeAssistant: Persona | null) => void;
files: FileDescriptor[]; files: FileDescriptor[];
setFiles: (files: FileDescriptor[]) => void; setFiles: (files: FileDescriptor[]) => void;
handleFileUpload: (files: File[]) => void; handleFileUpload: (files: File[]) => void;
textAreaRef: React.RefObject<HTMLTextAreaElement>; textAreaRef: React.RefObject<HTMLTextAreaElement>;
chatSessionId?: string;
toggleFilters?: () => void; toggleFilters?: () => void;
} }
export function ChatInputBar({ export function ChatInputBar({
removeFilters,
removeDocs, removeDocs,
openModelSettings, openModelSettings,
showConfigureAPIKey,
showDocs, showDocs,
showConfigureAPIKey,
selectedDocuments, selectedDocuments,
message, message,
setMessage, setMessage,
stopGenerating, stopGenerating,
onSubmit, onSubmit,
filterManager, filterManager,
llmOverrideManager,
chatState, chatState,
// assistants // assistants
selectedAssistant, selectedAssistant,
setSelectedAssistant,
setAlternativeAssistant, setAlternativeAssistant,
files, files,
@ -89,7 +81,6 @@ export function ChatInputBar({
handleFileUpload, handleFileUpload,
textAreaRef, textAreaRef,
alternativeAssistant, alternativeAssistant,
chatSessionId,
toggleFilters, toggleFilters,
}: ChatInputBarProps) { }: ChatInputBarProps) {
useEffect(() => { useEffect(() => {

View File

@ -156,6 +156,7 @@ export default async function RootLayout({
</div> </div>
); );
} }
if (productGating === GatingType.FULL) { if (productGating === GatingType.FULL) {
return getPageContent( return getPageContent(
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen">

View File

@ -1,5 +1,5 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default function NotFound() { export default function NotFound() {
redirect("/chat"); redirect("/auth/login");
} }

View File

@ -1,5 +1,5 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default async function Page() { export default async function Page() {
redirect("/chat"); redirect("/auth/login");
} }

View File

@ -59,9 +59,11 @@ const DropdownOption: React.FC<DropdownOptionProps> = ({
export function UserDropdown({ export function UserDropdown({
page, page,
toggleUserSettings, toggleUserSettings,
hideUserDropdown,
}: { }: {
page?: pageType; page?: pageType;
toggleUserSettings?: () => void; toggleUserSettings?: () => void;
hideUserDropdown?: boolean;
}) { }) {
const { user, isCurator } = useUser(); const { user, isCurator } = useUser();
const [userInfoVisible, setUserInfoVisible] = useState(false); const [userInfoVisible, setUserInfoVisible] = useState(false);
@ -114,6 +116,7 @@ export function UserDropdown({
}; };
const showAdminPanel = !user || user.role === UserRole.ADMIN; const showAdminPanel = !user || user.role === UserRole.ADMIN;
const showCuratorPanel = user && isCurator; const showCuratorPanel = user && isCurator;
const showLogout = const showLogout =
user && !checkUserIsNoAuthUser(user.id) && !LOGOUT_DISABLED; user && !checkUserIsNoAuthUser(user.id) && !LOGOUT_DISABLED;
@ -183,6 +186,12 @@ export function UserDropdown({
notifications={notifications || []} notifications={notifications || []}
refreshNotifications={refreshNotifications} refreshNotifications={refreshNotifications}
/> />
) : hideUserDropdown ? (
<DropdownOption
onClick={() => router.push("/auth/login")}
icon={<UserIcon className="h-5 w-5 my-auto mr-2" />}
label="Log In"
/>
) : ( ) : (
<> <>
{customNavItems.map((item, i) => ( {customNavItems.map((item, i) => (
@ -251,6 +260,7 @@ export function UserDropdown({
label="User Settings" label="User Settings"
/> />
)} )}
<DropdownOption <DropdownOption
onClick={() => { onClick={() => {
setUserInfoVisible(true); setUserInfoVisible(true);

View File

@ -35,7 +35,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
return redirect("/auth/login"); return redirect("/auth/login");
} }
if (user.role === UserRole.BASIC) { if (user.role === UserRole.BASIC) {
return redirect("/"); return redirect("/chat");
} }
if (!user.is_verified && requiresVerification) { if (!user.is_verified && requiresVerification) {
return redirect("/auth/waiting-on-verification"); return redirect("/auth/waiting-on-verification");

View File

@ -29,6 +29,7 @@ import { useRef, useState } from "react";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { EditIcon } from "@/components/icons/icons"; import { EditIcon } from "@/components/icons/icons";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Link from "next/link";
export function SectionHeader({ export function SectionHeader({
children, children,
@ -143,6 +144,7 @@ export function TextFormField({
small, small,
removeLabel, removeLabel,
min, min,
includeForgotPassword,
onChange, onChange,
width, width,
vertical, vertical,
@ -169,6 +171,7 @@ export function TextFormField({
explanationLink?: string; explanationLink?: string;
small?: boolean; small?: boolean;
min?: number; min?: number;
includeForgotPassword?: boolean;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
width?: string; width?: string;
vertical?: boolean; vertical?: boolean;
@ -238,7 +241,7 @@ export function TextFormField({
)} )}
</div> </div>
{subtext && <SubLabel>{subtext}</SubLabel>} {subtext && <SubLabel>{subtext}</SubLabel>}
<div className={`w-full flex ${includeRevert && "gap-x-2"}`}> <div className={`w-full flex ${includeRevert && "gap-x-2"} relative`}>
<Field <Field
onChange={handleChange} onChange={handleChange}
min={min} min={min}
@ -269,6 +272,14 @@ export function TextFormField({
placeholder={placeholder} placeholder={placeholder}
autoComplete={autoCompleteDisabled ? "off" : undefined} autoComplete={autoCompleteDisabled ? "off" : undefined}
/> />
{includeForgotPassword && (
<Link
href="/auth/forgot-password"
className="absolute right-3 top-1/2 mt-[3px] transform -translate-y-1/2 text-xs text-blue-500 cursor-pointer"
>
Forgot password?
</Link>
)}
</div> </div>
{explanationText && ( {explanationText && (

View File

@ -1,16 +1,41 @@
import Link from "next/link";
import { Logo } from "../logo/Logo"; import { Logo } from "../logo/Logo";
export default function AuthFlowContainer({ export default function AuthFlowContainer({
children, children,
authState,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
authState?: "signup" | "login";
}) { }) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen bg-background"> <div className="p-4 flex flex-col items-center justify-center min-h-screen bg-background">
<div className="w-full max-w-md bg-black pt-8 pb-4 px-8 mx-4 gap-y-4 bg-white flex items-center flex-col rounded-xl shadow-lg border border-bacgkround-100"> <div className="w-full max-w-md bg-black pt-8 pb-6 px-8 mx-4 gap-y-4 bg-white flex items-center flex-col rounded-xl shadow-lg border border-bacgkround-100">
<Logo width={70} height={70} /> <Logo width={70} height={70} />
{children} {children}
</div> </div>
{authState === "login" && (
<div className="text-sm mt-4 text-center w-full text-neutral-900 font-medium mx-auto">
Don&apos;t have an account?{" "}
<Link
href="/auth/signup"
className=" underline transition-colors duration-200"
>
Create one
</Link>
</div>
)}
{authState === "signup" && (
<div className="text-sm mt-4 text-center w-full text-neutral-900 font-medium mx-auto">
Already have an account?{" "}
<Link
href="/auth/login"
className=" underline transition-colors duration-200"
>
Log In
</Link>
</div>
)}
</div> </div>
); );
} }

View File

@ -21,6 +21,7 @@ export default function FunctionalHeader({
sidebarToggled, sidebarToggled,
documentSidebarToggled, documentSidebarToggled,
toggleUserSettings, toggleUserSettings,
hideUserDropdown,
}: { }: {
reset?: () => void; reset?: () => void;
page: pageType; page: pageType;
@ -30,6 +31,7 @@ export default function FunctionalHeader({
setSharingModalVisible?: (value: SetStateAction<boolean>) => void; setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
toggleSidebar?: () => void; toggleSidebar?: () => void;
toggleUserSettings?: () => void; toggleUserSettings?: () => void;
hideUserDropdown?: boolean;
}) { }) {
const settings = useContext(SettingsContext); const settings = useContext(SettingsContext);
useEffect(() => { useEffect(() => {
@ -106,7 +108,7 @@ export default function FunctionalHeader({
</div> </div>
<div className="absolute right-0 mobile:top-2 desktop:top-0 flex"> <div className="absolute right-0 mobile:top-2 desktop:top-0 flex">
{setSharingModalVisible && ( {setSharingModalVisible && !hideUserDropdown && (
<div <div
onClick={() => setSharingModalVisible(true)} onClick={() => setSharingModalVisible(true)}
className="mobile:hidden mr-2 my-auto rounded cursor-pointer hover:bg-hover-light" className="mobile:hidden mr-2 my-auto rounded cursor-pointer hover:bg-hover-light"
@ -114,8 +116,10 @@ export default function FunctionalHeader({
<FiShare2 size="18" /> <FiShare2 size="18" />
</div> </div>
)} )}
<div className="mobile:hidden flex my-auto"> <div className="mobile:hidden flex my-auto">
<UserDropdown <UserDropdown
hideUserDropdown={hideUserDropdown}
page={page} page={page}
toggleUserSettings={toggleUserSettings} toggleUserSettings={toggleUserSettings}
/> />

View File

@ -7,6 +7,7 @@ interface UseSidebarVisibilityProps {
setShowDocSidebar: Dispatch<SetStateAction<boolean>>; setShowDocSidebar: Dispatch<SetStateAction<boolean>>;
mobile?: boolean; mobile?: boolean;
setToggled?: () => void; setToggled?: () => void;
isAnonymousUser?: boolean;
} }
export const useSidebarVisibility = ({ export const useSidebarVisibility = ({
@ -16,11 +17,15 @@ export const useSidebarVisibility = ({
setToggled, setToggled,
showDocSidebar, showDocSidebar,
mobile, mobile,
isAnonymousUser,
}: UseSidebarVisibilityProps) => { }: UseSidebarVisibilityProps) => {
const xPosition = useRef(0); const xPosition = useRef(0);
useEffect(() => { useEffect(() => {
const handleEvent = (event: MouseEvent) => { const handleEvent = (event: MouseEvent) => {
if (isAnonymousUser) {
return;
}
const currentXPosition = event.clientX; const currentXPosition = event.clientX;
xPosition.current = currentXPosition; xPosition.current = currentXPosition;

View File

@ -2652,7 +2652,7 @@ export const OpenIcon = ({
<path <path
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M7 13.5a9.26 9.26 0 0 0-5.61-2.95a1 1 0 0 1-.89-1V1.5A1 1 0 0 1 1.64.51A9.3 9.3 0 0 1 7 3.43zm0 0a9.26 9.26 0 0 1 5.61-2.95a1 1 0 0 0 .89-1V1.5a1 1 0 0 0-1.14-.99A9.3 9.3 0 0 0 7 3.43z" d="M7 13.5a9.26 9.26 0 0 0-5.61-2.95a1 1 0 0 1-.89-1V1.5A1 1 0 0 1 1.64.51A9.3 9.3 0 0 1 7 3.43zm0 0a9.26 9.26 0 0 1 5.61-2.95a1 1 0 0 0 .89-1V1.5a1 1 0 0 0-1.14-.99A9.3 9.3 0 0 0 7 3.43z"
/> />
@ -2676,7 +2676,7 @@ export const DexpandTwoIcon = ({
<path <path
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="m.5 13.5l5-5m-4 0h4v4m8-12l-5 5m4 0h-4v-4" d="m.5 13.5l5-5m-4 0h4v4m8-12l-5 5m4 0h-4v-4"
/> />
@ -2700,7 +2700,7 @@ export const ExpandTwoIcon = ({
<path <path
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="m8.5 5.5l5-5m-4 0h4v4m-8 4l-5 5m4 0h-4v-4" d="m8.5 5.5l5-5m-4 0h4v4m-8 4l-5 5m4 0h-4v-4"
/> />
@ -2724,7 +2724,7 @@ export const DownloadCSVIcon = ({
<path <path
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M.5 10.5v1a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1M4 6l3 3.5L10 6M7 9.5v-9" d="M.5 10.5v1a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1M4 6l3 3.5L10 6M7 9.5v-9"
/> />
@ -2748,7 +2748,7 @@ export const UserIcon = ({
<path <path
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="1.5" stroke-width="1.5"
d="M19.618 21.25c0-3.602-4.016-6.53-7.618-6.53c-3.602 0-7.618 2.928-7.618 6.53M12 11.456a4.353 4.353 0 1 0 0-8.706a4.353 4.353 0 0 0 0 8.706" d="M19.618 21.25c0-3.602-4.016-6.53-7.618-6.53c-3.602 0-7.618 2.928-7.618 6.53M12 11.456a4.353 4.353 0 1 0 0-8.706a4.353 4.353 0 0 0 0 8.706"

View File

@ -49,10 +49,13 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
maximum_chat_retention_days: null, maximum_chat_retention_days: null,
notifications: [], notifications: [],
needs_reindexing: false, needs_reindexing: false,
anonymous_user_enabled: false,
}; };
} else { } else {
throw new Error( throw new Error(
`fetchStandardSettingsSS failed: status=${results[0].status} body=${await results[0].text()}` `fetchStandardSettingsSS failed: status=${
results[0].status
} body=${await results[0].text()}`
); );
} }
} else { } else {
@ -64,7 +67,9 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
if (!results[1].ok) { if (!results[1].ok) {
if (results[1].status !== 403 && results[1].status !== 401) { if (results[1].status !== 403 && results[1].status !== 401) {
throw new Error( throw new Error(
`fetchEnterpriseSettingsSS failed: status=${results[1].status} body=${await results[1].text()}` `fetchEnterpriseSettingsSS failed: status=${
results[1].status
} body=${await results[1].text()}`
); );
} }
} else { } else {
@ -77,7 +82,9 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
if (!results[2].ok) { if (!results[2].ok) {
if (results[2].status !== 403) { if (results[2].status !== 403) {
throw new Error( throw new Error(
`fetchCustomAnalyticsScriptSS failed: status=${results[2].status} body=${await results[2].text()}` `fetchCustomAnalyticsScriptSS failed: status=${
results[2].status
} body=${await results[2].text()}`
); );
} }
} else { } else {

View File

@ -86,7 +86,9 @@ export async function fetchChatData(searchParams: {
const foldersResponse = results[7] as Response | null; const foldersResponse = results[7] as Response | null;
const authDisabled = authTypeMetadata?.authType === "disabled"; const authDisabled = authTypeMetadata?.authType === "disabled";
if (!authDisabled && !user) {
// TODO Validate need
if (!authDisabled && !user && !authTypeMetadata?.anonymousUserEnabled) {
const headersList = await headers(); const headersList = await headers();
const fullUrl = headersList.get("x-url") || "/chat"; const fullUrl = headersList.get("x-url") || "/chat";
const searchParamsString = new URLSearchParams( const searchParamsString = new URLSearchParams(
@ -95,6 +97,7 @@ export async function fetchChatData(searchParams: {
const redirectUrl = searchParamsString const redirectUrl = searchParamsString
? `${fullUrl}?${searchParamsString}` ? `${fullUrl}?${searchParamsString}`
: fullUrl; : fullUrl;
return redirect(`/auth/login?next=${encodeURIComponent(redirectUrl)}`); return redirect(`/auth/login?next=${encodeURIComponent(redirectUrl)}`);
} }

View File

@ -62,6 +62,7 @@ export interface User {
oidc_expiry?: Date; oidc_expiry?: Date;
is_cloud_superuser?: boolean; is_cloud_superuser?: boolean;
organization_name: string | null; organization_name: string | null;
is_anonymous_user?: boolean;
} }
export interface MinimalUserSnapshot { export interface MinimalUserSnapshot {

View File

@ -8,6 +8,7 @@ export interface AuthTypeMetadata {
authType: AuthType; authType: AuthType;
autoRedirect: boolean; autoRedirect: boolean;
requiresVerification: boolean; requiresVerification: boolean;
anonymousUserEnabled: boolean | null;
} }
export const getAuthTypeMetadataSS = async (): Promise<AuthTypeMetadata> => { export const getAuthTypeMetadataSS = async (): Promise<AuthTypeMetadata> => {
@ -16,8 +17,11 @@ export const getAuthTypeMetadataSS = async (): Promise<AuthTypeMetadata> => {
throw new Error("Failed to fetch data"); throw new Error("Failed to fetch data");
} }
const data: { auth_type: string; requires_verification: boolean } = const data: {
await res.json(); auth_type: string;
requires_verification: boolean;
anonymous_user_enabled: boolean | null;
} = await res.json();
let authType: AuthType; let authType: AuthType;
@ -35,12 +39,14 @@ export const getAuthTypeMetadataSS = async (): Promise<AuthTypeMetadata> => {
authType, authType,
autoRedirect: true, autoRedirect: true,
requiresVerification: data.requires_verification, requiresVerification: data.requires_verification,
anonymousUserEnabled: data.anonymous_user_enabled,
}; };
} }
return { return {
authType, authType,
autoRedirect: false, autoRedirect: false,
requiresVerification: data.requires_verification, requiresVerification: data.requires_verification,
anonymousUserEnabled: data.anonymous_user_enabled,
}; };
}; };