Merge pull request 'updated admin page with more functionality, simplified burger menu and added many-to-many parent-child relationship' (#20) from expand-admin-page into main
Build and Push Docker Image / build-and-push (push) Successful in 25s
Python Code Quality / python-code-quality (push) Successful in 9s
Python Test / python-test (push) Successful in 17s

Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
2025-11-10 23:01:33 +01:00
26 changed files with 952 additions and 564 deletions
+12
View File
@@ -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)."""
-1
View File
@@ -19,5 +19,4 @@ class ChildResponse(BaseModel):
name: str
birth_time: datetime
birth_weight: float
user_id: int
created_at: datetime
+2 -3
View File
@@ -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"<Child(id={self.id}, name='{self.name}', user_id={self.user_id})>"
return f"<Child(id={self.id}, name='{self.name}')>"
@@ -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"<ChildParent(id={self.id}, "
f"child_id={self.child_id}, user_id={self.user_id})>"
)
@@ -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
@@ -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
@@ -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()
)
@@ -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()
)
@@ -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",
]
@@ -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."""
@@ -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."""
@@ -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()
)
@@ -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
+190 -1
View File
@@ -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,
)
+4 -2
View File
@@ -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
+5 -15
View File
@@ -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:
+5 -15
View File
@@ -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:
+5 -15
View File
@@ -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:
+349 -1
View File
@@ -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;
}
</style>
</head>
<body>
@@ -218,9 +384,46 @@
<div id="error" class="error"></div>
</div>
<div class="section user-management">
<h2>User Management</h2>
<p style="color: #666; margin-bottom: 15px; font-size: 14px">
View and manage all users in the system.
</p>
<input
type="text"
id="userSearch"
class="search-box"
placeholder="Search users by username..."
oninput="filterUsers()"
/>
<div id="userListContainer" class="loading">Loading users...</div>
</div>
<button class="button logout-button" onclick="logout()">Logout</button>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal" id="deleteModal">
<div class="modal-content">
<h3>⚠️ Confirm User Deletion</h3>
<p id="deleteMessage"></p>
<p style="color: #dc3545; font-weight: 600">
This action cannot be undone. All associated children and their logs
(feedings, diapers, sleep) will be permanently deleted.
</p>
<div class="modal-buttons">
<button class="modal-button cancel" onclick="closeDeleteModal()">
Cancel
</button>
<button class="modal-button confirm" onclick="confirmDelete()">
Delete User
</button>
</div>
</div>
</div>
<script>
// Check if user is authenticated and is admin
const token = localStorage.getItem("access_token");
@@ -228,7 +431,10 @@
window.location.href = "/login.html";
}
// Verify user is admin
let allUsers = [];
let userToDelete = null;
// Verify user is admin and load data
fetch("/api/me", {
headers: {
Authorization: `Bearer ${token}`,
@@ -240,12 +446,154 @@
alert("Access denied. Admin privileges required.");
window.location.href = "/";
}
// Load users after verifying admin status
loadUsers();
})
.catch((error) => {
console.error("Error verifying admin status:", error);
window.location.href = "/login.html";
});
async function loadUsers() {
const container = document.getElementById("userListContainer");
try {
const response = await fetch("/api/admin/users", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to load users");
}
allUsers = await response.json();
renderUsers(allUsers);
} catch (error) {
console.error("Error loading users:", error);
container.innerHTML = `
<div class="empty-state">
❌ Failed to load users: ${error.message}
</div>
`;
}
}
function renderUsers(users) {
const container = document.getElementById("userListContainer");
if (users.length === 0) {
container.innerHTML = `
<div class="empty-state">
No users found.
</div>
`;
return;
}
container.className = "user-list";
container.innerHTML = users
.map(
(user) => `
<div class="user-item">
<div class="user-info">
<div class="user-name">
${user.username}
${user.is_admin ? '<span class="admin-badge">ADMIN</span>' : ""}
</div>
<div class="user-meta">
ID: ${user.id} • Created: ${new Date(user.created_at).toLocaleDateString()}
</div>
</div>
<button
class="delete-user-btn"
onclick="showDeleteModal(${user.id}, '${user.username}', ${user.is_admin})"
${user.is_admin ? 'disabled title="Cannot delete admin users"' : ""}
>
Delete
</button>
</div>
`,
)
.join("");
}
function filterUsers() {
const searchTerm = document
.getElementById("userSearch")
.value.toLowerCase();
const filtered = allUsers.filter((user) =>
user.username.toLowerCase().includes(searchTerm),
);
renderUsers(filtered);
}
function showDeleteModal(userId, username, isAdmin) {
if (isAdmin) {
return; // Don't allow deleting admin users
}
userToDelete = { id: userId, username };
const modal = document.getElementById("deleteModal");
const message = document.getElementById("deleteMessage");
message.textContent = `Are you sure you want to delete user "${username}"?`;
modal.classList.add("show");
}
function closeDeleteModal() {
const modal = document.getElementById("deleteModal");
modal.classList.remove("show");
userToDelete = null;
}
async function confirmDelete() {
if (!userToDelete) return;
const modal = document.getElementById("deleteModal");
const confirmBtn = modal.querySelector(".modal-button.confirm");
confirmBtn.disabled = true;
confirmBtn.textContent = "Deleting...";
try {
const response = await fetch(`/api/admin/users/${userToDelete.id}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Failed to delete user");
}
const result = await response.json();
// Show success message
alert(
`${result.message}\n\n` +
`Deleted:\n` +
`${result.deleted_children.length} children\n` +
`${result.deleted_feedings} feedings\n` +
`${result.deleted_diapers} diaper changes\n` +
`${result.deleted_sleeps} sleep sessions`,
);
// Reload user list
await loadUsers();
closeDeleteModal();
} catch (error) {
console.error("Error deleting user:", error);
alert(`Failed to delete user: ${error.message}`);
} finally {
confirmBtn.disabled = false;
confirmBtn.textContent = "Delete User";
}
}
async function generateLink() {
const btn = document.getElementById("generateBtn");
const linkContainer = document.getElementById("linkContainer");
+6 -124
View File
@@ -3,7 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diaper Changes - Baby Monitor</title>
<title>Diapers - Baby Monitor</title>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
margin: 0;
@@ -240,93 +241,9 @@
.edit-button:hover {
background: #d77ee6;
}
.burger-menu {
position: fixed;
top: 1rem;
left: 1rem;
cursor: pointer;
z-index: 1000;
background: white;
padding: 0.5rem;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.burger-menu div {
width: 25px;
height: 3px;
background-color: #333;
margin: 5px 0;
transition: 0.3s;
}
.menu-overlay {
position: fixed;
top: 0;
left: -250px;
width: 250px;
height: 100vh;
background: white;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: left 0.3s;
z-index: 999;
padding: 4rem 1rem 1rem 1rem;
}
.menu-overlay.open {
left: 0;
}
.menu-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
display: none;
z-index: 998;
}
.menu-backdrop.open {
display: block;
}
.menu-item {
padding: 1rem;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s;
}
.menu-item:hover {
background: #f5f5f5;
}
.menu-item.logout {
color: #dc3545;
font-weight: 600;
}
</style>
</head>
<body>
<div class="burger-menu" id="burgerMenu">
<div></div>
<div></div>
<div></div>
</div>
<div class="menu-backdrop" id="menuBackdrop"></div>
<div class="menu-overlay" id="menuOverlay">
<div class="menu-item" id="menuHome">🏠 Home</div>
<div class="menu-item" id="menuAddChild">👶 Add Child</div>
<div class="menu-item" id="menuFeedings">🍼 Feedings</div>
<div class="menu-item" id="menuSleep">😴 Sleep</div>
<div class="menu-item logout" id="menuLogout">🚪 Logout</div>
</div>
<div class="container">
<h1>🧷 Diaper Changes</h1>
<p class="subtitle">Overview of all diaper change sessions</p>
@@ -337,6 +254,7 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="/menu.js"></script>
<script>
// Check if user is authenticated
const token = localStorage.getItem("access_token");
@@ -344,50 +262,14 @@
window.location.href = "/login.html";
}
// Initialize burger menu
initBurgerMenu({ includeHome: true });
let allDiaperChanges = [];
let allChildren = [];
let currentDaysRange = 7;
let selectedChildId = null; // null means "All Children"
// Burger menu functionality
const burgerMenu = document.getElementById("burgerMenu");
const menuOverlay = document.getElementById("menuOverlay");
const menuBackdrop = document.getElementById("menuBackdrop");
function toggleMenu() {
menuOverlay.classList.toggle("open");
menuBackdrop.classList.toggle("open");
}
function closeMenu() {
menuOverlay.classList.remove("open");
menuBackdrop.classList.remove("open");
}
burgerMenu.addEventListener("click", toggleMenu);
menuBackdrop.addEventListener("click", closeMenu);
document.getElementById("menuHome").addEventListener("click", () => {
window.location.href = "/";
});
document.getElementById("menuAddChild").addEventListener("click", () => {
window.location.href = "/add-child.html";
});
document.getElementById("menuFeedings").addEventListener("click", () => {
window.location.href = "/feedings.html";
});
document.getElementById("menuSleep").addEventListener("click", () => {
window.location.href = "/sleep.html";
});
document.getElementById("menuLogout").addEventListener("click", () => {
closeMenu();
logout();
});
// Load diaper changes data
async function loadDiaperChanges() {
try {
+5 -123
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Feedings - Baby Monitor</title>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
margin: 0;
@@ -277,93 +278,9 @@
.edit-button:hover {
background: #5568d3;
}
.burger-menu {
position: fixed;
top: 1rem;
left: 1rem;
cursor: pointer;
z-index: 1000;
background: white;
padding: 0.5rem;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.burger-menu div {
width: 25px;
height: 3px;
background-color: #333;
margin: 5px 0;
transition: 0.3s;
}
.menu-overlay {
position: fixed;
top: 0;
left: -250px;
width: 250px;
height: 100vh;
background: white;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: left 0.3s;
z-index: 999;
padding: 4rem 1rem 1rem 1rem;
}
.menu-overlay.open {
left: 0;
}
.menu-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
display: none;
z-index: 998;
}
.menu-backdrop.open {
display: block;
}
.menu-item {
padding: 1rem;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s;
}
.menu-item:hover {
background: #f5f5f5;
}
.menu-item.logout {
color: #dc3545;
font-weight: 600;
}
</style>
</head>
<body>
<div class="burger-menu" id="burgerMenu">
<div></div>
<div></div>
<div></div>
</div>
<div class="menu-backdrop" id="menuBackdrop"></div>
<div class="menu-overlay" id="menuOverlay">
<div class="menu-item" id="menuHome">🏠 Home</div>
<div class="menu-item" id="menuAddChild">👶 Add Child</div>
<div class="menu-item" id="menuDiapers">🧷 Diapers</div>
<div class="menu-item" id="menuSleep">😴 Sleep</div>
<div class="menu-item logout" id="menuLogout">🚪 Logout</div>
</div>
<div class="container">
<h1>🍼 Feedings</h1>
<p class="subtitle">Overview of all feeding sessions</p>
@@ -374,6 +291,7 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="/menu.js"></script>
<script>
// Check if user is authenticated
const token = localStorage.getItem("access_token");
@@ -381,50 +299,14 @@
window.location.href = "/login.html";
}
// Initialize burger menu
initBurgerMenu({ includeHome: true });
let allFeedings = [];
let allChildren = [];
let currentDaysRange = 7;
let selectedChildId = null; // null means "All Children"
// Burger menu functionality
const burgerMenu = document.getElementById("burgerMenu");
const menuOverlay = document.getElementById("menuOverlay");
const menuBackdrop = document.getElementById("menuBackdrop");
function toggleMenu() {
menuOverlay.classList.toggle("open");
menuBackdrop.classList.toggle("open");
}
function closeMenu() {
menuOverlay.classList.remove("open");
menuBackdrop.classList.remove("open");
}
burgerMenu.addEventListener("click", toggleMenu);
menuBackdrop.addEventListener("click", closeMenu);
document.getElementById("menuHome").addEventListener("click", () => {
window.location.href = "/";
});
document.getElementById("menuAddChild").addEventListener("click", () => {
window.location.href = "/add-child.html";
});
document.getElementById("menuDiapers").addEventListener("click", () => {
window.location.href = "/diapers.html";
});
document.getElementById("menuSleep").addEventListener("click", () => {
window.location.href = "/sleep.html";
});
document.getElementById("menuLogout").addEventListener("click", () => {
closeMenu();
logout();
});
// Load feedings data
async function loadFeedings() {
try {
+14 -125
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baby Monitor</title>
<link rel="stylesheet" href="/menu.css" />
<style>
body {
font-family: Arial, sans-serif;
@@ -12,69 +13,6 @@
padding: 0 1rem;
background-color: #f0f0f0;
}
.burger-menu {
position: fixed;
top: 1rem;
left: 1rem;
cursor: pointer;
z-index: 1000;
background: white;
padding: 0.5rem;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.burger-menu div {
width: 25px;
height: 3px;
background-color: #333;
margin: 5px 0;
transition: 0.3s;
}
.menu-overlay {
position: fixed;
top: 0;
left: -250px;
width: 250px;
height: 100vh;
background: white;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: left 0.3s;
z-index: 999;
padding: 4rem 1rem 1rem 1rem;
}
.menu-overlay.open {
left: 0;
}
.menu-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
display: none;
z-index: 998;
}
.menu-backdrop.open {
display: block;
}
.menu-item {
padding: 1rem;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s;
}
.menu-item:hover {
background: #f5f5f5;
}
.menu-item.admin {
color: #667eea;
font-weight: 600;
}
.menu-item.logout {
color: #dc3545;
font-weight: 600;
}
.container {
background: white;
padding: 2rem;
@@ -233,22 +171,6 @@
</style>
</head>
<body>
<div class="burger-menu" id="burgerMenu">
<div></div>
<div></div>
<div></div>
</div>
<div class="menu-backdrop" id="menuBackdrop"></div>
<div class="menu-overlay" id="menuOverlay">
<div class="menu-item" id="menuAddChild">👶 Add Child</div>
<div class="menu-item" id="menuFeedings">🍼 Feedings</div>
<div class="menu-item" id="menuDiapers">🧷 Diapers</div>
<div class="menu-item" id="menuSleep">😴 Sleep</div>
<div class="menu-item logout" id="menuLogout">🚪 Logout</div>
</div>
<div class="container">
<div id="content" class="loading">
<p>Loading...</p>
@@ -314,49 +236,11 @@
</div>
</div>
<script src="/menu.js"></script>
<script>
const token = localStorage.getItem("access_token");
const username = localStorage.getItem("username");
// Burger menu functionality
const burgerMenu = document.getElementById("burgerMenu");
const menuOverlay = document.getElementById("menuOverlay");
const menuBackdrop = document.getElementById("menuBackdrop");
function toggleMenu() {
menuOverlay.classList.toggle("open");
menuBackdrop.classList.toggle("open");
}
function closeMenu() {
menuOverlay.classList.remove("open");
menuBackdrop.classList.remove("open");
}
burgerMenu.addEventListener("click", toggleMenu);
menuBackdrop.addEventListener("click", closeMenu);
document.getElementById("menuAddChild").addEventListener("click", () => {
window.location.href = "/add-child.html";
});
document.getElementById("menuFeedings").addEventListener("click", () => {
window.location.href = "/feedings.html";
});
document.getElementById("menuDiapers").addEventListener("click", () => {
window.location.href = "/diapers.html";
});
document.getElementById("menuSleep").addEventListener("click", () => {
window.location.href = "/sleep.html";
});
document.getElementById("menuLogout").addEventListener("click", () => {
closeMenu();
logout();
});
// Check if user is logged in
if (!token) {
window.location.href = "/login.html";
@@ -379,6 +263,11 @@
}
})
.then((user) => {
// Initialize burger menu
initBurgerMenu({
includeHome: false,
});
// Check if user has any children
return fetch("/api/children", {
headers: {
@@ -529,7 +418,7 @@
const feedingButtonHtml = activeFeeding
? `
<button class="feeding-button active" onclick="stopFeeding()">
🛑 Stop Feeding
🛑 Stopped Feeding
<div class="feeding-info">
${activeFeeding.child_name || "Child"} - ${formatFeedingType(activeFeeding.feeding_type)}<br>
Started <span id="feedingTimeInfo">${formatTime(activeFeeding.start_time)}</span>
@@ -538,14 +427,14 @@
`
: `
<button class="feeding-button" onclick="showStartFeedingModal()">
▶️ Start Feeding
🍼 Started Feeding
</button>
`;
const sleepButtonHtml = activeSleep
? `
<button class="sleep-button active" onclick="stopSleep()">
🛑 Stop Sleep
🛑 Stopped Sleeping
<div class="sleep-info">
${activeSleep.child_name || "Child"}<br>
Started <span id="sleepTimeInfo">${formatTime(activeSleep.start_time)}</span>
@@ -554,7 +443,7 @@
`
: `
<button class="sleep-button" onclick="showStartSleepModal()">
😴 Start Sleep
😴 Started Sleeping
</button>
`;
@@ -563,11 +452,11 @@
<div class="user-info">
<p><strong>Logged in as:</strong> ${username}</p>
</div>
<button class="feeding-button" onclick="window.location.href='/log-diaper.html'" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); margin-bottom: 20px;">
🧷 Changed Diaper
</button>
${feedingButtonHtml}
${sleepButtonHtml}
<button class="feeding-button" onclick="window.location.href='/log-diaper.html'" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); margin-top: 20px;">
🧷 Log Diaper Change
</button>
`;
}
+73
View File
@@ -0,0 +1,73 @@
/* Burger Menu Styles */
.burger-menu {
position: fixed;
top: 1rem;
left: 1rem;
cursor: pointer;
z-index: 1000;
background: white;
padding: 0.5rem;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.burger-menu div {
width: 25px;
height: 3px;
background-color: #333;
margin: 5px 0;
transition: 0.3s;
}
.menu-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
display: none;
z-index: 998;
}
.menu-backdrop.open {
display: block;
}
.menu-overlay {
position: fixed;
top: 0;
left: -250px;
width: 250px;
height: 100vh;
background: white;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: left 0.3s;
z-index: 999;
padding: 4rem 1rem 1rem 1rem;
}
.menu-overlay.open {
left: 0;
}
.menu-item {
padding: 1rem;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s;
}
.menu-item:hover {
background: #f5f5f5;
}
.menu-item.admin {
color: #667eea;
font-weight: 600;
}
.menu-item.logout {
color: #dc3545;
font-weight: 600;
}
+106
View File
@@ -0,0 +1,106 @@
/**
* Initializes the burger menu navigation
* @param {Object} options - Configuration options
* @param {boolean} options.includeHome - Whether to include Home menu item
* @param {boolean} options.includeFeedings - Whether to include Feedings menu item
* @param {boolean} options.includeDiapers - Whether to include Diapers menu item
* @param {boolean} options.includeSleep - Whether to include Sleep menu item
*/
function initBurgerMenu(options = {}) {
const {
includeHome = true,
includeFeedings = true,
includeDiapers = true,
includeSleep = true,
} = options;
// Create burger menu HTML
const burgerMenuHTML = `
<div class="burger-menu" id="burgerMenu">
<div></div>
<div></div>
<div></div>
</div>
<div class="menu-backdrop" id="menuBackdrop"></div>
<div class="menu-overlay" id="menuOverlay">
${includeHome ? '<div class="menu-item" id="menuHome">🏠 Home</div>' : ""}
${includeDiapers ? '<div class="menu-item" id="menuDiapers">🧷 Diapers</div>' : ""}
${includeFeedings ? '<div class="menu-item" id="menuFeedings">🍼 Feedings</div>' : ""}
${includeSleep ? '<div class="menu-item" id="menuSleep">😴 Sleep</div>' : ""}
<div class="menu-item" id="menuAddChild">👶 Add Child</div>
<div class="menu-item logout" id="menuLogout">🚪 Logout</div>
</div>
`;
// Insert menu at the beginning of body
document.body.insertAdjacentHTML("afterbegin", burgerMenuHTML);
// Get elements
const burgerMenu = document.getElementById("burgerMenu");
const menuOverlay = document.getElementById("menuOverlay");
const menuBackdrop = document.getElementById("menuBackdrop");
// Toggle menu function
function toggleMenu() {
menuOverlay.classList.toggle("open");
menuBackdrop.classList.toggle("open");
}
// Close menu function
function closeMenu() {
menuOverlay.classList.remove("open");
menuBackdrop.classList.remove("open");
}
// Event listeners for menu toggle
burgerMenu.addEventListener("click", toggleMenu);
menuBackdrop.addEventListener("click", closeMenu);
// Navigation event listeners
if (includeHome) {
document.getElementById("menuHome")?.addEventListener("click", () => {
window.location.href = "/";
});
}
document.getElementById("menuAddChild").addEventListener("click", () => {
window.location.href = "/add-child.html";
});
if (includeFeedings) {
document.getElementById("menuFeedings")?.addEventListener("click", () => {
window.location.href = "/feedings.html";
});
}
if (includeDiapers) {
document.getElementById("menuDiapers")?.addEventListener("click", () => {
window.location.href = "/diapers.html";
});
}
if (includeSleep) {
document.getElementById("menuSleep")?.addEventListener("click", () => {
window.location.href = "/sleep.html";
});
}
document.getElementById("menuLogout").addEventListener("click", () => {
closeMenu();
// Call the page's logout function if it exists, otherwise use default
if (typeof logout === "function") {
logout();
} else {
defaultLogout();
}
});
// Default logout function
function defaultLogout() {
localStorage.removeItem("access_token");
localStorage.removeItem("username");
window.location.href = "/login.html";
}
}
+5 -124
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sleep - Baby Monitor</title>
<link rel="stylesheet" href="/menu.css" />
<style>
* {
margin: 0;
@@ -20,66 +21,6 @@
padding: 20px;
}
.burger-menu {
position: fixed;
top: 1rem;
left: 1rem;
cursor: pointer;
z-index: 1000;
background: white;
padding: 0.5rem;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.burger-menu div {
width: 25px;
height: 3px;
background-color: #333;
margin: 5px 0;
transition: 0.3s;
}
.menu-overlay {
position: fixed;
top: 0;
left: -250px;
width: 250px;
height: 100vh;
background: white;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: left 0.3s;
z-index: 999;
padding: 4rem 1rem 1rem 1rem;
}
.menu-overlay.open {
left: 0;
}
.menu-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
display: none;
z-index: 998;
}
.menu-backdrop.open {
display: block;
}
.menu-item {
padding: 1rem;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s;
}
.menu-item:hover {
background: #f5f5f5;
}
.menu-item.logout {
color: #dc3545;
font-weight: 600;
}
.container {
background: white;
border-radius: 12px;
@@ -335,23 +276,6 @@
</style>
</head>
<body>
<div class="burger-menu" id="burgerMenu">
<div></div>
<div></div>
<div></div>
</div>
<div class="menu-backdrop" id="menuBackdrop"></div>
<div class="menu-overlay" id="menuOverlay">
<div class="menu-item" id="menuHome">🏠 Home</div>
<div class="menu-item" id="menuAddChild">👶 Add Child</div>
<div class="menu-item" id="menuFeedings">🍼 Feedings</div>
<div class="menu-item" id="menuDiapers">🧷 Diapers</div>
<div class="menu-item" id="menuSleep">😴 Sleep</div>
<div class="menu-item logout" id="menuLogout">🚪 Logout</div>
</div>
<div class="container">
<h1>😴 Sleep Tracking</h1>
<p class="subtitle">Monitor your baby's sleep patterns</p>
@@ -382,57 +306,18 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="/menu.js"></script>
<script>
const token = localStorage.getItem("access_token");
// Burger menu functionality
const burgerMenu = document.getElementById("burgerMenu");
const menuOverlay = document.getElementById("menuOverlay");
const menuBackdrop = document.getElementById("menuBackdrop");
function toggleMenu() {
menuOverlay.classList.toggle("open");
menuBackdrop.classList.toggle("open");
}
function closeMenu() {
menuOverlay.classList.remove("open");
menuBackdrop.classList.remove("open");
}
burgerMenu.addEventListener("click", toggleMenu);
menuBackdrop.addEventListener("click", closeMenu);
document.getElementById("menuHome").addEventListener("click", () => {
window.location.href = "/";
});
document.getElementById("menuAddChild").addEventListener("click", () => {
window.location.href = "/add-child.html";
});
document.getElementById("menuFeedings").addEventListener("click", () => {
window.location.href = "/feedings.html";
});
document.getElementById("menuDiapers").addEventListener("click", () => {
window.location.href = "/diapers.html";
});
document.getElementById("menuSleep").addEventListener("click", () => {
closeMenu();
});
document.getElementById("menuLogout").addEventListener("click", () => {
closeMenu();
logout();
});
// Check if user is logged in
if (!token) {
window.location.href = "/login.html";
}
// Initialize burger menu
initBurgerMenu({ includeHome: true });
let allSleeps = [];
let allChildren = [];
let selectedChildId = "";
@@ -584,10 +469,6 @@
<h3>Total Sessions</h3>
<div class="value">${totalSessions}</div>
</div>
<div class="stat-card">
<h3>Ongoing</h3>
<div class="value">${ongoingSessions}</div>
</div>
<div class="stat-card">
<h3>Avg Duration</h3>
<div class="value">${formatDuration(avgMinutes)}</div>
+26
View File
@@ -0,0 +1,26 @@
"""Access control utility functions for verifying resource ownership."""
from fastapi import HTTPException
from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository
def verify_child_access(
child_repo: SQLiteChildRepository, child_id: int, user_id: int
) -> None:
"""Verify that user has access to the child.
Args:
child_repo: Child repository instance
child_id: ID of the child to verify access for
user_id: ID of the user requesting access
Raises:
HTTPException: 404 if child not found, 403 if user is not a parent
"""
child = child_repo.get_by_id(child_id)
if not child:
raise HTTPException(status_code=404, detail="Child not found")
parent_ids = child_repo.get_parent_ids(child_id)
if user_id not in parent_ids:
raise HTTPException(status_code=403, detail="Access denied to this child")