From a690116399e6889a4e19c9882d5f1b58c686b225 Mon Sep 17 00:00:00 2001 From: lorsan Date: Tue, 20 Jan 2026 17:06:06 +0300 Subject: [PATCH] Add websckets connection --- .github/workflows/deploy.yml | 0 backend/app/auth/router.py | 6 +- backend/app/chats/dao.py | 100 ++++++++++ backend/app/chats/models.py | 33 +++- backend/app/chats/router.py | 44 +++++ backend/app/chats/schemas.py | 67 +++++++ backend/app/chats/service.py | 183 ++++++++++++++++++ backend/app/main.py | 8 + backend/app/migration/env.py | 2 + .../versions/0d3f7039ba77_create_tables.py | 101 ++++++++++ .../versions/7a5ccb6859fe_edit_chat_tables.py | 32 +++ .../versions/7ad624ae1699_edit_chat_tables.py | 32 +++ .../versions/fd15ec3ae3fb_add_chat_tables.py | 53 +++++ backend/app/users/router.py | 4 + backend/app/users/schemas.py | 3 + backend/app/utils/OAuth2WithCookie.py | 17 +- backend/poetry.lock | 136 ++++++------- backend/pyproject.toml | 10 +- ...-compose.dev.yml => docker-compose.dev.yml | 0 19 files changed, 748 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 backend/app/chats/dao.py create mode 100644 backend/app/chats/schemas.py create mode 100644 backend/app/chats/service.py create mode 100644 backend/app/migration/versions/0d3f7039ba77_create_tables.py create mode 100644 backend/app/migration/versions/7a5ccb6859fe_edit_chat_tables.py create mode 100644 backend/app/migration/versions/7ad624ae1699_edit_chat_tables.py create mode 100644 backend/app/migration/versions/fd15ec3ae3fb_add_chat_tables.py rename backend/docker-compose.dev.yml => docker-compose.dev.yml (100%) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/auth/router.py b/backend/app/auth/router.py index 857bbdf..10a3f75 100644 --- a/backend/app/auth/router.py +++ b/backend/app/auth/router.py @@ -46,13 +46,15 @@ async def login(response: Response, credentials: OAuth2PasswordRequestForm = Dep 'access_token', token.access_token, max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60, - httponly=True + httponly=True, + samesite='lax' ) response.set_cookie( 'refresh_token', str(token.refresh_token), max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 30 * 24 * 60, - httponly=True + httponly=True, + samesite='lax' ) return token diff --git a/backend/app/chats/dao.py b/backend/app/chats/dao.py new file mode 100644 index 0000000..b807440 --- /dev/null +++ b/backend/app/chats/dao.py @@ -0,0 +1,100 @@ +import uuid +from typing import Optional, List + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import func, select, and_ + +from app.core.dao import BaseDAO +from app.chats.models import ChatModel, MessageModel, ParticipantModel +from app.chats.schemas import ChatCreateDB, MessageCreateDB, ParticipantCreateDB +from app.chats.schemas import ChatUpdateDB, MessageUpdateDB, ParticipantUpdateDB +from app.users.models import UserModel + + +class ChatDAO(BaseDAO[ChatModel, ChatCreateDB, ChatUpdateDB]): + model = ChatModel + + @classmethod + async def get_chat_id(cls, session: AsyncSession, user_a_id: int, user_b_id: int) -> Optional[uuid.UUID]: + stmt = ( + select(ParticipantModel.chat_id) + .join(ChatModel) + .where(ChatModel.is_group == False) + .where(ParticipantModel.user_id.in_([user_a_id, user_b_id])) + .group_by(ParticipantModel.chat_id) + .having(func.count(ParticipantModel.chat_id) == 2) + ) + result = await session.execute(stmt) + return result.scalar_one_or_none() + + + @classmethod + async def get_chats(cls, session: AsyncSession, user_id: int, offset: int = 0, limit: int = 10): + + stmt = ( + select( + ChatModel.id.label("chat_id"), + ChatModel.last_message, + UserModel.id.label("user_id"), + UserModel.avatar_url, + UserModel.display_name + ) + .join(ParticipantModel, ParticipantModel.chat_id==ChatModel.id) + .join(UserModel, UserModel.id==ParticipantModel.user_id) + .where( + ChatModel.id.in_( + select(ParticipantModel.chat_id).where(ParticipantModel.user_id==user_id) + ) + ) + .where(UserModel.id!=user_id) + .where(ChatModel.is_group==False) + .order_by(ChatModel.updated_at.desc()) + .limit(limit) + .offset(offset) + ) + result = await session.execute(stmt) + return result.mappings().all() + + + @classmethod + async def get_chat_with_participant(cls, session: AsyncSession, chat_id: uuid.UUID, user_id: int): + stmt = ( + select(ChatModel, ParticipantModel.id.label("participant_id")) + .outerjoin( + ParticipantModel, + and_( + ParticipantModel.chat_id==ChatModel.id, + ParticipantModel.user_id==user_id + ) + ) + .where(ChatModel.id==chat_id) + ) + result = await session.execute(stmt) + return result.first() + + +class MessageDAO(BaseDAO[MessageModel, MessageCreateDB, MessageUpdateDB]): + model = MessageModel + + @classmethod + async def find_all_asc( + cls, + session: AsyncSession, + offset: Optional[int], + limit: Optional[int], + *filter, + **filter_by + ) -> List[MessageModel]: + stmt = select(MessageModel).filter(*filter).filter_by(**filter_by).order_by(MessageModel.created_at.asc()) + + if offset is not None: + stmt = stmt.offset(offset) + if limit is not None: + stmt = stmt.limit(limit) + + result = await session.execute(stmt) + return result.scalars().all() + + +class ParticipantDAO(BaseDAO[ParticipantModel, ParticipantCreateDB, ParticipantUpdateDB]): + model = ParticipantModel \ No newline at end of file diff --git a/backend/app/chats/models.py b/backend/app/chats/models.py index 9cc3c19..ec66624 100644 --- a/backend/app/chats/models.py +++ b/backend/app/chats/models.py @@ -1,5 +1,7 @@ +import uuid + from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy import ForeignKey +from sqlalchemy import ForeignKey, UUID, UniqueConstraint from app.core.database import Base @@ -7,7 +9,28 @@ from app.core.database import Base class MessageModel(Base): __tablename__ = "message" - id: Mapped[int] = mapped_column(primary_key=True, index=True) - sender_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), index=True) - recipient_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), index=True) - content: Mapped[str] = mapped_column() \ No newline at end of file + id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, index=True, default=uuid.uuid4) + sender_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="SET NULL"), index=True) + chat_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey("chat.id", ondelete="CASCADE"), index=True) + content: Mapped[str] = mapped_column() + is_read: Mapped[bool] = mapped_column(default=False) + + +class ChatModel(Base): + __tablename__ = "chat" + + id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, index=True, default=uuid.uuid4) + is_group: Mapped[bool] = mapped_column(default=False) + last_message: Mapped[str] = mapped_column(nullable=True) + + +class ParticipantModel(Base): + __tablename__ = "Participant" + + id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, index=True, default=uuid.uuid4) + chat_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey("chat.id", ondelete="CASCADE"), index=True) + user_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), index=True) + + __table_args__ = ( + UniqueConstraint("chat_id", "user_id", name="uq_chat_user"), + ) \ No newline at end of file diff --git a/backend/app/chats/router.py b/backend/app/chats/router.py index e69de29..adbd733 100644 --- a/backend/app/chats/router.py +++ b/backend/app/chats/router.py @@ -0,0 +1,44 @@ +import uuid +from typing import List + +from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect + +from app.chats.service import ChatService +from app.auth.dependencies import get_current_verified_user +from app.users.models import UserModel +from app.chats.schemas import Chat, MessageCreate, Message + +router = APIRouter(prefix="/chats", tags=["chats"]) + +@router.get("/") +async def get_chats( + offset: int = 0, + limit: int = 10, + user: UserModel = Depends(get_current_verified_user) +) -> List[Chat]: + return await ChatService.get_chats(user, offset, limit) + +@router.get("/{chat_id}") +async def get_chat( + chat_id: uuid.UUID, + offset: int = 0, + limit: int = 10, + user: UserModel = Depends(get_current_verified_user) +) -> List[Message]: + return await ChatService.get_chat(chat_id, user, offset, limit) + +@router.post("/message") +async def send_message(message: MessageCreate, user: UserModel = Depends(get_current_verified_user)) -> Message: + return await ChatService.send_message(user, message) + + +@router.websocket("/ws") +async def websocket_endpoint(ws: WebSocket, user: UserModel = Depends(get_current_verified_user)): + await ws.accept() + await ChatService.save_websocket(user, ws) + + try: + while True: + await ws.receive_text() + except WebSocketDisconnect: + await ChatService.delete_websocket(user) \ No newline at end of file diff --git a/backend/app/chats/schemas.py b/backend/app/chats/schemas.py new file mode 100644 index 0000000..b47ce92 --- /dev/null +++ b/backend/app/chats/schemas.py @@ -0,0 +1,67 @@ +from datetime import datetime +from typing import Optional +import uuid + +from pydantic import BaseModel + + +class MessageCreate(BaseModel): + recipient_id: Optional[int] = None + chat_id: Optional[uuid.UUID] = None + content: str + + +class MessageUpdate(BaseModel): + id: uuid.UUID + content: str + + +class MessageCreateDB(BaseModel): + sender_id: Optional[int] + chat_id: Optional[uuid.UUID] + content: Optional[str] + is_read: Optional[bool] = False + + +class MessageUpdateDB(BaseModel): + content: Optional[str] + + +class Message(BaseModel): + id: uuid.UUID + sender_id: int + chat_id: uuid.UUID + content: str + created_at: datetime + updated_at: datetime + + +class ChatBase(BaseModel): + is_group: Optional[bool] = False + last_message: Optional[str] = None + + +class ChatCreateDB(ChatBase): + pass + + +class ChatUpdateDB(ChatBase): + pass + + +class Chat(BaseModel): + chat_id: uuid.UUID + user_id: int + last_message: Optional[str] + avatar_url: Optional[str] + display_name: str + + +class ParticipantCreateDB(BaseModel): + chat_id: Optional[uuid.UUID] + user_id: Optional[int] + + +class ParticipantUpdateDB(BaseModel): + chat_id: Optional[uuid.UUID] + user_id: Optional[int] \ No newline at end of file diff --git a/backend/app/chats/service.py b/backend/app/chats/service.py new file mode 100644 index 0000000..ed43429 --- /dev/null +++ b/backend/app/chats/service.py @@ -0,0 +1,183 @@ +import json +import uuid +from typing import List, Dict +import logging + +from fastapi import HTTPException, status, WebSocket +from sqlalchemy import and_ + +from app.core.database import async_session_maker +from app.chats.dao import ChatDAO, MessageDAO, ParticipantDAO +from app.chats.models import ChatModel, MessageModel, ParticipantModel +from app.chats.schemas import Chat, MessageCreate, MessageCreateDB, ChatCreateDB, ParticipantCreateDB, Message +from app.users.models import UserModel +from app.core.redis import get_redis + +log = logging.getLogger(__name__) + + +class ChatService: + active_connections: Dict[str, WebSocket] = {} + + @classmethod + async def get_chats(cls, user: UserModel, offset: int, limit: int) -> List[Chat]: + log.debug("Getting chats", extra={"user_id": user.id, "offset": offset, "limit": limit}) + async with async_session_maker() as session: + chats = await ChatDAO.get_chats(session, user.id, offset, limit) + log.debug("Retrieved chats", extra={"user_id": user.id, "count": len(chats)}) + return chats + + + @classmethod + async def send_message(cls, sender: UserModel, message: MessageCreate) -> Message: + log.info("Sending message", extra={"sender_id": sender.id, "chat_id": message.chat_id, "recipient_id": message.recipient_id}) + async with async_session_maker() as session: + target_chat_id = message.chat_id + + if target_chat_id is None: + if message.recipient_id is None: + log.warning("Message send failed: missing chat_id and recipient_id", extra={"sender_id": sender.id}) + raise HTTPException(status.HTTP_400_BAD_REQUEST, "Need chat_id or user_id") + + target_chat_id = await ChatDAO.get_chat_id(session, sender.id, message.recipient_id) + + if target_chat_id is None: + log.info("Creating new chat", extra={"sender_id": sender.id, "recipient_id": message.recipient_id}) + target_chat_db = await ChatDAO.add( + session, + obj_in=ChatCreateDB( + is_group=False, + last_message=message.content + ) + ) + target_chat_id: uuid.UUID = target_chat_db.id + + await ParticipantDAO.add( + session, + obj_in=ParticipantCreateDB( + user_id=sender.id, + chat_id=target_chat_id + ) + ) + await ParticipantDAO.add( + session, + obj_in=ParticipantCreateDB( + user_id=message.recipient_id, + chat_id=target_chat_id + ) + ) + log.info("Created new chat", extra={"chat_id": target_chat_id, "sender_id": sender.id, "recipient_id": message.recipient_id}) + + + members = await ParticipantDAO.find_all( + session, + None, + None, + ParticipantModel.chat_id==target_chat_id + ) + + members_ids = [member.user_id for member in members] + + if not sender.id in members_ids : + log.warning("Access denied to chat", extra={"user_id": sender.id, "chat_id": message.chat_id}) + raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Access denied") + + + message_db = await MessageDAO.add( + session, + obj_in=MessageCreateDB( + sender_id=sender.id, + chat_id=target_chat_id, + content=message.content + ) + ) + + await cls._send_ws_message(members_ids, Message( + id=message_db.id, + sender_id=message_db.sender_id, + chat_id=message_db.chat_id, + content=message_db.content, + created_at=message_db.created_at, + updated_at=message_db.updated_at + )) + + await ChatDAO.update( + session, + ChatModel.id==target_chat_id, + obj_in={"last_message": message.content} + ) + await session.commit() + log.info("Message sent", extra={"message_id": message_db.id, "sender_id": sender.id, "chat_id": target_chat_id}) + return message_db + + + @classmethod + async def get_chat(cls, chat_id: uuid.UUID, user: UserModel, offset: int = 0, limit: int = 0) -> List[Message]: + log.debug("Getting chat messages", extra={"user_id": user.id, "chat_id": chat_id, "offset": offset, "limit": limit}) + async with async_session_maker() as session: + chat_exist = await ChatDAO.get_chat_with_participant(session, chat_id, user.id) + + if chat_exist is None: + log.warning("Chat not found", extra={"user_id": user.id, "chat_id": chat_id}) + raise HTTPException(status.HTTP_404_NOT_FOUND, "Chat not found") + if chat_exist.participant_id is None: + log.warning("Access denied to chat", extra={"user_id": user.id, "chat_id": chat_id}) + raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Access denied") + + messages = await MessageDAO.find_all_asc( + session, + offset, + limit, + MessageModel.chat_id==chat_id + ) + + log.debug("Retrieved chat messages", extra={"user_id": user.id, "chat_id": chat_id, "count": len(messages)}) + return messages + + + @classmethod + async def save_websocket(cls, user: UserModel, ws: WebSocket): + cls.active_connections[str(user.id)] = ws + log.info("WebSocket connection saved", extra={"user_id": user.id, "active_connections": len(cls.active_connections) + 1}) + + + + @classmethod + async def delete_websocket(cls, user: UserModel): + cls.active_connections.pop(str(user.id)) + log.info("WebSocket connection deleted", extra={"user_id": user.id, "active_connections": len(cls.active_connections) - 1}) + + + @classmethod + async def message_listener(cls): + redis_client = await get_redis() + + pubsub = redis_client.pubsub() + + await pubsub.subscribe("messenger_updates") + + async for message in pubsub.listen(): + log.debug(f"Received message from Redis: {message}") + if message["type"] == "message": + payload = json.loads(message["data"]) + user_id = payload["user_id"] + + if user_id in cls.active_connections: + ws = cls.active_connections[user_id] + await ws.send_json(payload["message"]) + log.info(f"Message sent to user {user_id} via WebSocket") + else: + log.debug(f"User {user_id} not connected") + + + + @classmethod + async def _send_ws_message(cls, user_ids: List[int], message: Message): + redis_client = await get_redis() + for user_id in user_ids: + payload = { + "user_id": str(user_id), + "message": message.model_dump(mode='json') + } + await redis_client.publish("messenger_updates", json.dumps(payload)) + log.debug(f"Published message for user_id: {user_id}") \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index f3236d9..c1c52b7 100755 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,6 +3,7 @@ from contextlib import asynccontextmanager import uvicorn import logging import uuid +import asyncio from fastapi import FastAPI, APIRouter, Request, Response from fastapi.middleware.cors import CORSMiddleware @@ -10,6 +11,8 @@ from fastapi.middleware.cors import CORSMiddleware from app.core.redis import close_redis, init_redis from app.users.router import router as user_router from app.auth.router import router as auth_router +from app.chats.router import router as chat_router +from app.chats.service import ChatService from app.core.log_config import set_logging from app.core.config import settings @@ -21,14 +24,19 @@ log = logging.getLogger(__name__) async def lifespan(app: FastAPI): await init_redis() log.info("Redis connected") + task_send_message = asyncio.create_task(ChatService.message_listener()) + log.info("Message sender started") yield await close_redis() log.info("Redis disconnected") + task_send_message.cancel() + log.info("Message sender stopped") api_router = APIRouter(prefix="/api/v1") api_router.include_router(user_router) api_router.include_router(auth_router) +api_router.include_router(chat_router) @api_router.get("/health") async def test_health(): diff --git a/backend/app/migration/env.py b/backend/app/migration/env.py index 6c8c18e..e56bfd3 100755 --- a/backend/app/migration/env.py +++ b/backend/app/migration/env.py @@ -9,6 +9,8 @@ from alembic import context from app.core.database import Base from app.core.config import settings +from app.users.models import UserModel +from app.chats.models import MessageModel, ChatModel, ParticipantModel # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/backend/app/migration/versions/0d3f7039ba77_create_tables.py b/backend/app/migration/versions/0d3f7039ba77_create_tables.py new file mode 100644 index 0000000..c341157 --- /dev/null +++ b/backend/app/migration/versions/0d3f7039ba77_create_tables.py @@ -0,0 +1,101 @@ +"""Create tables + +Revision ID: 0d3f7039ba77 +Revises: 7ad624ae1699 +Create Date: 2026-01-12 15:51:43.453822 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0d3f7039ba77' +down_revision: Union[str, Sequence[str], None] = '7ad624ae1699' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('chat', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('is_group', sa.Boolean(), nullable=False), + sa.Column('last_message', sa.String(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('chat_pkey')) + ) + op.create_index(op.f('chat_id_idx'), 'chat', ['id'], unique=False) + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('display_name', sa.String(), nullable=False), + sa.Column('username', sa.String(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.Column('birth_day', sa.DATE(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.Column('avatar_url', sa.String(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('is_verified', sa.Boolean(), nullable=False), + sa.Column('is_superuser', sa.Boolean(), nullable=False), + sa.Column('hashed_password', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('user_pkey')) + ) + op.create_index(op.f('user_email_idx'), 'user', ['email'], unique=True) + op.create_index(op.f('user_id_idx'), 'user', ['id'], unique=False) + op.create_index(op.f('user_username_idx'), 'user', ['username'], unique=True) + op.create_table('Participant', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('chat_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['chat_id'], ['chat.id'], name=op.f('Participant_chat_id_fkey'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('Participant_user_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('Participant_pkey')), + sa.UniqueConstraint('chat_id', 'user_id', name='uq_chat_user') + ) + op.create_index(op.f('Participant_chat_id_idx'), 'Participant', ['chat_id'], unique=False) + op.create_index(op.f('Participant_id_idx'), 'Participant', ['id'], unique=False) + op.create_index(op.f('Participant_user_id_idx'), 'Participant', ['user_id'], unique=False) + op.create_table('message', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('sender_id', sa.Integer(), nullable=False), + sa.Column('chat_id', sa.UUID(), nullable=False), + sa.Column('content', sa.String(), nullable=False), + sa.Column('is_read', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['chat_id'], ['chat.id'], name=op.f('message_chat_id_fkey'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['sender_id'], ['user.id'], name=op.f('message_sender_id_fkey'), ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name=op.f('message_pkey')) + ) + op.create_index(op.f('message_chat_id_idx'), 'message', ['chat_id'], unique=False) + op.create_index(op.f('message_id_idx'), 'message', ['id'], unique=False) + op.create_index(op.f('message_sender_id_idx'), 'message', ['sender_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('message_sender_id_idx'), table_name='message') + op.drop_index(op.f('message_id_idx'), table_name='message') + op.drop_index(op.f('message_chat_id_idx'), table_name='message') + op.drop_table('message') + op.drop_index(op.f('Participant_user_id_idx'), table_name='Participant') + op.drop_index(op.f('Participant_id_idx'), table_name='Participant') + op.drop_index(op.f('Participant_chat_id_idx'), table_name='Participant') + op.drop_table('Participant') + op.drop_index(op.f('user_username_idx'), table_name='user') + op.drop_index(op.f('user_id_idx'), table_name='user') + op.drop_index(op.f('user_email_idx'), table_name='user') + op.drop_table('user') + op.drop_index(op.f('chat_id_idx'), table_name='chat') + op.drop_table('chat') + # ### end Alembic commands ### diff --git a/backend/app/migration/versions/7a5ccb6859fe_edit_chat_tables.py b/backend/app/migration/versions/7a5ccb6859fe_edit_chat_tables.py new file mode 100644 index 0000000..52afa55 --- /dev/null +++ b/backend/app/migration/versions/7a5ccb6859fe_edit_chat_tables.py @@ -0,0 +1,32 @@ +"""EDIT: chat tables + +Revision ID: 7a5ccb6859fe +Revises: fd15ec3ae3fb +Create Date: 2026-01-12 14:51:11.514074 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7a5ccb6859fe' +down_revision: Union[str, Sequence[str], None] = 'fd15ec3ae3fb' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/app/migration/versions/7ad624ae1699_edit_chat_tables.py b/backend/app/migration/versions/7ad624ae1699_edit_chat_tables.py new file mode 100644 index 0000000..6e991a5 --- /dev/null +++ b/backend/app/migration/versions/7ad624ae1699_edit_chat_tables.py @@ -0,0 +1,32 @@ +"""EDIT: chat tables + +Revision ID: 7ad624ae1699 +Revises: 7a5ccb6859fe +Create Date: 2026-01-12 14:54:04.459361 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7ad624ae1699' +down_revision: Union[str, Sequence[str], None] = '7a5ccb6859fe' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/app/migration/versions/fd15ec3ae3fb_add_chat_tables.py b/backend/app/migration/versions/fd15ec3ae3fb_add_chat_tables.py new file mode 100644 index 0000000..db6730c --- /dev/null +++ b/backend/app/migration/versions/fd15ec3ae3fb_add_chat_tables.py @@ -0,0 +1,53 @@ +"""ADD: chat tables + +Revision ID: fd15ec3ae3fb +Revises: 4d00c9b0516e +Create Date: 2026-01-11 21:54:51.418126 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'fd15ec3ae3fb' +down_revision: Union[str, Sequence[str], None] = '4d00c9b0516e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('user_email_idx'), table_name='user') + op.drop_index(op.f('user_id_idx'), table_name='user') + op.drop_index(op.f('user_username_idx'), table_name='user') + op.drop_table('user') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('display_name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('username', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('birth_day', sa.DATE(), autoincrement=False, nullable=True), + sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('avatar_url', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column('is_verified', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column('is_superuser', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('updated_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=False), + sa.Column('hashed_password', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('user_pkey')) + ) + op.create_index(op.f('user_username_idx'), 'user', ['username'], unique=True) + op.create_index(op.f('user_id_idx'), 'user', ['id'], unique=False) + op.create_index(op.f('user_email_idx'), 'user', ['email'], unique=True) + # ### end Alembic commands ### diff --git a/backend/app/users/router.py b/backend/app/users/router.py index 02a747d..43a63ff 100755 --- a/backend/app/users/router.py +++ b/backend/app/users/router.py @@ -24,6 +24,10 @@ async def get_users(offset: int, limit: int, user: UserModel = Depends(get_curre log.info("Getting users list", extra={"offset": offset, "limit": limit}) return await UserService.get_users_list(offset=offset, limit=limit) +@router.get("/{user_id}") +async def get_user(user_id: int, user: UserModel = Depends(get_current_verified_user)): + return await UserService.get_user(user_id) + @router.put("/me") async def update_current_user(update_user: UserUpdate, user: UserModel = Depends(get_current_verified_user)) -> User: return await UserService.update_user(user.id, update_user) diff --git a/backend/app/users/schemas.py b/backend/app/users/schemas.py index aa76308..352f8b5 100644 --- a/backend/app/users/schemas.py +++ b/backend/app/users/schemas.py @@ -40,6 +40,7 @@ class User(UserBase): is_verified: bool is_superuser: bool + class UserCreateDB(UserBase): email: Optional[str] = None hashed_password: Optional[str] = None @@ -47,6 +48,7 @@ class UserCreateDB(UserBase): is_verified: Optional[bool] = None is_superuser: Optional[bool] = None + class UserUpdateDB(UserBase): email: Optional[str] = None hashed_password: Optional[str] = None @@ -56,6 +58,7 @@ class UserUpdateDB(UserBase): is_verified: Optional[bool] = None is_superuser: Optional[bool] = None + class ChangePassword(BaseModel): old_password: str new_password: str diff --git a/backend/app/utils/OAuth2WithCookie.py b/backend/app/utils/OAuth2WithCookie.py index da858c3..40f9764 100644 --- a/backend/app/utils/OAuth2WithCookie.py +++ b/backend/app/utils/OAuth2WithCookie.py @@ -1,6 +1,6 @@ from typing import Dict, Optional -from fastapi import HTTPException, Request, status +from fastapi import HTTPException, Request, status, WebSocket from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel from fastapi.security import OAuth2 from fastapi.security.utils import get_authorization_scheme_param @@ -20,8 +20,19 @@ class OAuth2PasswordBearerWithCookie(OAuth2): password={"tokenUrl": tokenUrl, "scopes": scopes}) super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error) - async def __call__(self, request: Request) -> Optional[str]: - authorization: str = request.cookies.get("access_token") + + async def __call__( + self, + request: Request = None, + websocket: WebSocket = None + ) -> Optional[str]: + connection = request or websocket + + if connection is None: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No connection found") + + authorization: str = connection.cookies.get("access_token") + print(authorization) scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": diff --git a/backend/poetry.lock b/backend/poetry.lock index f796d67..86a9e34 100755 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -3504,81 +3504,73 @@ files = [ [[package]] name = "websockets" -version = "15.0.1" +version = "16.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, - {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, - {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, - {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, - {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, - {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, - {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, - {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, - {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, - {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, - {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, - {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, - {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, + {file = "websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a"}, + {file = "websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0"}, + {file = "websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957"}, + {file = "websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72"}, + {file = "websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde"}, + {file = "websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3"}, + {file = "websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3"}, + {file = "websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9"}, + {file = "websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35"}, + {file = "websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8"}, + {file = "websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad"}, + {file = "websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d"}, + {file = "websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe"}, + {file = "websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b"}, + {file = "websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5"}, + {file = "websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64"}, + {file = "websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6"}, + {file = "websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac"}, + {file = "websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00"}, + {file = "websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79"}, + {file = "websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39"}, + {file = "websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c"}, + {file = "websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f"}, + {file = "websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1"}, + {file = "websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2"}, + {file = "websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89"}, + {file = "websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea"}, + {file = "websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9"}, + {file = "websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230"}, + {file = "websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c"}, + {file = "websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5"}, + {file = "websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82"}, + {file = "websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8"}, + {file = "websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f"}, + {file = "websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a"}, + {file = "websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156"}, + {file = "websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0"}, + {file = "websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904"}, + {file = "websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4"}, + {file = "websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e"}, + {file = "websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4"}, + {file = "websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1"}, + {file = "websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3"}, + {file = "websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8"}, + {file = "websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d"}, + {file = "websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244"}, + {file = "websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e"}, + {file = "websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641"}, + {file = "websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8"}, + {file = "websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e"}, + {file = "websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944"}, + {file = "websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206"}, + {file = "websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6"}, + {file = "websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd"}, + {file = "websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d"}, + {file = "websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03"}, + {file = "websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da"}, + {file = "websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c"}, + {file = "websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767"}, + {file = "websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec"}, + {file = "websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5"}, ] [[package]] @@ -3849,4 +3841,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = ">=3.13" -content-hash = "0251ef60f483d739512c9ae09a00d85e239cc079667acae2be68da9a632a0ed5" +content-hash = "0bc3ba3c9bfed0b76ac6aa34c14882ea972d982a889c657d5c39d5c317df1775" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 6562c2d..e724d34 100755 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -21,10 +21,18 @@ dependencies = [ "bcrypt (<4.1)", "pytest (>=9.0.2,<10.0.0)", "aiobotocore (>=3.1.0,<4.0.0)", - "types-aiobotocore[essential] (>=3.1.0,<4.0.0)" + "types-aiobotocore[essential] (>=3.1.0,<4.0.0)", + "websockets (>=16.0,<17.0)" ] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "aetherbackend" +version = "0.1.0" +description = "" +authors = ["stasstrochewskij@gmail.com"] +package-mode = false # <--- Добавь это diff --git a/backend/docker-compose.dev.yml b/docker-compose.dev.yml similarity index 100% rename from backend/docker-compose.dev.yml rename to docker-compose.dev.yml