From 0b4bb19425a9e8a71d6e94b98cb5ae021b441aaa Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 02:34:24 +0300 Subject: [PATCH] feat(chat): add random public_id and fix users blocked route - add chats.public_id random identifier with migration 0011 - expose public_id in chat API payloads - use chat public_id in message search UI label - fix users router order so /users/blocked no longer conflicts with /users/{user_id} --- alembic/versions/0011_chat_public_id.py | 43 +++++++++++++++++++++++++ app/chats/models.py | 2 ++ app/chats/schemas.py | 1 + app/chats/service.py | 2 ++ app/users/router.py | 16 ++++----- app/utils/id_generator.py | 9 ++++++ web/src/chat/types.ts | 1 + web/src/pages/ChatsPage.tsx | 10 ++++-- 8 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 alembic/versions/0011_chat_public_id.py create mode 100644 app/utils/id_generator.py diff --git a/alembic/versions/0011_chat_public_id.py b/alembic/versions/0011_chat_public_id.py new file mode 100644 index 0000000..e42c9cb --- /dev/null +++ b/alembic/versions/0011_chat_public_id.py @@ -0,0 +1,43 @@ +"""add chats public_id + +Revision ID: 0011_chat_public_id +Revises: 0010_blocked_users +Create Date: 2026-03-08 15:00:00.000000 +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +from app.utils.id_generator import generate_public_id + + +revision: str = "0011_chat_public_id" +down_revision: Union[str, Sequence[str], None] = "0010_blocked_users" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("chats", sa.Column("public_id", sa.String(length=24), nullable=True)) + conn = op.get_bind() + rows = list(conn.execute(sa.text("SELECT id FROM chats"))) + used: set[str] = set() + for row in rows: + chat_id = row[0] + public_id = generate_public_id() + while public_id in used: + public_id = generate_public_id() + used.add(public_id) + conn.execute( + sa.text("UPDATE chats SET public_id = :public_id WHERE id = :id"), + {"public_id": public_id, "id": chat_id}, + ) + op.alter_column("chats", "public_id", existing_type=sa.String(length=24), nullable=False) + op.create_index(op.f("ix_chats_public_id"), "chats", ["public_id"], unique=True) + + +def downgrade() -> None: + op.drop_index(op.f("ix_chats_public_id"), table_name="chats") + op.drop_column("chats", "public_id") diff --git a/app/chats/models.py b/app/chats/models.py index 2b937f0..40c54ce 100644 --- a/app/chats/models.py +++ b/app/chats/models.py @@ -6,6 +6,7 @@ from sqlalchemy import Boolean, DateTime, Enum as SAEnum, ForeignKey, String, Un from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.base import Base +from app.utils.id_generator import generate_public_id if TYPE_CHECKING: from app.messages.models import Message @@ -28,6 +29,7 @@ class Chat(Base): __tablename__ = "chats" id: Mapped[int] = mapped_column(primary_key=True, index=True) + public_id: Mapped[str] = mapped_column(String(24), unique=True, index=True, nullable=False, default=generate_public_id) type: Mapped[ChatType] = mapped_column(SAEnum(ChatType), nullable=False, index=True) title: Mapped[str | None] = mapped_column(String(255), nullable=True) handle: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True, index=True) diff --git a/app/chats/schemas.py b/app/chats/schemas.py index 4d3ea77..287a67e 100644 --- a/app/chats/schemas.py +++ b/app/chats/schemas.py @@ -9,6 +9,7 @@ class ChatRead(BaseModel): model_config = ConfigDict(from_attributes=True) id: int + public_id: str type: ChatType title: str | None = None display_title: str | None = None diff --git a/app/chats/service.py b/app/chats/service.py index 59f5515..0cbdb4f 100644 --- a/app/chats/service.py +++ b/app/chats/service.py @@ -66,6 +66,7 @@ async def serialize_chat_for_user(db: AsyncSession, *, user_id: int, chat: Chat) return ChatRead.model_validate( { "id": chat.id, + "public_id": chat.public_id, "type": chat.type, "title": chat.title, "display_title": display_title, @@ -375,6 +376,7 @@ async def discover_public_chats_for_user(db: AsyncSession, *, user_id: int, quer ChatDiscoverRead.model_validate( { "id": chat.id, + "public_id": chat.public_id, "type": chat.type, "title": chat.title, "handle": chat.handle, diff --git a/app/users/router.py b/app/users/router.py index 0fef359..ccd9179 100644 --- a/app/users/router.py +++ b/app/users/router.py @@ -41,14 +41,6 @@ async def search_users( return users -@router.get("/{user_id}", response_model=UserRead) -async def read_user(user_id: int, db: AsyncSession = Depends(get_db), _current_user: User = Depends(get_current_user)) -> UserRead: - user = await get_user_by_id(db, user_id) - if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") - return user - - @router.put("/profile", response_model=UserRead) async def update_profile( payload: UserProfileUpdate, @@ -102,3 +94,11 @@ async def unblock_user_endpoint( if user_id == current_user.id: return await unblock_user(db, user_id=current_user.id, blocked_user_id=user_id) + + +@router.get("/{user_id}", response_model=UserRead) +async def read_user(user_id: int, db: AsyncSession = Depends(get_db), _current_user: User = Depends(get_current_user)) -> UserRead: + user = await get_user_by_id(db, user_id) + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + return user diff --git a/app/utils/id_generator.py b/app/utils/id_generator.py new file mode 100644 index 0000000..dbcf452 --- /dev/null +++ b/app/utils/id_generator.py @@ -0,0 +1,9 @@ +import secrets +import string + + +_ALPHABET = string.ascii_letters + string.digits + + +def generate_public_id(length: int = 12) -> str: + return "".join(secrets.choice(_ALPHABET) for _ in range(length)) diff --git a/web/src/chat/types.ts b/web/src/chat/types.ts index 4049fc0..0d270d8 100644 --- a/web/src/chat/types.ts +++ b/web/src/chat/types.ts @@ -4,6 +4,7 @@ export type DeliveryStatus = "sending" | "sent" | "delivered" | "read"; export interface Chat { id: number; + public_id: string; type: ChatType; title: string | null; display_title?: string | null; diff --git a/web/src/pages/ChatsPage.tsx b/web/src/pages/ChatsPage.tsx index a350c80..317f5d4 100644 --- a/web/src/pages/ChatsPage.tsx +++ b/web/src/pages/ChatsPage.tsx @@ -138,7 +138,10 @@ export function ChatsPage() {

Nothing found

) : null}
- {searchResults.map((message) => ( + {searchResults.map((message) => { + const chatMeta = chats.find((chat) => chat.id === message.chat_id); + const chatLabel = chatMeta?.public_id ?? String(message.chat_id); + return ( - ))} + ); + })}