mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-26 17:51:54 +01:00
Add anonymous user to main
Anonymous user
This commit is contained in:
commit
1291b3d930
@ -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(
|
||||
id=NO_AUTH_USER_ID,
|
||||
email=NO_AUTH_USER_EMAIL,
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
is_verified=True,
|
||||
role=UserRole.ADMIN,
|
||||
role=UserRole.BASIC if anonymous_user_enabled else UserRole.ADMIN,
|
||||
preferences=load_no_auth_user_preferences(store),
|
||||
is_anonymous_user=anonymous_user_enabled,
|
||||
)
|
||||
|
@ -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_PREFIX
|
||||
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 UNNAMED_KEY_PLACEHOLDER
|
||||
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 User
|
||||
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.telemetry import create_milestone_and_report
|
||||
from onyx.utils.telemetry import optional_telemetry
|
||||
@ -98,6 +99,11 @@ from shared_configs.contextvars import CURRENT_TENANT_ID_CONTEXTVAR
|
||||
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:
|
||||
if AUTH_TYPE == AuthType.DISABLED:
|
||||
return True
|
||||
@ -138,6 +144,20 @@ def user_needs_to_be_verified() -> bool:
|
||||
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:
|
||||
whitelist = get_invited_users()
|
||||
if not whitelist:
|
||||
@ -690,30 +710,36 @@ async def double_check_user(
|
||||
user: User | None,
|
||||
optional: bool = DISABLE_AUTH,
|
||||
include_expired: bool = False,
|
||||
allow_anonymous_access: bool = False,
|
||||
) -> User | None:
|
||||
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
|
||||
|
||||
if user is None:
|
||||
raise BasicAuthenticationError(
|
||||
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
|
||||
raise BasicAuthenticationError(
|
||||
detail="Access denied. User is not authenticated.",
|
||||
)
|
||||
|
||||
|
||||
async def current_user_with_expired_token(
|
||||
@ -728,6 +754,14 @@ async def current_limited_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(
|
||||
user: User | None = Depends(optional_user),
|
||||
) -> User | None:
|
||||
|
@ -279,6 +279,7 @@ class OnyxRedisLocks:
|
||||
|
||||
SLACK_BOT_LOCK = "da_lock:slack_bot"
|
||||
SLACK_BOT_HEARTBEAT_PREFIX = "da_heartbeat:slack_bot"
|
||||
ANONYMOUS_USER_ENABLED = "anonymous_user_enabled"
|
||||
|
||||
|
||||
class OnyxRedisSignals:
|
||||
|
@ -5,6 +5,7 @@ from fastapi.dependencies.models import Dependant
|
||||
from starlette.routing import BaseRoute
|
||||
|
||||
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_limited_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 == api_key_dep
|
||||
or depends_fn == current_user_with_expired_token
|
||||
or depends_fn == current_chat_accesssible_user
|
||||
or depends_fn == control_plane_dep
|
||||
or depends_fn == current_cloud_superuser
|
||||
):
|
||||
|
@ -14,6 +14,7 @@ from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
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_user
|
||||
from onyx.background.celery.celery_utils import get_deletion_attempt_snapshot
|
||||
@ -1055,7 +1056,7 @@ class BasicCCPairInfo(BaseModel):
|
||||
|
||||
@router.get("/connector-status")
|
||||
def get_basic_connector_indexing_status(
|
||||
_: User = Depends(current_user),
|
||||
_: User = Depends(current_chat_accesssible_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> list[BasicCCPairInfo]:
|
||||
cc_pairs = get_connector_credential_pairs(db_session, eager_load_connector=True)
|
||||
|
@ -10,6 +10,7 @@ from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
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_limited_user
|
||||
from onyx.auth.users import current_user
|
||||
@ -323,7 +324,7 @@ def get_image_generation_tool(
|
||||
|
||||
@basic_router.get("")
|
||||
def list_personas(
|
||||
user: User | None = Depends(current_user),
|
||||
user: User | None = Depends(current_chat_accesssible_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
include_deleted: bool = False,
|
||||
persona_ids: list[int] = Query(None),
|
||||
|
@ -1,6 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from onyx import __version__
|
||||
from onyx.auth.users import anonymous_user_enabled
|
||||
from onyx.auth.users import user_needs_to_be_verified
|
||||
from onyx.configs.app_configs import AUTH_TYPE
|
||||
from onyx.server.manage.models import AuthTypeResponse
|
||||
@ -18,7 +19,9 @@ def healthcheck() -> StatusResponse:
|
||||
@router.get("/auth/type")
|
||||
def get_auth_type() -> 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(),
|
||||
)
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@ from fastapi import Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
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.llm import fetch_existing_llm_providers
|
||||
from onyx.db.llm import fetch_provider
|
||||
@ -189,7 +189,7 @@ def set_provider_as_default(
|
||||
|
||||
@basic_router.get("/provider")
|
||||
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),
|
||||
) -> list[LLMProviderDescriptor]:
|
||||
return [
|
||||
|
@ -37,6 +37,7 @@ class AuthTypeResponse(BaseModel):
|
||||
# specifies whether the current auth setup requires
|
||||
# users to have verified emails
|
||||
requires_verification: bool
|
||||
anonymous_user_enabled: bool | None = None
|
||||
|
||||
|
||||
class UserPreferences(BaseModel):
|
||||
@ -61,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(
|
||||
@ -70,6 +72,7 @@ class UserInfo(BaseModel):
|
||||
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),
|
||||
@ -96,6 +99,7 @@ class UserInfo(BaseModel):
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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.schemas import UserRole
|
||||
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_curator_or_admin_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
|
||||
# to enforce user verification here - the frontend always wants to get the info about
|
||||
# the current user regardless of if they are currently verified
|
||||
|
||||
if user is None:
|
||||
# if auth type is disabled, return a dummy user with preferences from
|
||||
# the key-value store
|
||||
if AUTH_TYPE == AuthType.DISABLED:
|
||||
store = get_kv_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")
|
||||
if user.oidc_expiry and user.oidc_expiry < datetime.now(timezone.utc):
|
||||
|
@ -19,6 +19,7 @@ from PIL import Image
|
||||
from pydantic import BaseModel
|
||||
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_user
|
||||
from onyx.chat.chat_utils import create_chat_chain
|
||||
@ -145,7 +146,7 @@ def update_chat_session_model(
|
||||
def get_chat_session(
|
||||
session_id: UUID,
|
||||
is_shared: bool = False,
|
||||
user: User | None = Depends(current_user),
|
||||
user: User | None = Depends(current_chat_accesssible_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> ChatSessionDetailResponse:
|
||||
user_id = user.id if user is not None else None
|
||||
@ -200,7 +201,7 @@ def get_chat_session(
|
||||
@router.post("/create-chat-session")
|
||||
def create_new_chat_session(
|
||||
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),
|
||||
) -> CreateChatSessionID:
|
||||
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(
|
||||
chat_message_req: CreateChatMessageRequest,
|
||||
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),
|
||||
is_connected_func: Callable[[], bool] = Depends(is_connected),
|
||||
tenant_id: str = Depends(get_current_tenant_id),
|
||||
|
@ -11,7 +11,7 @@ from sqlalchemy import func
|
||||
from sqlalchemy import select
|
||||
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_with_tenant
|
||||
from onyx.db.models import ChatMessage
|
||||
@ -31,7 +31,7 @@ TOKEN_BUDGET_UNIT = 1_000
|
||||
|
||||
|
||||
def check_token_rate_limits(
|
||||
user: User | None = Depends(current_user),
|
||||
user: User | None = Depends(current_chat_accesssible_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
|
||||
|
@ -44,6 +44,7 @@ class Settings(BaseModel):
|
||||
maximum_chat_retention_days: int | None = None
|
||||
gpu_enabled: bool | None = None
|
||||
product_gating: GatingType = GatingType.NONE
|
||||
anonymous_user_enabled: bool | None = None
|
||||
|
||||
|
||||
class UserSettings(Settings):
|
||||
|
@ -1,21 +1,38 @@
|
||||
from typing import cast
|
||||
|
||||
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.interface import KvKeyNotFoundError
|
||||
from onyx.redis.redis_pool import get_redis_client
|
||||
from onyx.server.settings.models import Settings
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
|
||||
def load_settings() -> Settings:
|
||||
dynamic_config_store = get_kv_store()
|
||||
try:
|
||||
settings = Settings(**cast(dict, dynamic_config_store.load(KV_SETTINGS_KEY)))
|
||||
except KvKeyNotFoundError:
|
||||
settings = Settings()
|
||||
dynamic_config_store.store(KV_SETTINGS_KEY, settings.model_dump())
|
||||
if MULTI_TENANT:
|
||||
# If multi-tenant, anonymous user is always false
|
||||
anonymous_user_enabled = False
|
||||
else:
|
||||
redis_client = get_redis_client(tenant_id=None)
|
||||
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
|
||||
|
||||
|
||||
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())
|
||||
|
73
backend/tests/integration/common_utils/managers/settings.py
Normal file
73
backend/tests/integration/common_utils/managers/settings.py
Normal 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)
|
@ -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
|
||||
|
@ -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)
|
@ -10,6 +10,7 @@ import { DefaultDropdown, Option } from "@/components/Dropdown";
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { Modal } from "@/components/Modal";
|
||||
|
||||
export function Checkbox({
|
||||
label,
|
||||
@ -102,6 +103,7 @@ function IntegerInput({
|
||||
|
||||
export function SettingsForm() {
|
||||
const router = useRouter();
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [settings, setSettings] = useState<Settings | null>(null);
|
||||
const [chatRetention, setChatRetention] = useState("");
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -171,11 +173,22 @@ export function SettingsForm() {
|
||||
fieldName: keyof Settings,
|
||||
checked: boolean
|
||||
) {
|
||||
const updates: { fieldName: keyof Settings; newValue: any }[] = [
|
||||
{ fieldName, newValue: checked },
|
||||
];
|
||||
if (fieldName === "anonymous_user_enabled" && 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);
|
||||
setShowConfirmModal(false);
|
||||
}
|
||||
|
||||
function handleSetChatRetention() {
|
||||
@ -205,10 +218,41 @@ export function SettingsForm() {
|
||||
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 && (
|
||||
<>
|
||||
<Title className="mb-4">Chat Settings</Title>
|
||||
<Title className="mt-8 mb-4">Chat Settings</Title>
|
||||
<IntegerInput
|
||||
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."
|
||||
|
@ -5,6 +5,7 @@ export enum GatingType {
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
anonymous_user_enabled: boolean;
|
||||
maximum_chat_retention_days: number | null;
|
||||
notifications: Notification[];
|
||||
needs_reindexing: boolean;
|
||||
|
@ -26,72 +26,68 @@ const ForgotPasswordPage: React.FC = () => {
|
||||
return (
|
||||
<AuthFlowContainer>
|
||||
<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>
|
||||
{isWorking && <Spinner />}
|
||||
{popup}
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: "",
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
email: Yup.string().email().required(),
|
||||
})}
|
||||
onSubmit={async (values) => {
|
||||
setIsWorking(true);
|
||||
try {
|
||||
await forgotPassword(values.email);
|
||||
setPopup({
|
||||
type: "success",
|
||||
message:
|
||||
"Password reset email sent. Please check your inbox.",
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "An error occurred. Please try again.";
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: errorMessage,
|
||||
});
|
||||
} finally {
|
||||
setIsWorking(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form className="w-full flex flex-col items-stretch mt-2">
|
||||
<TextFormField
|
||||
name="email"
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="email@yourcompany.com"
|
||||
/>
|
||||
<div className="flex">
|
||||
<Title className="mb-2 mx-auto font-bold">Forgot Password</Title>
|
||||
</div>
|
||||
{isWorking && <Spinner />}
|
||||
{popup}
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: "",
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
email: Yup.string().email().required(),
|
||||
})}
|
||||
onSubmit={async (values) => {
|
||||
setIsWorking(true);
|
||||
try {
|
||||
await forgotPassword(values.email);
|
||||
setPopup({
|
||||
type: "success",
|
||||
message: "Password reset email sent. Please check your inbox.",
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "An error occurred. Please try again.";
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: errorMessage,
|
||||
});
|
||||
} finally {
|
||||
setIsWorking(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form className="w-full flex flex-col items-stretch mt-2">
|
||||
<TextFormField
|
||||
name="email"
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="email@yourcompany.com"
|
||||
/>
|
||||
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="mx-auto w-full"
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<div className="flex">
|
||||
<Text className="mt-4 mx-auto">
|
||||
<Link href="/auth/login" className="text-link font-medium">
|
||||
Back to Login
|
||||
</Link>
|
||||
</Text>
|
||||
</div>
|
||||
</CardSection>
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="mx-auto w-full"
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<div className="flex">
|
||||
<Text className="mt-4 mx-auto">
|
||||
<Link href="/auth/login" className="text-link font-medium">
|
||||
Back to Login
|
||||
</Link>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</AuthFlowContainer>
|
||||
);
|
||||
|
@ -12,6 +12,7 @@ import { Spinner } from "@/components/Spinner";
|
||||
import { set } from "lodash";
|
||||
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||
import Link from "next/link";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
|
||||
export function EmailPasswordForm({
|
||||
isSignup = false,
|
||||
@ -24,6 +25,7 @@ export function EmailPasswordForm({
|
||||
referralSource?: string;
|
||||
nextUrl?: string | null;
|
||||
}) {
|
||||
const { user } = useUser();
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [isWorking, setIsWorking] = useState(false);
|
||||
return (
|
||||
@ -116,24 +118,29 @@ export function EmailPasswordForm({
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
includeForgotPassword={
|
||||
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && !isSignup
|
||||
}
|
||||
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
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="mx-auto w-full"
|
||||
className="mx-auto !py-4 w-full"
|
||||
>
|
||||
{isSignup ? "Sign Up" : "Log In"}
|
||||
</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>
|
||||
)}
|
||||
</Formik>
|
||||
|
@ -17,6 +17,8 @@ import { getSecondsUntilExpiration } from "@/lib/time";
|
||||
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { useContext } from "react";
|
||||
|
||||
const Page = async (props: {
|
||||
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
|
||||
if (authTypeMetadata?.authType === "disabled") {
|
||||
return redirect("/");
|
||||
return redirect("/chat");
|
||||
}
|
||||
|
||||
// if user is already logged in, take them to the main app page
|
||||
@ -51,12 +53,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
|
||||
@ -74,67 +77,36 @@ const Page = async (props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthFlowContainer>
|
||||
<div className="absolute top-10x w-full">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="flex flex-col ">
|
||||
<AuthFlowContainer authState="login">
|
||||
<div className="absolute top-10x w-full">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full justify-center">
|
||||
{authUrl && authTypeMetadata && (
|
||||
<>
|
||||
<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">
|
||||
<div className="flex flex-col w-full justify-center">
|
||||
{authUrl && authTypeMetadata && (
|
||||
<>
|
||||
<h2 className="text-center text-xl text-strong font-bold">
|
||||
<LoginText />
|
||||
</Title>
|
||||
</div>
|
||||
<EmailPasswordForm nextUrl={nextUrl} />
|
||||
<div className="flex flex-col gap-y-2 items-center">
|
||||
<Text className="mt-4 ">
|
||||
Don't have an account?{" "}
|
||||
</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}` : ""
|
||||
@ -143,12 +115,33 @@ const Page = async (props: {
|
||||
>
|
||||
Create an account
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
className="text-link font-medium"
|
||||
>
|
||||
Reset Password
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardSection>
|
||||
)}
|
||||
</div>
|
||||
</AuthFlowContainer>
|
||||
)}
|
||||
|
||||
{authTypeMetadata?.authType === "basic" && (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -28,87 +28,84 @@ const ResetPasswordPage: React.FC = () => {
|
||||
return (
|
||||
<AuthFlowContainer>
|
||||
<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">Reset Password</Title>
|
||||
</div>
|
||||
{isWorking && <Spinner />}
|
||||
{popup}
|
||||
<Formik
|
||||
initialValues={{
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
password: Yup.string().required("Password is required"),
|
||||
confirmPassword: Yup.string()
|
||||
.oneOf([Yup.ref("password"), undefined], "Passwords must match")
|
||||
.required("Confirm Password is required"),
|
||||
})}
|
||||
onSubmit={async (values) => {
|
||||
if (!token) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: "Invalid or missing reset token.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsWorking(true);
|
||||
try {
|
||||
await resetPassword(token, values.password);
|
||||
setPopup({
|
||||
type: "success",
|
||||
message:
|
||||
"Password reset successfully. Redirecting to login...",
|
||||
});
|
||||
setTimeout(() => {
|
||||
redirect("/auth/login");
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: "An error occurred. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsWorking(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form className="w-full flex flex-col items-stretch mt-2">
|
||||
<TextFormField
|
||||
name="password"
|
||||
label="New Password"
|
||||
type="password"
|
||||
placeholder="Enter your new password"
|
||||
/>
|
||||
<TextFormField
|
||||
name="confirmPassword"
|
||||
label="Confirm New Password"
|
||||
type="password"
|
||||
placeholder="Confirm your new password"
|
||||
/>
|
||||
<div className="flex">
|
||||
<Title className="mb-2 mx-auto font-bold">Reset Password</Title>
|
||||
</div>
|
||||
{isWorking && <Spinner />}
|
||||
{popup}
|
||||
<Formik
|
||||
initialValues={{
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
password: Yup.string().required("Password is required"),
|
||||
confirmPassword: Yup.string()
|
||||
.oneOf([Yup.ref("password"), undefined], "Passwords must match")
|
||||
.required("Confirm Password is required"),
|
||||
})}
|
||||
onSubmit={async (values) => {
|
||||
if (!token) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: "Invalid or missing reset token.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsWorking(true);
|
||||
try {
|
||||
await resetPassword(token, values.password);
|
||||
setPopup({
|
||||
type: "success",
|
||||
message: "Password reset successfully. Redirecting to login...",
|
||||
});
|
||||
setTimeout(() => {
|
||||
redirect("/auth/login");
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: "An error occurred. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsWorking(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form className="w-full flex flex-col items-stretch mt-2">
|
||||
<TextFormField
|
||||
name="password"
|
||||
label="New Password"
|
||||
type="password"
|
||||
placeholder="Enter your new password"
|
||||
/>
|
||||
<TextFormField
|
||||
name="confirmPassword"
|
||||
label="Confirm New Password"
|
||||
type="password"
|
||||
placeholder="Confirm your new password"
|
||||
/>
|
||||
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="mx-auto w-full"
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<div className="flex">
|
||||
<Text className="mt-4 mx-auto">
|
||||
<Link href="/auth/login" className="text-link font-medium">
|
||||
Back to Login
|
||||
</Link>
|
||||
</Text>
|
||||
</div>
|
||||
</CardSection>
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="mx-auto w-full"
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<div className="flex">
|
||||
<Text className="mt-4 mx-auto">
|
||||
<Link href="/auth/login" className="text-link font-medium">
|
||||
Back to Login
|
||||
</Link>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</AuthFlowContainer>
|
||||
);
|
||||
|
@ -13,7 +13,6 @@ import Link from "next/link";
|
||||
import { SignInButton } from "../login/SignInButton";
|
||||
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
||||
import ReferralSourceSelector from "./ReferralSourceSelector";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
const Page = async (props: {
|
||||
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
|
||||
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 (currentUser && currentUser.is_active && !currentUser.is_anonymous_user) {
|
||||
if (!authTypeMetadata?.requiresVerification || currentUser.is_verified) {
|
||||
return redirect("/");
|
||||
return redirect("/chat");
|
||||
}
|
||||
return redirect("/auth/waiting-on-verification");
|
||||
}
|
||||
@ -53,7 +52,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;
|
||||
@ -62,7 +61,7 @@ const Page = async (props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthFlowContainer>
|
||||
<AuthFlowContainer authState="signup">
|
||||
<HealthCheckBanner />
|
||||
|
||||
<>
|
||||
@ -95,21 +94,6 @@ const Page = async (props: {
|
||||
shouldVerify={authTypeMetadata?.requiresVerification}
|
||||
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>
|
||||
</>
|
||||
</AuthFlowContainer>
|
||||
|
@ -23,7 +23,7 @@ export default async function Page() {
|
||||
}
|
||||
|
||||
if (!authTypeMetadata?.requiresVerification || currentUser?.is_verified) {
|
||||
return redirect("/");
|
||||
return redirect("/chat");
|
||||
}
|
||||
|
||||
return <Verify user={currentUser} />;
|
||||
|
@ -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 (
|
||||
|
@ -1661,6 +1661,7 @@ export function ChatPage({
|
||||
setShowDocSidebar: setShowHistorySidebar,
|
||||
setToggled: removeToggle,
|
||||
mobile: settings?.isMobile,
|
||||
isAnonymousUser: user?.is_anonymous_user,
|
||||
});
|
||||
|
||||
const autoScrollEnabled =
|
||||
@ -2229,6 +2230,7 @@ export function ChatPage({
|
||||
toggleSidebar={toggleSidebar}
|
||||
currentChatSession={selectedChatSession}
|
||||
documentSidebarToggled={documentSidebarToggled}
|
||||
hideUserDropdown={user?.is_anonymous_user}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -2767,12 +2769,6 @@ export function ChatPage({
|
||||
setFiltersToggled(false);
|
||||
setDocumentSidebarToggled(true);
|
||||
}}
|
||||
removeFilters={() => {
|
||||
filterManager.setSelectedSources([]);
|
||||
filterManager.setSelectedTags([]);
|
||||
filterManager.setSelectedDocumentSets([]);
|
||||
setDocumentSidebarToggled(false);
|
||||
}}
|
||||
showConfigureAPIKey={() =>
|
||||
setShowApiKeyModal(true)
|
||||
}
|
||||
@ -2782,7 +2778,6 @@ export function ChatPage({
|
||||
selectedDocuments={selectedDocuments}
|
||||
// assistant stuff
|
||||
selectedAssistant={liveAssistant}
|
||||
setSelectedAssistant={onAssistantChange}
|
||||
setAlternativeAssistant={setAlternativeAssistant}
|
||||
alternativeAssistant={alternativeAssistant}
|
||||
// end assistant stuff
|
||||
@ -2790,7 +2785,6 @@ export function ChatPage({
|
||||
setMessage={setMessage}
|
||||
onSubmit={onSubmit}
|
||||
filterManager={filterManager}
|
||||
llmOverrideManager={llmOverrideManager}
|
||||
files={currentMessageFiles}
|
||||
setFiles={setCurrentMessageFiles}
|
||||
toggleFilters={
|
||||
@ -2798,7 +2792,6 @@ export function ChatPage({
|
||||
}
|
||||
handleFileUpload={handleImageUpload}
|
||||
textAreaRef={textAreaRef}
|
||||
chatSessionId={chatSessionIdRef.current!}
|
||||
/>
|
||||
{enterpriseSettings &&
|
||||
enterpriseSettings.custom_lower_disclaimer_content && (
|
||||
|
@ -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 { 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";
|
||||
@ -37,9 +36,9 @@ import FiltersDisplay from "./FilterDisplay";
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
interface ChatInputBarProps {
|
||||
removeFilters: () => void;
|
||||
removeDocs: () => void;
|
||||
openModelSettings: () => void;
|
||||
showDocs: () => void;
|
||||
showConfigureAPIKey: () => void;
|
||||
selectedDocuments: OnyxDocument[];
|
||||
message: string;
|
||||
@ -47,41 +46,34 @@ interface ChatInputBarProps {
|
||||
stopGenerating: () => void;
|
||||
onSubmit: () => void;
|
||||
filterManager: FilterManager;
|
||||
llmOverrideManager: LlmOverrideManager;
|
||||
chatState: ChatState;
|
||||
showDocs: () => void;
|
||||
alternativeAssistant: Persona | null;
|
||||
// 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<HTMLTextAreaElement>;
|
||||
chatSessionId?: string;
|
||||
toggleFilters?: () => void;
|
||||
}
|
||||
|
||||
export function ChatInputBar({
|
||||
removeFilters,
|
||||
removeDocs,
|
||||
openModelSettings,
|
||||
showConfigureAPIKey,
|
||||
showDocs,
|
||||
showConfigureAPIKey,
|
||||
selectedDocuments,
|
||||
message,
|
||||
setMessage,
|
||||
stopGenerating,
|
||||
onSubmit,
|
||||
filterManager,
|
||||
llmOverrideManager,
|
||||
chatState,
|
||||
|
||||
// assistants
|
||||
selectedAssistant,
|
||||
setSelectedAssistant,
|
||||
setAlternativeAssistant,
|
||||
|
||||
files,
|
||||
@ -89,7 +81,6 @@ export function ChatInputBar({
|
||||
handleFileUpload,
|
||||
textAreaRef,
|
||||
alternativeAssistant,
|
||||
chatSessionId,
|
||||
toggleFilters,
|
||||
}: ChatInputBarProps) {
|
||||
useEffect(() => {
|
||||
|
@ -156,6 +156,7 @@ export default async function RootLayout({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (productGating === GatingType.FULL) {
|
||||
return getPageContent(
|
||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function NotFound() {
|
||||
redirect("/chat");
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page() {
|
||||
redirect("/chat");
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
@ -59,9 +59,11 @@ const DropdownOption: React.FC<DropdownOptionProps> = ({
|
||||
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 ? (
|
||||
<DropdownOption
|
||||
onClick={() => router.push("/auth/login")}
|
||||
icon={<UserIcon className="h-5 w-5 my-auto mr-2" />}
|
||||
label="Log In"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{customNavItems.map((item, i) => (
|
||||
@ -251,6 +260,7 @@ export function UserDropdown({
|
||||
label="User Settings"
|
||||
/>
|
||||
)}
|
||||
|
||||
<DropdownOption
|
||||
onClick={() => {
|
||||
setUserInfoVisible(true);
|
||||
|
@ -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");
|
||||
|
@ -29,6 +29,7 @@ import { useRef, useState } from "react";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { EditIcon } from "@/components/icons/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
|
||||
export function SectionHeader({
|
||||
children,
|
||||
@ -143,6 +144,7 @@ export function TextFormField({
|
||||
small,
|
||||
removeLabel,
|
||||
min,
|
||||
includeForgotPassword,
|
||||
onChange,
|
||||
width,
|
||||
vertical,
|
||||
@ -169,6 +171,7 @@ export function TextFormField({
|
||||
explanationLink?: string;
|
||||
small?: boolean;
|
||||
min?: number;
|
||||
includeForgotPassword?: boolean;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
width?: string;
|
||||
vertical?: boolean;
|
||||
@ -238,7 +241,7 @@ export function TextFormField({
|
||||
)}
|
||||
</div>
|
||||
{subtext && <SubLabel>{subtext}</SubLabel>}
|
||||
<div className={`w-full flex ${includeRevert && "gap-x-2"}`}>
|
||||
<div className={`w-full flex ${includeRevert && "gap-x-2"} relative`}>
|
||||
<Field
|
||||
onChange={handleChange}
|
||||
min={min}
|
||||
@ -269,6 +272,14 @@ export function TextFormField({
|
||||
placeholder={placeholder}
|
||||
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>
|
||||
|
||||
{explanationText && (
|
||||
|
@ -1,16 +1,41 @@
|
||||
import Link from "next/link";
|
||||
import { Logo } from "../logo/Logo";
|
||||
|
||||
export default function AuthFlowContainer({
|
||||
children,
|
||||
authState,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
authState?: "signup" | "login";
|
||||
}) {
|
||||
return (
|
||||
<div className="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="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-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} />
|
||||
{children}
|
||||
</div>
|
||||
{authState === "login" && (
|
||||
<div className="text-sm mt-4 text-center w-full text-neutral-900 font-medium mx-auto">
|
||||
Don'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>
|
||||
);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ export default function FunctionalHeader({
|
||||
sidebarToggled,
|
||||
documentSidebarToggled,
|
||||
toggleUserSettings,
|
||||
hideUserDropdown,
|
||||
}: {
|
||||
reset?: () => void;
|
||||
page: pageType;
|
||||
@ -30,6 +31,7 @@ export default function FunctionalHeader({
|
||||
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
|
||||
toggleSidebar?: () => void;
|
||||
toggleUserSettings?: () => void;
|
||||
hideUserDropdown?: boolean;
|
||||
}) {
|
||||
const settings = useContext(SettingsContext);
|
||||
useEffect(() => {
|
||||
@ -106,7 +108,7 @@ export default function FunctionalHeader({
|
||||
</div>
|
||||
|
||||
<div className="absolute right-0 mobile:top-2 desktop:top-0 flex">
|
||||
{setSharingModalVisible && (
|
||||
{setSharingModalVisible && !hideUserDropdown && (
|
||||
<div
|
||||
onClick={() => setSharingModalVisible(true)}
|
||||
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" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mobile:hidden flex my-auto">
|
||||
<UserDropdown
|
||||
hideUserDropdown={hideUserDropdown}
|
||||
page={page}
|
||||
toggleUserSettings={toggleUserSettings}
|
||||
/>
|
||||
|
@ -7,6 +7,7 @@ interface UseSidebarVisibilityProps {
|
||||
setShowDocSidebar: Dispatch<SetStateAction<boolean>>;
|
||||
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;
|
||||
|
||||
|
@ -2652,7 +2652,7 @@ export const OpenIcon = ({
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="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"
|
||||
/>
|
||||
@ -2676,7 +2676,7 @@ export const DexpandTwoIcon = ({
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m.5 13.5l5-5m-4 0h4v4m8-12l-5 5m4 0h-4v-4"
|
||||
/>
|
||||
@ -2700,7 +2700,7 @@ export const ExpandTwoIcon = ({
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m8.5 5.5l5-5m-4 0h4v4m-8 4l-5 5m4 0h-4v-4"
|
||||
/>
|
||||
@ -2724,7 +2724,7 @@ export const DownloadCSVIcon = ({
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="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"
|
||||
/>
|
||||
@ -2748,7 +2748,7 @@ export const UserIcon = ({
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke-linejoin="round"
|
||||
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"
|
||||
|
@ -49,10 +49,13 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
|
||||
maximum_chat_retention_days: null,
|
||||
notifications: [],
|
||||
needs_reindexing: false,
|
||||
anonymous_user_enabled: false,
|
||||
};
|
||||
} else {
|
||||
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 {
|
||||
@ -64,7 +67,9 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
|
||||
if (!results[1].ok) {
|
||||
if (results[1].status !== 403 && results[1].status !== 401) {
|
||||
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 {
|
||||
@ -77,7 +82,9 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
|
||||
if (!results[2].ok) {
|
||||
if (results[2].status !== 403) {
|
||||
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 {
|
||||
|
@ -86,7 +86,9 @@ export async function fetchChatData(searchParams: {
|
||||
const foldersResponse = results[7] as Response | null;
|
||||
|
||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||
if (!authDisabled && !user) {
|
||||
|
||||
// TODO Validate need
|
||||
if (!authDisabled && !user && !authTypeMetadata?.anonymousUserEnabled) {
|
||||
const headersList = await headers();
|
||||
const fullUrl = headersList.get("x-url") || "/chat";
|
||||
const searchParamsString = new URLSearchParams(
|
||||
@ -95,6 +97,7 @@ export async function fetchChatData(searchParams: {
|
||||
const redirectUrl = searchParamsString
|
||||
? `${fullUrl}?${searchParamsString}`
|
||||
: fullUrl;
|
||||
|
||||
return redirect(`/auth/login?next=${encodeURIComponent(redirectUrl)}`);
|
||||
}
|
||||
|
||||
|
@ -62,6 +62,7 @@ export interface User {
|
||||
oidc_expiry?: Date;
|
||||
is_cloud_superuser?: boolean;
|
||||
organization_name: string | null;
|
||||
is_anonymous_user?: boolean;
|
||||
}
|
||||
|
||||
export interface MinimalUserSnapshot {
|
||||
|
@ -8,6 +8,7 @@ export interface AuthTypeMetadata {
|
||||
authType: AuthType;
|
||||
autoRedirect: boolean;
|
||||
requiresVerification: boolean;
|
||||
anonymousUserEnabled: boolean | null;
|
||||
}
|
||||
|
||||
export const getAuthTypeMetadataSS = async (): Promise<AuthTypeMetadata> => {
|
||||
@ -16,8 +17,11 @@ export const getAuthTypeMetadataSS = async (): Promise<AuthTypeMetadata> => {
|
||||
throw new Error("Failed to fetch data");
|
||||
}
|
||||
|
||||
const data: { auth_type: string; requires_verification: boolean } =
|
||||
await res.json();
|
||||
const data: {
|
||||
auth_type: string;
|
||||
requires_verification: boolean;
|
||||
anonymous_user_enabled: boolean | null;
|
||||
} = await res.json();
|
||||
|
||||
let authType: AuthType;
|
||||
|
||||
@ -35,12 +39,14 @@ export const getAuthTypeMetadataSS = async (): Promise<AuthTypeMetadata> => {
|
||||
authType,
|
||||
autoRedirect: true,
|
||||
requiresVerification: data.requires_verification,
|
||||
anonymousUserEnabled: data.anonymous_user_enabled,
|
||||
};
|
||||
}
|
||||
return {
|
||||
authType,
|
||||
autoRedirect: false,
|
||||
requiresVerification: data.requires_verification,
|
||||
anonymousUserEnabled: data.anonymous_user_enabled,
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user