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.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

View File

@ -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"""

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):
"""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:
if not self.client:
self.connect()
if not self.client:
raise ValueError("Not connected to Firestore")
return self._db.collection(collection_name)
return self.client.collection(collection_name)
async def add_document(self, collection_name: str, data: Dict[str, Any]) -> str:
"""
@ -160,7 +179,26 @@ class FirestoreProvider:
List of documents
"""
try:
docs = self.get_collection(collection_name).stream()
# 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()
@ -168,8 +206,9 @@ class FirestoreProvider:
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:
"""

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
from typing import List, Optional
from bson import ObjectId
from src.db.repositories.firestore_repository import FirestoreRepository
from src.db.models.team import TeamModel
@ -10,5 +13,58 @@ 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()