diff --git a/.env.production b/.env.production new file mode 100644 index 00000000..898e83d7 --- /dev/null +++ b/.env.production @@ -0,0 +1,47 @@ +# Production Environment Configuration for Docker +# Django Settings +SECRET_KEY=ks68*5@of1l&4rn1imsqdk9$khcya!&a#jtd89f!v^qg1w0&hc +DEBUG=False +ALLOWED_HOSTS=gnxsoft.com,www.gnxsoft.com,localhost,127.0.0.1,backend + +# Database (SQLite for simplicity, or use PostgreSQL) +DATABASE_URL=postgresql://gnx:*4WfmDsfvNszbB3ozaQj0M#i@postgres:5432/gnxdb + +# Admin IP Restriction +ADMIN_ALLOWED_IPS=193.194.155.249 + +# Internal API Key (for nginx to backend communication) +INTERNAL_API_KEY=your-generated-key-here +PRODUCTION_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com +CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com + +# Email Configuration +EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend +EMAIL_HOST=mail.gnxsoft.com +EMAIL_PORT=587 +EMAIL_USE_TLS=True +EMAIL_USE_SSL=False +EMAIL_HOST_USER=support@gnxsoft.com +EMAIL_HOST_PASSWORD=P4eli240453. +DEFAULT_FROM_EMAIL=support@gnxsoft.com +COMPANY_EMAIL=support@gnxsoft.com +SUPPORT_EMAIL=support@gnxsoft.com + +# Site URL +SITE_URL=https://gnxsoft.com + +# Security Settings +SECURE_SSL_REDIRECT=True +SECURE_HSTS_SECONDS=31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS=True +SECURE_HSTS_PRELOAD=True + +# CORS Settings +CORS_ALLOWED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com + +# PostgreSQL Database Configuration (Recommended for Production) +POSTGRES_DB=gnxdb +POSTGRES_USER=gnx +POSTGRES_PASSWORD=*4WfmDsfvNszbB3ozaQj0M#i +# Update DATABASE_URL to use PostgreSQL (uncomment the line below and comment SQLite) +# DATABASE_URL=postgresql://gnxuser:change-this-password-in-production@postgres:5432/gnxdb diff --git a/.zipignore b/.zipignore new file mode 100644 index 00000000..42deebdc --- /dev/null +++ b/.zipignore @@ -0,0 +1,65 @@ +# Files to exclude from production zip +# These will be regenerated or are not needed on server + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ +.venv + +# Node +node_modules/ +.next/ +.npm +.yarn + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ +dev.log + +# Database (will be created fresh or migrated) +*.sqlite3 +*.db + +# Docker +.dockerignore + +# Git +.git/ +.gitignore + +# Backups +backups/ +*.backup +*.bak + +# Temporary files +*.tmp +*.temp + +# Environment files (will be created from .env.production) +.env.local +.env.development + +# Build artifacts +dist/ +build/ +*.egg-info/ + diff --git a/backEnd/.dockerignore b/backEnd/.dockerignore new file mode 100644 index 00000000..deaf5286 --- /dev/null +++ b/backEnd/.dockerignore @@ -0,0 +1,39 @@ +__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 + diff --git a/backEnd/.env b/backEnd/.env index 9d2f67ad..52475ce6 100644 --- a/backEnd/.env +++ b/backEnd/.env @@ -4,6 +4,10 @@ SECRET_KEY=ks68*5@of1l&4rn1imsqdk9$khcya!&a#jtd89f!v^qg1w0&hc DEBUG=True ALLOWED_HOSTS=localhost,127.0.0.1 +INTERNAL_API_KEY=your-generated-key-here +PRODUCTION_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com +CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com + # Email Configuration (Development - uses console backend by default) USE_SMTP_IN_DEV=True DEFAULT_FROM_EMAIL=support@gnxsoft.com diff --git a/backEnd/Dockerfile b/backEnd/Dockerfile new file mode 100644 index 00000000..657a230b --- /dev/null +++ b/backEnd/Dockerfile @@ -0,0 +1,36 @@ +# 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"] + diff --git a/backEnd/db.sqlite3 b/backEnd/db.sqlite3 index 7fdd4407..04ca5d87 100644 Binary files a/backEnd/db.sqlite3 and b/backEnd/db.sqlite3 differ diff --git a/backEnd/gnx/__pycache__/settings.cpython-312.pyc b/backEnd/gnx/__pycache__/settings.cpython-312.pyc index 0ad5b231..52eb0fde 100644 Binary files a/backEnd/gnx/__pycache__/settings.cpython-312.pyc and b/backEnd/gnx/__pycache__/settings.cpython-312.pyc differ diff --git a/backEnd/gnx/management/__init__.py b/backEnd/gnx/management/__init__.py new file mode 100644 index 00000000..e2a6b467 --- /dev/null +++ b/backEnd/gnx/management/__init__.py @@ -0,0 +1,2 @@ +# Django management package + diff --git a/backEnd/gnx/management/commands/__init__.py b/backEnd/gnx/management/commands/__init__.py new file mode 100644 index 00000000..7cf03715 --- /dev/null +++ b/backEnd/gnx/management/commands/__init__.py @@ -0,0 +1,2 @@ +# Django management commands package + diff --git a/backEnd/gnx/management/commands/__pycache__/update_admin.cpython-312.pyc b/backEnd/gnx/management/commands/__pycache__/update_admin.cpython-312.pyc new file mode 100644 index 00000000..d170acab Binary files /dev/null and b/backEnd/gnx/management/commands/__pycache__/update_admin.cpython-312.pyc differ diff --git a/backEnd/gnx/management/commands/show_api_key.py b/backEnd/gnx/management/commands/show_api_key.py new file mode 100644 index 00000000..48594794 --- /dev/null +++ b/backEnd/gnx/management/commands/show_api_key.py @@ -0,0 +1,34 @@ +""" +Management command to display the current INTERNAL_API_KEY +Useful for copying to nginx configuration +""" +from django.core.management.base import BaseCommand +from django.conf import settings + + +class Command(BaseCommand): + help = 'Display the current INTERNAL_API_KEY for nginx configuration' + + def handle(self, *args, **options): + api_key = getattr(settings, 'INTERNAL_API_KEY', None) + + if not api_key: + self.stdout.write( + self.style.ERROR('โŒ INTERNAL_API_KEY is not set!') + ) + self.stdout.write( + ' Set it in your .env file or it will be auto-generated in DEBUG mode.' + ) + return + + self.stdout.write(self.style.SUCCESS('\nโœ“ Current INTERNAL_API_KEY:')) + self.stdout.write(self.style.WARNING(f'\n{api_key}\n')) + + self.stdout.write('๐Ÿ“‹ Copy this key to your nginx configuration:') + self.stdout.write(' In nginx.conf, set:') + self.stdout.write(f' set $api_key "{api_key}";') + self.stdout.write('') + self.stdout.write(' Or add to your .env file:') + self.stdout.write(f' INTERNAL_API_KEY={api_key}') + self.stdout.write('') + diff --git a/backEnd/gnx/management/commands/update_admin.py b/backEnd/gnx/management/commands/update_admin.py new file mode 100644 index 00000000..52eb1a4b --- /dev/null +++ b/backEnd/gnx/management/commands/update_admin.py @@ -0,0 +1,99 @@ +""" +Management command to update admin user credentials +""" +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +from django.db import transaction + +User = get_user_model() + + +class Command(BaseCommand): + help = 'Update admin user username and password' + + def add_arguments(self, parser): + parser.add_argument( + '--username', + type=str, + default='gnx', + help='Admin username (default: gnx)' + ) + parser.add_argument( + '--password', + type=str, + default='P4eli240453', + help='Admin password (default: P4eli240453)' + ) + + def handle(self, *args, **options): + username = options['username'] + password = options['password'] + + try: + with transaction.atomic(): + # Try to find existing user with this username + user = User.objects.filter(username=username).first() + + if user: + # Update existing user + user.set_password(password) + user.is_staff = True + user.is_superuser = True + user.is_active = True + user.save() + self.stdout.write( + self.style.SUCCESS( + f'โœ“ Successfully updated admin user "{username}"' + ) + ) + else: + # Check if there are any existing superusers + existing_superusers = User.objects.filter(is_superuser=True) + + if existing_superusers.exists(): + # Update the first superuser + user = existing_superusers.first() + old_username = user.username + user.username = username + user.set_password(password) + user.is_staff = True + user.is_superuser = True + user.is_active = True + user.save() + self.stdout.write( + self.style.SUCCESS( + f'โœ“ Successfully updated admin user from "{old_username}" to "{username}"' + ) + ) + else: + # Create new superuser + user = User.objects.create_user( + username=username, + password=password, + is_staff=True, + is_superuser=True, + is_active=True + ) + self.stdout.write( + self.style.SUCCESS( + f'โœ“ Successfully created admin user "{username}"' + ) + ) + + self.stdout.write( + self.style.SUCCESS( + f'\nAdmin credentials:\n' + f' Username: {username}\n' + f' Password: {password}\n' + f' Is Staff: {user.is_staff}\n' + f' Is Superuser: {user.is_superuser}\n' + f' Is Active: {user.is_active}\n' + ) + ) + + except Exception as e: + self.stdout.write( + self.style.ERROR(f'โŒ Error updating admin user: {str(e)}') + ) + raise + diff --git a/backEnd/gnx/middleware/__pycache__/admin_ip_restriction.cpython-312.pyc b/backEnd/gnx/middleware/__pycache__/admin_ip_restriction.cpython-312.pyc new file mode 100644 index 00000000..bc71406e Binary files /dev/null and b/backEnd/gnx/middleware/__pycache__/admin_ip_restriction.cpython-312.pyc differ diff --git a/backEnd/gnx/middleware/admin_ip_restriction.py b/backEnd/gnx/middleware/admin_ip_restriction.py new file mode 100644 index 00000000..25e0c70a --- /dev/null +++ b/backEnd/gnx/middleware/admin_ip_restriction.py @@ -0,0 +1,132 @@ +""" +Admin IP Restriction Middleware +Restricts Django admin access to specific IP addresses only +""" + +from django.http import HttpResponseForbidden +from django.conf import settings +from django.urls import resolve +import ipaddress +import logging + +logger = logging.getLogger('django.security') + + +class AdminIPRestrictionMiddleware: + """ + Restricts Django admin access to whitelisted IP addresses only. + This provides an additional layer of security for the admin panel. + """ + + def __init__(self, get_response): + self.get_response = get_response + + # Get allowed admin IPs from settings + # Default to the user's IP if not specified + admin_ips = getattr(settings, 'ADMIN_ALLOWED_IPS', ['193.194.155.249']) + + # Convert to list if it's a string + if isinstance(admin_ips, str): + admin_ips = [ip.strip() for ip in admin_ips.split(',') if ip.strip()] + + self.allowed_ips = [] + for ip_str in admin_ips: + try: + # Support both single IPs and CIDR notation + if '/' in ip_str: + self.allowed_ips.append(ipaddress.ip_network(ip_str, strict=False)) + else: + self.allowed_ips.append(ipaddress.ip_address(ip_str)) + except ValueError: + logger.warning(f"Invalid IP address in ADMIN_ALLOWED_IPS: {ip_str}") + + # Also allow localhost for development + self.allowed_ips.extend([ + ipaddress.ip_address('127.0.0.1'), + ipaddress.ip_address('::1'), + ]) + + def get_client_ip(self, request): + """ + Get the real client IP address, handling proxy headers + """ + # Check for forwarded IP (from proxy/load balancer) + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + # Get the first IP in the chain (original client) + ip = x_forwarded_for.split(',')[0].strip() + return ip + + # Check for real IP header (some proxies use this) + x_real_ip = request.META.get('HTTP_X_REAL_IP') + if x_real_ip: + return x_real_ip.strip() + + # Fall back to REMOTE_ADDR + return request.META.get('REMOTE_ADDR', '') + + def is_admin_path(self, request): + """ + Check if the request is for the Django admin + """ + path = request.path + return path.startswith('/admin/') + + def is_ip_allowed(self, client_ip): + """ + Check if the client IP is in the allowed list + """ + try: + client_ip_obj = ipaddress.ip_address(client_ip) + + # Check if IP matches any allowed IP or network + for allowed in self.allowed_ips: + if isinstance(allowed, ipaddress.IPv4Address) or isinstance(allowed, ipaddress.IPv6Address): + # Direct IP match + if client_ip_obj == allowed: + return True + elif isinstance(allowed, ipaddress.IPv4Network) or isinstance(allowed, ipaddress.IPv6Network): + # Network/CIDR match + if client_ip_obj in allowed: + return True + + return False + except ValueError: + logger.error(f"Invalid IP address format: {client_ip}") + return False + + def __call__(self, request): + # Only check admin paths + if not self.is_admin_path(request): + return self.get_response(request) + + # In DEBUG mode, you might want to allow all (optional) + # Uncomment the next 3 lines if you want to disable IP restriction in DEBUG mode + # if settings.DEBUG: + # return self.get_response(request) + + # Get client IP + client_ip = self.get_client_ip(request) + + if not client_ip: + logger.warning("Could not determine client IP for admin access attempt") + return HttpResponseForbidden( + "

Access Denied

" + "

Unable to verify your IP address. Admin access is restricted.

" + ) + + # Check if IP is allowed + if not self.is_ip_allowed(client_ip): + logger.warning( + f"Blocked admin access attempt from IP: {client_ip} " + f"to path: {request.path}" + ) + return HttpResponseForbidden( + "

Access Denied

" + "

Admin access is restricted to authorized IP addresses only.

" + f"

Your IP: {client_ip}

" + ) + + # IP is allowed, continue + return self.get_response(request) + diff --git a/backEnd/gnx/settings.py b/backEnd/gnx/settings.py index 2f97caa8..6dfa1e29 100644 --- a/backEnd/gnx/settings.py +++ b/backEnd/gnx/settings.py @@ -12,6 +12,8 @@ https://docs.djangoproject.com/en/4.2/ref/settings/ from pathlib import Path import os +import secrets +import warnings from decouple import config # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -62,6 +64,7 @@ MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'gnx.middleware.ip_whitelist.IPWhitelistMiddleware', # Production: Block external access + 'gnx.middleware.admin_ip_restriction.AdminIPRestrictionMiddleware', # Restrict admin to specific IPs 'gnx.middleware.api_security.FrontendAPIProxyMiddleware', # Validate requests from frontend/nginx 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -95,6 +98,16 @@ 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) + } +else: + # SQLite configuration (development/fallback) DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', @@ -162,6 +175,10 @@ INTERNAL_IPS = ['127.0.0.1', '::1'] # Custom allowed IPs for IP whitelist middleware (comma-separated) CUSTOM_ALLOWED_IPS = config('CUSTOM_ALLOWED_IPS', default='', cast=lambda v: [s.strip() for s in v.split(',') if s.strip()]) +# Admin IP Restriction - Only these IPs can access Django admin +# Comma-separated list of IP addresses or CIDR networks +ADMIN_ALLOWED_IPS = config('ADMIN_ALLOWED_IPS', default='193.194.155.249', cast=lambda v: [s.strip() for s in v.split(',') if s.strip()]) + # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ @@ -236,8 +253,28 @@ REST_FRAMEWORK = { # ============================================================================ # Internal API Key - nginx will add this header to prove request came through proxy -# Generate a strong random key: python -c "import secrets; print(secrets.token_urlsafe(32))" -INTERNAL_API_KEY = config('INTERNAL_API_KEY', default='' if not DEBUG else 'dev-key-change-in-production') +# Auto-generates a secure key if not provided (only in DEBUG mode for development) +# In production, you MUST set INTERNAL_API_KEY in your .env file +_manual_api_key = config('INTERNAL_API_KEY', default='') +if not _manual_api_key: + if DEBUG: + # Auto-generate a secure key for development + _auto_generated_key = secrets.token_urlsafe(32) + INTERNAL_API_KEY = _auto_generated_key + warnings.warn( + f"โš ๏ธ INTERNAL_API_KEY not set. Auto-generated key for development: {_auto_generated_key}\n" + f" Add this to your nginx config and .env file for consistency.\n" + f" In production, you MUST set INTERNAL_API_KEY explicitly in .env", + UserWarning + ) + else: + # Production requires explicit key + raise ValueError( + "INTERNAL_API_KEY must be set in production. " + "Generate one with: python -c \"import secrets; print(secrets.token_urlsafe(32))\"" + ) +else: + INTERNAL_API_KEY = _manual_api_key # API security path pattern (default: all /api/ paths) API_SECURITY_PATH_PATTERN = config('API_SECURITY_PATH_PATTERN', default=r'^/api/') diff --git a/backEnd/logs/django.log b/backEnd/logs/django.log index 88f262e9..ba1f0950 100644 --- a/backEnd/logs/django.log +++ b/backEnd/logs/django.log @@ -32877,3 +32877,307 @@ INFO 2025-11-24 06:40:57,132 autoreload 139932 123776047718528 /home/gnx/Desktop INFO 2025-11-24 06:40:57,712 autoreload 140119 136966227431552 Watching for file changes with StatReloader INFO 2025-11-24 06:41:38,174 autoreload 140119 136966227431552 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading. INFO 2025-11-24 06:41:38,716 autoreload 140239 133780171337856 Watching for file changes with StatReloader +INFO 2025-11-24 13:09:15,381 autoreload 11176 128073016684672 Watching for file changes with StatReloader +INFO 2025-11-24 13:10:34,723 basehttp 11176 128072933168832 "OPTIONS /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 0 +INFO 2025-11-24 13:10:34,726 basehttp 11176 128072933168832 "OPTIONS /api/career/jobs HTTP/1.1" 200 0 +INFO 2025-11-24 13:10:34,726 basehttp 11176 128072949954240 "OPTIONS /api/home/banner/ HTTP/1.1" 200 0 +INFO 2025-11-24 13:10:34,725 basehttp 11176 128072941561536 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0 +INFO 2025-11-24 13:10:34,730 basehttp 11176 128072924776128 "OPTIONS /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 0 +INFO 2025-11-24 13:10:34,775 basehttp 11176 128072949954240 "GET /api/home/banner/ HTTP/1.1" 200 3438 +INFO 2025-11-24 13:10:34,779 basehttp 11176 128072916383424 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744 +INFO 2025-11-24 13:10:34,792 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:10:34,826 basehttp 11176 128072933168832 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:10:34,863 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:10:34,871 basehttp 11176 128072924776128 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374 +INFO 2025-11-24 13:10:34,919 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:04,757 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:04,757 basehttp 11176 128072924776128 "OPTIONS /api/blog/categories/ HTTP/1.1" 200 0 +INFO 2025-11-24 13:11:04,757 basehttp 11176 128072916383424 "OPTIONS /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 0 +INFO 2025-11-24 13:11:04,773 basehttp 11176 128072933168832 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:11:04,796 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:04,803 basehttp 11176 128072924776128 "GET /api/blog/categories/ HTTP/1.1" 200 1418 +INFO 2025-11-24 13:11:04,809 basehttp 11176 128072933168832 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:11:04,821 basehttp 11176 128072941561536 "GET /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 5121 +INFO 2025-11-24 13:11:04,852 basehttp 11176 128072949954240 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:04,859 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:04,875 basehttp 11176 128072924776128 "GET /api/blog/categories/ HTTP/1.1" 200 1418 +INFO 2025-11-24 13:11:04,889 basehttp 11176 128072949954240 "GET /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 5121 +INFO 2025-11-24 13:11:09,343 basehttp 11176 128072569321152 "GET /api/blog/posts/best-practices-for-building-scalable-enterprise-apis/ HTTP/1.1" 200 4445 +INFO 2025-11-24 13:11:09,514 basehttp 11176 128072924776128 "OPTIONS /api/blog/posts/latest/?limit=8 HTTP/1.1" 200 0 +INFO 2025-11-24 13:11:09,514 basehttp 11176 128072949954240 "OPTIONS /api/blog/posts/best-practices-for-building-scalable-enterprise-apis/ HTTP/1.1" 200 0 +INFO 2025-11-24 13:11:09,533 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:09,543 basehttp 11176 128072916383424 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:11:09,592 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:09,592 basehttp 11176 128072949954240 "GET /api/blog/posts/best-practices-for-building-scalable-enterprise-apis/ HTTP/1.1" 200 4445 +INFO 2025-11-24 13:11:09,602 basehttp 11176 128072916383424 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:11:09,611 basehttp 11176 128072924776128 "GET /api/blog/posts/latest/?limit=8 HTTP/1.1" 200 6678 +INFO 2025-11-24 13:11:09,652 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:09,672 basehttp 11176 128072949954240 "GET /api/blog/posts/best-practices-for-building-scalable-enterprise-apis/ HTTP/1.1" 200 4445 +INFO 2025-11-24 13:11:09,684 basehttp 11176 128072916383424 "GET /api/blog/posts/latest/?limit=8 HTTP/1.1" 200 6678 +INFO 2025-11-24 13:11:09,703 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:18,799 basehttp 11176 128072949954240 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:18,811 basehttp 11176 128072916383424 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:11:18,819 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:18,831 basehttp 11176 128072949954240 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:11:18,836 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:11:18,887 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:12:18,200 basehttp 11176 128072916383424 "GET /api/home/banner/ HTTP/1.1" 200 3438 +INFO 2025-11-24 13:12:18,208 basehttp 11176 128072949954240 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:12:18,221 basehttp 11176 128072941561536 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744 +INFO 2025-11-24 13:12:18,221 basehttp 11176 128072933168832 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:12:18,251 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:12:18,259 basehttp 11176 128072949954240 "GET /api/home/banner/ HTTP/1.1" 200 3438 +INFO 2025-11-24 13:12:18,267 basehttp 11176 128072933168832 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:12:18,274 basehttp 11176 128072941561536 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744 +INFO 2025-11-24 13:12:18,288 basehttp 11176 128072924776128 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374 +INFO 2025-11-24 13:12:18,304 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:12:18,334 basehttp 11176 128072924776128 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374 +INFO 2025-11-24 13:12:18,365 basehttp 11176 128072949954240 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:12:18,378 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:12:18,394 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:12:58,727 autoreload 11176 128073016684672 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading. +INFO 2025-11-24 13:12:59,333 autoreload 13550 127591779987584 Watching for file changes with StatReloader +INFO 2025-11-24 13:13:07,973 autoreload 13550 127591779987584 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading. +INFO 2025-11-24 13:13:08,571 autoreload 13656 125806206767232 Watching for file changes with StatReloader +INFO 2025-11-24 13:13:13,119 autoreload 13656 125806206767232 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading. +INFO 2025-11-24 13:13:13,738 autoreload 13700 126054542864512 Watching for file changes with StatReloader +INFO 2025-11-24 13:13:53,121 autoreload 13700 126054542864512 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading. +INFO 2025-11-24 13:13:53,738 autoreload 14178 134881899106432 Watching for file changes with StatReloader +INFO 2025-11-24 13:15:05,124 autoreload 14521 127272466882688 Watching for file changes with StatReloader +INFO 2025-11-24 13:15:28,205 basehttp 14521 127272391534272 "OPTIONS /api/about/page/ HTTP/1.1" 200 0 +INFO 2025-11-24 13:15:28,216 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:15:28,234 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:15:28,248 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:15:28,263 basehttp 14521 127272374748864 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 13:15:28,270 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:15:28,286 basehttp 14521 127272374748864 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 13:15:28,305 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:15:28,316 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:15:28,373 basehttp 14521 127272391534272 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 13:15:28,465 basehttp 14521 127272399926976 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 13:15:28,466 basehttp 14521 127272391534272 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 13:15:28,485 basehttp 14521 127272399926976 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 13:16:36,991 basehttp 14521 127272374748864 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744 +INFO 2025-11-24 13:16:37,011 basehttp 14521 127272399926976 "GET /api/home/banner/ HTTP/1.1" 200 3438 +INFO 2025-11-24 13:16:37,018 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:16:37,022 basehttp 14521 127272027682496 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:16:37,050 basehttp 14521 127272399926976 "GET /api/home/banner/ HTTP/1.1" 200 3438 +INFO 2025-11-24 13:16:37,058 basehttp 14521 127272374748864 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744 +INFO 2025-11-24 13:16:37,076 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:16:37,082 basehttp 14521 127272027682496 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:16:37,092 basehttp 14521 127272383141568 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374 +INFO 2025-11-24 13:16:37,141 basehttp 14521 127272374748864 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:16:37,150 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:16:37,155 basehttp 14521 127272399926976 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374 +INFO 2025-11-24 13:16:37,200 basehttp 14521 127272374748864 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:16:37,209 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:18:05,963 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:18:05,971 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:18:05,983 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:18:05,993 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:18:05,999 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:18:06,050 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:52:29,368 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 13:52:29,375 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:52:29,396 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:58:17,380 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:58:48,661 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:58:52,344 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 13:59:44,445 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:00:29,872 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:00:58,873 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:01:17,080 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:01:37,625 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:01:59,686 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:02:11,931 basehttp 14521 127272383141568 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0 +INFO 2025-11-24 14:02:11,936 basehttp 14521 127272391534272 "OPTIONS /api/career/jobs HTTP/1.1" 200 0 +INFO 2025-11-24 14:02:11,947 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:02:11,955 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:02:12,106 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:02:32,343 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:02:52,316 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:03:46,939 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:03:59,826 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:04:24,979 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:05:10,040 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:06:12,564 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:06:25,176 basehttp 14521 127272383141568 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0 +INFO 2025-11-24 14:06:25,184 basehttp 14521 127272383141568 "OPTIONS /api/career/jobs HTTP/1.1" 200 0 +INFO 2025-11-24 14:06:25,192 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:06:25,205 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:06:25,236 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:06:45,565 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:07:01,911 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:07:01,918 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:07:01,931 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:07:21,699 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:07:27,955 basehttp 14521 127272383141568 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0 +INFO 2025-11-24 14:07:27,965 basehttp 14521 127272391534272 "OPTIONS /api/career/jobs HTTP/1.1" 200 0 +INFO 2025-11-24 14:07:27,965 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:07:27,974 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:07:28,028 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:08:11,406 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:01,128 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:01,138 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:09:01,146 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:16,649 basehttp 14521 127272391534272 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0 +INFO 2025-11-24 14:09:16,656 basehttp 14521 127272383141568 "OPTIONS /api/career/jobs HTTP/1.1" 200 0 +INFO 2025-11-24 14:09:16,664 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:16,669 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:09:16,726 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:34,382 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:09:34,382 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:34,399 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:34,403 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:09:34,460 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:34,469 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:09:34,516 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:34,523 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:09:49,006 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:49,018 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:09:49,034 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:49,040 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:09:49,093 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:09:49,150 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:10:47,794 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:10:48,854 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:10:54,869 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:10:59,880 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:11:14,155 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:11:14,155 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:11:14,169 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:11:16,763 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:11:16,779 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:11:16,786 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:11:22,210 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:11:22,216 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:11:22,228 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:11:54,375 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:11:57,923 basehttp 14521 127272383141568 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0 +INFO 2025-11-24 14:11:57,936 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:12:12,466 basehttp 14521 127272399926976 "OPTIONS /api/career/jobs HTTP/1.1" 200 0 +INFO 2025-11-24 14:12:12,474 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:12:12,484 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:12:12,491 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:12:14,995 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:12:51,022 basehttp 14521 127272383141568 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0 +INFO 2025-11-24 14:12:51,025 basehttp 14521 127272374748864 "OPTIONS /api/career/jobs HTTP/1.1" 200 0 +INFO 2025-11-24 14:12:51,037 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:12:51,042 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:12:51,092 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:13:03,068 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:13:03,080 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:13:03,090 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:13:03,100 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:13:03,142 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:13:03,158 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:13:03,172 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:13:03,172 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:13:06,231 basehttp 14521 127272383141568 "OPTIONS /api/blog/categories/ HTTP/1.1" 200 0 +INFO 2025-11-24 14:13:06,238 basehttp 14521 127272374748864 "OPTIONS /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 0 +INFO 2025-11-24 14:13:06,262 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:13:06,262 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:13:06,299 basehttp 14521 127272383141568 "GET /api/blog/categories/ HTTP/1.1" 200 1418 +INFO 2025-11-24 14:13:06,311 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:13:06,321 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:13:06,326 basehttp 14521 127272374748864 "GET /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 5121 +INFO 2025-11-24 14:13:06,352 basehttp 14521 127272383141568 "GET /api/blog/categories/ HTTP/1.1" 200 1418 +INFO 2025-11-24 14:13:06,363 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:13:06,399 basehttp 14521 127272374748864 "GET /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 5121 +INFO 2025-11-24 14:13:06,413 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:13:18,876 basehttp 14521 127272026633920 "GET /admin/ HTTP/1.1" 302 0 +INFO 2025-11-24 14:13:18,898 basehttp 14521 127272026633920 "GET /admin/login/?next=/admin/ HTTP/1.1" 200 4173 +INFO 2025-11-24 14:13:18,993 basehttp 14521 127272026633920 "GET /static/admin/css/base.css HTTP/1.1" 200 22120 +INFO 2025-11-24 14:13:18,995 basehttp 14521 127272000407232 "GET /static/admin/css/nav_sidebar.css HTTP/1.1" 200 2810 +INFO 2025-11-24 14:13:18,996 basehttp 14521 127272017192640 "GET /static/admin/css/dark_mode.css HTTP/1.1" 200 2808 +INFO 2025-11-24 14:13:18,996 basehttp 14521 127272008799936 "GET /static/admin/js/theme.js HTTP/1.1" 200 1653 +INFO 2025-11-24 14:13:18,998 basehttp 14521 127271990965952 "GET /static/admin/js/nav_sidebar.js HTTP/1.1" 200 3063 +INFO 2025-11-24 14:13:18,999 basehttp 14521 127271982573248 "GET /static/admin/css/login.css HTTP/1.1" 200 951 +INFO 2025-11-24 14:13:19,001 basehttp 14521 127272000407232 "GET /static/admin/css/responsive.css HTTP/1.1" 200 16565 +WARNING 2025-11-24 14:13:19,077 log 14521 127272000407232 Not Found: /favicon.ico +WARNING 2025-11-24 14:13:19,077 basehttp 14521 127272000407232 "GET /favicon.ico HTTP/1.1" 404 3043 +INFO 2025-11-24 14:13:21,374 basehttp 14521 127272026633920 "POST /admin/login/?next=/admin/ HTTP/1.1" 200 4339 +INFO 2025-11-24 14:14:51,936 basehttp 14521 127272026633920 "POST /admin/login/?next=/admin/ HTTP/1.1" 302 0 +INFO 2025-11-24 14:14:51,973 basehttp 14521 127272026633920 "GET /admin/ HTTP/1.1" 200 39055 +INFO 2025-11-24 14:14:52,071 basehttp 14521 127272000407232 "GET /static/admin/css/dashboard.css HTTP/1.1" 200 441 +INFO 2025-11-24 14:14:52,088 basehttp 14521 127272000407232 "GET /static/admin/img/icon-addlink.svg HTTP/1.1" 200 331 +INFO 2025-11-24 14:14:52,088 basehttp 14521 127271990965952 "GET /static/admin/img/icon-changelink.svg HTTP/1.1" 200 380 +INFO 2025-11-24 14:14:52,090 basehttp 14521 127271990965952 "GET /static/admin/img/icon-deletelink.svg HTTP/1.1" 200 392 +INFO 2025-11-24 14:15:16,828 basehttp 14521 127272026633920 "GET /admin/case_studies/client/9/change/ HTTP/1.1" 302 0 +INFO 2025-11-24 14:15:16,866 basehttp 14521 127272026633920 "GET /admin/ HTTP/1.1" 200 39224 +INFO 2025-11-24 14:15:16,962 basehttp 14521 127272000407232 "GET /static/admin/img/icon-alert.svg HTTP/1.1" 200 504 +INFO 2025-11-24 14:15:20,705 basehttp 14521 127272026633920 "GET /admin/about/aboutmilestone/ HTTP/1.1" 200 34312 +INFO 2025-11-24 14:15:20,759 basehttp 14521 127272000407232 "GET /static/admin/css/changelists.css HTTP/1.1" 200 6878 +INFO 2025-11-24 14:15:20,761 basehttp 14521 127272017192640 "GET /static/admin/js/jquery.init.js HTTP/1.1" 200 347 +INFO 2025-11-24 14:15:20,761 basehttp 14521 127272008799936 "GET /static/admin/js/core.js HTTP/1.1" 200 6208 +INFO 2025-11-24 14:15:20,763 basehttp 14521 127271982573248 "GET /static/admin/js/admin/RelatedObjectLookups.js HTTP/1.1" 200 9777 +INFO 2025-11-24 14:15:20,769 basehttp 14521 127272000407232 "GET /static/admin/js/actions.js HTTP/1.1" 200 8076 +INFO 2025-11-24 14:15:20,770 basehttp 14521 127272017192640 "GET /static/admin/js/prepopulate.js HTTP/1.1" 200 1531 +INFO 2025-11-24 14:15:20,772 basehttp 14521 127272008799936 "GET /static/admin/js/urlify.js HTTP/1.1" 200 7887 +INFO 2025-11-24 14:15:20,774 basehttp 14521 127271990965952 "GET /admin/jsi18n/ HTTP/1.1" 200 3342 +INFO 2025-11-24 14:15:20,777 basehttp 14521 127272026633920 "GET /static/admin/js/vendor/jquery/jquery.js HTTP/1.1" 200 285314 +INFO 2025-11-24 14:15:20,778 basehttp 14521 127271990965952 "GET /static/admin/js/filters.js HTTP/1.1" 200 978 +INFO 2025-11-24 14:15:20,780 basehttp 14521 127271982573248 "GET /static/admin/js/vendor/xregexp/xregexp.js HTTP/1.1" 200 325171 +INFO 2025-11-24 14:15:20,859 basehttp 14521 127271982573248 "GET /static/admin/img/tooltag-add.svg HTTP/1.1" 200 331 +INFO 2025-11-24 14:15:20,860 basehttp 14521 127272000407232 "GET /static/admin/img/sorting-icons.svg HTTP/1.1" 200 1097 +INFO 2025-11-24 14:15:23,371 basehttp 14521 127271982573248 "GET /admin/about/aboutmilestone/27/change/ HTTP/1.1" 200 34486 +INFO 2025-11-24 14:15:23,411 basehttp 14521 127271982573248 "GET /static/admin/css/forms.css HTTP/1.1" 200 8525 +INFO 2025-11-24 14:15:23,416 basehttp 14521 127272000407232 "GET /admin/jsi18n/ HTTP/1.1" 200 3342 +INFO 2025-11-24 14:15:23,417 basehttp 14521 127272008799936 "GET /static/admin/js/change_form.js HTTP/1.1" 200 606 +INFO 2025-11-24 14:15:23,418 basehttp 14521 127271990965952 "GET /static/admin/js/prepopulate_init.js HTTP/1.1" 200 586 +INFO 2025-11-24 14:15:23,453 basehttp 14521 127272000407232 "GET /static/admin/img/icon-viewlink.svg HTTP/1.1" 200 581 +INFO 2025-11-24 14:15:23,458 basehttp 14521 127271982573248 "GET /static/admin/css/widgets.css HTTP/1.1" 200 11973 +INFO 2025-11-24 14:15:29,674 basehttp 14521 127271982573248 "GET /admin/about/aboutmilestone/ HTTP/1.1" 200 34312 +INFO 2025-11-24 14:15:33,502 basehttp 14521 127271982573248 "GET /admin/about/aboutmilestone/25/change/ HTTP/1.1" 200 34446 +INFO 2025-11-24 14:15:33,546 basehttp 14521 127271982573248 "GET /admin/jsi18n/ HTTP/1.1" 200 3342 +INFO 2025-11-24 14:15:38,415 basehttp 14521 127271982573248 "GET /admin/about/aboutmilestone/ HTTP/1.1" 200 34312 +INFO 2025-11-24 14:16:01,816 basehttp 14521 127272391534272 "OPTIONS /api/about/page/ HTTP/1.1" 200 0 +INFO 2025-11-24 14:16:01,837 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:16:01,840 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:01,867 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:16:01,879 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:01,879 basehttp 14521 127272374748864 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 14:16:01,917 basehttp 14521 127272391534272 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 14:16:01,929 basehttp 14521 127272374748864 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 14:16:01,939 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:01,957 basehttp 14521 127272391534272 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 14:16:02,019 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:02,026 basehttp 14521 127272399926976 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 14:16:02,034 basehttp 14521 127272374748864 "GET /api/about/page/ HTTP/1.1" 200 8597 +INFO 2025-11-24 14:16:10,126 basehttp 14521 127272399926976 "OPTIONS /api/case-studies/ HTTP/1.1" 200 0 +INFO 2025-11-24 14:16:10,147 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:10,156 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:16:10,160 basehttp 14521 127272374748864 "GET /api/case-studies/ HTTP/1.1" 200 5617 +INFO 2025-11-24 14:16:10,175 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:10,184 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:10,193 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:16:10,193 basehttp 14521 127272374748864 "GET /api/case-studies/ HTTP/1.1" 200 5617 +INFO 2025-11-24 14:16:10,232 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:15,085 basehttp 14521 127272374748864 "OPTIONS /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 0 +INFO 2025-11-24 14:16:15,085 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:15,085 basehttp 14521 127272391534272 "OPTIONS /api/home/banner/ HTTP/1.1" 200 0 +INFO 2025-11-24 14:16:15,085 basehttp 14521 127271972083392 "OPTIONS /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 0 +INFO 2025-11-24 14:16:15,102 basehttp 14521 127272391534272 "GET /api/home/banner/ HTTP/1.1" 200 3438 +INFO 2025-11-24 14:16:15,108 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:16:15,116 basehttp 14521 127271972083392 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:15,121 basehttp 14521 127272383141568 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744 +INFO 2025-11-24 14:16:15,153 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675 +INFO 2025-11-24 14:16:15,156 basehttp 14521 127272374748864 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374 +INFO 2025-11-24 14:16:15,174 basehttp 14521 127272391534272 "GET /api/home/banner/ HTTP/1.1" 200 3438 +INFO 2025-11-24 14:16:15,185 basehttp 14521 127271972083392 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:15,194 basehttp 14521 127272383141568 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744 +INFO 2025-11-24 14:16:15,201 basehttp 14521 127271490811584 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:15,276 basehttp 14521 127271490811584 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:15,294 basehttp 14521 127271972083392 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722 +INFO 2025-11-24 14:16:15,306 basehttp 14521 127272399926976 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374 +INFO 2025-11-24 14:17:22,716 autoreload 14521 127272466882688 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading. +INFO 2025-11-24 14:17:23,283 autoreload 40629 126035405783168 Watching for file changes with StatReloader +INFO 2025-11-24 14:17:24,688 autoreload 40629 126035405783168 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading. +INFO 2025-11-24 14:17:25,211 autoreload 40650 134584020750464 Watching for file changes with StatReloader +INFO 2025-11-24 14:17:52,231 autoreload 40650 134584020750464 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading. +INFO 2025-11-24 14:17:52,734 autoreload 41027 131304750628992 Watching for file changes with StatReloader +INFO 2025-11-24 14:17:55,154 autoreload 41027 131304750628992 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/middleware/admin_ip_restriction.py changed, reloading. +INFO 2025-11-24 14:17:55,632 autoreload 41073 132612486312064 Watching for file changes with StatReloader +INFO 2025-11-24 14:26:08,072 autoreload 41073 132612486312064 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading. +INFO 2025-11-24 14:26:08,639 autoreload 44508 130053635125376 Watching for file changes with StatReloader +INFO 2025-11-24 14:34:01,218 autoreload 47585 126321009672320 Watching for file changes with StatReloader diff --git a/backEnd/nginx.conf.example b/backEnd/nginx.conf.example index 29df60de..cb40142d 100644 --- a/backEnd/nginx.conf.example +++ b/backEnd/nginx.conf.example @@ -2,9 +2,12 @@ # This configuration shows how to set up nginx as a reverse proxy # to secure the Django API backend -# Generate a secure API key for INTERNAL_API_KEY: -# python -c "import secrets; print(secrets.token_urlsafe(32))" -# Add this key to your Django .env file as INTERNAL_API_KEY +# API Key Configuration: +# - In DEBUG mode, Django will auto-generate a secure API key if not set +# - To get the current API key, run: python manage.py show_api_key +# - Add the key to your Django .env file as INTERNAL_API_KEY +# - Use the same key in this nginx config (see line 69) +# - In production, you MUST set INTERNAL_API_KEY explicitly in .env upstream django_backend { # Django backend running on internal network only @@ -66,6 +69,8 @@ server { # Add custom header to prove request came through nginx # This value must match INTERNAL_API_KEY in Django settings + # Get the current key with: python manage.py show_api_key + # In development, Django auto-generates this key if not set set $api_key "YOUR_SECURE_API_KEY_HERE"; proxy_set_header X-Internal-API-Key $api_key; @@ -123,6 +128,7 @@ server { # deny all; # Same proxy settings as /api/ + # Use the same API key as /api/ location above set $api_key "YOUR_SECURE_API_KEY_HERE"; proxy_set_header X-Internal-API-Key $api_key; proxy_set_header Host $host; diff --git a/backEnd/production.env.example b/backEnd/production.env.example index 53e2b216..8b693a4b 100644 --- a/backEnd/production.env.example +++ b/backEnd/production.env.example @@ -44,9 +44,15 @@ CORS_ALLOW_CREDENTIALS=True CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com # API Security - Internal API Key (nginx will add this header) +# 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 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/ diff --git a/backEnd/requirements.txt b/backEnd/requirements.txt new file mode 100644 index 00000000..1e0e31d5 --- /dev/null +++ b/backEnd/requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.11.0 +Django==5.2.8 +django-cors-headers==4.9.0 +django-filter==25.2 +djangorestframework==3.16.1 +drf-yasg==1.21.11 +gunicorn==21.2.0 +inflection==0.5.1 +packaging==25.0 +pillow==12.0.0 +psycopg2-binary==2.9.9 +dj-database-url==2.1.0 +python-decouple==3.8 +pytz==2025.2 +PyYAML==6.0.3 +sqlparse==0.5.3 +uritemplate==4.2.0 diff --git a/create-deployment-zip.sh b/create-deployment-zip.sh new file mode 100644 index 00000000..255fcfc5 --- /dev/null +++ b/create-deployment-zip.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Script to create a production deployment zip file + +set -e + +ZIP_NAME="gnx-web-production-$(date +%Y%m%d).zip" +TEMP_DIR=$(mktemp -d) + +echo "๐Ÿ“ฆ Creating deployment package: $ZIP_NAME" +echo "" + +# Copy files to temp directory +echo "๐Ÿ“‹ Copying files..." +rsync -av --progress \ + --exclude='.git' \ + --exclude='node_modules' \ + --exclude='__pycache__' \ + --exclude='*.pyc' \ + --exclude='venv' \ + --exclude='env' \ + --exclude='.venv' \ + --exclude='*.log' \ + --exclude='*.sqlite3' \ + --exclude='backups' \ + --exclude='*.swp' \ + --exclude='*.swo' \ + --exclude='.DS_Store' \ + --exclude='.vscode' \ + --exclude='.idea' \ + --exclude='.next' \ + --exclude='dist' \ + --exclude='build' \ + --exclude='*.egg-info' \ + --exclude='.dockerignore' \ + --exclude='.zipignore' \ + ./ "$TEMP_DIR/gnx-web/" + +# Create zip +echo "" +echo "๐Ÿ—œ๏ธ Creating zip file..." +cd "$TEMP_DIR" +zip -r "$ZIP_NAME" gnx-web/ > /dev/null + +# Move to original directory +mv "$ZIP_NAME" "$OLDPWD/" + +# Cleanup +cd "$OLDPWD" +rm -rf "$TEMP_DIR" + +echo "โœ… Deployment package created: $ZIP_NAME" +echo "" +echo "๐Ÿ“‹ File size: $(du -h "$ZIP_NAME" | cut -f1)" +echo "" +echo "๐Ÿ“ค Ready to upload to server!" + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..7a41bb9b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,98 @@ +version: '3.8' + +services: + postgres: + image: postgres:16-alpine + container_name: gnx-postgres + restart: unless-stopped + environment: + - POSTGRES_DB=${POSTGRES_DB:-gnxdb} + - POSTGRES_USER=${POSTGRES_USER:-gnx} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-change-this-password} + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - gnx-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-gnx}"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: + context: ./backEnd + dockerfile: Dockerfile + container_name: gnx-backend + restart: unless-stopped + ports: + - "1086:1086" + env_file: + - .env.production + environment: + - DEBUG=False + - SECRET_KEY=${SECRET_KEY:-change-this-in-production} + - ALLOWED_HOSTS=${ALLOWED_HOSTS:-localhost,127.0.0.1,backend} + - DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-gnx}:${POSTGRES_PASSWORD:-change-this-password}@postgres:5432/${POSTGRES_DB:-gnxdb}} + - ADMIN_ALLOWED_IPS=${ADMIN_ALLOWED_IPS:-193.194.155.249} + - INTERNAL_API_KEY=${INTERNAL_API_KEY} + - EMAIL_BACKEND=${EMAIL_BACKEND:-django.core.mail.backends.console.EmailBackend} + - EMAIL_HOST=${EMAIL_HOST} + - EMAIL_PORT=${EMAIL_PORT:-587} + - EMAIL_USE_TLS=${EMAIL_USE_TLS:-True} + - EMAIL_HOST_USER=${EMAIL_HOST_USER} + - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD} + - DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL:-noreply@gnxsoft.com} + - COMPANY_EMAIL=${COMPANY_EMAIL:-contact@gnxsoft.com} + volumes: + - ./backEnd/media:/app/media + - ./backEnd/staticfiles:/app/staticfiles + - ./backEnd/logs:/app/logs + depends_on: + postgres: + condition: service_healthy + networks: + - gnx-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:1086/admin/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + frontend: + build: + context: ./frontEnd + dockerfile: Dockerfile + container_name: gnx-frontend + restart: unless-stopped + ports: + - "1087:1087" + env_file: + - .env.production + environment: + - NODE_ENV=production + - DOCKER_ENV=true + - NEXT_PUBLIC_API_URL=http://backend:1086 + - PORT=1087 + depends_on: + - backend + networks: + - gnx-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:1087/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: + gnx-network: + driver: bridge + +volumes: + postgres_data: + driver: local + media: + staticfiles: + diff --git a/docker-start.sh b/docker-start.sh new file mode 100755 index 00000000..8caedeeb --- /dev/null +++ b/docker-start.sh @@ -0,0 +1,240 @@ +#!/bin/bash +# Docker startup script for GNX Web Application +# This script handles automatic setup, permissions, and startup + +set -e + +echo "๐Ÿš€ Starting GNX Web Application..." +echo "" + +# Set proper permissions for scripts and directories +echo "๐Ÿ”ง Setting up permissions..." + +# Make scripts executable +chmod +x docker-start.sh 2>/dev/null || true +chmod +x migrate-data.sh 2>/dev/null || true +chmod +x migrate-sqlite-to-postgres.sh 2>/dev/null || true + +# Set permissions for directories +mkdir -p backEnd/media backEnd/staticfiles backEnd/logs backups +chmod 755 backEnd/media backEnd/staticfiles backEnd/logs backups 2>/dev/null || true + +# Set permissions for database file if it exists +if [ -f "backEnd/db.sqlite3" ]; then + chmod 644 backEnd/db.sqlite3 2>/dev/null || true +fi + +# Set permissions for .env files +if [ -f ".env.production" ]; then + chmod 600 .env.production 2>/dev/null || true +fi + +echo "โœ… Permissions set" +echo "" + +# Check if .env.production exists +if [ ! -f .env.production ]; then + echo "โš ๏ธ Warning: .env.production not found. Creating from example..." + if [ -f .env.production.example ]; then + cp .env.production.example .env.production + echo "๐Ÿ“ Please edit .env.production with your actual values before continuing." + exit 1 + else + echo "โŒ Error: .env.production.example not found!" + exit 1 + fi +fi + +# Load environment variables +export $(cat .env.production | grep -v '^#' | xargs) + +# Configure Nginx +echo "๐Ÿ”ง Configuring Nginx..." + +# Check for existing nginx configs for gnxsoft +NGINX_AVAILABLE="/etc/nginx/sites-available/gnxsoft" +NGINX_ENABLED="/etc/nginx/sites-enabled/gnxsoft" +NGINX_CONF="nginx.conf" + +# Check if nginx.conf exists +if [ ! -f "$NGINX_CONF" ]; then + echo "โŒ Error: nginx.conf not found in current directory!" + exit 1 +fi + +# Backup and remove old configs if they exist +if [ -f "$NGINX_AVAILABLE" ]; then + echo "๐Ÿ“ฆ Backing up existing nginx config..." + sudo cp "$NGINX_AVAILABLE" "${NGINX_AVAILABLE}.backup.$(date +%Y%m%d_%H%M%S)" + echo "โœ… Old config backed up" +fi + +if [ -L "$NGINX_ENABLED" ]; then + echo "๐Ÿ”— Removing old symlink..." + sudo rm -f "$NGINX_ENABLED" +fi + +# Check for other gnxsoft configs and remove them +for file in /etc/nginx/sites-available/gnxsoft* /etc/nginx/sites-enabled/gnxsoft*; do + if [ -f "$file" ] || [ -L "$file" ]; then + if [ "$file" != "$NGINX_AVAILABLE" ] && [ "$file" != "$NGINX_ENABLED" ]; then + echo "๐Ÿ—‘๏ธ Removing old config: $file" + sudo rm -f "$file" + fi + fi +done + +# Copy new nginx config +echo "๐Ÿ“‹ Installing new nginx configuration..." +sudo cp "$NGINX_CONF" "$NGINX_AVAILABLE" + +# Create symlink +echo "๐Ÿ”— Creating symlink..." +sudo ln -sf "$NGINX_AVAILABLE" "$NGINX_ENABLED" + +# Update paths in nginx config if needed (using current directory) +CURRENT_DIR=$(pwd) +echo "๐Ÿ“ Updating paths in nginx config..." +sudo sed -i "s|/home/gnx/Desktop/GNX-WEB|$CURRENT_DIR|g" "$NGINX_AVAILABLE" + +# Generate or get INTERNAL_API_KEY +if [ -z "$INTERNAL_API_KEY" ] || [ "$INTERNAL_API_KEY" = "your-generated-key-here" ]; then + echo "๐Ÿ”‘ Generating new INTERNAL_API_KEY..." + INTERNAL_API_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))" 2>/dev/null || openssl rand -base64 32 | tr -d "=+/" | cut -c1-32) + + # Update .env.production with the generated key + if [ -f .env.production ]; then + if grep -q "INTERNAL_API_KEY=" .env.production; then + sed -i "s|INTERNAL_API_KEY=.*|INTERNAL_API_KEY=$INTERNAL_API_KEY|" .env.production + else + echo "INTERNAL_API_KEY=$INTERNAL_API_KEY" >> .env.production + fi + echo "โœ… Updated .env.production with generated INTERNAL_API_KEY" + fi + + # Export for use in this script + export INTERNAL_API_KEY +fi + +# Set INTERNAL_API_KEY in nginx config +echo "๐Ÿ”‘ Setting INTERNAL_API_KEY in nginx config..." +sudo sed -i "s|PLACEHOLDER_INTERNAL_API_KEY|$INTERNAL_API_KEY|g" "$NGINX_AVAILABLE" +echo "โœ… INTERNAL_API_KEY configured in nginx" + +# Test nginx configuration +echo "๐Ÿงช Testing nginx configuration..." +if sudo nginx -t; then + echo "โœ… Nginx configuration is valid" + echo "๐Ÿ”„ Reloading nginx..." + sudo systemctl reload nginx + echo "โœ… Nginx reloaded successfully" +else + echo "โŒ Nginx configuration test failed!" + echo "โš ๏ธ Please check the configuration manually" + exit 1 +fi + +# Build images +echo "๐Ÿ”จ Building Docker images..." +docker-compose build + +# Start containers +echo "โ–ถ๏ธ Starting containers..." +docker-compose up -d + +# Wait for services to be ready +echo "โณ Waiting for services to start..." +sleep 10 + +# Wait for PostgreSQL to be ready (if using PostgreSQL) +if echo "$DATABASE_URL" | grep -q "postgresql://"; then + echo "โณ Waiting for PostgreSQL to be ready..." + timeout=30 + while [ $timeout -gt 0 ]; do + if docker-compose exec -T postgres pg_isready -U ${POSTGRES_USER:-gnx} > /dev/null 2>&1; then + echo "โœ… PostgreSQL is ready" + break + fi + echo " Waiting for PostgreSQL... ($timeout seconds remaining)" + sleep 2 + timeout=$((timeout - 2)) + done + if [ $timeout -le 0 ]; then + echo "โš ๏ธ Warning: PostgreSQL may not be ready, but continuing..." + fi + + # Check if we need to migrate from SQLite + if [ -f "./backEnd/db.sqlite3" ] && [ ! -f ".migrated_to_postgres" ]; then + echo "" + echo "๐Ÿ”„ SQLite database detected. Checking if migration is needed..." + + # Check if PostgreSQL database is empty (only has default tables) + POSTGRES_TABLES=$(docker-compose exec -T backend python manage.py shell -c " +from django.db import connection +cursor = connection.cursor() +cursor.execute(\"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name NOT LIKE 'django_%'\") +print(cursor.fetchone()[0]) +" 2>/dev/null | tail -1 || echo "0") + + # Check if SQLite has data + SQLITE_HAS_DATA=$(docker-compose exec -T backend bash -c " +export DATABASE_URL=sqlite:///db.sqlite3 +python manage.py shell -c \" +from django.contrib.auth.models import User +from django.db import connection +cursor = connection.cursor() +cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name NOT LIKE \"sqlite_%\" AND name NOT LIKE \"django_%\"') +tables = cursor.fetchall() +has_data = False +for table in tables: + cursor.execute(f'SELECT COUNT(*) FROM {table[0]}') + if cursor.fetchone()[0] > 0: + has_data = True + break +print('1' if has_data else '0') +\" 2>/dev/null +" | tail -1 || echo "0") + + if [ "$SQLITE_HAS_DATA" = "1" ] && [ "$POSTGRES_TABLES" = "0" ] || [ "$POSTGRES_TABLES" -lt 5 ]; then + echo "๐Ÿ“ฆ SQLite database has data. Starting migration to PostgreSQL..." + echo " This may take a few minutes..." + echo "" + + # Run migration script + if [ -f "./migrate-sqlite-to-postgres.sh" ]; then + ./migrate-sqlite-to-postgres.sh + else + echo "โš ๏ธ Migration script not found. Please run manually:" + echo " ./migrate-sqlite-to-postgres.sh" + fi + else + echo "โœ… No migration needed (PostgreSQL already has data or SQLite is empty)" + touch .migrated_to_postgres + fi + fi +fi + +# Run migrations +echo "๐Ÿ“ฆ Running database migrations..." +docker-compose exec -T backend python manage.py migrate --noinput + +# Collect static files +echo "๐Ÿ“ Collecting static files..." +docker-compose exec -T backend python manage.py collectstatic --noinput + +# Check health +echo "๐Ÿฅ Checking service health..." +docker-compose ps + +echo "" +echo "โœ… GNX Web Application is running!" +echo "" +echo "Backend: http://localhost:1086" +echo "Frontend: http://localhost:1087" +echo "Nginx: Configured and running" +echo "" +echo "View logs: docker-compose logs -f" +echo "Stop services: docker-compose down" +echo "" +echo "๐Ÿ“‹ Nginx config location: $NGINX_AVAILABLE" + diff --git a/frontEnd/.dockerignore b/frontEnd/.dockerignore new file mode 100644 index 00000000..d7763bd9 --- /dev/null +++ b/frontEnd/.dockerignore @@ -0,0 +1,26 @@ +node_modules +.next +.git +.gitignore +*.log +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +.vscode +.idea +*.swp +*.swo +*~ +coverage +.nyc_output +dist +build +README.md +*.md + diff --git a/frontEnd/Dockerfile b/frontEnd/Dockerfile new file mode 100644 index 00000000..4b5a3777 --- /dev/null +++ b/frontEnd/Dockerfile @@ -0,0 +1,50 @@ +# Next.js Frontend Dockerfile +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Copy package files +COPY package*.json ./ +RUN npm ci + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Set environment variables for build +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production + +# Build Next.js +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy necessary files from builder +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 1087 + +ENV PORT=1087 +ENV HOSTNAME="0.0.0.0" + +# Use the standalone server +CMD ["node", "server.js"] + diff --git a/frontEnd/app/career/[slug]/page.tsx b/frontEnd/app/career/[slug]/page.tsx index c21d0c29..81c072fe 100644 --- a/frontEnd/app/career/[slug]/page.tsx +++ b/frontEnd/app/career/[slug]/page.tsx @@ -2,6 +2,7 @@ import { useParams } from "next/navigation"; import { useEffect } from "react"; +import Link from "next/link"; import Header from "@/components/shared/layout/header/Header"; import JobSingle from "@/components/pages/career/JobSingle"; import Footer from "@/components/shared/layout/footer/Footer"; @@ -78,9 +79,9 @@ const JobPage = () => {

The job position you are looking for does not exist or is no longer available.

- + View All Positions - + diff --git a/frontEnd/app/policy/page.tsx b/frontEnd/app/policy/page.tsx index 4fbb2552..b9b1e587 100644 --- a/frontEnd/app/policy/page.tsx +++ b/frontEnd/app/policy/page.tsx @@ -41,7 +41,8 @@ const PolicyContent = () => { url: `/policy?type=${type}`, }); - document.title = metadata.title || `${policyTitles[type]} | GNX Soft`; + const titleString = typeof metadata.title === 'string' ? metadata.title : `${policyTitles[type]} | GNX Soft`; + document.title = titleString; let metaDescription = document.querySelector('meta[name="description"]'); if (!metaDescription) { @@ -49,7 +50,8 @@ const PolicyContent = () => { metaDescription.setAttribute('name', 'description'); document.head.appendChild(metaDescription); } - metaDescription.setAttribute('content', metadata.description || policyDescriptions[type]); + const descriptionString = typeof metadata.description === 'string' ? metadata.description : policyDescriptions[type]; + metaDescription.setAttribute('content', descriptionString); }, [type]); if (isLoading) { @@ -235,7 +237,7 @@ const PolicyContent = () => {

Questions?

-

If you have any questions about this policy, please don't hesitate to contact us.

+

If you have any questions about this policy, please don't hesitate to contact us.

Contact Us
diff --git a/frontEnd/components/pages/blog/BlogSingle.tsx b/frontEnd/components/pages/blog/BlogSingle.tsx index a61fb0b9..93427ca1 100644 --- a/frontEnd/components/pages/blog/BlogSingle.tsx +++ b/frontEnd/components/pages/blog/BlogSingle.tsx @@ -41,7 +41,7 @@ const BlogSingle = () => {

Insight Not Found

- The insight you're looking for doesn't exist or has been removed. + The insight you're looking for doesn't exist or has been removed.

diff --git a/frontEnd/components/pages/career/JobSingle.tsx b/frontEnd/components/pages/career/JobSingle.tsx index 481e860e..d2acc330 100644 --- a/frontEnd/components/pages/career/JobSingle.tsx +++ b/frontEnd/components/pages/career/JobSingle.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import Link from "next/link"; import { JobPosition } from "@/lib/api/careerService"; import JobApplicationForm from "./JobApplicationForm"; @@ -529,7 +530,7 @@ const JobSingle = ({ job }: JobSingleProps) => { ~5 min

- { arrow_back Back to Career Page Back - + diff --git a/frontEnd/components/pages/case-study/CaseSingle.tsx b/frontEnd/components/pages/case-study/CaseSingle.tsx index 3617e1d6..2a7c1fde 100644 --- a/frontEnd/components/pages/case-study/CaseSingle.tsx +++ b/frontEnd/components/pages/case-study/CaseSingle.tsx @@ -67,7 +67,7 @@ const CaseSingle = ({ slug }: CaseSingleProps) => {

Case Study Not Found

-

The case study you're looking for doesn't exist or has been removed.

+

The case study you're looking for doesn't exist or has been removed.

View All Case Studies diff --git a/frontEnd/components/pages/support/CreateTicketForm.tsx b/frontEnd/components/pages/support/CreateTicketForm.tsx index 1d3cbb5f..3db47dc9 100644 --- a/frontEnd/components/pages/support/CreateTicketForm.tsx +++ b/frontEnd/components/pages/support/CreateTicketForm.tsx @@ -203,7 +203,7 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {

- We've received your support request and will respond as soon as possible. + We've received your support request and will respond as soon as possible. Please save your ticket number for future reference.

diff --git a/frontEnd/components/pages/support/KnowledgeBase.tsx b/frontEnd/components/pages/support/KnowledgeBase.tsx index b01d613e..4198db21 100644 --- a/frontEnd/components/pages/support/KnowledgeBase.tsx +++ b/frontEnd/components/pages/support/KnowledgeBase.tsx @@ -236,7 +236,7 @@ const KnowledgeBase = () => {
{searchTerm && (

- Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}" + Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"

)}
diff --git a/frontEnd/components/shared/layout/footer/Footer.tsx b/frontEnd/components/shared/layout/footer/Footer.tsx index c02444c5..3db7dfea 100644 --- a/frontEnd/components/shared/layout/footer/Footer.tsx +++ b/frontEnd/components/shared/layout/footer/Footer.tsx @@ -153,11 +153,56 @@ const Footer = () => {
Ready to Transform Your Business?

Start your software journey with our enterprise solutions, incident management, and custom development services.

+
Start Your Journey
+
+ + GoodFirms Company Profile + +
+
diff --git a/frontEnd/components/shared/layout/header/Header.tsx b/frontEnd/components/shared/layout/header/Header.tsx index 1c705e84..a79e0895 100644 --- a/frontEnd/components/shared/layout/header/Header.tsx +++ b/frontEnd/components/shared/layout/header/Header.tsx @@ -1,5 +1,5 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useRef } from "react"; import { usePathname } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; @@ -12,6 +12,7 @@ const Header = () => { const [isActive, setIsActive] = useState(true); const [openDropdown, setOpenDropdown] = useState(null); const [isMobile, setIsMobile] = useState(false); + const dropdownTimeoutRef = useRef(null); // Fetch services from API const { services: apiServices, loading: servicesLoading, error: servicesError } = useNavigationServices(); @@ -112,6 +113,36 @@ const Header = () => { setOpenDropdown(openDropdown === index ? null : index); }; + const handleDropdownEnter = (index: number) => { + if (dropdownTimeoutRef.current) { + clearTimeout(dropdownTimeoutRef.current); + dropdownTimeoutRef.current = null; + } + if (!isMobile) { + setOpenDropdown(index); + } + }; + + const handleDropdownLeave = (e: React.MouseEvent) => { + if (!isMobile) { + // Check if we're moving to the dropdown menu itself + const relatedTarget = e.relatedTarget as HTMLElement; + const currentTarget = e.currentTarget as HTMLElement; + + // If moving to a child element (dropdown menu), don't close + if (relatedTarget && currentTarget.contains(relatedTarget)) { + return; + } + + if (dropdownTimeoutRef.current) { + clearTimeout(dropdownTimeoutRef.current); + } + dropdownTimeoutRef.current = setTimeout(() => { + setOpenDropdown(null); + }, 300); // Increased delay to allow for scrolling + } + }; + useEffect(() => { const handleScroll = () => { const scrollPosition = window.scrollY; @@ -145,6 +176,9 @@ const Header = () => { return () => { window.removeEventListener("resize", handleResize); + if (dropdownTimeoutRef.current) { + clearTimeout(dropdownTimeoutRef.current); + } }; }, []); @@ -200,12 +234,12 @@ const Header = () => {
    {navigationData.map((item) => - item.title === "Support Center" ? null : item.submenu ? ( + item.title === "Support Center" ? null : item.submenu ? (
  • !isMobile && setOpenDropdown(item.id)} - onMouseLeave={() => !isMobile && setOpenDropdown(null)} + onMouseEnter={() => handleDropdownEnter(item.id)} + onMouseLeave={(e) => handleDropdownLeave(e)} > -
      +
        { + if (dropdownTimeoutRef.current) { + clearTimeout(dropdownTimeoutRef.current); + dropdownTimeoutRef.current = null; + } + if (!isMobile) { + setOpenDropdown(item.id); + } + }} + onMouseLeave={(e) => handleDropdownLeave(e)} + onWheel={(e) => { + // Prevent dropdown from closing when scrolling + e.stopPropagation(); + if (dropdownTimeoutRef.current) { + clearTimeout(dropdownTimeoutRef.current); + dropdownTimeoutRef.current = null; + } + }} + > {item.title === "Services" && servicesLoading ? (
      • Loading services... diff --git a/frontEnd/lib/config/api.ts b/frontEnd/lib/config/api.ts index c4a816d3..d27b5214 100644 --- a/frontEnd/lib/config/api.ts +++ b/frontEnd/lib/config/api.ts @@ -8,10 +8,15 @@ // Production: Use relative URLs (nginx proxy) // Development: Use full backend URL +// Docker: Use backend service name or port 1086 const isProduction = process.env.NODE_ENV === 'production'; -export const API_BASE_URL = isProduction - ? '' // Use relative URLs in production (proxied by nginx) - : (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'); +const isDocker = process.env.DOCKER_ENV === 'true'; + +export const API_BASE_URL = isDocker + ? (process.env.NEXT_PUBLIC_API_URL || 'http://backend:1086') + : isProduction + ? '' // Use relative URLs in production (proxied by nginx) + : (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'); export const API_CONFIG = { // Django API Base URL diff --git a/frontEnd/lib/imageUtils.ts b/frontEnd/lib/imageUtils.ts index fe4afc80..766987a3 100644 --- a/frontEnd/lib/imageUtils.ts +++ b/frontEnd/lib/imageUtils.ts @@ -1,6 +1,5 @@ // Image utility functions - -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; +import { API_BASE_URL } from './config/api'; export const FALLBACK_IMAGES = { BLOG: '/images/blog/blog-poster.png', diff --git a/frontEnd/next.config.js b/frontEnd/next.config.js index 40ceebec..10314235 100644 --- a/frontEnd/next.config.js +++ b/frontEnd/next.config.js @@ -1,5 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + // Enable standalone output for Docker + output: 'standalone', images: { remotePatterns: [ { diff --git a/frontEnd/public/images/gnx-goodfirms.webp b/frontEnd/public/images/gnx-goodfirms.webp new file mode 100644 index 00000000..8de3fdd2 Binary files /dev/null and b/frontEnd/public/images/gnx-goodfirms.webp differ diff --git a/frontEnd/public/images/logo-light.png b/frontEnd/public/images/logo-light.png index 9451c396..42c11248 100644 Binary files a/frontEnd/public/images/logo-light.png and b/frontEnd/public/images/logo-light.png differ diff --git a/frontEnd/public/images/logo.png b/frontEnd/public/images/logo.png index 9451c396..42c11248 100644 Binary files a/frontEnd/public/images/logo.png and b/frontEnd/public/images/logo.png differ diff --git a/frontEnd/public/styles/layout/_footer.scss b/frontEnd/public/styles/layout/_footer.scss index 5c34d9da..03307740 100644 --- a/frontEnd/public/styles/layout/_footer.scss +++ b/frontEnd/public/styles/layout/_footer.scss @@ -747,6 +747,60 @@ justify-content: center; } } + + // GoodFirms Badge + .goodfirms-badge { + border: none !important; + outline: none !important; + box-shadow: none !important; + text-decoration: none !important; + padding: 0 !important; + margin: 0 !important; + display: inline-block !important; + background: transparent !important; + line-height: 0 !important; + + &:focus, + &:focus-visible, + &:active, + &:hover { + border: none !important; + outline: none !important; + box-shadow: none !important; + text-decoration: none !important; + } + + .goodfirms-image, + img { + border: none !important; + outline: none !important; + box-shadow: none !important; + padding: 0 !important; + margin: 0 !important; + display: block !important; + vertical-align: top !important; + max-width: 100% !important; + height: auto !important; + } + + // Target the actual img element that Next.js Image renders + img { + border: 0 !important; + border-style: none !important; + border-width: 0 !important; + border-color: transparent !important; + border-image: none !important; + } + } + + .goodfirms-wrapper { + line-height: 0 !important; + + * { + border: none !important; + outline: none !important; + } + } } /* ==== diff --git a/frontEnd/public/styles/layout/_header.scss b/frontEnd/public/styles/layout/_header.scss index 6205e3b8..8f7aa67e 100644 --- a/frontEnd/public/styles/layout/_header.scss +++ b/frontEnd/public/styles/layout/_header.scss @@ -141,13 +141,16 @@ min-width: 280px !important; max-width: 320px; max-height: 500px; - overflow-y: auto; - overflow-x: hidden; + overflow-y: auto !important; + overflow-x: hidden !important; opacity: 0; visibility: hidden; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 9999 !important; backdrop-filter: blur(25px) saturate(180%); + pointer-events: none; + scroll-behavior: smooth; + -webkit-overflow-scrolling: touch; // Custom scrollbar styles &::-webkit-scrollbar { @@ -174,6 +177,19 @@ visibility: visible !important; transform: translateX(-50%) translateY(0) scale(1) !important; display: block !important; + pointer-events: auto !important; + } + + // Bridge element to cover the gap + &::after { + content: ""; + position: absolute; + top: -12px; + left: 0; + right: 0; + height: 12px; + pointer-events: auto; + z-index: -1; } &::before { @@ -188,6 +204,7 @@ border-right: 8px solid transparent; border-bottom: 8px solid #1a1a1a; filter: drop-shadow(0 -3px 8px rgba(0, 0, 0, 0.5)); + pointer-events: none; } li { diff --git a/migrate-data.sh b/migrate-data.sh new file mode 100755 index 00000000..dc53d0e1 --- /dev/null +++ b/migrate-data.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Simplified script to migrate SQLite data to PostgreSQL + +set -e + +echo "๐Ÿ”„ Migrating data from SQLite to PostgreSQL..." + +# Load environment +if [ -f .env.production ]; then + export $(cat .env.production | grep -v '^#' | xargs) +fi + +# Check if SQLite exists +if [ ! -f "./backEnd/db.sqlite3" ]; then + echo "โŒ SQLite database not found" + exit 1 +fi + +# Ensure containers are running +if ! docker-compose ps | grep -q "backend.*Up"; then + echo "โ–ถ๏ธ Starting containers..." + docker-compose up -d + sleep 10 +fi + +# Wait for PostgreSQL +echo "โณ Waiting for PostgreSQL..." +timeout=30 +while [ $timeout -gt 0 ]; do + if docker-compose exec -T postgres pg_isready -U ${POSTGRES_USER:-gnx} > /dev/null 2>&1; then + break + fi + sleep 2 + timeout=$((timeout - 2)) +done + +# Create backup directory +mkdir -p ./backups +BACKUP_FILE="./backups/sqlite_export_$(date +%Y%m%d_%H%M%S).json" + +echo "๐Ÿ“ฆ Exporting from SQLite..." + +# Export using SQLite database +docker-compose exec -T backend bash -c " + # Temporarily use SQLite + export DATABASE_URL=sqlite:///db.sqlite3 + python manage.py dumpdata --natural-foreign --natural-primary \ + --exclude auth.permission \ + --exclude contenttypes \ + --indent 2 > /tmp/sqlite_export.json 2>&1 + cat /tmp/sqlite_export.json +" > "$BACKUP_FILE" + +echo "โœ… Exported to $BACKUP_FILE" + +# Run migrations on PostgreSQL +echo "๐Ÿ“ฆ Running migrations on PostgreSQL..." +docker-compose exec -T backend python manage.py migrate --noinput + +# Import into PostgreSQL +echo "๐Ÿ“ฅ Importing into PostgreSQL..." +docker-compose exec -T backend bash -c " + python manage.py loaddata /tmp/sqlite_export.json 2>&1 || echo 'Import completed with warnings' +" + +echo "โœ… Migration completed!" +echo "" +echo "๐Ÿ“Š Verifying migration..." + +# Count records +echo " Checking user count..." +USERS=$(docker-compose exec -T backend python manage.py shell -c "from django.contrib.auth.models import User; print(User.objects.count())" 2>/dev/null | tail -1) +echo " Users in PostgreSQL: $USERS" + +touch .migrated_to_postgres +echo "" +echo "โœ… Migration complete! Backend is now using PostgreSQL." + diff --git a/migrate-sqlite-to-postgres.sh b/migrate-sqlite-to-postgres.sh new file mode 100755 index 00000000..4fde73eb --- /dev/null +++ b/migrate-sqlite-to-postgres.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# Script to migrate data from SQLite to PostgreSQL + +set -e + +echo "๐Ÿ”„ Starting SQLite to PostgreSQL Migration..." + +# Check if SQLite database exists +SQLITE_DB="./backEnd/db.sqlite3" +if [ ! -f "$SQLITE_DB" ]; then + echo "โŒ SQLite database not found at $SQLITE_DB" + exit 1 +fi + +echo "โœ… Found SQLite database" + +# Check if PostgreSQL is running +if ! docker-compose ps postgres | grep -q "Up"; then + echo "โŒ PostgreSQL container is not running. Please start it first:" + echo " docker-compose up -d postgres" + exit 1 +fi + +echo "โœ… PostgreSQL container is running" + +# Load environment variables +if [ -f .env.production ]; then + export $(cat .env.production | grep -v '^#' | xargs) +fi + +# Check if DATABASE_URL is set for PostgreSQL +if [ -z "$DATABASE_URL" ] || ! echo "$DATABASE_URL" | grep -q "postgresql://"; then + echo "โŒ DATABASE_URL is not set to PostgreSQL" + echo " Please update .env.production with PostgreSQL DATABASE_URL" + exit 1 +fi + +echo "โœ… PostgreSQL DATABASE_URL is configured" + +# Create backup directory +BACKUP_DIR="./backups" +mkdir -p "$BACKUP_DIR" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/sqlite_backup_$TIMESTAMP.json" + +echo "๐Ÿ“ฆ Exporting data from SQLite..." +echo " Backup will be saved to: $BACKUP_FILE" + +# Export data from SQLite using Django's dumpdata +# First, temporarily switch to SQLite +docker-compose exec -T backend bash -c " + export DATABASE_URL=sqlite:///db.sqlite3 + python manage.py dumpdata --natural-foreign --natural-primary --exclude auth.permission --exclude contenttypes > /tmp/sqlite_export.json 2>&1 || true + cat /tmp/sqlite_export.json +" > "$BACKUP_FILE" + +# Check if export was successful +if [ ! -s "$BACKUP_FILE" ] || grep -q "Error\|Traceback\|Exception" "$BACKUP_FILE"; then + echo "โš ๏ธ Warning: Export may have issues, but continuing..." +fi + +echo "โœ… Data exported to $BACKUP_FILE" +echo " File size: $(du -h "$BACKUP_FILE" | cut -f1)" + +# Wait for PostgreSQL to be ready +echo "โณ Waiting for PostgreSQL to be ready..." +timeout=30 +while [ $timeout -gt 0 ]; do + if docker-compose exec -T postgres pg_isready -U ${POSTGRES_USER:-gnx} > /dev/null 2>&1; then + echo "โœ… PostgreSQL is ready" + break + fi + echo " Waiting for PostgreSQL... ($timeout seconds remaining)" + sleep 2 + timeout=$((timeout - 2)) +done + +if [ $timeout -le 0 ]; then + echo "โŒ PostgreSQL is not ready. Please check the logs:" + echo " docker-compose logs postgres" + exit 1 +fi + +# Create database if it doesn't exist +echo "๐Ÿ“Š Ensuring PostgreSQL database exists..." +docker-compose exec -T postgres psql -U ${POSTGRES_USER:-gnx} -d postgres -c "SELECT 1 FROM pg_database WHERE datname='${POSTGRES_DB:-gnxdb}'" | grep -q 1 || \ +docker-compose exec -T postgres psql -U ${POSTGRES_USER:-gnx} -d postgres -c "CREATE DATABASE ${POSTGRES_DB:-gnxdb};" + +echo "โœ… Database exists or created" + +# Run migrations on PostgreSQL +echo "๐Ÿ“ฆ Running migrations on PostgreSQL..." +docker-compose exec -T backend python manage.py migrate --noinput + +echo "โœ… Migrations completed" + +# Import data into PostgreSQL +echo "๐Ÿ“ฅ Importing data into PostgreSQL..." +if docker-compose exec -T backend bash -c "python manage.py loaddata /tmp/sqlite_export.json" < "$BACKUP_FILE" 2>&1 | tee /tmp/import_log.txt; then + echo "โœ… Data imported successfully" +else + echo "โš ๏ธ Warning: Some data may not have imported. Check the log above." + echo " You can retry the import manually:" + echo " docker-compose exec backend python manage.py loaddata /tmp/sqlite_export.json" +fi + +# Verify data transfer +echo "๐Ÿ” Verifying data transfer..." +SQLITE_COUNT=$(docker-compose exec -T backend bash -c "export DATABASE_URL=sqlite:///db.sqlite3 && python manage.py shell -c \"from django.contrib.auth.models import User; print(User.objects.count())\"" 2>/dev/null | tail -1 || echo "0") +POSTGRES_COUNT=$(docker-compose exec -T backend python manage.py shell -c "from django.contrib.auth.models import User; print(User.objects.count())" 2>/dev/null | tail -1 || echo "0") + +echo "" +echo "๐Ÿ“Š Migration Summary:" +echo " SQLite Users: $SQLITE_COUNT" +echo " PostgreSQL Users: $POSTGRES_COUNT" +echo "" + +# Create a flag file to indicate migration is complete +touch .migrated_to_postgres + +echo "โœ… Migration completed!" +echo "" +echo "๐Ÿ“‹ Next steps:" +echo " 1. Verify the data in PostgreSQL:" +echo " docker-compose exec backend python manage.py shell" +echo "" +echo " 2. Test the application with PostgreSQL" +echo "" +echo " 3. Once verified, you can backup and remove SQLite:" +echo " mv backEnd/db.sqlite3 backEnd/db.sqlite3.backup" +echo "" +echo " Backup file saved at: $BACKUP_FILE" + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..470e4306 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,218 @@ +# Production Nginx Configuration for GNX Soft (Docker) +# This configuration is for nginx running on the host machine +# It proxies to Docker containers: backend (1086) and frontend (1087) +# +# IMPORTANT PORT CONFIGURATION: +# - Backend (Django): Only accessible on port 1086 (internal) +# - Frontend (Next.js): Only accessible on port 1087 (internal) +# - Nginx: Public access on ports 80 (HTTP) and 443 (HTTPS) +# - Ports 1086 and 1087 should be blocked from external access by firewall + +# Frontend - Next.js running in Docker on port 1087 +# All frontend requests (/) are proxied here +upstream frontend { + server 127.0.0.1:1087; + keepalive 64; +} + +# Backend - Django running in Docker on port 1086 +# All API requests (/api/) and admin (/admin/) are proxied here +upstream backend_internal { + server 127.0.0.1:1086; + keepalive 64; +} + +# Redirect HTTP to HTTPS +server { + listen 80; + listen [::]:80; + server_name gnxsoft.com www.gnxsoft.com; + + # Let's Encrypt validation + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect all other traffic to HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} + +# HTTPS Server +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name gnxsoft.com www.gnxsoft.com; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/gnxsoft.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/gnxsoft.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Security Headers + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always; + + # Rate Limiting Zones + limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=general_limit:10m rate=100r/s; + limit_req_status 429; + + # Client settings + client_max_body_size 10M; + client_body_timeout 30s; + client_header_timeout 30s; + + # Logging + access_log /var/log/nginx/gnxsoft_access.log; + error_log /var/log/nginx/gnxsoft_error.log warn; + + # Root location - Frontend (Next.js on port 1087) + location / { + limit_req zone=general_limit burst=50 nodelay; + + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # API Proxy - Frontend talks to backend through this proxy + # Backend runs in Docker on port 1086 (internal only) + location /api/ { + limit_req zone=api_limit burst=20 nodelay; + + # Internal proxy to backend Docker container (127.0.0.1:1086) + proxy_pass http://backend_internal/api/; + proxy_http_version 1.1; + + # Backend sees request as coming from nginx + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # Add internal API key (will be replaced by docker-start.sh) + set $api_key "PLACEHOLDER_INTERNAL_API_KEY"; + proxy_set_header X-Internal-API-Key $api_key; + + # Hide backend server info + proxy_hide_header X-Powered-By; + proxy_hide_header Server; + + # Timeouts + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + + # CORS headers (if needed) + add_header Access-Control-Allow-Origin "https://gnxsoft.com" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Internal-API-Key" always; + add_header Access-Control-Allow-Credentials "true" always; + + # Handle preflight requests + if ($request_method = 'OPTIONS') { + return 204; + } + } + + # Media files - Served from Docker volume + location /media/ { + alias /home/gnx/Desktop/GNX-WEB/backEnd/media/; + expires 30d; + add_header Cache-Control "public, immutable"; + access_log off; + + # Security - deny execution of scripts + location ~ \.(php|py|pl|sh)$ { + deny all; + } + } + + # Static files - Served from Docker volume + location /static/ { + alias /home/gnx/Desktop/GNX-WEB/backEnd/staticfiles/; + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Next.js static files + location /_next/static/ { + proxy_pass http://frontend; + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Admin panel - Proxy to backend (with IP restriction) + location /admin/ { + # IP restriction is handled by Django middleware + # Add internal API key (will be replaced by docker-start.sh) + set $api_key "PLACEHOLDER_INTERNAL_API_KEY"; + proxy_set_header X-Internal-API-Key $api_key; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://backend_internal; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Connection ""; + + # Timeouts + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "OK\n"; + add_header Content-Type text/plain; + } +} + +# ============================================================================== +# IMPORTANT NOTES: +# ============================================================================== +# 1. Backend runs in Docker on port 1086 (internal only) +# 2. Frontend runs in Docker on port 1087 +# 3. Nginx runs on host and proxies to Docker containers +# 4. Firewall should BLOCK external access to ports 1086 and 1087 +# 5. Only nginx (ports 80, 443) should be accessible from internet +# 6. Set INTERNAL_API_KEY environment variable in nginx config or systemd service +# 7. Update media/static paths to match your actual deployment location +# ============================================================================== + diff --git a/setup.sh b/setup.sh new file mode 100755 index 00000000..ba5f65bd --- /dev/null +++ b/setup.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Initial setup script - Run this once after extracting the zip file + +set -e + +echo "๐Ÿ”ง GNX Web Application - Initial Setup" +echo "======================================" +echo "" + +# Set all necessary permissions +echo "๐Ÿ“‹ Setting up file permissions..." + +# Make all scripts executable +find . -name "*.sh" -type f -exec chmod +x {} \; 2>/dev/null || true + +# Set directory permissions +mkdir -p backEnd/media backEnd/staticfiles backEnd/logs backups +chmod 755 backEnd/media backEnd/staticfiles backEnd/logs backups 2>/dev/null || true + +# Set file permissions +if [ -f "backEnd/db.sqlite3" ]; then + chmod 644 backEnd/db.sqlite3 2>/dev/null || true +fi + +if [ -f ".env.production" ]; then + chmod 600 .env.production 2>/dev/null || true +fi + +# Ensure docker-start.sh is executable +chmod +x docker-start.sh 2>/dev/null || true + +echo "โœ… Permissions configured" +echo "" + +# Check for required files +echo "๐Ÿ“‹ Checking required files..." + +REQUIRED_FILES=( + "docker-compose.yml" + "nginx.conf" + ".env.production" + "backEnd/Dockerfile" + "frontEnd/Dockerfile" +) + +MISSING_FILES=() +for file in "${REQUIRED_FILES[@]}"; do + if [ ! -f "$file" ]; then + MISSING_FILES+=("$file") + fi +done + +if [ ${#MISSING_FILES[@]} -gt 0 ]; then + echo "โŒ Missing required files:" + for file in "${MISSING_FILES[@]}"; do + echo " - $file" + done + exit 1 +fi + +echo "โœ… All required files present" +echo "" + +# Check Docker +if ! command -v docker &> /dev/null; then + echo "โŒ Docker is not installed. Please install Docker first." + exit 1 +fi + +if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then + echo "โŒ Docker Compose is not installed. Please install Docker Compose first." + exit 1 +fi + +echo "โœ… Docker is installed" +echo "" + +echo "โœ… Setup complete!" +echo "" +echo "๐Ÿ“‹ Next steps:" +echo " 1. Review and update .env.production with your settings" +echo " 2. Run: ./docker-start.sh" +echo "" +