working now
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
16
app/main.py
16
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"])
|
||||
|
||||
0
app/middleware/__init__.py
Normal file
0
app/middleware/__init__.py
Normal file
130
app/middleware/logging.py
Normal file
130
app/middleware/logging.py
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -31,4 +31,7 @@ pytest-asyncio==0.24.0
|
||||
httpx==0.28.1
|
||||
|
||||
# Development
|
||||
python-dotenv==1.0.1
|
||||
python-dotenv==1.0.1
|
||||
|
||||
# Logging
|
||||
loguru==0.7.2
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user