From e54c565a79f917aa1b27c1f59589d929a2398113 Mon Sep 17 00:00:00 2001 From: Giga Date: Tue, 28 May 2024 18:22:14 +0400 Subject: [PATCH 01/19] fix: user_account get_user_account_by_user_id --- apps/server/models/user_account.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/server/models/user_account.py b/apps/server/models/user_account.py index b992599ba..3f1879ac5 100644 --- a/apps/server/models/user_account.py +++ b/apps/server/models/user_account.py @@ -31,7 +31,7 @@ class UserAccountModel(BaseModel): def __repr__(self) -> str: return ( f"User(id={self.id}, " - f"user_id='{self.user_id}', account_id='{self.account_id}', role_id='{self.role_id}')" + f"user_id='{self.user_id}', account_id='{self.account_id}', ')" # TODO add role_id='{self.role_id} ) @classmethod @@ -144,7 +144,29 @@ def get_user_account_by_id(cls, db, user_account_id, account): .first() ) return user_accounts + + @classmethod + def get_user_account_by_user_id(cls, db, user_id): + """_summary_ + + Args: + db (_type_): _description_ + user_id (_type_): _description_ + account (_type_): _description_ + Raises: + UserAccountNotFoundException: _description_ + """ + + user_account = ( + db.session.query(UserAccountModel) + .filter( + UserAccountModel.user_id == user_id, + ) + .first() + ) + return user_account + @classmethod def delete_by_id(cls, db, user_account_id): db_user_account = ( From 5d501279b60912fa36cf9f72c383fb5382a61507 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:28:59 +0400 Subject: [PATCH 02/19] feat: account access exception --- apps/server/exceptions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/server/exceptions.py b/apps/server/exceptions.py index 3bcad8304..a8f8b43fd 100644 --- a/apps/server/exceptions.py +++ b/apps/server/exceptions.py @@ -152,3 +152,11 @@ class TranscriberException(AppBaseException): class SynthesizerException(AppBaseException): pass + + +class UserAccessNotFoundException(DatasourceException): + pass + + +class UserAccountAccessException(AppBaseException): + pass From c529a389773b4e486b29226d3f960bc1e15d2e1b Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:30:40 +0400 Subject: [PATCH 03/19] feat: add account access route --- apps/server/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/server/main.py b/apps/server/main.py index 7666673c3..648db9281 100644 --- a/apps/server/main.py +++ b/apps/server/main.py @@ -29,6 +29,7 @@ from controllers.tool import router as tool_router from controllers.voice import router as voice_router from controllers.workspace import router as workspace_router +from controllers.user_account_access import router as user_account_access_router from models import * # noqa: F403 from models.db import Base, engine # noqa: F401 from resolvers.account import AccountMutation, AccountQuery @@ -133,6 +134,7 @@ def jwt_exception_handler(request: Request, exc: AuthJWTException): app.include_router(integrations_router, prefix="/integrations", include_in_schema=False) app.include_router(fine_tuning_router, prefix="/fine-tuning", include_in_schema=False) app.include_router(voice_router, prefix="/voice", include_in_schema=True) +app.include_router(user_account_access_router, prefix="/user-account-access", include_in_schema=False) @app.get("/", include_in_schema=False) From ea8576167125373c76a22d14e781da6c17e2e9e9 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:31:03 +0400 Subject: [PATCH 04/19] feat: user_account_access table --- ...8e16f34db_add_user_account_access_table.py | 62 +++++++++++++++++++ ...dd_assigned_account_id_in_user_account_.py | 34 ++++++++++ 2 files changed, 96 insertions(+) create mode 100644 apps/server/migrations/versions/29f8e16f34db_add_user_account_access_table.py create mode 100644 apps/server/migrations/versions/f1d5bc37bceb_add_assigned_account_id_in_user_account_.py diff --git a/apps/server/migrations/versions/29f8e16f34db_add_user_account_access_table.py b/apps/server/migrations/versions/29f8e16f34db_add_user_account_access_table.py new file mode 100644 index 000000000..29ec5de7a --- /dev/null +++ b/apps/server/migrations/versions/29f8e16f34db_add_user_account_access_table.py @@ -0,0 +1,62 @@ +"""Add user_account_access table + +Revision ID: 29f8e16f34db +Revises: +Create Date: 2024-05-28 14:27:22.559514 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '29f8e16f34db' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user_account_access', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('assigned_user_id', sa.UUID(), nullable=True), + sa.Column('account_id', sa.UUID(), nullable=True), + sa.Column('is_deleted', sa.Boolean(), nullable=True), + sa.Column('created_by', sa.UUID(), nullable=True), + sa.Column('modified_by', sa.UUID(), nullable=True), + sa.Column('created_on', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_on', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['account_id'], ['account.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['assigned_user_id'], ['user.id'], name='fk_assigned_user_id', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['created_by'], ['user.id'], name='fk_created_by', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['modified_by'], ['user.id'], name='fk_modified_by', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_user_account_access_assigned_user_id'), 'user_account_access', ['assigned_user_id'], unique=False) + op.create_index(op.f('ix_user_account_access_created_by'), 'user_account_access', ['created_by'], unique=False) + op.create_index(op.f('ix_user_account_access_id'), 'user_account_access', ['id'], unique=False) + op.create_index(op.f('ix_user_account_access_is_deleted'), 'user_account_access', ['is_deleted'], unique=False) + op.create_index(op.f('ix_user_account_access_modified_by'), 'user_account_access', ['modified_by'], unique=False) + op.alter_column('run_log', 'toolkit_id', + existing_type=sa.VARCHAR(length=255), + type_=sa.UUID(), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('run_log', 'toolkit_id', + existing_type=sa.UUID(), + type_=sa.VARCHAR(length=255), + existing_nullable=True) + op.drop_index(op.f('ix_user_account_access_modified_by'), table_name='user_account_access') + op.drop_index(op.f('ix_user_account_access_is_deleted'), table_name='user_account_access') + op.drop_index(op.f('ix_user_account_access_id'), table_name='user_account_access') + op.drop_index(op.f('ix_user_account_access_created_by'), table_name='user_account_access') + op.drop_index(op.f('ix_user_account_access_assigned_user_id'), table_name='user_account_access') + op.drop_table('user_account_access') + # ### end Alembic commands ### diff --git a/apps/server/migrations/versions/f1d5bc37bceb_add_assigned_account_id_in_user_account_.py b/apps/server/migrations/versions/f1d5bc37bceb_add_assigned_account_id_in_user_account_.py new file mode 100644 index 000000000..805f17420 --- /dev/null +++ b/apps/server/migrations/versions/f1d5bc37bceb_add_assigned_account_id_in_user_account_.py @@ -0,0 +1,34 @@ +"""Add assigned_account_id in user_account_access table + +Revision ID: f1d5bc37bceb +Revises: 29f8e16f34db +Create Date: 2024-05-28 15:01:07.211417 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f1d5bc37bceb' +down_revision: Union[str, None] = '29f8e16f34db' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user_account_access', sa.Column('assigned_account_id', sa.UUID(), nullable=True)) + op.create_index(op.f('ix_user_account_access_assigned_account_id'), 'user_account_access', ['assigned_account_id'], unique=False) + op.create_foreign_key('fk_assigned_account_id', 'user_account_access', 'account', ['assigned_account_id'], ['id'], ondelete='CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('fk_assigned_account_id', 'user_account_access', type_='foreignkey') + op.drop_index(op.f('ix_user_account_access_assigned_account_id'), table_name='user_account_access') + op.drop_column('user_account_access', 'assigned_account_id') + # ### end Alembic commands ### From 694919222c2906ed64a915dbbbcdf8e9bf2ca2c6 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:31:19 +0400 Subject: [PATCH 05/19] feat: user account_access controller --- .../server/controllers/user_account_access.py | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 apps/server/controllers/user_account_access.py diff --git a/apps/server/controllers/user_account_access.py b/apps/server/controllers/user_account_access.py new file mode 100644 index 000000000..f38683c7d --- /dev/null +++ b/apps/server/controllers/user_account_access.py @@ -0,0 +1,216 @@ +from fastapi_sqlalchemy import db +from fastapi import APIRouter, Depends, HTTPException, status +import services.auth as auth_service +from typings.user import UserInput +from models.user import UserModel +from models.user_account_access import UserAccountAccessModel +from models.user_account import UserAccountModel +from utils.auth import authenticate +from typings.auth import UserAccount +from typings.user_account_access import ( + GetUserAccountAccessOutput, + UserAccountAccessInput, + UserAccountAccessDbInput, + UserAccountAccessOutput, +) +from exceptions import UserAccessNotFoundException +from utils.user_account_access import ( + generate_random_string, + convert_user_access_to_list, + shared_user_access_to_list +) + +router = APIRouter() + + +@router.post("", response_model=UserAccountAccessOutput, status_code=201) +def create_user_account_access( + input: UserAccountAccessInput, + auth: UserAccount = Depends(authenticate) +): + """ + Create a new user_account_access with configurations. + + Args: + user_account_access (UserAccountAccessInput): Data for creating a new user_account_access with configurations. + + Returns: + Message: A message indicating that the user_account_access was successfully created. + """ + try: + body = input.dict() + + existing_user = UserModel.get_user_by_email(db=db, email=body['email']) + + if not existing_user: + random_pass = generate_random_string(8) + print('random_pass', random_pass) + user_input = UserInput(**{ + 'name': body['email'], + 'email': body['email'], + 'account_name': 'Account Name', + 'password': random_pass, + 'avatar': "" + }) + + response = auth_service.register(input=user_input) + create_user_account_access_input = UserAccountAccessDbInput( + assigned_user_id=response['user'].id, + assigned_account_id=response['account'].id + ) + + response = UserAccountAccessModel.create_user_account_access( + db=db, + user_account_access=create_user_account_access_input, + user=auth.user, + account=auth.account + ) + if not response: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="User access not successfully added" + ) + + return { + "success": True, + "message": "User access successfully added" + } + else: + user_account = UserAccountModel.get_user_account_by_user_id( + db=db, + user_id=existing_user.id + ) + + if UserAccountAccessModel.get_user_account_access_assigned( + db=db, + assigner_user_id=user_account.user_id, + assigned_account_id=user_account.account_id, + account=auth.account + ): + return { + 'success': False, + 'message': 'User access already exists' + } + + create_user_account_access_input = UserAccountAccessDbInput( + assigned_user_id=user_account.user_id, + assigned_account_id=user_account.account_id + ) + + response = UserAccountAccessModel.create_user_account_access( + db=db, + user_account_access=create_user_account_access_input, + user=auth.user, + account=auth.account + ) + + if response is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="User access not successfully added" + ) + + return {"success": True, "message": "User access successfully added"} + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + + +@router.get( + "", + response_model=list[GetUserAccountAccessOutput], + status_code=200 +) +def get_user_account_access(auth: UserAccount = Depends(authenticate)): + """ + Get user account access data. + + Args: + auth (UserAccount): User account object obtained from authentication. + + Returns: + UserAccountAccess: User account access data. + """ + try: + data = UserAccountAccessModel.get_user_account_access( + db=db, + account=auth.account, + user=auth.user + ) + + result = convert_user_access_to_list(data) + + return result + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + + +@router.delete( + "/{user_account_access_id}", + response_model=UserAccountAccessOutput, + status_code=201 +) +def delete_user_account_access( + user_account_access_id: str, + auth: UserAccount = Depends(authenticate) +): + """ + Delete user account access data. + + Args: + auth (UserAccount): User account object obtained from authentication. + + Returns: + UserAccountAccess: User account access data. + """ + try: + UserAccountAccessModel.delete_user_account_access_by_id( + db=db, + user_account_access_id=user_account_access_id, + account=auth.account + ) + + return {"success": True, "message": "User access successfully deleted"} + except UserAccessNotFoundException: + raise HTTPException(status_code=404, detail="User access not found") + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + + +@router.get("/access", status_code=200) +def get_shared_user_account_access(auth: UserAccount = Depends(authenticate)): + """_summary_ + + Args: + auth (UserAccount, optional): _description_. Defaults to Depends(authenticate). + + Raises: + HTTPException: _description_ + + Returns: + _type_: _description_ + """ + try: + user_access = UserAccountAccessModel.get_shared_user_account_access( + db=db, + account=auth.account, + user=auth.user + ) + + result = shared_user_access_to_list(user_access) + + return result + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) From dc5561d6f70c6731146979edc800645f73c979d6 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:31:28 +0400 Subject: [PATCH 06/19] feat: user account_access model --- apps/server/models/user_account_access.py | 231 ++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 apps/server/models/user_account_access.py diff --git a/apps/server/models/user_account_access.py b/apps/server/models/user_account_access.py new file mode 100644 index 000000000..9230f5b20 --- /dev/null +++ b/apps/server/models/user_account_access.py @@ -0,0 +1,231 @@ +from models.base_model import BaseModel +from sqlalchemy import UUID, Boolean, Column, ForeignKey, or_ +from sqlalchemy.orm import Session, aliased +import uuid +from typings.user_account_access import UserAccountAccessDbInput +from models.user import UserModel +from models.account import AccountModel +from exceptions import UserAccessNotFoundException + + +class UserAccountAccessModel(BaseModel): + """ + Represents an user_account_access entity. + + Attributes: + id (UUID): Unique identifier of the user_account_access. + is_deleted (bool): Flag indicating if the api_key has been soft-deleted. + assigned_user_id (UUID): ID of the user associated with the user_account_access. + """ + + __tablename__ = "user_account_access" + + id = Column(UUID, primary_key=True, index=True, default=uuid.uuid4) + assigned_user_id = Column( + UUID(as_uuid=True), + ForeignKey("user.id", name="fk_assigned_user_id", ondelete="CASCADE"), + nullable=True, + index=True, + ) + assigned_account_id = Column( + UUID(as_uuid=True), + ForeignKey("account.id", name="fk_assigned_account_id", ondelete="CASCADE"), + nullable=True, + index=True, + ) + account_id = Column( + UUID, ForeignKey("account.id", ondelete="CASCADE"), nullable=True + ) + is_deleted = Column(Boolean, default=False, index=True) + created_by = Column( + UUID, + ForeignKey("user.id", name="fk_created_by", ondelete="CASCADE"), + nullable=True, + index=True, + ) + modified_by = Column( + UUID, + ForeignKey("user.id", name="fk_modified_by", ondelete="CASCADE"), + nullable=True, + index=True, + ) + + def __repr__(self) -> str: + return ( + f"UserAccountAccess(id={self.id}, " + f"is_deleted={self.is_deleted}, account_id={self.account_id})" + ) + + @classmethod + def create_user_account_access( + cls, + db: Session, + user_account_access: UserAccountAccessDbInput, + user, + account + ): + """ + Creates a new user account access. + + Args: + db (Session): SQLAlchemy Session object. + user_account_access (UserAccountAccessModel): _description_ + + Returns: + _type_: _description_ + """ + + db_user_account_access = UserAccountAccessModel( + created_by=user.id, + account_id=account.id, # replace account.id to current_user_account_id + ) + + cls.update_model_from_input( + db_user_account_access, + user_account_access + ) + + db.session.add(db_user_account_access) + db.session.flush() + db.session.commit() + + return db_user_account_access + + @classmethod + def update_model_from_input( + cls, + user_account_access_model: "UserAccountAccessModel", + user_account_access_input: UserAccountAccessDbInput + ): + for field in user_account_access_input.__annotations__.keys(): + if hasattr(user_account_access_model, field): + setattr(user_account_access_model, field, getattr( + user_account_access_input, + field + )) + + @classmethod + def delete_user_account_access_by_id( + cls, + db, + user_account_access_id, + account + ): + user_access = ( + db.session.query(UserAccountAccessModel) + .filter( + UserAccountAccessModel.id == user_account_access_id, + UserAccountAccessModel.account_id == account.id + ) + .first() + ) + + if not user_access or user_access.is_deleted: + raise UserAccessNotFoundException("User access not found") + + user_access.is_deleted = True + db.session.commit() + + @classmethod + def get_user_account_access(cls, db, account, user): + assigned_user = aliased(UserModel) + created_by_user = aliased(UserModel) + user_account_access = ( + db.session.query( + UserAccountAccessModel.id, + UserAccountAccessModel.account_id, + UserAccountAccessModel.assigned_user_id, + UserAccountAccessModel.assigned_account_id, + UserAccountAccessModel.created_by, + UserAccountAccessModel.created_on, + assigned_user.email.label('assigned_user_email'), + assigned_user.name.label('assigned_user_name'), + created_by_user.email.label('created_by_email'), + created_by_user.name.label('created_by_name'), + ) + .join( + assigned_user, + UserAccountAccessModel.assigned_user_id == assigned_user.id + ) + .join( + created_by_user, + UserAccountAccessModel.created_by == created_by_user.id + ) + .filter( + UserAccountAccessModel.created_by == user.id, + or_( + or_( + UserAccountAccessModel.is_deleted.is_(False), + UserAccountAccessModel.is_deleted is None, + ), + UserAccountAccessModel.is_deleted is None, + ), + ) + .all() + ) + + return user_account_access + + @classmethod + def get_shared_user_account_access(cls, db, account, user): + assigned_account = aliased(AccountModel) + created_by_user = aliased(UserModel) + user_account_access = ( + db.session.query( + UserAccountAccessModel.id, + UserAccountAccessModel.account_id, + UserAccountAccessModel.created_by, + UserAccountAccessModel.created_on, + assigned_account.name.label('assigned_account_name'), + created_by_user.email.label('created_by_email'), + created_by_user.name.label('created_by_name'), + ) + .join( + created_by_user, + UserAccountAccessModel.created_by == created_by_user.id + ) + .join( + assigned_account, + UserAccountAccessModel.account_id == assigned_account.id + ) + .filter( + UserAccountAccessModel.assigned_user_id == user.id, + or_( + or_( + UserAccountAccessModel.is_deleted.is_(False), + UserAccountAccessModel.is_deleted is None, + ), + UserAccountAccessModel.is_deleted is None, + ), + ) + .all() + ) + + return user_account_access + + @classmethod + def get_user_account_access_assigned( + cls, + db, + assigner_user_id, + assigned_account_id, + account + ): + user_account_access = ( + db.session.query(UserAccountAccessModel) + .filter( + UserAccountAccessModel.assigned_user_id == assigner_user_id, + UserAccountAccessModel.assigned_account_id == assigned_account_id, + UserAccountAccessModel.account_id == account.id, + or_( + or_( + UserAccountAccessModel.is_deleted.is_(False), + UserAccountAccessModel.is_deleted is None, + ), + UserAccountAccessModel.is_deleted is None, + ) + ) + .first() + ) + + return user_account_access From c8abb8bd87ef203a741f80d5703402e7a17d9193 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:31:36 +0400 Subject: [PATCH 07/19] feat: user account_access types --- apps/server/typings/user_account_access.py | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 apps/server/typings/user_account_access.py diff --git a/apps/server/typings/user_account_access.py b/apps/server/typings/user_account_access.py new file mode 100644 index 000000000..93186eadd --- /dev/null +++ b/apps/server/typings/user_account_access.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel +from uuid import UUID +from typing import List +from datetime import datetime + + +class UserAccountAccessInput(BaseModel): + email: str + + +class UserAccountAccessDbInput(BaseModel): + assigned_user_id: UUID + assigned_account_id: UUID + + +class GetUserAccountAccessOutput(BaseModel): + id: UUID + assigned_user_id: UUID + assigned_account_id: UUID + account_id: UUID + created_by: UUID + created_on: datetime + assigned_user_email: str + assigned_user_name: str + created_by_email: str + created_by_name: str + + +class UserAccountAccessOutput(BaseModel): + success: bool + message: str From 95efc8742a74a718342a07ae65d0ecc3ced1c370 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:31:43 +0400 Subject: [PATCH 08/19] feat: user account_access utils --- apps/server/utils/user_account_access.py | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 apps/server/utils/user_account_access.py diff --git a/apps/server/utils/user_account_access.py b/apps/server/utils/user_account_access.py new file mode 100644 index 000000000..da5da63ca --- /dev/null +++ b/apps/server/utils/user_account_access.py @@ -0,0 +1,76 @@ +import random +import string + + +def generate_random_string(length=8): + if length < 8: + raise ValueError("Length must be at least 8 characters.") + + # Define the character sets + uppercase_letters = string.ascii_uppercase + lowercase_letters = string.ascii_lowercase + digits = string.digits + special_characters = "!@#$%&*" + + # Ensure the random string contains at least one uppercase letter, one number, and one special character + random_string = [ + random.choice(uppercase_letters), + random.choice(digits), + random.choice(special_characters), + ] + + # Fill the rest of the string length with a combination of allowed characters + allowed_characters = lowercase_letters + uppercase_letters + digits + special_characters + random_string += random.choices(allowed_characters, k=length - 3) + + # Shuffle the resulting list to ensure randomness + random.shuffle(random_string) + + # Join the list into a single string + random_string = ''.join(random_string) + + # Check if any forbidden characters are in the string and regenerate if necessary + forbidden_characters = set('?.,`\\[]<>+:;"^()') + while any(char in forbidden_characters for char in random_string): + random_string = ''.join(random.choices(allowed_characters, k=length)) + random_string += random.choice(uppercase_letters) + random_string += random.choice(digits) + random_string += random.choice(special_characters) + random.shuffle(list(random_string)) + random_string = ''.join(random_string[:length]) + + return random_string + + +def convert_user_access_to_list(user_account_access): + result = [] + for row in user_account_access: + result.append({ + 'id': row[0], + 'account_id': row[1], + 'assigned_user_id': row[2], + 'assigned_account_id': row[3], + 'created_by': row[4], + 'created_on': row[5].isoformat(), + 'assigned_user_email': row[6], + 'assigned_user_name': row[7], + 'created_by_email': row[8], + 'created_by_name': row[9] + }) + return result + + +def shared_user_access_to_list(shared_user_access): + result_dict = [ + { + "id": str(row[0]), + "account_id": str(row[1]), + "created_by": str(row[2]), + "created_on": row[3].isoformat(), + "assigned_account_name": row[4], + "created_by_email": row[5], + "created_by_name": row[6], + } + for row in shared_user_access + ] + return result_dict From 7d0ad4e9a232894125b74ddc3cd82d1afd3c0ccb Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:32:13 +0400 Subject: [PATCH 09/19] feat: add invite users route --- apps/ui/src/Route.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/ui/src/Route.tsx b/apps/ui/src/Route.tsx index 51f62bcff..6a0d8550b 100644 --- a/apps/ui/src/Route.tsx +++ b/apps/ui/src/Route.tsx @@ -108,6 +108,7 @@ import CreateScheduleModal from 'modals/CreateScheduleModal' import EditScheduleModal from 'modals/EditScheduleModal' import VoiceOptionsModal from 'modals/VoiceOptionsModal' import LlmSettingsModal from 'modals/LlmSettingsModal' +import { InviteUsers, CreateUserAccess } from 'pages/InviteUsers' const Route = () => { const { loading } = useContext(AuthContext) @@ -396,6 +397,20 @@ const Route = () => { key={document.location.href} /> + + } key={document.location.href}> + } key={document.location.href} /> + } + key={document.location.href} + /> + {/* } + key={document.location.href} + /> */} + {/* } key={document.location.href}> } key={document.location.href} /> */} From 0d3e77ebefa2acfd32b7567114db2d21a299dce3 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:32:39 +0400 Subject: [PATCH 10/19] feat: create/delete/fetch account access gql --- apps/ui/src/gql/inviteUser/createUserAccess.gql | 7 +++++++ apps/ui/src/gql/inviteUser/deleteUserAccess.gql | 13 +++++++++++++ apps/ui/src/gql/inviteUser/getSharedUserAccess.gql | 11 +++++++++++ apps/ui/src/gql/inviteUser/getUserAccess.gql | 14 ++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 apps/ui/src/gql/inviteUser/createUserAccess.gql create mode 100644 apps/ui/src/gql/inviteUser/deleteUserAccess.gql create mode 100644 apps/ui/src/gql/inviteUser/getSharedUserAccess.gql create mode 100644 apps/ui/src/gql/inviteUser/getUserAccess.gql diff --git a/apps/ui/src/gql/inviteUser/createUserAccess.gql b/apps/ui/src/gql/inviteUser/createUserAccess.gql new file mode 100644 index 000000000..fa2efa79d --- /dev/null +++ b/apps/ui/src/gql/inviteUser/createUserAccess.gql @@ -0,0 +1,7 @@ +mutation createUserAccess($input: input!) @api(name: ai) { + createUserAccess(input: $input) + @rest(type: "UserAccountAccess", path: "/user-account-access", method: "POST", bodyKey: "input", endpoint: "ai") { + success + message + } +} diff --git a/apps/ui/src/gql/inviteUser/deleteUserAccess.gql b/apps/ui/src/gql/inviteUser/deleteUserAccess.gql new file mode 100644 index 000000000..aa227e0d5 --- /dev/null +++ b/apps/ui/src/gql/inviteUser/deleteUserAccess.gql @@ -0,0 +1,13 @@ +mutation deleteUserAccess($id: id!) @api(name: ai) { + deleteUserAccess(id: $id) + @rest( + type: "ApiKeys" + path: "/user-account-access/{args.id}" + method: "DELETE" + bodyKey: "input" + endpoint: "ai" + ) { + success + message + } +} diff --git a/apps/ui/src/gql/inviteUser/getSharedUserAccess.gql b/apps/ui/src/gql/inviteUser/getSharedUserAccess.gql new file mode 100644 index 000000000..09c9ee366 --- /dev/null +++ b/apps/ui/src/gql/inviteUser/getSharedUserAccess.gql @@ -0,0 +1,11 @@ +query getSharedUserAccess @api(name: "ai") { + getSharedUserAccess @rest(type: "UserAccountAccess", path: "/user-account-access/access", method: "GET", endpoint: "ai") { + id + account_id + created_by + created_on + assigned_account_name + created_by_email + created_by_name + } +} diff --git a/apps/ui/src/gql/inviteUser/getUserAccess.gql b/apps/ui/src/gql/inviteUser/getUserAccess.gql new file mode 100644 index 000000000..d3023131c --- /dev/null +++ b/apps/ui/src/gql/inviteUser/getUserAccess.gql @@ -0,0 +1,14 @@ +query getUserAccess @api(name: "ai") { + getUserAccess @rest(type: "UserAccountAccess", path: "/user-account-access", method: "GET", endpoint: "ai") { + id + assigned_user_id + assigned_account_id + account_id + created_by + created_on + assigned_user_email + assigned_user_name + created_by_email + created_by_name + } +} From aa3b59dcee6de5bcbfdf3a5b9ba9bdcdcf5cc9a3 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:33:17 +0400 Subject: [PATCH 11/19] feat: add Invite User in avatar dropdown menu --- apps/ui/src/components/AvatarDropDown.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/ui/src/components/AvatarDropDown.tsx b/apps/ui/src/components/AvatarDropDown.tsx index 35c3b4489..e5af23007 100644 --- a/apps/ui/src/components/AvatarDropDown.tsx +++ b/apps/ui/src/components/AvatarDropDown.tsx @@ -72,6 +72,21 @@ const AvatarDropDown = () => { }} /> + navigate('invite-user')}> + + + + Date: Wed, 29 May 2024 18:33:36 +0400 Subject: [PATCH 12/19] fix: removeAccountId from cookies --- apps/ui/src/helpers/authHelper.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/helpers/authHelper.ts b/apps/ui/src/helpers/authHelper.ts index 8669ef5b0..57fd272a2 100644 --- a/apps/ui/src/helpers/authHelper.ts +++ b/apps/ui/src/helpers/authHelper.ts @@ -22,7 +22,10 @@ export const cleanCookie = () => { cookies.remove('account_id') } -export const removeAccountId = () => cookies.remove('account_id') +export const removeAccountId = () => { + cookies.remove('account_id') + cookies.addChangeListener(history.go(0)) +} export const logout = () => { removeAccountId() From 6efc58c56f71cb2c1fb66819daa8da5d330efe0b Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:33:48 +0400 Subject: [PATCH 13/19] fix: account_id --- apps/ui/src/hooks/useApollo.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/ui/src/hooks/useApollo.ts b/apps/ui/src/hooks/useApollo.ts index 4bc4e84c4..2ff8302e0 100644 --- a/apps/ui/src/hooks/useApollo.ts +++ b/apps/ui/src/hooks/useApollo.ts @@ -19,13 +19,13 @@ const locations = ['/login', '/register', '/forgot-password', '/reset-password', const useApollo = () => { const [cookies] = useCookies(['']) // @ts-expect-error TODO: fix cookie types - const { accountId, authorization, 'x-refresh-token': refreshToken } = cookies + const { account_id, authorization, 'x-refresh-token': refreshToken } = cookies let authConfig: any = { // credentials: 'include', headers: { 'Content-Type': 'application/json', - account_id: accountId, + account_id: account_id, }, } @@ -33,10 +33,12 @@ const useApollo = () => { headers: { // 'x-refresh-token': refreshToken, authorization: `Bearer ${authorization}`, - account_id: accountId, + account_id: account_id, }, } + // 'b3834015-eb0e-4b04-9fcc-6a1de3bc4a5c' + const apollo = React.useMemo( () => { const logout = async () => { From a632a950a0046acd58eeb04616d15619b0638419 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:34:21 +0400 Subject: [PATCH 14/19] feat: column configs invite user page --- .../ui/src/pages/InviteUsers/columnConfig.tsx | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 apps/ui/src/pages/InviteUsers/columnConfig.tsx diff --git a/apps/ui/src/pages/InviteUsers/columnConfig.tsx b/apps/ui/src/pages/InviteUsers/columnConfig.tsx new file mode 100644 index 000000000..3d6984747 --- /dev/null +++ b/apps/ui/src/pages/InviteUsers/columnConfig.tsx @@ -0,0 +1,126 @@ +import moment from 'moment' +import styled from 'styled-components' +import IconButton from 'share-ui/components/IconButton/IconButton' +import { + StyledDeleteIcon, +} from 'pages/TeamOfAgents/TeamOfAgentsCard/TeamOfAgentsCard' +import Loader from 'share-ui/components/Loader/Loader' +import Checkbox from 'share-ui/components/Checkbox/Checkbox' + +interface RendererColumnProps { + deleteUserAccess: (id: string) => void + deleting_loading: { + loading: boolean + id: string + } +} + +interface ColumnRowProps { + id: string + assigned_user_name: string + assigned_user_email: string + created_by_email: string + created_on: Date +} + +// eslint-disable-next-line react/prop-types +const DateRenderer: React.FC<{ value: Date }> = ({ value }) => { + const formattedDate = moment(value).fromNow() + return {formattedDate} +} + +const base = [ + { + Header: 'User Name', + accessor: 'assigned_user_name', + minWidth: 100, + width: 150, + }, + { + Header: 'User email', + accessor: 'assigned_user_email', + minWidth: 300, + width: 350, + }, + { + Header: 'Creator', + accessor: 'created_by_email', + minWidth: 250, + width: 300, + }, + { + Header: 'Created On', + accessor: 'created_on', + minWidth: 100, + width: 140, + Cell: DateRenderer, + }, +] + +export const renderColumns = ({ deleteUserAccess, deleting_loading }: RendererColumnProps) => ([ + ...base, + { + Header: 'Actions', + accessor: 'actions', + minWidth: 100, + width: 130, + + Cell: ({ row: { original: data } }: { row: { original: ColumnRowProps } }) => ( + + deleteUserAccess(data.id)} + icon={() => deleting_loading.loading && deleting_loading.id === data.id ? : } + size={IconButton.sizes?.SMALL} + kind={IconButton.kinds?.TERTIARY} + ariaLabel='Delete' + /> + + ), + }, +]) + +interface SharedColumnProps { + selected_account_id: string | null + handleSelectAccess: (account_id: string) => void +} +export const renderSharedColumns = ({ selected_account_id, handleSelectAccess }: SharedColumnProps) => ([ + { + Header: 'Select account', + accessor: 'actions', + minWidth: 100, + width: 130, + + Cell: ({ row: { original: data } }: any) => ( + 0 && selectedTables?.length !== data?.length} + checked={data.account_id === selected_account_id} + size='large' + kind='secondary' + onChange={() => handleSelectAccess(data.account_id)} + /> + ), + }, + { + Header: 'Account Name', + accessor: 'assigned_account_name', + minWidth: 100, + width: 150, + }, + ...base.slice(2, base.length), +]) + + + + +const StyledActionWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + + .components-IconButton-IconButton-module__iconButtonContainer--ttuRB { + &:hover { + background: ${({ theme }) => theme.body.humanMessageBgColor}; + border-radius: 50%; + } + } +` \ No newline at end of file From 1204ab9753236cf0526c581aa36221d9bde8e117 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:34:54 +0400 Subject: [PATCH 15/19] feat: invite user page --- .../pages/InviteUsers/CreateUserAccess.tsx | 73 ++++++++++++ apps/ui/src/pages/InviteUsers/InviteUsers.tsx | 50 ++++++++ apps/ui/src/pages/InviteUsers/index.ts | 2 + .../src/pages/InviteUsers/useInviteUsers.ts | 84 ++++++++++++++ .../inviteUser/useInviteUserService.ts | 107 ++++++++++++++++++ 5 files changed, 316 insertions(+) create mode 100644 apps/ui/src/pages/InviteUsers/CreateUserAccess.tsx create mode 100644 apps/ui/src/pages/InviteUsers/InviteUsers.tsx create mode 100644 apps/ui/src/pages/InviteUsers/index.ts create mode 100644 apps/ui/src/pages/InviteUsers/useInviteUsers.ts create mode 100644 apps/ui/src/services/inviteUser/useInviteUserService.ts diff --git a/apps/ui/src/pages/InviteUsers/CreateUserAccess.tsx b/apps/ui/src/pages/InviteUsers/CreateUserAccess.tsx new file mode 100644 index 000000000..92e239fb9 --- /dev/null +++ b/apps/ui/src/pages/InviteUsers/CreateUserAccess.tsx @@ -0,0 +1,73 @@ +import { FormikProvider } from 'formik' +import { + StyledHeaderGroup, + StyledSectionTitle, + StyledSectionWrapper, + } from 'pages/Home/homeStyle.css' +import BackButton from 'components/BackButton' +import { ButtonPrimary } from 'components/Button/Button' +import ComponentsWrapper from 'components/ComponentsWrapper/ComponentsWrapper' +import { StyledFormWrapper } from 'styles/formStyles.css' +import styled from 'styled-components' +import FormikTextField from 'components/TextFieldFormik' +import { useTranslation } from 'react-i18next' +import Button from 'share-ui/components/Button/Button' +import Loader from 'share-ui/components/Loader/Loader' +import useInviteUsers from './useInviteUsers' + + +const CreateUserAccess = () => { + const { t } = useTranslation() + const { formik, create_access_loading } = useInviteUsers() + + return ( + + + + Invite user + +
+ + + {create_access_loading ? : 'Save'} + +
+
+ + + + + + + + + + +
+
+ ) +} + +export default CreateUserAccess + +const StyledFormContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + overflow-y: auto; + height: 100%; + width: 100%; +` +const StyledInputWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 25px; + width: 100%; + max-width: 800px; + height: calc(100% - 100px); + padding: 0 20px; +` \ No newline at end of file diff --git a/apps/ui/src/pages/InviteUsers/InviteUsers.tsx b/apps/ui/src/pages/InviteUsers/InviteUsers.tsx new file mode 100644 index 000000000..abf34e9d7 --- /dev/null +++ b/apps/ui/src/pages/InviteUsers/InviteUsers.tsx @@ -0,0 +1,50 @@ +// import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' + +import { ButtonPrimary } from 'components/Button/Button' +import Button from 'share-ui/components/Button/Button' +import Add from 'share-ui/components/Icon/Icons/components/Add' +import ComponentsWrapper from 'components/ComponentsWrapper/ComponentsWrapper' +import { StyledTableWrapper } from 'plugins/contact/pages/Contact/Contacts' +import Table from 'components/Table' +import { + StyledHeaderGroup, + StyledSectionTitle, + StyledSectionWrapper, +} from 'pages/Home/homeStyle.css' +import useInviteUsers from './useInviteUsers' +import SharedAccess from './SharedAccess' + + +const InviteUsers = () => { +// const { t } = useTranslation() + const navigate = useNavigate() + const { data, fetch_data_loading, columns } = useInviteUsers() + + return ( + + + Invite Users + + navigate('/invite-user/invite')} + leftIcon={Add} + size={Button.sizes?.SMALL} + > + {/* {t('create-api-key')} */} + Invite User + + + + + + + + + + + + ) +}; + +export default InviteUsers \ No newline at end of file diff --git a/apps/ui/src/pages/InviteUsers/index.ts b/apps/ui/src/pages/InviteUsers/index.ts new file mode 100644 index 000000000..cf6f23182 --- /dev/null +++ b/apps/ui/src/pages/InviteUsers/index.ts @@ -0,0 +1,2 @@ +export { default as InviteUsers } from './InviteUsers' +export { default as CreateUserAccess } from './CreateUserAccess' \ No newline at end of file diff --git a/apps/ui/src/pages/InviteUsers/useInviteUsers.ts b/apps/ui/src/pages/InviteUsers/useInviteUsers.ts new file mode 100644 index 000000000..5d8786f86 --- /dev/null +++ b/apps/ui/src/pages/InviteUsers/useInviteUsers.ts @@ -0,0 +1,84 @@ +import * as yup from 'yup' + +import { + useGetUserAccess, + useCreateUserAccessService, + useDeleteUserAccessService, +} from 'services/inviteUser/useInviteUserService' +import { useFormik } from 'formik' +import React, { useContext } from 'react' +import { ToastContext } from 'contexts' +import { useNavigate } from 'react-router-dom' +import { renderColumns } from './columnConfig' + +const validationSchema = yup.object().shape({ + email: yup + .string() + .email('Invalid email') + .required('Please use a valid email format. Example - user@l3agi.com') +}) + +const useInviteUsers = () => { + const { setToast } = useContext(ToastContext) + const navigate = useNavigate() + const [deleting_id, setDeletingId] = React.useState('') + + const { data, loading: fetch_data_loading, refetch } = useGetUserAccess() + const { createUserAccess, loading: create_access_loading } = useCreateUserAccessService() + const { deleteUserAccess: deleteUserAccessService, loading: deleting_loading } = useDeleteUserAccessService() + + const formik = useFormik({ + initialValues: { email: '' }, + onSubmit: values => handleSubmit(values), + validationSchema, + }) + + async function handleSubmit(values: { email: string }) { + + const result = await createUserAccess(values.email); + + if(result) { + setToast({ + message: result.message, + type: result.success ? 'positive' : 'warning', + open: true, + }) + + if(result.success) { + navigate(`/invite-user`) + } + } + + } + + const deleteUserAccess = async (id: string) => { + setDeletingId(id) + const result = await deleteUserAccessService(id) + if(result) { + if(result.success) { + refetch() + } + setToast({ + message: result.message, + type: result.success ? 'positive' : 'warning', + open: true, + }) + } + } + + const columns = renderColumns({ + deleteUserAccess, + deleting_loading: { loading: deleting_loading, id: deleting_id } + }) + + return { + data, + fetch_data_loading, + create_access_loading, + formik, + deleteUserAccess, + columns, + } +} + +export default useInviteUsers \ No newline at end of file diff --git a/apps/ui/src/services/inviteUser/useInviteUserService.ts b/apps/ui/src/services/inviteUser/useInviteUserService.ts new file mode 100644 index 000000000..f2c0198ba --- /dev/null +++ b/apps/ui/src/services/inviteUser/useInviteUserService.ts @@ -0,0 +1,107 @@ +import { useQuery, useMutation } from '@apollo/client' +import { useContext } from 'react' +import { ToastContext } from 'contexts' +import getUserAccess from '../../gql/inviteUser/getUserAccess.gql' +import createUserAccessGql from '../../gql/inviteUser/createUserAccess.gql' +import deleteUserAccessGql from '../../gql/inviteUser/deleteUserAccess.gql' +import getSharedUserAccessGql from '../../gql/inviteUser/getSharedUserAccess.gql' + +export const useGetUserAccess = () => { + const { data, error, loading, refetch } = useQuery(getUserAccess) + + try { + if (error) { + // Handle error here + console.error("Error fetching user access:", error) + } + } catch (error) { + // Handle any unexpected errors + console.error("An unexpected error occurred:", error) + } + + return { + data: data?.getUserAccess || [], + error, + loading, + refetch, + } +} + +export const useCreateUserAccessService = () => { + const { setToast } = useContext(ToastContext) + const [mutation, { loading }] = useMutation(createUserAccessGql) + + const createUserAccess = async (email: string) => { + try { + const { + data: { createUserAccess }, + } = await mutation({ + variables: { + input: { + email + }, + }, + }) + + return createUserAccess + } catch (error) { + setToast({ + message: error?.message ?? 'Error creating user access', + type: 'negative', + open: true, + }) + throw error; // Rethrow the error to propagate it to the caller + } + } + + return { createUserAccess, loading } +} + +export const useDeleteUserAccessService = () => { + const { setToast } = useContext(ToastContext) + const [mutation, { loading }] = useMutation(deleteUserAccessGql) + + const deleteUserAccess = async (id: string) => { + try { + const { + data: { deleteUserAccess }, + } = await mutation({ + variables: { + id + }, + }) + + return deleteUserAccess + } catch (error) { + setToast({ + message: error?.message ?? 'Error deleting user access', + type: 'negative', + open: true, + }) + throw error; // Rethrow the error to propagate it to the caller + } + } + + return { deleteUserAccess, loading } +} + +export const useGetSharedUserAccess = () => { + const { data, error, loading, refetch } = useQuery(getSharedUserAccessGql) + + try { + if (error) { + // Handle error here + console.error("Error fetching user access:", error) + } + } catch (error) { + // Handle any unexpected errors + console.error("An unexpected error occurred:", error) + } + + return { + data: data?.getSharedUserAccess || [], + error, + loading, + refetch, + } +} \ No newline at end of file From 179490cfd33ff7f4e54ecc2d645b1e86fd42db72 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:35:27 +0400 Subject: [PATCH 16/19] feat: shared access list --- .../ui/src/pages/InviteUsers/SharedAccess.tsx | 28 ++++++++++++++++ .../src/pages/InviteUsers/useSharedAccess.ts | 33 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 apps/ui/src/pages/InviteUsers/SharedAccess.tsx create mode 100644 apps/ui/src/pages/InviteUsers/useSharedAccess.ts diff --git a/apps/ui/src/pages/InviteUsers/SharedAccess.tsx b/apps/ui/src/pages/InviteUsers/SharedAccess.tsx new file mode 100644 index 000000000..abeaacb7d --- /dev/null +++ b/apps/ui/src/pages/InviteUsers/SharedAccess.tsx @@ -0,0 +1,28 @@ +import ComponentsWrapper from 'components/ComponentsWrapper/ComponentsWrapper' +import { StyledTableWrapper } from 'plugins/contact/pages/Contact/Contacts' +import Table from 'components/Table' +import useSharedAccess from './useSharedAccess' +import { + StyledHeaderGroup, + StyledSectionTitle, + StyledSectionWrapper, +} from 'pages/Home/homeStyle.css' + +const SharedAccess = () => { + const { columns, data, fetch_data_loading } = useSharedAccess() + return ( + + + Access + + + + +
+ + + + ) +} + +export default SharedAccess \ No newline at end of file diff --git a/apps/ui/src/pages/InviteUsers/useSharedAccess.ts b/apps/ui/src/pages/InviteUsers/useSharedAccess.ts new file mode 100644 index 000000000..028b12ba6 --- /dev/null +++ b/apps/ui/src/pages/InviteUsers/useSharedAccess.ts @@ -0,0 +1,33 @@ +import { renderSharedColumns } from './columnConfig' +import { useGetSharedUserAccess } from '../../services/inviteUser/useInviteUserService' +import { setAccountId, removeAccountId } from 'helpers/authHelper' +import { useCookies } from 'react-cookie' + +const useSharedAccess = () => { + const { data, loading: fetch_data_loading } = useGetSharedUserAccess() + const [cookies] = useCookies(['']) + const { account_id } = cookies + + console.log('cookies', cookies) + + // const selected_account_id = 'ed0fa07b-d5a8-462f-8f81-97f4c54b4eb4' + + const handleSelectAccess = (access_account_id: string) => { + if(access_account_id === account_id) { + removeAccountId() + return + } + setAccountId(access_account_id) + } + + + const columns = renderSharedColumns({ handleSelectAccess, selected_account_id: account_id }) + + return { + columns, + data, + fetch_data_loading + } +} + +export default useSharedAccess \ No newline at end of file From cd3ba71f79c626e58fa324048779324b9c047146 Mon Sep 17 00:00:00 2001 From: Giga Date: Wed, 29 May 2024 18:38:04 +0400 Subject: [PATCH 17/19] fix: useSharedAccess --- apps/ui/src/pages/InviteUsers/useSharedAccess.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/ui/src/pages/InviteUsers/useSharedAccess.ts b/apps/ui/src/pages/InviteUsers/useSharedAccess.ts index 028b12ba6..049f2797d 100644 --- a/apps/ui/src/pages/InviteUsers/useSharedAccess.ts +++ b/apps/ui/src/pages/InviteUsers/useSharedAccess.ts @@ -8,10 +8,6 @@ const useSharedAccess = () => { const [cookies] = useCookies(['']) const { account_id } = cookies - console.log('cookies', cookies) - - // const selected_account_id = 'ed0fa07b-d5a8-462f-8f81-97f4c54b4eb4' - const handleSelectAccess = (access_account_id: string) => { if(access_account_id === account_id) { removeAccountId() @@ -20,7 +16,6 @@ const useSharedAccess = () => { setAccountId(access_account_id) } - const columns = renderSharedColumns({ handleSelectAccess, selected_account_id: account_id }) return { From 127b0b8d6499380d8c1a76f5148f10eefe9712e9 Mon Sep 17 00:00:00 2001 From: Giga Date: Thu, 30 May 2024 10:55:04 +0400 Subject: [PATCH 18/19] fix: account access --- .../server/controllers/user_account_access.py | 25 ++++++++++--- apps/server/models/user_account_access.py | 37 ++++++++++++++++--- apps/server/typings/user_account_access.py | 10 +++++ apps/server/utils/auth.py | 15 ++++++-- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/apps/server/controllers/user_account_access.py b/apps/server/controllers/user_account_access.py index f38683c7d..1f264d662 100644 --- a/apps/server/controllers/user_account_access.py +++ b/apps/server/controllers/user_account_access.py @@ -12,6 +12,7 @@ UserAccountAccessInput, UserAccountAccessDbInput, UserAccountAccessOutput, + SharedUserAccountAccessOutput ) from exceptions import UserAccessNotFoundException from utils.user_account_access import ( @@ -40,6 +41,11 @@ def create_user_account_access( try: body = input.dict() + current_user_account = UserAccountModel.get_user_account_by_user_id( + db=db, + user_id=auth.user.id + ) + existing_user = UserModel.get_user_by_email(db=db, email=body['email']) if not existing_user: @@ -63,7 +69,7 @@ def create_user_account_access( db=db, user_account_access=create_user_account_access_input, user=auth.user, - account=auth.account + account_id=current_user_account.account_id ) if not response: raise HTTPException( @@ -85,7 +91,7 @@ def create_user_account_access( db=db, assigner_user_id=user_account.user_id, assigned_account_id=user_account.account_id, - account=auth.account + account_id=current_user_account.account_id ): return { 'success': False, @@ -101,7 +107,7 @@ def create_user_account_access( db=db, user_account_access=create_user_account_access_input, user=auth.user, - account=auth.account + account_id=current_user_account.account_id ) if response is None: @@ -169,10 +175,15 @@ def delete_user_account_access( UserAccountAccess: User account access data. """ try: + current_user_account = UserAccountModel.get_user_account_by_user_id( + db=db, + user_id=auth.user.id + ) + UserAccountAccessModel.delete_user_account_access_by_id( db=db, user_account_access_id=user_account_access_id, - account=auth.account + account_id=current_user_account.account_id ) return {"success": True, "message": "User access successfully deleted"} @@ -186,7 +197,11 @@ def delete_user_account_access( ) -@router.get("/access", status_code=200) +@router.get( + "/access", + response_model=list[SharedUserAccountAccessOutput], + status_code=200 +) def get_shared_user_account_access(auth: UserAccount = Depends(authenticate)): """_summary_ diff --git a/apps/server/models/user_account_access.py b/apps/server/models/user_account_access.py index 9230f5b20..b8c451e3b 100644 --- a/apps/server/models/user_account_access.py +++ b/apps/server/models/user_account_access.py @@ -62,7 +62,7 @@ def create_user_account_access( db: Session, user_account_access: UserAccountAccessDbInput, user, - account + account_id ): """ Creates a new user account access. @@ -77,7 +77,7 @@ def create_user_account_access( db_user_account_access = UserAccountAccessModel( created_by=user.id, - account_id=account.id, # replace account.id to current_user_account_id + account_id=account_id ) cls.update_model_from_input( @@ -109,13 +109,13 @@ def delete_user_account_access_by_id( cls, db, user_account_access_id, - account + account_id ): user_access = ( db.session.query(UserAccountAccessModel) .filter( UserAccountAccessModel.id == user_account_access_id, - UserAccountAccessModel.account_id == account.id + UserAccountAccessModel.account_id == account_id ) .first() ) @@ -209,14 +209,14 @@ def get_user_account_access_assigned( db, assigner_user_id, assigned_account_id, - account + account_id ): user_account_access = ( db.session.query(UserAccountAccessModel) .filter( UserAccountAccessModel.assigned_user_id == assigner_user_id, UserAccountAccessModel.assigned_account_id == assigned_account_id, - UserAccountAccessModel.account_id == account.id, + UserAccountAccessModel.account_id == account_id, or_( or_( UserAccountAccessModel.is_deleted.is_(False), @@ -229,3 +229,28 @@ def get_user_account_access_assigned( ) return user_account_access + + @classmethod + def check_exist_user_account_access_by_account_id( + cls, + db, + account_id, + user_id + ): + user_account_access = ( + db.session.query(UserAccountAccessModel) + .filter( + UserAccountAccessModel.account_id == account_id, + UserAccountAccessModel.assigned_user_id == user_id, + or_( + or_( + UserAccountAccessModel.is_deleted.is_(False), + UserAccountAccessModel.is_deleted is None, + ), + UserAccountAccessModel.is_deleted is None, + ), + ) + .first() + ) + + return user_account_access diff --git a/apps/server/typings/user_account_access.py b/apps/server/typings/user_account_access.py index 93186eadd..13b0b65ab 100644 --- a/apps/server/typings/user_account_access.py +++ b/apps/server/typings/user_account_access.py @@ -29,3 +29,13 @@ class GetUserAccountAccessOutput(BaseModel): class UserAccountAccessOutput(BaseModel): success: bool message: str + + +class SharedUserAccountAccessOutput(BaseModel): + id: UUID + account_id: UUID + created_by: UUID + created_on: datetime + assigned_account_name: str + created_by_email: str + created_by_name: str diff --git a/apps/server/utils/auth.py b/apps/server/utils/auth.py index b91a97ae8..c99bc4db3 100644 --- a/apps/server/utils/auth.py +++ b/apps/server/utils/auth.py @@ -20,6 +20,7 @@ convert_model_to_response as convert_model_to_response_account from utils.user import \ convert_model_to_response as convert_model_to_response_user +from models.user_account_access import UserAccountAccessModel oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") @@ -33,12 +34,20 @@ def authenticate( email = authorize.get_jwt_subject() db_user = UserModel.get_user_by_email(db, email) account_id = request.headers.get("account_id", None) - if account_id == "undefined" or not account_id: - db_account = AccountModel.get_account_created_by(db, db_user.id) - else: + + if account_id != "undefined" and account_id and UserAccountAccessModel.check_exist_user_account_access_by_account_id(db, account_id, db_user.id): db_account = AccountModel.get_account_by_access( db, user_id=db_user.id, account_id=account_id ) + else: + db_account = AccountModel.get_account_created_by(db, db_user.id) + + # if account_id == "undefined" or not account_id: + # db_account = AccountModel.get_account_created_by(db, db_user.id) + # else: + # db_account = AccountModel.get_account_by_access( + # db, user_id=db_user.id, account_id=account_id + # ) return UserAccount( user=convert_model_to_response_user(db_user), From 7c71151e133e6476b9deab0bbffa7d199a132d27 Mon Sep 17 00:00:00 2001 From: Giga Date: Thu, 30 May 2024 10:55:07 +0400 Subject: [PATCH 19/19] fix: account access --- apps/ui/src/pages/InviteUsers/columnConfig.tsx | 18 ++++++++---------- .../src/pages/InviteUsers/useSharedAccess.ts | 1 + 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/ui/src/pages/InviteUsers/columnConfig.tsx b/apps/ui/src/pages/InviteUsers/columnConfig.tsx index 3d6984747..5af342e5a 100644 --- a/apps/ui/src/pages/InviteUsers/columnConfig.tsx +++ b/apps/ui/src/pages/InviteUsers/columnConfig.tsx @@ -21,6 +21,7 @@ interface ColumnRowProps { assigned_user_email: string created_by_email: string created_on: Date + account_id: string } // eslint-disable-next-line react/prop-types @@ -29,6 +30,11 @@ const DateRenderer: React.FC<{ value: Date }> = ({ value }) => { return {formattedDate} } +interface SharedColumnProps { + selected_account_id: string | null + handleSelectAccess: (account_id: string) => void +} + const base = [ { Header: 'User Name', @@ -65,7 +71,7 @@ export const renderColumns = ({ deleteUserAccess, deleting_loading }: RendererCo minWidth: 100, width: 130, - Cell: ({ row: { original: data } }: { row: { original: ColumnRowProps } }) => ( + Cell: ({ row: { original: data } }: { row: { original: Pick } }) => ( deleteUserAccess(data.id)} @@ -79,10 +85,6 @@ export const renderColumns = ({ deleteUserAccess, deleting_loading }: RendererCo }, ]) -interface SharedColumnProps { - selected_account_id: string | null - handleSelectAccess: (account_id: string) => void -} export const renderSharedColumns = ({ selected_account_id, handleSelectAccess }: SharedColumnProps) => ([ { Header: 'Select account', @@ -90,9 +92,8 @@ export const renderSharedColumns = ({ selected_account_id, handleSelectAccess }: minWidth: 100, width: 130, - Cell: ({ row: { original: data } }: any) => ( + Cell: ({ row: { original: data } }: { row: { original: Pick } }) => ( 0 && selectedTables?.length !== data?.length} checked={data.account_id === selected_account_id} size='large' kind='secondary' @@ -109,9 +110,6 @@ export const renderSharedColumns = ({ selected_account_id, handleSelectAccess }: ...base.slice(2, base.length), ]) - - - const StyledActionWrapper = styled.div` display: flex; justify-content: center; diff --git a/apps/ui/src/pages/InviteUsers/useSharedAccess.ts b/apps/ui/src/pages/InviteUsers/useSharedAccess.ts index 049f2797d..986b28591 100644 --- a/apps/ui/src/pages/InviteUsers/useSharedAccess.ts +++ b/apps/ui/src/pages/InviteUsers/useSharedAccess.ts @@ -5,6 +5,7 @@ import { useCookies } from 'react-cookie' const useSharedAccess = () => { const { data, loading: fetch_data_loading } = useGetSharedUserAccess() + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [cookies] = useCookies(['']) const { account_id } = cookies