This commit is contained in:
Iliyan Angelov
2025-12-10 01:36:00 +02:00
parent 2f6dca736a
commit 6a9e823402
84 changed files with 5293 additions and 1836 deletions

View File

@@ -1,39 +0,0 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build
.venv
venv/
env/
ENV/
.env
.venv
*.log
logs/
*.db
*.sqlite3
db.sqlite3
.git
.gitignore
README.md
*.md
.DS_Store
.vscode
.idea
*.swp
*.swo
*~
.pytest_cache
.coverage
htmlcov/
.tox/
.mypy_cache/
.dmypy.json
dmypy.json

View File

@@ -1,10 +1,10 @@
# Development Environment Configuration
# Django Settings
SECRET_KEY=ks68*5@of1l&4rn1imsqdk9$khcya!&a#jtd89f!v^qg1w0&hc
SECRET_KEY=2Yq6sylwG3rLGvD6AQHCsk2nmcwy2EOj5iFhOOR8ZkEeGsnDz_BNvu7J_fGudIkIyug
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
ALLOWED_HOSTS=gnxsoft.com,www.gnxsoft.com,YOUR_SERVER_IP,localhost,127.0.0.1
INTERNAL_API_KEY=your-generated-key-here
INTERNAL_API_KEY=9hZtPwyScigoBAl59Uvcz_9VztSRC6Zt_6L1B2xTM2M
PRODUCTION_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
@@ -15,11 +15,11 @@ COMPANY_EMAIL=support@gnxsoft.com
SUPPORT_EMAIL=support@gnxsoft.com
# Site URL
SITE_URL=http://localhost:3000
SITE_URL=https://gnxsoft.com
# SMTP Configuration (for production or when USE_SMTP_IN_DEV=True)
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=mail.gnxsoft.com
EMAIL_HOST=localhost
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_USE_SSL=False

68
backEnd/.gitignore vendored Normal file
View File

@@ -0,0 +1,68 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
env/
ENV/
.venv
# Django
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
/media
/staticfiles
/static
# Environment variables
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
logs/
*.log
# Coverage
htmlcov/
.coverage
.coverage.*
coverage.xml
*.cover
# Testing
.pytest_cache/
.tox/

View File

@@ -1,36 +0,0 @@
# Django Backend Dockerfile
FROM python:3.12-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
DEBIAN_FRONTEND=noninteractive
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
# Copy project
COPY . /app/
# Create directories for media and static files
RUN mkdir -p /app/media /app/staticfiles /app/logs
# Collect static files (will be done at runtime if needed)
# RUN python manage.py collectstatic --noinput
# Expose port
EXPOSE 1086
# Run gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:1086", "--workers", "3", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "gnx.wsgi:application"]

View File

@@ -1,4 +1,7 @@
from rest_framework import serializers
from django.utils.html import strip_tags, escape
from django.core.exceptions import ValidationError
import re
from .models import ContactSubmission
@@ -126,6 +129,72 @@ class ContactSubmissionCreateSerializer(serializers.ModelSerializer):
'privacy_consent',
]
def _sanitize_text_field(self, value):
"""
Sanitize text fields by detecting and rejecting HTML/script tags.
Returns cleaned text or raises ValidationError if dangerous content is detected.
"""
if not value:
return value
# Check for script tags and other dangerous HTML patterns
dangerous_patterns = [
(r'<script[^>]*>.*?</script>', 'Script tags are not allowed'),
(r'<iframe[^>]*>.*?</iframe>', 'Iframe tags are not allowed'),
(r'javascript:', 'JavaScript protocol is not allowed'),
(r'on\w+\s*=', 'Event handlers are not allowed'),
(r'<svg[^>]*onload', 'SVG onload handlers are not allowed'),
(r'<img[^>]*onerror', 'Image onerror handlers are not allowed'),
(r'<[^>]+>', 'HTML tags are not allowed'), # Catch any remaining HTML tags
]
value_lower = value.lower()
for pattern, message in dangerous_patterns:
if re.search(pattern, value_lower, re.IGNORECASE | re.DOTALL):
raise serializers.ValidationError(
f"Invalid input detected: {message}. Please remove HTML tags and scripts."
)
# Strip any remaining HTML tags (defense in depth)
cleaned = strip_tags(value)
# Remove any remaining script-like content
cleaned = re.sub(r'javascript:', '', cleaned, flags=re.IGNORECASE)
return cleaned.strip()
def validate_first_name(self, value):
"""Sanitize first name field."""
return self._sanitize_text_field(value)
def validate_last_name(self, value):
"""Sanitize last name field."""
return self._sanitize_text_field(value)
def validate_company(self, value):
"""Sanitize company field."""
return self._sanitize_text_field(value)
def validate_job_title(self, value):
"""Sanitize job title field."""
return self._sanitize_text_field(value)
def validate_message(self, value):
"""Sanitize message field."""
return self._sanitize_text_field(value)
def validate_phone(self, value):
"""Sanitize phone field - only allow alphanumeric, spaces, dashes, parentheses, and plus."""
if not value:
return value
# Remove HTML tags
cleaned = strip_tags(value)
# Only allow phone number characters
if not re.match(r'^[\d\s\-\+\(\)]+$', cleaned):
raise serializers.ValidationError("Phone number contains invalid characters.")
return cleaned.strip()
def validate_privacy_consent(self, value):
"""
Ensure privacy consent is given.

View File

@@ -62,6 +62,15 @@ class ContactSubmissionViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
return [permission() for permission in permission_classes]
def get_authenticators(self):
"""
Override authentication for create action to bypass CSRF.
By returning an empty list, DRF won't enforce CSRF for this action.
"""
if hasattr(self, 'action') and self.action == 'create':
return []
return super().get_authenticators()
def create(self, request, *args, **kwargs):
"""
Create a new contact submission.
@@ -259,4 +268,4 @@ class ContactSubmissionViewSet(viewsets.ModelViewSet):
return Response({
'error': 'Failed to send test email',
'status': 'error'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

Binary file not shown.

View File

@@ -0,0 +1,101 @@
"""
Custom email backend that handles localhost SSL certificate issues.
Disables SSL certificate verification for localhost connections.
"""
import ssl
from django.core.mail.backends.smtp import EmailBackend
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
class LocalhostSMTPBackend(EmailBackend):
"""
Custom SMTP backend that disables SSL certificate verification
for localhost connections. This is safe for localhost mail servers.
"""
def open(self):
"""
Override to create SSL context without certificate verification
when connecting to localhost.
"""
if self.use_ssl or self.use_tls:
# Check if connecting to localhost
if self.host in ['localhost', '127.0.0.1', '::1']:
# Create SSL context without certificate verification for localhost
self.connection = None
try:
import smtplib
if self.use_ssl:
# For SSL connections
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
# SMTP_SSL uses 'context' parameter (Python 3.3+)
import sys
if sys.version_info >= (3, 3):
self.connection = smtplib.SMTP_SSL(
self.host,
self.port,
timeout=self.timeout,
context=context
)
else:
# For older Python, use unverified context
self.connection = smtplib.SMTP_SSL(
self.host,
self.port,
timeout=self.timeout
)
else:
# For TLS connections
self.connection = smtplib.SMTP(
self.host,
self.port,
timeout=self.timeout
)
# Create SSL context without certificate verification
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
# Use context parameter (Python 3.4+ uses 'context', not 'ssl_context')
# For older versions, we'll need to patch the socket after starttls
import sys
if sys.version_info >= (3, 4):
# Python 3.4+ supports context parameter
self.connection.starttls(context=context)
else:
# For older Python, disable verification globally for this connection
# by monkey-patching ssl._create_default_https_context temporarily
original_context = ssl._create_default_https_context
ssl._create_default_https_context = ssl._create_unverified_context
try:
self.connection.starttls()
finally:
ssl._create_default_https_context = original_context
if self.username and self.password:
self.connection.login(self.username, self.password)
logger.info(f"Successfully connected to localhost mail server at {self.host}:{self.port}")
return True
except Exception as e:
logger.error(f"Failed to connect to localhost mail server: {str(e)}")
if self.connection:
try:
self.connection.quit()
except:
pass
self.connection = None
raise
else:
# For non-localhost, use standard SSL/TLS with certificate verification
return super().open()
else:
# No SSL/TLS, use standard connection
return super().open()

View File

@@ -0,0 +1,37 @@
"""
CSRF Exemption Middleware
Exempts CSRF checks for specific public API endpoints that don't require authentication.
"""
from django.utils.deprecation import MiddlewareMixin
import re
class CSRFExemptMiddleware(MiddlewareMixin):
"""
Middleware to exempt CSRF for public API endpoints.
Runs before CSRF middleware to set the exemption flag.
"""
# Paths that should be exempt from CSRF (public endpoints)
# Patterns match both with and without trailing slashes
EXEMPT_PATHS = [
r'^/api/contact/submissions/?$', # Contact form submission
r'^/api/career/applications/?$', # Job application submission (if needed)
r'^/api/support/tickets/?$', # Support ticket creation (if needed)
]
def process_request(self, request):
"""
Set CSRF exemption flag for matching paths.
"""
if request.method == 'POST':
path = request.path
for pattern in self.EXEMPT_PATHS:
if re.match(pattern, path):
# Set flag to bypass CSRF check
setattr(request, '_dont_enforce_csrf_checks', True)
break
return None

View File

@@ -68,6 +68,7 @@ MIDDLEWARE = [
'gnx.middleware.api_security.FrontendAPIProxyMiddleware', # Validate requests from frontend/nginx
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'gnx.middleware.csrf_exempt.CSRFExemptMiddleware', # Exempt CSRF for public API endpoints
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
@@ -98,22 +99,34 @@ WSGI_APPLICATION = 'gnx.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
# Support both PostgreSQL (production) and SQLite (development)
DATABASE_URL = config('DATABASE_URL', default='')
if DATABASE_URL and DATABASE_URL.startswith('postgresql://'):
# PostgreSQL configuration
import dj_database_url
DATABASES = {
'default': dj_database_url.parse(DATABASE_URL, conn_max_age=600)
}
# Force SQLite - change this to False and set USE_POSTGRESQL=True to use PostgreSQL
FORCE_SQLITE = True # Set to False to allow PostgreSQL
if not FORCE_SQLITE:
# PostgreSQL configuration (only if FORCE_SQLITE is False)
USE_POSTGRESQL = config('USE_POSTGRESQL', default='False', cast=bool)
DATABASE_URL = config('DATABASE_URL', default='')
if USE_POSTGRESQL and DATABASE_URL and DATABASE_URL.startswith('postgresql://'):
import dj_database_url
DATABASES = {
'default': dj_database_url.parse(DATABASE_URL, conn_max_age=600)
}
else:
# Fallback to SQLite
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
else:
# SQLite configuration (development/fallback)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
# SQLite configuration (forced)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
}
# Password validation
@@ -355,8 +368,12 @@ if DEBUG and not USE_SMTP_IN_DEV:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
else:
# Production or Dev with SMTP enabled - use SMTP backend
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
EMAIL_HOST = config('EMAIL_HOST', default='mail.gnxsoft.com')
# Use custom backend for localhost to handle SSL certificate issues
if EMAIL_HOST in ['localhost', '127.0.0.1', '::1']:
EMAIL_BACKEND = 'gnx.email_backend.LocalhostSMTPBackend'
else:
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)
EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=False, cast=bool)
@@ -367,7 +384,8 @@ else:
EMAIL_TIMEOUT = config('EMAIL_TIMEOUT', default=30, cast=int)
# Site URL for email links
SITE_URL = config('SITE_URL', default='http://localhost:3000')
# Use production URL by default if not in DEBUG mode
SITE_URL = config('SITE_URL', default='https://gnxsoft.com' if not DEBUG else 'http://localhost:3000')
# Email connection settings for production reliability
EMAIL_CONNECTION_TIMEOUT = config('EMAIL_CONNECTION_TIMEOUT', default=10, cast=int)

View File

@@ -1,26 +1,33 @@
# Production Environment Configuration for GNX Contact Form
# Copy this file to .env and update with your actual values
# Production Environment Configuration for GNX-WEB
# Copy this file to .env in the backEnd directory and update with your actual values
# Backend runs on port 1086 (internal only, proxied through nginx)
# Django Settings
SECRET_KEY=your-super-secret-production-key-here
SECRET_KEY=your-super-secret-production-key-here-change-this-immediately
DEBUG=False
ALLOWED_HOSTS=gnxsoft.com,www.gnxsoft.com,your-server-ip
ALLOWED_HOSTS=gnxsoft.com,www.gnxsoft.com,your-server-ip,localhost,127.0.0.1
# Database - Using SQLite (default)
# SQLite is configured in settings.py - no DATABASE_URL needed
# Database - PostgreSQL on host (port 5433 to avoid conflict with Docker instance on 5432)
# Format: postgresql://USER:PASSWORD@HOST:PORT/DBNAME
# Create database: sudo -u postgres psql
# CREATE DATABASE gnx_db;
# CREATE USER gnx_user WITH PASSWORD 'your_secure_password';
# GRANT ALL PRIVILEGES ON DATABASE gnx_db TO gnx_user;
DATABASE_URL=postgresql://gnx_user:your_password_here@localhost:5433/gnx_db
# Email Configuration (Production)
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.gmail.com
EMAIL_HOST=mail.gnxsoft.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_USE_SSL=False
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
EMAIL_HOST_USER=your-email@gnxsoft.com
EMAIL_HOST_PASSWORD=your-email-password
DEFAULT_FROM_EMAIL=noreply@gnxsoft.com
# Company email for contact form notifications
COMPANY_EMAIL=contact@gnxsoft.com
SUPPORT_EMAIL=support@gnxsoft.com
# Email timeout settings for production reliability
EMAIL_TIMEOUT=30
@@ -35,6 +42,8 @@ SECURE_HSTS_PRELOAD=True
SECURE_CONTENT_TYPE_NOSNIFF=True
SECURE_BROWSER_XSS_FILTER=True
X_FRAME_OPTIONS=DENY
SESSION_COOKIE_SECURE=True
CSRF_COOKIE_SECURE=True
# CORS Settings (Production)
PRODUCTION_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
@@ -47,15 +56,27 @@ CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
# REQUIRED in production! Auto-generated only in DEBUG mode.
# Generate a secure key: python -c "import secrets; print(secrets.token_urlsafe(32))"
# Or get current key: python manage.py show_api_key
# This key must match the one in nginx configuration
INTERNAL_API_KEY=your-secure-api-key-here-change-this-in-production
# Admin IP Restriction - Only these IPs can access Django admin
# Comma-separated list of IP addresses or CIDR networks (e.g., 193.194.155.249 or 192.168.1.0/24)
ADMIN_ALLOWED_IPS=193.194.155.249
# Static Files
STATIC_ROOT=/var/www/gnx/staticfiles/
MEDIA_ROOT=/var/www/gnx/media/
# Custom allowed IPs for IP whitelist middleware (optional, comma-separated)
CUSTOM_ALLOWED_IPS=
# Site URL for email links and absolute URLs
SITE_URL=https://gnxsoft.com
# Static and Media Files (relative to backEnd directory)
# These will be collected/served from these locations
STATIC_ROOT=/home/gnx/Desktop/GNX-WEB/backEnd/staticfiles
MEDIA_ROOT=/home/gnx/Desktop/GNX-WEB/backEnd/media
# Logging
LOG_LEVEL=INFO
# Backend Port (internal only, nginx proxies to this)
# Backend runs on 127.0.0.1:1086
BACKEND_PORT=1086