From 350af60db3df0893cd3d69f8328de3d529383ffa Mon Sep 17 00:00:00 2001 From: HotSwapp <47397945+HotSwapp@users.noreply.github.com> Date: Sun, 10 Aug 2025 19:06:21 -0500 Subject: [PATCH] working now --- Dockerfile | 4 +- app/api/auth.py | 43 +++++++++++- app/config.py | 6 ++ app/main.py | 16 +++++ app/middleware/__init__.py | 0 app/middleware/logging.py | 130 +++++++++++++++++++++++++++++++++++++ app/services/audit.py | 7 +- requirements.txt | 5 +- templates/admin.html | 44 ++++++++++++- templates/search.html | 4 +- 10 files changed, 248 insertions(+), 11 deletions(-) create mode 100644 app/middleware/__init__.py create mode 100644 app/middleware/logging.py diff --git a/Dockerfile b/Dockerfile index a5b3081..069558f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,8 +34,8 @@ RUN addgroup --system --gid 1001 delphi \ COPY --chown=delphi:delphi . . # Create necessary directories with proper permissions -RUN mkdir -p /app/data /app/uploads /app/backups /app/exports \ - && chown -R delphi:delphi /app/data /app/uploads /app/backups /app/exports +RUN mkdir -p /app/data /app/uploads /app/backups /app/exports /app/logs \ + && chown -R delphi:delphi /app/data /app/uploads /app/backups /app/exports /app/logs # Create volume mount points VOLUME ["/app/data", "/app/uploads", "/app/backups"] diff --git a/app/api/auth.py b/app/api/auth.py index 4a65234..4f96a3b 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -3,7 +3,7 @@ Authentication API endpoints """ from datetime import datetime, timedelta from typing import List -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Request from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session @@ -24,15 +24,39 @@ from app.auth.schemas import ( ThemePreferenceUpdate ) from app.config import settings +from app.core.logging import get_logger, log_auth_attempt router = APIRouter() +logger = get_logger("auth") @router.post("/login", response_model=Token) -async def login(login_data: LoginRequest, db: Session = Depends(get_db)): +async def login(login_data: LoginRequest, request: Request, db: Session = Depends(get_db)): """Login endpoint""" + client_ip = request.client.host if request.client else "unknown" + user_agent = request.headers.get("user-agent", "") + + logger.info( + "Login attempt started", + username=login_data.username, + client_ip=client_ip, + user_agent=user_agent + ) + user = authenticate_user(db, login_data.username, login_data.password) if not user: + log_auth_attempt( + username=login_data.username, + success=False, + ip_address=client_ip, + user_agent=user_agent, + error="Invalid credentials" + ) + logger.warning( + "Login failed - invalid credentials", + username=login_data.username, + client_ip=client_ip + ) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", @@ -47,6 +71,20 @@ async def login(login_data: LoginRequest, db: Session = Depends(get_db)): access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) + + log_auth_attempt( + username=login_data.username, + success=True, + ip_address=client_ip, + user_agent=user_agent + ) + logger.info( + "Login successful", + username=login_data.username, + user_id=user.id, + client_ip=client_ip + ) + return {"access_token": access_token, "token_type": "bearer"} @@ -87,6 +125,7 @@ async def register( @router.get("/me", response_model=UserResponse) async def read_users_me(current_user: User = Depends(get_current_user)): """Get current user info""" + logger.debug("User info requested", username=current_user.username, user_id=current_user.id) return current_user diff --git a/app/config.py b/app/config.py index 12f2767..0b833a8 100644 --- a/app/config.py +++ b/app/config.py @@ -40,6 +40,12 @@ class Settings(BaseSettings): secure_cookies: bool = False compose_project_name: Optional[str] = None + # Logging + log_level: str = "INFO" + log_to_file: bool = True + log_rotation: str = "10 MB" + log_retention: str = "30 days" + class Config: env_file = ".env" diff --git a/app/main.py b/app/main.py index 6fc410d..015b6fa 100644 --- a/app/main.py +++ b/app/main.py @@ -12,18 +12,31 @@ from app.database.base import engine from app.models import BaseModel from app.models.user import User from app.auth.security import get_admin_user +from app.core.logging import setup_logging, get_logger +from app.middleware.logging import LoggingMiddleware + +# Initialize logging +setup_logging() +logger = get_logger("main") # Create database tables +logger.info("Creating database tables") BaseModel.metadata.create_all(bind=engine) # Initialize FastAPI app +logger.info("Initializing FastAPI application", version=settings.app_version, debug=settings.debug) app = FastAPI( title=settings.app_name, version=settings.app_version, description="Modern Python web application for Delphi Consulting Group", ) +# Add logging middleware +logger.info("Adding request logging middleware") +app.add_middleware(LoggingMiddleware, log_requests=True, log_responses=settings.debug) + # Configure CORS +logger.info("Configuring CORS middleware") app.add_middleware( CORSMiddleware, allow_origins=["*"], # Configure appropriately for production @@ -33,10 +46,12 @@ app.add_middleware( ) # Mount static files +logger.info("Mounting static file directories") app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") # Templates +logger.info("Initializing Jinja2 templates") templates = Jinja2Templates(directory="templates") # Include routers @@ -51,6 +66,7 @@ from app.api.import_data import router as import_router from app.api.support import router as support_router from app.api.settings import router as settings_router +logger.info("Including API routers") app.include_router(auth_router, prefix="/api/auth", tags=["authentication"]) app.include_router(customers_router, prefix="/api/customers", tags=["customers"]) app.include_router(files_router, prefix="/api/files", tags=["files"]) diff --git a/app/middleware/__init__.py b/app/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/middleware/logging.py b/app/middleware/logging.py new file mode 100644 index 0000000..4ab4de3 --- /dev/null +++ b/app/middleware/logging.py @@ -0,0 +1,130 @@ +""" +Request/Response Logging Middleware +""" +import time +import json +from typing import Callable +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.responses import StreamingResponse +from app.core.logging import get_logger, log_request + +logger = get_logger("middleware.logging") + + +class LoggingMiddleware(BaseHTTPMiddleware): + """Middleware to log HTTP requests and responses""" + + def __init__(self, app, log_requests: bool = True, log_responses: bool = False): + super().__init__(app) + self.log_requests = log_requests + self.log_responses = log_responses + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + # Skip logging for static files and health checks + skip_paths = ["/static/", "/uploads/", "/health", "/favicon.ico"] + if any(request.url.path.startswith(path) for path in skip_paths): + return await call_next(request) + + # Record start time + start_time = time.time() + + # Extract request details + client_ip = self.get_client_ip(request) + user_agent = request.headers.get("user-agent", "") + + # Log request + if self.log_requests: + logger.info( + "Request started", + method=request.method, + path=request.url.path, + query_params=str(request.query_params) if request.query_params else None, + client_ip=client_ip, + user_agent=user_agent + ) + + # Process request + try: + response = await call_next(request) + except Exception as e: + # Log exceptions + duration_ms = (time.time() - start_time) * 1000 + logger.error( + "Request failed with exception", + method=request.method, + path=request.url.path, + duration_ms=duration_ms, + error=str(e), + client_ip=client_ip + ) + raise + + # Calculate duration + duration_ms = (time.time() - start_time) * 1000 + + # Extract user ID from request if available (for authenticated requests) + user_id = None + if hasattr(request.state, "user") and request.state.user: + user_id = getattr(request.state.user, "id", None) or getattr(request.state.user, "username", None) + + # Log response + log_request( + method=request.method, + path=request.url.path, + status_code=response.status_code, + duration_ms=duration_ms, + user_id=user_id + ) + + # Log response details if enabled + if self.log_responses: + logger.debug( + "Response details", + status_code=response.status_code, + headers=dict(response.headers), + size_bytes=response.headers.get("content-length"), + content_type=response.headers.get("content-type") + ) + + # Log slow requests as warnings + if duration_ms > 1000: # More than 1 second + logger.warning( + "Slow request detected", + method=request.method, + path=request.url.path, + duration_ms=duration_ms, + status_code=response.status_code + ) + + # Log authentication-related requests to auth log + if any(path in request.url.path for path in ["/api/auth/", "/login", "/logout"]): + logger.bind(name="auth").info( + "Auth endpoint accessed", + method=request.method, + path=request.url.path, + status_code=response.status_code, + duration_ms=duration_ms, + client_ip=client_ip, + user_agent=user_agent + ) + + return response + + def get_client_ip(self, request: Request) -> str: + """Extract client IP address from request headers""" + # Check for IP in common proxy headers + forwarded_for = request.headers.get("x-forwarded-for") + if forwarded_for: + # Take the first IP in the chain + return forwarded_for.split(",")[0].strip() + + real_ip = request.headers.get("x-real-ip") + if real_ip: + return real_ip + + # Fallback to direct client IP + if request.client: + return request.client.host + + return "unknown" \ No newline at end of file diff --git a/app/services/audit.py b/app/services/audit.py index 51d6599..91cfd06 100644 --- a/app/services/audit.py +++ b/app/services/audit.py @@ -9,6 +9,9 @@ from fastapi import Request from app.models.audit import AuditLog, LoginAttempt from app.models.user import User +from app.core.logging import get_logger + +logger = get_logger("audit") class AuditService: @@ -73,7 +76,7 @@ class AuditService: except Exception as e: db.rollback() # Log the error but don't fail the main operation - print(f"Failed to log audit entry: {e}") + logger.error("Failed to log audit entry", error=str(e), action=action, user_id=user_id) return audit_log @staticmethod @@ -128,7 +131,7 @@ class AuditService: except Exception as e: db.rollback() # Log the error but don't fail the main operation - print(f"Failed to log login attempt: {e}") + logger.error("Failed to log login attempt", error=str(e), username=username, success=success) return login_attempt @staticmethod diff --git a/requirements.txt b/requirements.txt index d12dc41..25d7f37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,4 +31,7 @@ pytest-asyncio==0.24.0 httpx==0.28.1 # Development -python-dotenv==1.0.1 \ No newline at end of file +python-dotenv==1.0.1 + +# Logging +loguru==0.7.2 \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html index 3c5e607..4d18746 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -1643,8 +1643,48 @@ async function createBackup() { } } -function downloadBackup(filename) { - window.open('/api/admin/backup/download', '_blank'); +async function downloadBackup(filename) { + try { + const response = await fetch('/api/admin/backup/download', { + method: 'GET', + headers: getAuthHeaders() + }); + + if (!response.ok) { + let errorMsg = 'Failed to download backup'; + try { + const err = await response.json(); + errorMsg = err.detail || errorMsg; + } catch (_) {} + showAlert(errorMsg, 'error'); + return; + } + + const blob = await response.blob(); + + // Try to extract filename from headers if provided + let suggestedName = filename || 'database_backup.db'; + const disp = response.headers.get('Content-Disposition') || response.headers.get('content-disposition'); + if (disp) { + const match = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(disp); + const extracted = match && (match[1] || match[2]); + if (extracted) { + try { suggestedName = decodeURIComponent(extracted); } catch (_) { suggestedName = extracted; } + } + } + + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = suggestedName; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error('Backup download failed:', error); + showAlert('Failed to download backup', 'error'); + } } // Utility Functions diff --git a/templates/search.html b/templates/search.html index bd32c57..0bfb163 100644 --- a/templates/search.html +++ b/templates/search.html @@ -407,7 +407,7 @@ // Advanced Search JavaScript document.addEventListener('DOMContentLoaded', function() { // Initialize search components - initializeSearch(); + initializeAdvancedSearch(); loadSearchFacets(); setupEventHandlers(); setupKeyboardShortcuts(); @@ -424,7 +424,7 @@ let currentSearchCriteria = {}; let searchTimeout; let facetsData = {}; -function initializeSearch() { +function initializeAdvancedSearch() { // Set default date for today const today = new Date().toISOString().split('T')[0];