diff --git a/Back/README.md b/Back/README.md index af97574..f338d37 100644 --- a/Back/README.md +++ b/Back/README.md @@ -64,8 +64,16 @@ REDIS_URL=redis://redis:6379/0 LIVEKIT_API_KEY=neda_key LIVEKIT_API_SECRET=neda_secret LIVEKIT_HOST=http://livekit:7880 +LIVEKIT_PORT=7880 +LIVEKIT_TCP_PORT=7881 +LIVEKIT_UDP_PORT=7882 ``` +LiveKit note: +- `livekit.yaml.template` is a template, not a direct runtime config. +- The `livekit` container generates `/etc/livekit.yaml` from env values at startup. +- Raw YAML does not automatically resolve `${...}` placeholders unless templated first. + ## Run With Docker ```bash diff --git a/Back/alembic/versions/24e07dd1307e_feat_add_unique_constraint_to_.py b/Back/alembic/versions/24e07dd1307e_feat_add_unique_constraint_to_.py deleted file mode 100644 index 7ce576a..0000000 --- a/Back/alembic/versions/24e07dd1307e_feat_add_unique_constraint_to_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""feat: add unique constraint to GroupMember - -Revision ID: 24e07dd1307e -Revises: 4080314c8f5a -Create Date: 2026-03-08 15:57:14.560371 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '24e07dd1307e' -down_revision: Union[str, Sequence[str], None] = '4080314c8f5a' -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_unique_constraint('uq_group_member', 'group_members', ['user_id', 'group_id']) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('uq_group_member', 'group_members', type_='unique') - # ### end Alembic commands ### diff --git a/Back/alembic/versions/4080314c8f5a_add_notifications.py b/Back/alembic/versions/4080314c8f5a_add_notifications.py deleted file mode 100644 index 92205d9..0000000 --- a/Back/alembic/versions/4080314c8f5a_add_notifications.py +++ /dev/null @@ -1,88 +0,0 @@ -"""add notifications - -Revision ID: 4080314c8f5a -Revises: b1f09d977759 -Create Date: 2026-03-07 16:16:30.792790 - -""" -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 = '4080314c8f5a' -down_revision: Union[str, Sequence[str], None] = 'b1f09d977759' -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! ### - group_member_role_enum = postgresql.ENUM( - 'MANAGER', - 'MEMBER', - name='group_member_role' - ) - group_member_role_enum.create(op.get_bind(), checkfirst=True) - - op.create_table('notifications', - sa.Column('title', sa.String(length=200), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('type', sa.Enum('PUBLIC', 'JOIN_REQUEST', name='notification_type'), nullable=False), - sa.Column('is_accepted', sa.Boolean(), nullable=True), - sa.Column('receiver_id', sa.UUID(), nullable=False), - sa.Column('sender_id', sa.UUID(), nullable=True), - sa.Column('group_id', sa.UUID(), nullable=True), - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['receiver_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['sender_id'], ['users.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_notifications_receiver_id'), 'notifications', ['receiver_id'], unique=False) - op.add_column( - 'group_members', - sa.Column( - 'role', - sa.Enum('MANAGER', 'MEMBER', name='group_member_role'), - nullable=False, - server_default='MEMBER' - ) - ) - op.alter_column('group_members', 'role', server_default=None) - op.add_column('users', sa.Column('is_admin', sa.Boolean(), nullable=False, server_default=sa.false())) - op.add_column('users', sa.Column('phone_number', sa.String(length=11), nullable=True)) - op.add_column('users', sa.Column('token_version', sa.Integer(), nullable=False, server_default='1')) - op.alter_column('users', 'is_admin', server_default=None) - op.alter_column('users', 'token_version', server_default=None) - op.drop_index(op.f('ix_users_role'), table_name='users') - op.create_index(op.f('ix_users_is_admin'), 'users', ['is_admin'], unique=False) - op.create_index(op.f('ix_users_phone_number'), 'users', ['phone_number'], unique=True) - op.drop_column('users', 'role') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('users', sa.Column('role', postgresql.ENUM('ADMIN', 'GROUP_MANAGER', 'MEMBER', name='user_role'), autoincrement=False, nullable=False)) - op.drop_index(op.f('ix_users_phone_number'), table_name='users') - op.drop_index(op.f('ix_users_is_admin'), table_name='users') - op.create_index(op.f('ix_users_role'), 'users', ['role'], unique=False) - op.drop_column('users', 'token_version') - op.drop_column('users', 'phone_number') - op.drop_column('users', 'is_admin') - op.drop_column('group_members', 'role') - postgresql.ENUM( - 'MANAGER', - 'MEMBER', - name='group_member_role' - ).drop(op.get_bind(), checkfirst=True) - op.drop_index(op.f('ix_notifications_receiver_id'), table_name='notifications') - op.drop_table('notifications') - # ### end Alembic commands ### diff --git a/Back/alembic/versions/b1f09d977759_change_groups_models.py b/Back/alembic/versions/b1f09d977759_change_groups_models.py deleted file mode 100644 index 114c993..0000000 --- a/Back/alembic/versions/b1f09d977759_change_groups_models.py +++ /dev/null @@ -1,32 +0,0 @@ -"""change groups models - -Revision ID: b1f09d977759 -Revises: 4413cbcf58c3 -Create Date: 2026-03-06 16:21:31.915163 - -""" -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 = 'b1f09d977759' -down_revision: Union[str, Sequence[str], None] = '4413cbcf58c3' -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_column('group_members', 'role') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('group_members', sa.Column('role', postgresql.ENUM('MANAGER', 'MEMBER', name='group_role'), autoincrement=False, nullable=False)) - # ### end Alembic commands ### diff --git a/Back/alembic/versions/4413cbcf58c3_create_db.py b/Back/alembic/versions/b5ace31192c3_initial_database.py similarity index 60% rename from Back/alembic/versions/4413cbcf58c3_create_db.py rename to Back/alembic/versions/b5ace31192c3_initial_database.py index 2189fa3..347a0a3 100644 --- a/Back/alembic/versions/4413cbcf58c3_create_db.py +++ b/Back/alembic/versions/b5ace31192c3_initial_database.py @@ -1,8 +1,8 @@ -"""create db +"""initial database -Revision ID: 4413cbcf58c3 +Revision ID: b5ace31192c3 Revises: -Create Date: 2026-03-06 12:26:21.920942 +Create Date: 2026-03-10 13:32:13.070448 """ from typing import Sequence, Union @@ -12,7 +12,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '4413cbcf58c3' +revision: str = 'b5ace31192c3' down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -23,7 +23,7 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('groups', sa.Column('name', sa.String(length=100), nullable=False), - sa.Column('type', sa.Enum('GROUP', 'DIRECT', name='group_type'), nullable=False), + sa.Column('type', sa.Enum('PUBLIC', 'PRIVATE', name='group_type'), nullable=False), sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column('id', sa.UUID(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), @@ -35,7 +35,9 @@ def upgrade() -> None: op.create_table('users', sa.Column('username', sa.String(length=50), nullable=False), sa.Column('secret_hash', sa.String(length=255), nullable=False), - sa.Column('role', sa.Enum('ADMIN', 'GROUP_MANAGER', 'MEMBER', name='user_role'), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.Column('phone_number', sa.String(length=11), nullable=True), + sa.Column('token_version', sa.Integer(), nullable=False), sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column('id', sa.UUID(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), @@ -43,32 +45,54 @@ def upgrade() -> None: sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_users_is_active'), 'users', ['is_active'], unique=False) - op.create_index(op.f('ix_users_role'), 'users', ['role'], unique=False) + op.create_index(op.f('ix_users_is_admin'), 'users', ['is_admin'], unique=False) + op.create_index(op.f('ix_users_phone_number'), 'users', ['phone_number'], unique=True) op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True) op.create_table('group_members', sa.Column('user_id', sa.UUID(), nullable=False), sa.Column('group_id', sa.UUID(), nullable=False), - sa.Column('role', sa.Enum('MANAGER', 'MEMBER', name='group_role'), nullable=False), + sa.Column('role', sa.Enum('MANAGER', 'MEMBER', name='group_member_role'), nullable=False), sa.Column('id', sa.UUID(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'group_id', name='uq_group_member') ) op.create_index(op.f('ix_group_members_group_id'), 'group_members', ['group_id'], unique=False) op.create_index(op.f('ix_group_members_user_id'), 'group_members', ['user_id'], unique=False) + op.create_table('notifications', + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('type', sa.Enum('PUBLIC', 'JOIN_REQUEST', name='notification_type'), nullable=False), + sa.Column('is_accepted', sa.Boolean(), nullable=True), + sa.Column('receiver_id', sa.UUID(), nullable=False), + sa.Column('sender_id', sa.UUID(), nullable=True), + sa.Column('group_id', sa.UUID(), nullable=True), + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['receiver_id'], ['users.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['sender_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_notifications_receiver_id'), 'notifications', ['receiver_id'], unique=False) # ### end Alembic commands ### def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_notifications_receiver_id'), table_name='notifications') + op.drop_table('notifications') op.drop_index(op.f('ix_group_members_user_id'), table_name='group_members') op.drop_index(op.f('ix_group_members_group_id'), table_name='group_members') op.drop_table('group_members') op.drop_index(op.f('ix_users_username'), table_name='users') - op.drop_index(op.f('ix_users_role'), table_name='users') + op.drop_index(op.f('ix_users_phone_number'), table_name='users') + op.drop_index(op.f('ix_users_is_admin'), table_name='users') op.drop_index(op.f('ix_users_is_active'), table_name='users') op.drop_table('users') op.drop_index(op.f('ix_groups_name'), table_name='groups') diff --git a/Back/core/security.py b/Back/core/security.py index dbd6f1b..7ecf433 100755 --- a/Back/core/security.py +++ b/Back/core/security.py @@ -1,7 +1,7 @@ from passlib.context import CryptContext pwd_context = CryptContext( - schemes=["argon2"], + schemes=["bcrypt"], deprecated="auto", ) diff --git a/Back/domains/admin/service.py b/Back/domains/admin/service.py index e798163..e5980c4 100644 --- a/Back/domains/admin/service.py +++ b/Back/domains/admin/service.py @@ -14,7 +14,9 @@ from core.security import hash_password def generate_user_secret(): - return secrets.token_urlsafe(16) + # return secrets.token_urlsafe(16) + #for test + return "1234" async def admin_create_user( db: AsyncSession, diff --git a/Back/domains/realtime/speaker_service.py b/Back/domains/realtime/speaker_service.py index 089749b..8a5382f 100644 --- a/Back/domains/realtime/speaker_service.py +++ b/Back/domains/realtime/speaker_service.py @@ -27,8 +27,8 @@ async def request_speak( if not group: return None - # direct chat → no speaker lock - if str(group.type) == "direct": + # private chat → no speaker lock + if str(group.type) == "private": token = generate_join_token( user_id=user_id_str, @@ -67,8 +67,8 @@ async def stop_speaking( if not group: return False - # direct chat → nothing to release - if str(group.type) == "direct": + # private chat → nothing to release + if str(group.type) == "private": return True return await release_speaker(group_id_str, user_id_str) @@ -86,7 +86,7 @@ async def current_speaker( if not group: return None - if str(group.type) == "direct": + if str(group.type) == "private": return None return await get_active_speaker(group_id_str) diff --git a/Back/livekit.yaml b/Back/livekit.yaml old mode 100755 new mode 100644 diff --git a/Back/livekit.yaml.template b/Back/livekit.yaml.template new file mode 100644 index 0000000..72efebb --- /dev/null +++ b/Back/livekit.yaml.template @@ -0,0 +1,7 @@ +port: ${LIVEKIT_PORT} +rtc: + udp_port: ${LIVEKIT_UDP_PORT} + tcp_port: ${LIVEKIT_TCP_PORT} + +keys: + ${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET} diff --git a/Back/main.py b/Back/main.py index 79a86da..f2f717e 100644 --- a/Back/main.py +++ b/Back/main.py @@ -8,6 +8,7 @@ from domains.users.api import router as users_router from domains.admin.api import router as admin_router from domains.groups.api import router as groups_router from domains.realtime.ws import router as realtime_router +from domains.notifications.api import router as notifications_router from db.redis import redis_client @@ -58,4 +59,5 @@ app.include_router(auth_router) app.include_router(users_router) app.include_router(admin_router) app.include_router(groups_router) -app.include_router(realtime_router) \ No newline at end of file +app.include_router(realtime_router) +app.include_router(notifications_router) \ No newline at end of file diff --git a/Back/tests/test_health.py b/Back/tests/test_health.py deleted file mode 100644 index e7f6bb9..0000000 --- a/Back/tests/test_health.py +++ /dev/null @@ -1,12 +0,0 @@ -import pytest -from httpx import AsyncClient, ASGITransport - -from main import app - -@pytest.mark.asyncio -async def test_health_check(): - transport = ASGITransport(app=app) - async with AsyncClient(transport=transport, base_url="http://test") as ac: - response = await ac.get("/health") - assert response.status_code == 200 - assert response.json() == {"status": "ok"}