updates
This commit is contained in:
47
.env.production
Normal file
47
.env.production
Normal 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
65
.zipignore
Normal 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
39
backEnd/.dockerignore
Normal 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
|
||||||
|
|
||||||
@@ -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
36
backEnd/Dockerfile
Normal 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.
Binary file not shown.
2
backEnd/gnx/management/__init__.py
Normal file
2
backEnd/gnx/management/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Django management package
|
||||||
|
|
||||||
2
backEnd/gnx/management/commands/__init__.py
Normal file
2
backEnd/gnx/management/commands/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Django management commands package
|
||||||
|
|
||||||
Binary file not shown.
34
backEnd/gnx/management/commands/show_api_key.py
Normal file
34
backEnd/gnx/management/commands/show_api_key.py
Normal 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('')
|
||||||
|
|
||||||
99
backEnd/gnx/management/commands/update_admin.py
Normal file
99
backEnd/gnx/management/commands/update_admin.py
Normal 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
|
||||||
|
|
||||||
Binary file not shown.
132
backEnd/gnx/middleware/admin_ip_restriction.py
Normal file
132
backEnd/gnx/middleware/admin_ip_restriction.py
Normal 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)
|
||||||
|
|
||||||
@@ -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/')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
17
backEnd/requirements.txt
Normal 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
56
create-deployment-zip.sh
Normal 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
98
docker-compose.yml
Normal 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
240
docker-start.sh
Executable 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
26
frontEnd/.dockerignore
Normal 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
50
frontEnd/Dockerfile
Normal 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"]
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'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>
|
||||||
|
|||||||
@@ -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're looking for doesn'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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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're looking for doesn'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>
|
||||||
|
|||||||
@@ -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'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">
|
||||||
|
|||||||
@@ -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 "{searchTerm}"
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
BIN
frontEnd/public/images/gnx-goodfirms.webp
Normal file
BIN
frontEnd/public/images/gnx-goodfirms.webp
Normal file
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 |
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ====
|
/* ====
|
||||||
|
|||||||
@@ -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
78
migrate-data.sh
Executable 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
133
migrate-sqlite-to-postgres.sh
Executable 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
218
nginx.conf
Normal 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
84
setup.sh
Executable 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 ""
|
||||||
|
|
||||||
Reference in New Issue
Block a user