This commit is contained in:
Iliyan Angelov
2025-11-24 16:47:37 +02:00
parent d7ff5c71e6
commit 0b1cabcfaf
45 changed files with 2021 additions and 28 deletions

47
.env.production Normal file
View File

@@ -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

65
.zipignore Normal file
View File

@@ -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/

39
backEnd/.dockerignore Normal file
View File

@@ -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

View File

@@ -4,6 +4,10 @@ SECRET_KEY=ks68*5@of1l&4rn1imsqdk9$khcya!&a#jtd89f!v^qg1w0&hc
DEBUG=True DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1 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) # Email Configuration (Development - uses console backend by default)
USE_SMTP_IN_DEV=True USE_SMTP_IN_DEV=True
DEFAULT_FROM_EMAIL=support@gnxsoft.com DEFAULT_FROM_EMAIL=support@gnxsoft.com

36
backEnd/Dockerfile Normal file
View File

@@ -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"]

Binary file not shown.

View File

@@ -0,0 +1,2 @@
# Django management package

View File

@@ -0,0 +1,2 @@
# Django management commands package

View File

@@ -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('')

View File

@@ -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

View File

@@ -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(
"<h1>Access Denied</h1>"
"<p>Unable to verify your IP address. Admin access is restricted.</p>"
)
# 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(
"<h1>Access Denied</h1>"
"<p>Admin access is restricted to authorized IP addresses only.</p>"
f"<p>Your IP: {client_ip}</p>"
)
# IP is allowed, continue
return self.get_response(request)

View File

@@ -12,6 +12,8 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
from pathlib import Path from pathlib import Path
import os import os
import secrets
import warnings
from decouple import config from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -62,6 +64,7 @@ MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', 'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'gnx.middleware.ip_whitelist.IPWhitelistMiddleware', # Production: Block external access '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 'gnx.middleware.api_security.FrontendAPIProxyMiddleware', # Validate requests from frontend/nginx
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
@@ -95,6 +98,16 @@ WSGI_APPLICATION = 'gnx.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases # 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 = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', '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 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()]) 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 # Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/ # 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 # 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))" # Auto-generates a secure key if not provided (only in DEBUG mode for development)
INTERNAL_API_KEY = config('INTERNAL_API_KEY', default='' if not DEBUG else 'dev-key-change-in-production') # 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 (default: all /api/ paths)
API_SECURITY_PATH_PATTERN = config('API_SECURITY_PATH_PATTERN', default=r'^/api/') API_SECURITY_PATH_PATTERN = config('API_SECURITY_PATH_PATTERN', default=r'^/api/')

View File

@@ -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: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,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 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

View File

@@ -2,9 +2,12 @@
# This configuration shows how to set up nginx as a reverse proxy # This configuration shows how to set up nginx as a reverse proxy
# to secure the Django API backend # to secure the Django API backend
# Generate a secure API key for INTERNAL_API_KEY: # API Key Configuration:
# python -c "import secrets; print(secrets.token_urlsafe(32))" # - In DEBUG mode, Django will auto-generate a secure API key if not set
# Add this key to your Django .env file as INTERNAL_API_KEY # - 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 { upstream django_backend {
# Django backend running on internal network only # Django backend running on internal network only
@@ -66,6 +69,8 @@ server {
# Add custom header to prove request came through nginx # Add custom header to prove request came through nginx
# This value must match INTERNAL_API_KEY in Django settings # 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"; set $api_key "YOUR_SECURE_API_KEY_HERE";
proxy_set_header X-Internal-API-Key $api_key; proxy_set_header X-Internal-API-Key $api_key;
@@ -123,6 +128,7 @@ server {
# deny all; # deny all;
# Same proxy settings as /api/ # Same proxy settings as /api/
# Use the same API key as /api/ location above
set $api_key "YOUR_SECURE_API_KEY_HERE"; set $api_key "YOUR_SECURE_API_KEY_HERE";
proxy_set_header X-Internal-API-Key $api_key; proxy_set_header X-Internal-API-Key $api_key;
proxy_set_header Host $host; proxy_set_header Host $host;

View File

@@ -44,9 +44,15 @@ CORS_ALLOW_CREDENTIALS=True
CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
# API Security - Internal API Key (nginx will add this header) # 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))" # 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 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 Files
STATIC_ROOT=/var/www/gnx/staticfiles/ STATIC_ROOT=/var/www/gnx/staticfiles/
MEDIA_ROOT=/var/www/gnx/media/ MEDIA_ROOT=/var/www/gnx/media/

17
backEnd/requirements.txt Normal file
View File

@@ -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

56
create-deployment-zip.sh Normal file
View File

@@ -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!"

98
docker-compose.yml Normal file
View File

@@ -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:

240
docker-start.sh Executable file
View File

@@ -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"

26
frontEnd/.dockerignore Normal file
View File

@@ -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

50
frontEnd/Dockerfile Normal file
View File

@@ -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"]

View File

@@ -2,6 +2,7 @@
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import Link from "next/link";
import Header from "@/components/shared/layout/header/Header"; import Header from "@/components/shared/layout/header/Header";
import JobSingle from "@/components/pages/career/JobSingle"; import JobSingle from "@/components/pages/career/JobSingle";
import Footer from "@/components/shared/layout/footer/Footer"; import Footer from "@/components/shared/layout/footer/Footer";
@@ -78,9 +79,9 @@ const JobPage = () => {
<p className="mt-24"> <p className="mt-24">
The job position you are looking for does not exist or is no longer available. The job position you are looking for does not exist or is no longer available.
</p> </p>
<a href="/career" className="btn mt-40"> <Link href="/career" className="btn mt-40">
View All Positions View All Positions
</a> </Link>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -41,7 +41,8 @@ const PolicyContent = () => {
url: `/policy?type=${type}`, 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"]'); let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) { if (!metaDescription) {
@@ -49,7 +50,8 @@ const PolicyContent = () => {
metaDescription.setAttribute('name', 'description'); metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription); 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]); }, [type]);
if (isLoading) { if (isLoading) {
@@ -235,7 +237,7 @@ const PolicyContent = () => {
<div className="policy-footer"> <div className="policy-footer">
<div className="contact-box"> <div className="contact-box">
<h3>Questions?</h3> <h3>Questions?</h3>
<p>If you have any questions about this policy, please don't hesitate to contact us.</p> <p>If you have any questions about this policy, please don&apos;t hesitate to contact us.</p>
<a href="/contact-us" className="btn btn-primary">Contact Us</a> <a href="/contact-us" className="btn btn-primary">Contact Us</a>
</div> </div>
</div> </div>

View File

@@ -41,7 +41,7 @@ const BlogSingle = () => {
</div> </div>
<h2 className="text-secondary mb-3">Insight Not Found</h2> <h2 className="text-secondary mb-3">Insight Not Found</h2>
<p className="text-tertiary mb-4"> <p className="text-tertiary mb-4">
The insight you're looking for doesn't exist or has been removed. The insight you&apos;re looking for doesn&apos;t exist or has been removed.
</p> </p>
<Link href="/insights" className="btn btn-primary"> <Link href="/insights" className="btn btn-primary">
<i className="fa-solid fa-arrow-left me-2"></i> <i className="fa-solid fa-arrow-left me-2"></i>

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Link from "next/link";
import { JobPosition } from "@/lib/api/careerService"; import { JobPosition } from "@/lib/api/careerService";
import JobApplicationForm from "./JobApplicationForm"; import JobApplicationForm from "./JobApplicationForm";
@@ -529,7 +530,7 @@ const JobSingle = ({ job }: JobSingleProps) => {
<span className="d-sm-none">~5 min</span> <span className="d-sm-none">~5 min</span>
</p> </p>
<a <Link
href="/career" href="/career"
className="btn w-100 mt-12 mt-md-16" className="btn w-100 mt-12 mt-md-16"
style={{ style={{
@@ -560,7 +561,7 @@ const JobSingle = ({ job }: JobSingleProps) => {
<span className="material-symbols-outlined" style={{ fontSize: 'clamp(16px, 3vw, 18px)' }}>arrow_back</span> <span className="material-symbols-outlined" style={{ fontSize: 'clamp(16px, 3vw, 18px)' }}>arrow_back</span>
<span className="d-none d-sm-inline">Back to Career Page</span> <span className="d-none d-sm-inline">Back to Career Page</span>
<span className="d-sm-none">Back</span> <span className="d-sm-none">Back</span>
</a> </Link>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -67,7 +67,7 @@ const CaseSingle = ({ slug }: CaseSingleProps) => {
<div className="col-12"> <div className="col-12">
<div className="error-state"> <div className="error-state">
<h2>Case Study Not Found</h2> <h2>Case Study Not Found</h2>
<p>The case study you're looking for doesn't exist or has been removed.</p> <p>The case study you&apos;re looking for doesn&apos;t exist or has been removed.</p>
<Link href="/case-study" className="btn btn-primary"> <Link href="/case-study" className="btn btn-primary">
View All Case Studies View All Case Studies
</Link> </Link>

View File

@@ -203,7 +203,7 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
</button> </button>
</div> </div>
<p className="ticket-info"> <p className="ticket-info">
We've received your support request and will respond as soon as possible. We&apos;ve received your support request and will respond as soon as possible.
Please save your ticket number for future reference. Please save your ticket number for future reference.
</p> </p>
<div className="success-actions"> <div className="success-actions">

View File

@@ -236,7 +236,7 @@ const KnowledgeBase = () => {
</div> </div>
{searchTerm && ( {searchTerm && (
<p className="search-info"> <p className="search-info">
Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}" Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for &quot;{searchTerm}&quot;
</p> </p>
)} )}
</div> </div>

View File

@@ -153,11 +153,56 @@ const Footer = () => {
</div> </div>
<h6 className="cta-title">Ready to Transform Your Business?</h6> <h6 className="cta-title">Ready to Transform Your Business?</h6>
<p className="cta-description">Start your software journey with our enterprise solutions, incident management, and custom development services.</p> <p className="cta-description">Start your software journey with our enterprise solutions, incident management, and custom development services.</p>
<div className="cta-button-wrapper">
<Link href="/contact-us" className="btn-luxury-cta"> <Link href="/contact-us" className="btn-luxury-cta">
<span>Start Your Journey</span> <span>Start Your Journey</span>
<i className="fa-solid fa-arrow-right"></i> <i className="fa-solid fa-arrow-right"></i>
<div className="btn-shine"></div> <div className="btn-shine"></div>
</Link> </Link>
<div className="goodfirms-wrapper text-center mt-3" style={{ lineHeight: 0 }}>
<Link
href="https://www.goodfirms.co/company/gnx-soft-ltd"
target="_blank"
rel="noopener noreferrer"
className="goodfirms-badge d-inline-block"
title="View our company profile on GoodFirms"
style={{
border: 'none',
outline: 'none',
boxShadow: 'none',
textDecoration: 'none',
padding: 0,
margin: 0,
display: 'inline-block',
lineHeight: 0,
background: 'transparent'
}}
>
<Image
src="/images/gnx-goodfirms.webp"
alt="GoodFirms Company Profile"
width={150}
height={80}
className="goodfirms-image"
style={{
width: '20px',
height: '20px',
objectFit: 'contain',
border: 'none',
outline: 'none',
boxShadow: 'none',
padding: 0,
margin: 0,
display: 'block',
verticalAlign: 'top',
borderWidth: 0,
borderStyle: 'none',
borderColor: 'transparent'
}}
/>
</Link>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo, useRef } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
@@ -12,6 +12,7 @@ const Header = () => {
const [isActive, setIsActive] = useState(true); const [isActive, setIsActive] = useState(true);
const [openDropdown, setOpenDropdown] = useState<number | null>(null); const [openDropdown, setOpenDropdown] = useState<number | null>(null);
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const dropdownTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Fetch services from API // Fetch services from API
const { services: apiServices, loading: servicesLoading, error: servicesError } = useNavigationServices(); const { services: apiServices, loading: servicesLoading, error: servicesError } = useNavigationServices();
@@ -112,6 +113,36 @@ const Header = () => {
setOpenDropdown(openDropdown === index ? null : index); 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(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
const scrollPosition = window.scrollY; const scrollPosition = window.scrollY;
@@ -145,6 +176,9 @@ const Header = () => {
return () => { return () => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
if (dropdownTimeoutRef.current) {
clearTimeout(dropdownTimeoutRef.current);
}
}; };
}, []); }, []);
@@ -204,8 +238,8 @@ const Header = () => {
<li <li
className="navbar__item navbar__item--has-children" className="navbar__item navbar__item--has-children"
key={item.id} key={item.id}
onMouseEnter={() => !isMobile && setOpenDropdown(item.id)} onMouseEnter={() => handleDropdownEnter(item.id)}
onMouseLeave={() => !isMobile && setOpenDropdown(null)} onMouseLeave={(e) => handleDropdownLeave(e)}
> >
<button <button
aria-label="dropdown menu" aria-label="dropdown menu"
@@ -222,7 +256,27 @@ const Header = () => {
<span className="loading-indicator"></span> <span className="loading-indicator"></span>
)} )}
</button> </button>
<ul className={`navbar__sub-menu ${openDropdown === item.id ? 'show' : ''}`}> <ul
className={`navbar__sub-menu ${openDropdown === item.id ? 'show' : ''}`}
onMouseEnter={() => {
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 ? ( {item.title === "Services" && servicesLoading ? (
<li> <li>
<span className="text-muted">Loading services...</span> <span className="text-muted">Loading services...</span>

View File

@@ -8,8 +8,13 @@
// Production: Use relative URLs (nginx proxy) // Production: Use relative URLs (nginx proxy)
// Development: Use full backend URL // Development: Use full backend URL
// Docker: Use backend service name or port 1086
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
export const API_BASE_URL = isProduction 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) ? '' // Use relative URLs in production (proxied by nginx)
: (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'); : (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000');

View File

@@ -1,6 +1,5 @@
// Image utility functions // Image utility functions
import { API_BASE_URL } from './config/api';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
export const FALLBACK_IMAGES = { export const FALLBACK_IMAGES = {
BLOG: '/images/blog/blog-poster.png', BLOG: '/images/blog/blog-poster.png',

View File

@@ -1,5 +1,7 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
// Enable standalone output for Docker
output: 'standalone',
images: { images: {
remotePatterns: [ remotePatterns: [
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -747,6 +747,60 @@
justify-content: center; 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;
}
}
} }
/* ==== /* ====

View File

@@ -141,13 +141,16 @@
min-width: 280px !important; min-width: 280px !important;
max-width: 320px; max-width: 320px;
max-height: 500px; max-height: 500px;
overflow-y: auto; overflow-y: auto !important;
overflow-x: hidden; overflow-x: hidden !important;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 9999 !important; z-index: 9999 !important;
backdrop-filter: blur(25px) saturate(180%); backdrop-filter: blur(25px) saturate(180%);
pointer-events: none;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
// Custom scrollbar styles // Custom scrollbar styles
&::-webkit-scrollbar { &::-webkit-scrollbar {
@@ -174,6 +177,19 @@
visibility: visible !important; visibility: visible !important;
transform: translateX(-50%) translateY(0) scale(1) !important; transform: translateX(-50%) translateY(0) scale(1) !important;
display: block !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 { &::before {
@@ -188,6 +204,7 @@
border-right: 8px solid transparent; border-right: 8px solid transparent;
border-bottom: 8px solid #1a1a1a; border-bottom: 8px solid #1a1a1a;
filter: drop-shadow(0 -3px 8px rgba(0, 0, 0, 0.5)); filter: drop-shadow(0 -3px 8px rgba(0, 0, 0, 0.5));
pointer-events: none;
} }
li { li {

78
migrate-data.sh Executable file
View File

@@ -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."

133
migrate-sqlite-to-postgres.sh Executable file
View File

@@ -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"

218
nginx.conf Normal file
View File

@@ -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
# ==============================================================================

84
setup.sh Executable file
View File

@@ -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 ""