feat: enable public access for podcasts in shared chats

This commit is contained in:
CREDO23 2026-01-26 15:56:49 +02:00
parent 7017a14107
commit aeb0deb21e
3 changed files with 50 additions and 19 deletions

View file

@ -25,7 +25,7 @@ from app.db import (
get_async_session, get_async_session,
) )
from app.schemas import PodcastRead from app.schemas import PodcastRead
from app.users import current_active_user from app.users import current_active_user, current_optional_user
from app.utils.rbac import check_permission from app.utils.rbac import check_permission
router = APIRouter() router = APIRouter()
@ -161,46 +161,49 @@ async def delete_podcast(
async def stream_podcast( async def stream_podcast(
podcast_id: int, podcast_id: int,
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user), user: User | None = Depends(current_optional_user),
): ):
""" """
Stream a podcast audio file. Stream a podcast audio file.
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
Note: Both /stream and /audio endpoints are supported for compatibility. Note: Both /stream and /audio endpoints are supported for compatibility.
""" """
from app.services.public_chat_service import is_podcast_publicly_accessible
try: try:
result = await session.execute(select(Podcast).filter(Podcast.id == podcast_id)) result = await session.execute(select(Podcast).filter(Podcast.id == podcast_id))
podcast = result.scalars().first() podcast = result.scalars().first()
if not podcast: if not podcast:
raise HTTPException( raise HTTPException(status_code=404, detail="Podcast not found")
status_code=404,
detail="Podcast not found", 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 access podcasts in this search space",
) )
# 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 access podcasts in this search space",
)
# Get the file path
file_path = podcast.file_location file_path = podcast.file_location
# Check if the file exists
if not file_path or not os.path.isfile(file_path): if not file_path or not os.path.isfile(file_path):
raise HTTPException(status_code=404, detail="Podcast audio file not found") raise HTTPException(status_code=404, detail="Podcast audio file not found")
# Define a generator function to stream the file
def iterfile(): def iterfile():
with open(file_path, mode="rb") as file_like: with open(file_path, mode="rb") as file_like:
yield from file_like yield from file_like
# Return a streaming response with appropriate headers
return StreamingResponse( return StreamingResponse(
iterfile(), iterfile(),
media_type="audio/mpeg", media_type="audio/mpeg",

View file

@ -289,6 +289,7 @@ async def clone_public_chat(
session, session,
old_podcast_id, old_podcast_id,
target_search_space_id, target_search_space_id,
new_thread.id,
) )
if new_podcast_id: if new_podcast_id:
podcast_id_map[old_podcast_id] = new_podcast_id podcast_id_map[old_podcast_id] = new_podcast_id
@ -331,6 +332,7 @@ async def _clone_podcast(
session: AsyncSession, session: AsyncSession,
podcast_id: int, podcast_id: int,
target_search_space_id: int, target_search_space_id: int,
target_thread_id: int,
) -> int | None: ) -> int | None:
"""Clone a podcast record and its audio file.""" """Clone a podcast record and its audio file."""
import shutil import shutil
@ -359,6 +361,7 @@ async def _clone_podcast(
podcast_transcript=original.podcast_transcript, podcast_transcript=original.podcast_transcript,
file_location=new_file_path, file_location=new_file_path,
search_space_id=target_search_space_id, search_space_id=target_search_space_id,
thread_id=target_thread_id,
) )
session.add(new_podcast) session.add(new_podcast)
await session.flush() await session.flush()
@ -412,3 +415,27 @@ async def _create_clone_failure_notification(
) )
session.add(notification) session.add(notification)
await session.commit() await session.commit()
async def is_podcast_publicly_accessible(
session: AsyncSession,
podcast_id: int,
) -> bool:
"""
Check if a podcast belongs to a publicly shared thread.
Uses the thread_id foreign key for efficient lookup.
"""
from app.db import Podcast
result = await session.execute(
select(Podcast)
.options(selectinload(Podcast.thread))
.filter(Podcast.id == podcast_id)
)
podcast = result.scalars().first()
if not podcast or not podcast.thread:
return False
return podcast.thread.public_share_enabled

View file

@ -229,3 +229,4 @@ auth_backend = AuthenticationBackend(
fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend]) fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend])
current_active_user = fastapi_users.current_user(active=True) current_active_user = fastapi_users.current_user(active=True)
current_optional_user = fastapi_users.current_user(active=True, optional=True)