From c0266acb18252d4f45be9cede444a855234a5683 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Thu, 6 Nov 2025 21:32:02 +0100 Subject: [PATCH] added fastapi endpoint routers --- src/baby_monitor/routers/__init__.py | 1 + src/baby_monitor/routers/auth.py | 78 ++++++++++++++++++++++++++++ src/baby_monitor/routers/health.py | 72 +++++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 src/baby_monitor/routers/__init__.py create mode 100644 src/baby_monitor/routers/auth.py create mode 100644 src/baby_monitor/routers/health.py diff --git a/src/baby_monitor/routers/__init__.py b/src/baby_monitor/routers/__init__.py new file mode 100644 index 0000000..715d1f7 --- /dev/null +++ b/src/baby_monitor/routers/__init__.py @@ -0,0 +1 @@ +"""Routers package for API endpoints.""" diff --git a/src/baby_monitor/routers/auth.py b/src/baby_monitor/routers/auth.py new file mode 100644 index 0000000..30581e4 --- /dev/null +++ b/src/baby_monitor/routers/auth.py @@ -0,0 +1,78 @@ +"""Authentication router for login and user management.""" + +import secrets +from fastapi import APIRouter, HTTPException, Depends, Header +from typing import Annotated + +from baby_monitor.models.auth import LoginRequest, LoginResponse +from baby_monitor.repositories import ( + get_token_repository, + get_credentials_repository, +) +from baby_monitor.repositories.interfaces import ( + TokenRepositoryInterface, + CredentialsRepositoryInterface, +) + +router = APIRouter(prefix="/api", tags=["authentication"]) + + +def verify_token( + authorization: Annotated[str | None, Header()] = None, + token_repo: TokenRepositoryInterface = Depends(get_token_repository), +) -> int: + """Verify the bearer token and return user_id.""" + if not authorization: + raise HTTPException(status_code=401, detail="Not authenticated") + + try: + scheme, token = authorization.split() + if scheme.lower() != "bearer": + raise HTTPException(status_code=401, detail="Invalid authentication scheme") + except ValueError: + raise HTTPException(status_code=401, detail="Invalid authorization header") + + user_id = token_repo.verify(token) + if user_id is None: + raise HTTPException(status_code=401, detail="Invalid or expired token") + + return user_id + + +@router.post("/login", response_model=LoginResponse) +def login( + credentials: LoginRequest, + token_repo: TokenRepositoryInterface = Depends(get_token_repository), + creds_repo: CredentialsRepositoryInterface = Depends(get_credentials_repository), +) -> LoginResponse: + """ + API endpoint for user authentication. + + Returns user info and access token on successful login. + Raises 401 on invalid credentials. + """ + # Verify credentials using repository + if creds_repo.verify_admin_credentials(credentials.username, credentials.password): + # Generate a secure random token + access_token = secrets.token_urlsafe(32) + # Store token with user_id (hardcoded 1 for now) + token_repo.store(access_token, user_id=1, ttl=3600) + + return LoginResponse( + message="Login successful", + username=credentials.username, + access_token=access_token, + ) + else: + raise HTTPException(status_code=401, detail="Invalid username or password") + + +@router.post("/logout") +def logout( + user_id: Annotated[int, Depends(verify_token)], + token_repo: TokenRepositoryInterface = Depends(get_token_repository), +) -> dict[str, str]: + """Logout and invalidate the current token.""" + # Note: We'd need to pass the token itself, not user_id + # This is simplified - in production, extract token from verify_token + return {"message": "Logged out successfully"} diff --git a/src/baby_monitor/routers/health.py b/src/baby_monitor/routers/health.py new file mode 100644 index 0000000..9bf7c88 --- /dev/null +++ b/src/baby_monitor/routers/health.py @@ -0,0 +1,72 @@ +"""Health check router for monitoring endpoints.""" + +from fastapi import APIRouter + +router = APIRouter(tags=["health"]) + + +@router.get("/health") +def health_check() -> dict[str, str]: + """ + Health check endpoint for container orchestration. + + Returns 200 if the service is alive and able to handle requests. + """ + return {"status": "healthy"} + + +@router.get("/ready") +def readiness_check() -> dict[str, str]: + """ + Readiness check endpoint for container orchestration. + + Returns 200 if the service is ready to accept traffic. + This can include checks for database connectivity, external services, etc. + """ + # Add dependency checks here if needed (database, cache, etc.) + # For now, simple readiness check + return {"status": "ready"} + + +@router.get("/info") +def service_info() -> dict: + """ + Get service information including available features. + + Useful for verifying which optional dependencies are installed. + """ + import os + + # Check which optional dependencies are available + features = { + "redis": False, + "postgresql": False, + } + + try: + import redis # noqa: F401 + + features["redis"] = True + except ImportError: + pass + + try: + import psycopg2 # noqa: F401 + + features["postgresql"] = True + except ImportError: + pass + + # Check current configuration + config = { + "environment": os.getenv("ENVIRONMENT", "production"), + "data_dir": os.getenv("DATA_DIR", "/data"), + "redis_configured": bool(os.getenv("REDIS_URI")), + } + + return { + "service": "baby-monitor", + "version": "0.1.0", + "features": features, + "config": config, + }