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( + "
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( + "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 = () => {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- 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) => {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.
- Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}" + Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"
)}Start your software journey with our enterprise solutions, incident management, and custom development services.
+