This commit is contained in:
johnpccd 2025-05-24 07:15:07 +02:00
parent 3d584aeb88
commit e6cddd3dc6
6 changed files with 257 additions and 20 deletions

13
main.py
View File

@ -12,6 +12,8 @@ from src.api.v1 import teams, users, images, auth, search
from src.core.config import settings from src.core.config import settings
from src.core.logging import setup_logging from src.core.logging import setup_logging
from src.db import db 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
setup_logging() setup_logging()
@ -31,6 +33,14 @@ app = FastAPI(
try: try:
db.connect_to_database() db.connect_to_database()
logger.info("Database connection initialized") 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: except Exception as e:
logger.error(f"Failed to connect to database: {e}", exc_info=True) 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 # 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(): async def shutdown_event():
logger.info("Application shutting down") logger.info("Application shutting down")
db.close_database_connection() db.close_database_connection()
# Also disconnect the Firestore provider
firestore_db.disconnect()
logger.info("Firestore provider disconnected")
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn

View File

@ -8,6 +8,7 @@ import sys
import asyncio import asyncio
import logging import logging
import argparse import argparse
import string
from datetime import datetime, timedelta from datetime import datetime, timedelta
import secrets import secrets
import hashlib 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_user_repository import firestore_user_repository
from src.db.repositories.firestore_api_key_repository import firestore_api_key_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.db.repositories.firestore_image_repository import firestore_image_repository
from src.core.security import hash_api_key as app_hash_api_key
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
@ -45,13 +47,22 @@ class CustomJSONEncoder(json.JSONEncoder):
return str(obj) return str(obj)
return super().default(obj) return super().default(obj)
def generate_api_key(length=32): def generate_api_key(team_id=None, user_id=None):
"""Generate a random API key""" """Generate a random API key using the same format as the application"""
return secrets.token_hex(length) # 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): def hash_api_key(api_key):
"""Hash an API key for storage""" """Hash an API key for storage using the application's hashing method"""
return hashlib.sha256(api_key.encode()).hexdigest() return app_hash_api_key(api_key)
async def clear_database(): async def clear_database():
"""Clear all collections from the database""" """Clear all collections from the database"""

80
scripts/verify_api_key.py Normal file
View File

@ -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()

View File

@ -28,8 +28,12 @@ class FirestoreProvider:
def connect(self): def connect(self):
"""Connect to Firestore""" """Connect to Firestore"""
try: 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): 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 from google.oauth2 import service_account
credentials = service_account.Credentials.from_service_account_file( credentials = service_account.Credentials.from_service_account_file(
settings.GCS_CREDENTIALS_FILE settings.GCS_CREDENTIALS_FILE
@ -37,18 +41,31 @@ class FirestoreProvider:
# Create client with credentials and database name # Create client with credentials and database name
self.client = firestore.Client( self.client = firestore.Client(
project=settings.FIRESTORE_PROJECT_ID,
credentials=credentials, credentials=credentials,
database=settings.FIRESTORE_DATABASE_NAME database=settings.FIRESTORE_DATABASE_NAME
) )
logger.info(f"Connected to Firestore with credentials file")
else: else:
# Use application default credentials with specific database # 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 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}") logger.info(f"Connected to Firestore database: {settings.FIRESTORE_DATABASE_NAME}")
return True return True
except Exception as e: 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 raise
def disconnect(self): def disconnect(self):
@ -62,9 +79,11 @@ class FirestoreProvider:
def get_collection(self, collection_name: str): def get_collection(self, collection_name: str):
"""Get a Firestore collection reference""" """Get a Firestore collection reference"""
if not self._db: if not self.client:
raise ValueError("Not connected to Firestore") self.connect()
return self._db.collection(collection_name) 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: async def add_document(self, collection_name: str, data: Dict[str, Any]) -> str:
""" """
@ -160,16 +179,36 @@ class FirestoreProvider:
List of documents List of documents
""" """
try: try:
docs = self.get_collection(collection_name).stream() # Get collection reference using the proper method
results = [] collection_ref = self.get_collection(collection_name)
for doc in docs:
data = doc.to_dict() # Ensure we're accessing documents correctly
data["_id"] = doc.id try:
results.append(data) # Debug log to understand the client state
return results 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: except Exception as e:
logger.error(f"Error listing documents in {collection_name}: {e}") logger.error(f"Error listing documents in {collection_name}: {e}", exc_info=True)
raise # 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: async def update_document(self, collection_name: str, doc_id: str, data: Dict[str, Any]) -> bool:
""" """

View File

@ -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()

View File

@ -1,4 +1,7 @@
import logging import logging
from typing import List, Optional
from bson import ObjectId
from src.db.repositories.firestore_repository import FirestoreRepository from src.db.repositories.firestore_repository import FirestoreRepository
from src.db.models.team import TeamModel from src.db.models.team import TeamModel
@ -9,6 +12,59 @@ class FirestoreTeamRepository(FirestoreRepository[TeamModel]):
def __init__(self): def __init__(self):
super().__init__("teams", TeamModel) 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 # Create a singleton repository
firestore_team_repository = FirestoreTeamRepository() firestore_team_repository = FirestoreTeamRepository()