From 402b5898bb48c2ff510b0392c9720646ea9b84ad Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Mon, 10 Nov 2025 22:58:52 +0100 Subject: [PATCH] updated admin page with more functionality, simplified burger menu and added many-to-many parent-child relationship --- src/baby_monitor/main.py | 12 + src/baby_monitor/models/child.py | 1 - src/baby_monitor/models/db/child.py | 5 +- src/baby_monitor/models/db/child_parent.py | 29 ++ .../repositories/child/sqlite_child.py | 57 ++- .../repositories/dependencies/get_database.py | 1 + .../diaper_change/sqlite_diaper_change.py | 6 +- .../repositories/feeding/sqlite_feeding.py | 6 +- .../repositories/interfaces/__init__.py | 16 + .../interfaces/child_repository_interface.py | 8 + .../interfaces/user_repository_interface.py | 8 + .../repositories/sleep/sqlite_sleep.py | 6 +- .../repositories/user/sqlite_user.py | 23 ++ src/baby_monitor/routers/admin.py | 191 +++++++++- src/baby_monitor/routers/child.py | 6 +- src/baby_monitor/routers/diaper_change.py | 20 +- src/baby_monitor/routers/feeding.py | 20 +- src/baby_monitor/routers/sleep.py | 20 +- src/baby_monitor/static/admin.html | 350 +++++++++++++++++- src/baby_monitor/static/diapers.html | 130 +------ src/baby_monitor/static/feedings.html | 128 +------ src/baby_monitor/static/index.html | 139 +------ src/baby_monitor/static/menu.css | 73 ++++ src/baby_monitor/static/menu.js | 106 ++++++ src/baby_monitor/static/sleep.html | 129 +------ src/baby_monitor/utils/access_control.py | 26 ++ 26 files changed, 952 insertions(+), 564 deletions(-) create mode 100644 src/baby_monitor/models/db/child_parent.py create mode 100644 src/baby_monitor/static/menu.css create mode 100644 src/baby_monitor/static/menu.js create mode 100644 src/baby_monitor/utils/access_control.py diff --git a/src/baby_monitor/main.py b/src/baby_monitor/main.py index fe17988..8817abc 100644 --- a/src/baby_monitor/main.py +++ b/src/baby_monitor/main.py @@ -119,6 +119,18 @@ def serve_sleep() -> FileResponse: return FileResponse(static_path / "sleep.html") +@app.get("/menu.css", include_in_schema=False) +def serve_menu_css() -> FileResponse: + """Serve the shared menu CSS.""" + return FileResponse(static_path / "menu.css") + + +@app.get("/menu.js", include_in_schema=False) +def serve_menu_js() -> FileResponse: + """Serve the shared menu JavaScript.""" + return FileResponse(static_path / "menu.js") + + @app.get("/api/") def read_root(token: Annotated[str, Depends(verify_token)]) -> dict: """API root endpoint (requires authentication).""" diff --git a/src/baby_monitor/models/child.py b/src/baby_monitor/models/child.py index 86096e7..17ab949 100644 --- a/src/baby_monitor/models/child.py +++ b/src/baby_monitor/models/child.py @@ -19,5 +19,4 @@ class ChildResponse(BaseModel): name: str birth_time: datetime birth_weight: float - user_id: int created_at: datetime diff --git a/src/baby_monitor/models/db/child.py b/src/baby_monitor/models/db/child.py index 7a633ae..3c7a408 100644 --- a/src/baby_monitor/models/db/child.py +++ b/src/baby_monitor/models/db/child.py @@ -1,7 +1,7 @@ """Child database model.""" from typing import TYPE_CHECKING -from sqlalchemy import Column, Integer, String, DateTime, Float, ForeignKey +from sqlalchemy import Column, Integer, String, DateTime, Float from datetime import datetime if TYPE_CHECKING: @@ -21,8 +21,7 @@ class Child(Base): name = Column(String, nullable=False) birth_time = Column(DateTime, nullable=False) birth_weight = Column(Float, nullable=False) # in grams - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) created_at = Column(DateTime, default=datetime.utcnow, nullable=False) def __repr__(self) -> str: - return f"" + return f"" diff --git a/src/baby_monitor/models/db/child_parent.py b/src/baby_monitor/models/db/child_parent.py new file mode 100644 index 0000000..1d0bc6e --- /dev/null +++ b/src/baby_monitor/models/db/child_parent.py @@ -0,0 +1,29 @@ +"""Child-Parent junction table for many-to-many relationship.""" + +from typing import TYPE_CHECKING +from sqlalchemy import Column, Integer, ForeignKey, DateTime +from datetime import datetime + +if TYPE_CHECKING: + from sqlalchemy.orm import DeclarativeBase + + Base = DeclarativeBase +else: + from baby_monitor.repositories.dependencies.get_database import Base + + +class ChildParent(Base): + """Junction table linking children to their parents (users).""" + + __tablename__ = "child_parents" + + id = Column(Integer, primary_key=True, index=True) + child_id = Column(Integer, ForeignKey("children.id"), nullable=False) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + + def __repr__(self) -> str: + return ( + f"" + ) diff --git a/src/baby_monitor/repositories/child/sqlite_child.py b/src/baby_monitor/repositories/child/sqlite_child.py index e18d2dc..d9868fa 100644 --- a/src/baby_monitor/repositories/child/sqlite_child.py +++ b/src/baby_monitor/repositories/child/sqlite_child.py @@ -7,6 +7,7 @@ from baby_monitor.repositories.interfaces.child_repository_interface import ( ChildRepositoryInterface, ) from baby_monitor.models.db.child import Child +from baby_monitor.models.db.child_parent import ChildParent class SQLiteChildRepository(ChildRepositoryInterface): @@ -18,23 +19,29 @@ class SQLiteChildRepository(ChildRepositoryInterface): def create( self, name: str, birth_time: datetime, birth_weight: float, user_id: int ) -> dict: - """Create a new child record.""" + """Create a new child record and link to user.""" child = Child( name=name, birth_time=birth_time, birth_weight=birth_weight, - user_id=user_id, ) self.db.add(child) self.db.commit() self.db.refresh(child) + # Create the child-parent relationship + child_parent = ChildParent( + child_id=child.id, + user_id=user_id, + ) + self.db.add(child_parent) + self.db.commit() + return { "id": child.id, "name": child.name, "birth_time": child.birth_time, "birth_weight": child.birth_weight, - "user_id": child.user_id, "created_at": child.created_at, } @@ -49,13 +56,18 @@ class SQLiteChildRepository(ChildRepositoryInterface): "name": child.name, "birth_time": child.birth_time, "birth_weight": child.birth_weight, - "user_id": child.user_id, "created_at": child.created_at, } def get_by_user_id(self, user_id: int) -> list[dict]: - """Get all children for a specific user.""" - children = self.db.query(Child).filter(Child.user_id == user_id).all() + """Get all children for a specific user via junction table.""" + # Join Child and ChildParent tables + children = ( + self.db.query(Child) + .join(ChildParent, Child.id == ChildParent.child_id) + .filter(ChildParent.user_id == user_id) + .all() + ) return [ { @@ -63,12 +75,36 @@ class SQLiteChildRepository(ChildRepositoryInterface): "name": child.name, "birth_time": child.birth_time, "birth_weight": child.birth_weight, - "user_id": child.user_id, "created_at": child.created_at, } for child in children ] + def get_parent_ids(self, child_id: int) -> list[int]: + """Get all parent user IDs for a specific child.""" + parent_ids = ( + self.db.query(ChildParent.user_id) + .filter(ChildParent.child_id == child_id) + .all() + ) + return [user_id for (user_id,) in parent_ids] + + def remove_parent(self, child_id: int, user_id: int) -> bool: + """Remove a parent-child relationship.""" + link = ( + self.db.query(ChildParent) + .filter( + ChildParent.child_id == child_id, + ChildParent.user_id == user_id, + ) + .first() + ) + if link: + self.db.delete(link) + self.db.commit() + return True + return False + def update( self, child_id: int, @@ -96,16 +132,19 @@ class SQLiteChildRepository(ChildRepositoryInterface): "name": child.name, "birth_time": child.birth_time, "birth_weight": child.birth_weight, - "user_id": child.user_id, "created_at": child.created_at, } def delete(self, child_id: int) -> bool: - """Delete a child record.""" + """Delete a child record and all parent relationships.""" child = self.db.query(Child).filter(Child.id == child_id).first() if not child: return False + # Delete all parent relationships first + self.db.query(ChildParent).filter(ChildParent.child_id == child_id).delete() + + # Delete the child self.db.delete(child) self.db.commit() return True diff --git a/src/baby_monitor/repositories/dependencies/get_database.py b/src/baby_monitor/repositories/dependencies/get_database.py index c724566..fbe0d36 100644 --- a/src/baby_monitor/repositories/dependencies/get_database.py +++ b/src/baby_monitor/repositories/dependencies/get_database.py @@ -78,6 +78,7 @@ def init_db() -> None: # This must be done before create_all() is called from baby_monitor.models.db.user import User # noqa: F401 from baby_monitor.models.db.child import Child # noqa: F401 + from baby_monitor.models.db.child_parent import ChildParent # noqa: F401 from baby_monitor.models.db.feeding import Feeding # noqa: F401 from baby_monitor.models.db.diaper_change import DiaperChange # noqa: F401 from baby_monitor.models.db.sleep import Sleep # noqa: F401 diff --git a/src/baby_monitor/repositories/diaper_change/sqlite_diaper_change.py b/src/baby_monitor/repositories/diaper_change/sqlite_diaper_change.py index 2bf0f8f..f883c8e 100644 --- a/src/baby_monitor/repositories/diaper_change/sqlite_diaper_change.py +++ b/src/baby_monitor/repositories/diaper_change/sqlite_diaper_change.py @@ -8,6 +8,7 @@ from baby_monitor.repositories.interfaces.diaper_change_repository_interface imp ) from baby_monitor.models.db.diaper_change import DiaperChange from baby_monitor.models.db.child import Child +from baby_monitor.models.db.child_parent import ChildParent class SQLiteDiaperChangeRepository(DiaperChangeRepositoryInterface): @@ -65,8 +66,9 @@ class SQLiteDiaperChangeRepository(DiaperChangeRepositoryInterface): """Get all diaper change logs for a specific user's children.""" diaper_changes = ( self.db.query(DiaperChange) - .join(Child) - .filter(Child.user_id == user_id) + .join(Child, DiaperChange.child_id == Child.id) + .join(ChildParent, Child.id == ChildParent.child_id) + .filter(ChildParent.user_id == user_id) .order_by(DiaperChange.change_time.desc()) .all() ) diff --git a/src/baby_monitor/repositories/feeding/sqlite_feeding.py b/src/baby_monitor/repositories/feeding/sqlite_feeding.py index 75367ad..e7a96a9 100644 --- a/src/baby_monitor/repositories/feeding/sqlite_feeding.py +++ b/src/baby_monitor/repositories/feeding/sqlite_feeding.py @@ -8,6 +8,7 @@ from baby_monitor.repositories.interfaces.feeding_repository_interface import ( ) from baby_monitor.models.db.feeding import Feeding from baby_monitor.models.db.child import Child +from baby_monitor.models.db.child_parent import ChildParent class SQLiteFeedingRepository(FeedingRepositoryInterface): @@ -57,8 +58,9 @@ class SQLiteFeedingRepository(FeedingRepositoryInterface): """Get all feeding logs for a specific user's children.""" feedings = ( self.db.query(Feeding) - .join(Child) - .filter(Child.user_id == user_id) + .join(Child, Feeding.child_id == Child.id) + .join(ChildParent, Child.id == ChildParent.child_id) + .filter(ChildParent.user_id == user_id) .order_by(Feeding.start_time.desc()) .all() ) diff --git a/src/baby_monitor/repositories/interfaces/__init__.py b/src/baby_monitor/repositories/interfaces/__init__.py index 21a8cac..f454ec0 100644 --- a/src/baby_monitor/repositories/interfaces/__init__.py +++ b/src/baby_monitor/repositories/interfaces/__init__.py @@ -12,10 +12,26 @@ from .token_repository_interface import ( from .user_repository_interface import ( UserRepositoryInterface, ) +from .child_repository_interface import ( + ChildRepositoryInterface, +) +from .feeding_repository_interface import ( + FeedingRepositoryInterface, +) +from .diaper_change_repository_interface import ( + DiaperChangeRepositoryInterface, +) +from .sleep_repository_interface import ( + SleepRepositoryInterface, +) __all__ = [ "UserRepositoryInterface", "TokenRepositoryInterface", "CredentialsRepositoryInterface", "InvitationRepositoryInterface", + "ChildRepositoryInterface", + "FeedingRepositoryInterface", + "DiaperChangeRepositoryInterface", + "SleepRepositoryInterface", ] diff --git a/src/baby_monitor/repositories/interfaces/child_repository_interface.py b/src/baby_monitor/repositories/interfaces/child_repository_interface.py index 0f04844..b14a109 100644 --- a/src/baby_monitor/repositories/interfaces/child_repository_interface.py +++ b/src/baby_monitor/repositories/interfaces/child_repository_interface.py @@ -34,3 +34,11 @@ class ChildRepositoryInterface(ABC): @abstractmethod def delete(self, child_id: int) -> bool: """Delete a child record.""" + + @abstractmethod + def get_parent_ids(self, child_id: int) -> list[int]: + """Get all parent user IDs for a specific child.""" + + @abstractmethod + def remove_parent(self, child_id: int, user_id: int) -> bool: + """Remove a parent-child relationship.""" diff --git a/src/baby_monitor/repositories/interfaces/user_repository_interface.py b/src/baby_monitor/repositories/interfaces/user_repository_interface.py index 3b49b48..50fe515 100644 --- a/src/baby_monitor/repositories/interfaces/user_repository_interface.py +++ b/src/baby_monitor/repositories/interfaces/user_repository_interface.py @@ -17,3 +17,11 @@ class UserRepositoryInterface(ABC): @abstractmethod def get_by_id(self, user_id: int) -> dict | None: """Get user by ID.""" + + @abstractmethod + def get_all(self) -> list[dict]: + """Get all users.""" + + @abstractmethod + def delete(self, user_id: int) -> bool: + """Delete a user by ID. Returns True if successful.""" diff --git a/src/baby_monitor/repositories/sleep/sqlite_sleep.py b/src/baby_monitor/repositories/sleep/sqlite_sleep.py index cf3cc7e..0e641d0 100644 --- a/src/baby_monitor/repositories/sleep/sqlite_sleep.py +++ b/src/baby_monitor/repositories/sleep/sqlite_sleep.py @@ -8,6 +8,7 @@ from baby_monitor.repositories.interfaces.sleep_repository_interface import ( ) from baby_monitor.models.db.sleep import Sleep from baby_monitor.models.db.child import Child +from baby_monitor.models.db.child_parent import ChildParent class SQLiteSleepRepository(SleepRepositoryInterface): @@ -54,8 +55,9 @@ class SQLiteSleepRepository(SleepRepositoryInterface): """Get all sleep logs for a specific user's children.""" sleeps = ( self.db.query(Sleep) - .join(Child) - .filter(Child.user_id == user_id) + .join(Child, Sleep.child_id == Child.id) + .join(ChildParent, Child.id == ChildParent.child_id) + .filter(ChildParent.user_id == user_id) .order_by(Sleep.start_time.desc()) .all() ) diff --git a/src/baby_monitor/repositories/user/sqlite_user.py b/src/baby_monitor/repositories/user/sqlite_user.py index fc64a21..78e98e0 100644 --- a/src/baby_monitor/repositories/user/sqlite_user.py +++ b/src/baby_monitor/repositories/user/sqlite_user.py @@ -55,3 +55,26 @@ class SQLiteUserRepository(UserRepositoryInterface): "created_at": user.created_at, } return None + + def get_all(self) -> list[dict]: + """Get all users.""" + users = self.db.query(User).all() + return [ + { + "id": user.id, + "username": user.username, + "hashed_password": user.hashed_password, + "is_admin": user.is_admin, + "created_at": user.created_at, + } + for user in users + ] + + def delete(self, user_id: int) -> bool: + """Delete a user by ID. Returns True if successful.""" + user = self.db.query(User).filter(User.id == user_id).first() + if user: + self.db.delete(user) + self.db.commit() + return True + return False diff --git a/src/baby_monitor/routers/admin.py b/src/baby_monitor/routers/admin.py index 2f90071..49d697b 100644 --- a/src/baby_monitor/routers/admin.py +++ b/src/baby_monitor/routers/admin.py @@ -3,15 +3,35 @@ import secrets from datetime import datetime, timedelta, UTC from typing import Annotated -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from baby_monitor.routers.auth import verify_admin from baby_monitor.repositories.dependencies.get_invitation_repository import ( get_invitation_repository, ) +from baby_monitor.repositories.dependencies.get_user_repository import ( + get_user_repository, +) +from baby_monitor.repositories.dependencies.get_child_repository import ( + get_child_repository, +) +from baby_monitor.repositories.dependencies.get_feeding_repository import ( + get_feeding_repository, +) +from baby_monitor.repositories.dependencies.get_diaper_change_repository import ( + get_diaper_change_repository, +) +from baby_monitor.repositories.dependencies.get_sleep_repository import ( + get_sleep_repository, +) from baby_monitor.repositories.interfaces import ( InvitationRepositoryInterface, + UserRepositoryInterface, + ChildRepositoryInterface, + FeedingRepositoryInterface, + DiaperChangeRepositoryInterface, + SleepRepositoryInterface, ) router = APIRouter(prefix="/api/admin", tags=["admin"]) @@ -64,3 +84,172 @@ async def generate_invitation_link( expires_at=expires_at.isoformat(), message="Invitation link generated successfully. Valid for 24 hours.", ) + + +class UserResponse(BaseModel): + """Response model for user data.""" + + id: int + username: str + is_admin: bool + created_at: datetime + + +class DeleteUserResponse(BaseModel): + """Response model for user deletion.""" + + message: str + deleted_children: list[int] + deleted_feedings: int + deleted_diapers: int + deleted_sleeps: int + + +@router.get("/users", response_model=list[UserResponse]) +async def get_all_users( + admin_id: Annotated[int, Depends(verify_admin)], + user_repository: Annotated[ + UserRepositoryInterface, + Depends(get_user_repository), + ], +) -> list[UserResponse]: + """Get all users in the system. + + Requires admin role. + + Args: + admin_id: Admin user ID (from verify_admin) + user_repository: User storage backend + + Returns: + List of all users with their basic information + """ + users = user_repository.get_all() + return [ + UserResponse( + id=user["id"], + username=user["username"], + is_admin=user["is_admin"], + created_at=user["created_at"], + ) + for user in users + ] + + +@router.delete("/users/{user_id}", response_model=DeleteUserResponse) +async def delete_user( + user_id: int, + admin_id: Annotated[int, Depends(verify_admin)], + user_repository: Annotated[ + UserRepositoryInterface, + Depends(get_user_repository), + ], + child_repository: Annotated[ + ChildRepositoryInterface, + Depends(get_child_repository), + ], + feeding_repository: Annotated[ + FeedingRepositoryInterface, + Depends(get_feeding_repository), + ], + diaper_repository: Annotated[ + DiaperChangeRepositoryInterface, + Depends(get_diaper_change_repository), + ], + sleep_repository: Annotated[ + SleepRepositoryInterface, + Depends(get_sleep_repository), + ], +) -> DeleteUserResponse: + """Delete a user and cascade delete orphaned children and their logs. + + When a user is deleted: + 1. Find all children belonging to this user + 2. For each child, check if other users also have this child + 3. Delete only children that have no other parents (orphaned) + 4. For orphaned children, delete all associated logs (feedings, diapers, sleep) + 5. Finally delete the user + + Requires admin role. + + Args: + user_id: ID of user to delete + admin_id: Admin user ID (from verify_admin) + user_repository: User storage backend + child_repository: Child storage backend + feeding_repository: Feeding storage backend + diaper_repository: Diaper change storage backend + sleep_repository: Sleep storage backend + + Returns: + DeleteUserResponse with deletion summary + + Raises: + HTTPException: If user not found or trying to delete yourself + """ + # Prevent admin from deleting themselves + if user_id == admin_id: + raise HTTPException(status_code=400, detail="Cannot delete your own account") + + # Check if user exists + user = user_repository.get_by_id(user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Get all children belonging to this user + children = child_repository.get_by_user_id(user_id) + + deleted_children_ids = [] + total_feedings_deleted = 0 + total_diapers_deleted = 0 + total_sleeps_deleted = 0 + + # For each child, check if they have other parents + for child in children: + child_id = child["id"] + + # Remove this user's relationship with the child + child_repository.remove_parent(child_id, user_id) + + # Check if child has any remaining parents + remaining_parents = child_repository.get_parent_ids(child_id) + + # Only delete child and logs if they have no other parents + if len(remaining_parents) == 0: + # Delete all logs for this orphaned child + feedings = feeding_repository.get_by_child_id(child_id) + for feeding in feedings: + feeding_repository.delete(feeding["id"]) + total_feedings_deleted += 1 + + diapers = diaper_repository.get_by_child_id(child_id) + for diaper in diapers: + diaper_repository.delete(diaper["id"]) + total_diapers_deleted += 1 + + sleeps = sleep_repository.get_by_child_id(child_id) + for sleep in sleeps: + sleep_repository.delete(sleep["id"]) + total_sleeps_deleted += 1 + + # Delete the orphaned child + child_repository.delete(child_id) + deleted_children_ids.append(child_id) + + # Finally, delete the user + user_repository.delete(user_id) + + message = f"User '{user['username']}' deleted successfully" + if deleted_children_ids: + message += ( + f" with {len(deleted_children_ids)} orphaned " + f"child{'ren' if len(deleted_children_ids) != 1 else ''}" + ) + + return DeleteUserResponse( + message=message, + deleted_children=deleted_children_ids, + deleted_feedings=total_feedings_deleted, + deleted_diapers=total_diapers_deleted, + deleted_sleeps=total_sleeps_deleted, + ) diff --git a/src/baby_monitor/routers/child.py b/src/baby_monitor/routers/child.py index 718b461..55fe089 100644 --- a/src/baby_monitor/routers/child.py +++ b/src/baby_monitor/routers/child.py @@ -53,7 +53,8 @@ def get_child( raise HTTPException(status_code=404, detail="Child not found") # Verify the child belongs to the authenticated user - if child["user_id"] != user_id: + parent_ids = child_repo.get_parent_ids(child_id) + if user_id not in parent_ids: raise HTTPException(status_code=403, detail="Access denied") return ChildResponse(**child) @@ -73,7 +74,8 @@ def update_child( if not child: raise HTTPException(status_code=404, detail="Child not found") - if child["user_id"] != user_id: + parent_ids = child_repo.get_parent_ids(child_id) + if user_id not in parent_ids: raise HTTPException(status_code=403, detail="Access denied") # Update the child diff --git a/src/baby_monitor/routers/diaper_change.py b/src/baby_monitor/routers/diaper_change.py index 19154b4..a62f063 100644 --- a/src/baby_monitor/routers/diaper_change.py +++ b/src/baby_monitor/routers/diaper_change.py @@ -19,6 +19,7 @@ from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository from baby_monitor.repositories.dependencies.get_child_repository import ( get_child_repository, ) +from baby_monitor.utils.access_control import verify_child_access router = APIRouter(prefix="/api/diaper-changes", tags=["diaper-changes"]) @@ -34,12 +35,7 @@ def create_diaper_change( ) -> DiaperChangeResponse: """Create a new diaper change log entry.""" # Verify the child belongs to the authenticated user - child = child_repo.get_by_id(request.child_id) - if not child: - raise HTTPException(status_code=404, detail="Child not found") - - if child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied to this child") + verify_child_access(child_repo, request.child_id, user_id) diaper_change = diaper_repo.create( child_id=request.child_id, @@ -81,9 +77,7 @@ def get_diaper_change( raise HTTPException(status_code=404, detail="Diaper change log not found") # Verify the diaper change belongs to user's child - child = child_repo.get_by_id(diaper_change["child_id"]) - if not child or child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied") + verify_child_access(child_repo, diaper_change["child_id"], user_id) return DiaperChangeResponse(**diaper_change) @@ -105,9 +99,7 @@ def update_diaper_change( raise HTTPException(status_code=404, detail="Diaper change log not found") # Verify the diaper change belongs to user's child - child = child_repo.get_by_id(diaper_change["child_id"]) - if not child or child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied") + verify_child_access(child_repo, diaper_change["child_id"], user_id) updated = diaper_repo.update( diaper_change_id=diaper_change_id, @@ -142,9 +134,7 @@ def delete_diaper_change( raise HTTPException(status_code=404, detail="Diaper change log not found") # Verify the diaper change belongs to user's child - child = child_repo.get_by_id(diaper_change["child_id"]) - if not child or child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied") + verify_child_access(child_repo, diaper_change["child_id"], user_id) success = diaper_repo.delete(diaper_change_id) if not success: diff --git a/src/baby_monitor/routers/feeding.py b/src/baby_monitor/routers/feeding.py index ff15153..c205738 100644 --- a/src/baby_monitor/routers/feeding.py +++ b/src/baby_monitor/routers/feeding.py @@ -19,6 +19,7 @@ from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository from baby_monitor.repositories.dependencies.get_child_repository import ( get_child_repository, ) +from baby_monitor.utils.access_control import verify_child_access router = APIRouter(prefix="/api/feedings", tags=["feedings"]) @@ -32,12 +33,7 @@ def create_feeding( ) -> FeedingResponse: """Create a new feeding log entry.""" # Verify the child belongs to the authenticated user - child = child_repo.get_by_id(request.child_id) - if not child: - raise HTTPException(status_code=404, detail="Child not found") - - if child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied to this child") + verify_child_access(child_repo, request.child_id, user_id) feeding = feeding_repo.create( child_id=request.child_id, @@ -95,9 +91,7 @@ def get_feeding( raise HTTPException(status_code=404, detail="Feeding log not found") # Verify the feeding belongs to user's child - child = child_repo.get_by_id(feeding["child_id"]) - if not child or child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied") + verify_child_access(child_repo, feeding["child_id"], user_id) return FeedingResponse(**feeding) @@ -117,9 +111,7 @@ def update_feeding( raise HTTPException(status_code=404, detail="Feeding log not found") # Verify ownership - child = child_repo.get_by_id(feeding["child_id"]) - if not child or child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied") + verify_child_access(child_repo, feeding["child_id"], user_id) # Update the feeding updated_feeding = feeding_repo.update( @@ -149,9 +141,7 @@ def delete_feeding( raise HTTPException(status_code=404, detail="Feeding log not found") # Verify ownership - child = child_repo.get_by_id(feeding["child_id"]) - if not child or child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied") + verify_child_access(child_repo, feeding["child_id"], user_id) success = feeding_repo.delete(feeding_id) if not success: diff --git a/src/baby_monitor/routers/sleep.py b/src/baby_monitor/routers/sleep.py index 89cb9e6..a2308b2 100644 --- a/src/baby_monitor/routers/sleep.py +++ b/src/baby_monitor/routers/sleep.py @@ -17,6 +17,7 @@ from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository from baby_monitor.repositories.dependencies.get_child_repository import ( get_child_repository, ) +from baby_monitor.utils.access_control import verify_child_access router = APIRouter(prefix="/api/sleep", tags=["sleep"]) @@ -30,12 +31,7 @@ def create_sleep( ) -> SleepResponse: """Create a new sleep log entry.""" # Verify the child belongs to the authenticated user - child = child_repo.get_by_id(request.child_id) - if not child: - raise HTTPException(status_code=404, detail="Child not found") - - if child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied to this child") + verify_child_access(child_repo, request.child_id, user_id) sleep = sleep_repo.create( child_id=request.child_id, @@ -91,9 +87,7 @@ def get_sleep( raise HTTPException(status_code=404, detail="Sleep log not found") # Verify the sleep belongs to user's child - child = child_repo.get_by_id(sleep["child_id"]) - if not child or child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied") + verify_child_access(child_repo, sleep["child_id"], user_id) return SleepResponse(**sleep) @@ -113,9 +107,7 @@ def update_sleep( raise HTTPException(status_code=404, detail="Sleep log not found") # Verify the sleep belongs to user's child - child = child_repo.get_by_id(sleep["child_id"]) - if not child or child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied") + verify_child_access(child_repo, sleep["child_id"], user_id) updated = sleep_repo.update( sleep_id=sleep_id, @@ -142,9 +134,7 @@ def delete_sleep( raise HTTPException(status_code=404, detail="Sleep log not found") # Verify the sleep belongs to user's child - child = child_repo.get_by_id(sleep["child_id"]) - if not child or child["user_id"] != user_id: - raise HTTPException(status_code=403, detail="Access denied") + verify_child_access(child_repo, sleep["child_id"], user_id) success = sleep_repo.delete(sleep_id) if not success: diff --git a/src/baby_monitor/static/admin.html b/src/baby_monitor/static/admin.html index e27d0c9..acf2843 100644 --- a/src/baby_monitor/static/admin.html +++ b/src/baby_monitor/static/admin.html @@ -185,6 +185,172 @@ background: #c82333; box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4); } + + .user-management h2::before { + content: "๐Ÿ‘ฅ"; + } + + .search-box { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + margin-bottom: 15px; + } + + .user-list { + max-height: 400px; + overflow-y: auto; + border: 1px solid #ddd; + border-radius: 4px; + } + + .user-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; + border-bottom: 1px solid #eee; + background: white; + } + + .user-item:last-child { + border-bottom: none; + } + + .user-item:hover { + background: #f8f9fa; + } + + .user-info { + flex: 1; + } + + .user-name { + font-weight: 600; + color: #333; + margin-bottom: 4px; + } + + .user-meta { + font-size: 12px; + color: #666; + } + + .admin-badge { + display: inline-block; + padding: 2px 8px; + background: #667eea; + color: white; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + margin-left: 8px; + } + + .delete-user-btn { + padding: 6px 12px; + background: #dc3545; + color: white; + border: none; + border-radius: 4px; + font-size: 13px; + cursor: pointer; + transition: background 0.2s; + } + + .delete-user-btn:hover { + background: #c82333; + } + + .delete-user-btn:disabled { + background: #ccc; + cursor: not-allowed; + } + + .modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + justify-content: center; + align-items: center; + } + + .modal.show { + display: flex; + } + + .modal-content { + background: white; + padding: 30px; + border-radius: 8px; + max-width: 500px; + width: 90%; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + } + + .modal h3 { + color: #333; + margin-bottom: 15px; + } + + .modal p { + color: #666; + margin-bottom: 20px; + line-height: 1.5; + } + + .modal-buttons { + display: flex; + gap: 10px; + justify-content: flex-end; + } + + .modal-button { + padding: 10px 20px; + border: none; + border-radius: 4px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; + } + + .modal-button.cancel { + background: #6c757d; + color: white; + } + + .modal-button.cancel:hover { + background: #5a6268; + } + + .modal-button.confirm { + background: #dc3545; + color: white; + } + + .modal-button.confirm:hover { + background: #c82333; + } + + .empty-state { + text-align: center; + padding: 40px 20px; + color: #999; + } + + .loading { + text-align: center; + padding: 20px; + color: #666; + } @@ -218,9 +384,46 @@
+
+

User Management

+

+ View and manage all users in the system. +

+ + + +
Loading users...
+
+ + + + + + +