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 zcmZp8VAb%zYJxOl#6%fq#)ypx%i>x582J4*3l_NW*Ei}j@-j5qI&zBh>T_~B@)jgz zr=%Ih7iT0InVPAX8GwL_XL3?#Qb<{rZ*rlbNlBuce@SvxWrj*>cvx|nkymkYfS*aF zxtDWNim7RGnzw7PQId&whJInBL9$0~Kyq$`YqqVCfsv`Mp@FWUv4Wwcm63^+p}C%+ zxfzFnu~}kDZf4%(tNDg3-x>J7Zx(#;mY-J5mOhH^6oP1jt_;dMgZs#*#YUG>TV4%*T!tBd9eU~be z!uA_>OgfCBoP4hs_#g2f=3m0!%AdyX&ac7G$+unc1Ir}d%?$?jjMGocF>!7G;>RR( zk;R9B)n{{|ffK9PR6{OFhSJhS)=iAOro6n3wT*(DoE+N|RhfOc+Sltd12GE_vjQ<2 b5VHd@2M}`tF-V>ph6#@1A?^xp(p)^xbgD*DjZX!QX%5^}_Q#cSIF>>QzZwutgAH`B^ezzyte!K+2ww(eyvHE&fcL?g`|J|A zmwM$!>Wy#BPc}D)^5w*2pRx0Dz8or$qddNSr8o6L<%6{bZyK!v;VVT4RDh2bL)A<% z!@#;ZR!5;4YA`Jh*3%M&g<6q79qRRSELt|;tQs~7BxdH<8tHBVe?c{T(7;z2md!@| z7K|r!U0z+P(}uZ`mN=P$&>92Zx&r6G#;?b(76N=7Y~$+9Rd8yJ&{-vb@6w*cP;`}pn9$v5Ew z&G@yzetrkmHmKK@-w6l!U8o(@wKU%fUHoqR+Mt_nheP}x=z-oC3!xZ+!*B$9qwg-E zC9)ZgLVt`ESvVR)T`|J|45CiZ;}{IVFoa=b&J4%r?0koj*olJf*-Q5-yO5_5ZRYru zrLNG=fB6Ta%T2_0(oTLqI$Gk_maVTxI7vHy#sL3w6FsopoP-e_=zkE#mN~8wy4T>a z!6|qvuZ_Em$CH?B^bQ^F4i+rY_n1^tGQM@uehJ73lX`;a1OMB%~+7s7aC%8PL zz01iE29Mz|_<5`+--nEVj7M=yQ=IgpaloKI`$h*38fXkA^YTAtU_t>lWMFYRl;<~$ z#kSBv#RTX0u$bFk0;cjj!Yq@hj+C zlZKvprSpb<0WRVS>DO=9m;qmc>AYl48Q2V5E=cAr1HJMFdK%saKU{@0%;Nj-&eC`d z=hxu|_WQfW%krKf--pZa0em=TO~cJR<41)?u)@di3H)Hr1h??JjW^;t+(E)0;#{Zo zgNFEz4DIHUma|!xv>&7WCv#RlGHq)3Y1EHTzoRQ26=M>oP*oMg`OB7ZmT>cbww)rR zSld%{vvq$=ycC!c021_KRHlI_ObKE{NJeC;sIoAj3X&KI>R@Pa=ruI|tD>*i{aI&M zFdRG(9PW;c4E1H11|r+A9cNrgNl|0Sa6Zw#ZJU7!cur`~IwvGij7&u@MWQjPU39jQ zM(s=I=cI9d)HTYIFSIY+!%=HfK zrnVh{rky(j%{$t3_tU?`b(KLBvrI;mo{Fi*Vk&cC`WtcPUko!wLTpBeXRfCIjgJsH z#4hHBm-^7lX2woE9UFf{Qaw2 z92e>jcMtUk`y#=?P~=GWNluhhz19IPm{O&!F)C6ys#4(2(P{3iBy&J7(1etnqN3W) z&0KeI92W%O)JZ|%REe9GQZgsT1o0Awbvi4=xTpwRfQlEmvqCyS4Gy#@f}&D!VmhD` zghVbWOOP_os!`|Qj*bk6^*mN4NG~N5!i1nEra3`0m}f4p*u_*zQMqwyFre2GdsuG; zu4idaoFr22qM%N;bIECSQWCkX6I`t@m6T+?1QRk9Euv1RYl)^)(!CpMAhhc zRH6FbvX*32R;bomwUKPsPFK|yx9H37(8W&eqbf>TwZ*E#q)qEsx4p7mr*pnK0qk$N zL-*{|X4m~Oops|?5mV7bB$^Z=*hE=ZK@GEIktjA=JcD)?S(^^WW}# zTIPRT=D%0<>w{0qdKXM=sr8-Ef|Ds){cKJ1Gf&m0S1@MLV)8f^OqNpXY-qlR3lQI; zo$>lIzCdPo2(SJzd=CD}Fy6y#hAGuj>wl9D6Q-;(Q@Q2g(391>XWh?jYo6Pj-&VJL z8hcvZ^0>O?;el_eTQhdwGo-LrJhOX0u-|fh=z3NX&WucE!omW>#>p{)gKLN^SQ%Hv z%OXpWV{sMZu3TV=>kRqMWO6$e7*mmB!NF8)%9uTiR%TsYrf%Qwik{5#wf(I5_gJ<6 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}bWswxepQPO&#K=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
+
+ + GoodFirms Company Profile + +
+
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 = () => {