Skip to content
Merged

Main #18

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed .env
Empty file.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ENVIRONMENT=development
GCP_PROJECT_ID=nxtdo-dev
FIREBASE_SERVICE_ACCOUNT_KEY={"type":"service_account","project_id":"your-project-id","private_key_id":"your-key-id","private_key":"-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----\n","client_email":"your-service-account@project.iam.gserviceaccount.com","client_id":"your-client-id","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"your-cert-url","universe_domain":"googleapis.com"}

# Optional: Microsoft Azure Authentication
AZURE_CLIENT_ID=your_azure_client_id
AZURE_TENANT_ID=your_azure_tenant_id
23 changes: 22 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,26 @@ wheels/

# Virtual environments
.venv

# Environment files (NEVER COMMIT THESE)
.env
.env.local
.env.*
!.env.example

# Firebase keys and secrets
firebase-key.json
*-key.json
service-account*.json

# Test files
test_firebase.py

# IDEs
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db
41 changes: 33 additions & 8 deletions app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,45 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
from functools import lru_cache
from typing import Optional

class Settings(BaseSettings):
# App Settings
PROJECT_NAME: str = "NxtDo"
ENVIRONMENT: str = "development" # development, staging, production

# Firebase / GCP Settings (Injected via .env locally or GCP Console in Prod)
GCP_PROJECT_ID: str
DATABASE_URL: str
# Firebase / GCP Settings
GCP_PROJECT_ID: str = "nxtdo-dev"

# Authentication (Microsoft)
AZURE_CLIENT_ID: str
AZURE_TENANT_ID: str
# Firebase Service Account Key (JSON string)
FIREBASE_SERVICE_ACCOUNT_KEY: Optional[str] = None

# Database URL (optional, for backward compatibility)
DATABASE_URL: Optional[str] = None

# Authentication (Microsoft) - made optional for now
AZURE_CLIENT_ID: Optional[str] = None
AZURE_TENANT_ID: Optional[str] = None

# Tell Pydantic to look for a .env file locally
model_config = SettingsConfigDict(env_file=".env")
model_config = SettingsConfigDict(
env_file=".env.local",
env_file_encoding="utf-8",
extra="ignore"
)

def get_firebase_config(self) -> dict:
"""Get Firebase configuration based on environment"""
if self.ENVIRONMENT == "production":
return {
"project_id": "nxtdo-prod",
"storage_bucket": "nxtdo-prod.firebasestorage.app",
"database_id": "nxtdo-prod-db"
}
else:
return {
"project_id": "nxtdo-dev",
"storage_bucket": "nxtdo-dev.firebasestorage.app",
"database_id": "nxtdo-dev-db"
}

@lru_cache
def get_settings():
Expand Down
91 changes: 91 additions & 0 deletions app/core/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from .firebase import get_firestore_client
from .config import settings
from typing import Dict, List, Optional, Any
from datetime import datetime
import logging

logger = logging.getLogger(__name__)

def get_collection_name(base_name: str) -> str:
"""Get environment-prefixed collection name to avoid data mixing"""
env = settings.ENVIRONMENT
if env == "production":
return base_name # No prefix for production
return f"{env}_{base_name}" # e.g., staging_tasks, development_tasks

class DatabaseService:
def __init__(self):
self._db = None

@property
def db(self):
if self._db is None:
self._db = get_firestore_client()
return self._db

def get_collection(self, collection_name: str):
"""Get collection with environment prefix"""
prefixed_name = get_collection_name(collection_name)
logger.debug(f"Accessing collection: {prefixed_name}")
return self.db.collection(prefixed_name)

def create_document(self, collection: str, data: Dict[str, Any], doc_id: Optional[str] = None) -> str:
"""Create a document"""
try:
# Add timestamps
data["created_at"] = datetime.utcnow().isoformat()
data["updated_at"] = datetime.utcnow().isoformat()

collection_ref = self.get_collection(collection)

if doc_id:
collection_ref.document(doc_id).set(data)
return doc_id
else:
_, doc_ref = collection_ref.add(data)
return doc_ref.id
except Exception as e:
logger.error(f"Error creating document: {e}")
raise

def get_document(self, collection: str, doc_id: str) -> Optional[Dict[str, Any]]:
"""Get a document by ID"""
try:
doc = self.get_collection(collection).document(doc_id).get()
if doc.exists:
return {"id": doc.id, **doc.to_dict()}
return None
except Exception as e:
logger.error(f"Error getting document: {e}")
raise

def list_documents(self, collection: str, limit: int = 100) -> List[Dict[str, Any]]:
"""List documents in a collection"""
try:
docs = self.get_collection(collection).limit(limit).stream()
return [{"id": doc.id, **doc.to_dict()} for doc in docs]
except Exception as e:
logger.error(f"Error listing documents: {e}")
raise

def update_document(self, collection: str, doc_id: str, data: Dict[str, Any]) -> bool:
"""Update a document"""
try:
data["updated_at"] = datetime.utcnow().isoformat()
self.get_collection(collection).document(doc_id).update(data)
return True
except Exception as e:
logger.error(f"Error updating document: {e}")
raise

def delete_document(self, collection: str, doc_id: str) -> bool:
"""Delete a document"""
try:
self.get_collection(collection).document(doc_id).delete()
return True
except Exception as e:
logger.error(f"Error deleting document: {e}")
raise

# Global instance
db_service = DatabaseService()
63 changes: 63 additions & 0 deletions app/core/firebase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import firebase_admin
from firebase_admin import credentials, firestore
from .config import settings
import json
import logging

logger = logging.getLogger(__name__)

_firebase_app = None
_firestore_client = None

def get_firebase_app():
"""Initialize Firebase app (singleton)"""
global _firebase_app

if _firebase_app is not None:
return _firebase_app

if firebase_admin._apps:
_firebase_app = firebase_admin.get_app()
return _firebase_app

firebase_config = settings.get_firebase_config()

try:
if settings.FIREBASE_SERVICE_ACCOUNT_KEY:
try:
# Try parsing as JSON string first
service_account_info = json.loads(settings.FIREBASE_SERVICE_ACCOUNT_KEY)
cred = credentials.Certificate(service_account_info)
logger.info("Using service account key from environment variable")
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON in FIREBASE_SERVICE_ACCOUNT_KEY: {e}")
raise ValueError("FIREBASE_SERVICE_ACCOUNT_KEY is not valid JSON")
else:
# Fallback to Application Default Credentials (works on GCP)
cred = credentials.ApplicationDefault()
logger.info("Using Application Default Credentials")

_firebase_app = firebase_admin.initialize_app(cred, {
'projectId': firebase_config['project_id'],
'storageBucket': firebase_config['storage_bucket'],
'databaseId': firebase_config.get('database_id', 'nxtdo-dev-db')
})

logger.info(f"Firebase initialized for project: {firebase_config['project_id']}")
return _firebase_app

except Exception as e:
logger.error(f"Failed to initialize Firebase: {e}")
raise

def get_firestore_client():
"""Get Firestore client (singleton)"""
global _firestore_client

if _firestore_client is not None:
return _firestore_client

get_firebase_app()
# For non-default databases, use the database parameter
_firestore_client = firestore.client(database_id='nxtdo-dev-db')
return _firestore_client
Loading
Loading