diff --git a/surfsense_backend/app/routes/podcasts_routes.py b/surfsense_backend/app/routes/podcasts_routes.py index 467ef8d23..27970b707 100644 --- a/surfsense_backend/app/routes/podcasts_routes.py +++ b/surfsense_backend/app/routes/podcasts_routes.py @@ -84,12 +84,17 @@ async def read_podcasts( async def read_podcast( podcast_id: int, session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), + user: User | None = Depends(current_optional_user), ): """ Get a specific podcast by ID. - Requires PODCASTS_READ permission for the search space. + + Access is allowed if: + - User is authenticated with PODCASTS_READ permission, OR + - Podcast belongs to a publicly shared thread """ + from app.services.public_chat_service import is_podcast_publicly_accessible + try: result = await session.execute(select(Podcast).filter(Podcast.id == podcast_id)) podcast = result.scalars().first() @@ -100,14 +105,18 @@ async def read_podcast( detail="Podcast not found", ) - # Check permission for the search space - await check_permission( - session, - user, - podcast.search_space_id, - Permission.PODCASTS_READ.value, - "You don't have permission to read podcasts in this search space", - ) + is_public = await is_podcast_publicly_accessible(session, podcast_id) + + if not is_public: + if not user: + raise HTTPException(status_code=401, detail="Authentication required") + await check_permission( + session, + user, + podcast.search_space_id, + Permission.PODCASTS_READ.value, + "You don't have permission to read podcasts in this search space", + ) return podcast except HTTPException as he: diff --git a/surfsense_backend/app/schemas/new_chat.py b/surfsense_backend/app/schemas/new_chat.py index ef2868495..5e9d44beb 100644 --- a/surfsense_backend/app/schemas/new_chat.py +++ b/surfsense_backend/app/schemas/new_chat.py @@ -96,6 +96,7 @@ class NewChatThreadRead(NewChatThreadBase, IDModel): visibility: ChatVisibility created_by_id: UUID | None = None public_share_enabled: bool = False + public_share_token: str | None = None created_at: datetime updated_at: datetime diff --git a/surfsense_backend/app/services/public_chat_service.py b/surfsense_backend/app/services/public_chat_service.py index 42a26c403..62fd4f923 100644 --- a/surfsense_backend/app/services/public_chat_service.py +++ b/surfsense_backend/app/services/public_chat_service.py @@ -27,8 +27,8 @@ def strip_citations(text: str) -> str: Remove [citation:X] and [citation:doc-X] patterns from text. Preserves newlines to maintain markdown formatting. """ - # Remove citation patterns (including Chinese brackets 【】) - text = re.sub(r"[\[【]citation:(doc-)?\d+[\]】]", "", text) + # Remove citation patterns + text = re.sub(r"[\[【]\u200B?citation:(doc-)?\d+\u200B?[\]】]", "", text) # Collapse multiple spaces/tabs (but NOT newlines) into single space text = re.sub(r"[^\S\n]+", " ", text) # Normalize excessive blank lines (3+ newlines → 2) @@ -63,8 +63,17 @@ def sanitize_content_for_public(content: list | str | None) -> list: sanitized.append({"type": "text", "text": clean_text}) elif part_type == "tool-call": - if part.get("toolName") in UI_TOOLS: - sanitized.append(part) + tool_name = part.get("toolName") + if tool_name not in UI_TOOLS: + continue + + # Skip podcasts that are still processing (would cause auth errors) + if tool_name == "generate_podcast": + result = part.get("result", {}) + if result.get("status") in ("processing", "already_generating"): + continue + + sanitized.append(part) return sanitized diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 2af50f8e2..9b45d4d62 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -355,7 +355,7 @@ export default function NewChatPage() { hasComments: currentThread?.has_comments ?? false, addingCommentToMessageId: null, publicShareEnabled: currentThread?.public_share_enabled ?? false, - publicShareToken: null, + publicShareToken: currentThread?.public_share_token ?? null, }); }, [currentThread, setCurrentThreadState]); diff --git a/surfsense_web/components/new-chat/chat-share-button.tsx b/surfsense_web/components/new-chat/chat-share-button.tsx index 4e811779f..2df363203 100644 --- a/surfsense_web/components/new-chat/chat-share-button.tsx +++ b/surfsense_web/components/new-chat/chat-share-button.tsx @@ -245,17 +245,24 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS

{isPublicEnabled && publicShareToken && ( - + )} diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts index a87d4deaf..b14818ac1 100644 --- a/surfsense_web/lib/apis/base-api.service.ts +++ b/surfsense_web/lib/apis/base-api.service.ts @@ -26,7 +26,7 @@ class BaseApiService { noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"]; // Prefixes that don't require auth (checked with startsWith) - noAuthPrefixes: string[] = ["/api/v1/public/"]; + noAuthPrefixes: string[] = ["/api/v1/public/", "/api/v1/podcasts/"]; // Use a getter to always read fresh token from localStorage // This ensures the token is always up-to-date after login/logout diff --git a/surfsense_web/lib/chat/thread-persistence.ts b/surfsense_web/lib/chat/thread-persistence.ts index 6990ff582..2188d9cec 100644 --- a/surfsense_web/lib/chat/thread-persistence.ts +++ b/surfsense_web/lib/chat/thread-persistence.ts @@ -25,6 +25,7 @@ export interface ThreadRecord { updated_at: string; has_comments?: boolean; public_share_enabled?: boolean; + public_share_token?: string | null; } export interface MessageRecord {