added repositories with dependency injection
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
"""Repository implementations package."""
|
||||
|
||||
from baby_monitor.repositories.dependencies import (
|
||||
get_credentials_repository,
|
||||
get_token_repository,
|
||||
get_user_repository,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"get_credentials_repository",
|
||||
"get_token_repository",
|
||||
"get_user_repository",
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Environment-based credentials repository."""
|
||||
|
||||
import os
|
||||
from baby_monitor.repositories.interfaces import CredentialsRepositoryInterface
|
||||
|
||||
|
||||
class EnvCredentialsRepository(CredentialsRepositoryInterface):
|
||||
"""
|
||||
Credentials repository that reads from environment variables.
|
||||
|
||||
Environment variables:
|
||||
- ADMIN_USERNAME: Admin username (default: "admin")
|
||||
- ADMIN_PASSWORD: Admin password (required in production)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.admin_username = os.getenv("ADMIN_USERNAME", "admin")
|
||||
self.admin_password = os.getenv("ADMIN_PASSWORD")
|
||||
|
||||
# Warn if using default password in production
|
||||
env = os.getenv("ENVIRONMENT", "production")
|
||||
if env == "production" and not self.admin_password:
|
||||
raise ValueError(
|
||||
"ADMIN_PASSWORD environment variable must be set in production"
|
||||
)
|
||||
|
||||
# Use default for development
|
||||
if not self.admin_password:
|
||||
self.admin_password = "password"
|
||||
|
||||
def verify_admin_credentials(self, username: str, password: str) -> bool:
|
||||
"""Verify admin username and password."""
|
||||
return username == self.admin_username and password == self.admin_password
|
||||
|
||||
def get_admin_username(self) -> str:
|
||||
"""Get the admin username."""
|
||||
return self.admin_username
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Dependency injection for repositories."""
|
||||
|
||||
from .get_credentials_repository import get_credentials_repository
|
||||
from .get_token_repository import get_token_repository
|
||||
from .get_user_repository import get_user_repository
|
||||
|
||||
__all__ = [
|
||||
"get_user_repository",
|
||||
"get_token_repository",
|
||||
"get_credentials_repository",
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Definition of get_credentials_repository dependency."""
|
||||
|
||||
import logging
|
||||
|
||||
from baby_monitor.repositories.interfaces import (
|
||||
CredentialsRepositoryInterface,
|
||||
)
|
||||
from baby_monitor.repositories.credentials.env_credentials import (
|
||||
EnvCredentialsRepository,
|
||||
)
|
||||
|
||||
# Singleton instance
|
||||
_credentials_repository: CredentialsRepositoryInterface | None = None
|
||||
|
||||
|
||||
def _initialize_credentials_repository() -> CredentialsRepositoryInterface:
|
||||
"""
|
||||
Initialize credentials repository based on environment.
|
||||
|
||||
Currently uses environment variables.
|
||||
Can be extended to support:
|
||||
- Database-backed credentials
|
||||
- External secret managers (AWS Secrets Manager, HashiCorp Vault)
|
||||
- LDAP/Active Directory
|
||||
"""
|
||||
|
||||
# Setup logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info("Using environment variable based credentials repository")
|
||||
return EnvCredentialsRepository()
|
||||
|
||||
|
||||
def get_credentials_repository() -> CredentialsRepositoryInterface:
|
||||
"""
|
||||
Get credentials repository instance.
|
||||
"""
|
||||
|
||||
global _credentials_repository
|
||||
|
||||
if _credentials_repository is None:
|
||||
_credentials_repository = _initialize_credentials_repository()
|
||||
|
||||
return _credentials_repository
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Database configuration and session management."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from collections.abc import Generator
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker, Session
|
||||
|
||||
# Database directory
|
||||
DATA_DIR = Path(os.getenv("DATA_DIR", "/data"))
|
||||
|
||||
# SQLite database URL
|
||||
DATABASE_URL = f"sqlite:///{DATA_DIR}/baby_monitor.db"
|
||||
|
||||
# Create engine with check_same_thread=False for SQLite
|
||||
engine = create_engine(
|
||||
DATABASE_URL,
|
||||
connect_args={"check_same_thread": False},
|
||||
echo=False, # Set to True for SQL query logging
|
||||
)
|
||||
|
||||
# Session factory
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# Base class for declarative models
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
"""Initialize the database by creating all tables."""
|
||||
# Ensure data directory exists
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
def get_database() -> Generator[Session, None, None]:
|
||||
"""
|
||||
Dependency that provides a database session.
|
||||
|
||||
Yields a database session and ensures it's closed after use.
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Definition of get_token_repository dependency."""
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from baby_monitor.repositories.interfaces import (
|
||||
TokenRepositoryInterface,
|
||||
)
|
||||
from baby_monitor.repositories.token.memory_token import (
|
||||
InMemoryTokenRepository,
|
||||
)
|
||||
|
||||
# Singleton instance
|
||||
_token_repository: TokenRepositoryInterface | None = None
|
||||
|
||||
|
||||
def _initialize_token_repository() -> TokenRepositoryInterface:
|
||||
"""
|
||||
Initialize token repository based on environment.
|
||||
|
||||
Uses Redis if REDIS_URI is set, otherwise uses in-memory storage.
|
||||
Raises exception if REDIS_URI is set but connection fails.
|
||||
"""
|
||||
|
||||
# Setup logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Check for REDIS_URI environment variable
|
||||
redis_uri = os.getenv("REDIS_URI")
|
||||
|
||||
# Use in-memory token repository if REDIS_URI is not set
|
||||
if not redis_uri:
|
||||
logger.info("Using in-memory token storage")
|
||||
return InMemoryTokenRepository()
|
||||
|
||||
# Attempt to use RedisTokenRepository
|
||||
try:
|
||||
import redis
|
||||
except ImportError as e:
|
||||
raise RuntimeError(
|
||||
"REDIS_URI is set but redis package is not installed. "
|
||||
"Install with: uv sync --extra redis"
|
||||
) from e
|
||||
|
||||
try:
|
||||
from baby_monitor.repositories.token.redis_token import (
|
||||
RedisTokenRepository,
|
||||
)
|
||||
|
||||
redis_client = redis.from_url(redis_uri)
|
||||
# Test connection
|
||||
redis_client.ping()
|
||||
logger.info(f"Connected to Redis at {redis_uri}")
|
||||
return RedisTokenRepository(redis_client)
|
||||
except redis.ConnectionError as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to connect to Redis at {redis_uri}. "
|
||||
f"Ensure Redis is running and accessible. Error: {e}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to initialize Redis token repository: {e}"
|
||||
) from e
|
||||
|
||||
|
||||
def get_token_repository() -> TokenRepositoryInterface:
|
||||
"""
|
||||
Get token repository instance.
|
||||
|
||||
Automatically uses Redis if REDIS_URI environment variable is set,
|
||||
otherwise falls back to in-memory storage.
|
||||
"""
|
||||
|
||||
global _token_repository
|
||||
|
||||
if _token_repository is None:
|
||||
_token_repository = _initialize_token_repository()
|
||||
|
||||
return _token_repository
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Definition of get_user_repository dependency."""
|
||||
|
||||
from typing import Annotated
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from baby_monitor.repositories.dependencies.get_database import (
|
||||
get_database,
|
||||
)
|
||||
from baby_monitor.repositories.interfaces import (
|
||||
UserRepositoryInterface,
|
||||
)
|
||||
from baby_monitor.repositories.user.sqlite_user import (
|
||||
SQLiteUserRepository,
|
||||
)
|
||||
|
||||
# Singleton instance
|
||||
_user_repository: UserRepositoryInterface | None = None
|
||||
|
||||
|
||||
def get_user_repository(
|
||||
db: Annotated[Session, Depends(get_database)],
|
||||
) -> UserRepositoryInterface:
|
||||
"""
|
||||
Get user repository instance.
|
||||
"""
|
||||
|
||||
global _user_repository
|
||||
|
||||
if _user_repository is None:
|
||||
_user_repository = SQLiteUserRepository(db)
|
||||
|
||||
return _user_repository
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Repository interfaces package."""
|
||||
|
||||
from .credentials_repository_interface import (
|
||||
CredentialsRepositoryInterface,
|
||||
)
|
||||
from .token_repository_interface import (
|
||||
TokenRepositoryInterface,
|
||||
)
|
||||
from .user_repository_interface import (
|
||||
UserRepositoryInterface,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"UserRepositoryInterface",
|
||||
"TokenRepositoryInterface",
|
||||
"CredentialsRepositoryInterface",
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Definition of CredentialsRepositoryInterface."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class CredentialsRepositoryInterface(ABC):
|
||||
"""Interface for credentials/secrets management."""
|
||||
|
||||
@abstractmethod
|
||||
def verify_admin_credentials(self, username: str, password: str) -> bool:
|
||||
"""Verify admin username and password."""
|
||||
|
||||
@abstractmethod
|
||||
def get_admin_username(self) -> str:
|
||||
"""Get the admin username."""
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Definition of the TokenRepositoryInterface."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class TokenRepositoryInterface(ABC):
|
||||
"""Interface for token storage operations."""
|
||||
|
||||
@abstractmethod
|
||||
def store(self, token: str, user_id: int, ttl: int = 3600) -> None:
|
||||
"""Store a token with optional TTL (time to live in seconds)."""
|
||||
|
||||
@abstractmethod
|
||||
def verify(self, token: str) -> int | None:
|
||||
"""Verify token and return user_id if valid, None otherwise."""
|
||||
|
||||
@abstractmethod
|
||||
def invalidate(self, token: str) -> None:
|
||||
"""Invalidate/delete a token."""
|
||||
|
||||
@abstractmethod
|
||||
def cleanup_expired(self) -> None:
|
||||
"""Remove expired tokens (for implementations without auto-expiry)."""
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Definition of UserRepositoryInterface."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class UserRepositoryInterface(ABC):
|
||||
"""Interface for user data operations."""
|
||||
|
||||
@abstractmethod
|
||||
def get_by_username(self, username: str) -> dict | None:
|
||||
"""Get user by username."""
|
||||
|
||||
@abstractmethod
|
||||
def create(self, username: str, hashed_password: str) -> dict:
|
||||
"""Create a new user."""
|
||||
|
||||
@abstractmethod
|
||||
def get_by_id(self, user_id: int) -> dict | None:
|
||||
"""Get user by ID."""
|
||||
@@ -0,0 +1,50 @@
|
||||
"""In-memory token repository (can be replaced with Redis)."""
|
||||
|
||||
from datetime import datetime, timedelta, UTC
|
||||
|
||||
from baby_monitor.repositories.interfaces import TokenRepositoryInterface
|
||||
|
||||
|
||||
class InMemoryTokenRepository(TokenRepositoryInterface):
|
||||
"""
|
||||
In-memory token storage.
|
||||
|
||||
This is a simple implementation that stores tokens in memory.
|
||||
Replace with RedisTokenRepository for production use.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# token -> (user_id, expiry_time)
|
||||
self._tokens: dict[str, tuple[int, datetime]] = {}
|
||||
|
||||
def store(self, token: str, user_id: int, ttl: int = 3600) -> None:
|
||||
"""Store a token with TTL (time to live in seconds)."""
|
||||
expiry = datetime.now(UTC) + timedelta(seconds=ttl)
|
||||
self._tokens[token] = (user_id, expiry)
|
||||
|
||||
def verify(self, token: str) -> int | None:
|
||||
"""Verify token and return user_id if valid, None otherwise."""
|
||||
if token not in self._tokens:
|
||||
return None
|
||||
|
||||
user_id, expiry = self._tokens[token]
|
||||
|
||||
# Check if token has expired
|
||||
if datetime.now(UTC) > expiry:
|
||||
del self._tokens[token]
|
||||
return None
|
||||
|
||||
return user_id
|
||||
|
||||
def invalidate(self, token: str) -> None:
|
||||
"""Invalidate/delete a token."""
|
||||
self._tokens.pop(token, None)
|
||||
|
||||
def cleanup_expired(self) -> None:
|
||||
"""Remove expired tokens."""
|
||||
now = datetime.utcnow()
|
||||
expired_tokens = [
|
||||
token for token, (_, expiry) in self._tokens.items() if now > expiry
|
||||
]
|
||||
for token in expired_tokens:
|
||||
del self._tokens[token]
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Redis token repository (future implementation)."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from baby_monitor.repositories.interfaces import TokenRepositoryInterface
|
||||
|
||||
|
||||
class RedisTokenRepository(TokenRepositoryInterface):
|
||||
"""
|
||||
Redis-based token storage.
|
||||
|
||||
Requires redis package: pip install redis
|
||||
Set REDIS_URI environment variable (e.g., redis://localhost:6379/0)
|
||||
"""
|
||||
|
||||
def __init__(self, redis_client: Any) -> None:
|
||||
"""
|
||||
Initialize with Redis client.
|
||||
|
||||
Args:
|
||||
redis_client: Redis client instance from redis.from_url()
|
||||
"""
|
||||
self.redis = redis_client
|
||||
|
||||
def store(self, token: str, user_id: int, ttl: int = 3600) -> None:
|
||||
"""Store a token with TTL (time to live in seconds)."""
|
||||
key = f"token:{token}"
|
||||
self.redis.setex(key, ttl, str(user_id))
|
||||
|
||||
def verify(self, token: str) -> int | None:
|
||||
"""Verify token and return user_id if valid, None otherwise."""
|
||||
key = f"token:{token}"
|
||||
user_id_str = self.redis.get(key)
|
||||
if user_id_str:
|
||||
return int(user_id_str)
|
||||
return None
|
||||
|
||||
def invalidate(self, token: str) -> None:
|
||||
"""Invalidate/delete a token."""
|
||||
key = f"token:{token}"
|
||||
self.redis.delete(key)
|
||||
|
||||
def cleanup_expired(self) -> None:
|
||||
"""Redis automatically handles expiration, so no cleanup needed."""
|
||||
pass # Redis handles TTL automatically
|
||||
@@ -0,0 +1,64 @@
|
||||
"""PostgreSQL user repository (future implementation)."""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from baby_monitor.repositories.interfaces import UserRepositoryInterface
|
||||
from baby_monitor.models.db.user import User
|
||||
|
||||
|
||||
class PostgreSQLUserRepository(UserRepositoryInterface):
|
||||
"""
|
||||
PostgreSQL-based user repository.
|
||||
|
||||
This implementation uses the same SQLAlchemy models as SQLite,
|
||||
just with a different database engine.
|
||||
|
||||
To use this implementation:
|
||||
1. Add psycopg2-binary dependency
|
||||
2. Update DATABASE_URL in database.py:
|
||||
DATABASE_URL = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql://user:password@localhost:5432/baby_monitor"
|
||||
)
|
||||
3. Replace SQLiteUserRepository with this in dependencies
|
||||
"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_by_username(self, username: str) -> dict | None:
|
||||
"""Get user by username."""
|
||||
user = self.db.query(User).filter(User.username == username).first()
|
||||
if user:
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"hashed_password": user.hashed_password,
|
||||
"created_at": user.created_at,
|
||||
}
|
||||
return None
|
||||
|
||||
def create(self, username: str, hashed_password: str) -> dict:
|
||||
"""Create a new user."""
|
||||
user = User(username=username, hashed_password=hashed_password)
|
||||
self.db.add(user)
|
||||
self.db.commit()
|
||||
self.db.refresh(user)
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"hashed_password": user.hashed_password,
|
||||
"created_at": user.created_at,
|
||||
}
|
||||
|
||||
def get_by_id(self, user_id: int) -> dict | None:
|
||||
"""Get user by ID."""
|
||||
user = self.db.query(User).filter(User.id == user_id).first()
|
||||
if user:
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"hashed_password": user.hashed_password,
|
||||
"created_at": user.created_at,
|
||||
}
|
||||
return None
|
||||
@@ -0,0 +1,50 @@
|
||||
"""SQLite implementation of user repository."""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from baby_monitor.repositories.interfaces import UserRepositoryInterface
|
||||
from baby_monitor.models.db.user import User
|
||||
|
||||
|
||||
class SQLiteUserRepository(UserRepositoryInterface):
|
||||
"""SQLite-based user repository."""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_by_username(self, username: str) -> dict | None:
|
||||
"""Get user by username."""
|
||||
user = self.db.query(User).filter(User.username == username).first()
|
||||
if user:
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"hashed_password": user.hashed_password,
|
||||
"created_at": user.created_at,
|
||||
}
|
||||
return None
|
||||
|
||||
def create(self, username: str, hashed_password: str) -> dict:
|
||||
"""Create a new user."""
|
||||
user = User(username=username, hashed_password=hashed_password)
|
||||
self.db.add(user)
|
||||
self.db.commit()
|
||||
self.db.refresh(user)
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"hashed_password": user.hashed_password,
|
||||
"created_at": user.created_at,
|
||||
}
|
||||
|
||||
def get_by_id(self, user_id: int) -> dict | None:
|
||||
"""Get user by ID."""
|
||||
user = self.db.query(User).filter(User.id == user_id).first()
|
||||
if user:
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"hashed_password": user.hashed_password,
|
||||
"created_at": user.created_at,
|
||||
}
|
||||
return None
|
||||
Reference in New Issue
Block a user