From 7794f8ff7da2f9a45adc33d2f3e90dbc26950906 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:57:32 +0530 Subject: [PATCH] [PAI-979] Chore pi default oauth check (#4744) --- apps/pi/pi/app/api/v1/endpoints/chat.py | 69 +++++ apps/pi/pi/app/api/v1/endpoints/oauth.py | 266 +++++++++--------- apps/pi/pi/app/schemas/chat.py | 14 + apps/web/app/assets/auth/pi-chat-dark.webp | Bin 0 -> 10954 bytes apps/web/app/assets/auth/pi-chat-light.webp | Bin 0 -> 11814 bytes apps/web/ee/components/home/header.tsx | 24 +- .../pi-chat/conversation/ai-message.tsx | 13 +- .../pi-chat/conversation/feedback.tsx | 22 +- .../pi-chat/conversation/new-converstaion.tsx | 44 +-- apps/web/ee/components/pi-chat/detail.tsx | 9 +- .../ee/components/pi-chat/unauthorized.tsx | 68 +++++ apps/web/ee/layouts/workspace-wrapper.tsx | 11 +- apps/web/ee/services/pi-chat.service.ts | 13 +- apps/web/ee/store/pi-chat/pi-chat.ts | 16 +- apps/web/ee/types/pi-chat.d.ts | 10 + 15 files changed, 382 insertions(+), 197 deletions(-) create mode 100644 apps/web/app/assets/auth/pi-chat-dark.webp create mode 100644 apps/web/app/assets/auth/pi-chat-light.webp create mode 100644 apps/web/ee/components/pi-chat/unauthorized.tsx diff --git a/apps/pi/pi/app/api/v1/endpoints/chat.py b/apps/pi/pi/app/api/v1/endpoints/chat.py index 726c0e56a9..e2af7573c9 100644 --- a/apps/pi/pi/app/api/v1/endpoints/chat.py +++ b/apps/pi/pi/app/api/v1/endpoints/chat.py @@ -6,6 +6,7 @@ from typing import AsyncGenerator from typing import Coroutine from typing import Optional from typing import cast +from urllib.parse import urlparse from uuid import UUID from uuid import uuid4 @@ -20,6 +21,7 @@ from sqlalchemy import select from sqlmodel.ext.asyncio.session import AsyncSession from pi import logger +from pi import settings from pi.app.api.v1.dependencies import cookie_schema from pi.app.api.v1.dependencies import is_valid_session from pi.app.api.v1.dependencies import validate_plane_token @@ -39,6 +41,7 @@ from pi.app.schemas.chat import ActionBatchExecutionRequest from pi.app.schemas.chat import ArtifactData from pi.app.schemas.chat import ChatFeedback from pi.app.schemas.chat import ChatInitializationRequest +from pi.app.schemas.chat import ChatInitResponse from pi.app.schemas.chat import ChatRequest from pi.app.schemas.chat import ChatSearchResponse from pi.app.schemas.chat import ChatSuggestionTemplate @@ -57,6 +60,7 @@ from pi.app.utils.background_tasks import schedule_chat_search_upsert from pi.app.utils.exceptions import SQLGenerationError from pi.core.db.plane_pi.lifecycle import get_async_session from pi.core.db.plane_pi.lifecycle import get_streaming_db_session +from pi.services.actions.oauth_service import PlaneOAuthService from pi.services.chat.chat import PlaneChatBot from pi.services.chat.helpers.batch_execution_helpers import get_original_user_query from pi.services.chat.helpers.batch_execution_helpers import get_planned_actions_for_execution @@ -97,6 +101,71 @@ BATCH_EXECUTION_ERRORS = { } +@router.get("/start/", response_model=ChatInitResponse) +async def chat_start( + workspace_id: UUID = Query(..., description="Workspace ID to check authorization for"), + db: AsyncSession = Depends(get_async_session), + session: str = Depends(cookie_schema), +): + """ + Start/Bootstrap Pi chat - first API call when starting a chat session. + Checks OAuth authorization status and returns chat templates. + + Usage: GET /api/v1/chat/start/?workspace_id= + + If authorized: returns is_authorized=true with full templates list + If not authorized: returns is_authorized=false with empty templates list + """ + try: + # Validate session + auth = await is_valid_session(session) + if not auth.user: + return JSONResponse(status_code=401, content={"detail": "Invalid User"}) + user_id = auth.user.id + except Exception as e: + log.error(f"Error validating session: {e!s}") + return JSONResponse(status_code=401, content={"detail": "Invalid Session"}) + + try: + oauth_service = PlaneOAuthService() + + # Check if user has valid OAuth token for workspace + access_token = await oauth_service.get_valid_token(db=db, user_id=user_id, workspace_id=workspace_id) + + if access_token: + # User is authorized - get templates + templates = tiles_factory() + return ChatInitResponse(is_authorized=True, templates=templates, oauth_url=None) + else: + # User is not authorized - return empty templates with OAuth URL + from pi.services.actions.oauth_url_encoder import OAuthUrlEncoder + + redirect = urlparse(settings.plane_api.OAUTH_REDIRECT_URI) + base_url = f"{redirect.scheme}://{redirect.netloc}" + + # Include BASE_PATH if configured + base_path = settings.plane_api.BASE_PATH or "" + if base_path and not base_path.startswith("/"): + base_path = f"/{base_path}" + base_path = base_path.rstrip("/") + + # Build OAuth parameters (only user_id and workspace_id) + oauth_params = { + "user_id": str(user_id), + "workspace_id": str(workspace_id), + } + + # Generate clean, encrypted OAuth URL + oauth_encoder = OAuthUrlEncoder() + oauth_url = oauth_encoder.generate_clean_oauth_url(f"{base_url}{base_path}", oauth_params) + + return ChatInitResponse(is_authorized=False, templates=[], oauth_url=oauth_url) + + except Exception as e: + log.error(f"Error in chat start: {e!s}") + return JSONResponse(status_code=500, content={"detail": "Failed to initialize chat"}) + + @router.post("/slack/answer/") async def get_answer_for_slack(data: ChatRequest, request: Request, db: AsyncSession = Depends(get_async_session)): try: diff --git a/apps/pi/pi/app/api/v1/endpoints/oauth.py b/apps/pi/pi/app/api/v1/endpoints/oauth.py index 91ff40aa67..1ffe53ddb0 100644 --- a/apps/pi/pi/app/api/v1/endpoints/oauth.py +++ b/apps/pi/pi/app/api/v1/endpoints/oauth.py @@ -4,6 +4,7 @@ Handles authorization flow, callback processing, and token management """ from datetime import datetime +from typing import Optional from uuid import UUID from fastapi import APIRouter @@ -35,10 +36,11 @@ async def browser_initiate_oauth( db: AsyncSession = Depends(get_async_session), session: str = Depends(cookie_schema), ): + """Browser-based OAuth initiation with session cookie authentication.""" auth = await is_valid_session(session) if not auth.user: return JSONResponse(status_code=401, content={"detail": "Invalid User"}) - user_id = auth.user.id + oauth_service = PlaneOAuthService() # Clean up expired states periodically @@ -48,19 +50,16 @@ async def browser_initiate_oauth( log.warning(f"Failed to cleanup expired states: {e}") # Get workspace slug if workspace_id is provided - workspace_slug = None - if workspace_id: - workspace_slug = await get_workspace_slug(str(workspace_id)) + workspace_slug = await get_workspace_slug(str(workspace_id)) if workspace_id else None - # same logic as POST /initiate/ + # Generate authorization URL and save state auth_url, state = oauth_service.generate_authorization_url( - user_id=user_id, + user_id=auth.user.id, workspace_id=workspace_id, workspace_slug=workspace_slug, ) - await oauth_service.save_oauth_state(db, state, user_id, workspace_id, workspace_slug) + await oauth_service.save_oauth_state(db, state, auth.user.id, workspace_id, workspace_slug) - # just redirect the browser return RedirectResponse(url=auth_url, status_code=302) @@ -69,10 +68,11 @@ async def public_initiate_oauth( user_id: UUID = Query(..., description="User ID requesting authorization"), workspace_id: UUID = Query(..., description="Workspace ID to authorize"), force_reset: bool = Query(False, description="Force reset of existing OAuth states"), + sidebar_open_url: Optional[str] = Query(None, description="The URL where the sidebar was opened from"), db: AsyncSession = Depends(get_async_session), ): """ - Public OAuth initiation endpoint that doesn't require session authentication. + Public OAuth initiation endpoint without session authentication. Used when session cookies can't be shared across domains. """ oauth_service = PlaneOAuthService() @@ -83,7 +83,7 @@ async def public_initiate_oauth( except Exception as e: log.warning(f"Failed to cleanup expired states: {e}") - # If force_reset is requested, clear existing states for this user/workspace + # Force reset if requested if force_reset: try: reset_count = await oauth_service.reset_user_oauth_states(db, user_id, workspace_id) @@ -91,10 +91,8 @@ async def public_initiate_oauth( except Exception as e: log.warning(f"Failed to reset OAuth states: {e}") - # Get workspace slug for the workspace - workspace_slug = None - if workspace_id: - workspace_slug = await get_workspace_slug(str(workspace_id)) + # Get workspace slug + workspace_slug = await get_workspace_slug(str(workspace_id)) if workspace_id else None # Generate authorization URL with state auth_url, state = oauth_service.generate_authorization_url( @@ -103,19 +101,25 @@ async def public_initiate_oauth( workspace_slug=workspace_slug, ) - # Save state for verification - await oauth_service.save_oauth_state(db, state, user_id, workspace_id, workspace_slug) + # Save OAuth state with sidebar context + await oauth_service.save_oauth_state( + db, + state, + user_id, + workspace_id, + workspace_slug, + pi_sidebar_open=sidebar_open_url is not None, + sidebar_open_url=sidebar_open_url, + ) - log.debug(f"Generated new OAuth state for user {user_id}") - - # Redirect to Plane for authorization + log.debug(f"Generated OAuth state for user {user_id}") return RedirectResponse(url=auth_url, status_code=302) @router.get("/callback/") async def oauth_callback( code: str = Query(..., description="Authorization code from Plane"), - state: str | None = Query(None, description="State parameter for verification - optional for marketplace install"), + state: str | None = Query(None, description="State parameter for verification"), app_installation_id: str = Query(None, description="App installation ID from Plane"), db: AsyncSession = Depends(get_async_session), ): @@ -125,41 +129,33 @@ async def oauth_callback( """ try: oauth_service = PlaneOAuthService() + log.debug(f"OAuth callback - code: {code[:10]}..., state: {state}, app_installation_id: {app_installation_id}") - log.debug(f"OAuth callback received - code: {code[:10]}..., state: {state}, app_installation_id: {app_installation_id}") - + # Verify state parameter oauth_state = None if state: - # Flow initiated via /oauth/initiate/ or /oauth/init/ oauth_state = await oauth_service.verify_state(db, state) if not oauth_state: log.error(f"Invalid or expired OAuth state: {state}") - # Clean up expired states to help with future requests cleanup_count = await oauth_service.cleanup_expired_states(db) log.info(f"Cleaned up {cleanup_count} expired states") return JSONResponse(status_code=400, content={"detail": "Invalid or expired authorization state. Please try initiating OAuth again."}) - else: - log.debug(f"OAuth state verified successfully: {state}") + log.debug(f"OAuth state verified: {state}") else: log.info("Processing OAuth callback without state parameter") # Exchange code for tokens token_data = await oauth_service.exchange_code_for_tokens(code=code) - # Get app installation details to fetch workspace info + # Extract workspace and user info from state workspace_id = oauth_state.workspace_id if oauth_state else None workspace_slug = oauth_state.workspace_slug if oauth_state and oauth_state.workspace_slug else "unknown" + user_id_for_token = oauth_state.user_id if oauth_state else None - # Ensure we have a valid workspace_id + # Validate required fields if not workspace_id: - log.error("No workspace_id available from state or installation details") + log.error("No workspace_id available from state") raise Exception("Unable to determine workspace for token storage") - - # Determine user ID to associate with token - user_id_for_token = None - if oauth_state: - user_id_for_token = oauth_state.user_id - if not user_id_for_token: log.error("Unable to determine user_id for token storage") raise Exception("user_id required for token storage") @@ -175,85 +171,77 @@ async def oauth_callback( app_bot_user_id=None, ) - # Mark state as used only after successful completion + # Mark state as used if oauth_state: await oauth_service.mark_state_as_used(db, oauth_state) - log.info( - "OAuth completed successfully for user %s, workspace %s", - str(user_id_for_token), - workspace_slug, - ) + log.info(f"OAuth completed for user {user_id_for_token}, workspace {workspace_slug}") # Determine redirect URL based on stored context - if oauth_state and oauth_state.chat_id: - # Include message_token in redirect URL for frontend to use with stream-answer - message_token_param = "" - if oauth_state.message_token: - message_token_param = f"&message_token={oauth_state.message_token}" - - # Update the MessageFlowStep to mark OAuth as complete - try: - from sqlalchemy import text - - # Update the oauth_completed column to mark OAuth as complete - update_stmt = text(""" - UPDATE message_flow_steps - SET oauth_completed = true, oauth_completed_at = :timestamp - WHERE message_id = :message_id AND tool_name = 'QUEUE' - """) - await db.execute(update_stmt, {"message_id": str(oauth_state.message_token), "timestamp": datetime.utcnow()}) - await db.commit() - log.info(f"Updated MessageFlowStep {oauth_state.message_token} to mark OAuth as complete") - except Exception as e: - log.warning(f"Failed to update MessageFlowStep OAuth status: {e}") - # Don't fail the OAuth flow if this update fails - - # Sidebar open redirect - if getattr(oauth_state, "pi_sidebar_open", False) and oauth_state.sidebar_open_url: - # Build sidebar redirect URL - sidebar_url = oauth_state.sidebar_open_url - chat_id = oauth_state.chat_id - redirect_url = ( - f"{settings.plane_api.FRONTEND_URL}/{sidebar_url}/?chat_id={chat_id}{message_token_param}&pi_sidebar_open=true&oauth_success=true" # noqa: E501 - ) - # Project chat redirect - elif getattr(oauth_state, "is_project_chat", False): - redirect_url = f"{settings.plane_api.FRONTEND_URL}/{workspace_slug}/projects/pi-chat/{oauth_state.chat_id}/?oauth_success=true{message_token_param}" # noqa: E501 - # Default chat redirect - else: - redirect_url = ( - f"{settings.plane_api.FRONTEND_URL}/{workspace_slug}/pi-chat/{oauth_state.chat_id}/?oauth_success=true{message_token_param}" - ) - else: - # Fallback to default success page - redirect_url = f"{settings.plane_api.FRONTEND_URL}?oauth_success=true&workspace={workspace_slug}" + redirect_url = await _build_redirect_url(oauth_state, workspace_slug, db) return RedirectResponse(url=redirect_url, status_code=302) except Exception as e: log.error(f"Error processing OAuth callback: {e!s}") - # Redirect to frontend with error - frontend_url = settings.plane_api.FRONTEND_URL - redirect_url = f"{frontend_url}?oauth_error=true&message=authorization_failed" + redirect_url = f"{settings.plane_api.FRONTEND_URL}?oauth_error=true&message=authorization_failed" return RedirectResponse(url=redirect_url, status_code=302) +async def _build_redirect_url(oauth_state, workspace_slug: str, db: AsyncSession) -> str: + """Build the appropriate redirect URL based on OAuth state context.""" + if not oauth_state: + return f"{settings.plane_api.FRONTEND_URL}/{workspace_slug}/pi-chat/?oauth_success=true" + + # Handle message token parameter + message_token_param = "" + if oauth_state.message_token: + message_token_param = f"&message_token={oauth_state.message_token}" + + # Update MessageFlowStep to mark OAuth as complete + try: + from sqlalchemy import text + + update_stmt = text(""" + UPDATE message_flow_steps + SET oauth_completed = true, oauth_completed_at = :timestamp + WHERE message_id = :message_id AND tool_name = 'QUEUE' + """) + await db.execute(update_stmt, {"message_id": str(oauth_state.message_token), "timestamp": datetime.utcnow()}) + await db.commit() + log.info(f"Updated MessageFlowStep {oauth_state.message_token} to mark OAuth as complete") + except Exception as e: + log.warning(f"Failed to update MessageFlowStep OAuth status: {e}") + + # Sidebar open redirect (highest priority) + if getattr(oauth_state, "pi_sidebar_open", False) and oauth_state.sidebar_open_url: + return oauth_state.sidebar_open_url + + # Chat-specific redirects + if oauth_state.chat_id: + # Project chat redirect + if getattr(oauth_state, "is_project_chat", False): + return ( + f"{settings.plane_api.FRONTEND_URL}/{workspace_slug}/projects/pi-chat/{oauth_state.chat_id}/?oauth_success=true{message_token_param}" + ) + # Default chat redirect + return f"{settings.plane_api.FRONTEND_URL}/{workspace_slug}/pi-chat/{oauth_state.chat_id}/?oauth_success=true{message_token_param}" + + # Fallback to default success page + return f"{settings.plane_api.FRONTEND_URL}/{workspace_slug}/pi-chat/?oauth_success=true" + + @router.post("/status/", response_model=OAuthStatusResponse) async def check_oauth_status( data: OAuthStatusRequest, db: AsyncSession = Depends(get_async_session), session: str = Depends(cookie_schema), ): - """ - Check OAuth authorization status for a specific workspace. - Returns whether user has valid authorization and token details. - """ + """Check OAuth authorization status for a specific workspace.""" try: auth = await is_valid_session(session) if not auth.user: return JSONResponse(status_code=401, content={"detail": "Invalid User"}) - user_id = auth.user.id except Exception as e: log.error(f"Error validating session: {e!s}") return JSONResponse(status_code=401, content={"detail": "Invalid Session"}) @@ -262,7 +250,7 @@ async def check_oauth_status( oauth_service = PlaneOAuthService() # Check if user has valid token for workspace - access_token = await oauth_service.get_valid_token(db=db, user_id=user_id, workspace_id=data.workspace_id) + access_token = await oauth_service.get_valid_token(db=db, user_id=auth.user.id, workspace_id=data.workspace_id) if access_token: # Get token details for expiry info @@ -272,7 +260,9 @@ async def check_oauth_status( result = await db.execute( select(PlaneOAuthToken).where( - PlaneOAuthToken.user_id == user_id, PlaneOAuthToken.workspace_id == data.workspace_id, PlaneOAuthToken.is_active is True + PlaneOAuthToken.user_id == auth.user.id, + PlaneOAuthToken.workspace_id == data.workspace_id, + PlaneOAuthToken.is_active is True, ) ) oauth_token = result.scalar_one_or_none() @@ -282,8 +272,8 @@ async def check_oauth_status( workspace_slug=oauth_token.workspace_slug if oauth_token else None, expires_at=oauth_token.expires_at.isoformat() if oauth_token else None, ) - else: - return OAuthStatusResponse(is_authorized=False, workspace_slug=None, expires_at=None) + + return OAuthStatusResponse(is_authorized=False, workspace_slug=None, expires_at=None) except Exception as e: log.error(f"Error checking OAuth status: {e!s}") @@ -296,30 +286,24 @@ async def revoke_oauth_authorization( db: AsyncSession = Depends(get_async_session), session: str = Depends(cookie_schema), ): - """ - Revoke OAuth authorization for a specific workspace. - Deactivates stored tokens and prevents further API access. - """ + """Revoke OAuth authorization for a specific workspace.""" try: auth = await is_valid_session(session) if not auth.user: return JSONResponse(status_code=401, content={"detail": "Invalid User"}) - user_id = auth.user.id except Exception as e: log.error(f"Error validating session: {e!s}") return JSONResponse(status_code=401, content={"detail": "Invalid Session"}) try: oauth_service = PlaneOAuthService() - - # Revoke token - success = await oauth_service.revoke_token(db=db, user_id=user_id, workspace_id=data.workspace_id) + success = await oauth_service.revoke_token(db=db, user_id=auth.user.id, workspace_id=data.workspace_id) if success: - log.info(f"OAuth authorization revoked for user {user_id}, workspace {data.workspace_id}") + log.info(f"OAuth authorization revoked for user {auth.user.id}, workspace {data.workspace_id}") return OAuthRevokeResponse(success=True, message="Authorization revoked successfully") - else: - return OAuthRevokeResponse(success=False, message="No active authorization found for this workspace") + + return OAuthRevokeResponse(success=False, message="No active authorization found for this workspace") except Exception as e: log.error(f"Error revoking OAuth authorization: {e!s}") @@ -331,15 +315,11 @@ async def list_authorized_workspaces( db: AsyncSession = Depends(get_async_session), session: str = Depends(cookie_schema), ): - """ - List all workspaces the user has authorized access to. - Returns workspace details and token status. - """ + """List all workspaces the user has authorized access to.""" try: auth = await is_valid_session(session) if not auth.user: return JSONResponse(status_code=401, content={"detail": "Invalid User"}) - user_id = auth.user.id except Exception as e: log.error(f"Error validating session: {e!s}") return JSONResponse(status_code=401, content={"detail": "Invalid Session"}) @@ -350,18 +330,19 @@ async def list_authorized_workspaces( from pi.app.models.oauth import PlaneOAuthToken # Get all active tokens for user - result = await db.execute(select(PlaneOAuthToken).where(PlaneOAuthToken.user_id == user_id, PlaneOAuthToken.is_active is True)) + result = await db.execute(select(PlaneOAuthToken).where(PlaneOAuthToken.user_id == auth.user.id, PlaneOAuthToken.is_active is True)) tokens = result.scalars().all() - workspaces = [] - for token in tokens: - workspaces.append({ + workspaces = [ + { "workspace_id": str(token.workspace_id), "workspace_slug": token.workspace_slug, "expires_at": token.expires_at.isoformat(), "needs_refresh": token.needs_refresh(), "is_expired": token.is_expired(), - }) + } + for token in tokens + ] return JSONResponse(content={"workspaces": workspaces}) @@ -373,20 +354,15 @@ async def list_authorized_workspaces( @router.post("/reset-states/") async def reset_oauth_states( user_id: UUID = Query(..., description="User ID to reset states for"), - workspace_id: UUID = Query(None, description="Workspace ID to reset (optional - if not provided, resets all user states)"), + workspace_id: UUID = Query(None, description="Workspace ID to reset (optional)"), db: AsyncSession = Depends(get_async_session), ): - """ - Reset OAuth states for a user to allow fresh authorization attempts. - This endpoint can be used when users are stuck with invalid/expired state errors. - """ + """Reset OAuth states for a user to allow fresh authorization attempts.""" try: oauth_service = PlaneOAuthService() # Reset states for the user reset_count = await oauth_service.reset_user_oauth_states(db, user_id, workspace_id) - - # Also cleanup expired states cleanup_count = await oauth_service.cleanup_expired_states(db) message = f"Reset {reset_count} OAuth states for user {user_id}" @@ -395,7 +371,6 @@ async def reset_oauth_states( message += f". Cleaned up {cleanup_count} expired states." log.info(message) - return JSONResponse(content={"success": True, "message": message, "reset_count": reset_count, "cleanup_count": cleanup_count}) except Exception as e: @@ -406,16 +381,17 @@ async def reset_oauth_states( @router.get("/authorize/{encoded_params}", tags=["oauth"]) async def clean_oauth_init( encoded_params: str, + sidebar_open_url: Optional[str] = Query(None, description="Sidebar open URL from frontend"), + pi_sidebar_open: Optional[str] = Query(None, description="Pi sidebar open flag from frontend"), db: AsyncSession = Depends(get_async_session), ): """ - Clean OAuth initiation endpoint with encrypted parameters. - This replaces the ugly URL with a clean, encrypted one. + Clean OAuth initialization endpoint with encrypted parameters. """ try: from pi.services.actions.oauth_url_encoder import OAuthUrlEncoder - # Decode the parameters + # Decode the parameters from encrypted token oauth_encoder = OAuthUrlEncoder() params = oauth_encoder.decode_oauth_params(encoded_params) @@ -426,8 +402,20 @@ async def clean_oauth_init( message_token = params.get("message_token") is_project_chat = params.get("is_project_chat", "false").lower() == "true" project_id = params.get("project_id") - pi_sidebar_open = params.get("pi_sidebar_open", "false").lower() == "true" - sidebar_open_url = params.get("sidebar_open_url") + + # Use query parameters if provided, otherwise fallback to encrypted params + final_sidebar_open_url = sidebar_open_url or params.get("sidebar_open_url") + final_pi_sidebar_open = ( + pi_sidebar_open.strip().lower() == "true" if pi_sidebar_open else params.get("pi_sidebar_open", "false").lower() == "true" + ) + + # Set pi_sidebar_open=True when sidebar_open_url is provided + if final_sidebar_open_url and not final_pi_sidebar_open: + final_pi_sidebar_open = True + + # Prepend FRONTEND_URL if sidebar_open_url is a relative path + if final_sidebar_open_url and not final_sidebar_open_url.startswith(("http://", "https://")): + final_sidebar_open_url = f"{settings.plane_api.FRONTEND_URL}{final_sidebar_open_url}" oauth_service = PlaneOAuthService() @@ -438,9 +426,11 @@ async def clean_oauth_init( log.warning(f"Failed to cleanup expired states: {e}") # Get workspace slug for the workspace - workspace_slug = None - if workspace_id: - workspace_slug = await get_workspace_slug(str(workspace_id)) + workspace_slug = await get_workspace_slug(str(workspace_id)) + if not workspace_slug: + log.error(f"Workspace not found for id {workspace_id}") + redirect_url = f"{settings.plane_api.FRONTEND_URL}?oauth_error=true&message=workspace_not_found" + return RedirectResponse(url=redirect_url, status_code=302) # Generate authorization URL with state auth_url, state = oauth_service.generate_authorization_url( @@ -460,18 +450,16 @@ async def clean_oauth_init( message_token=message_token, is_project_chat=is_project_chat, project_id=project_id, - pi_sidebar_open=pi_sidebar_open, - sidebar_open_url=sidebar_open_url, + pi_sidebar_open=final_pi_sidebar_open, + sidebar_open_url=final_sidebar_open_url, ) - log.debug(f"Generated new OAuth state for user {user_id}") + log.debug(f"Generated OAuth state for user {user_id}, workspace {workspace_slug}") # Redirect to Plane for authorization return RedirectResponse(url=auth_url, status_code=302) except Exception as e: - log.error(f"Error processing clean OAuth init: {e}") - # Redirect to frontend with error - frontend_url = settings.plane_api.FRONTEND_URL - redirect_url = f"{frontend_url}?oauth_error=true&message=invalid_parameters" + log.error(f"Error processing OAuth init: {e}") + redirect_url = f"{settings.plane_api.FRONTEND_URL}?oauth_error=true&message=invalid_parameters" return RedirectResponse(url=redirect_url, status_code=302) diff --git a/apps/pi/pi/app/schemas/chat.py b/apps/pi/pi/app/schemas/chat.py index e21eab2709..5b1ef69385 100644 --- a/apps/pi/pi/app/schemas/chat.py +++ b/apps/pi/pi/app/schemas/chat.py @@ -228,3 +228,17 @@ class GetThreadsPaginatedResponse(PaginationResponse): """Paginated response for user chat threads.""" results: list[dict[str, Any]] + + +class ChatInitRequest(BaseModel): + """Request to initialize chat - checks OAuth and gets templates""" + + workspace_id: UUID4 = Field(description="Workspace ID to check") + + +class ChatInitResponse(BaseModel): + """Response with OAuth status and chat templates for initialization""" + + is_authorized: bool = Field(description="Whether user has valid authorization for workspace") + templates: List[Any] = Field(default_factory=list, description="Chat suggestion templates") + oauth_url: Optional[str] = Field(None, description="OAuth authorization URL if not authorized") diff --git a/apps/web/app/assets/auth/pi-chat-dark.webp b/apps/web/app/assets/auth/pi-chat-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..0cf59ae7796d762ed29f4e263fb0b7c169598cf0 GIT binary patch literal 10954 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 literal 0 HcmV?d00001 diff --git a/apps/web/app/assets/auth/pi-chat-light.webp b/apps/web/app/assets/auth/pi-chat-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..0d050113130fe788d3e5f1be5695dafbec2be532 GIT binary patch literal 11814 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 literal 0 HcmV?d00001 diff --git a/apps/web/ee/components/home/header.tsx b/apps/web/ee/components/home/header.tsx index 9274b9d997..46fc5d2328 100644 --- a/apps/web/ee/components/home/header.tsx +++ b/apps/web/ee/components/home/header.tsx @@ -11,10 +11,11 @@ import { EWorkspaceFeatures } from "@/plane-web/types/workspace-feature"; import { BetaBadge } from "../common/beta"; import { WithFeatureFlagHOC } from "../feature-flags"; import { InputBox } from "../pi-chat/input"; +import { UnauthorizedView } from "../pi-chat/unauthorized"; export const HomePageHeader = observer(() => { const { workspaceSlug } = useParams(); - const { activeChatId, initPiChat } = usePiChat(); + const { activeChatId, isWorkspaceAuthorized, initPiChat } = usePiChat(); const { isWorkspaceFeatureEnabled } = useWorkspaceFeatures(); if (!isWorkspaceFeatureEnabled(EWorkspaceFeatures.IS_PI_ENABLED)) return <>; @@ -36,13 +37,20 @@ export const HomePageHeader = observer(() => { - + {isWorkspaceAuthorized ? ( + + ) : ( + + )} ); diff --git a/apps/web/ee/components/pi-chat/conversation/ai-message.tsx b/apps/web/ee/components/pi-chat/conversation/ai-message.tsx index a0aaa6e19d..fe9633696d 100644 --- a/apps/web/ee/components/pi-chat/conversation/ai-message.tsx +++ b/apps/web/ee/components/pi-chat/conversation/ai-message.tsx @@ -43,14 +43,11 @@ export const AiMessage = observer((props: TProps) => { remarkPlugins={[remarkGfm]} className="pi-chat-root [&>*:first-child]:mt-0 animate-fade-in" components={{ - a: ({ children, href }) => - href?.startsWith(`${PI_URL}/api/v1/oauth/authorize/`) && !isLatest ? ( // NOTE: Prev auth links shouldn't be accessible - {children} - ) : ( - - {children} - - ), + a: ({ children, href }) => ( + + {children} + + ), table: ({ children }) => (
{children}
diff --git a/apps/web/ee/components/pi-chat/conversation/feedback.tsx b/apps/web/ee/components/pi-chat/conversation/feedback.tsx index ae925a99d9..a95ee4de5e 100644 --- a/apps/web/ee/components/pi-chat/conversation/feedback.tsx +++ b/apps/web/ee/components/pi-chat/conversation/feedback.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { observer } from "mobx-react"; import { Copy, FilePlus2, ThumbsDown, ThumbsUp, Repeat2 } from "lucide-react"; import { setToast, TOAST_TYPE } from "@plane/propel/toast"; import { cn, Tooltip } from "@plane/ui"; @@ -18,13 +19,13 @@ export type TProps = { handleConvertToPage?: () => void; }; -export const Feedback = (props: TProps) => { +export const Feedback = observer((props: TProps) => { // props const { answer, activeChatId, id, workspaceId, feedback, queryId, isLatest, handleConvertToPage } = props; // states const [isFeedbackModalOpen, setIsFeedbackModalOpen] = useState(false); // store - const { sendFeedback, regenerateAnswer } = usePiChat(); + const { isWorkspaceAuthorized, sendFeedback, regenerateAnswer } = usePiChat(); // handlers const handleCopyLink = () => { copyTextToClipboard(answer).then(() => { @@ -119,12 +120,21 @@ export const Feedback = (props: TProps) => { {/* Convert to page */}
- -
); -}; +}); diff --git a/apps/web/ee/components/pi-chat/conversation/new-converstaion.tsx b/apps/web/ee/components/pi-chat/conversation/new-converstaion.tsx index 72def83e5f..1fa7bceacc 100644 --- a/apps/web/ee/components/pi-chat/conversation/new-converstaion.tsx +++ b/apps/web/ee/components/pi-chat/conversation/new-converstaion.tsx @@ -7,7 +7,9 @@ import { Loader } from "@plane/ui"; import { cn } from "@plane/utils"; import { useWorkspace } from "@/hooks/store/use-workspace"; import { usePiChat } from "@/plane-web/hooks/store/use-pi-chat"; +import type { TTemplate } from "@/plane-web/types"; import SystemPrompts from "../system-prompts"; +import { UnauthorizedView } from "../unauthorized"; type TProps = { currentUser: IUser | undefined; @@ -19,19 +21,24 @@ export const NewConversation = observer((props: TProps) => { const { currentUser, isFullScreen, shouldRedirect = true, isProjectLevel = false } = props; // store hooks const { workspaceSlug } = useParams(); - const { getTemplates } = usePiChat(); + const { getInstance } = usePiChat(); const { getWorkspaceBySlug } = useWorkspace(); const workspaceId = getWorkspaceBySlug(workspaceSlug as string)?.id; - const { data: templates, isLoading } = useSWR("PI_TEMPLATES", () => getTemplates(workspaceId), { - revalidateOnFocus: false, - revalidateIfStale: false, - errorRetryCount: 0, - }); + // SWR + const { data: instance, isLoading } = useSWR( + workspaceId ? `PI_STARTER_${workspaceId}` : null, + workspaceId ? () => getInstance(workspaceId) : null, + { + revalidateOnFocus: false, + revalidateIfStale: false, + errorRetryCount: 0, + } + ); // state const [isInitializing, setIsInitializing] = useState(null); if (!currentUser) return null; - + if (!isLoading && !instance?.is_authorized) return ; return (
{
) : ( -
- {templates?.map((prompt, index) => ( - setIsInitializing(value)} - /> - ))} +
+ {instance?.is_authorized && + instance?.templates?.map((prompt: TTemplate, index: number) => ( + setIsInitializing(value)} + /> + ))}
)}
diff --git a/apps/web/ee/components/pi-chat/detail.tsx b/apps/web/ee/components/pi-chat/detail.tsx index 4fe5d92f81..42c7ee5e4b 100644 --- a/apps/web/ee/components/pi-chat/detail.tsx +++ b/apps/web/ee/components/pi-chat/detail.tsx @@ -11,6 +11,7 @@ import { Loading } from "./conversation/loading"; import { Messages } from "./conversation/messages"; import { scrollIntoViewHelper } from "./helper"; import { InputBox } from "./input"; +import { UnauthorizedView } from "./unauthorized"; type TProps = { isFullScreen?: boolean; @@ -23,13 +24,13 @@ export const PiChatDetail = observer((props: TProps) => { // router const pathName = usePathname(); // store hooks - const { isAuthorized, isLoading, activeChatId } = usePiChat(); + const { isAuthorized: isChatAuthorized, isWorkspaceAuthorized, isLoading, activeChatId } = usePiChat(); const { data: currentUser } = useUser(); // derived values const isFullScreen = pathName.includes("pi-chat") || isFullScreenProp; return ( <> - {isAuthorized ? ( + {isChatAuthorized && isWorkspaceAuthorized ? (
{ />
- ) : ( + ) : isWorkspaceAuthorized ? ( + ) : ( + )} ); diff --git a/apps/web/ee/components/pi-chat/unauthorized.tsx b/apps/web/ee/components/pi-chat/unauthorized.tsx new file mode 100644 index 0000000000..6f19c7baaf --- /dev/null +++ b/apps/web/ee/components/pi-chat/unauthorized.tsx @@ -0,0 +1,68 @@ +import Link from "next/link"; +import { useParams, usePathname } from "next/navigation"; +import { useTheme } from "next-themes"; +import useSWR from "swr"; +import { getButtonStyling } from "@plane/propel/button"; +import { cn, resolveGeneralTheme } from "@plane/utils"; +import darkState from "@/app/assets/auth/pi-chat-dark.webp?url"; +import lightState from "@/app/assets/auth/pi-chat-light.webp?url"; +import { useWorkspace } from "@/hooks/store/use-workspace"; +import { usePiChat } from "@/plane-web/hooks/store/use-pi-chat"; + +export const UnauthorizedView = (props: { className?: string; imgClassName?: string }) => { + const { className, imgClassName } = props; + // router + const pathname = usePathname(); + const { workspaceSlug } = useParams(); + // store hooks + const { isPiChatDrawerOpen, getInstance } = usePiChat(); + const { resolvedTheme } = useTheme(); + const { getWorkspaceBySlug } = useWorkspace(); + // derived values + const workspaceId = getWorkspaceBySlug(workspaceSlug as string)?.id; + // SWR + const { data: instance } = useSWR( + workspaceId ? `PI_STARTER_${workspaceId}` : null, + workspaceId ? () => getInstance(workspaceId) : null, + { + revalidateOnFocus: false, + revalidateIfStale: false, + errorRetryCount: 0, + } + ); + + return ( +
+
+
+ Unauthorized +
+ +
+
+
+ Plane AI can now take actions for you. +
+
+ Use Build mode to create work items, cycles and more. Activate now to start Plane AI actions. +
+
+ {instance && !instance?.is_authorized && ( + + Activate Build mode + + )} +
+
+
+ ); +}; diff --git a/apps/web/ee/layouts/workspace-wrapper.tsx b/apps/web/ee/layouts/workspace-wrapper.tsx index ee2b9d6292..4721b80eb1 100644 --- a/apps/web/ee/layouts/workspace-wrapper.tsx +++ b/apps/web/ee/layouts/workspace-wrapper.tsx @@ -31,6 +31,7 @@ import { import { useProjectAdvanced } from "@/plane-web/hooks/store/projects/use-projects"; import { EWorkspaceFeatures } from "@/plane-web/types/workspace-feature"; import { useInitiatives } from "../hooks/store/use-initiatives"; +import { usePiChat } from "../hooks/store/use-pi-chat"; export const WorkspaceAuthWrapper: FC = observer((props) => { // props const { children } = props; @@ -55,6 +56,8 @@ export const WorkspaceAuthWrapper: FC = observer((props) const { fetchAllCustomerPropertiesAndOptions } = useCustomerProperties(); const { isCustomersFeatureEnabled, fetchCustomers } = useCustomers(); const { initiative } = useInitiatives(); + const { getWorkspaceBySlug } = useWorkspace(); + const { getInstance } = usePiChat(); // derived values const isFreeMemberCountExceeded = subscriptionDetail?.is_free_member_count_exceeded; const isWorkspaceSettingsRoute = pathname.includes(`/${workspaceSlug}/settings`); @@ -68,7 +71,7 @@ export const WorkspaceAuthWrapper: FC = observer((props) const isPageTemplatesEnabled = useFlag(workspaceSlug?.toString(), "PAGE_TEMPLATES"); const isInitiativesFeatureEnabled = initiative.isInitiativesFeatureEnabled; const isTemplatePublishEnabled = getIsTemplatePublishEnabled(workspaceSlug.toString()); - + const workspaceId = getWorkspaceBySlug(workspaceSlug as string)?.id; // fetching feature flags const { isLoading: flagsLoader, error: flagsError } = useSWR( workspaceSlug ? `WORKSPACE_FLAGS_${workspaceSlug}` : null, @@ -216,6 +219,12 @@ export const WorkspaceAuthWrapper: FC = observer((props) { revalidateIfStale: false, revalidateOnFocus: false } ); + useSWR(workspaceId ? `PI_STARTER_${workspaceId}` : null, workspaceId ? () => getInstance(workspaceId) : null, { + revalidateOnFocus: false, + revalidateIfStale: false, + errorRetryCount: 0, + }); + // loading state const isLoading = flagsLoader && !flagsError; diff --git a/apps/web/ee/services/pi-chat.service.ts b/apps/web/ee/services/pi-chat.service.ts index 07143dbdd0..9f3127ac22 100644 --- a/apps/web/ee/services/pi-chat.service.ts +++ b/apps/web/ee/services/pi-chat.service.ts @@ -9,7 +9,7 @@ import type { TFeedback, TQuery, TSearchQuery, - TTemplate, + TInstanceResponse, TUserThreads, TAiModels, TInitPayload, @@ -23,9 +23,6 @@ import type { TPiAttachment, } from "../types"; -type TTemplateResponse = { - templates: TTemplate[]; -}; type TChatHistoryResponse = { results: { chat_id: string; @@ -123,11 +120,11 @@ export class PiChatService extends APIService { return r; } - // fetch templates - async listTemplates(workspaceId: string | undefined): Promise { - return this.get(`/api/v1/chat/get-templates/`, { + // fetch instance + async getInstance(workspaceId: string): Promise { + return this.get(`/api/v1/chat/start/`, { params: { - ...(workspaceId && { workspace_id: workspaceId }), + workspace_id: workspaceId, }, }) .then((response) => response?.data) diff --git a/apps/web/ee/store/pi-chat/pi-chat.ts b/apps/web/ee/store/pi-chat/pi-chat.ts index 3706233df1..732beb4744 100644 --- a/apps/web/ee/store/pi-chat/pi-chat.ts +++ b/apps/web/ee/store/pi-chat/pi-chat.ts @@ -16,10 +16,10 @@ import type { TFocus, TInitPayload, TQuery, - TTemplate, TUserThreads, TArtifact, TUpdatedArtifact, + TInstanceResponse, } from "@/plane-web/types"; import { ESource, EExecutionStatus } from "@/plane-web/types"; import { ArtifactsStore } from "./artifacts"; @@ -36,6 +36,7 @@ export interface IPiChatStore { activeModel: TAiModels | undefined; models: TAiModels[]; isAuthorized: boolean; + isWorkspaceAuthorized: boolean; favoriteChats: string[]; isLoading: boolean; isPiTyping: boolean; @@ -62,7 +63,7 @@ export interface IPiChatStore { callbackUrl: string, attachmentIds: string[] ) => Promise; - getTemplates: (workspaceId: string | undefined) => Promise; + getInstance: (workspaceId: string) => Promise; fetchUserThreads: (workspaceId: string | undefined, isProjectChat: boolean) => void; searchCallback: (workspace: string, query: string, focus: TFocus) => Promise; sendFeedback: ( @@ -120,6 +121,7 @@ export class PiChatStore implements IPiChatStore { models: TAiModels[] = []; activeModel: TAiModels | undefined = undefined; isAuthorized = true; + isWorkspaceAuthorized = true; chatMap: Record = {}; piThreads: string[] = []; projectThreads: string[] = []; @@ -151,6 +153,7 @@ export class PiChatStore implements IPiChatStore { isPiTypingMap: observable, isLoadingMap: observable, isAuthorized: observable, + isWorkspaceAuthorized: observable, isLoadingThreads: observable, favoriteChats: observable, isPiChatDrawerOpen: observable.ref, @@ -163,7 +166,7 @@ export class PiChatStore implements IPiChatStore { // actions initPiChat: action, getAnswer: action, - getTemplates: action, + getInstance: action, fetchUserThreads: action, searchCallback: action, sendFeedback: action, @@ -355,9 +358,10 @@ export class PiChatStore implements IPiChatStore { return payload; }; - getTemplates = async (workspaceId: string | undefined) => { - const response = await this.piChatService.listTemplates(workspaceId); - return response?.templates; + getInstance = async (workspaceId: string): Promise => { + const response = await this.piChatService.getInstance(workspaceId); + this.isWorkspaceAuthorized = response.is_authorized; + return response; }; getStreamingAnswer = async ( diff --git a/apps/web/ee/types/pi-chat.d.ts b/apps/web/ee/types/pi-chat.d.ts index c69a2d7cd1..8c193c06dd 100644 --- a/apps/web/ee/types/pi-chat.d.ts +++ b/apps/web/ee/types/pi-chat.d.ts @@ -252,5 +252,15 @@ export type TPiAttachmentIdMap = { [chatId: string]: string[]; }; +export type TInstanceResponse = + | { + is_authorized: true; + templates: TTemplate[]; + } + | { + is_authorized: false; + oauth_url: string; + }; + // constants export const EDITABLE_ARTIFACT_TYPES = ["workitem", "epic", "page"];