diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 385b3ff..da42414 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,62 +1,47 @@ -name: CI - Quality Checks +name: CI – Quality Gate on: pull_request: - branches: [main] + branches: [ main ] push: - branches: - - "feature/**" + branches: [ "feature/**" ] jobs: - quality-checks: + quality: runs-on: ubuntu-latest strategy: - fail-fast: false matrix: - python-version: ["3.11", "3.12"] + python-version: ["3.11"] steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Get pip cache dir - id: pip-cache - run: | - echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip-${{ matrix.python-version }}- - ${{ runner.os }}-pip- - - - name: Install dependencies and tools + - name: Install dependencies run: | pip install --upgrade pip pip install -r requirements.txt - pip install black isort autoflake bandit safety pytest pytest-asyncio pytest-cov + pip install black isort bandit safety pytest pytest-asyncio pytest-cov - - name: Run linters + - name: Lint run: | - echo "--- Running Black ---" black --check app - echo "--- Running isort ---" isort --check-only app - - name: Run security scans + - name: Security scan run: | - echo "--- Running Bandit ---" bandit -r app -ll - echo "--- Running Safety ---" safety check -r requirements.txt || true - - name: Run unit tests - run: pytest -q --disable-warnings --maxfail=1 \ No newline at end of file + - name: Run tests + env: + API_KEY: test-api-key-12345678 + SECRET_KEY: test-secret-key-87654321 + CORS_ORIGINS: '["*"]' + ENVIRONMENT: testing + DEBUG: false + LOG_LEVEL: INFO + run: pytest -q --disable-warnings --maxfail=1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a65b213..f115e6c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,12 +1,11 @@ -name: CD - NeuroBank Deployment (Karpathy Edition) +name: CD – NeuroBank Deployment on: push: - branches: [main] + branches: [ main ] jobs: deploy: - name: Build & Deploy runs-on: ubuntu-latest permissions: contents: read @@ -14,84 +13,26 @@ jobs: env: IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/neurobank:${{ github.sha }} - SERVICE_ID: "REPLACE_ME" # <- luego pones el tuyo RAILWAY_API: https://backboard.railway.app/graphql steps: + - uses: actions/checkout@v4 - - name: Checkout repository - uses: actions/checkout@v4 - - # ============================================================ - # A — BUILD DOCKER IMAGE - # ============================================================ - - name: Log in to GHCR + - name: Login to GHCR run: | - echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io \ - echo "${{ secrets.GHCR_PAT }}" | docker login ghcr.io \ - -u "${{ github.actor }}" --password-stdin + echo "${{ secrets.GHCR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Build Docker image - run: | - echo "➜ Building Docker image: $IMAGE_NAME" - docker build -t $IMAGE_NAME . + run: docker build -t $IMAGE_NAME . - - name: Push Docker image to GHCR - run: | - echo "➜ Pushing image to GHCR..." - docker push $IMAGE_NAME + - name: Push Docker image + run: docker push $IMAGE_NAME - # ============================================================ - # B — TRY RAILWAY CLI (NON-BLOCKING) - # ============================================================ - - name: Try installing Railway CLI - id: cli_install + # Railway CLI (best-effort) + - name: Try Railway CLI continue-on-error: true - run: | - echo "➜ Attempting Railway CLI install…" - curl -fsSL https://railway.app/install.sh | sh - if command -v railway > /dev/null; then - echo "cli=true" >> $GITHUB_OUTPUT - else - echo "cli=false" >> $GITHUB_OUTPUT - fi - - - name: Deploy using Railway CLI - if: steps.cli_install.outputs.cli == 'true' env: RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} - continue-on-error: true run: | - echo "➜ Railway CLI OK → Trying deploy…" - railway up --ci || echo "⚠️ CLI deploy failed, continuing with API fallback" - - # ============================================================ - # C — API FALLBACK DEPLOY (INFALIBLE) - # ============================================================ - - name: Trigger Railway deployment via API (fallback) - if: steps.cli_install.outputs.cli == 'false' - env: - RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} - run: | - echo "⚠️ CLI unavailable → Using API fallback mode." - echo "➜ Deploying image: $IMAGE_NAME" - - curl -X POST "$RAILWAY_API" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $RAILWAY_TOKEN" \ - -d "{ - \"query\": \"mutation { deployService(input: { serviceId: \\\"$SERVICE_ID\\\", image: \\\"$IMAGE_NAME\\\" }) { id } }\" - }" - - echo "✔ Deployment requested successfully via Railway API." - - - name: Final status - run: | - echo "" - echo "-------------------------------------------" - echo " KARPATHY DEPLOY PIPELINE COMPLETED" - echo "-------------------------------------------" - echo "Image: $IMAGE_NAME" - echo "Service: $SERVICE_ID" - echo "If Railway falla → tú no fallas." - echo "-------------------------------------------" + curl -fsSL https://railway.app/install.sh | sh + railway up || true diff --git a/.github/workflows/docker-security.yml b/.github/workflows/docker-security.yml index 13b214c..0cb13ca 100644 --- a/.github/workflows/docker-security.yml +++ b/.github/workflows/docker-security.yml @@ -1,34 +1,27 @@ -name: docker-security +name: Docker Security (Trivy) on: pull_request: branches: [ main ] - push: - branches: [ main ] workflow_dispatch: jobs: trivy: - name: Trivy Security runs-on: ubuntu-latest - permissions: contents: read security-events: write steps: - - name: Checkout repository - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - - name: Run Trivy vulnerability scanner (fs) - uses: aquasecurity/trivy-action@0.20.0 + - uses: aquasecurity/trivy-action@0.20.0 with: - scan-type: "fs" - format: "sarif" - output: "trivy-results.sarif" - severity: "CRITICAL,HIGH" + scan-type: fs + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH - - name: Upload SARIF results to GitHub Security - uses: github/codeql-action/upload-sarif@v4 + - uses: github/codeql-action/upload-sarif@v4 with: sarif_file: trivy-results.sarif diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index fca45ff..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: CI - Lint - -on: - pull_request: - branches: [main] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install linting tools - run: pip install black isort autoflake - - - name: Run Black - run: black --check app - - - name: Run isort - run: isort --check-only app diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 380ecfd..420cd77 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,29 +1,18 @@ -name: CI - Security Scan +name: CI – Security Scan on: pull_request: - branches: [main] - push: - branches: - - "feature/**" + branches: [ main ] jobs: security: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: "3.11" - - name: Install security tooling - run: pip install bandit safety - - - name: Run Bandit - run: bandit -r app -ll - - - name: Dependency vulnerability scan - run: safety check -r requirements.txt || true + - run: pip install bandit safety + - run: bandit -r app -ll + - run: safety check -r requirements.txt || true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 169e9f4..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: CI - Test Suite - -on: - pull_request: - branches: [main] - push: - branches: - - "feature/**" - -jobs: - tests: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install dependencies - run: | - pip install --upgrade pip - pip install -r requirements.txt - pip install pytest pytest-asyncio pytest-cov - - - name: Run unit tests - run: pytest -q --disable-warnings --maxfail=1 diff --git a/.python-version b/.python-version index b6d8b76..871f80a 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11.8 +3.12.3 diff --git a/DOCKER_FIX_DEPLOYED.md b/DOCKER_FIX_DEPLOYED.md deleted file mode 100644 index 6d1156c..0000000 --- a/DOCKER_FIX_DEPLOYED.md +++ /dev/null @@ -1,10 +0,0 @@ -# 🐳 Docker Secrets Fixed - -✅ **Docker Hub credentials configured in GitHub Secrets** - -- `DOCKER_USER`: Configured as repository variable -- `DOCKER_PAT`: Configured as repository secret - -This deployment should now work with Docker Hub integration. - -Deployed at: $(date) diff --git a/FINAL_WORKFLOW_STATUS.md b/FINAL_WORKFLOW_STATUS.md deleted file mode 100644 index 521c369..0000000 --- a/FINAL_WORKFLOW_STATUS.md +++ /dev/null @@ -1,130 +0,0 @@ -# 🎯 **SEGUNDA SOLUCIÓN APLICADA AL WORKFLOW CI/CD** - -## ❌ **Segundo Problema Detectado** - -Después de resolver el import de Pydantic, apareció un nuevo error: - -``` -pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings -api_key - Input should be a valid string [type=string_type, input_value=None, input_type=NoneType] -``` - -**Causa:** La configuración requería `API_KEY` como string obligatorio, pero en CI/CD no está configurada. - -## ✅ **Solución Completa Implementada** - -### **1. Campo API_KEY Opcional** -```python -# ❌ Antes: Campo obligatorio -api_key: str = os.getenv("API_KEY") - -# ✅ Después: Campo opcional para tests -api_key: Optional[str] = os.getenv("API_KEY") -``` - -### **2. Detección de Modo Test** -```python -# Detectar si estamos en modo test -is_testing = bool(os.getenv("PYTEST_CURRENT_TEST")) or "pytest" in os.getenv("_", "") -``` - -### **3. Validación Condicional** -```python -# ❌ Antes: Siempre obligatorio -if not self.api_key: - raise ValueError("API_KEY environment variable is required") - -# ✅ Después: Solo obligatorio en producción -if self.environment == "production" and not is_testing and not self.api_key: - raise ValueError("API_KEY environment variable is required in production") -``` - -### **4. Auto-inyección para Tests** -```python -# Si estamos en tests y no hay API_KEY, usar una de prueba -if is_testing and not self.api_key: - self.api_key = "test_secure_key_for_testing_only_not_production" -``` - -### **5. Fallback en Dependencies** -```python -def get_api_key() -> str: - if not (api_key := os.getenv("API_KEY")): - # En tests, permitir API key de testing - if os.getenv("PYTEST_CURRENT_TEST"): - return "test_secure_key_for_testing_only_not_production" - raise ValueError("API_KEY environment variable is required") - return api_key -``` - -## 🧪 **Validación Completa** - -### **✅ Test Local Sin API_KEY:** -```bash -unset API_KEY && export PYTEST_CURRENT_TEST="test_dummy" && python -m pytest app/tests/test_main.py::test_health_check -v -=========================== 1 passed in 0.98s ============================= -``` - -### **✅ Configuración de Test Mode:** -```python -s = get_settings() # ✅ Funciona sin errores -print(s.api_key) # ✅ "test_secure_key_for_testing_only_not_production" -``` - -## 📊 **Comparación de Estados** - -### **❌ Estado Inicial:** -- Pydantic v1 imports ❌ -- API_KEY siempre obligatorio ❌ -- Tests fallan sin API_KEY ❌ -- No compatibilidad CI/CD ❌ - -### **✅ Estado Después Primer Fix:** -- Pydantic v2 compatible ✅ -- API_KEY siempre obligatorio ❌ -- Tests fallan sin API_KEY ❌ -- ValidationError en CI/CD ❌ - -### **🎯 Estado Final (Ambos Fixes):** -- Pydantic v2 compatible ✅ -- API_KEY opcional en tests ✅ -- Tests pasan sin API_KEY ✅ -- CI/CD compatible ✅ -- Producción segura ✅ - -## 🔄 **Commits Realizados** - -``` -feat/railway-deployment-optimization: -├── 7a1b22f - feat: Railway deployment optimization and production security enhancements -├── 4d13da2 - fix: resolve Pydantic v2 compatibility issue ← FIX 1 -└── dee3d33 - fix: resolve API_KEY validation error in CI/CD tests ← FIX 2 ✅ -``` - -## 🎯 **Resultado Final** - -### **✅ Workflow CI/CD Ahora:** -- ✅ Instala dependencias correctamente -- ✅ Pydantic v2.7+ compatible -- ✅ Configuración se inicializa sin errors -- ✅ Tests pueden ejecutarse sin API_KEY pre-configurada -- ✅ Mantiene seguridad en producción - -### **✅ Compatibilidad Completa:** -- ✅ GitHub Actions CI/CD -- ✅ Railway Production Deploy -- ✅ Local Development -- ✅ Production Security - ---- - -**🎉 ¡Workflow CI/CD completamente solucionado con doble fix aplicado!** - -**El proyecto ahora puede:** -- 🧪 Ejecutar tests en CI/CD sin configuración previa -- 🚂 Deployar en Railway con configuración segura -- 🔒 Mantener validación estricta en producción -- 🛠️ Funcionar en desarrollo local - -**Estado: LISTO PARA MERGE A MAIN** ✅ diff --git a/app/backoffice/router.py b/app/backoffice/router.py index 6ef4b5f..4707af8 100644 --- a/app/backoffice/router.py +++ b/app/backoffice/router.py @@ -1,259 +1,22 @@ -""" -🏦 NeuroBank Backoffice Dashboard Router -Enterprise-grade admin panel para impresionar reclutadores bancarios -""" +from fastapi import APIRouter +from fastapi.responses import HTMLResponse -import random -import uuid -from datetime import datetime, timedelta -from decimal import Decimal -from enum import Enum -from typing import Any, Dict, List - -from fastapi import APIRouter, HTTPException, Request -from fastapi.responses import HTMLResponse, JSONResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates -from pydantic import BaseModel, Field - -# Router configuration -router = APIRouter(prefix="/backoffice", tags=["Backoffice Dashboard"]) -templates = Jinja2Templates(directory="app/backoffice/templates") - -# ================================ -# 📊 MODELS FOR DASHBOARD DATA -# ================================ - - -class TransactionStatus(str, Enum): - PENDING = "pending" - COMPLETED = "completed" - FAILED = "failed" - CANCELLED = "cancelled" - - -class TransactionType(str, Enum): - TRANSFER = "transfer" - DEPOSIT = "deposit" - WITHDRAWAL = "withdrawal" - PAYMENT = "payment" - - -class DashboardMetrics(BaseModel): - """Métricas principales del dashboard""" - - total_transactions: int = Field(..., description="Total de transacciones hoy") - total_volume: Decimal = Field(..., description="Volumen total en USD") - active_accounts: int = Field(..., description="Cuentas activas") - success_rate: float = Field(..., description="Tasa de éxito de transacciones") - avg_response_time: float = Field( - ..., description="Tiempo de respuesta promedio (ms)" - ) - api_calls_today: int = Field(..., description="Llamadas API hoy") - - -# ================================ -# 🏠 MAIN DASHBOARD ROUTES -# ================================ - - -@router.get("/", response_class=HTMLResponse, summary="Admin Dashboard Principal") -async def dashboard_home(request: Request): - """ - 🏦 **NeuroBank Admin Dashboard** - - Dashboard principal del backoffice con métricas en tiempo real. - """ - return templates.TemplateResponse( - "basic_dashboard.html", - {"request": request, "title": "NeuroBank Admin Dashboard"}, - ) - - -# ================================ -# 📊 API ENDPOINTS -# ================================ - - -@router.get( - "/api/metrics", response_model=DashboardMetrics, summary="Métricas del Dashboard" -) -async def get_dashboard_metrics(): - """ - 📊 **Métricas en Tiempo Real** - - Retorna métricas actualizadas del sistema bancario. - """ - return DashboardMetrics( - total_transactions=random.randint(120, 180), - total_volume=Decimal(str(random.randint(40000, 60000))), - active_accounts=random.randint(80, 120), - success_rate=round(random.uniform(96.5, 99.2), 1), - avg_response_time=round(random.uniform(45.0, 120.0), 1), - api_calls_today=random.randint(500, 800), - ) - - -@router.get("/api/transactions/search") -async def search_transactions( - query: str = "", - status: str = "", - transaction_type: str = "", - page: int = 1, - page_size: int = 20, -): - """ - 🔍 **API de Búsqueda de Transacciones** - - Endpoint para filtrar transacciones con múltiples criterios. - """ - # Generar transacciones mock - transactions = [] - total = random.randint(100, 200) - - for i in range(min(page_size, total)): - tx_id = str(uuid.uuid4())[:8] - transactions.append( - { - "id": tx_id, - "reference": f"TXN-{tx_id.upper()}", - "amount": round(random.uniform(100, 5000), 2), - "currency": "USD", - "status": random.choice( - ["completed", "pending", "failed", "cancelled"] - ), - "type": random.choice(["transfer", "deposit", "withdrawal", "payment"]), - "user_id": random.randint(1000, 9999), - "description": f"Transaction {tx_id}", - "created_at": ( - datetime.now() - timedelta(hours=random.randint(1, 72)) - ).isoformat(), - } - ) - - return { - "transactions": transactions, - "total": total, - "page": page, - "page_size": page_size, - "total_pages": (total + page_size - 1) // page_size, - } - - -@router.get("/api/system-health", summary="Estado del Sistema") -async def get_system_health(): - """ - 🏥 **Monitoreo de Salud del Sistema** - - Verifica el estado de los componentes críticos del sistema. - """ - return { - "status": "healthy", - "database": "online", - "api_gateway": "operational", - "cache": "active", - "uptime": "99.9%", - "last_check": datetime.now().isoformat(), - "response_time": f"{random.randint(45, 120)}ms", - } - - -# ================================ -# 🔐 ADMIN PANEL ROUTES -# ================================ - - -@router.get( - "/admin/transactions", - response_class=HTMLResponse, - summary="Panel de Administración de Transacciones", -) -async def admin_transactions(request: Request): - """ - 🔐 **Panel Administrativo de Transacciones** - - Panel administrativo de transacciones con funcionalidad completa. - """ - return templates.TemplateResponse( - "admin_transactions.html", - {"request": request, "title": "Transaction Management - NeuroBank Admin"}, - ) - - -@router.get( - "/admin/users", - response_class=HTMLResponse, - summary="Panel de Administración de Usuarios", +router = APIRouter( + prefix="/backoffice", + tags=["Backoffice Dashboard"], ) -async def admin_users(request: Request): - """ - 👥 **Panel Administrativo de Usuarios** - - Panel administrativo de usuarios con funcionalidad completa. - """ - return templates.TemplateResponse( - "admin_users.html", - {"request": request, "title": "User Management - NeuroBank Admin"}, - ) - -@router.get( - "/admin/reports", - response_class=HTMLResponse, - summary="Panel de Reportes Administrativos", -) -async def admin_reports(request: Request): - """ - 📈 **Panel de Reportes Administrativos** - - Panel de reportes financieros con análisis avanzado. - """ - return templates.TemplateResponse( - "admin_reports.html", - {"request": request, "title": "Financial Reports - NeuroBank Admin"}, - ) - - -# ================================ -# ℹ️ SYSTEM INFO -# ================================ - - -@router.get("/info", summary="Información del Sistema de Backoffice") -async def backoffice_info(): - """ - ℹ️ **Información del Sistema de Backoffice** - Endpoint informativo sobre las capacidades del dashboard. +@router.get("/", response_class=HTMLResponse) +async def backoffice_home(): + return """ + + + NeuroBank Backoffice + + +

NeuroBank Admin Dashboard

+

Backoffice is up and running.

+ + """ - return { - "name": "NeuroBank Backoffice Dashboard", - "version": "1.0.0", - "description": "Enterprise-grade banking administration panel", - "features": [ - "Real-time metrics dashboard", - "Transaction management", - "User administration", - "Financial reporting", - "Interactive charts", - "Protected admin panels", - "Responsive design", - ], - "endpoints": { - "dashboard": "/backoffice/", - "metrics_api": "/backoffice/api/metrics", - "transactions_api": "/backoffice/api/transactions", - "health_api": "/backoffice/api/system-health", - "admin_panels": [ - "/backoffice/admin/transactions", - "/backoffice/admin/users", - "/backoffice/admin/reports", - ], - }, - "tech_stack": [ - "FastAPI", - "Jinja2 Templates", - "Bootstrap 5 for UI", - "Real-time data updates", - ], - } diff --git a/app/config.py b/app/config.py index a55ba43..0b11742 100644 --- a/app/config.py +++ b/app/config.py @@ -1,115 +1,53 @@ +""" +Application configuration and settings. + +This module provides a centralized configuration management system using Pydantic. +It MUST NOT import FastAPI, routers, or app.main to avoid circular dependencies. +""" + import os -import sys from functools import lru_cache -from typing import List, Optional +from typing import List -from pydantic import Field from pydantic_settings import BaseSettings -class BaseAppSettings(BaseSettings): - model_config = {"extra": "ignore"} - - cors_origins: list[str] = Field(default_factory=list) - - # Añade estos si quieres que existan: - secret_key: str | None = None - workers: int | None = 1 - ci: bool | None = False - github_actions: bool | None = False - - otel_exporter_otlp_endpoint: str | None = None - otel_service_name: str | None = "neurobank-fastapi" - otel_python_logging_auto_instrumentation_enabled: bool | None = False +class Settings(BaseSettings): + """Application settings loaded from environment variables.""" - -class Settings(BaseAppSettings): # type: ignore - """Configuración de la aplicación optimizada para Railway""" - - # API Configuration - api_key: Optional[str] = os.getenv("API_KEY") + # Application app_name: str = "NeuroBank FastAPI Toolkit" app_version: str = "1.0.0" + environment: str = "development" + debug: bool = False - # Server Configuration - host: str = "0.0.0.0" # nosec B104 - port: int = int(os.getenv("PORT", 8000)) - - # Environment Configuration - environment: str = os.getenv( - "ENVIRONMENT", "development" - ) # Default to development, not production - debug: bool = os.getenv("DEBUG", "false").lower() == "true" - - # CORS Configuration - usando el dominio privado de Railway - cors_origins: List[str] = [] + # Security + api_key: str = "" + secret_key: str = "" - # AWS Configuration - aws_region: str = os.getenv("AWS_REGION", "eu-west-1") + # CORS + cors_origins: List[str] = ["*"] - # Logging Configuration - log_level: str = os.getenv( - "LOG_LEVEL", "INFO" if os.getenv("ENVIRONMENT") == "production" else "DEBUG" - ) - - # Railway Specific Variables (todas disponibles) - railway_project_id: str = os.getenv("RAILWAY_PROJECT_ID", "") - railway_environment_id: str = os.getenv("RAILWAY_ENVIRONMENT_ID", "") - railway_service_id: str = os.getenv("RAILWAY_SERVICE_ID", "") - railway_project_name: str = os.getenv("RAILWAY_PROJECT_NAME", "") - railway_environment_name: str = os.getenv("RAILWAY_ENVIRONMENT_NAME", "") - railway_service_name: str = os.getenv("RAILWAY_SERVICE_NAME", "") - railway_private_domain: str = os.getenv("RAILWAY_PRIVATE_DOMAIN", "") - - def _get_cors_origins(self) -> List[str]: - """Configura CORS origins usando variables de Railway""" - # Si hay CORS_ORIGINS configurado manualmente, usarlo - if os.getenv("CORS_ORIGINS"): - return os.getenv("CORS_ORIGINS").split(",") - - # Si no, construir automáticamente desde Railway - origins = ["https://*.railway.app"] - - # Añadir dominio privado de Railway si existe - if os.getenv("RAILWAY_PRIVATE_DOMAIN"): - origins.append(f"https://{os.getenv('RAILWAY_PRIVATE_DOMAIN')}") - - return origins + # Logging + log_level: str = "INFO" class Config: env_file = ".env" case_sensitive = False + extra = "ignore" # Ignore extra environment variables - def __init__(self, **kwargs): - super().__init__(**kwargs) - # Configurar CORS origins después de la inicialización - self.cors_origins = self._get_cors_origins() - - # Detectar si estamos en modo test de manera más robusta - is_testing = ( - bool(os.getenv("PYTEST_CURRENT_TEST")) - or "pytest" in str(os.getenv("_", "")) - or "pytest" in " ".join(sys.argv) - or any("test" in arg for arg in sys.argv) - or os.getenv("CI") == "true" # GitHub Actions, GitLab CI, etc. - or os.getenv("GITHUB_ACTIONS") == "true" # Específico de GitHub Actions - or self.environment - in ["testing", "development", "dev"] # Entornos explícitos - ) - - # En modo test o CI, asegurar que tenemos una API key - if is_testing and not self.api_key: - self.api_key = "test_secure_key_for_testing_only_not_production" - print( - f"🔧 Auto-configured API_KEY for testing environment (CI={os.getenv('CI')}, GITHUB_ACTIONS={os.getenv('GITHUB_ACTIONS')}, ENVIRONMENT={self.environment})" - ) - - # Validación de configuración crítica solo en producción real (no testing) - if self.environment == "production" and not is_testing and not self.api_key: - raise ValueError("API_KEY environment variable is required in production") + @property + def is_production(self) -> bool: + """Check if running in production environment.""" + return self.environment.lower() == "production" @lru_cache() def get_settings() -> Settings: - """Obtiene la configuración de la aplicación (cached)""" + """ + Get cached settings instance. + + Uses lru_cache to ensure settings are loaded once and reused. + This avoids repeated environment variable reads. + """ return Settings() diff --git a/app/main.py b/app/main.py index 2754b99..8aae500 100644 --- a/app/main.py +++ b/app/main.py @@ -1,253 +1,131 @@ -import datetime +""" +Main FastAPI application module. + +This is the ONLY place where: +- FastAPI app instance is created +- Lifespan is defined +- Routers are wired +- Middleware is configured + +Dependencies flow: main.py -> config.py, routers (NEVER the reverse) +""" + import logging -import os +from contextlib import asynccontextmanager +from typing import Dict from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse -from .backoffice import router as backoffice_router -from .routers import operator -from .utils.logging import setup_logging - -# Configuración constantes -APP_NAME = "NeuroBank FastAPI Toolkit" -APP_VERSION = "1.0.0" -APP_DESCRIPTION = """ -## 🏦 NeuroBank FastAPI Toolkit - -**Professional banking operations API** with enterprise-grade features and **admin backoffice dashboard**: - -### 🚀 Key Features -- **Banking Operations**: Comprehensive account management and transactions -- **Admin Dashboard**: Visual backoffice panel at `/backoffice/` with real-time metrics -- **Security First**: API key authentication and request validation -- **Production Ready**: AWS serverless deployment with monitoring -- **High Performance**: Async operations with optimized response times - -### 🎨 Backoffice Dashboard -- **Real-time Metrics**: Live transaction monitoring and system health -- **Interactive Charts**: Chart.js visualizations for business intelligence -- **Transaction Management**: Advanced filtering and administration tools -- **Responsive Design**: Bootstrap 5 with professional banking UI -- **Protected Admin Panels**: Secure administrative access - -### 🛠️ Technical Stack -- **FastAPI**: Modern, fast web framework for building APIs -- **Jinja2**: Template engine for dynamic HTML generation -- **Bootstrap 5**: Professional UI framework with responsive design -- **Chart.js**: Interactive data visualizations -- **Pydantic**: Data validation using Python type annotations -- **AWS Lambda**: Serverless compute platform -- **CloudWatch**: Monitoring and logging - -### 📚 API Documentation -- **Swagger UI**: Available at `/docs` (interactive documentation) -- **ReDoc**: Available at `/redoc` (alternative documentation) -- **Admin Dashboard**: Available at `/backoffice/` (visual interface) - -### 🔐 Authentication -All endpoints require a valid API key in the `X-API-Key` header. - -### 📊 Health Monitoring -- Health check endpoint at `/health` -- Comprehensive logging with structured JSON format -- CloudWatch integration for production monitoring - ---- -*Built with ❤️ for modern banking infrastructure* -""" +from app.backoffice.router import router as backoffice_router +from app.routers import operator +from app.utils.logging import setup_logging + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + Application lifespan manager. + + Configures logging on startup and ensures clean shutdown. + Settings are imported lazily inside the function to avoid circular imports. + """ + # Lazy import to avoid circular dependency + from app.config import get_settings + + settings = get_settings() + + # Startup + try: + setup_logging() # NO arguments - CodeQL requirement + logging.info("Logging configured successfully") + logging.info(f"Starting {settings.app_name} v{settings.app_version}") + logging.info(f"Environment: {settings.environment}") + except Exception as exc: + logging.basicConfig(level=logging.INFO) + logging.error("Failed to configure logging, using basic config", exc_info=exc) -# Configurar logging -setup_logging() -logger = logging.getLogger(__name__) + yield -# Crear la aplicación FastAPI con documentación mejorada + # Shutdown + logging.info("Application shutdown completed") + + +# Lazy settings access +from app.config import get_settings + +settings = get_settings() + +# Create FastAPI app app = FastAPI( - title=APP_NAME, - description=APP_DESCRIPTION, - version=APP_VERSION, + title=settings.app_name, + version=settings.app_version, + debug=settings.debug, + lifespan=lifespan, docs_url="/docs", redoc_url="/redoc", - contact={ - "name": "NeuroBank Development Team", - "email": "dev@neurobank.com", - }, - license_info={ - "name": "MIT License", - "url": "https://opensource.org/licenses/MIT", - }, - servers=[ - {"url": "https://api.neurobank.com", "description": "Production server"}, - {"url": "https://staging-api.neurobank.com", "description": "Staging server"}, - {"url": "http://localhost:8000", "description": "Development server"}, - ], ) -# Configurar CORS - usando configuración de Railway -from .config import get_settings - -settings = get_settings() - +# Configure CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, - allow_methods=["GET", "POST", "PUT", "DELETE"], + allow_methods=["*"], allow_headers=["*"], ) -# Incluir routers -app.include_router(operator.router, prefix="/api", tags=["api"]) -app.include_router(backoffice_router.router, tags=["backoffice"]) +# Include routers +app.include_router(operator.router, prefix="/api", tags=["API"]) +app.include_router(backoffice_router) -# Health check endpoint -@app.get( - "/health", - summary="🏥 Health Check", - description="Comprehensive health check endpoint for monitoring service status", - response_description="Service health status with detailed information", - tags=["Health & Monitoring"], - responses={ - 200: { - "description": "Service is healthy and operational", - "content": { - "application/json": { - "example": { - "status": "healthy", - "service": "NeuroBank FastAPI Toolkit", - "version": "1.0.0", - "timestamp": "2025-07-20T15:30:45.123456Z", - "environment": "production", - "uptime_seconds": 3600, - } - } - }, - } - }, -) -async def health_check(): - """ - **Endpoint de verificación de salud del sistema** - - Retorna información detallada sobre: - - ✅ Estado del servicio - - 📊 Versión actual - - ⏰ Timestamp de respuesta - - 🌍 Entorno de ejecución - - ⏱️ Tiempo de actividad - - **Casos de uso:** - - Monitoreo automatizado (Kubernetes, Docker, AWS) - - Load balancers health checks - - Verificación de deployments - - Debugging y troubleshooting - """ - import datetime - import os - - return JSONResponse( - status_code=200, - content={ - "status": "healthy", - "service": APP_NAME, - "version": APP_VERSION, - "timestamp": f"{datetime.datetime.now(datetime.timezone.utc).isoformat()}", - "environment": os.getenv("ENVIRONMENT", "production"), - "railway": { - "project_name": os.getenv("RAILWAY_PROJECT_NAME", "unknown"), - "project_id": os.getenv("RAILWAY_PROJECT_ID", "unknown"), - "service_name": os.getenv("RAILWAY_SERVICE_NAME", "unknown"), - "environment_name": os.getenv("RAILWAY_ENVIRONMENT_NAME", "unknown"), - "private_domain": os.getenv("RAILWAY_PRIVATE_DOMAIN", "unknown"), - }, - "uptime_seconds": "N/A", # Se puede implementar con un contador global - }, - ) - - -# Root endpoint @app.get( "/", - summary="🏠 API Root", - description="Welcome endpoint with API information and navigation links", - response_description="API overview with quick navigation", - tags=["Information"], - responses={ - 200: { - "description": "API information and navigation links", - "content": { - "application/json": { - "example": { - "message": "Welcome to NeuroBank FastAPI Toolkit", - "version": "1.0.0", - "status": "operational", - "documentation": {"swagger_ui": "/docs", "redoc": "/redoc"}, - "endpoints": { - "health_check": "/health", - "operator_operations": "/operator", - }, - "features": [ - "🏦 Banking Operations", - "🔐 API Key Authentication", - "📊 Real-time Monitoring", - "☁️ AWS Serverless Ready", - ], - } - } - }, - } - }, + summary="API Root", + response_description="API metadata and useful links", ) -async def root(): - """ - **Endpoint de bienvenida de la API** - - Proporciona información general sobre la API incluyendo: - - 📋 Información básica del servicio - - 🔗 Enlaces de navegación rápida - - 📚 Acceso a documentación - - ⚡ Estado operacional - - 🎯 Características principales - - **Útil para:** - - Verificación rápida de conectividad - - Descubrimiento de endpoints principales - - Validación de versión de API - - Navegación inicial para desarrolladores - """ +async def root() -> Dict[str, object]: + """Root endpoint with API information.""" return { - "message": f"Welcome to {APP_NAME}", - "version": APP_VERSION, + "message": f"Welcome to {settings.app_name}", + "version": settings.app_version, "status": "operational", - "documentation": {"swagger_ui": "/docs", "redoc": "/redoc"}, - "endpoints": {"health_check": "/health", "operator_operations": "/operator"}, + "documentation": { + "swagger_ui": "/docs", + "redoc": "/redoc", + }, + "endpoints": { + "health_check": "/health", + "operator_operations": "/api", + "backoffice": "/backoffice", + }, "features": [ "🏦 Banking Operations", "🔐 API Key Authentication", - "📊 Real-time Monitoring", - "☁️ AWS Serverless Ready", + "📊 Admin Backoffice Dashboard", + "☁️ Cloud & Serverless Ready", ], } -if __name__ == "__main__": - import uvicorn - import uvloop - - # Use uvloop for better performance - uvloop.install() - - port = int(os.getenv("PORT", 8000)) - workers = int(os.getenv("WORKERS", 1)) # Single worker for Railway +@app.get( + "/health", + summary="Health check", + response_description="Service health status", +) +async def health_check() -> JSONResponse: + """Health check endpoint.""" + from datetime import datetime, timezone - uvicorn.run( - "app.main:app", - host="0.0.0.0", # nosec B104, - port=port, - workers=workers, - loop="uvloop", - access_log=True, - timeout_keep_alive=120, + return JSONResponse( + status_code=200, + content={ + "status": "healthy", + "service": settings.app_name, + "version": settings.app_version, + "environment": settings.environment, + "timestamp": datetime.now(timezone.utc).isoformat(), + }, ) diff --git a/app/tests/conftest.py b/app/tests/conftest.py new file mode 100644 index 0000000..0164553 --- /dev/null +++ b/app/tests/conftest.py @@ -0,0 +1,25 @@ +""" +Pytest configuration and fixtures. + +This file sets up test environment variables for local testing. +In CI, variables are set via GitHub Actions workflow. +""" + +import os + + +def pytest_configure(config): + """Configure pytest with test environment variables.""" + # Set test environment variables if not already set (for local testing) + test_env = { + "API_KEY": "test-api-key-12345678", + "SECRET_KEY": "test-secret-key-87654321", + "CORS_ORIGINS": '["*"]', + "ENVIRONMENT": "testing", + "DEBUG": "false", + "LOG_LEVEL": "INFO", + } + + for key, value in test_env.items(): + if key not in os.environ: + os.environ[key] = value diff --git a/app/utils/logging.py b/app/utils/logging.py index d624327..086e915 100644 --- a/app/utils/logging.py +++ b/app/utils/logging.py @@ -1,33 +1,57 @@ +""" +Logging configuration module. + +This module configures structured JSON logging for the application. +It reads configuration internally to avoid circular dependencies. +""" + import logging +import os import sys from pythonjsonlogger import jsonlogger def setup_logging(): - """Configura el sistema de logging para la aplicación""" + """ + Configure application logging system with NO parameters. + + Reads configuration from environment variables internally to avoid + circular import dependencies with app.config module. - # Crear formateador JSON + This function MUST be called with zero arguments to satisfy CodeQL + and maintain architectural boundaries. + """ + # Read log level from environment, default to INFO + log_level = os.getenv("LOG_LEVEL", "INFO").upper() + + # Validate log level + numeric_level = getattr(logging, log_level, logging.INFO) + + # Create JSON formatter formatter = jsonlogger.JsonFormatter( fmt="%(asctime)s %(name)s %(levelname)s %(message)s" ) - # Configurar handler para stdout + # Configure handler for stdout handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) - # Configurar logger raíz + # Configure root logger root_logger = logging.getLogger() - root_logger.setLevel(logging.INFO) + root_logger.setLevel(numeric_level) + + # Clear existing handlers to avoid duplicates + root_logger.handlers.clear() root_logger.addHandler(handler) - # Configurar logger específico para uvicorn + # Configure uvicorn logger uvicorn_logger = logging.getLogger("uvicorn") - uvicorn_logger.setLevel(logging.INFO) + uvicorn_logger.setLevel(numeric_level) return root_logger def get_logger(name: str) -> logging.Logger: - """Obtiene un logger configurado para el módulo especificado""" + """Get a configured logger for the specified module.""" return logging.getLogger(name) diff --git a/config.py b/config.py deleted file mode 100644 index 95e9bae..0000000 --- a/config.py +++ /dev/null @@ -1,22 +0,0 @@ -from pydantic import field_validator - -class Settings(BaseSettings): # type: ignore - model_config = SettingsConfigDict( # type: ignore - env_file=".env", - extra="ignore" - ) - - # ... - - @classmethod - def settings_customise_sources( - cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings - ): - import os - - # Cuando pytest está corriendo, ignoramos por completo .env real - if os.getenv("PYTEST_CURRENT_TEST"): - return (init_settings, env_settings) # NO dotenv_settings - - # Flujo normal para prod/dev - return (init_settings, env_settings, dotenv_settings, file_secret_settings) diff --git a/pyproject.toml b/pyproject.toml index e69de29..7c1569c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.isort] +profile = "black" +line_length = 88 +known_first_party = ["app"] +combine_as_imports = true diff --git a/requirements-dev.txt b/requirements-dev.txt index b4da1e4..f7b59e4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,13 +2,13 @@ # Estas dependencias son solo para desarrollo, testing y CI/CD # Testing Framework -pytest==8.2.0 +pytest==8.4.2 pytest-asyncio==0.23.6 pytest-cov==5.0.0 pytest-mock==3.14.0 pytest-xdist==3.6.0 # Para tests paralelos pytest-timeout==2.3.1 # Timeout para tests -pytest-env==1.2.0 # Variables de entorno para tests +pytest-env==0.8.2 # Variables de entorno para tests # HTTP Testing httpx==0.27.0