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',
|
'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.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',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
@@ -151,7 +152,7 @@ CSRF_COOKIE_HTTPONLY = True
|
|||||||
CSRF_COOKIE_SAMESITE = 'Strict'
|
CSRF_COOKIE_SAMESITE = 'Strict'
|
||||||
CSRF_TRUSTED_ORIGINS = config(
|
CSRF_TRUSTED_ORIGINS = config(
|
||||||
'CSRF_TRUSTED_ORIGINS',
|
'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(',')]
|
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 = [
|
CORS_ALLOWED_ORIGINS = [
|
||||||
"http://localhost:3000", # React development server
|
"http://localhost:3000", # React development server
|
||||||
"http://127.0.0.1:3000",
|
"http://127.0.0.1:3000",
|
||||||
@@ -237,14 +262,43 @@ CORS_ALLOWED_ORIGINS = [
|
|||||||
"http://127.0.0.1:3001",
|
"http://127.0.0.1:3001",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add production origins if configured
|
# Add production origins if configured (defaults to gnxsoft.com domains)
|
||||||
PRODUCTION_ORIGINS = config('PRODUCTION_ORIGINS', default='', cast=lambda v: [s.strip() for s in v.split(',') if s.strip()])
|
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:
|
if PRODUCTION_ORIGINS:
|
||||||
CORS_ALLOWED_ORIGINS.extend(PRODUCTION_ORIGINS)
|
CORS_ALLOWED_ORIGINS.extend(PRODUCTION_ORIGINS)
|
||||||
|
|
||||||
|
# Strict CORS configuration - only allow configured origins
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
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
|
# Django URL configuration
|
||||||
APPEND_SLASH = True
|
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: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,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: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
|
# Django Settings
|
||||||
SECRET_KEY=your-super-secret-production-key-here
|
SECRET_KEY=your-super-secret-production-key-here
|
||||||
DEBUG=False
|
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)
|
# Database - Using SQLite (default)
|
||||||
# SQLite is configured in settings.py - no DATABASE_URL needed
|
# SQLite is configured in settings.py - no DATABASE_URL needed
|
||||||
@@ -37,9 +37,16 @@ SECURE_BROWSER_XSS_FILTER=True
|
|||||||
X_FRAME_OPTIONS=DENY
|
X_FRAME_OPTIONS=DENY
|
||||||
|
|
||||||
# CORS Settings (Production)
|
# 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
|
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 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/
|
||||||
|
|||||||
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";
|
// Server Component - metadata export is allowed here
|
||||||
import { useEffect } from 'react';
|
import { Metadata } from 'next';
|
||||||
import Header from "@/components/shared/layout/header/Header";
|
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
|
||||||
import AboutBanner from "@/components/pages/about/AboutBanner";
|
import AboutUsClient from "./AboutUsClient";
|
||||||
import AboutServiceComponent from "@/components/pages/about/AboutService";
|
|
||||||
import Footer from "@/components/shared/layout/footer/Footer";
|
export const metadata: Metadata = createMetadata({
|
||||||
import AboutScrollProgressButton from "@/components/pages/about/AboutScrollProgressButton";
|
title: "About Us - Enterprise Software Development Company",
|
||||||
import AboutInitAnimations from "@/components/pages/about/AboutInitAnimations";
|
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.",
|
||||||
import AboutStarter from "@/components/pages/about/AboutStarter";
|
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 = () => {
|
const AboutUsPage = () => {
|
||||||
useEffect(() => {
|
return <AboutUsClient />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AboutUsPage;
|
export default AboutUsPage;
|
||||||
|
|||||||
@@ -1,18 +1,48 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
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";
|
||||||
import CareerScrollProgressButton from "@/components/pages/career/CareerScrollProgressButton";
|
import CareerScrollProgressButton from "@/components/pages/career/CareerScrollProgressButton";
|
||||||
import CareerInitAnimations from "@/components/pages/career/CareerInitAnimations";
|
import CareerInitAnimations from "@/components/pages/career/CareerInitAnimations";
|
||||||
import { useJob } from "@/lib/hooks/useCareer";
|
import { useJob } from "@/lib/hooks/useCareer";
|
||||||
|
import { generateCareerMetadata } from "@/lib/seo/metadata";
|
||||||
|
|
||||||
const JobPage = () => {
|
const JobPage = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const slug = params?.slug as string;
|
const slug = params?.slug as string;
|
||||||
const { job, loading, error } = useJob(slug);
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="tp-app">
|
<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 Header from "@/components/shared/layout/header/Header";
|
||||||
import BlogSingle from "@/components/pages/blog/BlogSingle";
|
import BlogSingle from "@/components/pages/blog/BlogSingle";
|
||||||
import LatestPost from "@/components/pages/blog/LatestPost";
|
import LatestPost from "@/components/pages/blog/LatestPost";
|
||||||
import Footer from "@/components/shared/layout/footer/Footer";
|
import Footer from "@/components/shared/layout/footer/Footer";
|
||||||
import BlogScrollProgressButton from "@/components/pages/blog/BlogScrollProgressButton";
|
import BlogScrollProgressButton from "@/components/pages/blog/BlogScrollProgressButton";
|
||||||
import BlogInitAnimations from "@/components/pages/blog/BlogInitAnimations";
|
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 (
|
return (
|
||||||
<div className="tp-app">
|
<div className="tp-app">
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
import Header from "@/components/shared/layout/header/Header";
|
import Header from "@/components/shared/layout/header/Header";
|
||||||
import HomeBanner from "@/components/pages/home/HomeBanner";
|
import HomeBanner from "@/components/pages/home/HomeBanner";
|
||||||
import Overview from "@/components/pages/home/Overview";
|
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 Footer from "@/components/shared/layout/footer/Footer";
|
||||||
import HomeScrollProgressButton from "@/components/pages/home/HomeScrollProgressButton";
|
import HomeScrollProgressButton from "@/components/pages/home/HomeScrollProgressButton";
|
||||||
import HomeInitAnimations from "@/components/pages/home/HomeInitAnimations";
|
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 = () => {
|
const page = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import Header from "@/components/shared/layout/header/Header";
|
import Header from "@/components/shared/layout/header/Header";
|
||||||
import Footer from "@/components/shared/layout/footer/Footer";
|
import Footer from "@/components/shared/layout/footer/Footer";
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { usePolicy } from '@/lib/hooks/usePolicy';
|
import { usePolicy } from '@/lib/hooks/usePolicy';
|
||||||
|
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
|
||||||
|
|
||||||
const PolicyContent = () => {
|
const PolicyContent = () => {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -12,6 +14,44 @@ const PolicyContent = () => {
|
|||||||
|
|
||||||
const { data: policy, isLoading, error } = usePolicy(type);
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<section className="policy-section section-padding">
|
<section className="policy-section section-padding">
|
||||||
|
|||||||
@@ -13,19 +13,59 @@ export default function robots(): MetadataRoute.Robots {
|
|||||||
'/admin/',
|
'/admin/',
|
||||||
'/_next/',
|
'/_next/',
|
||||||
'/private/',
|
'/private/',
|
||||||
|
'/static/',
|
||||||
'/*.json$',
|
'/*.json$',
|
||||||
'/*?*',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
userAgent: 'Googlebot',
|
userAgent: 'Googlebot',
|
||||||
allow: '/',
|
allow: [
|
||||||
disallow: ['/api/', '/admin/', '/private/'],
|
'/',
|
||||||
|
'/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',
|
userAgent: 'Bingbot',
|
||||||
allow: '/',
|
allow: [
|
||||||
disallow: ['/api/', '/admin/', '/private/'],
|
'/',
|
||||||
|
'/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`,
|
sitemap: `${baseUrl}/sitemap.xml`,
|
||||||
|
|||||||
@@ -1,13 +1,43 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import { useEffect } from "react";
|
||||||
import Header from "@/components/shared/layout/header/Header";
|
import Header from "@/components/shared/layout/header/Header";
|
||||||
import Footer from "@/components/shared/layout/footer/Footer";
|
import Footer from "@/components/shared/layout/footer/Footer";
|
||||||
import SupportCenterHero from "@/components/pages/support/SupportCenterHero";
|
import SupportCenterHero from "@/components/pages/support/SupportCenterHero";
|
||||||
import SupportCenterContent from "@/components/pages/support/SupportCenterContent";
|
import SupportCenterContent from "@/components/pages/support/SupportCenterContent";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
|
||||||
|
|
||||||
type ModalType = 'create' | 'knowledge' | 'status' | null;
|
type ModalType = 'create' | 'knowledge' | 'status' | null;
|
||||||
|
|
||||||
const SupportCenterPage = () => {
|
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);
|
const [activeModal, setActiveModal] = useState<ModalType>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user