Add anonymous user to main

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

View File

@ -30,13 +30,16 @@ def load_no_auth_user_preferences(store: KeyValueStore) -> UserPreferences:
)
def fetch_no_auth_user(store: KeyValueStore) -> UserInfo:
def fetch_no_auth_user(
store: KeyValueStore, *, anonymous_user_enabled: bool | None = None
) -> UserInfo:
return UserInfo(
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,
)

View File

@ -69,6 +69,7 @@ from onyx.configs.constants import AuthType
from onyx.configs.constants import DANSWER_API_KEY_DUMMY_EMAIL_DOMAIN
from onyx.configs.constants import DANSWER_API_KEY_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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@ from onyx.auth.noauth_user import fetch_no_auth_user
from onyx.auth.noauth_user import set_no_auth_user_preferences
from onyx.auth.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):

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,73 @@
from typing import Any
from typing import Dict
from typing import Optional
import requests
from tests.integration.common_utils.constants import API_SERVER_URL
from tests.integration.common_utils.constants import GENERAL_HEADERS
from tests.integration.common_utils.test_models import DATestSettings
from tests.integration.common_utils.test_models import DATestUser
class SettingsManager:
@staticmethod
def get_settings(
user_performing_action: DATestUser | None = None,
) -> tuple[Dict[str, Any], str]:
headers = (
user_performing_action.headers
if user_performing_action
else GENERAL_HEADERS
)
headers.pop("Content-Type", None)
response = requests.get(
f"{API_SERVER_URL}/api/manage/admin/settings",
headers=headers,
)
if not response.ok:
return (
{},
f"Failed to get settings - {response.json().get('detail', 'Unknown error')}",
)
return response.json(), ""
@staticmethod
def update_settings(
settings: DATestSettings,
user_performing_action: DATestUser | None = None,
) -> tuple[Dict[str, Any], str]:
headers = (
user_performing_action.headers
if user_performing_action
else GENERAL_HEADERS
)
headers.pop("Content-Type", None)
payload = settings.model_dump()
response = requests.patch(
f"{API_SERVER_URL}/api/manage/admin/settings",
json=payload,
headers=headers,
)
if not response.ok:
return (
{},
f"Failed to update settings - {response.json().get('detail', 'Unknown error')}",
)
return response.json(), ""
@staticmethod
def get_setting(
key: str,
user_performing_action: DATestUser | None = None,
) -> Optional[Any]:
settings, error = SettingsManager.get_settings(user_performing_action)
if error:
return None
return settings.get(key)

View File

@ -1,3 +1,4 @@
from enum import Enum
from typing import Any
from 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

View File

@ -0,0 +1,14 @@
from tests.integration.common_utils.managers.settings import SettingsManager
from tests.integration.common_utils.managers.user import UserManager
from tests.integration.common_utils.test_models import DATestSettings
from tests.integration.common_utils.test_models import DATestUser
def test_limited(reset: None) -> None:
"""Verify that with a limited role key, limited endpoints are accessible and
others are not."""
# Creating an admin user (first user created is automatically an admin)
admin_user: DATestUser = UserManager.create(name="admin_user")
SettingsManager.update_settings(DATestSettings(anonymous_user_enabled=True))
print(admin_user.headers)

View File

@ -10,6 +10,7 @@ import { DefaultDropdown, Option } from "@/components/Dropdown";
import React, { useContext, useState, useEffect } from "react";
import { 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."

View File

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

View File

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

View File

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

View File

@ -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&apos;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>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -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 && (

View File

@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 && (

View File

@ -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&apos;t have an account?{" "}
<Link
href="/auth/signup"
className=" underline transition-colors duration-200"
>
Create one
</Link>
</div>
)}
{authState === "signup" && (
<div className="text-sm mt-4 text-center w-full text-neutral-900 font-medium mx-auto">
Already have an account?{" "}
<Link
href="/auth/login"
className=" underline transition-colors duration-200"
>
Log In
</Link>
</div>
)}
</div>
);
}

View File

@ -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}
/>

View File

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

View File

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

View File

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

View File

@ -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)}`);
}

View File

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

View File

@ -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,
};
};