From cacfb2a5e8c78a8e3d7b2d7f299a5548f33375bf Mon Sep 17 00:00:00 2001 From: mohamad Date: Tue, 13 May 2025 20:33:02 +0200 Subject: [PATCH] commit i guess --- be/app/api/v1/endpoints/auth.py | 7 +- be/app/api/v1/endpoints/groups.py | 30 ++- be/app/crud/list.py | 18 +- be/app/database.py | 4 +- be/app/schemas/group.py | 22 +- fe/src/components/CreateListModal.vue | 26 +-- .../components/global/NotificationDisplay.vue | 156 +++++++------- fe/src/layouts/MainLayout.vue | 13 +- fe/src/main.ts | 13 +- fe/src/pages/GroupDetailPage.vue | 199 +++++++++++++++--- fe/src/pages/GroupsPage.vue | 166 ++++++++------- fe/src/pages/ListsPage.vue | 140 ++++++------ fe/src/services/api.ts | 47 +++-- fe/src/stores/auth.ts | 46 +++- 14 files changed, 581 insertions(+), 306 deletions(-) diff --git a/be/app/api/v1/endpoints/auth.py b/be/app/api/v1/endpoints/auth.py index 6871d65..c9bd1e1 100644 --- a/be/app/api/v1/endpoints/auth.py +++ b/be/app/api/v1/endpoints/auth.py @@ -105,7 +105,7 @@ async def refresh_token( """ Handles access token refresh. - Verifies the provided refresh token. - - If valid, generates and returns a new JWT access token and the same refresh token. + - If valid, generates and returns a new JWT access token and a new refresh token. """ logger.info("Access token refresh attempt") payload = verify_refresh_token(refresh_token_str) @@ -127,9 +127,10 @@ async def refresh_token( ) new_access_token = create_access_token(subject=user_email) - logger.info(f"Access token refreshed for user: {user_email}") + new_refresh_token = create_refresh_token(subject=user_email) + logger.info(f"Access token refreshed and new refresh token issued for user: {user_email}") return Token( access_token=new_access_token, - refresh_token=refresh_token_str, + refresh_token=new_refresh_token, token_type=settings.TOKEN_TYPE ) \ No newline at end of file diff --git a/be/app/api/v1/endpoints/groups.py b/be/app/api/v1/endpoints/groups.py index 4e6aff3..1b5ead7 100644 --- a/be/app/api/v1/endpoints/groups.py +++ b/be/app/api/v1/endpoints/groups.py @@ -11,8 +11,10 @@ from app.models import User as UserModel, UserRoleEnum # Import model and enum from app.schemas.group import GroupCreate, GroupPublic from app.schemas.invite import InviteCodePublic from app.schemas.message import Message # For simple responses +from app.schemas.list import ListPublic from app.crud import group as crud_group from app.crud import invite as crud_invite +from app.crud import list as crud_list from app.core.exceptions import ( GroupNotFoundError, GroupPermissionError, @@ -197,4 +199,30 @@ async def remove_group_member( raise GroupOperationError("Failed to remove member") logger.info(f"Owner {current_user.email} successfully removed user {user_id_to_remove} from group {group_id}") - return Message(detail="Successfully removed member from the group") \ No newline at end of file + return Message(detail="Successfully removed member from the group") + +@router.get( + "/{group_id}/lists", + response_model=List[ListPublic], + summary="Get Group Lists", + tags=["Groups", "Lists"] +) +async def read_group_lists( + group_id: int, + db: AsyncSession = Depends(get_db), + current_user: UserModel = Depends(get_current_user), +): + """Retrieves all lists belonging to a specific group, if the user is a member.""" + logger.info(f"User {current_user.email} requesting lists for group ID: {group_id}") + + # Check if user is a member first + is_member = await crud_group.is_user_member(db=db, group_id=group_id, user_id=current_user.id) + if not is_member: + logger.warning(f"Access denied: User {current_user.email} not member of group {group_id}") + raise GroupMembershipError(group_id, "view group lists") + + # Get all lists for the user and filter by group_id + lists = await crud_list.get_lists_for_user(db=db, user_id=current_user.id) + group_lists = [list for list in lists if list.group_id == group_id] + + return group_lists \ No newline at end of file diff --git a/be/app/crud/list.py b/be/app/crud/list.py index 7e3655e..3921cae 100644 --- a/be/app/crud/list.py +++ b/be/app/crud/list.py @@ -23,7 +23,9 @@ from app.core.exceptions import ( async def create_list(db: AsyncSession, list_in: ListCreate, creator_id: int) -> ListModel: """Creates a new list record.""" try: - async with db.begin(): + # Check if we're already in a transaction + if db.in_transaction(): + # If we're already in a transaction, just create the list db_list = ListModel( name=list_in.name, description=list_in.description, @@ -35,6 +37,20 @@ async def create_list(db: AsyncSession, list_in: ListCreate, creator_id: int) -> await db.flush() await db.refresh(db_list) return db_list + else: + # If no transaction is active, start one + async with db.begin(): + db_list = ListModel( + name=list_in.name, + description=list_in.description, + group_id=list_in.group_id, + created_by_id=creator_id, + is_complete=False + ) + db.add(db_list) + await db.flush() + await db.refresh(db_list) + return db_list except IntegrityError as e: raise DatabaseIntegrityError(f"Failed to create list: {str(e)}") except OperationalError as e: diff --git a/be/app/database.py b/be/app/database.py index ca3c74d..d1bda5a 100644 --- a/be/app/database.py +++ b/be/app/database.py @@ -38,8 +38,8 @@ async def get_db() -> AsyncSession: # type: ignore async with AsyncSessionLocal() as session: try: yield session - # Optionally commit if your endpoints modify data directly - # await session.commit() # Usually commit happens within endpoint logic + # Commit the transaction if no errors occurred + await session.commit() except Exception: await session.rollback() raise diff --git a/be/app/schemas/group.py b/be/app/schemas/group.py index 00c227b..6773e83 100644 --- a/be/app/schemas/group.py +++ b/be/app/schemas/group.py @@ -1,5 +1,5 @@ # app/schemas/group.py -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, computed_field from datetime import datetime from typing import Optional, List @@ -15,7 +15,25 @@ class GroupPublic(BaseModel): name: str created_by_id: int created_at: datetime - members: Optional[List[UserPublic]] = None # Include members only in detailed view + member_associations: Optional[List["UserGroupPublic"]] = None + + @computed_field + @property + def members(self) -> Optional[List[UserPublic]]: + if not self.member_associations: + return None + return [assoc.user for assoc in self.member_associations if assoc.user] + + model_config = ConfigDict(from_attributes=True) + +# Properties for UserGroup association +class UserGroupPublic(BaseModel): + id: int + user_id: int + group_id: int + role: str + joined_at: datetime + user: Optional[UserPublic] = None model_config = ConfigDict(from_attributes=True) diff --git a/fe/src/components/CreateListModal.vue b/fe/src/components/CreateListModal.vue index 0640e7c..310d413 100644 --- a/fe/src/components/CreateListModal.vue +++ b/fe/src/components/CreateListModal.vue @@ -4,32 +4,22 @@