working now

This commit is contained in:
HotSwapp
2025-08-10 19:06:21 -05:00
parent c2f3c4411d
commit 350af60db3
10 changed files with 248 additions and 11 deletions

View File

@@ -34,8 +34,8 @@ RUN addgroup --system --gid 1001 delphi \
COPY --chown=delphi:delphi . . COPY --chown=delphi:delphi . .
# Create necessary directories with proper permissions # Create necessary directories with proper permissions
RUN mkdir -p /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 && chown -R delphi:delphi /app/data /app/uploads /app/backups /app/exports /app/logs
# Create volume mount points # Create volume mount points
VOLUME ["/app/data", "/app/uploads", "/app/backups"] VOLUME ["/app/data", "/app/uploads", "/app/backups"]

View File

@@ -3,7 +3,7 @@ Authentication API endpoints
""" """
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List 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 fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -24,15 +24,39 @@ from app.auth.schemas import (
ThemePreferenceUpdate ThemePreferenceUpdate
) )
from app.config import settings from app.config import settings
from app.core.logging import get_logger, log_auth_attempt
router = APIRouter() router = APIRouter()
logger = get_logger("auth")
@router.post("/login", response_model=Token) @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""" """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) user = authenticate_user(db, login_data.username, login_data.password)
if not user: 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( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", 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( access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires 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"} return {"access_token": access_token, "token_type": "bearer"}
@@ -87,6 +125,7 @@ async def register(
@router.get("/me", response_model=UserResponse) @router.get("/me", response_model=UserResponse)
async def read_users_me(current_user: User = Depends(get_current_user)): async def read_users_me(current_user: User = Depends(get_current_user)):
"""Get current user info""" """Get current user info"""
logger.debug("User info requested", username=current_user.username, user_id=current_user.id)
return current_user return current_user

View File

@@ -40,6 +40,12 @@ class Settings(BaseSettings):
secure_cookies: bool = False secure_cookies: bool = False
compose_project_name: Optional[str] = None 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: class Config:
env_file = ".env" env_file = ".env"

View File

@@ -12,18 +12,31 @@ from app.database.base import engine
from app.models import BaseModel from app.models import BaseModel
from app.models.user import User from app.models.user import User
from app.auth.security import get_admin_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 # Create database tables
logger.info("Creating database tables")
BaseModel.metadata.create_all(bind=engine) BaseModel.metadata.create_all(bind=engine)
# Initialize FastAPI app # Initialize FastAPI app
logger.info("Initializing FastAPI application", version=settings.app_version, debug=settings.debug)
app = FastAPI( app = FastAPI(
title=settings.app_name, title=settings.app_name,
version=settings.app_version, version=settings.app_version,
description="Modern Python web application for Delphi Consulting Group", 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 # Configure CORS
logger.info("Configuring CORS middleware")
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], # Configure appropriately for production allow_origins=["*"], # Configure appropriately for production
@@ -33,10 +46,12 @@ app.add_middleware(
) )
# Mount static files # Mount static files
logger.info("Mounting static file directories")
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
# Templates # Templates
logger.info("Initializing Jinja2 templates")
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
# Include routers # 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.support import router as support_router
from app.api.settings import router as settings_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(auth_router, prefix="/api/auth", tags=["authentication"])
app.include_router(customers_router, prefix="/api/customers", tags=["customers"]) app.include_router(customers_router, prefix="/api/customers", tags=["customers"])
app.include_router(files_router, prefix="/api/files", tags=["files"]) app.include_router(files_router, prefix="/api/files", tags=["files"])

View File

130
app/middleware/logging.py Normal file
View File

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

View File

@@ -9,6 +9,9 @@ from fastapi import Request
from app.models.audit import AuditLog, LoginAttempt from app.models.audit import AuditLog, LoginAttempt
from app.models.user import User from app.models.user import User
from app.core.logging import get_logger
logger = get_logger("audit")
class AuditService: class AuditService:
@@ -73,7 +76,7 @@ class AuditService:
except Exception as e: except Exception as e:
db.rollback() db.rollback()
# Log the error but don't fail the main operation # 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 return audit_log
@staticmethod @staticmethod
@@ -128,7 +131,7 @@ class AuditService:
except Exception as e: except Exception as e:
db.rollback() db.rollback()
# Log the error but don't fail the main operation # 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 return login_attempt
@staticmethod @staticmethod

View File

@@ -32,3 +32,6 @@ httpx==0.28.1
# Development # Development
python-dotenv==1.0.1 python-dotenv==1.0.1
# Logging
loguru==0.7.2

View File

@@ -1643,8 +1643,48 @@ async function createBackup() {
} }
} }
function downloadBackup(filename) { async function downloadBackup(filename) {
window.open('/api/admin/backup/download', '_blank'); 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 // Utility Functions

View File

@@ -407,7 +407,7 @@
// Advanced Search JavaScript // Advanced Search JavaScript
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Initialize search components // Initialize search components
initializeSearch(); initializeAdvancedSearch();
loadSearchFacets(); loadSearchFacets();
setupEventHandlers(); setupEventHandlers();
setupKeyboardShortcuts(); setupKeyboardShortcuts();
@@ -424,7 +424,7 @@ let currentSearchCriteria = {};
let searchTimeout; let searchTimeout;
let facetsData = {}; let facetsData = {};
function initializeSearch() { function initializeAdvancedSearch() {
// Set default date for today // Set default date for today
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];