updates
This commit is contained in:
Binary file not shown.
BIN
backEnd/gnx/middleware/__pycache__/api_security.cpython-312.pyc
Normal file
BIN
backEnd/gnx/middleware/__pycache__/api_security.cpython-312.pyc
Normal file
Binary file not shown.
145
backEnd/gnx/middleware/api_security.py
Normal file
145
backEnd/gnx/middleware/api_security.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
API Security Middleware
|
||||
Validates that API requests come from the frontend through nginx reverse proxy
|
||||
This ensures the API is only accessible from the frontend, not directly from the internet
|
||||
"""
|
||||
|
||||
from django.http import HttpResponseForbidden, JsonResponse
|
||||
from django.conf import settings
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
import logging
|
||||
import re
|
||||
|
||||
logger = logging.getLogger('django.security')
|
||||
|
||||
|
||||
class FrontendAPIProxyMiddleware(MiddlewareMixin):
|
||||
"""
|
||||
Enterprise Security: Only allow API requests from frontend through nginx
|
||||
|
||||
This middleware validates:
|
||||
1. Custom header (X-Internal-API-Key) that nginx adds to prove request came through proxy
|
||||
2. Origin/Referer header matches allowed frontend domains
|
||||
3. Request comes from internal network (nginx proxy)
|
||||
|
||||
In production, this ensures the API cannot be accessed directly from the internet.
|
||||
All requests must come through the frontend (nginx reverse proxy).
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
super().__init__(get_response)
|
||||
|
||||
# Get API key from settings (nginx will add this header)
|
||||
self.required_api_key = getattr(settings, 'INTERNAL_API_KEY', None)
|
||||
|
||||
# Get allowed frontend origins
|
||||
self.allowed_origins = getattr(settings, 'CORS_ALLOWED_ORIGINS', [])
|
||||
|
||||
# Get allowed referer patterns
|
||||
self.allowed_referer_patterns = getattr(settings, 'ALLOWED_REFERER_PATTERNS', [])
|
||||
|
||||
# API paths that require validation (default: all /api/ paths)
|
||||
self.api_path_pattern = getattr(settings, 'API_SECURITY_PATH_PATTERN', r'^/api/')
|
||||
|
||||
# Skip validation in DEBUG mode for development
|
||||
self.enforce_in_debug = getattr(settings, 'ENFORCE_API_SECURITY_IN_DEBUG', False)
|
||||
|
||||
def process_request(self, request):
|
||||
"""
|
||||
Validate API requests come from frontend through nginx
|
||||
"""
|
||||
# Skip validation for non-API paths
|
||||
if not re.match(self.api_path_pattern, request.path):
|
||||
return None
|
||||
|
||||
# Skip validation in DEBUG mode unless explicitly enabled
|
||||
if settings.DEBUG and not self.enforce_in_debug:
|
||||
return None
|
||||
|
||||
# Skip validation for OPTIONS requests (CORS preflight)
|
||||
if request.method == 'OPTIONS':
|
||||
return None
|
||||
|
||||
# Get the internal API key from header (added by nginx)
|
||||
internal_api_key = request.META.get('HTTP_X_INTERNAL_API_KEY', '')
|
||||
|
||||
# Validate API key if configured
|
||||
if self.required_api_key:
|
||||
if internal_api_key != self.required_api_key:
|
||||
logger.warning(
|
||||
f"Blocked API request: Missing or invalid X-Internal-API-Key header. "
|
||||
f"Path: {request.path}, IP: {self._get_client_ip(request)}"
|
||||
)
|
||||
return JsonResponse(
|
||||
{
|
||||
'error': 'Access Denied',
|
||||
'message': 'This API is only accessible through the frontend application.',
|
||||
'code': 'API_ACCESS_DENIED'
|
||||
},
|
||||
status=403
|
||||
)
|
||||
|
||||
# Validate Origin header (for CORS requests)
|
||||
origin = request.META.get('HTTP_ORIGIN', '')
|
||||
if origin:
|
||||
if origin not in self.allowed_origins:
|
||||
logger.warning(
|
||||
f"Blocked API request: Invalid Origin header. "
|
||||
f"Origin: {origin}, Path: {request.path}, IP: {self._get_client_ip(request)}"
|
||||
)
|
||||
return JsonResponse(
|
||||
{
|
||||
'error': 'Access Denied',
|
||||
'message': 'Invalid origin. This API is only accessible from authorized domains.',
|
||||
'code': 'INVALID_ORIGIN'
|
||||
},
|
||||
status=403
|
||||
)
|
||||
|
||||
# Validate Referer header (for same-origin requests)
|
||||
referer = request.META.get('HTTP_REFERER', '')
|
||||
if referer and not origin: # Only check referer if no origin header
|
||||
is_valid_referer = False
|
||||
|
||||
# Check against allowed origins
|
||||
for allowed_origin in self.allowed_origins:
|
||||
if referer.startswith(allowed_origin):
|
||||
is_valid_referer = True
|
||||
break
|
||||
|
||||
# Check against referer patterns
|
||||
if not is_valid_referer and self.allowed_referer_patterns:
|
||||
for pattern in self.allowed_referer_patterns:
|
||||
if re.match(pattern, referer):
|
||||
is_valid_referer = True
|
||||
break
|
||||
|
||||
if not is_valid_referer:
|
||||
logger.warning(
|
||||
f"Blocked API request: Invalid Referer header. "
|
||||
f"Referer: {referer}, Path: {request.path}, IP: {self._get_client_ip(request)}"
|
||||
)
|
||||
return JsonResponse(
|
||||
{
|
||||
'error': 'Access Denied',
|
||||
'message': 'Invalid referer. This API is only accessible from authorized domains.',
|
||||
'code': 'INVALID_REFERER'
|
||||
},
|
||||
status=403
|
||||
)
|
||||
|
||||
# Additional validation: Check if request comes from nginx (internal network)
|
||||
# This is a secondary check - the IP whitelist middleware should handle this,
|
||||
# but we can add additional validation here if needed
|
||||
|
||||
# Request is valid, continue processing
|
||||
return None
|
||||
|
||||
def _get_client_ip(self, request):
|
||||
"""Get client IP address from request"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
return x_forwarded_for.split(',')[0].strip()
|
||||
return request.META.get('REMOTE_ADDR', 'unknown')
|
||||
|
||||
@@ -62,6 +62,7 @@ MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'gnx.middleware.ip_whitelist.IPWhitelistMiddleware', # Production: Block external access
|
||||
'gnx.middleware.api_security.FrontendAPIProxyMiddleware', # Validate requests from frontend/nginx
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
@@ -151,7 +152,7 @@ CSRF_COOKIE_HTTPONLY = True
|
||||
CSRF_COOKIE_SAMESITE = 'Strict'
|
||||
CSRF_TRUSTED_ORIGINS = config(
|
||||
'CSRF_TRUSTED_ORIGINS',
|
||||
default='https://gnxsoft.com',
|
||||
default='https://gnxsoft.com,https://www.gnxsoft.com',
|
||||
cast=lambda v: [s.strip() for s in v.split(',')]
|
||||
)
|
||||
|
||||
@@ -229,7 +230,31 @@ REST_FRAMEWORK = {
|
||||
},
|
||||
}
|
||||
|
||||
# CORS Configuration
|
||||
# ============================================================================
|
||||
# API SECURITY CONFIGURATION
|
||||
# API is only accessible from frontend through nginx reverse 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))"
|
||||
INTERNAL_API_KEY = config('INTERNAL_API_KEY', default='' if not DEBUG else 'dev-key-change-in-production')
|
||||
|
||||
# API security path pattern (default: all /api/ paths)
|
||||
API_SECURITY_PATH_PATTERN = config('API_SECURITY_PATH_PATTERN', default=r'^/api/')
|
||||
|
||||
# Enforce API security even in DEBUG mode (for testing)
|
||||
ENFORCE_API_SECURITY_IN_DEBUG = config('ENFORCE_API_SECURITY_IN_DEBUG', default=False, cast=bool)
|
||||
|
||||
# Allowed referer patterns (regex patterns for referer validation)
|
||||
ALLOWED_REFERER_PATTERNS = config(
|
||||
'ALLOWED_REFERER_PATTERNS',
|
||||
default='',
|
||||
cast=lambda v: [s.strip() for s in v.split(',') if s.strip()]
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# CORS Configuration - Strict origin validation
|
||||
# ============================================================================
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000", # React development server
|
||||
"http://127.0.0.1:3000",
|
||||
@@ -237,14 +262,43 @@ CORS_ALLOWED_ORIGINS = [
|
||||
"http://127.0.0.1:3001",
|
||||
]
|
||||
|
||||
# Add production origins if configured
|
||||
PRODUCTION_ORIGINS = config('PRODUCTION_ORIGINS', default='', cast=lambda v: [s.strip() for s in v.split(',') if s.strip()])
|
||||
# Add production origins if configured (defaults to gnxsoft.com domains)
|
||||
PRODUCTION_ORIGINS = config(
|
||||
'PRODUCTION_ORIGINS',
|
||||
default='https://gnxsoft.com,https://www.gnxsoft.com',
|
||||
cast=lambda v: [s.strip() for s in v.split(',') if s.strip()]
|
||||
)
|
||||
if PRODUCTION_ORIGINS:
|
||||
CORS_ALLOWED_ORIGINS.extend(PRODUCTION_ORIGINS)
|
||||
|
||||
# Strict CORS configuration - only allow configured origins
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_ALL_ORIGINS = False # Never allow all origins, even in development
|
||||
CORS_ALLOWED_ORIGIN_REGEXES = [] # No regex patterns by default
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = DEBUG # Only allow all origins in development
|
||||
# CORS allowed methods
|
||||
CORS_ALLOW_METHODS = [
|
||||
'DELETE',
|
||||
'GET',
|
||||
'OPTIONS',
|
||||
'PATCH',
|
||||
'POST',
|
||||
'PUT',
|
||||
]
|
||||
|
||||
# CORS allowed headers
|
||||
CORS_ALLOW_HEADERS = [
|
||||
'accept',
|
||||
'accept-encoding',
|
||||
'authorization',
|
||||
'content-type',
|
||||
'dnt',
|
||||
'origin',
|
||||
'user-agent',
|
||||
'x-csrftoken',
|
||||
'x-requested-with',
|
||||
'x-internal-api-key', # Custom header for nginx validation
|
||||
]
|
||||
|
||||
# Django URL configuration
|
||||
APPEND_SLASH = True
|
||||
|
||||
@@ -32835,3 +32835,45 @@ INFO 2025-11-24 06:12:16,803 basehttp 108393 126550263457472 "GET /api/services/
|
||||
INFO 2025-11-24 06:12:16,840 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:13:09,003 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:13:09,043 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:36:47,411 basehttp 108393 126550271850176 "GET /api/career/jobs HTTP/1.1" 200 4675
|
||||
INFO 2025-11-24 06:36:47,412 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:36:47,426 basehttp 108393 126550271850176 "GET /api/career/jobs HTTP/1.1" 200 4675
|
||||
INFO 2025-11-24 06:36:47,430 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:36:47,488 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:36:47,493 basehttp 108393 126550271850176 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:36:57,635 basehttp 108393 126550271850176 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0
|
||||
INFO 2025-11-24 06:36:57,637 basehttp 108393 126550271850176 "OPTIONS /api/career/jobs HTTP/1.1" 200 0
|
||||
INFO 2025-11-24 06:36:57,658 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:36:57,668 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:36:57,693 basehttp 108393 126550271850176 "GET /api/career/jobs HTTP/1.1" 200 4675
|
||||
INFO 2025-11-24 06:37:09,801 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:37:09,806 basehttp 108393 126550632552128 "OPTIONS /api/home/banner/ HTTP/1.1" 200 0
|
||||
INFO 2025-11-24 06:37:09,807 basehttp 108393 126550623110848 "OPTIONS /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 0
|
||||
INFO 2025-11-24 06:37:09,826 basehttp 108393 126550649337536 "OPTIONS /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 0
|
||||
INFO 2025-11-24 06:37:09,842 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:37:09,843 basehttp 108393 126550271850176 "GET /api/career/jobs HTTP/1.1" 200 4675
|
||||
INFO 2025-11-24 06:37:09,847 basehttp 108393 126550640944832 "GET /api/home/banner/ HTTP/1.1" 200 3438
|
||||
INFO 2025-11-24 06:37:09,863 basehttp 108393 126550632552128 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
|
||||
INFO 2025-11-24 06:37:09,868 basehttp 108393 126550640944832 "GET /api/home/banner/ HTTP/1.1" 200 3438
|
||||
INFO 2025-11-24 06:37:09,878 basehttp 108393 126550271850176 "GET /api/career/jobs HTTP/1.1" 200 4675
|
||||
INFO 2025-11-24 06:37:09,898 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:37:09,909 basehttp 108393 126550623110848 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
|
||||
INFO 2025-11-24 06:37:09,915 basehttp 108393 126550632552128 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
|
||||
INFO 2025-11-24 06:37:09,955 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:37:09,976 basehttp 108393 126550623110848 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
|
||||
INFO 2025-11-24 06:37:10,010 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:37:10,060 basehttp 108393 126550263457472 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
|
||||
INFO 2025-11-24 06:39:11,537 autoreload 108393 126550715764864 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
|
||||
INFO 2025-11-24 06:39:12,172 autoreload 139522 135175989801088 Watching for file changes with StatReloader
|
||||
INFO 2025-11-24 06:39:24,941 autoreload 139522 135175989801088 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
|
||||
INFO 2025-11-24 06:39:25,492 autoreload 139604 123745838387328 Watching for file changes with StatReloader
|
||||
INFO 2025-11-24 06:39:59,747 autoreload 139604 123745838387328 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
|
||||
INFO 2025-11-24 06:40:00,322 autoreload 139793 134765910855808 Watching for file changes with StatReloader
|
||||
INFO 2025-11-24 06:40:19,231 autoreload 139793 134765910855808 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
|
||||
INFO 2025-11-24 06:40:19,802 autoreload 139883 138604311842944 Watching for file changes with StatReloader
|
||||
INFO 2025-11-24 06:40:25,359 autoreload 139883 138604311842944 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
|
||||
INFO 2025-11-24 06:40:25,924 autoreload 139932 123776047718528 Watching for file changes with StatReloader
|
||||
INFO 2025-11-24 06:40:57,132 autoreload 139932 123776047718528 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
|
||||
INFO 2025-11-24 06:40:57,712 autoreload 140119 136966227431552 Watching for file changes with StatReloader
|
||||
INFO 2025-11-24 06:41:38,174 autoreload 140119 136966227431552 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
|
||||
INFO 2025-11-24 06:41:38,716 autoreload 140239 133780171337856 Watching for file changes with StatReloader
|
||||
|
||||
173
backEnd/nginx.conf.example
Normal file
173
backEnd/nginx.conf.example
Normal file
@@ -0,0 +1,173 @@
|
||||
# Nginx Configuration Example for GNX Web Application
|
||||
# This configuration shows how to set up nginx as a reverse proxy
|
||||
# to secure the Django API backend
|
||||
|
||||
# Generate a secure API key for INTERNAL_API_KEY:
|
||||
# python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
# Add this key to your Django .env file as INTERNAL_API_KEY
|
||||
|
||||
upstream django_backend {
|
||||
# Django backend running on internal network only
|
||||
server 127.0.0.1:8000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream nextjs_frontend {
|
||||
# Next.js frontend
|
||||
server 127.0.0.1:3000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# 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=30r/s;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name gnxsoft.com www.gnxsoft.com;
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
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 HIGH:!aNULL:!MD5;
|
||||
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 "DENY" 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 Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/gnxsoft_access.log;
|
||||
error_log /var/log/nginx/gnxsoft_error.log;
|
||||
|
||||
# Client body size limit
|
||||
client_max_body_size 10M;
|
||||
|
||||
# API Endpoints - Proxy to Django backend
|
||||
# These requests will have the X-Internal-API-Key header added
|
||||
location /api/ {
|
||||
# Rate limiting
|
||||
limit_req zone=api_limit burst=20 nodelay;
|
||||
|
||||
# Add custom header to prove request came through nginx
|
||||
# This value must match INTERNAL_API_KEY in Django settings
|
||||
set $api_key "YOUR_SECURE_API_KEY_HERE";
|
||||
proxy_set_header X-Internal-API-Key $api_key;
|
||||
|
||||
# Standard proxy headers
|
||||
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;
|
||||
|
||||
# Preserve original request headers
|
||||
proxy_set_header Origin $http_origin;
|
||||
proxy_set_header Referer $http_referer;
|
||||
|
||||
# Proxy settings
|
||||
proxy_pass http://django_backend;
|
||||
proxy_redirect off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# Buffering
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
proxy_busy_buffers_size 8k;
|
||||
}
|
||||
|
||||
# Media files - Serve from Django
|
||||
location /media/ {
|
||||
alias /path/to/gnx/backEnd/media/;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Static files - Serve from Django
|
||||
location /static/ {
|
||||
alias /path/to/gnx/backEnd/staticfiles/;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Admin panel - Only allow from specific IPs (optional)
|
||||
location /admin/ {
|
||||
# Uncomment to restrict admin access
|
||||
# allow 192.168.1.0/24;
|
||||
# allow 10.0.0.0/8;
|
||||
# deny all;
|
||||
|
||||
# Same proxy settings as /api/
|
||||
set $api_key "YOUR_SECURE_API_KEY_HERE";
|
||||
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://django_backend;
|
||||
proxy_redirect off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
}
|
||||
|
||||
# Frontend - Proxy to Next.js
|
||||
location / {
|
||||
# Rate limiting
|
||||
limit_req zone=general_limit burst=50 nodelay;
|
||||
|
||||
# Proxy to Next.js frontend
|
||||
proxy_pass http://nextjs_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;
|
||||
}
|
||||
|
||||
# Health check endpoint (optional)
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
||||
# Block direct access to backend port 8000 from external IPs
|
||||
# This should be done at the firewall level:
|
||||
# ufw deny 8000
|
||||
# iptables -A INPUT -p tcp --dport 8000 ! -s 127.0.0.1 -j DROP
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Django Settings
|
||||
SECRET_KEY=your-super-secret-production-key-here
|
||||
DEBUG=False
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com,your-server-ip
|
||||
ALLOWED_HOSTS=gnxsoft.com,www.gnxsoft.com,your-server-ip
|
||||
|
||||
# Database - Using SQLite (default)
|
||||
# SQLite is configured in settings.py - no DATABASE_URL needed
|
||||
@@ -37,9 +37,16 @@ SECURE_BROWSER_XSS_FILTER=True
|
||||
X_FRAME_OPTIONS=DENY
|
||||
|
||||
# CORS Settings (Production)
|
||||
CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
|
||||
PRODUCTION_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
|
||||
CORS_ALLOW_CREDENTIALS=True
|
||||
|
||||
# CSRF Trusted Origins
|
||||
CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
|
||||
|
||||
# API Security - Internal API Key (nginx will add this header)
|
||||
# Generate a secure key: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
INTERNAL_API_KEY=your-secure-api-key-here-change-this-in-production
|
||||
|
||||
# Static Files
|
||||
STATIC_ROOT=/var/www/gnx/staticfiles/
|
||||
MEDIA_ROOT=/var/www/gnx/media/
|
||||
|
||||
27
frontEnd/app/about-us/AboutUsClient.tsx
Normal file
27
frontEnd/app/about-us/AboutUsClient.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
import Header from "@/components/shared/layout/header/Header";
|
||||
import AboutBanner from "@/components/pages/about/AboutBanner";
|
||||
import AboutServiceComponent from "@/components/pages/about/AboutService";
|
||||
import Footer from "@/components/shared/layout/footer/Footer";
|
||||
import AboutScrollProgressButton from "@/components/pages/about/AboutScrollProgressButton";
|
||||
import AboutInitAnimations from "@/components/pages/about/AboutInitAnimations";
|
||||
import AboutStarter from "@/components/pages/about/AboutStarter";
|
||||
|
||||
const AboutUsClient = () => {
|
||||
return (
|
||||
<div className="enterprise-about-page">
|
||||
<Header />
|
||||
<main>
|
||||
<AboutBanner />
|
||||
<AboutServiceComponent />
|
||||
<AboutStarter />
|
||||
</main>
|
||||
<Footer />
|
||||
<AboutScrollProgressButton />
|
||||
<AboutInitAnimations />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutUsClient;
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
"use client";
|
||||
import { useEffect } from 'react';
|
||||
import Header from "@/components/shared/layout/header/Header";
|
||||
import AboutBanner from "@/components/pages/about/AboutBanner";
|
||||
import AboutServiceComponent from "@/components/pages/about/AboutService";
|
||||
import Footer from "@/components/shared/layout/footer/Footer";
|
||||
import AboutScrollProgressButton from "@/components/pages/about/AboutScrollProgressButton";
|
||||
import AboutInitAnimations from "@/components/pages/about/AboutInitAnimations";
|
||||
import AboutStarter from "@/components/pages/about/AboutStarter";
|
||||
// Server Component - metadata export is allowed here
|
||||
import { Metadata } from 'next';
|
||||
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
|
||||
import AboutUsClient from "./AboutUsClient";
|
||||
|
||||
export const metadata: Metadata = createMetadata({
|
||||
title: "About Us - Enterprise Software Development Company",
|
||||
description: "Learn about GNX Soft - a leading enterprise software development company founded in 2020, specializing in custom software solutions, data replication, AI business intelligence, incident management, and comprehensive IT solutions for modern businesses.",
|
||||
keywords: [
|
||||
"About GNX Soft",
|
||||
"Software Development Company",
|
||||
"Enterprise Solutions Provider",
|
||||
"IT Services Company",
|
||||
"Custom Software Development",
|
||||
"Technology Company",
|
||||
"Software Engineering Team",
|
||||
"Digital Transformation Experts",
|
||||
],
|
||||
url: "/about-us",
|
||||
});
|
||||
|
||||
// Note: Since this is a client component, we'll set metadata via useEffect
|
||||
const AboutUsPage = () => {
|
||||
useEffect(() => {
|
||||
document.title = "About Us - Enterprise Software Development Company | GNX Soft";
|
||||
const metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (metaDescription) {
|
||||
metaDescription.setAttribute('content', 'Learn about GNX Soft - a leading enterprise software development company with expertise in custom software, data replication, AI business intelligence, and comprehensive IT solutions.');
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<div className="enterprise-about-page">
|
||||
<Header />
|
||||
<main>
|
||||
<AboutBanner />
|
||||
<AboutServiceComponent />
|
||||
<AboutStarter />
|
||||
</main>
|
||||
<Footer />
|
||||
<AboutScrollProgressButton />
|
||||
<AboutInitAnimations />
|
||||
</div>
|
||||
);
|
||||
return <AboutUsClient />;
|
||||
};
|
||||
|
||||
export default AboutUsPage;
|
||||
|
||||
@@ -1,18 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import Header from "@/components/shared/layout/header/Header";
|
||||
import JobSingle from "@/components/pages/career/JobSingle";
|
||||
import Footer from "@/components/shared/layout/footer/Footer";
|
||||
import CareerScrollProgressButton from "@/components/pages/career/CareerScrollProgressButton";
|
||||
import CareerInitAnimations from "@/components/pages/career/CareerInitAnimations";
|
||||
import { useJob } from "@/lib/hooks/useCareer";
|
||||
import { generateCareerMetadata } from "@/lib/seo/metadata";
|
||||
|
||||
const JobPage = () => {
|
||||
const params = useParams();
|
||||
const slug = params?.slug as string;
|
||||
const { job, loading, error } = useJob(slug);
|
||||
|
||||
// Update metadata dynamically for client component
|
||||
useEffect(() => {
|
||||
if (job) {
|
||||
const metadata = generateCareerMetadata(job);
|
||||
const title = typeof metadata.title === 'string' ? metadata.title : `Career - ${job.title} | GNX Soft`;
|
||||
document.title = title;
|
||||
|
||||
// Update meta description
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
const description = typeof metadata.description === 'string' ? metadata.description : `Apply for ${job.title} at GNX Soft. ${job.location || 'Remote'} position.`;
|
||||
metaDescription.setAttribute('content', description);
|
||||
|
||||
// Update canonical URL
|
||||
let canonical = document.querySelector('link[rel="canonical"]');
|
||||
if (!canonical) {
|
||||
canonical = document.createElement('link');
|
||||
canonical.setAttribute('rel', 'canonical');
|
||||
document.head.appendChild(canonical);
|
||||
}
|
||||
canonical.setAttribute('href', `${window.location.origin}/career/${job.slug}`);
|
||||
}
|
||||
}, [job]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="tp-app">
|
||||
|
||||
@@ -1,11 +1,64 @@
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import Header from "@/components/shared/layout/header/Header";
|
||||
import BlogSingle from "@/components/pages/blog/BlogSingle";
|
||||
import LatestPost from "@/components/pages/blog/LatestPost";
|
||||
import Footer from "@/components/shared/layout/footer/Footer";
|
||||
import BlogScrollProgressButton from "@/components/pages/blog/BlogScrollProgressButton";
|
||||
import BlogInitAnimations from "@/components/pages/blog/BlogInitAnimations";
|
||||
import { generateBlogMetadata } from "@/lib/seo/metadata";
|
||||
import { API_CONFIG } from "@/lib/config/api";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
slug: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/blog/posts/${slug}/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
next: { revalidate: 3600 },
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const post = await response.json();
|
||||
|
||||
return generateBlogMetadata({
|
||||
title: post.title,
|
||||
description: post.meta_description || post.excerpt || post.content?.substring(0, 160),
|
||||
excerpt: post.excerpt,
|
||||
slug: post.slug,
|
||||
image: post.featured_image || post.thumbnail,
|
||||
published_at: post.published_at,
|
||||
updated_at: post.updated_at,
|
||||
author: post.author ? { name: post.author.name } : undefined,
|
||||
category: post.category ? { name: post.category.title } : undefined,
|
||||
tags: post.tags?.map((tag: any) => tag.name) || [],
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
title: 'Insight Not Found | GNX Soft',
|
||||
description: 'The requested insight article could not be found.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const page = async ({ params }: PageProps) => {
|
||||
const { slug } = await params;
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div className="tp-app">
|
||||
<Header />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Metadata } from 'next';
|
||||
import Header from "@/components/shared/layout/header/Header";
|
||||
import HomeBanner from "@/components/pages/home/HomeBanner";
|
||||
import Overview from "@/components/pages/home/Overview";
|
||||
@@ -7,6 +8,27 @@ import HomeLatestPost from "@/components/pages/home/HomeLatestPost";
|
||||
import Footer from "@/components/shared/layout/footer/Footer";
|
||||
import HomeScrollProgressButton from "@/components/pages/home/HomeScrollProgressButton";
|
||||
import HomeInitAnimations from "@/components/pages/home/HomeInitAnimations";
|
||||
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
|
||||
|
||||
export const metadata: Metadata = createMetadata({
|
||||
title: "Enterprise Software Development & IT Solutions",
|
||||
description: "GNX Soft - Leading enterprise software development company specializing in custom software solutions, data replication, incident management, AI business intelligence, backend & frontend engineering, and comprehensive system integrations for modern businesses.",
|
||||
keywords: [
|
||||
"Enterprise Software Development",
|
||||
"Custom Software Solutions",
|
||||
"Data Replication Services",
|
||||
"Incident Management SaaS",
|
||||
"AI Business Intelligence",
|
||||
"Backend Engineering",
|
||||
"Frontend Development",
|
||||
"Systems Integration",
|
||||
"Cloud Solutions",
|
||||
"DevOps Services",
|
||||
"API Development",
|
||||
"Digital Transformation",
|
||||
],
|
||||
url: "/",
|
||||
});
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import Header from "@/components/shared/layout/header/Header";
|
||||
import Footer from "@/components/shared/layout/footer/Footer";
|
||||
import { Suspense } from 'react';
|
||||
import { usePolicy } from '@/lib/hooks/usePolicy';
|
||||
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
|
||||
|
||||
const PolicyContent = () => {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -12,6 +14,44 @@ const PolicyContent = () => {
|
||||
|
||||
const { data: policy, isLoading, error } = usePolicy(type);
|
||||
|
||||
// Update metadata based on policy type
|
||||
useEffect(() => {
|
||||
const policyTitles = {
|
||||
privacy: 'Privacy Policy - Data Protection & Privacy',
|
||||
terms: 'Terms of Use - Terms & Conditions',
|
||||
support: 'Support Policy - Support Terms & Guidelines',
|
||||
};
|
||||
|
||||
const policyDescriptions = {
|
||||
privacy: 'Read GNX Soft\'s Privacy Policy to understand how we collect, use, and protect your personal information and data.',
|
||||
terms: 'Review GNX Soft\'s Terms of Use and Conditions for using our software services and platforms.',
|
||||
support: 'Learn about GNX Soft\'s Support Policy, including support terms, response times, and service level agreements.',
|
||||
};
|
||||
|
||||
const metadata = createMetadata({
|
||||
title: policyTitles[type],
|
||||
description: policyDescriptions[type],
|
||||
keywords: [
|
||||
type === 'privacy' ? 'Privacy Policy' : type === 'terms' ? 'Terms of Use' : 'Support Policy',
|
||||
'Legal Documents',
|
||||
'Company Policies',
|
||||
'Data Protection',
|
||||
'Terms and Conditions',
|
||||
],
|
||||
url: `/policy?type=${type}`,
|
||||
});
|
||||
|
||||
document.title = metadata.title || `${policyTitles[type]} | GNX Soft`;
|
||||
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', metadata.description || policyDescriptions[type]);
|
||||
}, [type]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<section className="policy-section section-padding">
|
||||
|
||||
@@ -13,19 +13,59 @@ export default function robots(): MetadataRoute.Robots {
|
||||
'/admin/',
|
||||
'/_next/',
|
||||
'/private/',
|
||||
'/static/',
|
||||
'/*.json$',
|
||||
'/*?*',
|
||||
],
|
||||
},
|
||||
{
|
||||
userAgent: 'Googlebot',
|
||||
allow: '/',
|
||||
disallow: ['/api/', '/admin/', '/private/'],
|
||||
allow: [
|
||||
'/',
|
||||
'/services',
|
||||
'/services/*',
|
||||
'/about-us',
|
||||
'/contact-us',
|
||||
'/career',
|
||||
'/career/*',
|
||||
'/case-study',
|
||||
'/case-study/*',
|
||||
'/insights',
|
||||
'/insights/*',
|
||||
'/support-center',
|
||||
'/policy',
|
||||
],
|
||||
disallow: [
|
||||
'/api/',
|
||||
'/admin/',
|
||||
'/private/',
|
||||
'/_next/',
|
||||
'/static/',
|
||||
],
|
||||
},
|
||||
{
|
||||
userAgent: 'Bingbot',
|
||||
allow: '/',
|
||||
disallow: ['/api/', '/admin/', '/private/'],
|
||||
allow: [
|
||||
'/',
|
||||
'/services',
|
||||
'/services/*',
|
||||
'/about-us',
|
||||
'/contact-us',
|
||||
'/career',
|
||||
'/career/*',
|
||||
'/case-study',
|
||||
'/case-study/*',
|
||||
'/insights',
|
||||
'/insights/*',
|
||||
'/support-center',
|
||||
'/policy',
|
||||
],
|
||||
disallow: [
|
||||
'/api/',
|
||||
'/admin/',
|
||||
'/private/',
|
||||
'/_next/',
|
||||
'/static/',
|
||||
],
|
||||
},
|
||||
],
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
|
||||
@@ -1,13 +1,43 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import Header from "@/components/shared/layout/header/Header";
|
||||
import Footer from "@/components/shared/layout/footer/Footer";
|
||||
import SupportCenterHero from "@/components/pages/support/SupportCenterHero";
|
||||
import SupportCenterContent from "@/components/pages/support/SupportCenterContent";
|
||||
import { useState } from "react";
|
||||
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
|
||||
|
||||
type ModalType = 'create' | 'knowledge' | 'status' | null;
|
||||
|
||||
const SupportCenterPage = () => {
|
||||
// Set metadata for client component
|
||||
useEffect(() => {
|
||||
const metadata = createMetadata({
|
||||
title: "Support Center - Enterprise Support & Help Desk",
|
||||
description: "Get 24/7 enterprise support from GNX Soft. Access our knowledge base, create support tickets, check ticket status, and get help with our software solutions and services.",
|
||||
keywords: [
|
||||
"Support Center",
|
||||
"Customer Support",
|
||||
"Help Desk",
|
||||
"Technical Support",
|
||||
"Knowledge Base",
|
||||
"Support Tickets",
|
||||
"Enterprise Support",
|
||||
"IT Support",
|
||||
],
|
||||
url: "/support-center",
|
||||
});
|
||||
|
||||
document.title = metadata.title || "Support Center | GNX Soft";
|
||||
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', metadata.description || 'Get enterprise support from GNX Soft');
|
||||
}, []);
|
||||
const [activeModal, setActiveModal] = useState<ModalType>(null);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user