From 0b1cabcfaf67ec2bd7695ecd44986cd29a667e7c Mon Sep 17 00:00:00 2001
From: Iliyan Angelov
Date: Mon, 24 Nov 2025 16:47:37 +0200
Subject: [PATCH] updates
---
.env.production | 47 +++
.zipignore | 65 ++++
backEnd/.dockerignore | 39 +++
backEnd/.env | 4 +
backEnd/Dockerfile | 36 +++
backEnd/db.sqlite3 | Bin 962560 -> 962560 bytes
.../gnx/__pycache__/settings.cpython-312.pyc | Bin 9388 -> 10739 bytes
backEnd/gnx/management/__init__.py | 2 +
backEnd/gnx/management/commands/__init__.py | 2 +
.../__pycache__/update_admin.cpython-312.pyc | Bin 0 -> 3847 bytes
.../gnx/management/commands/show_api_key.py | 34 ++
.../gnx/management/commands/update_admin.py | 99 ++++++
.../admin_ip_restriction.cpython-312.pyc | Bin 0 -> 5390 bytes
.../gnx/middleware/admin_ip_restriction.py | 132 ++++++++
backEnd/gnx/settings.py | 41 ++-
backEnd/logs/django.log | 304 ++++++++++++++++++
backEnd/nginx.conf.example | 12 +-
backEnd/production.env.example | 6 +
backEnd/requirements.txt | 17 +
create-deployment-zip.sh | 56 ++++
docker-compose.yml | 98 ++++++
docker-start.sh | 240 ++++++++++++++
frontEnd/.dockerignore | 26 ++
frontEnd/Dockerfile | 50 +++
frontEnd/app/career/[slug]/page.tsx | 5 +-
frontEnd/app/policy/page.tsx | 8 +-
frontEnd/components/pages/blog/BlogSingle.tsx | 2 +-
.../components/pages/career/JobSingle.tsx | 5 +-
.../pages/case-study/CaseSingle.tsx | 2 +-
.../pages/support/CreateTicketForm.tsx | 2 +-
.../pages/support/KnowledgeBase.tsx | 2 +-
.../shared/layout/footer/Footer.tsx | 45 +++
.../shared/layout/header/Header.tsx | 64 +++-
frontEnd/lib/config/api.ts | 11 +-
frontEnd/lib/imageUtils.ts | 3 +-
frontEnd/next.config.js | 2 +
frontEnd/public/images/gnx-goodfirms.webp | Bin 0 -> 32386 bytes
frontEnd/public/images/logo-light.png | Bin 122560 -> 36408 bytes
frontEnd/public/images/logo.png | Bin 122560 -> 36408 bytes
frontEnd/public/styles/layout/_footer.scss | 54 ++++
frontEnd/public/styles/layout/_header.scss | 21 +-
migrate-data.sh | 78 +++++
migrate-sqlite-to-postgres.sh | 133 ++++++++
nginx.conf | 218 +++++++++++++
setup.sh | 84 +++++
45 files changed, 2021 insertions(+), 28 deletions(-)
create mode 100644 .env.production
create mode 100644 .zipignore
create mode 100644 backEnd/.dockerignore
create mode 100644 backEnd/Dockerfile
create mode 100644 backEnd/gnx/management/__init__.py
create mode 100644 backEnd/gnx/management/commands/__init__.py
create mode 100644 backEnd/gnx/management/commands/__pycache__/update_admin.cpython-312.pyc
create mode 100644 backEnd/gnx/management/commands/show_api_key.py
create mode 100644 backEnd/gnx/management/commands/update_admin.py
create mode 100644 backEnd/gnx/middleware/__pycache__/admin_ip_restriction.cpython-312.pyc
create mode 100644 backEnd/gnx/middleware/admin_ip_restriction.py
create mode 100644 backEnd/requirements.txt
create mode 100644 create-deployment-zip.sh
create mode 100644 docker-compose.yml
create mode 100755 docker-start.sh
create mode 100644 frontEnd/.dockerignore
create mode 100644 frontEnd/Dockerfile
create mode 100644 frontEnd/public/images/gnx-goodfirms.webp
create mode 100755 migrate-data.sh
create mode 100755 migrate-sqlite-to-postgres.sh
create mode 100644 nginx.conf
create mode 100755 setup.sh
diff --git a/.env.production b/.env.production
new file mode 100644
index 00000000..898e83d7
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,47 @@
+# Production Environment Configuration for Docker
+# Django Settings
+SECRET_KEY=ks68*5@of1l&4rn1imsqdk9$khcya!&a#jtd89f!v^qg1w0&hc
+DEBUG=False
+ALLOWED_HOSTS=gnxsoft.com,www.gnxsoft.com,localhost,127.0.0.1,backend
+
+# Database (SQLite for simplicity, or use PostgreSQL)
+DATABASE_URL=postgresql://gnx:*4WfmDsfvNszbB3ozaQj0M#i@postgres:5432/gnxdb
+
+# Admin IP Restriction
+ADMIN_ALLOWED_IPS=193.194.155.249
+
+# Internal API Key (for nginx to backend communication)
+INTERNAL_API_KEY=your-generated-key-here
+PRODUCTION_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
+CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
+
+# Email Configuration
+EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
+EMAIL_HOST=mail.gnxsoft.com
+EMAIL_PORT=587
+EMAIL_USE_TLS=True
+EMAIL_USE_SSL=False
+EMAIL_HOST_USER=support@gnxsoft.com
+EMAIL_HOST_PASSWORD=P4eli240453.
+DEFAULT_FROM_EMAIL=support@gnxsoft.com
+COMPANY_EMAIL=support@gnxsoft.com
+SUPPORT_EMAIL=support@gnxsoft.com
+
+# Site URL
+SITE_URL=https://gnxsoft.com
+
+# Security Settings
+SECURE_SSL_REDIRECT=True
+SECURE_HSTS_SECONDS=31536000
+SECURE_HSTS_INCLUDE_SUBDOMAINS=True
+SECURE_HSTS_PRELOAD=True
+
+# CORS Settings
+CORS_ALLOWED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
+
+# PostgreSQL Database Configuration (Recommended for Production)
+POSTGRES_DB=gnxdb
+POSTGRES_USER=gnx
+POSTGRES_PASSWORD=*4WfmDsfvNszbB3ozaQj0M#i
+# Update DATABASE_URL to use PostgreSQL (uncomment the line below and comment SQLite)
+# DATABASE_URL=postgresql://gnxuser:change-this-password-in-production@postgres:5432/gnxdb
diff --git a/.zipignore b/.zipignore
new file mode 100644
index 00000000..42deebdc
--- /dev/null
+++ b/.zipignore
@@ -0,0 +1,65 @@
+# Files to exclude from production zip
+# These will be regenerated or are not needed on server
+
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+venv/
+env/
+ENV/
+.venv
+
+# Node
+node_modules/
+.next/
+.npm
+.yarn
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+logs/
+dev.log
+
+# Database (will be created fresh or migrated)
+*.sqlite3
+*.db
+
+# Docker
+.dockerignore
+
+# Git
+.git/
+.gitignore
+
+# Backups
+backups/
+*.backup
+*.bak
+
+# Temporary files
+*.tmp
+*.temp
+
+# Environment files (will be created from .env.production)
+.env.local
+.env.development
+
+# Build artifacts
+dist/
+build/
+*.egg-info/
+
diff --git a/backEnd/.dockerignore b/backEnd/.dockerignore
new file mode 100644
index 00000000..deaf5286
--- /dev/null
+++ b/backEnd/.dockerignore
@@ -0,0 +1,39 @@
+__pycache__
+*.pyc
+*.pyo
+*.pyd
+.Python
+*.so
+*.egg
+*.egg-info
+dist
+build
+.venv
+venv/
+env/
+ENV/
+.env
+.venv
+*.log
+logs/
+*.db
+*.sqlite3
+db.sqlite3
+.git
+.gitignore
+README.md
+*.md
+.DS_Store
+.vscode
+.idea
+*.swp
+*.swo
+*~
+.pytest_cache
+.coverage
+htmlcov/
+.tox/
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
diff --git a/backEnd/.env b/backEnd/.env
index 9d2f67ad..52475ce6 100644
--- a/backEnd/.env
+++ b/backEnd/.env
@@ -4,6 +4,10 @@ SECRET_KEY=ks68*5@of1l&4rn1imsqdk9$khcya!&a#jtd89f!v^qg1w0&hc
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
+INTERNAL_API_KEY=your-generated-key-here
+PRODUCTION_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
+CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
+
# Email Configuration (Development - uses console backend by default)
USE_SMTP_IN_DEV=True
DEFAULT_FROM_EMAIL=support@gnxsoft.com
diff --git a/backEnd/Dockerfile b/backEnd/Dockerfile
new file mode 100644
index 00000000..657a230b
--- /dev/null
+++ b/backEnd/Dockerfile
@@ -0,0 +1,36 @@
+# Django Backend Dockerfile
+FROM python:3.12-slim
+
+# Set environment variables
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ DEBIAN_FRONTEND=noninteractive
+
+# Set work directory
+WORKDIR /app
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y \
+ gcc \
+ postgresql-client \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install Python dependencies
+COPY requirements.txt /app/
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy project
+COPY . /app/
+
+# Create directories for media and static files
+RUN mkdir -p /app/media /app/staticfiles /app/logs
+
+# Collect static files (will be done at runtime if needed)
+# RUN python manage.py collectstatic --noinput
+
+# Expose port
+EXPOSE 1086
+
+# Run gunicorn
+CMD ["gunicorn", "--bind", "0.0.0.0:1086", "--workers", "3", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "gnx.wsgi:application"]
+
diff --git a/backEnd/db.sqlite3 b/backEnd/db.sqlite3
index 7fdd440715f92c7592ce416824a929843f26a0ba..04ca5d874df4c73ae201020f807a3b8479b27721 100644
GIT binary patch
delta 721
zcmajc&1=(e7zgmAO;?jGtz#&PJ51>i!FI_nZ%sudYt#26>-w^`m)e-ju3g`frAq}_
zI}e`5CL&(NgV*7P7W^-E5MNF^d+|K*V5S!jf)CH<^6bPiuaQ33C%Eo~jt%tjqBY=9(wSAf};G+C^ag`SW!
zqLvu%6xvNQFvw@C`6`p=K(cM}Wi3&RvKyOgDz6q5p0BX^Qo0+D)rDn1+yw++f+QIP
z^a4pooMy-nPk8}LQI9(Ii%(q=IfMLX$FrwD{N5?|9O_JWM;}Ho^7;yf>_NV7`1_fI
z=@u48uX}$^efAt7d&hmEkL>K;z&z5^yio6RjNP#ao~L!NWo}sma%eY5sz;NIMF1_N
zr|nWx6qW4_A-Ag6qM#A2G`k9|DdFl+TF&)Xd#Su(4yjF`YMG>3<2f#Cm*{e_SJ4?R
zmR9N=XC=kp1x;_teKSH1rZpLbMsltS-7F*YIe_{qfBK&vEG*5Apqea&$hGD*a
z$lvyT_3ck)2-nzmE8v#k|3!zLFTMJ>=vtV3x#zkKPmqhQ6W^tOoxvvGW!L-(l7T!K
z=Rbny@=JhSg)HmMon07baD4n^Jd2|6L|t@C80>y_xO5J2cpOs>uj7IPbzqKZ$BYA?
J94`5CqhGo4*?s^3
delta 399
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
E0n^hXe*gdg
delta 1848
zcmZux=}%l$6o2>42D342Wf+!W7M7t5Wv6Tf3Y2{*P^H&aI&hwkVVR+Cpcb?a)Vj24
z2)T(dX5t6@2qv0I{G>)>j34}hi7}NgCjAR&OpG5)<9T;LXQ-3BbMN{6e&^it&OLAb
zm^v7={$#Nj1^l_5d4gMwyH>l{uG_6}=4+R=D>|wpf+$F$q~9m1NDvk?c2^6cwuF;x
z7yk#N=@_g7-Dxm2a$gSy$#jsmuSvvHGe^_G2&M-@5^j+NFz*p+#npmq23V;LGGrkN
z2CyNRxkrKovSM19&`ijtS#{QaYU5GzkLa#l#??*Vhij<{E!O6HPkYgCZ}AO1YpKESU3YbG{xL
zpfQE7fF|mLW?G4P&f`-B7pWh6*rN83RzoYjfLGW2dN_I$UQVjNmt(Uj
zSRco3NqtGWE7(V!)DzJ_fcA$HiaD59715LS^z$$PDJg7#W3PamGNOYVT{!dM4RJJh
zrs!dgE}p(S9pV1czy2tmj~B2ED=9~eaajn$NyUtFY!z;&C?`0ooI$Tb1l+I&>u?7@
z%~wy>ySU$gO&r13_(%P^YNK!uZo?Z8jp<=4iQMLr|2RF|hd1FtOapJ>^EM9Y2E2m_
z-^IO})(sC4f5g456EDH(PrUb#e?O+DlN%b}2Z0Lw6EyZN2IA}bWswxepQPOK=00
zkK!iNXQECq;Ki`NEG>E;ZZ-$lwzY!IS~rL<{+o41Br#T(IVBzmY{~H>`IL=j4aR@Z
zUeT0p7J_ugza(w==NAGYS@H)%{#7Lm>+@^DaL7k^-uNeW4-x+s*!TH$q&@y;emgN)
zupng;&r;gs9R(VZOtJ2wXJn0a6^F9c5%+17yDF_tw{nktTkNghR$=id4Nrn)Nr@x`
z>PVsZzN%9rj?%G!)qosWlawu%=e(AyQb$@P99}yzEQe(oLw!?h+gZpyckZx-k}~3F
zJ0)&YjhdoXHFfN(5{Wc0w`+(ru@zTMPP3ZBJUBMpH#yclGCMdirL?dwUB6~3qiVpI
zYQ|MF!JfJuWSW^hJN8)>xTTtaYUWfkkA{8h@tT%ZZ$&j>_Qc~ceuG|hVQb%n=PL1%rORsGmz|ET?ETlrI+#-=~k=q%z^dOWAJny{N?Kjcml
gA;)p(sQOO##oXa=@R#hxL%|mRu>88H{Tg%s1G?n0fB*mh
diff --git a/backEnd/gnx/management/__init__.py b/backEnd/gnx/management/__init__.py
new file mode 100644
index 00000000..e2a6b467
--- /dev/null
+++ b/backEnd/gnx/management/__init__.py
@@ -0,0 +1,2 @@
+# Django management package
+
diff --git a/backEnd/gnx/management/commands/__init__.py b/backEnd/gnx/management/commands/__init__.py
new file mode 100644
index 00000000..7cf03715
--- /dev/null
+++ b/backEnd/gnx/management/commands/__init__.py
@@ -0,0 +1,2 @@
+# Django management commands package
+
diff --git a/backEnd/gnx/management/commands/__pycache__/update_admin.cpython-312.pyc b/backEnd/gnx/management/commands/__pycache__/update_admin.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d170acab2b09edf224bb7eba1187ca78c6f2b7a8
GIT binary patch
literal 3847
zcmdTH+in}z@htbk<<%rbnbgf%-7GC-OTNe#$aVl*R$@ts;#hTy5CNDqcO{YGMcG|O
z7C{w8;fE9mPzy~V39(`KsWoIE`2+#&gWSH5kN_?g3E-rC=^GQt4WPVq&T?0zq#9{a
z^z%4=X^`Z2s2Vz%BWmC
zn@$TEkyEo=ZcY?bi4(+hBE#hrN#^3RB!W;vNGZ;LU>!W6yRHd}G*aCKsCPzEV^}zr
z&WchB3#hV?QG~df$Y$cEKgL3s$aX6L%Sb{|0?uX=i6|-B?xQFrk}%T?6i@4R(?Y)Y
z_93sZwTzGkPr>gwK~d(jvKY73nPLFOXWpJQEER~PWxGyDB-Fh^9QEOC{O`uoo
zpzC%pqP>2{0^-@pyz}Hq%R8N%$=uDiUEPbO;c}lSO$)h{I?Mr==SiJX7v?10ZjyCs
zR!YrbHP*Rw*Ucd*l{hnWYUteAd{0%K#pG)0>ZEwyrn?1Ej0y5g4kuYB6;%czrAVo1
zotYD4h@gTG&vCoG;K(49fLRcOWkjIfey`o(GYlv1-5ZA;`NF10pSSz
z-~KsVk!4hX(rE&8kp#$QK$HWidpnYQ=>;KkSQq3RB9LmP8=)jFm;)o}I6wfjKt3ba
zO#MmMVZNn(kuK2kT!Bt{4$BlMO9NbAszAr=mW+Y1`>;J9Kv$#1sjZehC|Hy{TBBP~
z5(nkL0LkV9BtVmGhiM{RAU}f#*dYrJGZ(ZKKWimW=Di=uM3)td+6pnEMC0$7bIttV
zK-K;fRpfeg75yak+=e|*fr6-cA^Z4AIxv891UP#&Qey+h2b@5SVnM+1KXO@7w^q2r
z#^0Lx)(~g`b)nNpA?D#z{Vu#Rm~xkV7tIr0NbR^w1V`p7p->g?VpjI};AJR#%f)eTMof)oy7`O
zVR~A|Pn+%l6n7dQN&nt6XkehF+3#7Nb5_5ib=}RdljCliZN)H`_W*j+Y%EN4OyQ=m
zT|oW7#i(^IAbG2#tA;DX+%DnF;iv7JN1t$`vYeHxAxdOs>S8p^@r^RRdO9Ph*>obV
zQ`iOFp1qTl;;N!E(}|P{1bR9lE2{2RpwqGvBI7sQh=JUKRZxoXk)$(aJ>RjNVOmjNXk)bGT#E>Cs!a-o7Q{?=0_-
z@fFc2Xe1Qy%Mhq;uV#b7Y1JDHwQ#svsukUxO^JqX-1g{%q~K2(XAK!&FrAr&4lX6-
zy;dtx#c_M66yQQZ57E%PGF)mn{xMsFD%3T?5%|*ITK0F9{9T*=o^R15PvGyNmgT}y
zp&W{oLXj_S6ho2C(3Ns%q!b$242?dZzHSJ;2!_kSo>H*q$?R5eQ0wk{aQ)M_w7wG$
zu5SfqPNNi7&&9$OmwbOHktOB2-%TDbMGbJbZ6M@r#{c5Gz*#`^HaJH^(ww@F)5
z^FK+d@#u~h^$&i2;^~QU%d_gUm7n5kC%eSD>kB~SL+>olB^)&^ghT4#FH%0&-ZKngXR9q
zrT)thLLj`>2a)*P`_x(n@cl|rcO*^++Unl55RUAuB@($e|i;3Ad*k}=xLS`mA5YNieK&`JIxC1}iWc;H@
zZ`hN_sB+@YfRIyXW!wwv4wEC^F&-xQ7!a$z;AqtR+hs3T&-9vn2^hE|R{CM8kOV<|
nYqJs5O9l}`U!j4oP{-Hy#!Y+Z9<{^#f*=APfAA9F)ll{?rovA<
literal 0
HcmV?d00001
diff --git a/backEnd/gnx/management/commands/show_api_key.py b/backEnd/gnx/management/commands/show_api_key.py
new file mode 100644
index 00000000..48594794
--- /dev/null
+++ b/backEnd/gnx/management/commands/show_api_key.py
@@ -0,0 +1,34 @@
+"""
+Management command to display the current INTERNAL_API_KEY
+Useful for copying to nginx configuration
+"""
+from django.core.management.base import BaseCommand
+from django.conf import settings
+
+
+class Command(BaseCommand):
+ help = 'Display the current INTERNAL_API_KEY for nginx configuration'
+
+ def handle(self, *args, **options):
+ api_key = getattr(settings, 'INTERNAL_API_KEY', None)
+
+ if not api_key:
+ self.stdout.write(
+ self.style.ERROR('ā INTERNAL_API_KEY is not set!')
+ )
+ self.stdout.write(
+ ' Set it in your .env file or it will be auto-generated in DEBUG mode.'
+ )
+ return
+
+ self.stdout.write(self.style.SUCCESS('\nā Current INTERNAL_API_KEY:'))
+ self.stdout.write(self.style.WARNING(f'\n{api_key}\n'))
+
+ self.stdout.write('š Copy this key to your nginx configuration:')
+ self.stdout.write(' In nginx.conf, set:')
+ self.stdout.write(f' set $api_key "{api_key}";')
+ self.stdout.write('')
+ self.stdout.write(' Or add to your .env file:')
+ self.stdout.write(f' INTERNAL_API_KEY={api_key}')
+ self.stdout.write('')
+
diff --git a/backEnd/gnx/management/commands/update_admin.py b/backEnd/gnx/management/commands/update_admin.py
new file mode 100644
index 00000000..52eb1a4b
--- /dev/null
+++ b/backEnd/gnx/management/commands/update_admin.py
@@ -0,0 +1,99 @@
+"""
+Management command to update admin user credentials
+"""
+from django.core.management.base import BaseCommand
+from django.contrib.auth import get_user_model
+from django.db import transaction
+
+User = get_user_model()
+
+
+class Command(BaseCommand):
+ help = 'Update admin user username and password'
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '--username',
+ type=str,
+ default='gnx',
+ help='Admin username (default: gnx)'
+ )
+ parser.add_argument(
+ '--password',
+ type=str,
+ default='P4eli240453',
+ help='Admin password (default: P4eli240453)'
+ )
+
+ def handle(self, *args, **options):
+ username = options['username']
+ password = options['password']
+
+ try:
+ with transaction.atomic():
+ # Try to find existing user with this username
+ user = User.objects.filter(username=username).first()
+
+ if user:
+ # Update existing user
+ user.set_password(password)
+ user.is_staff = True
+ user.is_superuser = True
+ user.is_active = True
+ user.save()
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'ā Successfully updated admin user "{username}"'
+ )
+ )
+ else:
+ # Check if there are any existing superusers
+ existing_superusers = User.objects.filter(is_superuser=True)
+
+ if existing_superusers.exists():
+ # Update the first superuser
+ user = existing_superusers.first()
+ old_username = user.username
+ user.username = username
+ user.set_password(password)
+ user.is_staff = True
+ user.is_superuser = True
+ user.is_active = True
+ user.save()
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'ā Successfully updated admin user from "{old_username}" to "{username}"'
+ )
+ )
+ else:
+ # Create new superuser
+ user = User.objects.create_user(
+ username=username,
+ password=password,
+ is_staff=True,
+ is_superuser=True,
+ is_active=True
+ )
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'ā Successfully created admin user "{username}"'
+ )
+ )
+
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'\nAdmin credentials:\n'
+ f' Username: {username}\n'
+ f' Password: {password}\n'
+ f' Is Staff: {user.is_staff}\n'
+ f' Is Superuser: {user.is_superuser}\n'
+ f' Is Active: {user.is_active}\n'
+ )
+ )
+
+ except Exception as e:
+ self.stdout.write(
+ self.style.ERROR(f'ā Error updating admin user: {str(e)}')
+ )
+ raise
+
diff --git a/backEnd/gnx/middleware/__pycache__/admin_ip_restriction.cpython-312.pyc b/backEnd/gnx/middleware/__pycache__/admin_ip_restriction.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..bc71406e06dfe3d2acee5560121bbb6df02e7ed7
GIT binary patch
literal 5390
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
literal 0
HcmV?d00001
diff --git a/backEnd/gnx/middleware/admin_ip_restriction.py b/backEnd/gnx/middleware/admin_ip_restriction.py
new file mode 100644
index 00000000..25e0c70a
--- /dev/null
+++ b/backEnd/gnx/middleware/admin_ip_restriction.py
@@ -0,0 +1,132 @@
+"""
+Admin IP Restriction Middleware
+Restricts Django admin access to specific IP addresses only
+"""
+
+from django.http import HttpResponseForbidden
+from django.conf import settings
+from django.urls import resolve
+import ipaddress
+import logging
+
+logger = logging.getLogger('django.security')
+
+
+class AdminIPRestrictionMiddleware:
+ """
+ Restricts Django admin access to whitelisted IP addresses only.
+ This provides an additional layer of security for the admin panel.
+ """
+
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ # Get allowed admin IPs from settings
+ # Default to the user's IP if not specified
+ admin_ips = getattr(settings, 'ADMIN_ALLOWED_IPS', ['193.194.155.249'])
+
+ # Convert to list if it's a string
+ if isinstance(admin_ips, str):
+ admin_ips = [ip.strip() for ip in admin_ips.split(',') if ip.strip()]
+
+ self.allowed_ips = []
+ for ip_str in admin_ips:
+ try:
+ # Support both single IPs and CIDR notation
+ if '/' in ip_str:
+ self.allowed_ips.append(ipaddress.ip_network(ip_str, strict=False))
+ else:
+ self.allowed_ips.append(ipaddress.ip_address(ip_str))
+ except ValueError:
+ logger.warning(f"Invalid IP address in ADMIN_ALLOWED_IPS: {ip_str}")
+
+ # Also allow localhost for development
+ self.allowed_ips.extend([
+ ipaddress.ip_address('127.0.0.1'),
+ ipaddress.ip_address('::1'),
+ ])
+
+ def get_client_ip(self, request):
+ """
+ Get the real client IP address, handling proxy headers
+ """
+ # Check for forwarded IP (from proxy/load balancer)
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ # Get the first IP in the chain (original client)
+ ip = x_forwarded_for.split(',')[0].strip()
+ return ip
+
+ # Check for real IP header (some proxies use this)
+ x_real_ip = request.META.get('HTTP_X_REAL_IP')
+ if x_real_ip:
+ return x_real_ip.strip()
+
+ # Fall back to REMOTE_ADDR
+ return request.META.get('REMOTE_ADDR', '')
+
+ def is_admin_path(self, request):
+ """
+ Check if the request is for the Django admin
+ """
+ path = request.path
+ return path.startswith('/admin/')
+
+ def is_ip_allowed(self, client_ip):
+ """
+ Check if the client IP is in the allowed list
+ """
+ try:
+ client_ip_obj = ipaddress.ip_address(client_ip)
+
+ # Check if IP matches any allowed IP or network
+ for allowed in self.allowed_ips:
+ if isinstance(allowed, ipaddress.IPv4Address) or isinstance(allowed, ipaddress.IPv6Address):
+ # Direct IP match
+ if client_ip_obj == allowed:
+ return True
+ elif isinstance(allowed, ipaddress.IPv4Network) or isinstance(allowed, ipaddress.IPv6Network):
+ # Network/CIDR match
+ if client_ip_obj in allowed:
+ return True
+
+ return False
+ except ValueError:
+ logger.error(f"Invalid IP address format: {client_ip}")
+ return False
+
+ def __call__(self, request):
+ # Only check admin paths
+ if not self.is_admin_path(request):
+ return self.get_response(request)
+
+ # In DEBUG mode, you might want to allow all (optional)
+ # Uncomment the next 3 lines if you want to disable IP restriction in DEBUG mode
+ # if settings.DEBUG:
+ # return self.get_response(request)
+
+ # Get client IP
+ client_ip = self.get_client_ip(request)
+
+ if not client_ip:
+ logger.warning("Could not determine client IP for admin access attempt")
+ return HttpResponseForbidden(
+ "Access Denied "
+ "Unable to verify your IP address. Admin access is restricted.
"
+ )
+
+ # Check if IP is allowed
+ if not self.is_ip_allowed(client_ip):
+ logger.warning(
+ f"Blocked admin access attempt from IP: {client_ip} "
+ f"to path: {request.path}"
+ )
+ return HttpResponseForbidden(
+ "Access Denied "
+ "Admin access is restricted to authorized IP addresses only.
"
+ f"Your IP: {client_ip}
"
+ )
+
+ # IP is allowed, continue
+ return self.get_response(request)
+
diff --git a/backEnd/gnx/settings.py b/backEnd/gnx/settings.py
index 2f97caa8..6dfa1e29 100644
--- a/backEnd/gnx/settings.py
+++ b/backEnd/gnx/settings.py
@@ -12,6 +12,8 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
from pathlib import Path
import os
+import secrets
+import warnings
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -62,6 +64,7 @@ MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'gnx.middleware.ip_whitelist.IPWhitelistMiddleware', # Production: Block external access
+ 'gnx.middleware.admin_ip_restriction.AdminIPRestrictionMiddleware', # Restrict admin to specific IPs
'gnx.middleware.api_security.FrontendAPIProxyMiddleware', # Validate requests from frontend/nginx
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -95,6 +98,16 @@ WSGI_APPLICATION = 'gnx.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
+# Support both PostgreSQL (production) and SQLite (development)
+DATABASE_URL = config('DATABASE_URL', default='')
+if DATABASE_URL and DATABASE_URL.startswith('postgresql://'):
+ # PostgreSQL configuration
+ import dj_database_url
+ DATABASES = {
+ 'default': dj_database_url.parse(DATABASE_URL, conn_max_age=600)
+ }
+else:
+ # SQLite configuration (development/fallback)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
@@ -162,6 +175,10 @@ INTERNAL_IPS = ['127.0.0.1', '::1']
# Custom allowed IPs for IP whitelist middleware (comma-separated)
CUSTOM_ALLOWED_IPS = config('CUSTOM_ALLOWED_IPS', default='', cast=lambda v: [s.strip() for s in v.split(',') if s.strip()])
+# Admin IP Restriction - Only these IPs can access Django admin
+# Comma-separated list of IP addresses or CIDR networks
+ADMIN_ALLOWED_IPS = config('ADMIN_ALLOWED_IPS', default='193.194.155.249', cast=lambda v: [s.strip() for s in v.split(',') if s.strip()])
+
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
@@ -236,8 +253,28 @@ REST_FRAMEWORK = {
# ============================================================================
# Internal API Key - nginx will add this header to prove request came through proxy
-# 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')
+# Auto-generates a secure key if not provided (only in DEBUG mode for development)
+# In production, you MUST set INTERNAL_API_KEY in your .env file
+_manual_api_key = config('INTERNAL_API_KEY', default='')
+if not _manual_api_key:
+ if DEBUG:
+ # Auto-generate a secure key for development
+ _auto_generated_key = secrets.token_urlsafe(32)
+ INTERNAL_API_KEY = _auto_generated_key
+ warnings.warn(
+ f"ā ļø INTERNAL_API_KEY not set. Auto-generated key for development: {_auto_generated_key}\n"
+ f" Add this to your nginx config and .env file for consistency.\n"
+ f" In production, you MUST set INTERNAL_API_KEY explicitly in .env",
+ UserWarning
+ )
+ else:
+ # Production requires explicit key
+ raise ValueError(
+ "INTERNAL_API_KEY must be set in production. "
+ "Generate one with: python -c \"import secrets; print(secrets.token_urlsafe(32))\""
+ )
+else:
+ INTERNAL_API_KEY = _manual_api_key
# API security path pattern (default: all /api/ paths)
API_SECURITY_PATH_PATTERN = config('API_SECURITY_PATH_PATTERN', default=r'^/api/')
diff --git a/backEnd/logs/django.log b/backEnd/logs/django.log
index 88f262e9..ba1f0950 100644
--- a/backEnd/logs/django.log
+++ b/backEnd/logs/django.log
@@ -32877,3 +32877,307 @@ INFO 2025-11-24 06:40:57,132 autoreload 139932 123776047718528 /home/gnx/Desktop
INFO 2025-11-24 06:40:57,712 autoreload 140119 136966227431552 Watching for file changes with StatReloader
INFO 2025-11-24 06:41:38,174 autoreload 140119 136966227431552 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
INFO 2025-11-24 06:41:38,716 autoreload 140239 133780171337856 Watching for file changes with StatReloader
+INFO 2025-11-24 13:09:15,381 autoreload 11176 128073016684672 Watching for file changes with StatReloader
+INFO 2025-11-24 13:10:34,723 basehttp 11176 128072933168832 "OPTIONS /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 0
+INFO 2025-11-24 13:10:34,726 basehttp 11176 128072933168832 "OPTIONS /api/career/jobs HTTP/1.1" 200 0
+INFO 2025-11-24 13:10:34,726 basehttp 11176 128072949954240 "OPTIONS /api/home/banner/ HTTP/1.1" 200 0
+INFO 2025-11-24 13:10:34,725 basehttp 11176 128072941561536 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0
+INFO 2025-11-24 13:10:34,730 basehttp 11176 128072924776128 "OPTIONS /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 0
+INFO 2025-11-24 13:10:34,775 basehttp 11176 128072949954240 "GET /api/home/banner/ HTTP/1.1" 200 3438
+INFO 2025-11-24 13:10:34,779 basehttp 11176 128072916383424 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
+INFO 2025-11-24 13:10:34,792 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:10:34,826 basehttp 11176 128072933168832 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:10:34,863 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:10:34,871 basehttp 11176 128072924776128 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
+INFO 2025-11-24 13:10:34,919 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:04,757 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:04,757 basehttp 11176 128072924776128 "OPTIONS /api/blog/categories/ HTTP/1.1" 200 0
+INFO 2025-11-24 13:11:04,757 basehttp 11176 128072916383424 "OPTIONS /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 0
+INFO 2025-11-24 13:11:04,773 basehttp 11176 128072933168832 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:11:04,796 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:04,803 basehttp 11176 128072924776128 "GET /api/blog/categories/ HTTP/1.1" 200 1418
+INFO 2025-11-24 13:11:04,809 basehttp 11176 128072933168832 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:11:04,821 basehttp 11176 128072941561536 "GET /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 5121
+INFO 2025-11-24 13:11:04,852 basehttp 11176 128072949954240 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:04,859 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:04,875 basehttp 11176 128072924776128 "GET /api/blog/categories/ HTTP/1.1" 200 1418
+INFO 2025-11-24 13:11:04,889 basehttp 11176 128072949954240 "GET /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 5121
+INFO 2025-11-24 13:11:09,343 basehttp 11176 128072569321152 "GET /api/blog/posts/best-practices-for-building-scalable-enterprise-apis/ HTTP/1.1" 200 4445
+INFO 2025-11-24 13:11:09,514 basehttp 11176 128072924776128 "OPTIONS /api/blog/posts/latest/?limit=8 HTTP/1.1" 200 0
+INFO 2025-11-24 13:11:09,514 basehttp 11176 128072949954240 "OPTIONS /api/blog/posts/best-practices-for-building-scalable-enterprise-apis/ HTTP/1.1" 200 0
+INFO 2025-11-24 13:11:09,533 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:09,543 basehttp 11176 128072916383424 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:11:09,592 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:09,592 basehttp 11176 128072949954240 "GET /api/blog/posts/best-practices-for-building-scalable-enterprise-apis/ HTTP/1.1" 200 4445
+INFO 2025-11-24 13:11:09,602 basehttp 11176 128072916383424 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:11:09,611 basehttp 11176 128072924776128 "GET /api/blog/posts/latest/?limit=8 HTTP/1.1" 200 6678
+INFO 2025-11-24 13:11:09,652 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:09,672 basehttp 11176 128072949954240 "GET /api/blog/posts/best-practices-for-building-scalable-enterprise-apis/ HTTP/1.1" 200 4445
+INFO 2025-11-24 13:11:09,684 basehttp 11176 128072916383424 "GET /api/blog/posts/latest/?limit=8 HTTP/1.1" 200 6678
+INFO 2025-11-24 13:11:09,703 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:18,799 basehttp 11176 128072949954240 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:18,811 basehttp 11176 128072916383424 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:11:18,819 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:18,831 basehttp 11176 128072949954240 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:11:18,836 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:11:18,887 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:12:18,200 basehttp 11176 128072916383424 "GET /api/home/banner/ HTTP/1.1" 200 3438
+INFO 2025-11-24 13:12:18,208 basehttp 11176 128072949954240 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:12:18,221 basehttp 11176 128072941561536 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
+INFO 2025-11-24 13:12:18,221 basehttp 11176 128072933168832 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:12:18,251 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:12:18,259 basehttp 11176 128072949954240 "GET /api/home/banner/ HTTP/1.1" 200 3438
+INFO 2025-11-24 13:12:18,267 basehttp 11176 128072933168832 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:12:18,274 basehttp 11176 128072941561536 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
+INFO 2025-11-24 13:12:18,288 basehttp 11176 128072924776128 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
+INFO 2025-11-24 13:12:18,304 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:12:18,334 basehttp 11176 128072924776128 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
+INFO 2025-11-24 13:12:18,365 basehttp 11176 128072949954240 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:12:18,378 basehttp 11176 128072941561536 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:12:18,394 basehttp 11176 128072916383424 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:12:58,727 autoreload 11176 128073016684672 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
+INFO 2025-11-24 13:12:59,333 autoreload 13550 127591779987584 Watching for file changes with StatReloader
+INFO 2025-11-24 13:13:07,973 autoreload 13550 127591779987584 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
+INFO 2025-11-24 13:13:08,571 autoreload 13656 125806206767232 Watching for file changes with StatReloader
+INFO 2025-11-24 13:13:13,119 autoreload 13656 125806206767232 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
+INFO 2025-11-24 13:13:13,738 autoreload 13700 126054542864512 Watching for file changes with StatReloader
+INFO 2025-11-24 13:13:53,121 autoreload 13700 126054542864512 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
+INFO 2025-11-24 13:13:53,738 autoreload 14178 134881899106432 Watching for file changes with StatReloader
+INFO 2025-11-24 13:15:05,124 autoreload 14521 127272466882688 Watching for file changes with StatReloader
+INFO 2025-11-24 13:15:28,205 basehttp 14521 127272391534272 "OPTIONS /api/about/page/ HTTP/1.1" 200 0
+INFO 2025-11-24 13:15:28,216 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:15:28,234 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:15:28,248 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:15:28,263 basehttp 14521 127272374748864 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 13:15:28,270 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:15:28,286 basehttp 14521 127272374748864 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 13:15:28,305 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:15:28,316 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:15:28,373 basehttp 14521 127272391534272 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 13:15:28,465 basehttp 14521 127272399926976 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 13:15:28,466 basehttp 14521 127272391534272 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 13:15:28,485 basehttp 14521 127272399926976 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 13:16:36,991 basehttp 14521 127272374748864 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
+INFO 2025-11-24 13:16:37,011 basehttp 14521 127272399926976 "GET /api/home/banner/ HTTP/1.1" 200 3438
+INFO 2025-11-24 13:16:37,018 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:16:37,022 basehttp 14521 127272027682496 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:16:37,050 basehttp 14521 127272399926976 "GET /api/home/banner/ HTTP/1.1" 200 3438
+INFO 2025-11-24 13:16:37,058 basehttp 14521 127272374748864 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
+INFO 2025-11-24 13:16:37,076 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:16:37,082 basehttp 14521 127272027682496 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:16:37,092 basehttp 14521 127272383141568 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
+INFO 2025-11-24 13:16:37,141 basehttp 14521 127272374748864 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:16:37,150 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:16:37,155 basehttp 14521 127272399926976 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
+INFO 2025-11-24 13:16:37,200 basehttp 14521 127272374748864 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:16:37,209 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:18:05,963 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:18:05,971 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:18:05,983 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:18:05,993 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:18:05,999 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:18:06,050 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:52:29,368 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 13:52:29,375 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:52:29,396 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:58:17,380 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:58:48,661 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:58:52,344 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 13:59:44,445 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:00:29,872 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:00:58,873 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:01:17,080 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:01:37,625 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:01:59,686 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:02:11,931 basehttp 14521 127272383141568 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0
+INFO 2025-11-24 14:02:11,936 basehttp 14521 127272391534272 "OPTIONS /api/career/jobs HTTP/1.1" 200 0
+INFO 2025-11-24 14:02:11,947 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:02:11,955 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:02:12,106 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:02:32,343 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:02:52,316 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:03:46,939 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:03:59,826 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:04:24,979 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:05:10,040 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:06:12,564 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:06:25,176 basehttp 14521 127272383141568 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0
+INFO 2025-11-24 14:06:25,184 basehttp 14521 127272383141568 "OPTIONS /api/career/jobs HTTP/1.1" 200 0
+INFO 2025-11-24 14:06:25,192 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:06:25,205 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:06:25,236 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:06:45,565 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:07:01,911 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:07:01,918 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:07:01,931 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:07:21,699 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:07:27,955 basehttp 14521 127272383141568 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0
+INFO 2025-11-24 14:07:27,965 basehttp 14521 127272391534272 "OPTIONS /api/career/jobs HTTP/1.1" 200 0
+INFO 2025-11-24 14:07:27,965 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:07:27,974 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:07:28,028 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:08:11,406 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:01,128 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:01,138 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:09:01,146 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:16,649 basehttp 14521 127272391534272 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0
+INFO 2025-11-24 14:09:16,656 basehttp 14521 127272383141568 "OPTIONS /api/career/jobs HTTP/1.1" 200 0
+INFO 2025-11-24 14:09:16,664 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:16,669 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:09:16,726 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:34,382 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:09:34,382 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:34,399 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:34,403 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:09:34,460 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:34,469 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:09:34,516 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:34,523 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:09:49,006 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:49,018 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:09:49,034 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:49,040 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:09:49,093 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:09:49,150 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:10:47,794 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:10:48,854 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:10:54,869 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:10:59,880 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:11:14,155 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:11:14,155 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:11:14,169 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:11:16,763 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:11:16,779 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:11:16,786 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:11:22,210 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:11:22,216 basehttp 14521 127272383141568 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:11:22,228 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:11:54,375 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:11:57,923 basehttp 14521 127272383141568 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0
+INFO 2025-11-24 14:11:57,936 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:12:12,466 basehttp 14521 127272399926976 "OPTIONS /api/career/jobs HTTP/1.1" 200 0
+INFO 2025-11-24 14:12:12,474 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:12:12,484 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:12:12,491 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:12:14,995 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:12:51,022 basehttp 14521 127272383141568 "OPTIONS /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 0
+INFO 2025-11-24 14:12:51,025 basehttp 14521 127272374748864 "OPTIONS /api/career/jobs HTTP/1.1" 200 0
+INFO 2025-11-24 14:12:51,037 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:12:51,042 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:12:51,092 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:13:03,068 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:13:03,080 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:13:03,090 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:13:03,100 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:13:03,142 basehttp 14521 127272374748864 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:13:03,158 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:13:03,172 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:13:03,172 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:13:06,231 basehttp 14521 127272383141568 "OPTIONS /api/blog/categories/ HTTP/1.1" 200 0
+INFO 2025-11-24 14:13:06,238 basehttp 14521 127272374748864 "OPTIONS /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 0
+INFO 2025-11-24 14:13:06,262 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:13:06,262 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:13:06,299 basehttp 14521 127272383141568 "GET /api/blog/categories/ HTTP/1.1" 200 1418
+INFO 2025-11-24 14:13:06,311 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:13:06,321 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:13:06,326 basehttp 14521 127272374748864 "GET /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 5121
+INFO 2025-11-24 14:13:06,352 basehttp 14521 127272383141568 "GET /api/blog/categories/ HTTP/1.1" 200 1418
+INFO 2025-11-24 14:13:06,363 basehttp 14521 127272391534272 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:13:06,399 basehttp 14521 127272374748864 "GET /api/blog/posts/?page=1&page_size=6 HTTP/1.1" 200 5121
+INFO 2025-11-24 14:13:06,413 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:13:18,876 basehttp 14521 127272026633920 "GET /admin/ HTTP/1.1" 302 0
+INFO 2025-11-24 14:13:18,898 basehttp 14521 127272026633920 "GET /admin/login/?next=/admin/ HTTP/1.1" 200 4173
+INFO 2025-11-24 14:13:18,993 basehttp 14521 127272026633920 "GET /static/admin/css/base.css HTTP/1.1" 200 22120
+INFO 2025-11-24 14:13:18,995 basehttp 14521 127272000407232 "GET /static/admin/css/nav_sidebar.css HTTP/1.1" 200 2810
+INFO 2025-11-24 14:13:18,996 basehttp 14521 127272017192640 "GET /static/admin/css/dark_mode.css HTTP/1.1" 200 2808
+INFO 2025-11-24 14:13:18,996 basehttp 14521 127272008799936 "GET /static/admin/js/theme.js HTTP/1.1" 200 1653
+INFO 2025-11-24 14:13:18,998 basehttp 14521 127271990965952 "GET /static/admin/js/nav_sidebar.js HTTP/1.1" 200 3063
+INFO 2025-11-24 14:13:18,999 basehttp 14521 127271982573248 "GET /static/admin/css/login.css HTTP/1.1" 200 951
+INFO 2025-11-24 14:13:19,001 basehttp 14521 127272000407232 "GET /static/admin/css/responsive.css HTTP/1.1" 200 16565
+WARNING 2025-11-24 14:13:19,077 log 14521 127272000407232 Not Found: /favicon.ico
+WARNING 2025-11-24 14:13:19,077 basehttp 14521 127272000407232 "GET /favicon.ico HTTP/1.1" 404 3043
+INFO 2025-11-24 14:13:21,374 basehttp 14521 127272026633920 "POST /admin/login/?next=/admin/ HTTP/1.1" 200 4339
+INFO 2025-11-24 14:14:51,936 basehttp 14521 127272026633920 "POST /admin/login/?next=/admin/ HTTP/1.1" 302 0
+INFO 2025-11-24 14:14:51,973 basehttp 14521 127272026633920 "GET /admin/ HTTP/1.1" 200 39055
+INFO 2025-11-24 14:14:52,071 basehttp 14521 127272000407232 "GET /static/admin/css/dashboard.css HTTP/1.1" 200 441
+INFO 2025-11-24 14:14:52,088 basehttp 14521 127272000407232 "GET /static/admin/img/icon-addlink.svg HTTP/1.1" 200 331
+INFO 2025-11-24 14:14:52,088 basehttp 14521 127271990965952 "GET /static/admin/img/icon-changelink.svg HTTP/1.1" 200 380
+INFO 2025-11-24 14:14:52,090 basehttp 14521 127271990965952 "GET /static/admin/img/icon-deletelink.svg HTTP/1.1" 200 392
+INFO 2025-11-24 14:15:16,828 basehttp 14521 127272026633920 "GET /admin/case_studies/client/9/change/ HTTP/1.1" 302 0
+INFO 2025-11-24 14:15:16,866 basehttp 14521 127272026633920 "GET /admin/ HTTP/1.1" 200 39224
+INFO 2025-11-24 14:15:16,962 basehttp 14521 127272000407232 "GET /static/admin/img/icon-alert.svg HTTP/1.1" 200 504
+INFO 2025-11-24 14:15:20,705 basehttp 14521 127272026633920 "GET /admin/about/aboutmilestone/ HTTP/1.1" 200 34312
+INFO 2025-11-24 14:15:20,759 basehttp 14521 127272000407232 "GET /static/admin/css/changelists.css HTTP/1.1" 200 6878
+INFO 2025-11-24 14:15:20,761 basehttp 14521 127272017192640 "GET /static/admin/js/jquery.init.js HTTP/1.1" 200 347
+INFO 2025-11-24 14:15:20,761 basehttp 14521 127272008799936 "GET /static/admin/js/core.js HTTP/1.1" 200 6208
+INFO 2025-11-24 14:15:20,763 basehttp 14521 127271982573248 "GET /static/admin/js/admin/RelatedObjectLookups.js HTTP/1.1" 200 9777
+INFO 2025-11-24 14:15:20,769 basehttp 14521 127272000407232 "GET /static/admin/js/actions.js HTTP/1.1" 200 8076
+INFO 2025-11-24 14:15:20,770 basehttp 14521 127272017192640 "GET /static/admin/js/prepopulate.js HTTP/1.1" 200 1531
+INFO 2025-11-24 14:15:20,772 basehttp 14521 127272008799936 "GET /static/admin/js/urlify.js HTTP/1.1" 200 7887
+INFO 2025-11-24 14:15:20,774 basehttp 14521 127271990965952 "GET /admin/jsi18n/ HTTP/1.1" 200 3342
+INFO 2025-11-24 14:15:20,777 basehttp 14521 127272026633920 "GET /static/admin/js/vendor/jquery/jquery.js HTTP/1.1" 200 285314
+INFO 2025-11-24 14:15:20,778 basehttp 14521 127271990965952 "GET /static/admin/js/filters.js HTTP/1.1" 200 978
+INFO 2025-11-24 14:15:20,780 basehttp 14521 127271982573248 "GET /static/admin/js/vendor/xregexp/xregexp.js HTTP/1.1" 200 325171
+INFO 2025-11-24 14:15:20,859 basehttp 14521 127271982573248 "GET /static/admin/img/tooltag-add.svg HTTP/1.1" 200 331
+INFO 2025-11-24 14:15:20,860 basehttp 14521 127272000407232 "GET /static/admin/img/sorting-icons.svg HTTP/1.1" 200 1097
+INFO 2025-11-24 14:15:23,371 basehttp 14521 127271982573248 "GET /admin/about/aboutmilestone/27/change/ HTTP/1.1" 200 34486
+INFO 2025-11-24 14:15:23,411 basehttp 14521 127271982573248 "GET /static/admin/css/forms.css HTTP/1.1" 200 8525
+INFO 2025-11-24 14:15:23,416 basehttp 14521 127272000407232 "GET /admin/jsi18n/ HTTP/1.1" 200 3342
+INFO 2025-11-24 14:15:23,417 basehttp 14521 127272008799936 "GET /static/admin/js/change_form.js HTTP/1.1" 200 606
+INFO 2025-11-24 14:15:23,418 basehttp 14521 127271990965952 "GET /static/admin/js/prepopulate_init.js HTTP/1.1" 200 586
+INFO 2025-11-24 14:15:23,453 basehttp 14521 127272000407232 "GET /static/admin/img/icon-viewlink.svg HTTP/1.1" 200 581
+INFO 2025-11-24 14:15:23,458 basehttp 14521 127271982573248 "GET /static/admin/css/widgets.css HTTP/1.1" 200 11973
+INFO 2025-11-24 14:15:29,674 basehttp 14521 127271982573248 "GET /admin/about/aboutmilestone/ HTTP/1.1" 200 34312
+INFO 2025-11-24 14:15:33,502 basehttp 14521 127271982573248 "GET /admin/about/aboutmilestone/25/change/ HTTP/1.1" 200 34446
+INFO 2025-11-24 14:15:33,546 basehttp 14521 127271982573248 "GET /admin/jsi18n/ HTTP/1.1" 200 3342
+INFO 2025-11-24 14:15:38,415 basehttp 14521 127271982573248 "GET /admin/about/aboutmilestone/ HTTP/1.1" 200 34312
+INFO 2025-11-24 14:16:01,816 basehttp 14521 127272391534272 "OPTIONS /api/about/page/ HTTP/1.1" 200 0
+INFO 2025-11-24 14:16:01,837 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:16:01,840 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:01,867 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:16:01,879 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:01,879 basehttp 14521 127272374748864 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 14:16:01,917 basehttp 14521 127272391534272 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 14:16:01,929 basehttp 14521 127272374748864 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 14:16:01,939 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:01,957 basehttp 14521 127272391534272 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 14:16:02,019 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:02,026 basehttp 14521 127272399926976 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 14:16:02,034 basehttp 14521 127272374748864 "GET /api/about/page/ HTTP/1.1" 200 8597
+INFO 2025-11-24 14:16:10,126 basehttp 14521 127272399926976 "OPTIONS /api/case-studies/ HTTP/1.1" 200 0
+INFO 2025-11-24 14:16:10,147 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:10,156 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:16:10,160 basehttp 14521 127272374748864 "GET /api/case-studies/ HTTP/1.1" 200 5617
+INFO 2025-11-24 14:16:10,175 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:10,184 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:10,193 basehttp 14521 127272391534272 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:16:10,193 basehttp 14521 127272374748864 "GET /api/case-studies/ HTTP/1.1" 200 5617
+INFO 2025-11-24 14:16:10,232 basehttp 14521 127272399926976 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:15,085 basehttp 14521 127272374748864 "OPTIONS /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 0
+INFO 2025-11-24 14:16:15,085 basehttp 14521 127272383141568 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:15,085 basehttp 14521 127272391534272 "OPTIONS /api/home/banner/ HTTP/1.1" 200 0
+INFO 2025-11-24 14:16:15,085 basehttp 14521 127271972083392 "OPTIONS /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 0
+INFO 2025-11-24 14:16:15,102 basehttp 14521 127272391534272 "GET /api/home/banner/ HTTP/1.1" 200 3438
+INFO 2025-11-24 14:16:15,108 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:16:15,116 basehttp 14521 127271972083392 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:15,121 basehttp 14521 127272383141568 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
+INFO 2025-11-24 14:16:15,153 basehttp 14521 127272399926976 "GET /api/career/jobs HTTP/1.1" 200 4675
+INFO 2025-11-24 14:16:15,156 basehttp 14521 127272374748864 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
+INFO 2025-11-24 14:16:15,174 basehttp 14521 127272391534272 "GET /api/home/banner/ HTTP/1.1" 200 3438
+INFO 2025-11-24 14:16:15,185 basehttp 14521 127271972083392 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:15,194 basehttp 14521 127272383141568 "GET /api/case-studies/?ordering=display_order&page_size=5 HTTP/1.1" 200 4744
+INFO 2025-11-24 14:16:15,201 basehttp 14521 127271490811584 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:15,276 basehttp 14521 127271490811584 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:15,294 basehttp 14521 127271972083392 "GET /api/services/?ordering=display_order&page=1 HTTP/1.1" 200 10722
+INFO 2025-11-24 14:16:15,306 basehttp 14521 127272399926976 "GET /api/blog/posts/latest/?limit=12 HTTP/1.1" 200 8374
+INFO 2025-11-24 14:17:22,716 autoreload 14521 127272466882688 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
+INFO 2025-11-24 14:17:23,283 autoreload 40629 126035405783168 Watching for file changes with StatReloader
+INFO 2025-11-24 14:17:24,688 autoreload 40629 126035405783168 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
+INFO 2025-11-24 14:17:25,211 autoreload 40650 134584020750464 Watching for file changes with StatReloader
+INFO 2025-11-24 14:17:52,231 autoreload 40650 134584020750464 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
+INFO 2025-11-24 14:17:52,734 autoreload 41027 131304750628992 Watching for file changes with StatReloader
+INFO 2025-11-24 14:17:55,154 autoreload 41027 131304750628992 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/middleware/admin_ip_restriction.py changed, reloading.
+INFO 2025-11-24 14:17:55,632 autoreload 41073 132612486312064 Watching for file changes with StatReloader
+INFO 2025-11-24 14:26:08,072 autoreload 41073 132612486312064 /home/gnx/Desktop/GNX-WEB/backEnd/gnx/settings.py changed, reloading.
+INFO 2025-11-24 14:26:08,639 autoreload 44508 130053635125376 Watching for file changes with StatReloader
+INFO 2025-11-24 14:34:01,218 autoreload 47585 126321009672320 Watching for file changes with StatReloader
diff --git a/backEnd/nginx.conf.example b/backEnd/nginx.conf.example
index 29df60de..cb40142d 100644
--- a/backEnd/nginx.conf.example
+++ b/backEnd/nginx.conf.example
@@ -2,9 +2,12 @@
# 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
+# API Key Configuration:
+# - In DEBUG mode, Django will auto-generate a secure API key if not set
+# - To get the current API key, run: python manage.py show_api_key
+# - Add the key to your Django .env file as INTERNAL_API_KEY
+# - Use the same key in this nginx config (see line 69)
+# - In production, you MUST set INTERNAL_API_KEY explicitly in .env
upstream django_backend {
# Django backend running on internal network only
@@ -66,6 +69,8 @@ server {
# Add custom header to prove request came through nginx
# This value must match INTERNAL_API_KEY in Django settings
+ # Get the current key with: python manage.py show_api_key
+ # In development, Django auto-generates this key if not set
set $api_key "YOUR_SECURE_API_KEY_HERE";
proxy_set_header X-Internal-API-Key $api_key;
@@ -123,6 +128,7 @@ server {
# deny all;
# Same proxy settings as /api/
+ # Use the same API key as /api/ location above
set $api_key "YOUR_SECURE_API_KEY_HERE";
proxy_set_header X-Internal-API-Key $api_key;
proxy_set_header Host $host;
diff --git a/backEnd/production.env.example b/backEnd/production.env.example
index 53e2b216..8b693a4b 100644
--- a/backEnd/production.env.example
+++ b/backEnd/production.env.example
@@ -44,9 +44,15 @@ CORS_ALLOW_CREDENTIALS=True
CSRF_TRUSTED_ORIGINS=https://gnxsoft.com,https://www.gnxsoft.com
# API Security - Internal API Key (nginx will add this header)
+# REQUIRED in production! Auto-generated only in DEBUG mode.
# Generate a secure key: python -c "import secrets; print(secrets.token_urlsafe(32))"
+# Or get current key: python manage.py show_api_key
INTERNAL_API_KEY=your-secure-api-key-here-change-this-in-production
+# Admin IP Restriction - Only these IPs can access Django admin
+# Comma-separated list of IP addresses or CIDR networks (e.g., 193.194.155.249 or 192.168.1.0/24)
+ADMIN_ALLOWED_IPS=193.194.155.249
+
# Static Files
STATIC_ROOT=/var/www/gnx/staticfiles/
MEDIA_ROOT=/var/www/gnx/media/
diff --git a/backEnd/requirements.txt b/backEnd/requirements.txt
new file mode 100644
index 00000000..1e0e31d5
--- /dev/null
+++ b/backEnd/requirements.txt
@@ -0,0 +1,17 @@
+asgiref==3.11.0
+Django==5.2.8
+django-cors-headers==4.9.0
+django-filter==25.2
+djangorestframework==3.16.1
+drf-yasg==1.21.11
+gunicorn==21.2.0
+inflection==0.5.1
+packaging==25.0
+pillow==12.0.0
+psycopg2-binary==2.9.9
+dj-database-url==2.1.0
+python-decouple==3.8
+pytz==2025.2
+PyYAML==6.0.3
+sqlparse==0.5.3
+uritemplate==4.2.0
diff --git a/create-deployment-zip.sh b/create-deployment-zip.sh
new file mode 100644
index 00000000..255fcfc5
--- /dev/null
+++ b/create-deployment-zip.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+# Script to create a production deployment zip file
+
+set -e
+
+ZIP_NAME="gnx-web-production-$(date +%Y%m%d).zip"
+TEMP_DIR=$(mktemp -d)
+
+echo "š¦ Creating deployment package: $ZIP_NAME"
+echo ""
+
+# Copy files to temp directory
+echo "š Copying files..."
+rsync -av --progress \
+ --exclude='.git' \
+ --exclude='node_modules' \
+ --exclude='__pycache__' \
+ --exclude='*.pyc' \
+ --exclude='venv' \
+ --exclude='env' \
+ --exclude='.venv' \
+ --exclude='*.log' \
+ --exclude='*.sqlite3' \
+ --exclude='backups' \
+ --exclude='*.swp' \
+ --exclude='*.swo' \
+ --exclude='.DS_Store' \
+ --exclude='.vscode' \
+ --exclude='.idea' \
+ --exclude='.next' \
+ --exclude='dist' \
+ --exclude='build' \
+ --exclude='*.egg-info' \
+ --exclude='.dockerignore' \
+ --exclude='.zipignore' \
+ ./ "$TEMP_DIR/gnx-web/"
+
+# Create zip
+echo ""
+echo "šļø Creating zip file..."
+cd "$TEMP_DIR"
+zip -r "$ZIP_NAME" gnx-web/ > /dev/null
+
+# Move to original directory
+mv "$ZIP_NAME" "$OLDPWD/"
+
+# Cleanup
+cd "$OLDPWD"
+rm -rf "$TEMP_DIR"
+
+echo "ā
Deployment package created: $ZIP_NAME"
+echo ""
+echo "š File size: $(du -h "$ZIP_NAME" | cut -f1)"
+echo ""
+echo "š¤ Ready to upload to server!"
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..7a41bb9b
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,98 @@
+version: '3.8'
+
+services:
+ postgres:
+ image: postgres:16-alpine
+ container_name: gnx-postgres
+ restart: unless-stopped
+ environment:
+ - POSTGRES_DB=${POSTGRES_DB:-gnxdb}
+ - POSTGRES_USER=${POSTGRES_USER:-gnx}
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-change-this-password}
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ networks:
+ - gnx-network
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-gnx}"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ backend:
+ build:
+ context: ./backEnd
+ dockerfile: Dockerfile
+ container_name: gnx-backend
+ restart: unless-stopped
+ ports:
+ - "1086:1086"
+ env_file:
+ - .env.production
+ environment:
+ - DEBUG=False
+ - SECRET_KEY=${SECRET_KEY:-change-this-in-production}
+ - ALLOWED_HOSTS=${ALLOWED_HOSTS:-localhost,127.0.0.1,backend}
+ - DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-gnx}:${POSTGRES_PASSWORD:-change-this-password}@postgres:5432/${POSTGRES_DB:-gnxdb}}
+ - ADMIN_ALLOWED_IPS=${ADMIN_ALLOWED_IPS:-193.194.155.249}
+ - INTERNAL_API_KEY=${INTERNAL_API_KEY}
+ - EMAIL_BACKEND=${EMAIL_BACKEND:-django.core.mail.backends.console.EmailBackend}
+ - EMAIL_HOST=${EMAIL_HOST}
+ - EMAIL_PORT=${EMAIL_PORT:-587}
+ - EMAIL_USE_TLS=${EMAIL_USE_TLS:-True}
+ - EMAIL_HOST_USER=${EMAIL_HOST_USER}
+ - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD}
+ - DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL:-noreply@gnxsoft.com}
+ - COMPANY_EMAIL=${COMPANY_EMAIL:-contact@gnxsoft.com}
+ volumes:
+ - ./backEnd/media:/app/media
+ - ./backEnd/staticfiles:/app/staticfiles
+ - ./backEnd/logs:/app/logs
+ depends_on:
+ postgres:
+ condition: service_healthy
+ networks:
+ - gnx-network
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:1086/admin/"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+
+ frontend:
+ build:
+ context: ./frontEnd
+ dockerfile: Dockerfile
+ container_name: gnx-frontend
+ restart: unless-stopped
+ ports:
+ - "1087:1087"
+ env_file:
+ - .env.production
+ environment:
+ - NODE_ENV=production
+ - DOCKER_ENV=true
+ - NEXT_PUBLIC_API_URL=http://backend:1086
+ - PORT=1087
+ depends_on:
+ - backend
+ networks:
+ - gnx-network
+ healthcheck:
+ test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:1087/"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+
+networks:
+ gnx-network:
+ driver: bridge
+
+volumes:
+ postgres_data:
+ driver: local
+ media:
+ staticfiles:
+
diff --git a/docker-start.sh b/docker-start.sh
new file mode 100755
index 00000000..8caedeeb
--- /dev/null
+++ b/docker-start.sh
@@ -0,0 +1,240 @@
+#!/bin/bash
+# Docker startup script for GNX Web Application
+# This script handles automatic setup, permissions, and startup
+
+set -e
+
+echo "š Starting GNX Web Application..."
+echo ""
+
+# Set proper permissions for scripts and directories
+echo "š§ Setting up permissions..."
+
+# Make scripts executable
+chmod +x docker-start.sh 2>/dev/null || true
+chmod +x migrate-data.sh 2>/dev/null || true
+chmod +x migrate-sqlite-to-postgres.sh 2>/dev/null || true
+
+# Set permissions for directories
+mkdir -p backEnd/media backEnd/staticfiles backEnd/logs backups
+chmod 755 backEnd/media backEnd/staticfiles backEnd/logs backups 2>/dev/null || true
+
+# Set permissions for database file if it exists
+if [ -f "backEnd/db.sqlite3" ]; then
+ chmod 644 backEnd/db.sqlite3 2>/dev/null || true
+fi
+
+# Set permissions for .env files
+if [ -f ".env.production" ]; then
+ chmod 600 .env.production 2>/dev/null || true
+fi
+
+echo "ā
Permissions set"
+echo ""
+
+# Check if .env.production exists
+if [ ! -f .env.production ]; then
+ echo "ā ļø Warning: .env.production not found. Creating from example..."
+ if [ -f .env.production.example ]; then
+ cp .env.production.example .env.production
+ echo "š Please edit .env.production with your actual values before continuing."
+ exit 1
+ else
+ echo "ā Error: .env.production.example not found!"
+ exit 1
+ fi
+fi
+
+# Load environment variables
+export $(cat .env.production | grep -v '^#' | xargs)
+
+# Configure Nginx
+echo "š§ Configuring Nginx..."
+
+# Check for existing nginx configs for gnxsoft
+NGINX_AVAILABLE="/etc/nginx/sites-available/gnxsoft"
+NGINX_ENABLED="/etc/nginx/sites-enabled/gnxsoft"
+NGINX_CONF="nginx.conf"
+
+# Check if nginx.conf exists
+if [ ! -f "$NGINX_CONF" ]; then
+ echo "ā Error: nginx.conf not found in current directory!"
+ exit 1
+fi
+
+# Backup and remove old configs if they exist
+if [ -f "$NGINX_AVAILABLE" ]; then
+ echo "š¦ Backing up existing nginx config..."
+ sudo cp "$NGINX_AVAILABLE" "${NGINX_AVAILABLE}.backup.$(date +%Y%m%d_%H%M%S)"
+ echo "ā
Old config backed up"
+fi
+
+if [ -L "$NGINX_ENABLED" ]; then
+ echo "š Removing old symlink..."
+ sudo rm -f "$NGINX_ENABLED"
+fi
+
+# Check for other gnxsoft configs and remove them
+for file in /etc/nginx/sites-available/gnxsoft* /etc/nginx/sites-enabled/gnxsoft*; do
+ if [ -f "$file" ] || [ -L "$file" ]; then
+ if [ "$file" != "$NGINX_AVAILABLE" ] && [ "$file" != "$NGINX_ENABLED" ]; then
+ echo "šļø Removing old config: $file"
+ sudo rm -f "$file"
+ fi
+ fi
+done
+
+# Copy new nginx config
+echo "š Installing new nginx configuration..."
+sudo cp "$NGINX_CONF" "$NGINX_AVAILABLE"
+
+# Create symlink
+echo "š Creating symlink..."
+sudo ln -sf "$NGINX_AVAILABLE" "$NGINX_ENABLED"
+
+# Update paths in nginx config if needed (using current directory)
+CURRENT_DIR=$(pwd)
+echo "š Updating paths in nginx config..."
+sudo sed -i "s|/home/gnx/Desktop/GNX-WEB|$CURRENT_DIR|g" "$NGINX_AVAILABLE"
+
+# Generate or get INTERNAL_API_KEY
+if [ -z "$INTERNAL_API_KEY" ] || [ "$INTERNAL_API_KEY" = "your-generated-key-here" ]; then
+ echo "š Generating new INTERNAL_API_KEY..."
+ INTERNAL_API_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))" 2>/dev/null || openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
+
+ # Update .env.production with the generated key
+ if [ -f .env.production ]; then
+ if grep -q "INTERNAL_API_KEY=" .env.production; then
+ sed -i "s|INTERNAL_API_KEY=.*|INTERNAL_API_KEY=$INTERNAL_API_KEY|" .env.production
+ else
+ echo "INTERNAL_API_KEY=$INTERNAL_API_KEY" >> .env.production
+ fi
+ echo "ā
Updated .env.production with generated INTERNAL_API_KEY"
+ fi
+
+ # Export for use in this script
+ export INTERNAL_API_KEY
+fi
+
+# Set INTERNAL_API_KEY in nginx config
+echo "š Setting INTERNAL_API_KEY in nginx config..."
+sudo sed -i "s|PLACEHOLDER_INTERNAL_API_KEY|$INTERNAL_API_KEY|g" "$NGINX_AVAILABLE"
+echo "ā
INTERNAL_API_KEY configured in nginx"
+
+# Test nginx configuration
+echo "š§Ŗ Testing nginx configuration..."
+if sudo nginx -t; then
+ echo "ā
Nginx configuration is valid"
+ echo "š Reloading nginx..."
+ sudo systemctl reload nginx
+ echo "ā
Nginx reloaded successfully"
+else
+ echo "ā Nginx configuration test failed!"
+ echo "ā ļø Please check the configuration manually"
+ exit 1
+fi
+
+# Build images
+echo "šØ Building Docker images..."
+docker-compose build
+
+# Start containers
+echo "ā¶ļø Starting containers..."
+docker-compose up -d
+
+# Wait for services to be ready
+echo "ā³ Waiting for services to start..."
+sleep 10
+
+# Wait for PostgreSQL to be ready (if using PostgreSQL)
+if echo "$DATABASE_URL" | grep -q "postgresql://"; then
+ echo "ā³ Waiting for PostgreSQL to be ready..."
+ timeout=30
+ while [ $timeout -gt 0 ]; do
+ if docker-compose exec -T postgres pg_isready -U ${POSTGRES_USER:-gnx} > /dev/null 2>&1; then
+ echo "ā
PostgreSQL is ready"
+ break
+ fi
+ echo " Waiting for PostgreSQL... ($timeout seconds remaining)"
+ sleep 2
+ timeout=$((timeout - 2))
+ done
+ if [ $timeout -le 0 ]; then
+ echo "ā ļø Warning: PostgreSQL may not be ready, but continuing..."
+ fi
+
+ # Check if we need to migrate from SQLite
+ if [ -f "./backEnd/db.sqlite3" ] && [ ! -f ".migrated_to_postgres" ]; then
+ echo ""
+ echo "š SQLite database detected. Checking if migration is needed..."
+
+ # Check if PostgreSQL database is empty (only has default tables)
+ POSTGRES_TABLES=$(docker-compose exec -T backend python manage.py shell -c "
+from django.db import connection
+cursor = connection.cursor()
+cursor.execute(\"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name NOT LIKE 'django_%'\")
+print(cursor.fetchone()[0])
+" 2>/dev/null | tail -1 || echo "0")
+
+ # Check if SQLite has data
+ SQLITE_HAS_DATA=$(docker-compose exec -T backend bash -c "
+export DATABASE_URL=sqlite:///db.sqlite3
+python manage.py shell -c \"
+from django.contrib.auth.models import User
+from django.db import connection
+cursor = connection.cursor()
+cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name NOT LIKE \"sqlite_%\" AND name NOT LIKE \"django_%\"')
+tables = cursor.fetchall()
+has_data = False
+for table in tables:
+ cursor.execute(f'SELECT COUNT(*) FROM {table[0]}')
+ if cursor.fetchone()[0] > 0:
+ has_data = True
+ break
+print('1' if has_data else '0')
+\" 2>/dev/null
+" | tail -1 || echo "0")
+
+ if [ "$SQLITE_HAS_DATA" = "1" ] && [ "$POSTGRES_TABLES" = "0" ] || [ "$POSTGRES_TABLES" -lt 5 ]; then
+ echo "š¦ SQLite database has data. Starting migration to PostgreSQL..."
+ echo " This may take a few minutes..."
+ echo ""
+
+ # Run migration script
+ if [ -f "./migrate-sqlite-to-postgres.sh" ]; then
+ ./migrate-sqlite-to-postgres.sh
+ else
+ echo "ā ļø Migration script not found. Please run manually:"
+ echo " ./migrate-sqlite-to-postgres.sh"
+ fi
+ else
+ echo "ā
No migration needed (PostgreSQL already has data or SQLite is empty)"
+ touch .migrated_to_postgres
+ fi
+ fi
+fi
+
+# Run migrations
+echo "š¦ Running database migrations..."
+docker-compose exec -T backend python manage.py migrate --noinput
+
+# Collect static files
+echo "š Collecting static files..."
+docker-compose exec -T backend python manage.py collectstatic --noinput
+
+# Check health
+echo "š„ Checking service health..."
+docker-compose ps
+
+echo ""
+echo "ā
GNX Web Application is running!"
+echo ""
+echo "Backend: http://localhost:1086"
+echo "Frontend: http://localhost:1087"
+echo "Nginx: Configured and running"
+echo ""
+echo "View logs: docker-compose logs -f"
+echo "Stop services: docker-compose down"
+echo ""
+echo "š Nginx config location: $NGINX_AVAILABLE"
+
diff --git a/frontEnd/.dockerignore b/frontEnd/.dockerignore
new file mode 100644
index 00000000..d7763bd9
--- /dev/null
+++ b/frontEnd/.dockerignore
@@ -0,0 +1,26 @@
+node_modules
+.next
+.git
+.gitignore
+*.log
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.DS_Store
+.vscode
+.idea
+*.swp
+*.swo
+*~
+coverage
+.nyc_output
+dist
+build
+README.md
+*.md
+
diff --git a/frontEnd/Dockerfile b/frontEnd/Dockerfile
new file mode 100644
index 00000000..4b5a3777
--- /dev/null
+++ b/frontEnd/Dockerfile
@@ -0,0 +1,50 @@
+# Next.js Frontend Dockerfile
+FROM node:20-alpine AS base
+
+# Install dependencies only when needed
+FROM base AS deps
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+RUN npm ci
+
+# Rebuild the source code only when needed
+FROM base AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+
+# Set environment variables for build
+ENV NEXT_TELEMETRY_DISABLED=1
+ENV NODE_ENV=production
+
+# Build Next.js
+RUN npm run build
+
+# Production image, copy all the files and run next
+FROM base AS runner
+WORKDIR /app
+
+ENV NODE_ENV=production
+ENV NEXT_TELEMETRY_DISABLED=1
+
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+# Copy necessary files from builder
+COPY --from=builder /app/public ./public
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+USER nextjs
+
+EXPOSE 1087
+
+ENV PORT=1087
+ENV HOSTNAME="0.0.0.0"
+
+# Use the standalone server
+CMD ["node", "server.js"]
+
diff --git a/frontEnd/app/career/[slug]/page.tsx b/frontEnd/app/career/[slug]/page.tsx
index c21d0c29..81c072fe 100644
--- a/frontEnd/app/career/[slug]/page.tsx
+++ b/frontEnd/app/career/[slug]/page.tsx
@@ -2,6 +2,7 @@
import { useParams } from "next/navigation";
import { useEffect } from "react";
+import Link from "next/link";
import Header from "@/components/shared/layout/header/Header";
import JobSingle from "@/components/pages/career/JobSingle";
import Footer from "@/components/shared/layout/footer/Footer";
@@ -78,9 +79,9 @@ const JobPage = () => {
The job position you are looking for does not exist or is no longer available.
-
+
View All Positions
-
+
diff --git a/frontEnd/app/policy/page.tsx b/frontEnd/app/policy/page.tsx
index 4fbb2552..b9b1e587 100644
--- a/frontEnd/app/policy/page.tsx
+++ b/frontEnd/app/policy/page.tsx
@@ -41,7 +41,8 @@ const PolicyContent = () => {
url: `/policy?type=${type}`,
});
- document.title = metadata.title || `${policyTitles[type]} | GNX Soft`;
+ const titleString = typeof metadata.title === 'string' ? metadata.title : `${policyTitles[type]} | GNX Soft`;
+ document.title = titleString;
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
@@ -49,7 +50,8 @@ const PolicyContent = () => {
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
}
- metaDescription.setAttribute('content', metadata.description || policyDescriptions[type]);
+ const descriptionString = typeof metadata.description === 'string' ? metadata.description : policyDescriptions[type];
+ metaDescription.setAttribute('content', descriptionString);
}, [type]);
if (isLoading) {
@@ -235,7 +237,7 @@ const PolicyContent = () => {
Questions?
-
If you have any questions about this policy, please don't hesitate to contact us.
+
If you have any questions about this policy, please don't hesitate to contact us.
Contact Us
diff --git a/frontEnd/components/pages/blog/BlogSingle.tsx b/frontEnd/components/pages/blog/BlogSingle.tsx
index a61fb0b9..93427ca1 100644
--- a/frontEnd/components/pages/blog/BlogSingle.tsx
+++ b/frontEnd/components/pages/blog/BlogSingle.tsx
@@ -41,7 +41,7 @@ const BlogSingle = () => {
Insight Not Found
- The insight you're looking for doesn't exist or has been removed.
+ The insight you're looking for doesn't exist or has been removed.
diff --git a/frontEnd/components/pages/career/JobSingle.tsx b/frontEnd/components/pages/career/JobSingle.tsx
index 481e860e..d2acc330 100644
--- a/frontEnd/components/pages/career/JobSingle.tsx
+++ b/frontEnd/components/pages/career/JobSingle.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect } from "react";
+import Link from "next/link";
import { JobPosition } from "@/lib/api/careerService";
import JobApplicationForm from "./JobApplicationForm";
@@ -529,7 +530,7 @@ const JobSingle = ({ job }: JobSingleProps) => {
~5 min
- {
arrow_back
Back to Career Page
Back
-
+
diff --git a/frontEnd/components/pages/case-study/CaseSingle.tsx b/frontEnd/components/pages/case-study/CaseSingle.tsx
index 3617e1d6..2a7c1fde 100644
--- a/frontEnd/components/pages/case-study/CaseSingle.tsx
+++ b/frontEnd/components/pages/case-study/CaseSingle.tsx
@@ -67,7 +67,7 @@ const CaseSingle = ({ slug }: CaseSingleProps) => {
Case Study Not Found
-
The case study you're looking for doesn't exist or has been removed.
+
The case study you're looking for doesn't exist or has been removed.
View All Case Studies
diff --git a/frontEnd/components/pages/support/CreateTicketForm.tsx b/frontEnd/components/pages/support/CreateTicketForm.tsx
index 1d3cbb5f..3db47dc9 100644
--- a/frontEnd/components/pages/support/CreateTicketForm.tsx
+++ b/frontEnd/components/pages/support/CreateTicketForm.tsx
@@ -203,7 +203,7 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
- We've received your support request and will respond as soon as possible.
+ We've received your support request and will respond as soon as possible.
Please save your ticket number for future reference.
diff --git a/frontEnd/components/pages/support/KnowledgeBase.tsx b/frontEnd/components/pages/support/KnowledgeBase.tsx
index b01d613e..4198db21 100644
--- a/frontEnd/components/pages/support/KnowledgeBase.tsx
+++ b/frontEnd/components/pages/support/KnowledgeBase.tsx
@@ -236,7 +236,7 @@ const KnowledgeBase = () => {
{searchTerm && (
- Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"
+ Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"
)}
diff --git a/frontEnd/components/shared/layout/footer/Footer.tsx b/frontEnd/components/shared/layout/footer/Footer.tsx
index c02444c5..3db7dfea 100644
--- a/frontEnd/components/shared/layout/footer/Footer.tsx
+++ b/frontEnd/components/shared/layout/footer/Footer.tsx
@@ -153,11 +153,56 @@ const Footer = () => {
Ready to Transform Your Business?
Start your software journey with our enterprise solutions, incident management, and custom development services.
+
Start Your Journey
+
+
+
+
+
+
diff --git a/frontEnd/components/shared/layout/header/Header.tsx b/frontEnd/components/shared/layout/header/Header.tsx
index 1c705e84..a79e0895 100644
--- a/frontEnd/components/shared/layout/header/Header.tsx
+++ b/frontEnd/components/shared/layout/header/Header.tsx
@@ -1,5 +1,5 @@
"use client";
-import { useState, useEffect, useMemo } from "react";
+import { useState, useEffect, useMemo, useRef } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
@@ -12,6 +12,7 @@ const Header = () => {
const [isActive, setIsActive] = useState(true);
const [openDropdown, setOpenDropdown] = useState(null);
const [isMobile, setIsMobile] = useState(false);
+ const dropdownTimeoutRef = useRef(null);
// Fetch services from API
const { services: apiServices, loading: servicesLoading, error: servicesError } = useNavigationServices();
@@ -112,6 +113,36 @@ const Header = () => {
setOpenDropdown(openDropdown === index ? null : index);
};
+ const handleDropdownEnter = (index: number) => {
+ if (dropdownTimeoutRef.current) {
+ clearTimeout(dropdownTimeoutRef.current);
+ dropdownTimeoutRef.current = null;
+ }
+ if (!isMobile) {
+ setOpenDropdown(index);
+ }
+ };
+
+ const handleDropdownLeave = (e: React.MouseEvent) => {
+ if (!isMobile) {
+ // Check if we're moving to the dropdown menu itself
+ const relatedTarget = e.relatedTarget as HTMLElement;
+ const currentTarget = e.currentTarget as HTMLElement;
+
+ // If moving to a child element (dropdown menu), don't close
+ if (relatedTarget && currentTarget.contains(relatedTarget)) {
+ return;
+ }
+
+ if (dropdownTimeoutRef.current) {
+ clearTimeout(dropdownTimeoutRef.current);
+ }
+ dropdownTimeoutRef.current = setTimeout(() => {
+ setOpenDropdown(null);
+ }, 300); // Increased delay to allow for scrolling
+ }
+ };
+
useEffect(() => {
const handleScroll = () => {
const scrollPosition = window.scrollY;
@@ -145,6 +176,9 @@ const Header = () => {
return () => {
window.removeEventListener("resize", handleResize);
+ if (dropdownTimeoutRef.current) {
+ clearTimeout(dropdownTimeoutRef.current);
+ }
};
}, []);
@@ -200,12 +234,12 @@ const Header = () => {
{navigationData.map((item) =>
- item.title === "Support Center" ? null : item.submenu ? (
+ item.title === "Support Center" ? null : item.submenu ? (
!isMobile && setOpenDropdown(item.id)}
- onMouseLeave={() => !isMobile && setOpenDropdown(null)}
+ onMouseEnter={() => handleDropdownEnter(item.id)}
+ onMouseLeave={(e) => handleDropdownLeave(e)}
>
{
ā³
)}
-