mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-17 07:45:47 +00:00
Compare commits
14 Commits
cloud_debu
...
forgot_pas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab61f9b08b | ||
|
|
9f6eebc2e8 | ||
|
|
0bd7782246 | ||
|
|
8ead244be8 | ||
|
|
c23fbfe027 | ||
|
|
fd269983c8 | ||
|
|
063440866d | ||
|
|
7933f6500b | ||
|
|
e7c52accab | ||
|
|
7f74466828 | ||
|
|
bd2538dbd0 | ||
|
|
f83e7bfcd9 | ||
|
|
4d2e26ce4b | ||
|
|
817fdc1f36 |
@@ -66,6 +66,7 @@ jobs:
|
||||
NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.POSTHOG_HOST }}
|
||||
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
|
||||
NEXT_PUBLIC_GTM_ENABLED=true
|
||||
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=true
|
||||
# needed due to weird interactions with the builds for different platforms
|
||||
no-cache: true
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -10,6 +10,7 @@ logger = setup_logger()
|
||||
|
||||
|
||||
def posthog_on_error(error: Any, items: Any) -> None:
|
||||
"""Log any PostHog delivery errors."""
|
||||
logger.error(f"PostHog error: {error}, items: {items}")
|
||||
|
||||
|
||||
@@ -24,15 +25,10 @@ posthog = Posthog(
|
||||
def event_telemetry(
|
||||
distinct_id: str, event: str, properties: dict | None = None
|
||||
) -> None:
|
||||
logger.info(f"Capturing Posthog event: {distinct_id} {event} {properties}")
|
||||
print("API KEY", POSTHOG_API_KEY)
|
||||
print("HOST", POSTHOG_HOST)
|
||||
"""Capture and send an event to PostHog, flushing immediately."""
|
||||
logger.info(f"Capturing PostHog event: {distinct_id} {event} {properties}")
|
||||
try:
|
||||
print(type(distinct_id))
|
||||
print(type(event))
|
||||
print(type(properties))
|
||||
response = posthog.capture(distinct_id, event, properties)
|
||||
posthog.capture(distinct_id, event, properties)
|
||||
posthog.flush()
|
||||
print(response)
|
||||
except Exception as e:
|
||||
logger.error(f"Error capturing Posthog event: {e}")
|
||||
logger.error(f"Error capturing PostHog event: {e}")
|
||||
|
||||
80
backend/onyx/auth/email_utils.py
Normal file
80
backend/onyx/auth/email_utils.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from textwrap import dedent
|
||||
|
||||
from onyx.configs.app_configs import EMAIL_CONFIGURED
|
||||
from onyx.configs.app_configs import EMAIL_FROM
|
||||
from onyx.configs.app_configs import SMTP_PASS
|
||||
from onyx.configs.app_configs import SMTP_PORT
|
||||
from onyx.configs.app_configs import SMTP_SERVER
|
||||
from onyx.configs.app_configs import SMTP_USER
|
||||
from onyx.configs.app_configs import WEB_DOMAIN
|
||||
from onyx.db.models import User
|
||||
|
||||
|
||||
def send_email(
|
||||
user_email: str,
|
||||
subject: str,
|
||||
body: str,
|
||||
mail_from: str = EMAIL_FROM,
|
||||
) -> None:
|
||||
if not EMAIL_CONFIGURED:
|
||||
raise ValueError("Email is not configured.")
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg["Subject"] = subject
|
||||
msg["To"] = user_email
|
||||
if mail_from:
|
||||
msg["From"] = mail_from
|
||||
|
||||
msg.attach(MIMEText(body))
|
||||
|
||||
try:
|
||||
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as s:
|
||||
s.starttls()
|
||||
s.login(SMTP_USER, SMTP_PASS)
|
||||
s.send_message(msg)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
def send_user_email_invite(user_email: str, current_user: User) -> None:
|
||||
subject = "Invitation to Join Onyx Workspace"
|
||||
body = dedent(
|
||||
f"""\
|
||||
Hello,
|
||||
|
||||
You have been invited to join a workspace on Onyx.
|
||||
|
||||
To join the workspace, please visit the following link:
|
||||
|
||||
{WEB_DOMAIN}/auth/login
|
||||
|
||||
Best regards,
|
||||
The Onyx Team
|
||||
"""
|
||||
)
|
||||
send_email(user_email, subject, body, current_user.email)
|
||||
|
||||
|
||||
def send_forgot_password_email(
|
||||
user_email: str,
|
||||
token: str,
|
||||
mail_from: str = EMAIL_FROM,
|
||||
) -> None:
|
||||
subject = "Onyx Forgot Password"
|
||||
link = f"{WEB_DOMAIN}/auth/reset-password?token={token}"
|
||||
body = f"Click the following link to reset your password: {link}"
|
||||
send_email(user_email, subject, body, mail_from)
|
||||
|
||||
|
||||
def send_user_verification_email(
|
||||
user_email: str,
|
||||
token: str,
|
||||
mail_from: str = EMAIL_FROM,
|
||||
) -> None:
|
||||
subject = "Onyx Email Verification"
|
||||
link = f"{WEB_DOMAIN}/auth/verify-email?token={token}"
|
||||
body = f"Click the following link to verify your email address: {link}"
|
||||
send_email(user_email, subject, body, mail_from)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import smtplib
|
||||
import uuid
|
||||
from collections.abc import AsyncGenerator
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
@@ -53,19 +50,17 @@ from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from onyx.auth.api_key import get_hashed_api_key_from_request
|
||||
from onyx.auth.email_utils import send_forgot_password_email
|
||||
from onyx.auth.email_utils import send_user_verification_email
|
||||
from onyx.auth.invited_users import get_invited_users
|
||||
from onyx.auth.schemas import UserCreate
|
||||
from onyx.auth.schemas import UserRole
|
||||
from onyx.auth.schemas import UserUpdate
|
||||
from onyx.configs.app_configs import AUTH_TYPE
|
||||
from onyx.configs.app_configs import DISABLE_AUTH
|
||||
from onyx.configs.app_configs import EMAIL_FROM
|
||||
from onyx.configs.app_configs import EMAIL_CONFIGURED
|
||||
from onyx.configs.app_configs import REQUIRE_EMAIL_VERIFICATION
|
||||
from onyx.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS
|
||||
from onyx.configs.app_configs import SMTP_PASS
|
||||
from onyx.configs.app_configs import SMTP_PORT
|
||||
from onyx.configs.app_configs import SMTP_SERVER
|
||||
from onyx.configs.app_configs import SMTP_USER
|
||||
from onyx.configs.app_configs import TRACK_EXTERNAL_IDP_EXPIRY
|
||||
from onyx.configs.app_configs import USER_AUTH_SECRET
|
||||
from onyx.configs.app_configs import VALID_EMAIL_DOMAINS
|
||||
@@ -74,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
|
||||
@@ -89,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
|
||||
@@ -103,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
|
||||
@@ -143,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:
|
||||
@@ -193,30 +208,6 @@ def verify_email_domain(email: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def send_user_verification_email(
|
||||
user_email: str,
|
||||
token: str,
|
||||
mail_from: str = EMAIL_FROM,
|
||||
) -> None:
|
||||
msg = MIMEMultipart()
|
||||
msg["Subject"] = "Onyx Email Verification"
|
||||
msg["To"] = user_email
|
||||
if mail_from:
|
||||
msg["From"] = mail_from
|
||||
|
||||
link = f"{WEB_DOMAIN}/auth/verify-email?token={token}"
|
||||
|
||||
body = MIMEText(f"Click the following link to verify your email address: {link}")
|
||||
msg.attach(body)
|
||||
|
||||
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as s:
|
||||
s.starttls()
|
||||
# If credentials fails with gmail, check (You need an app password, not just the basic email password)
|
||||
# https://support.google.com/accounts/answer/185833?sjid=8512343437447396151-NA
|
||||
s.login(SMTP_USER, SMTP_PASS)
|
||||
s.send_message(msg)
|
||||
|
||||
|
||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
reset_password_token_secret = USER_AUTH_SECRET
|
||||
verification_token_secret = USER_AUTH_SECRET
|
||||
@@ -506,7 +497,15 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
async def on_after_forgot_password(
|
||||
self, user: User, token: str, request: Optional[Request] = None
|
||||
) -> None:
|
||||
logger.notice(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
if not EMAIL_CONFIGURED:
|
||||
logger.error(
|
||||
"Email is not configured. Please configure email in the admin panel"
|
||||
)
|
||||
raise HTTPException(
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
"Your admin has not enbaled this feature.",
|
||||
)
|
||||
send_forgot_password_email(user.email, token)
|
||||
|
||||
async def on_after_request_verify(
|
||||
self, user: User, token: str, request: Optional[Request] = None
|
||||
@@ -624,9 +623,7 @@ def get_database_strategy(
|
||||
|
||||
|
||||
auth_backend = AuthenticationBackend(
|
||||
name="jwt" if MULTI_TENANT else "database",
|
||||
transport=cookie_transport,
|
||||
get_strategy=get_jwt_strategy if MULTI_TENANT else get_database_strategy, # type: ignore
|
||||
name="jwt", transport=cookie_transport, get_strategy=get_jwt_strategy
|
||||
) # type: ignore
|
||||
|
||||
|
||||
@@ -713,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(
|
||||
@@ -751,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:
|
||||
|
||||
@@ -92,6 +92,7 @@ SMTP_SERVER = os.environ.get("SMTP_SERVER") or "smtp.gmail.com"
|
||||
SMTP_PORT = int(os.environ.get("SMTP_PORT") or "587")
|
||||
SMTP_USER = os.environ.get("SMTP_USER", "your-email@gmail.com")
|
||||
SMTP_PASS = os.environ.get("SMTP_PASS", "your-gmail-password")
|
||||
EMAIL_CONFIGURED = all([SMTP_SERVER, SMTP_USER, SMTP_PASS])
|
||||
EMAIL_FROM = os.environ.get("EMAIL_FROM") or SMTP_USER
|
||||
|
||||
# If set, Onyx will listen to the `expires_at` returned by the identity
|
||||
|
||||
@@ -36,6 +36,8 @@ DISABLED_GEN_AI_MSG = (
|
||||
|
||||
DEFAULT_PERSONA_ID = 0
|
||||
|
||||
DEFAULT_CC_PAIR_ID = 1
|
||||
|
||||
# Postgres connection constants for application_name
|
||||
POSTGRES_WEB_APP_NAME = "web"
|
||||
POSTGRES_INDEXER_APP_NAME = "indexer"
|
||||
@@ -273,6 +275,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:
|
||||
|
||||
@@ -310,6 +310,9 @@ def associate_default_cc_pair(db_session: Session) -> None:
|
||||
if existing_association is not None:
|
||||
return
|
||||
|
||||
# DefaultCCPair has id 1 since it is the first CC pair created
|
||||
# It is DEFAULT_CC_PAIR_ID, but can't set it explicitly because it messed with the
|
||||
# auto-incrementing id
|
||||
association = ConnectorCredentialPair(
|
||||
connector_id=0,
|
||||
credential_id=0,
|
||||
|
||||
@@ -55,9 +55,7 @@ def remove_invalid_unicode_chars(text: str) -> str:
|
||||
return _illegal_xml_chars_RE.sub("", text)
|
||||
|
||||
|
||||
def get_vespa_http_client(
|
||||
no_timeout: bool = False, http2: bool = False
|
||||
) -> httpx.Client:
|
||||
def get_vespa_http_client(no_timeout: bool = False, http2: bool = True) -> httpx.Client:
|
||||
"""
|
||||
Configure and return an HTTP client for communicating with Vespa,
|
||||
including authentication if needed.
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -532,7 +532,8 @@ def associate_credential_to_connector(
|
||||
)
|
||||
|
||||
return response
|
||||
except IntegrityError:
|
||||
except IntegrityError as e:
|
||||
logger.error(f"IntegrityError: {e}")
|
||||
raise HTTPException(status_code=400, detail="Name must be unique")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -190,7 +190,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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ee.onyx.configs.app_configs import SUPER_USERS
|
||||
from onyx.auth.email_utils import send_user_email_invite
|
||||
from onyx.auth.invited_users import get_invited_users
|
||||
from onyx.auth.invited_users import write_invited_users
|
||||
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
|
||||
@@ -61,7 +63,6 @@ from onyx.server.models import FullUserSnapshot
|
||||
from onyx.server.models import InvitedUserSnapshot
|
||||
from onyx.server.models import MinimalUserSnapshot
|
||||
from onyx.server.utils import BasicAuthenticationError
|
||||
from onyx.server.utils import send_user_email_invite
|
||||
from onyx.utils.logger import setup_logger
|
||||
from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
@@ -522,13 +523,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):
|
||||
|
||||
@@ -4,6 +4,7 @@ from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.auth.users import api_key_dep
|
||||
from onyx.configs.constants import DEFAULT_CC_PAIR_ID
|
||||
from onyx.configs.constants import DocumentSource
|
||||
from onyx.connectors.models import Document
|
||||
from onyx.connectors.models import IndexAttemptMetadata
|
||||
@@ -79,7 +80,7 @@ def upsert_ingestion_doc(
|
||||
document.source = DocumentSource.FILE
|
||||
|
||||
cc_pair = get_connector_credential_pair_from_id(
|
||||
cc_pair_id=doc_info.cc_pair_id or 0, db_session=db_session
|
||||
cc_pair_id=doc_info.cc_pair_id or DEFAULT_CC_PAIR_ID, db_session=db_session
|
||||
)
|
||||
if cc_pair is None:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -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
|
||||
@@ -197,7 +198,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_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
|
||||
@@ -330,7 +331,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())
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import json
|
||||
import smtplib
|
||||
from datetime import datetime
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi import status
|
||||
|
||||
from onyx.configs.app_configs import SMTP_PASS
|
||||
from onyx.configs.app_configs import SMTP_PORT
|
||||
from onyx.configs.app_configs import SMTP_SERVER
|
||||
from onyx.configs.app_configs import SMTP_USER
|
||||
from onyx.configs.app_configs import WEB_DOMAIN
|
||||
from onyx.db.models import User
|
||||
|
||||
|
||||
class BasicAuthenticationError(HTTPException):
|
||||
def __init__(self, detail: str):
|
||||
@@ -62,31 +51,3 @@ def mask_credential_dict(credential_dict: dict[str, Any]) -> dict[str, str]:
|
||||
|
||||
masked_creds[key] = mask_string(val)
|
||||
return masked_creds
|
||||
|
||||
|
||||
def send_user_email_invite(user_email: str, current_user: User) -> None:
|
||||
msg = MIMEMultipart()
|
||||
msg["Subject"] = "Invitation to Join Onyx Workspace"
|
||||
msg["From"] = current_user.email
|
||||
msg["To"] = user_email
|
||||
|
||||
email_body = dedent(
|
||||
f"""\
|
||||
Hello,
|
||||
|
||||
You have been invited to join a workspace on Onyx.
|
||||
|
||||
To join the workspace, please visit the following link:
|
||||
|
||||
{WEB_DOMAIN}/auth/login
|
||||
|
||||
Best regards,
|
||||
The Onyx Team
|
||||
"""
|
||||
)
|
||||
|
||||
msg.attach(MIMEText(email_body, "plain"))
|
||||
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp_server:
|
||||
smtp_server.starttls()
|
||||
smtp_server.login(SMTP_USER, SMTP_PASS)
|
||||
smtp_server.send_message(msg)
|
||||
|
||||
@@ -22,7 +22,6 @@ from onyx.utils.variable_functionality import (
|
||||
from onyx.utils.variable_functionality import noop_fallback
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
|
||||
_DANSWER_TELEMETRY_ENDPOINT = "https://telemetry.onyx.app/anonymous_telemetry"
|
||||
_CACHED_UUID: str | None = None
|
||||
_CACHED_INSTANCE_DOMAIN: str | None = None
|
||||
@@ -118,12 +117,9 @@ def mt_cloud_telemetry(
|
||||
event: MilestoneRecordType,
|
||||
properties: dict | None = None,
|
||||
) -> None:
|
||||
print(f"mt_cloud_telemetry {distinct_id} {event} {properties}")
|
||||
if not MULTI_TENANT:
|
||||
print("mt_cloud_telemetry not MULTI_TENANT")
|
||||
return
|
||||
|
||||
print("mt_cloud_telemetry MULTI_TENANT")
|
||||
# MIT version should not need to include any Posthog code
|
||||
# This is only for Onyx MT Cloud, this code should also never be hit, no reason for any orgs to
|
||||
# be running the Multi Tenant version of Onyx.
|
||||
@@ -141,11 +137,8 @@ def create_milestone_and_report(
|
||||
properties: dict | None,
|
||||
db_session: Session,
|
||||
) -> None:
|
||||
print(f"create_milestone_and_report {user} {event_type} {db_session}")
|
||||
_, is_new = create_milestone_if_not_exists(user, event_type, db_session)
|
||||
print(f"create_milestone_and_report {is_new}")
|
||||
if is_new:
|
||||
print("create_milestone_and_report is_new")
|
||||
mt_cloud_telemetry(
|
||||
distinct_id=distinct_id,
|
||||
event=event_type,
|
||||
|
||||
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)
|
||||
@@ -267,7 +267,7 @@ services:
|
||||
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||
- NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN=${NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN:-}
|
||||
|
||||
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
|
||||
# Enterprise Edition only
|
||||
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||
# DO NOT TURN ON unless you have EXPLICIT PERMISSION from Onyx.
|
||||
|
||||
@@ -72,6 +72,7 @@ services:
|
||||
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
|
||||
depends_on:
|
||||
- api_server
|
||||
restart: always
|
||||
|
||||
@@ -99,6 +99,7 @@ services:
|
||||
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
|
||||
depends_on:
|
||||
- api_server
|
||||
restart: always
|
||||
|
||||
@@ -75,6 +75,9 @@ ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
||||
ARG NEXT_PUBLIC_GTM_ENABLED
|
||||
ENV NEXT_PUBLIC_GTM_ENABLED=${NEXT_PUBLIC_GTM_ENABLED}
|
||||
|
||||
ARG NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED
|
||||
ENV NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED}
|
||||
|
||||
RUN npx next build
|
||||
|
||||
# Step 2. Production image, copy all the files and run next
|
||||
@@ -150,6 +153,9 @@ ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
||||
ARG NEXT_PUBLIC_GTM_ENABLED
|
||||
ENV NEXT_PUBLIC_GTM_ENABLED=${NEXT_PUBLIC_GTM_ENABLED}
|
||||
|
||||
ARG NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED
|
||||
ENV NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED}
|
||||
|
||||
# Note: Don't expose ports here, Compose will handle that for us if necessary.
|
||||
# If you want to run this without compose, specify the ports to
|
||||
# expose via cli
|
||||
|
||||
@@ -81,13 +81,13 @@ export const getProviderIcon = (providerName: string, modelName?: string) => {
|
||||
}
|
||||
if (modelName?.toLowerCase().includes("phi")) {
|
||||
return MicrosoftIconSVG;
|
||||
}
|
||||
}
|
||||
if (modelName?.toLowerCase().includes("mistral")) {
|
||||
return MistralIcon;
|
||||
}
|
||||
}
|
||||
if (modelName?.toLowerCase().includes("llama")) {
|
||||
return MetaIcon;
|
||||
}
|
||||
}
|
||||
if (modelName?.toLowerCase().includes("gemini")) {
|
||||
return GeminiIcon;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
96
web/src/app/auth/forgot-password/page.tsx
Normal file
96
web/src/app/auth/forgot-password/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { forgotPassword } from "./utils";
|
||||
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import Title from "@/components/ui/title";
|
||||
import Text from "@/components/ui/text";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, Formik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { redirect } from "next/navigation";
|
||||
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||
|
||||
const ForgotPasswordPage: React.FC = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [isWorking, setIsWorking] = useState(false);
|
||||
|
||||
if (!NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthFlowContainer>
|
||||
<div className="flex flex-col w-full justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</AuthFlowContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPasswordPage;
|
||||
33
web/src/app/auth/forgot-password/utils.ts
Normal file
33
web/src/app/auth/forgot-password/utils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const forgotPassword = async (email: string): Promise<void> => {
|
||||
const response = await fetch(`/api/auth/forgot-password`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
const errorMessage =
|
||||
error?.detail || "An error occurred during password reset.";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export const resetPassword = async (
|
||||
token: string,
|
||||
password: string
|
||||
): Promise<void> => {
|
||||
const response = await fetch(`/api/auth/reset-password`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ token, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to reset password");
|
||||
}
|
||||
};
|
||||
@@ -10,6 +10,9 @@ import { requestEmailVerification } from "../lib";
|
||||
import { useState } from "react";
|
||||
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,
|
||||
@@ -22,6 +25,7 @@ export function EmailPasswordForm({
|
||||
referralSource?: string;
|
||||
nextUrl?: string | null;
|
||||
}) {
|
||||
const { user } = useUser();
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [isWorking, setIsWorking] = useState(false);
|
||||
return (
|
||||
@@ -107,18 +111,29 @@ export function EmailPasswordForm({
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
includeForgotPassword={
|
||||
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && !isSignup
|
||||
}
|
||||
placeholder="**************"
|
||||
/>
|
||||
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="mx-auto w-full"
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
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"
|
||||
>
|
||||
{isSignup ? "Sign Up" : "Log In"}
|
||||
</Button>
|
||||
</div>
|
||||
<span className="hover:border-b hover:border-dotted hover:border-blue-500">
|
||||
or continue as guest
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
@@ -16,6 +16,9 @@ import { LoginText } from "./LoginText";
|
||||
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 }>;
|
||||
@@ -42,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
|
||||
@@ -50,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
|
||||
@@ -73,71 +77,71 @@ 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">
|
||||
<Text className="mt-4 mx-auto">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
href={`/auth/signup${searchParams?.next ? `?next=${searchParams.next}` : ""}`}
|
||||
className="text-link font-medium"
|
||||
>
|
||||
Create an account
|
||||
</Link>
|
||||
</Text>
|
||||
</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">
|
||||
<Text className="mt-4 mx-auto">
|
||||
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}` : ""}`}
|
||||
href={`/auth/signup${
|
||||
searchParams?.next ? `?next=${searchParams.next}` : ""
|
||||
}`}
|
||||
className="text-link font-medium"
|
||||
>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
114
web/src/app/auth/reset-password/page.tsx
Normal file
114
web/src/app/auth/reset-password/page.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { resetPassword } from "../forgot-password/utils";
|
||||
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import Title from "@/components/ui/title";
|
||||
import Text from "@/components/ui/text";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, Formik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { redirect, useSearchParams } from "next/navigation";
|
||||
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||
|
||||
const ResetPasswordPage: React.FC = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [isWorking, setIsWorking] = useState(false);
|
||||
const searchParams = useSearchParams();
|
||||
const token = searchParams.get("token");
|
||||
|
||||
if (!NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthFlowContainer>
|
||||
<div className="flex flex-col w-full justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</AuthFlowContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPasswordPage;
|
||||
@@ -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 =
|
||||
@@ -2228,6 +2229,7 @@ export function ChatPage({
|
||||
toggleSidebar={toggleSidebar}
|
||||
currentChatSession={selectedChatSession}
|
||||
documentSidebarToggled={documentSidebarToggled}
|
||||
hideUserDropdown={user?.is_anonymous_user}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -2766,12 +2768,6 @@ export function ChatPage({
|
||||
setFiltersToggled(false);
|
||||
setDocumentSidebarToggled(true);
|
||||
}}
|
||||
removeFilters={() => {
|
||||
filterManager.setSelectedSources([]);
|
||||
filterManager.setSelectedTags([]);
|
||||
filterManager.setSelectedDocumentSets([]);
|
||||
setDocumentSidebarToggled(false);
|
||||
}}
|
||||
showConfigureAPIKey={() =>
|
||||
setShowApiKeyModal(true)
|
||||
}
|
||||
@@ -2781,7 +2777,6 @@ export function ChatPage({
|
||||
selectedDocuments={selectedDocuments}
|
||||
// assistant stuff
|
||||
selectedAssistant={liveAssistant}
|
||||
setSelectedAssistant={onAssistantChange}
|
||||
setAlternativeAssistant={setAlternativeAssistant}
|
||||
alternativeAssistant={alternativeAssistant}
|
||||
// end assistant stuff
|
||||
@@ -2789,7 +2784,6 @@ export function ChatPage({
|
||||
setMessage={setMessage}
|
||||
onSubmit={onSubmit}
|
||||
filterManager={filterManager}
|
||||
llmOverrideManager={llmOverrideManager}
|
||||
files={currentMessageFiles}
|
||||
setFiles={setCurrentMessageFiles}
|
||||
toggleFilters={
|
||||
@@ -2797,7 +2791,6 @@ export function ChatPage({
|
||||
}
|
||||
handleFileUpload={handleImageUpload}
|
||||
textAreaRef={textAreaRef}
|
||||
chatSessionId={chatSessionIdRef.current!}
|
||||
/>
|
||||
{enterpriseSettings &&
|
||||
enterpriseSettings.custom_lower_disclaimer_content && (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { DocumentUpdatedAtBadge } from "@/components/search/DocumentUpdatedAtBad
|
||||
import { MetadataBadge } from "@/components/MetadataBadge";
|
||||
import { WebResultIcon } from "@/components/WebResultIcon";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { openDocument } from "@/lib/search/utils";
|
||||
|
||||
interface DocumentDisplayProps {
|
||||
closeSidebar: () => void;
|
||||
@@ -73,14 +73,6 @@ export function ChatDocumentDisplay({
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleViewFile = async () => {
|
||||
if (document.source_type == ValidSources.File && setPresentingDocument) {
|
||||
setPresentingDocument(document);
|
||||
} else if (document.link) {
|
||||
window.open(document.link, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
const hasMetadata =
|
||||
document.updated_at || Object.keys(document.metadata).length > 0;
|
||||
return (
|
||||
@@ -91,7 +83,7 @@ export function ChatDocumentDisplay({
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={handleViewFile}
|
||||
onClick={() => openDocument(document, setPresentingDocument)}
|
||||
className="cursor-pointer text-left flex flex-col px-2 py-1.5"
|
||||
>
|
||||
<div className="line-clamp-1 mb-1 flex h-6 items-center gap-2 text-xs">
|
||||
|
||||
@@ -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;
|
||||
@@ -211,7 +214,11 @@ export function TextFormField({
|
||||
|
||||
return (
|
||||
<div className={`w-full ${width}`}>
|
||||
<div className={`flex ${vertical ? "flex-col" : "flex-row"} items-start`}>
|
||||
<div
|
||||
className={`flex ${
|
||||
vertical ? "flex-col" : "flex-row"
|
||||
} gap-x-2 items-start`}
|
||||
>
|
||||
<div className="flex gap-x-2 items-center">
|
||||
{!removeLabel && (
|
||||
<Label className={sizeClass.label} small={small}>
|
||||
@@ -234,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}
|
||||
@@ -265,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;
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ import { WebResultIcon } from "@/components/WebResultIcon";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { truncateString } from "@/lib/utils";
|
||||
import { SetStateAction } from "react";
|
||||
import { Dispatch } from "react";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { openDocument } from "@/lib/search/utils";
|
||||
|
||||
export default function SourceCard({
|
||||
doc,
|
||||
@@ -16,13 +14,7 @@ export default function SourceCard({
|
||||
return (
|
||||
<div
|
||||
key={doc.document_id}
|
||||
onClick={() => {
|
||||
if (doc.source_type == ValidSources.File && setPresentingDocument) {
|
||||
setPresentingDocument(doc);
|
||||
} else if (doc.link) {
|
||||
window.open(doc.link, "_blank");
|
||||
}
|
||||
}}
|
||||
onClick={() => openDocument(doc, setPresentingDocument)}
|
||||
className="cursor-pointer text-left overflow-hidden flex flex-col gap-0.5 rounded-sm px-3 py-2.5 hover:bg-background-125 bg-background-100 w-[200px]"
|
||||
>
|
||||
<div className="line-clamp-1 font-semibold text-ellipsis text-text-900 flex h-6 items-center gap-2 text-sm">
|
||||
|
||||
@@ -1121,12 +1121,16 @@ export const MetaIcon = ({
|
||||
export const MicrosoftIconSVG = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => <LogoIcon size={size} className={className} src={microsoftSVG} />;
|
||||
}: IconProps) => (
|
||||
<LogoIcon size={size} className={className} src={microsoftSVG} />
|
||||
);
|
||||
|
||||
export const MistralIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => <LogoIcon size={size} className={className} src={mistralSVG} />;
|
||||
}: IconProps) => (
|
||||
<LogoIcon size={size} className={className} src={mistralSVG} />
|
||||
);
|
||||
|
||||
export const VoyageIcon = ({
|
||||
size = 16,
|
||||
@@ -2645,7 +2649,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"
|
||||
/>
|
||||
@@ -2669,7 +2673,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"
|
||||
/>
|
||||
@@ -2693,7 +2697,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"
|
||||
/>
|
||||
@@ -2717,7 +2721,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"
|
||||
/>
|
||||
@@ -2741,7 +2745,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"
|
||||
|
||||
@@ -22,6 +22,7 @@ import { WarningCircle } from "@phosphor-icons/react";
|
||||
import TextView from "../chat_search/TextView";
|
||||
import { SearchResultIcon } from "../SearchResultIcon";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { openDocument } from "@/lib/search/utils";
|
||||
|
||||
export const buildDocumentSummaryDisplay = (
|
||||
matchHighlights: string[],
|
||||
@@ -428,19 +429,15 @@ export function CompactDocumentCard({
|
||||
url,
|
||||
updatePresentingDocument,
|
||||
}: {
|
||||
document: LoadedOnyxDocument;
|
||||
document: OnyxDocument;
|
||||
icon?: React.ReactNode;
|
||||
url?: string;
|
||||
updatePresentingDocument: (documentIndex: LoadedOnyxDocument) => void;
|
||||
updatePresentingDocument: (document: OnyxDocument) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (document.source_type === ValidSources.File) {
|
||||
updatePresentingDocument(document);
|
||||
} else if (document.link) {
|
||||
window.open(document.link, "_blank");
|
||||
}
|
||||
openDocument(document, updatePresentingDocument);
|
||||
}}
|
||||
className="max-w-[250px] cursor-pointer pb-0 pt-0 mt-0 flex gap-y-0 flex-col content-start items-start gap-0 "
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import { CompactDocumentCard } from "../DocumentDisplay";
|
||||
import { LoadedOnyxDocument } from "@/lib/search/interfaces";
|
||||
import { LoadedOnyxDocument, OnyxDocument } from "@/lib/search/interfaces";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { openDocument } from "@/lib/search/utils";
|
||||
|
||||
export function Citation({
|
||||
children,
|
||||
@@ -21,7 +22,7 @@ export function Citation({
|
||||
link?: string;
|
||||
children?: JSX.Element | string | null | ReactNode;
|
||||
index?: number;
|
||||
updatePresentingDocument: (documentIndex: LoadedOnyxDocument) => void;
|
||||
updatePresentingDocument: (document: OnyxDocument) => void;
|
||||
document: LoadedOnyxDocument;
|
||||
icon?: React.ReactNode;
|
||||
url?: string;
|
||||
@@ -30,20 +31,12 @@ export function Citation({
|
||||
? children?.toString().split("[")[1].split("]")[0]
|
||||
: index;
|
||||
|
||||
const onClick = () => {
|
||||
if (document.source_type == ValidSources.File) {
|
||||
updatePresentingDocument(document);
|
||||
} else {
|
||||
window.open(link || document.link, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
onMouseDown={onClick}
|
||||
onClick={() => openDocument(document, updatePresentingDocument)}
|
||||
className="inline-flex items-center cursor-pointer transition-all duration-200 ease-in-out"
|
||||
>
|
||||
<span className="flex items-center justify-center w-6 h-6 text-[11px] font-medium text-gray-700 bg-gray-100 rounded-full border border-gray-300 hover:bg-gray-200 hover:text-gray-900 shadow-sm">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,34 +2,15 @@ import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, isEditing = true, style, ...props }, ref) => {
|
||||
const textClassName = "text-2xl text-strong dark:text-neutral-50";
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<span className={cn(textClassName, className)}>
|
||||
{props.value || props.defaultValue}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
textClassName,
|
||||
"w-[1ch] min-w-[1ch] box-content pr-1",
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
width: `${Math.max(1, String(props.value || props.defaultValue || "").length)}ch`,
|
||||
...style,
|
||||
}}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,12 @@ export const NEXT_PUBLIC_CLOUD_ENABLED =
|
||||
export const REGISTRATION_URL =
|
||||
process.env.INTERNAL_URL || "http://127.0.0.1:3001";
|
||||
|
||||
export const SERVER_SIDE_ONLY__CLOUD_ENABLED =
|
||||
process.env.NEXT_PUBLIC_CLOUD_ENABLED?.toLowerCase() === "true";
|
||||
|
||||
export const NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED =
|
||||
process.env.NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED?.toLowerCase() === "true";
|
||||
|
||||
export const TEST_ENV = process.env.TEST_ENV?.toLowerCase() === "true";
|
||||
|
||||
export const NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED =
|
||||
|
||||
@@ -101,7 +101,7 @@ const MODEL_NAMES_SUPPORTING_IMAGE_INPUT = [
|
||||
"amazon.nova-pro@v1",
|
||||
// meta models
|
||||
"llama-3.2-90b-vision-instruct",
|
||||
"llama-3.2-11b-vision-instruct"
|
||||
"llama-3.2-11b-vision-instruct",
|
||||
];
|
||||
|
||||
export function checkLLMSupportsImageInput(model: string) {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Tag } from "../types";
|
||||
import { Filters, SourceMetadata } from "./interfaces";
|
||||
import { Tag, ValidSources } from "../types";
|
||||
import {
|
||||
Filters,
|
||||
LoadedOnyxDocument,
|
||||
OnyxDocument,
|
||||
SourceMetadata,
|
||||
} from "./interfaces";
|
||||
import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector";
|
||||
|
||||
export const buildFilters = (
|
||||
@@ -22,3 +27,16 @@ export const buildFilters = (
|
||||
export function endsWithLetterOrNumber(str: string) {
|
||||
return /[a-zA-Z0-9]$/.test(str);
|
||||
}
|
||||
|
||||
// If we have a link, open it in a new tab (including if it's a file)
|
||||
// If above fails and we have a file, update the presenting document
|
||||
export const openDocument = (
|
||||
document: OnyxDocument,
|
||||
updatePresentingDocument?: (document: OnyxDocument) => void
|
||||
) => {
|
||||
if (document.link) {
|
||||
window.open(document.link, "_blank");
|
||||
} else if (document.source_type === ValidSources.File) {
|
||||
updatePresentingDocument?.(document);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user