feat: use frontend URL for public share links

This commit is contained in:
CREDO23 2026-02-02 15:36:59 +02:00
parent 0bcd7505fb
commit 3821630404
3 changed files with 76 additions and 14 deletions

View file

@ -739,7 +739,6 @@ async def update_thread_visibility(
@router.post("/threads/{thread_id}/snapshots", response_model=SnapshotCreateResponse) @router.post("/threads/{thread_id}/snapshots", response_model=SnapshotCreateResponse)
async def create_thread_snapshot( async def create_thread_snapshot(
thread_id: int, thread_id: int,
request: Request,
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user), user: User = Depends(current_active_user),
): ):
@ -747,23 +746,19 @@ async def create_thread_snapshot(
Create a public snapshot of the thread. Create a public snapshot of the thread.
Returns existing snapshot URL if content unchanged (deduplication). Returns existing snapshot URL if content unchanged (deduplication).
Only the thread owner can create snapshots.
""" """
from app.services.public_chat_service import create_snapshot from app.services.public_chat_service import create_snapshot
base_url = str(request.base_url).rstrip("/")
return await create_snapshot( return await create_snapshot(
session=session, session=session,
thread_id=thread_id, thread_id=thread_id,
user=user, user=user,
base_url=base_url,
) )
@router.get("/threads/{thread_id}/snapshots", response_model=SnapshotListResponse) @router.get("/threads/{thread_id}/snapshots", response_model=SnapshotListResponse)
async def list_thread_snapshots( async def list_thread_snapshots(
thread_id: int, thread_id: int,
request: Request,
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user), user: User = Depends(current_active_user),
): ):
@ -774,13 +769,11 @@ async def list_thread_snapshots(
""" """
from app.services.public_chat_service import list_snapshots_for_thread from app.services.public_chat_service import list_snapshots_for_thread
base_url = str(request.base_url).rstrip("/")
return SnapshotListResponse( return SnapshotListResponse(
snapshots=await list_snapshots_for_thread( snapshots=await list_snapshots_for_thread(
session=session, session=session,
thread_id=thread_id, thread_id=thread_id,
user=user, user=user,
base_url=base_url,
) )
) )

View file

@ -236,6 +236,24 @@ class SnapshotListResponse(BaseModel):
snapshots: list[SnapshotInfo] snapshots: list[SnapshotInfo]
class SearchSpaceSnapshotInfo(BaseModel):
"""Snapshot info with thread context for search space listing."""
id: int
share_token: str
public_url: str
created_at: datetime
message_count: int
thread_id: int
thread_title: str
class SearchSpaceSnapshotListResponse(BaseModel):
"""List of all snapshots in a search space."""
snapshots: list[SearchSpaceSnapshotInfo]
# ============================================================================= # =============================================================================
# Public Chat View Schemas (for unauthenticated access) # Public Chat View Schemas (for unauthenticated access)
# ============================================================================= # =============================================================================

View file

@ -161,7 +161,6 @@ async def create_snapshot(
session: AsyncSession, session: AsyncSession,
thread_id: int, thread_id: int,
user: User, user: User,
base_url: str,
) -> dict: ) -> dict:
""" """
Create a public snapshot of a chat thread. Create a public snapshot of a chat thread.
@ -169,6 +168,9 @@ async def create_snapshot(
Returns existing snapshot if content unchanged (same hash). Returns existing snapshot if content unchanged (same hash).
Returns new snapshot with unique URL if content changed. Returns new snapshot with unique URL if content changed.
""" """
from app.config import config
frontend_url = (config.NEXT_FRONTEND_URL or "").rstrip("/")
result = await session.execute( result = await session.execute(
select(NewChatThread) select(NewChatThread)
.options(selectinload(NewChatThread.messages)) .options(selectinload(NewChatThread.messages))
@ -250,7 +252,7 @@ async def create_snapshot(
return { return {
"snapshot_id": existing.id, "snapshot_id": existing.id,
"share_token": existing.share_token, "share_token": existing.share_token,
"public_url": f"{base_url}/public/{existing.share_token}", "public_url": f"{frontend_url}/public/{existing.share_token}",
"is_new": False, "is_new": False,
} }
@ -283,7 +285,7 @@ async def create_snapshot(
return { return {
"snapshot_id": snapshot.id, "snapshot_id": snapshot.id,
"share_token": snapshot.share_token, "share_token": snapshot.share_token,
"public_url": f"{base_url}/public/{snapshot.share_token}", "public_url": f"{frontend_url}/public/{snapshot.share_token}",
"is_new": True, "is_new": True,
} }
@ -352,10 +354,10 @@ async def list_snapshots_for_thread(
session: AsyncSession, session: AsyncSession,
thread_id: int, thread_id: int,
user: User, user: User,
base_url: str,
) -> list[dict]: ) -> list[dict]:
"""List all public snapshots for a thread.""" """List all public snapshots for a thread."""
# Verify ownership from app.config import config
result = await session.execute( result = await session.execute(
select(NewChatThread).filter(NewChatThread.id == thread_id) select(NewChatThread).filter(NewChatThread.id == thread_id)
) )
@ -370,7 +372,6 @@ async def list_snapshots_for_thread(
detail="Only the creator can view snapshots", detail="Only the creator can view snapshots",
) )
# Get snapshots
result = await session.execute( result = await session.execute(
select(PublicChatSnapshot) select(PublicChatSnapshot)
.filter(PublicChatSnapshot.thread_id == thread_id) .filter(PublicChatSnapshot.thread_id == thread_id)
@ -378,11 +379,13 @@ async def list_snapshots_for_thread(
) )
snapshots = result.scalars().all() snapshots = result.scalars().all()
frontend_url = (config.NEXT_FRONTEND_URL or "").rstrip("/")
return [ return [
{ {
"id": s.id, "id": s.id,
"share_token": s.share_token, "share_token": s.share_token,
"public_url": f"{base_url}/public/{s.share_token}", "public_url": f"{frontend_url}/public/{s.share_token}",
"created_at": s.created_at.isoformat() if s.created_at else None, "created_at": s.created_at.isoformat() if s.created_at else None,
"message_count": len(s.message_ids) if s.message_ids else 0, "message_count": len(s.message_ids) if s.message_ids else 0,
} }
@ -390,6 +393,54 @@ async def list_snapshots_for_thread(
] ]
async def list_snapshots_for_search_space(
session: AsyncSession,
search_space_id: int,
user: User,
) -> list[dict]:
"""List all public snapshots for a search space."""
from app.config import config
await check_permission(
session,
user,
search_space_id,
Permission.PUBLIC_SHARING_VIEW.value,
"You don't have permission to view public share links",
)
result = await session.execute(
select(PublicChatSnapshot)
.join(NewChatThread, PublicChatSnapshot.thread_id == NewChatThread.id)
.filter(NewChatThread.search_space_id == search_space_id)
.order_by(PublicChatSnapshot.created_at.desc())
)
snapshots = result.scalars().all()
snapshot_thread_ids = [s.thread_id for s in snapshots]
thread_result = await session.execute(
select(NewChatThread.id, NewChatThread.title).filter(
NewChatThread.id.in_(snapshot_thread_ids)
)
)
thread_titles = {row[0]: row[1] for row in thread_result.fetchall()}
frontend_url = (config.NEXT_FRONTEND_URL or "").rstrip("/")
return [
{
"id": s.id,
"share_token": s.share_token,
"public_url": f"{frontend_url}/public/{s.share_token}",
"created_at": s.created_at.isoformat() if s.created_at else None,
"message_count": len(s.message_ids) if s.message_ids else 0,
"thread_id": s.thread_id,
"thread_title": thread_titles.get(s.thread_id, "Untitled"),
}
for s in snapshots
]
# ============================================================================= # =============================================================================
# Snapshot Deletion # Snapshot Deletion
# ============================================================================= # =============================================================================