{ pkgs, ... }: { services.nginx = { enable = true; # ── Frontend ──────────────────────────────────────────────────────────── virtualHosts."learnlytics-frontend" = { listen = [ { addr = "0.0.0.0"; port = 80; } ]; root = "/var/www/learnlytics"; locations."/" = { tryFiles = "$uri $uri/ /index.html"; }; }; # ── API + avatars + upload ─────────────────────────────────────────────── virtualHosts."learnlytics-api" = { listen = [ { addr = "0.0.0.0"; port = 3002; } ]; # PostgREST proxy (default) locations."/" = { proxyPass = "http://127.0.0.1:3001"; extraConfig = '' add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Prefer, Accept, Range' always; add_header 'Access-Control-Expose-Headers' 'Content-Range, Content-Location' always; if ($request_method = OPTIONS) { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, PUT, DELETE, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Prefer, Accept, Range'; add_header 'Content-Length' '0'; add_header 'Content-Type' 'text/plain'; return 204; } proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_intercept_errors off; client_max_body_size 1m; ''; }; # Serve avatar images directly from disk locations."/avatars/" = { alias = "/var/lib/learnlytics/avatars/"; extraConfig = '' expires 7d; add_header Cache-Control "public, immutable"; add_header 'Access-Control-Allow-Origin' '*'; ''; }; # Proxy avatar uploads to the Python upload server on port 3003 locations."/upload/" = { proxyPass = "http://127.0.0.1:3003"; extraConfig = '' proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; client_max_body_size 3m; add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always; if ($request_method = OPTIONS) { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type'; add_header 'Content-Length' '0'; add_header 'Content-Type' 'text/plain'; return 204; } ''; }; }; }; # Avatar storage directory systemd.tmpfiles.rules = [ "d /var/www/learnlytics 0755 nginx nginx -" "d /var/lib/learnlytics 0755 postgres postgres -" "d /var/lib/learnlytics/avatars 0755 postgres postgres -" ]; networking.firewall.allowedTCPPorts = [ 3000 3001 3002 80 ]; }