diff --git a/infra/ansible/roles/nginx/templates/pw-portal-tls.conf.j2 b/infra/ansible/roles/nginx/templates/pw-portal-tls.conf.j2 index 510f9ec..67c285e 100644 --- a/infra/ansible/roles/nginx/templates/pw-portal-tls.conf.j2 +++ b/infra/ansible/roles/nginx/templates/pw-portal-tls.conf.j2 @@ -17,14 +17,51 @@ server { ssl_certificate_key /etc/letsencrypt/live/{{ portal_domain }}/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + add_header Strict-Transport-Security "max-age=31536000" always; client_max_body_size 50m; + # Inject PW navy branding CSS for the portal navbar + location = /portal-branding.css { + default_type text/css; + return 200 ' + .navbar { background-color: #1e3a5f !important; border-bottom: 2px solid #f59e0b !important; } + .navbar .nav-link, .navbar .navbar-brand, .navbar a { color: #ffffff !important; } + .navbar .nav-link:hover { color: #f59e0b !important; } + .navbar .navbar-toggler-icon { filter: invert(1); } + .btn-primary-dark { background-color: #1e3a5f !important; border-color: #1e3a5f !important; } + .btn-primary-dark:hover { background-color: #2d4f7a !important; } + .navbar .nav-link[href*="/order/"] { display: none !important; } + .btn.btn-primary-sm[href*="payment_request"], .btn[onclick*="payment_request"], a[href*="make_payment_request"] { display: none !important; } + .page-header-actions-block .btn-primary-dark { display: none !important; } + '; + } + + # Site-uploaded public /files/ (portal logo, etc). + # IMPORTANT: served from a stable, www-data-owned host path that is re-synced + # by extract-erpnext-assets.sh. Do NOT point this at /var/lib/docker/volumes/... + # directly: /var/lib/docker is root-only (0700) and docker resets that perm on + # restart, which makes nginx 403 on /files/ and breaks the portal logo. + location /files/ { + alias /opt/erpnext-assets/assets/files/; + expires 7d; + add_header Cache-Control "public"; + access_log off; + } + + # ERPNext static assets from the extracted host copy. + # Re-extracted by deploy.sh / extract-erpnext-assets.sh after every + # bench migrate/build so the content-hashed bundles never drift. + location /assets/ { + alias /opt/erpnext-assets/assets/; + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Proxy everything else to ERPNext gunicorn location / { proxy_pass http://127.0.0.1:{{ erpnext_port }}; proxy_http_version 1.1; @@ -34,14 +71,16 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host {{ portal_domain }}; - # WebSocket support (ERPNext real-time) - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; proxy_read_timeout 120s; proxy_buffering off; - } - location /.well-known/acme-challenge/ { - root {{ certbot_webroot }}; + # Inject PW branding into HTML responses (logo + copy + branding CSS) + sub_filter 'Login to Frappe' 'Performance West Portal'; + sub_filter 'Create a Frappe Account' 'Create an Account'; + sub_filter 'Powered by Frappe' 'Performance West'; + sub_filter '/assets/erpnext/images/erpnext-logo.svg' '/files/pw-logo.png'; + sub_filter '' ''; + sub_filter_once off; + sub_filter_types text/html; } }