chore: enhance security configuration across applications

- Add root-level *.json to .gitignore to prevent wallet backup leaks
- Replace wildcard CORS origins with explicit localhost URLs across all apps
- Add OPTIONS method to CORS allowed methods for preflight requests
- Update coordinator database to use absolute path in data/ directory to prevent duplicates
- Add JWT secret validation in coordinator config (must be set via environment)
- Replace deprecated get_session dependency with Session
This commit is contained in:
oib
2026-02-13 16:07:03 +01:00
parent e9646cc7dd
commit c984a1e052
13 changed files with 434 additions and 120 deletions

View File

@@ -1,5 +1,7 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import List, Optional
from pathlib import Path
import os
class Settings(BaseSettings):
@@ -9,14 +11,35 @@ class Settings(BaseSettings):
app_host: str = "127.0.0.1"
app_port: int = 8011
database_url: str = "sqlite:///./coordinator.db"
# Use absolute path to avoid database duplicates in different working directories
@property
def database_url(self) -> str:
# Find project root by looking for .git directory
current = Path(__file__).resolve()
while current.parent != current:
if (current / ".git").exists():
project_root = current
break
current = current.parent
else:
# Fallback to relative path if .git not found
project_root = Path(__file__).resolve().parents[3]
db_path = project_root / "data" / "coordinator.db"
db_path.parent.mkdir(parents=True, exist_ok=True)
return f"sqlite:///{db_path}"
client_api_keys: List[str] = []
miner_api_keys: List[str] = []
admin_api_keys: List[str] = []
hmac_secret: Optional[str] = None
allow_origins: List[str] = ["*"]
allow_origins: List[str] = [
"http://localhost:3000",
"http://localhost:8080",
"http://localhost:8000",
"http://localhost:8011"
]
job_ttl_seconds: int = 900
heartbeat_interval_seconds: int = 10

View File

@@ -17,7 +17,7 @@ class Settings(BaseSettings):
database_url: str = "postgresql://localhost:5432/aitbc_coordinator"
# JWT Configuration
jwt_secret: str = "change-me-in-production"
jwt_secret: str = "" # Must be provided via environment
jwt_algorithm: str = "HS256"
jwt_expiration_hours: int = 24
@@ -51,7 +51,17 @@ class Settings(BaseSettings):
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
def validate_secrets(self) -> None:
"""Validate that all required secrets are provided"""
if not self.jwt_secret:
raise ValueError("JWT_SECRET environment variable is required")
if self.jwt_secret == "change-me-in-production":
raise ValueError("JWT_SECRET must be changed from default value")
# Create global settings instance
settings = Settings()
# Validate secrets on import
settings.validate_secrets()

View File

@@ -1,21 +1,9 @@
from typing import Callable, Generator, Annotated
from typing import Callable, Annotated
from fastapi import Depends, Header, HTTPException
from sqlmodel import Session
from .config import settings
def get_session() -> Generator[Session, None, None]:
"""Get database session"""
from .database import engine
with Session(engine) as session:
yield session
# Type alias for session dependency
SessionDep = Annotated[Session, Depends(get_session)]
class APIKeyValidator:
def __init__(self, allowed_keys: list[str]):
self.allowed_keys = {key.strip() for key in allowed_keys if key}

View File

@@ -3,7 +3,6 @@ from fastapi.middleware.cors import CORSMiddleware
from prometheus_client import make_asgi_app
from .config import settings
from .database import create_db_and_tables
from .storage import init_db
from .routers import (
client,
@@ -38,8 +37,8 @@ def create_app() -> FastAPI:
CORSMiddleware,
allow_origins=settings.allow_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"] # Allow all headers for API keys and content types
)
app.include_router(client, prefix="/v1")

View File

@@ -10,7 +10,7 @@ import time
import hashlib
from datetime import datetime, timedelta
from ..deps import get_session
from ..storage import SessionDep
from ..domain import User, Wallet
from ..schemas import UserCreate, UserLogin, UserProfile, UserBalance
@@ -50,7 +50,7 @@ def verify_session_token(token: str) -> Optional[str]:
@router.post("/register", response_model=UserProfile)
async def register_user(
user_data: UserCreate,
session: Session = Depends(get_session)
session: SessionDep
) -> Dict[str, Any]:
"""Register a new user"""
@@ -103,7 +103,7 @@ async def register_user(
@router.post("/login", response_model=UserProfile)
async def login_user(
login_data: UserLogin,
session: Session = Depends(get_session)
session: SessionDep
) -> Dict[str, Any]:
"""Login user with wallet address"""
@@ -161,7 +161,7 @@ async def login_user(
@router.get("/users/me", response_model=UserProfile)
async def get_current_user(
token: str,
session: Session = Depends(get_session)
session: SessionDep
) -> Dict[str, Any]:
"""Get current user profile"""
@@ -190,7 +190,7 @@ async def get_current_user(
@router.get("/users/{user_id}/balance", response_model=UserBalance)
async def get_user_balance(
user_id: str,
session: Session = Depends(get_session)
session: SessionDep
) -> Dict[str, Any]:
"""Get user's AITBC balance"""
@@ -223,7 +223,7 @@ async def logout_user(token: str) -> Dict[str, str]:
@router.get("/users/{user_id}/transactions")
async def get_user_transactions(
user_id: str,
session: Session = Depends(get_session)
session: SessionDep
) -> Dict[str, Any]:
"""Get user's transaction history"""

View File

@@ -30,12 +30,16 @@ Base = declarative_base()
# Direct PostgreSQL connection for performance
def get_pg_connection():
"""Get direct PostgreSQL connection"""
# Parse database URL from settings
from urllib.parse import urlparse
parsed = urlparse(settings.database_url)
return psycopg2.connect(
host="localhost",
database="aitbc_coordinator",
user="aitbc_user",
password="aitbc_password",
port=5432,
host=parsed.hostname or "localhost",
database=parsed.path[1:] if parsed.path else "aitbc_coordinator",
user=parsed.username or "aitbc_user",
password=parsed.password or "aitbc_password",
port=parsed.port or 5432,
cursor_factory=RealDictCursor
)
@@ -194,8 +198,16 @@ class PostgreSQLAdapter:
if self.connection:
self.connection.close()
# Global adapter instance
db_adapter = PostgreSQLAdapter()
# Global adapter instance (lazy initialization)
db_adapter: Optional[PostgreSQLAdapter] = None
def get_db_adapter() -> PostgreSQLAdapter:
"""Get or create database adapter instance"""
global db_adapter
if db_adapter is None:
db_adapter = PostgreSQLAdapter()
return db_adapter
# Database initialization
def init_db():
@@ -212,7 +224,8 @@ def init_db():
def check_db_health() -> Dict[str, Any]:
"""Check database health"""
try:
result = db_adapter.execute_query("SELECT 1 as health_check")
adapter = get_db_adapter()
result = adapter.execute_query("SELECT 1 as health_check")
return {
"status": "healthy",
"database": "postgresql",