diff --git a/.gitignore b/.gitignore
index 117844b..55bf421 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,12 @@ uploads/
.env
.flaskenv
+# Test scripts
+test_upload.py
+test_b2_connection.py
+diagnose_ssl.py
+test_b2_insecure.py
+
# IDE
.vscode/
.idea/
diff --git a/Makefile b/Makefile
index 54fc4bf..066ace6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: help install run down restart test clean format lint check migrate createsuperuser
+.PHONY: help install run down restart test clean format lint check migrate createsuperuser shell
PYTHON := backend/venv/bin/python
PYTEST := backend/venv/bin/pytest
@@ -16,6 +16,7 @@ help:
@echo " make check Run Django system checks"
@echo " make migrate Run database migrations"
@echo " make createsuperuser Create Django superuser"
+ @echo " make shell Open Django shell"
install:
cd backend && venv/bin/pip install -r ../requirements.txt
@@ -54,3 +55,6 @@ migrate:
createsuperuser:
cd backend && ../$(PYTHON) manage.py createsuperuser
+
+shell:
+ cd backend && ../$(PYTHON) manage.py shell
diff --git a/backend/config/decorators.py b/backend/config/decorators.py
new file mode 100644
index 0000000..5252f29
--- /dev/null
+++ b/backend/config/decorators.py
@@ -0,0 +1,34 @@
+"""
+Custom decorators for database error handling
+"""
+from functools import wraps
+from django.db.utils import OperationalError, DatabaseError
+from django.http import JsonResponse
+from django.shortcuts import render
+
+
+def handle_database_errors(view_func):
+ """
+ Decorator to catch database errors in views and return graceful error responses
+ Use this on views that might fail if database is down
+ """
+ @wraps(view_func)
+ def wrapper(request, *args, **kwargs):
+ try:
+ return view_func(request, *args, **kwargs)
+ except (OperationalError, DatabaseError) as e:
+ # Log the error
+ print(f"Database error in {view_func.__name__}: {e}")
+
+ # Return appropriate error response based on request type
+ if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.content_type == 'application/json':
+ # For AJAX/API requests, return JSON error
+ return JsonResponse({
+ 'error': 'Database temporarily unavailable',
+ 'message': 'Please try again in a moment'
+ }, status=503)
+ else:
+ # For regular requests, render error page
+ return render(request, 'errors/database_error.html', status=503)
+
+ return wrapper
diff --git a/backend/config/middleware.py b/backend/config/middleware.py
new file mode 100644
index 0000000..39d852f
--- /dev/null
+++ b/backend/config/middleware.py
@@ -0,0 +1,106 @@
+"""
+Custom middleware for database connection handling
+"""
+import time
+from django.conf import settings
+from django.db import connection
+from django.db.utils import OperationalError
+from django.http import HttpResponse
+from django.shortcuts import render
+
+
+class DatabaseHealthCheckMiddleware:
+ """
+ Middleware to handle database connection issues gracefully with retry logic
+ """
+
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ def __call__(self, request):
+ # Try to ensure database connection with retries
+ if not self._ensure_database_connection():
+ # If all retries failed, show a friendly error page
+ return self._render_database_error_page(request)
+
+ response = self.get_response(request)
+ return response
+
+ def _ensure_database_connection(self):
+ """
+ Attempt to connect to database with exponential backoff retries
+ Returns True if successful, False otherwise
+ """
+ for attempt in range(settings.DATABASE_RETRY_ATTEMPTS):
+ try:
+ # Try a simple database query to check connection
+ connection.ensure_connection()
+ return True
+ except OperationalError as e:
+ # If this is the last attempt, give up
+ if attempt == settings.DATABASE_RETRY_ATTEMPTS - 1:
+ print(f"Database connection failed after {settings.DATABASE_RETRY_ATTEMPTS} attempts")
+ return False
+
+ # Wait before retrying (exponential backoff)
+ delay = settings.DATABASE_RETRY_DELAYS[attempt]
+ print(f"Database connection attempt {attempt + 1} failed, retrying in {delay}s...")
+ time.sleep(delay)
+
+ return False
+
+ def _render_database_error_page(self, request):
+ """
+ Render a friendly error page when database is unavailable
+ """
+ html = """
+
+
+
+ Service Temporarily Unavailable
+
+
+
+
+
We'll be right back!
+
Our database is taking a quick nap. This usually resolves itself in a few moments.
+
Please try refreshing the page in a moment.
+
+
+
+
+ """
+ return HttpResponse(html, status=503)
diff --git a/backend/config/settings.py b/backend/config/settings.py
index df25bfd..079ea35 100644
--- a/backend/config/settings.py
+++ b/backend/config/settings.py
@@ -59,6 +59,7 @@
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # Serve static files
+ "config.middleware.DatabaseHealthCheckMiddleware", # Database health check with retry
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@@ -96,9 +97,14 @@
"default": dj_database_url.config(
default=os.environ.get("DATABASE_URL"),
conn_max_age=600,
+ conn_health_checks=True, # Enable connection health checks
)
}
+# Database connection retry settings (exponential backoff: 1s, 2s, 4s)
+DATABASE_RETRY_ATTEMPTS = 3
+DATABASE_RETRY_DELAYS = [1, 2, 4] # Exponential backoff in seconds
+
# Password validation
diff --git a/backend/listings/utils.py b/backend/listings/utils.py
index 5da358b..28bbd3e 100644
--- a/backend/listings/utils.py
+++ b/backend/listings/utils.py
@@ -138,7 +138,7 @@ def save_picture_to_b2(form_picture):
def save_picture(form_picture):
- """Save picture to B2 if configured, otherwise save locally"""
+ """Save picture to B2 storage only - no local fallback"""
# Validate file first
try:
validate_image_file(form_picture)
@@ -146,41 +146,14 @@ def save_picture(form_picture):
print(f"File validation error: {e}")
return None
- # Try B2 first if configured and available
- if B2_AVAILABLE and settings.B2_KEY_ID:
- b2_filename = save_picture_to_b2(form_picture)
- if b2_filename:
- return b2_filename
-
- # Fallback to local storage
- random_hex = secrets.token_hex(8)
- picture_fn = f"{random_hex}.jpg" # Always save as .jpg for smaller files
- picture_path = os.path.join(settings.MEDIA_ROOT, picture_fn)
-
- # Create upload directory if it doesn't exist
- os.makedirs(os.path.dirname(picture_path), exist_ok=True)
-
- # Resize and optimize image
- img = Image.open(form_picture)
-
- # Apply EXIF orientation to prevent rotation issues
- try:
- from PIL import ImageOps
- img = ImageOps.exif_transpose(img)
- except Exception:
- pass # If EXIF orientation fails, continue without it
-
- # Convert to RGB if necessary (for JPEG)
- if img.mode in ("RGBA", "LA", "P"):
- img = img.convert("RGB")
-
- # Resize to max 800x600 while maintaining aspect ratio
- img.thumbnail((800, 600), Image.Resampling.LANCZOS)
-
- # Save as optimized JPEG
- img.save(picture_path, format="JPEG", quality=85, optimize=True)
+ # B2 is required - fail if not configured
+ if not B2_AVAILABLE or not settings.B2_KEY_ID:
+ print("ERROR: B2 storage not configured")
+ return None
- return picture_fn
+ # Upload to B2 - fail if it doesn't work
+ b2_filename = save_picture_to_b2(form_picture)
+ return b2_filename # Will be None if upload failed
def delete_photo_from_b2(filename):
diff --git a/backend/listings/views.py b/backend/listings/views.py
index 79edfe0..14b40ab 100644
--- a/backend/listings/views.py
+++ b/backend/listings/views.py
@@ -157,22 +157,20 @@ def user_login(request):
if request.method == "POST":
form = LoginForm(request, data=request.POST)
if form.is_valid():
- email = form.cleaned_data.get("username")
- password = form.cleaned_data.get("password")
- user = authenticate(request, username=email, password=password)
- if user is not None:
- login(request, user)
- messages.success(request, f"Welcome back, {user.first_name}!")
-
- # Validate next parameter to prevent open redirects
- next_url = request.GET.get("next", "index")
- if url_has_allowed_host_and_scheme(
- url=next_url,
- allowed_hosts={request.get_host()},
- require_https=request.is_secure()
- ):
- return redirect(next_url)
- return redirect("index")
+ # AuthenticationForm validates credentials automatically
+ user = form.get_user()
+ login(request, user)
+ messages.success(request, f"Welcome back, {user.first_name}!")
+
+ # Validate next parameter to prevent open redirects
+ next_url = request.GET.get("next", "index")
+ if url_has_allowed_host_and_scheme(
+ url=next_url,
+ allowed_hosts={request.get_host()},
+ require_https=request.is_secure()
+ ):
+ return redirect(next_url)
+ return redirect("index")
else:
form = LoginForm()
diff --git a/backend/templates/errors/database_error.html b/backend/templates/errors/database_error.html
new file mode 100644
index 0000000..d603f26
--- /dev/null
+++ b/backend/templates/errors/database_error.html
@@ -0,0 +1,113 @@
+
+
+
+
+
+ Service Temporarily Unavailable - VedgyProject
+
+
+
+
+
💤
+
We'll be right back!
+
Our database is taking a quick nap. This usually resolves itself in a few moments.
+
If you're seeing this, it means our service is temporarily experiencing connection issues.
+
+
+
← Return to Home
+
+
+
+
+
diff --git a/backend/templates/login.html b/backend/templates/login.html
index f367cac..03eb464 100644
--- a/backend/templates/login.html
+++ b/backend/templates/login.html
@@ -4,10 +4,18 @@