feat(chat): share a chat by link — anonymous read-only /shared/<token> page, share/unshare

This commit is contained in:
2026-08-30 01:34:44 -04:00
parent ece93a7c8f
commit 114b115034
28 changed files with 3442 additions and 54 deletions
@@ -0,0 +1,45 @@
"""saved_chats.share_token: the anonymous share link (phase 51)
Revision ID: 0009
Revises: 0008
Create Date: 2026-08-29
Phase 51 (share-a-chat-by-link, owner-locked 2026-08-29): a saved chat
can be turned into a public link — ``/shared/<token>`` — that anyone
with the URL can read anonymously (no admin session); unsharing sets
the token back to NULL and revokes the link.
* ``saved_chats.share_token`` — ``UUID`` (a 128-bit ``uuid4``), NULL =
not shared. One column, no new table (A10 extension unchanged —
sharing reuses the already-stored row).
* ``ix_saved_chats_share_token`` — UNIQUE index on the column. A unique
index on a NULLable column: Postgres treats NULLs as distinct, so any
number of unshared chats coexist while two identical non-NULL tokens
can never exist (the ``git_sources.path`` house precedent, phase 38).
"""
from __future__ import annotations
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision = "0009"
down_revision = "0008"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"saved_chats",
sa.Column("share_token", postgresql.UUID(as_uuid=True), nullable=True),
)
op.create_index(
"ix_saved_chats_share_token", "saved_chats", ["share_token"], unique=True
)
def downgrade() -> None:
op.drop_index("ix_saved_chats_share_token", table_name="saved_chats")
op.drop_column("saved_chats", "share_token")