diff --git a/backEnd/gnx/__pycache__/settings.cpython-312.pyc b/backEnd/gnx/__pycache__/settings.cpython-312.pyc index 431ec15e..0ad5b231 100644 Binary files a/backEnd/gnx/__pycache__/settings.cpython-312.pyc and b/backEnd/gnx/__pycache__/settings.cpython-312.pyc differ diff --git a/backEnd/gnx/middleware/__pycache__/api_security.cpython-312.pyc b/backEnd/gnx/middleware/__pycache__/api_security.cpython-312.pyc new file mode 100644 index 00000000..bc482812 Binary files /dev/null and b/backEnd/gnx/middleware/__pycache__/api_security.cpython-312.pyc differ diff --git a/backEnd/gnx/middleware/api_security.py b/backEnd/gnx/middleware/api_security.py new file mode 100644 index 00000000..f4ce87c4 --- /dev/null +++ b/backEnd/gnx/middleware/api_security.py @@ -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') + diff --git a/backEnd/gnx/settings.py b/backEnd/gnx/settings.py index 4ab3907c..2f97caa8 100644 --- a/backEnd/gnx/settings.py +++ b/backEnd/gnx/settings.py @@ -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 diff --git a/backEnd/logs/django.log b/backEnd/logs/django.log index 0bb9aa02..88f262e9 100644 --- a/backEnd/logs/django.log +++ b/backEnd/logs/django.log @@ -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 diff --git a/backEnd/nginx.conf.example b/backEnd/nginx.conf.example new file mode 100644 index 00000000..29df60de --- /dev/null +++ b/backEnd/nginx.conf.example @@ -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 + diff --git a/backEnd/production.env.example b/backEnd/production.env.example index 74094691..53e2b216 100644 --- a/backEnd/production.env.example +++ b/backEnd/production.env.example @@ -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/ diff --git a/frontEnd/app/about-us/AboutUsClient.tsx b/frontEnd/app/about-us/AboutUsClient.tsx new file mode 100644 index 00000000..ca045831 --- /dev/null +++ b/frontEnd/app/about-us/AboutUsClient.tsx @@ -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 ( +
+
+
+ + + +
+
+ ); +}; + +export default AboutUsClient; + diff --git a/frontEnd/app/about-us/page.tsx b/frontEnd/app/about-us/page.tsx index a21eba79..c16b84ff 100644 --- a/frontEnd/app/about-us/page.tsx +++ b/frontEnd/app/about-us/page.tsx @@ -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 ( -
-
-
- - - -
-
- ); + return ; }; export default AboutUsPage; diff --git a/frontEnd/app/career/[slug]/page.tsx b/frontEnd/app/career/[slug]/page.tsx index f4143328..c21d0c29 100644 --- a/frontEnd/app/career/[slug]/page.tsx +++ b/frontEnd/app/career/[slug]/page.tsx @@ -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 (
diff --git a/frontEnd/app/insights/[slug]/page.tsx b/frontEnd/app/insights/[slug]/page.tsx index 161b6754..2224e35f 100644 --- a/frontEnd/app/insights/[slug]/page.tsx +++ b/frontEnd/app/insights/[slug]/page.tsx @@ -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"; -const page = () => { +interface PageProps { + params: Promise<{ + slug: string; + }>; +} + +export async function generateMetadata({ params }: PageProps): Promise { + 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; + return (
diff --git a/frontEnd/app/page.tsx b/frontEnd/app/page.tsx index 2f4a27f3..befe477f 100644 --- a/frontEnd/app/page.tsx +++ b/frontEnd/app/page.tsx @@ -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 ( diff --git a/frontEnd/app/policy/page.tsx b/frontEnd/app/policy/page.tsx index 7667f20c..4fbb2552 100644 --- a/frontEnd/app/policy/page.tsx +++ b/frontEnd/app/policy/page.tsx @@ -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 (
diff --git a/frontEnd/app/robots.ts b/frontEnd/app/robots.ts index 65458484..1335c6df 100644 --- a/frontEnd/app/robots.ts +++ b/frontEnd/app/robots.ts @@ -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`, diff --git a/frontEnd/app/support-center/page.tsx b/frontEnd/app/support-center/page.tsx index 9d9a929c..1feba094 100644 --- a/frontEnd/app/support-center/page.tsx +++ b/frontEnd/app/support-center/page.tsx @@ -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(null); return (