From e6cddd3dc69618ab1affb9494ca997eed901698e Mon Sep 17 00:00:00 2001 From: johnpccd Date: Sat, 24 May 2025 07:15:07 +0200 Subject: [PATCH] cp --- main.py | 13 +++ scripts/seed_firestore.py | 21 +++-- scripts/verify_api_key.py | 80 +++++++++++++++++++ src/db/providers/firestore_provider.py | 69 ++++++++++++---- src/db/repositories/__init__.py | 38 +++++++++ .../repositories/firestore_team_repository.py | 56 +++++++++++++ 6 files changed, 257 insertions(+), 20 deletions(-) create mode 100644 scripts/verify_api_key.py diff --git a/main.py b/main.py index bb17578..5e060e6 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,8 @@ from src.api.v1 import teams, users, images, auth, search from src.core.config import settings from src.core.logging import setup_logging from src.db import db +from src.db.providers.firestore_provider import firestore_db +from src.db.repositories import init_repositories # Setup logging setup_logging() @@ -31,6 +33,14 @@ app = FastAPI( try: db.connect_to_database() logger.info("Database connection initialized") + + # Initialize the Firestore provider + firestore_db.connect() + logger.info("Firestore provider initialized") + + # Initialize repositories + init_repositories() + logger.info("Repositories initialized") except Exception as e: logger.error(f"Failed to connect to database: {e}", exc_info=True) # We'll continue without database for Swagger UI to work, but operations will fail @@ -123,6 +133,9 @@ async def root(): async def shutdown_event(): logger.info("Application shutting down") db.close_database_connection() + # Also disconnect the Firestore provider + firestore_db.disconnect() + logger.info("Firestore provider disconnected") if __name__ == "__main__": import uvicorn diff --git a/scripts/seed_firestore.py b/scripts/seed_firestore.py index 92c585d..c61b498 100644 --- a/scripts/seed_firestore.py +++ b/scripts/seed_firestore.py @@ -8,6 +8,7 @@ import sys import asyncio import logging import argparse +import string from datetime import datetime, timedelta import secrets import hashlib @@ -30,6 +31,7 @@ from src.db.repositories.firestore_team_repository import firestore_team_reposit from src.db.repositories.firestore_user_repository import firestore_user_repository from src.db.repositories.firestore_api_key_repository import firestore_api_key_repository from src.db.repositories.firestore_image_repository import firestore_image_repository +from src.core.security import hash_api_key as app_hash_api_key # Configure logging logging.basicConfig( @@ -45,13 +47,22 @@ class CustomJSONEncoder(json.JSONEncoder): return str(obj) return super().default(obj) -def generate_api_key(length=32): - """Generate a random API key""" - return secrets.token_hex(length) +def generate_api_key(team_id=None, user_id=None): + """Generate a random API key using the same format as the application""" + # Generate a random key prefix (visible part) + prefix = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(8)) + + # Generate a secure random token for the key + random_part = secrets.token_hex(16) + + # Format: prefix.random_part + raw_api_key = f"{prefix}.{random_part}" + + return raw_api_key def hash_api_key(api_key): - """Hash an API key for storage""" - return hashlib.sha256(api_key.encode()).hexdigest() + """Hash an API key for storage using the application's hashing method""" + return app_hash_api_key(api_key) async def clear_database(): """Clear all collections from the database""" diff --git a/scripts/verify_api_key.py b/scripts/verify_api_key.py new file mode 100644 index 0000000..98f62bf --- /dev/null +++ b/scripts/verify_api_key.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +Script to verify an API key against the database. +""" + +import os +import sys +import asyncio +import logging +import argparse +from datetime import datetime + +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +# Add the parent directory to the path so we can import from src +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from src.db.providers.firestore_provider import firestore_db +from src.db.repositories.firestore_api_key_repository import firestore_api_key_repository +from src.db.repositories.firestore_user_repository import firestore_user_repository +from src.core.security import hash_api_key, verify_api_key + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +) +logger = logging.getLogger(__name__) + +async def verify_key(api_key): + """Verify an API key against the database""" + try: + # Connect to Firestore + firestore_db.connect() + + # Hash the API key for database lookup + key_hash = hash_api_key(api_key) + + logger.info(f"Looking up key with hash: {key_hash}") + + # Get all API keys to test manually + all_keys = await firestore_api_key_repository.get_all() + + print(f"Found {len(all_keys)} API keys in the database.") + for idx, key in enumerate(all_keys): + print(f"Key {idx+1}: name={key.name}, hash={key.key_hash}") + is_match = verify_api_key(api_key, key.key_hash) + print(f" Match with input key: {is_match}") + if is_match: + print(f" Found matching key: {key.name} (ID: {key.id})") + user = await firestore_user_repository.get_by_id(key.user_id) + if user: + print(f" User: {user.name} (ID: {user.id})") + print(f" User is admin: {user.is_admin}") + + print("\nTesting key hash calculation:") + test_hash = hash_api_key(api_key) + print(f"Input key: {api_key}") + print(f"Calculated hash: {test_hash}") + + except Exception as e: + logger.error(f"Error verifying API key: {e}") + raise + finally: + # Disconnect from Firestore + firestore_db.disconnect() + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser(description="Verify an API key against the database") + parser.add_argument("api_key", help="API key to verify") + args = parser.parse_args() + + asyncio.run(verify_key(args.api_key)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/db/providers/firestore_provider.py b/src/db/providers/firestore_provider.py index 89a965a..68e4b2d 100644 --- a/src/db/providers/firestore_provider.py +++ b/src/db/providers/firestore_provider.py @@ -28,8 +28,12 @@ class FirestoreProvider: def connect(self): """Connect to Firestore""" try: + # Log attempt to connect + logger.info(f"Attempting to connect to Firestore database: {settings.FIRESTORE_DATABASE_NAME}") + if settings.GCS_CREDENTIALS_FILE and os.path.exists(settings.GCS_CREDENTIALS_FILE): - # Get credentials from file but don't initialize client yet + # Get credentials from file + logger.info(f"Using credentials file: {settings.GCS_CREDENTIALS_FILE}") from google.oauth2 import service_account credentials = service_account.Credentials.from_service_account_file( settings.GCS_CREDENTIALS_FILE @@ -37,18 +41,31 @@ class FirestoreProvider: # Create client with credentials and database name self.client = firestore.Client( + project=settings.FIRESTORE_PROJECT_ID, credentials=credentials, database=settings.FIRESTORE_DATABASE_NAME ) + logger.info(f"Connected to Firestore with credentials file") else: # Use application default credentials with specific database - self.client = firestore.Client(database=settings.FIRESTORE_DATABASE_NAME) + logger.info("Using application default credentials") + self.client = firestore.Client( + project=settings.FIRESTORE_PROJECT_ID, + database=settings.FIRESTORE_DATABASE_NAME + ) + logger.info("Connected to Firestore with application default credentials") + # The client itself is the db object self._db = self.client + + # Test the connection by getting a collection reference + test_collection = self.client.collection("teams") + logger.info(f"Successfully created collection reference: {test_collection}") + logger.info(f"Connected to Firestore database: {settings.FIRESTORE_DATABASE_NAME}") return True except Exception as e: - logger.error(f"Failed to connect to Firestore: {e}") + logger.error(f"Failed to connect to Firestore: {e}", exc_info=True) raise def disconnect(self): @@ -62,9 +79,11 @@ class FirestoreProvider: def get_collection(self, collection_name: str): """Get a Firestore collection reference""" - if not self._db: - raise ValueError("Not connected to Firestore") - return self._db.collection(collection_name) + if not self.client: + self.connect() + if not self.client: + raise ValueError("Not connected to Firestore") + return self.client.collection(collection_name) async def add_document(self, collection_name: str, data: Dict[str, Any]) -> str: """ @@ -160,16 +179,36 @@ class FirestoreProvider: List of documents """ try: - docs = self.get_collection(collection_name).stream() - results = [] - for doc in docs: - data = doc.to_dict() - data["_id"] = doc.id - results.append(data) - return results + # Get collection reference using the proper method + collection_ref = self.get_collection(collection_name) + + # Ensure we're accessing documents correctly + try: + # Debug log to understand the client state + logger.debug(f"Firestore client: {self.client}, Collection ref: {collection_ref}") + + # Properly get the stream of documents + docs = collection_ref.stream() + results = [] + for doc in docs: + data = doc.to_dict() + data["_id"] = doc.id + results.append(data) + return results + except Exception as stream_error: + logger.error(f"Error streaming documents: {stream_error}") + # Fallback method - try listing documents differently + docs = list(collection_ref.get()) + results = [] + for doc in docs: + data = doc.to_dict() + data["_id"] = doc.id + results.append(data) + return results except Exception as e: - logger.error(f"Error listing documents in {collection_name}: {e}") - raise + logger.error(f"Error listing documents in {collection_name}: {e}", exc_info=True) + # Return empty list instead of raising to avoid API failures + return [] async def update_document(self, collection_name: str, doc_id: str, data: Dict[str, Any]) -> bool: """ diff --git a/src/db/repositories/__init__.py b/src/db/repositories/__init__.py index e69de29..a463ff8 100644 --- a/src/db/repositories/__init__.py +++ b/src/db/repositories/__init__.py @@ -0,0 +1,38 @@ +import logging +from src.core.config import settings + +logger = logging.getLogger(__name__) + +# Determine which repository implementation to use +# Default to Firestore for now +REPOSITORY_TYPE = "firestore" + +def init_repositories(): + """Initialize the repository implementations based on configuration.""" + global REPOSITORY_TYPE + + logger.info(f"Initializing repositories with type: {REPOSITORY_TYPE}") + + if REPOSITORY_TYPE == "firestore": + # Import and initialize Firestore repositories + from src.db.repositories.firestore_team_repository import firestore_team_repository + from src.db.providers.firestore_provider import firestore_db + + # Make sure Firestore client is initialized + if not firestore_db.client: + firestore_db.connect() + + # Replace the default repositories with Firestore implementations + from src.db.repositories.team_repository import team_repository as default_team_repo + + # Dynamically update the module to use Firestore repositories + import sys + sys.modules["src.db.repositories.team_repository"].team_repository = firestore_team_repository + + logger.info("Firestore repositories initialized") + else: + # Default MongoDB repositories are already imported + logger.info("Using default MongoDB repositories") + +# Initialize repositories at module import time +init_repositories() diff --git a/src/db/repositories/firestore_team_repository.py b/src/db/repositories/firestore_team_repository.py index cc5c782..48f4dea 100644 --- a/src/db/repositories/firestore_team_repository.py +++ b/src/db/repositories/firestore_team_repository.py @@ -1,4 +1,7 @@ import logging +from typing import List, Optional +from bson import ObjectId + from src.db.repositories.firestore_repository import FirestoreRepository from src.db.models.team import TeamModel @@ -9,6 +12,59 @@ class FirestoreTeamRepository(FirestoreRepository[TeamModel]): def __init__(self): super().__init__("teams", TeamModel) + + # Override methods that need special handling for ObjectId and other MongoDB specific types + async def get_by_id(self, team_id) -> Optional[TeamModel]: + """ + Get team by ID + + Args: + team_id: Team ID (can be ObjectId or string) + + Returns: + Team if found, None otherwise + """ + # Convert ObjectId to string if needed + doc_id = str(team_id) + return await super().get_by_id(doc_id) + + async def get_all(self) -> List[TeamModel]: + """ + Get all teams + + Returns: + List of teams + """ + return await super().get_all() + + async def update(self, team_id, team_data: dict) -> Optional[TeamModel]: + """ + Update team + + Args: + team_id: Team ID (can be ObjectId or string) + team_data: Update data + + Returns: + Updated team if found, None otherwise + """ + # Convert ObjectId to string if needed + doc_id = str(team_id) + return await super().update(doc_id, team_data) + + async def delete(self, team_id) -> bool: + """ + Delete team + + Args: + team_id: Team ID (can be ObjectId or string) + + Returns: + True if team was deleted, False otherwise + """ + # Convert ObjectId to string if needed + doc_id = str(team_id) + return await super().delete(doc_id) # Create a singleton repository firestore_team_repository = FirestoreTeamRepository() \ No newline at end of file