From b204c1d133c5f1b145cf05e6e40efe9a804587f8 Mon Sep 17 00:00:00 2001 From: DerGrumpf Date: Wed, 25 Feb 2026 11:54:11 +0100 Subject: [PATCH] Added: Routes for learnlytics --- hosts/cyper-pi-1/configuration.nix | 2 + hosts/cyper-pi-1/learnlytics.nix | 19 +++++ hosts/cyper-pi-1/nginx.nix | 108 +++++++++++++++++++++++++++++ hosts/cyper-pi-1/postgres.nix | 64 ++++++----------- 4 files changed, 150 insertions(+), 43 deletions(-) create mode 100644 hosts/cyper-pi-1/learnlytics.nix create mode 100644 hosts/cyper-pi-1/nginx.nix diff --git a/hosts/cyper-pi-1/configuration.nix b/hosts/cyper-pi-1/configuration.nix index e56adc2..3c156de 100644 --- a/hosts/cyper-pi-1/configuration.nix +++ b/hosts/cyper-pi-1/configuration.nix @@ -8,6 +8,8 @@ ./postgres.nix ./postgrest.nix ./swagger.nix + ./nginx.nix + ./learnlytics.nix # ./k3s-master.nix ]; diff --git a/hosts/cyper-pi-1/learnlytics.nix b/hosts/cyper-pi-1/learnlytics.nix new file mode 100644 index 0000000..b400aa2 --- /dev/null +++ b/hosts/cyper-pi-1/learnlytics.nix @@ -0,0 +1,19 @@ +{ pkgs, ... }: +{ + systemd.services.learnlytics-avatar = { + description = "Learnlytics Avatar Upload Server"; + wantedBy = [ "multi-user.target" ]; + after = [ + "network.target" + "postgrest.service" + ]; + serviceConfig = { + ExecStart = "${pkgs.python3}/bin/python3 + /etc/learnlytics/avatar_server.py"; + WorkingDirectory = "/etc/learnlytics"; + Restart = "on-failure"; + User = "postgres"; # or whatever user runs postgrest + StateDirectory = "learnlytics"; + }; + }; +} diff --git a/hosts/cyper-pi-1/nginx.nix b/hosts/cyper-pi-1/nginx.nix new file mode 100644 index 0000000..41fd307 --- /dev/null +++ b/hosts/cyper-pi-1/nginx.nix @@ -0,0 +1,108 @@ +{ + 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 + ]; +} diff --git a/hosts/cyper-pi-1/postgres.nix b/hosts/cyper-pi-1/postgres.nix index b77edaa..4020c62 100644 --- a/hosts/cyper-pi-1/postgres.nix +++ b/hosts/cyper-pi-1/postgres.nix @@ -2,84 +2,62 @@ pkgs, ... }: - { services.postgresql = { enable = true; package = pkgs.postgresql_15; enableTCPIP = true; - # Initial database setup - initialScript = pkgs.writeText "backend-init-script" '' - CREATE USER postgres WITH SUPERUSER PASSWORD 'postgres'; + extraPlugins = with pkgs.postgresql15Packages; [ + pgjwt + ]; - -- Create web_anon role for PostgREST - CREATE ROLE web_anon NOLOGIN; - GRANT USAGE ON SCHEMA public TO web_anon; - GRANT SELECT ON ALL TABLES IN SCHEMA public TO web_anon; + initialScript = /etc/learnlytics/init.sql; - -- Create example users table - CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - name VARCHAR(255), - email VARCHAR(255), - created_at TIMESTAMP DEFAULT NOW() - ); - - -- Grant permissions - GRANT SELECT, INSERT, UPDATE, DELETE ON users TO web_anon; - GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO web_anon; - ''; - - # Raspberry Pi 4 optimized settings (2GB RAM assumed) settings = { - # Should match firewall port = 5432; - - # Memory settings (RPi 4 has limited RAM) shared_buffers = "128MB"; effective_cache_size = "512MB"; maintenance_work_mem = "32MB"; work_mem = "2MB"; wal_buffers = "4MB"; - - # Connection settings max_connections = 20; - - # Performance tuning for ARM/RPi random_page_cost = 2.0; effective_io_concurrency = 100; - - # WAL settings (conservative for SD card) wal_level = "replica"; checkpoint_timeout = "15min"; checkpoint_completion_target = 0.7; min_wal_size = "1GB"; max_wal_size = "4GB"; - - # Query planning default_statistics_target = 50; - - # Logging log_min_duration_statement = 1000; log_duration = false; - - # ARM/RPi specific cpu_index_tuple_cost = 0.1; cpu_operator_cost = 0.05; }; authentication = '' - local all all trust - host all all 127.0.0.1/32 md5 - host all all ::1/128 md5 - host all all 192.168.2.0/24 md5 + local all all trust + host all all 127.0.0.1/32 md5 + host all all ::1/128 md5 + host all all 192.168.2.0/24 md5 ''; }; - # Enable the PostgreSQL service to start on boot systemd.services.postgresql.wantedBy = [ "multi-user.target" ]; - # Open firewall port for PostgreSQL + systemd.services.learnlytics-auth-migration = { + description = "Learnlytics auth schema migration"; + after = [ "postgresql.service" ]; + wants = [ "postgresql.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = "postgres"; + ExecStart = "${pkgs.postgresql_15}/bin/psql -U postgres -f /etc/learnlytics/auth-migration.sql"; + }; + }; + networking.firewall.allowedTCPPorts = [ 5432 ]; }