diff --git a/backend/ee/onyx/main.py b/backend/ee/onyx/main.py index 7d7278bb2..22f64d6cb 100644 --- a/backend/ee/onyx/main.py +++ b/backend/ee/onyx/main.py @@ -27,6 +27,7 @@ from ee.onyx.server.reporting.usage_export_api import router as usage_export_rou from ee.onyx.server.saml import router as saml_router from ee.onyx.server.seeding import seed_db from ee.onyx.server.tenants.api import router as tenants_router +from ee.onyx.server.tenants.router import router as new_router from ee.onyx.server.token_rate_limits.api import ( router as token_rate_limit_settings_router, ) @@ -123,6 +124,7 @@ def get_application() -> FastAPI: include_router_with_global_prefix_prepended(application, user_group_router) # Analytics endpoints include_router_with_global_prefix_prepended(application, analytics_router) + include_router_with_global_prefix_prepended(application, new_router) include_router_with_global_prefix_prepended(application, query_history_router) # EE only backend APIs include_router_with_global_prefix_prepended(application, query_router) diff --git a/backend/ee/onyx/server/middleware/tenant_tracking.py b/backend/ee/onyx/server/middleware/tenant_tracking.py index 5a031c35a..c3e0124ba 100644 --- a/backend/ee/onyx/server/middleware/tenant_tracking.py +++ b/backend/ee/onyx/server/middleware/tenant_tracking.py @@ -25,8 +25,11 @@ def add_tenant_id_middleware(app: FastAPI, logger: logging.LoggerAdapter) -> Non ) -> Response: try: if MULTI_TENANT: + print("Shold set tenant id") tenant_id = await _get_tenant_id_from_request(request, logger) + print(f"Tenant id: {tenant_id}") else: + print("Should not set tenant id") tenant_id = POSTGRES_DEFAULT_SCHEMA CURRENT_TENANT_ID_CONTEXTVAR.set(tenant_id) @@ -64,8 +67,9 @@ async def _get_tenant_id_from_request( try: # Look up token data in Redis - + print("I AM IN THIS FUNCTION 7") token_data = await retrieve_auth_token_data_from_redis(request) + print("I AM IN THIS FUNCTION 8") if not token_data: logger.debug( diff --git a/backend/ee/onyx/server/tenants/provisioning.py b/backend/ee/onyx/server/tenants/provisioning.py index 5b1fc99a5..a5f29e81a 100644 --- a/backend/ee/onyx/server/tenants/provisioning.py +++ b/backend/ee/onyx/server/tenants/provisioning.py @@ -333,7 +333,7 @@ async def delete_user_from_control_plane(tenant_id: str, email: str) -> None: ) -async def complete_tenant_setup(tenant_id: str, email: str) -> None: +async def complete_tenant_setup(tenant_id: str) -> None: """Complete the tenant setup process after user creation. This function handles the remaining steps of tenant provisioning after the initial @@ -374,9 +374,7 @@ async def complete_tenant_setup(tenant_id: str, email: str) -> None: user=None, distinct_id=tenant_id, event_type=MilestoneRecordType.TENANT_CREATED, - properties={ - "email": email, - }, + properties={}, db_session=db_session, ) diff --git a/backend/ee/onyx/server/tenants/router.py b/backend/ee/onyx/server/tenants/router.py index 81ebb7cc4..a62ea19d7 100644 --- a/backend/ee/onyx/server/tenants/router.py +++ b/backend/ee/onyx/server/tenants/router.py @@ -6,10 +6,9 @@ from fastapi import HTTPException from pydantic import BaseModel from ee.onyx.server.tenants.provisioning import complete_tenant_setup -from ee.onyx.server.tenants.user_mapping import get_tenant_id_for_email -from onyx.auth.users import current_user -from onyx.auth.users import exceptions -from onyx.db.models import User +from onyx.auth.users import optional_minimal_user +from onyx.db.models import MinimalUser +from shared_configs.contextvars import get_current_tenant_id logger = logging.getLogger(__name__) @@ -23,25 +22,18 @@ class CompleteTenantSetupRequest(BaseModel): @router.post("/complete-setup") async def api_complete_tenant_setup( request: CompleteTenantSetupRequest, - user: User = Depends(current_user), -) -> dict: + user: MinimalUser = Depends(optional_minimal_user), +) -> None: """Complete the tenant setup process for a user. This endpoint is called from the frontend after user creation to complete the tenant setup process (migrations, seeding, etc.). """ - if not user.is_admin and user.email != request.email: - raise HTTPException( - status_code=403, detail="You can only complete setup for your own tenant" - ) + + tenant_id = get_current_tenant_id() try: - tenant_id = get_tenant_id_for_email(request.email) - except exceptions.UserNotExists: - raise HTTPException(status_code=404, detail="User or tenant not found") - - try: - await complete_tenant_setup(tenant_id, request.email) + await complete_tenant_setup(tenant_id) return {"status": "success"} except Exception as e: logger.error(f"Failed to complete tenant setup: {e}") diff --git a/backend/onyx/auth/users.py b/backend/onyx/auth/users.py index b02f0905b..8df7a39af 100644 --- a/backend/onyx/auth/users.py +++ b/backend/onyx/auth/users.py @@ -13,6 +13,7 @@ from typing import Optional from typing import Tuple import jwt +import sqlalchemy.exc from email_validator import EmailNotValidError from email_validator import EmailUndeliverableError from email_validator import validate_email @@ -244,6 +245,29 @@ class SimpleUserManager(UUIDIDMixin, BaseUserManager[MinimalUser, uuid.UUID]): verification_token_lifetime_seconds = AUTH_COOKIE_EXPIRE_TIME_SECONDS user_db: SQLAlchemyUserDatabase[MinimalUser, uuid.UUID] + async def get(self, id: uuid.UUID) -> MinimalUser: + """Get a user by id, with error handling for partial database provisioning.""" + try: + return await super().get(id) + except sqlalchemy.exc.ProgrammingError as e: + # Handle database schema mismatch during partial provisioning + if "column user.temperature_override_enabled does not exist" in str(e): + # Create a minimal user with just the required fields + # This is a temporary solution during partial provisioning + from onyx.db.models import MinimalUser + + return MinimalUser( + id=id, + email="temp@example.com", # Will be replaced with actual data + hashed_password="", + is_active=True, + is_verified=True, + is_superuser=False, + role="BASIC", + ) + # Re-raise other database errors + raise + class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): reset_password_token_secret = USER_AUTH_SECRET @@ -688,6 +712,12 @@ async def get_user_manager( yield UserManager(user_db) +async def get_minimal_user_manager( + user_db: SQLAlchemyUserDatabase = Depends(get_user_db), +) -> AsyncGenerator[SimpleUserManager, None]: + yield SimpleUserManager(user_db) + + cookie_transport = CookieTransport( cookie_max_age=SESSION_EXPIRE_TIME_SECONDS, cookie_secure=WEB_DOMAIN.startswith("https"), @@ -699,6 +729,10 @@ def get_redis_strategy() -> RedisStrategy: return TenantAwareRedisStrategy() +def get_minimal_redis_strategy() -> RedisStrategy: + return CustomRedisStrategy() + + def get_database_strategy( access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db), ) -> DatabaseStrategy: @@ -764,14 +798,139 @@ class TenantAwareRedisStrategy(RedisStrategy[User, uuid.UUID]): await redis.delete(f"{self.key_prefix}{token}") +class CustomRedisStrategy(RedisStrategy[MinimalUser, uuid.UUID]): + """ + A custom strategy that fetches the actual async Redis connection inside each method. + We do NOT pass a synchronous or "coroutine" redis object to the constructor. + """ + + def __init__( + self, + lifetime_seconds: Optional[int] = SESSION_EXPIRE_TIME_SECONDS, + key_prefix: str = REDIS_AUTH_KEY_PREFIX, + ): + self.lifetime_seconds = lifetime_seconds + self.key_prefix = key_prefix + + async def write_token(self, user: MinimalUser) -> str: + redis = await get_async_redis_connection() + + tenant_id, is_newly_created = await fetch_ee_implementation_or_noop( + "onyx.server.tenants.provisioning", + "get_or_provision_tenant", + async_return_default_schema, + )(email=user.email) + + token_data = { + "sub": str(user.id), + "tenant_id": tenant_id, + } + token = secrets.token_urlsafe() + await redis.set( + f"{self.key_prefix}{token}", + json.dumps(token_data), + ex=self.lifetime_seconds, + ) + return token + + async def read_token( + self, + token: Optional[str], + user_manager: BaseUserManager[MinimalUser, uuid.UUID], + ) -> Optional[MinimalUser]: + redis = await get_async_redis_connection() + token_data_str = await redis.get(f"{self.key_prefix}{token}") + if not token_data_str: + return None + + try: + token_data = json.loads(token_data_str) + user_id = token_data["sub"] + parsed_id = user_manager.parse_id(user_id) + return await user_manager.get(parsed_id) + except (exceptions.UserNotExists, exceptions.InvalidID, KeyError): + return None + + async def destroy_token(self, token: str, user: MinimalUser) -> None: + """Properly delete the token from async redis.""" + redis = await get_async_redis_connection() + await redis.delete(f"{self.key_prefix}{token}") + + +# class CustomRedisStrategy(RedisStrategy[MinimalUser, uuid.UUID]): +# """Custom Redis strategy that handles database schema mismatches during partial provisioning.""" + +# def __init__( +# self, +# lifetime_seconds: Optional[int] = SESSION_EXPIRE_TIME_SECONDS, +# key_prefix: str = REDIS_AUTH_KEY_PREFIX, +# ): +# self.lifetime_seconds = lifetime_seconds +# self.key_prefix = key_prefix + +# async def read_token( +# self, token: Optional[str], user_manager: BaseUserManager[MinimalUser, uuid.UUID] +# ) -> Optional[MinimalUser]: +# try: +# redis = await get_async_redis_connection() +# token_data_str = await redis.get(f"{self.key_prefix}{token}") +# if not token_data_str: +# return None + +# try: +# token_data = json.loads(token_data_str) +# user_id = token_data["sub"] +# parsed_id = user_manager.parse_id(user_id) +# return await user_manager.get(parsed_id) +# except (exceptions.UserNotExists, exceptions.InvalidID, KeyError): +# return None +# except sqlalchemy.exc.ProgrammingError as e: +# # Handle database schema mismatch during partial provisioning +# if "column user.temperature_override_enabled does not exist" in str(e): +# # Return None to allow unauthenticated access during partial provisioning +# return None +# # Re-raise other database errors +# raise + +# async def write_token(self, user: MinimalUser) -> str: +# redis = await get_async_redis_connection() + +# token = generate_jwt( +# data={"sub": str(user.id)}, +# secret=USER_AUTH_SECRET, +# lifetime_seconds=self.lifetime_seconds, +# ) + +# await redis.set( +# f"{self.key_prefix}{token}", +# json.dumps({"sub": str(user.id)}), +# ex=self.lifetime_seconds, +# ) + +# return token + +# async def destroy_token(self, token: str, user: MinimalUser) -> None: +# """Properly delete the token from async redis.""" +# redis = await get_async_redis_connection() +# await redis.delete(f"{self.key_prefix}{token}") + + if AUTH_BACKEND == AuthBackend.REDIS: auth_backend = AuthenticationBackend( name="redis", transport=cookie_transport, get_strategy=get_redis_strategy ) + minimal_auth_backend = AuthenticationBackend( + name="redis", + transport=cookie_transport, + get_strategy=get_minimal_redis_strategy, + ) elif AUTH_BACKEND == AuthBackend.POSTGRES: auth_backend = AuthenticationBackend( name="postgres", transport=cookie_transport, get_strategy=get_database_strategy ) + minimal_auth_backend = AuthenticationBackend( + name="postgres", transport=cookie_transport, get_strategy=get_database_strategy + ) else: raise ValueError(f"Invalid auth backend: {AUTH_BACKEND}") @@ -818,6 +977,9 @@ fastapi_users = FastAPIUserWithLogoutRouter[User, uuid.UUID]( get_user_manager, [auth_backend] ) +fastapi_minimal_users = FastAPIUserWithLogoutRouter[MinimalUser, uuid.UUID]( + get_minimal_user_manager, [minimal_auth_backend] +) # NOTE: verified=REQUIRE_EMAIL_VERIFICATION is not used here since we # take care of that in `double_check_user` ourself. This is needed, since @@ -826,6 +988,30 @@ fastapi_users = FastAPIUserWithLogoutRouter[User, uuid.UUID]( optional_fastapi_current_user = fastapi_users.current_user(active=True, optional=True) +optional_minimal_user_dependency = fastapi_minimal_users.current_user( + active=True, optional=True +) + + +async def optional_minimal_user( + user: MinimalUser | None = Depends(optional_minimal_user_dependency), +) -> MinimalUser | None: + """NOTE: `request` and `db_session` are not used here, but are included + for the EE version of this function.""" + print("I AM IN THIS FUNCTION 1") + try: + print("I AM IN THIS FUNCTION 2") + return user + except sqlalchemy.exc.ProgrammingError as e: + print("I AM IN THIS FUNCTION 3") + # Handle database schema mismatch during partial provisioning + if "column user.temperature_override_enabled does not exist" in str(e): + # Return None to allow unauthenticated access during partial provisioning + return None + # Re-raise other database errors + raise + + async def optional_user_( request: Request, user: User | None, diff --git a/backend/onyx/db/engine.py b/backend/onyx/db/engine.py index 1b32b4b48..2d0d2fb2d 100644 --- a/backend/onyx/db/engine.py +++ b/backend/onyx/db/engine.py @@ -475,17 +475,27 @@ def get_session_generator_with_tenant() -> Generator[Session, None, None]: def get_session() -> Generator[Session, None, None]: tenant_id = get_current_tenant_id() + print(f"Retrieved tenant_id: {tenant_id}") + if tenant_id == POSTGRES_DEFAULT_SCHEMA and MULTI_TENANT: + print("Authentication error: User must authenticate") raise BasicAuthenticationError(detail="User must authenticate") engine = get_sqlalchemy_engine() + print("SQLAlchemy engine obtained") with Session(engine, expire_on_commit=False) as session: if MULTI_TENANT: + print("MULTI_TENANT mode enabled") if not is_valid_schema_name(tenant_id): + print(f"Invalid tenant ID detected: {tenant_id}") raise HTTPException(status_code=400, detail="Invalid tenant ID") + print(f"Setting search_path to tenant schema: {tenant_id}") session.execute(text(f'SET search_path = "{tenant_id}"')) + else: + print("MULTI_TENANT mode disabled") yield session + print("Session yielded and closed") async def get_async_session() -> AsyncGenerator[AsyncSession, None]: diff --git a/backend/onyx/redis/redis_pool.py b/backend/onyx/redis/redis_pool.py index 3e7c722d5..dab7877cf 100644 --- a/backend/onyx/redis/redis_pool.py +++ b/backend/onyx/redis/redis_pool.py @@ -315,13 +315,20 @@ async def retrieve_auth_token_data_from_redis(request: Request) -> dict | None: try: redis = await get_async_redis_connection() - redis_key = REDIS_AUTH_KEY_PREFIX + token + print("[Redis] Obtained async Redis connection") + redis_key = f"{REDIS_AUTH_KEY_PREFIX}{token}" + print(f"[Redis] Fetching token data for key: {redis_key}") token_data_str = await redis.get(redis_key) + print( + f"[Redis] Retrieved token data: {'found' if token_data_str else 'not found'}" + ) if not token_data_str: - logger.debug(f"Token key {redis_key} not found or expired in Redis") + logger.debug(f"[Redis] Token key '{redis_key}' not found or expired") return None + print("[Redis] Decoding token data") + print(f"[Redis] Token data: {token_data_str}") return json.loads(token_data_str) except json.JSONDecodeError: logger.error("Error decoding token data from Redis") diff --git a/backend/onyx/server/auth_check.py b/backend/onyx/server/auth_check.py index c1bbb6b46..3f9bb8283 100644 --- a/backend/onyx/server/auth_check.py +++ b/backend/onyx/server/auth_check.py @@ -10,6 +10,7 @@ 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 from onyx.auth.users import current_user_with_expired_token +from onyx.auth.users import optional_minimal_user from onyx.configs.app_configs import APP_API_PREFIX from onyx.server.onyx_api.ingestion import api_key_dep from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop @@ -114,6 +115,7 @@ def check_router_auth( or depends_fn == current_user_with_expired_token or depends_fn == current_chat_accesssible_user or depends_fn == control_plane_dep + or depends_fn == optional_minimal_user or depends_fn == current_cloud_superuser ): found_auth = True diff --git a/backend/onyx/server/manage/models.py b/backend/onyx/server/manage/models.py index cf51a7b08..4aafa4ebe 100644 --- a/backend/onyx/server/manage/models.py +++ b/backend/onyx/server/manage/models.py @@ -112,6 +112,12 @@ class UserInfo(BaseModel): ) +class MinimalUserInfo(BaseModel): + id: str + email: str + is_active: bool + + class UserByEmail(BaseModel): user_email: str diff --git a/backend/onyx/server/manage/users.py b/backend/onyx/server/manage/users.py index ad8f5098d..04dbd9a7d 100644 --- a/backend/onyx/server/manage/users.py +++ b/backend/onyx/server/manage/users.py @@ -32,6 +32,7 @@ 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 +from onyx.auth.users import optional_minimal_user from onyx.auth.users import optional_user from onyx.configs.app_configs import AUTH_TYPE from onyx.configs.app_configs import DEV_MODE @@ -44,6 +45,7 @@ from onyx.db.api_key import is_api_key_email_address from onyx.db.auth import get_total_users_count from onyx.db.engine import get_session from onyx.db.models import AccessToken +from onyx.db.models import MinimalUser from onyx.db.models import User from onyx.db.users import delete_user_from_db from onyx.db.users import get_all_users @@ -55,6 +57,7 @@ from onyx.key_value_store.factory import get_kv_store from onyx.server.documents.models import PaginatedReturn from onyx.server.manage.models import AllUsersResponse from onyx.server.manage.models import AutoScrollRequest +from onyx.server.manage.models import MinimalUserInfo from onyx.server.manage.models import UserByEmail from onyx.server.manage.models import UserInfo from onyx.server.manage.models import UserPreferences @@ -534,6 +537,27 @@ def get_current_token_creation( return None +@router.get("/me-info") +def verify_user_attempting_to_login( + request: Request, + user: MinimalUser | None = Depends(optional_minimal_user), + # db_session: Session = Depends(get_session), +) -> MinimalUserInfo: + # Check if the authentication cookie exists + # Print cookie names for debugging + cookie_names = list(request.cookies.keys()) + logger.info(f"Available cookies: {cookie_names}") + + if not request.cookies.get(FASTAPI_USERS_AUTH_COOKIE_NAME): + raise HTTPException(status_code=401, detail="User not found") + # print("I AM IN THIS FUNCTION 4") + # if user is None: + # print("I AM IN THIS FUNCTION 5") + # raise HTTPException(status_code=401, detail="User not found") + # print("I AM IN THIS FUNCTION 6") + return MinimalUserInfo(id="", email="", is_active=True) + + @router.get("/me") def verify_user_logged_in( user: User | None = Depends(optional_user), diff --git a/web/src/app/auth/waiting-on-setup/WaitingOnSetup.tsx b/web/src/app/auth/waiting-on-setup/WaitingOnSetup.tsx new file mode 100644 index 000000000..a769025ca --- /dev/null +++ b/web/src/app/auth/waiting-on-setup/WaitingOnSetup.tsx @@ -0,0 +1,242 @@ +"use client"; + +import { redirect } from "next/navigation"; +import { HealthCheckBanner } from "@/components/health/healthcheck"; +import { MinimalUserInfo } from "@/lib/types"; +import Text from "@/components/ui/text"; +import { Logo } from "@/components/logo/Logo"; +import { completeTenantSetup } from "@/lib/tenant"; +import { useEffect, useState, useRef } from "react"; +import { + Card, + CardContent, + CardHeader, + CardFooter, +} from "@/components/ui/card"; +import { LoadingSpinner } from "@/app/chat/chat_search/LoadingSpinner"; +import { Button } from "@/components/ui/button"; +import { logout } from "@/lib/user"; +import { FiLogOut } from "react-icons/fi"; + +export default function WaitingOnSetupPage({ + minimalUserInfo, +}: { + minimalUserInfo: MinimalUserInfo; +}) { + const [progress, setProgress] = useState(0); + const [setupStage, setSetupStage] = useState( + "Setting up your account" + ); + const progressRef = useRef(0); + const animationRef = useRef(); + const startTimeRef = useRef(Date.now()); + const [isReady, setIsReady] = useState(false); + const pollIntervalRef = useRef(null); + + // Setup stages that will cycle through during the loading process + const setupStages = [ + "Setting up your account", + "Configuring workspace", + "Preparing resources", + "Setting up permissions", + "Finalizing setup", + ]; + + // Function to poll the /api/me endpoint + const pollAccountStatus = async () => { + try { + const response = await fetch("/api/me"); + if (response.status === 200) { + // Account is ready + setIsReady(true); + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + } + return true; + } + } catch (error) { + console.error("Error polling account status:", error); + } + return false; + }; + + // Handle logout + const handleLogout = async () => { + try { + await logout(); + window.location.href = "/auth/login"; + } catch (error) { + console.error("Failed to logout:", error); + } + }; + + useEffect(() => { + // Animation setup for progress bar + let lastUpdateTime = 0; + const updateInterval = 100; + const normalAnimationDuration = 30000; // 30 seconds for normal animation + const acceleratedAnimationDuration = 1500; // 1.5 seconds for accelerated animation after ready + let currentStageIndex = 0; + let lastStageUpdateTime = Date.now(); + const stageRotationInterval = 3000; // Rotate stages every 3 seconds + + const animate = (timestamp: number) => { + const elapsedTime = timestamp - startTimeRef.current; + const now = Date.now(); + + // Calculate progress using different curves based on ready status + const maxProgress = 99; + let progress; + + if (isReady) { + // Accelerate to 100% when account is ready + progress = + maxProgress + + (100 - maxProgress) * + ((now - startTimeRef.current) / acceleratedAnimationDuration); + if (progress >= 100) progress = 100; + } else { + // Slower progress when still waiting + progress = + maxProgress * (1 - Math.exp(-elapsedTime / normalAnimationDuration)); + } + + // Update progress if enough time has passed + if (timestamp - lastUpdateTime > updateInterval) { + progressRef.current = progress; + setProgress(Math.round(progress * 10) / 10); + + // Cycle through setup stages + if (now - lastStageUpdateTime > stageRotationInterval && !isReady) { + currentStageIndex = (currentStageIndex + 1) % setupStages.length; + setSetupStage(setupStages[currentStageIndex]); + lastStageUpdateTime = now; + } + + lastUpdateTime = timestamp; + } + + // Continue animation if not completed + if (progress < 100) { + animationRef.current = requestAnimationFrame(animate); + } else if (progress >= 100) { + // Redirect when progress reaches 100% + setSetupStage("Setup complete!"); + setTimeout(() => { + window.location.href = "/chat"; + }, 500); + } + }; + + // Start animation + startTimeRef.current = performance.now(); + animationRef.current = requestAnimationFrame(animate); + + // Start polling the /api/me endpoint + pollIntervalRef.current = setInterval(async () => { + const ready = await pollAccountStatus(); + if (ready) { + // If ready, we'll let the animation handle the redirect + console.log("Account is ready!"); + } + }, 2000); // Poll every 2 seconds + + // Attempt to complete tenant setup + // completeTenantSetup(minimalUserInfo.email).catch((error) => { + // console.error("Failed to complete tenant setup:", error); + // }); + + // Cleanup function + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + } + }; + }, [isReady, minimalUserInfo.email]); + useEffect(() => { + completeTenantSetup(minimalUserInfo.email).catch((error) => { + console.error("Failed to complete tenant setup:", error); + }); + }, []); + + return ( +
+
+ +
+
+
+
+ +

+ Account Setup +

+
+ + + +
+
+
+ +
+

+ {setupStage} +

+
+ + {progress}% + +
+
+ + {/* Progress bar */} +
+
+
+ +
+
+ + Welcome,{" "} + + {minimalUserInfo?.email} + + + + We're setting up your account. This may take a few moments. + +
+ +
+ + You'll be redirected automatically when your account is + ready. If you're not redirected within a minute, please + refresh the page. + +
+
+ + + + + +
+
+
+ ); +} diff --git a/web/src/app/auth/waiting-on-setup/page.tsx b/web/src/app/auth/waiting-on-setup/page.tsx deleted file mode 100644 index bae959196..000000000 --- a/web/src/app/auth/waiting-on-setup/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { - AuthTypeMetadata, - getAuthTypeMetadataSS, - getCurrentUserSS, -} from "@/lib/userSS"; -import { redirect } from "next/navigation"; -import { HealthCheckBanner } from "@/components/health/healthcheck"; -import { User } from "@/lib/types"; -import Text from "@/components/ui/text"; -import { Logo } from "@/components/logo/Logo"; -import { completeTenantSetup } from "@/lib/tenant"; - -export default async function Page() { - // catch cases where the backend is completely unreachable here - // without try / catch, will just raise an exception and the page - // will not render - let authTypeMetadata: AuthTypeMetadata | null = null; - let currentUser: User | null = null; - try { - [authTypeMetadata, currentUser] = await Promise.all([ - getAuthTypeMetadataSS(), - getCurrentUserSS(), - ]); - } catch (e) { - console.log(`Some fetch failed for the waiting-on-setup page - ${e}`); - } - - if (!currentUser) { - if (authTypeMetadata?.authType === "disabled") { - return redirect("/chat"); - } - return redirect("/auth/login"); - } - - // If the user is already verified, redirect to chat - if (!authTypeMetadata?.requiresVerification || currentUser.is_verified) { - // Trigger the tenant setup completion in the background - if (currentUser.email) { - try { - await completeTenantSetup(currentUser.email); - } catch (e) { - console.error("Failed to complete tenant setup:", e); - } - } - return redirect("/chat"); - } - - return ( -
-
- -
-
-
- - -
- - Hey {currentUser.email} - we're setting up your account. -
- This may take a few moments. You'll be redirected automatically - when it's ready. -
-
- If you're not redirected within a minute, please refresh the page. -
-
-
-
-
- ); -} diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index dc4835522..4fdb807bb 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -20,7 +20,7 @@ import { import { fetchAssistantData } from "@/lib/chat/fetchAssistantdata"; import { AppProvider } from "@/components/context/AppProvider"; import { PHProvider } from "./providers"; -import { getCurrentUserSS } from "@/lib/userSS"; +import { getCurrentUserSS, getMinimalUserInfoSS } from "@/lib/userSS"; import { Suspense } from "react"; import PostHogPageView from "./PostHogPageView"; import Script from "next/script"; @@ -30,6 +30,8 @@ import { ThemeProvider } from "next-themes"; import CloudError from "@/components/errorPages/CloudErrorPage"; import Error from "@/components/errorPages/ErrorPage"; import AccessRestrictedPage from "@/components/errorPages/AccessRestrictedPage"; +import CompleteTenantSetupPage from "./auth/waiting-on-setup/WaitingOnSetup"; +import WaitingOnSetupPage from "./auth/waiting-on-setup/WaitingOnSetup"; const inter = Inter({ subsets: ["latin"], @@ -70,12 +72,17 @@ export default async function RootLayout({ }: { children: React.ReactNode; }) { - const [combinedSettings, assistantsData, user] = await Promise.all([ - fetchSettingsSS(), - fetchAssistantData(), - getCurrentUserSS(), - ]); + const [combinedSettings, assistantsData, user, minimalUserInfo] = + await Promise.all([ + fetchSettingsSS(), + fetchAssistantData(), + getCurrentUserSS(), + getMinimalUserInfoSS(), + ]); + // if (!user && minimalUserInfo) { + // return ; + // } const productGating = combinedSettings?.settings.application_status ?? ApplicationStatus.ACTIVE; @@ -135,6 +142,11 @@ export default async function RootLayout({ if (productGating === ApplicationStatus.GATED_ACCESS) { return getPageContent(); } + if (!user && minimalUserInfo) { + return getPageContent( + + ); + } if (!combinedSettings) { return getPageContent( diff --git a/web/src/lib/tenant.ts b/web/src/lib/tenant.ts index 5553c4da8..7759c196b 100644 --- a/web/src/lib/tenant.ts +++ b/web/src/lib/tenant.ts @@ -1,8 +1,3 @@ -/** - * Completes the tenant setup process for a user - * @param email The email of the user - * @returns A promise that resolves when the setup is complete - */ export async function completeTenantSetup(email: string): Promise { const response = await fetch(`/api/tenants/complete-setup`, { method: "POST", diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index c667bc156..b3daa7d38 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -15,6 +15,12 @@ interface UserPreferences { temperature_override_enabled: boolean; } +export interface MinimalUserInfo { + id: string; + email: string; + is_active: boolean; +} + export enum UserRole { LIMITED = "limited", BASIC = "basic", diff --git a/web/src/lib/userSS.ts b/web/src/lib/userSS.ts index 67363b608..594182f64 100644 --- a/web/src/lib/userSS.ts +++ b/web/src/lib/userSS.ts @@ -1,5 +1,5 @@ import { cookies } from "next/headers"; -import { User } from "./types"; +import { MinimalUserInfo, User } from "./types"; import { buildUrl } from "./utilsSS"; import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies"; import { AuthType, NEXT_PUBLIC_CLOUD_ENABLED } from "./constants"; @@ -162,6 +162,30 @@ export const logoutSS = async ( } } }; +export const getMinimalUserInfoSS = + async (): Promise => { + try { + const response = await fetch(buildUrl("/me-info"), { + credentials: "include", + headers: { + cookie: (await cookies()) + .getAll() + .map((cookie) => `${cookie.name}=${cookie.value}`) + .join("; "), + }, + + next: { revalidate: 0 }, + }); + if (!response.ok) { + console.error("Failed to fetch minimal user info"); + return null; + } + return await response.json(); + } catch (e) { + console.log(`Error fetching minimal user info: ${e}`); + return null; + } + }; export const getCurrentUserSS = async (): Promise => { try {